feat(web): coquille IDE + arbre de projets unifié (route /ide)
components/ide/ : IdeShell (grille barre d'activité + panneau gauche + zone centrale + dock + barre de statut, splitters persistés, réconciliation des ressources ouvertes contre les données live), ActivityBar (Explorer/Git/Sessions/Groupes + Réglages/Aide), PrimarySidebar, ProjectTree + ProjectTreeNode (arbre unique repos -> worktrees -> sessions, fichiers embarqués via FileTree en mode sans chrome, dépliage paresseux), StatusBar. Route /ide (layout 'ide'), entrée de nav « IDE », namespace i18n ide (EN/FR). Clics de l'arbre pilotent le store (contexte worktree, ouverture de fichier/terminal). Centre et dock en placeholders (remplis en B4/B5). FileTree gagne un mode embedded. 427 tests.
This commit is contained in:
100
packages/web/src/components/ide/ProjectTreeNode.vue
Normal file
100
packages/web/src/components/ide/ProjectTreeNode.vue
Normal file
@@ -0,0 +1,100 @@
|
||||
<template>
|
||||
<div>
|
||||
<!-- ligne repo (niveau projet) -->
|
||||
<button
|
||||
type="button"
|
||||
class="flex w-full items-center gap-1 rounded px-2 py-1 text-left text-xs text-fg hover:bg-surface-2/60"
|
||||
@click="ide.toggleRepo(repo.id)"
|
||||
>
|
||||
<component :is="repoExpanded ? ChevronDown : ChevronRight" :size="14" class="shrink-0 text-fg-subtle" />
|
||||
<FolderGit2 :size="13" class="shrink-0 text-fg-subtle" />
|
||||
<span class="truncate font-medium">{{ repo.label }}</span>
|
||||
<span v-if="worktreeList.length" class="ml-auto shrink-0 text-[10px] text-fg-subtle">{{ worktreeList.length }}</span>
|
||||
</button>
|
||||
|
||||
<div v-if="repoExpanded" class="pl-3">
|
||||
<p v-if="worktreeList.length === 0" class="px-3 py-0.5 text-[11px] text-fg-subtle">{{ t('ide.noWorktrees') }}</p>
|
||||
|
||||
<div v-for="wt in worktreeList" :key="wt.path">
|
||||
<!-- ligne worktree -->
|
||||
<button
|
||||
type="button"
|
||||
class="flex w-full items-center gap-1 rounded px-2 py-1 text-left text-xs hover:bg-surface-2/60"
|
||||
:class="isActiveWt(wt) ? 'bg-surface-2 text-fg' : 'text-fg-muted'"
|
||||
@click="onWtClick(wt)"
|
||||
>
|
||||
<component :is="isWtExpanded(wt) ? ChevronDown : ChevronRight" :size="14" class="shrink-0 text-fg-subtle" />
|
||||
<component :is="wt.isMain ? Home : GitBranch" :size="12" class="shrink-0 text-fg-subtle" />
|
||||
<span class="truncate font-mono">{{ wt.branch ?? wt.head.slice(0, 7) }}</span>
|
||||
<span v-if="isDirty(wt)" class="ml-auto shrink-0 text-amber-400" :title="t('worktrees.dirty', { n: wt.git.dirtyCount })">●</span>
|
||||
</button>
|
||||
|
||||
<!-- sessions live corrélées au worktree (par cwd) -->
|
||||
<button
|
||||
v-for="s in sessionsFor(wt)"
|
||||
:key="s.id"
|
||||
type="button"
|
||||
class="flex w-full items-center gap-1.5 rounded py-0.5 pr-2 pl-9 text-left text-[11px] hover:bg-surface-2/60"
|
||||
:class="ide.activeDockSessionId === s.id ? 'bg-surface-2 text-fg' : 'text-fg-muted'"
|
||||
@click="ide.openTerminal(s.id)"
|
||||
>
|
||||
<SessionStateBadge :session="s" />
|
||||
<span class="truncate font-mono">{{ s.command }}</span>
|
||||
</button>
|
||||
|
||||
<!-- fichiers du worktree (arbre embarqué, chargé paresseusement au dépliage) -->
|
||||
<div v-if="isWtExpanded(wt)" class="pl-4">
|
||||
<FileTree
|
||||
:wt="wt.path"
|
||||
:active="activeFileFor(wt)"
|
||||
embedded
|
||||
:depth="0"
|
||||
@open="(rel) => ide.openFile(wt.repoId, wt.path, rel)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { ChevronDown, ChevronRight, FolderGit2, GitBranch, Home } from '@lucide/vue';
|
||||
import type { RepoSummary, SessionSummary, WorktreeSummary } from '@arboretum/shared';
|
||||
import { useIdeStore } from '../../stores/ide';
|
||||
import { useWorktreesStore } from '../../stores/worktrees';
|
||||
import { useSessionsStore } from '../../stores/sessions';
|
||||
import FileTree from '../workspace/FileTree.vue';
|
||||
import SessionStateBadge from '../SessionStateBadge.vue';
|
||||
|
||||
const props = defineProps<{ repo: RepoSummary }>();
|
||||
const { t } = useI18n();
|
||||
const ide = useIdeStore();
|
||||
const worktrees = useWorktreesStore();
|
||||
const sessions = useSessionsStore();
|
||||
|
||||
const repoExpanded = computed(() => ide.expandedRepoIds.includes(props.repo.id));
|
||||
const worktreeList = computed(() => worktrees.worktreesForRepo(props.repo.id));
|
||||
|
||||
const isWtExpanded = (wt: WorktreeSummary): boolean => ide.expandedWtPaths.includes(wt.path);
|
||||
const isActiveWt = (wt: WorktreeSummary): boolean =>
|
||||
ide.activeContext?.repoId === wt.repoId && ide.activeContext?.wtPath === wt.path;
|
||||
const isDirty = (wt: WorktreeSummary): boolean => wt.git.dirtyCount > 0;
|
||||
|
||||
// sessions vivantes dont le cwd correspond au worktree (corrélation par répertoire).
|
||||
function sessionsFor(wt: WorktreeSummary): SessionSummary[] {
|
||||
return sessions.sessions.filter((s) => s.live && s.cwd === wt.path);
|
||||
}
|
||||
|
||||
// fichier actif si l'onglet éditeur courant appartient à ce worktree.
|
||||
function activeFileFor(wt: WorktreeSummary): string | null {
|
||||
const tab = ide.activeTab;
|
||||
return tab && tab.repoId === wt.repoId && tab.wtPath === wt.path ? tab.file : null;
|
||||
}
|
||||
|
||||
function onWtClick(wt: WorktreeSummary): void {
|
||||
ide.setActiveWorktree(wt.repoId, wt.path);
|
||||
ide.toggleWt(wt.path);
|
||||
}
|
||||
</script>
|
||||
Reference in New Issue
Block a user