feat(web): zone éditeur à onglets (Monaco multi-modèles + diff)
CI / Build & test (Node 24) (push) Has been cancelled
CI / Pack & boot smoke (Node 22) (push) Has been cancelled
CI / No em/en dashes (push) Has been cancelled
CI / Build & test (Node 22) (push) Has been cancelled

components/ide/EditorArea.vue : UNE instance Monaco partagée avec cache de modèles par onglet (préserve contenu, undo et curseur au changement d'onglet ; view states restaurés), thème arboretum-dark, save + conflit STALE_FILE + reload/overwrite, dispose des modèles à la fermeture d'onglet et au démontage. Onglets diff via DiffViewer.

components/ide/EditorTabs.vue : bandeau d'onglets multi-projets, point de modification, fermeture immédiate si propre et garde à deux clics si modifié. Bascule éditeur/diff par onglet. Branché dans IdeShell (centre). 427 tests.
This commit is contained in:
2026-07-17 17:12:15 +02:00
parent c96d929c7a
commit f62404234b
3 changed files with 313 additions and 3 deletions
@@ -0,0 +1,56 @@
<template>
<div class="flex h-[var(--ide-tab-h)] shrink-0 items-stretch overflow-x-auto border-b border-border bg-surface-1">
<div
v-for="tab in ide.editorTabs"
:key="tab.id"
class="group flex cursor-pointer items-center gap-1.5 border-r border-border px-3 text-xs select-none"
:class="tab.id === ide.activeTabId ? 'bg-surface-0 text-fg' : 'text-fg-muted hover:bg-surface-2/50'"
:title="tab.file"
@click="ide.setActiveTab(tab.id)"
@mousedown.middle.prevent="requestClose(tab.id)"
>
<FileCode :size="12" class="shrink-0 text-fg-subtle" />
<span class="truncate font-mono">{{ basename(tab.file) }}</span>
<span v-if="tab.view === 'diff'" class="text-[9px] tracking-wide text-fg-subtle uppercase">diff</span>
<button
type="button"
class="ml-0.5 flex h-4 w-4 shrink-0 items-center justify-center rounded hover:bg-surface-3"
:class="closeButtonClass(tab.id)"
:title="pendingCloseId === tab.id ? t('editor.unsaved') : t('common.close')"
@click.stop="requestClose(tab.id)"
>
<component :is="pendingCloseId === tab.id ? AlertTriangle : ide.isDirty(tab.id) ? Dot : X" :size="pendingCloseId === tab.id ? 12 : 14" />
</button>
</div>
</div>
</template>
<script setup lang="ts">
// Bandeau d'onglets de l'éditeur. Fermeture immédiate si propre ; garde à deux clics si modifié
// (cohérent avec les autres confirmations de l'app), pour ne pas perdre des changements non sauvés.
import { ref } from 'vue';
import { useI18n } from 'vue-i18n';
import { AlertTriangle, Dot, FileCode, X } from '@lucide/vue';
import { useIdeStore } from '../../stores/ide';
const { t } = useI18n();
const ide = useIdeStore();
const pendingCloseId = ref<string | null>(null);
const basename = (path: string): string => path.split('/').pop() ?? path;
function closeButtonClass(id: string): string {
if (pendingCloseId.value === id) return 'text-amber-400 opacity-100';
// dirty : point visible en permanence ; propre : croix visible au survol de l'onglet.
return ide.isDirty(id) ? 'text-amber-400 opacity-100 group-hover:text-fg' : 'opacity-0 group-hover:opacity-100';
}
function requestClose(id: string): void {
if (ide.isDirty(id) && pendingCloseId.value !== id) {
pendingCloseId.value = id; // premier clic sur un onglet modifié : demande confirmation
return;
}
pendingCloseId.value = null;
ide.closeTab(id);
}
</script>