feat(web): IDE responsive (panneau unique mobile) + défaut adapté à l'écran
IdeShell dégrade sous le breakpoint md en un panneau unique (Explorateur/Éditeur/Terminal/Git) commuté par une barre d'onglets basse, avec un bouton retour vers le dashboard. La redirection / envoie le mobile vers /dashboard (supervision + nav) et le desktop vers /ide. Garde-fou : pas de '<' dans les commentaires de template Vue (rolldown casse le build alors que vue-tsc passe). 431 tests.
This commit is contained in:
@@ -1,5 +1,34 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex h-dvh min-h-0 flex-col bg-surface-0 text-fg">
|
<div class="flex h-dvh min-h-0 flex-col bg-surface-0 text-fg">
|
||||||
|
<div v-if="isMobile" class="flex min-h-0 flex-1 flex-col">
|
||||||
|
<header class="flex items-center gap-2 border-b border-border px-2 py-1.5">
|
||||||
|
<RouterLink :to="{ name: 'dashboard' }" class="rounded p-1 text-fg-muted hover:bg-surface-2 hover:text-fg" :title="t('nav.worktrees')">
|
||||||
|
<Home :size="18" />
|
||||||
|
</RouterLink>
|
||||||
|
<span class="truncate text-sm font-medium">{{ mobileTitle }}</span>
|
||||||
|
</header>
|
||||||
|
<div class="min-h-0 flex-1 overflow-hidden">
|
||||||
|
<ProjectTree v-if="ide.mobilePanel === 'explorer'" />
|
||||||
|
<GitPanel v-else-if="ide.mobilePanel === 'git'" />
|
||||||
|
<TerminalDock v-else-if="ide.mobilePanel === 'terminal'" />
|
||||||
|
<EditorArea v-else />
|
||||||
|
</div>
|
||||||
|
<nav class="flex shrink-0 border-t border-border">
|
||||||
|
<button
|
||||||
|
v-for="p in mobilePanels"
|
||||||
|
:key="p.key"
|
||||||
|
type="button"
|
||||||
|
class="flex flex-1 flex-col items-center gap-0.5 py-1.5 text-[10px] transition-colors"
|
||||||
|
:class="ide.mobilePanel === p.key ? 'text-accent' : 'text-fg-subtle'"
|
||||||
|
@click="setMobilePanel(p.key)"
|
||||||
|
>
|
||||||
|
<component :is="p.icon" :size="18" />
|
||||||
|
{{ p.label }}
|
||||||
|
</button>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else class="flex min-h-0 flex-1 flex-col">
|
||||||
<div class="flex min-h-0 flex-1">
|
<div class="flex min-h-0 flex-1">
|
||||||
<ActivityBar />
|
<ActivityBar />
|
||||||
<template v-if="ide.leftVisible">
|
<template v-if="ide.leftVisible">
|
||||||
@@ -8,12 +37,10 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<div class="flex min-w-0 flex-1 flex-col">
|
<div class="flex min-w-0 flex-1 flex-col">
|
||||||
<!-- centre : éditeur Monaco à onglets + diff (B4) -->
|
|
||||||
<main class="min-h-0 flex-1 overflow-hidden">
|
<main class="min-h-0 flex-1 overflow-hidden">
|
||||||
<EditorArea />
|
<EditorArea />
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<!-- dock terminaux (B5) -->
|
|
||||||
<template v-if="ide.bottomVisible">
|
<template v-if="ide.bottomVisible">
|
||||||
<PanelSplitter v-model="ide.bottomHeight" axis="y" :min="120" :max="640" invert />
|
<PanelSplitter v-model="ide.bottomHeight" axis="y" :min="120" :max="640" invert />
|
||||||
<section class="shrink-0 overflow-hidden border-t border-border" :style="{ height: `${ide.bottomHeight}px` }">
|
<section class="shrink-0 overflow-hidden border-t border-border" :style="{ height: `${ide.bottomHeight}px` }">
|
||||||
@@ -25,14 +52,17 @@
|
|||||||
|
|
||||||
<StatusBar />
|
<StatusBar />
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
// Coquille de la vue IDE (layout 'ide', plein écran). Assemble barre d'activité, panneau gauche,
|
// Coquille de la vue IDE (layout 'ide', plein écran). Assemble barre d'activité, panneau gauche,
|
||||||
// zone centrale (éditeur), dock terminaux et barre de statut. Propriétaire de la réconciliation
|
// zone centrale (éditeur), dock terminaux et barre de statut. Propriétaire de la réconciliation
|
||||||
// des ressources ouvertes contre les données live (worktrees / sessions).
|
// des ressources ouvertes contre les données live (worktrees / sessions).
|
||||||
import { onMounted, watch } from 'vue';
|
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue';
|
||||||
import { useRoute } from 'vue-router';
|
import { useRoute } from 'vue-router';
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
|
import { FileCode, FolderTree, GitCompare, Home, SquareTerminal } from '@lucide/vue';
|
||||||
import { decodeWtKey } from '@arboretum/shared';
|
import { decodeWtKey } from '@arboretum/shared';
|
||||||
import { useIdeStore, wtKey } from '../../stores/ide';
|
import { useIdeStore, wtKey } from '../../stores/ide';
|
||||||
import { useWorktreesStore } from '../../stores/worktrees';
|
import { useWorktreesStore } from '../../stores/worktrees';
|
||||||
@@ -43,12 +73,39 @@ import PanelSplitter from './PanelSplitter.vue';
|
|||||||
import StatusBar from './StatusBar.vue';
|
import StatusBar from './StatusBar.vue';
|
||||||
import EditorArea from './EditorArea.vue';
|
import EditorArea from './EditorArea.vue';
|
||||||
import TerminalDock from './TerminalDock.vue';
|
import TerminalDock from './TerminalDock.vue';
|
||||||
|
import ProjectTree from './ProjectTree.vue';
|
||||||
|
import GitPanel from './GitPanel.vue';
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
const ide = useIdeStore();
|
const ide = useIdeStore();
|
||||||
const worktrees = useWorktreesStore();
|
const worktrees = useWorktreesStore();
|
||||||
const sessions = useSessionsStore();
|
const sessions = useSessionsStore();
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
|
|
||||||
|
// --- responsive : panneau unique sous le breakpoint md (768px) ---
|
||||||
|
const isMobile = ref(false);
|
||||||
|
let mql: MediaQueryList | null = null;
|
||||||
|
const syncMobile = (): void => {
|
||||||
|
isMobile.value = !!mql?.matches;
|
||||||
|
};
|
||||||
|
onMounted(() => {
|
||||||
|
mql = window.matchMedia('(max-width: 767px)');
|
||||||
|
syncMobile();
|
||||||
|
mql.addEventListener('change', syncMobile);
|
||||||
|
});
|
||||||
|
onBeforeUnmount(() => mql?.removeEventListener('change', syncMobile));
|
||||||
|
|
||||||
|
const mobilePanels = computed(() => [
|
||||||
|
{ key: 'explorer', icon: FolderTree, label: t('ide.activity.explorer') },
|
||||||
|
{ key: 'editor', icon: FileCode, label: t('workspace.editor') },
|
||||||
|
{ key: 'terminal', icon: SquareTerminal, label: t('ide.terminals') },
|
||||||
|
{ key: 'git', icon: GitCompare, label: t('ide.activity.git') },
|
||||||
|
]);
|
||||||
|
const mobileTitle = computed(() => mobilePanels.value.find((p) => p.key === ide.mobilePanel)?.label ?? '');
|
||||||
|
const setMobilePanel = (key: string): void => {
|
||||||
|
ide.mobilePanel = key;
|
||||||
|
};
|
||||||
|
|
||||||
// Deep-link : /workspace/:repoId/:wt (ex. lien de l'extension VS Code) cible un worktree précis.
|
// Deep-link : /workspace/:repoId/:wt (ex. lien de l'extension VS Code) cible un worktree précis.
|
||||||
// On le rend actif, on déplie son sous-arbre, et on ouvre éventuellement ?file=<relPath>.
|
// On le rend actif, on déplie son sous-arbre, et on ouvre éventuellement ?file=<relPath>.
|
||||||
function applyDeepLink(): void {
|
function applyDeepLink(): void {
|
||||||
|
|||||||
@@ -14,7 +14,9 @@ export const router = createRouter({
|
|||||||
history: createWebHistory(),
|
history: createWebHistory(),
|
||||||
routes: [
|
routes: [
|
||||||
{ path: '/login', name: 'login', component: () => import('../views/LoginView.vue'), meta: { layout: 'bare' } },
|
{ path: '/login', name: 'login', component: () => import('../views/LoginView.vue'), meta: { layout: 'bare' } },
|
||||||
{ path: '/', redirect: { name: 'ide' } },
|
// Desktop : l'IDE est la vue par défaut. Mobile : supervision (dashboard + nav mobile),
|
||||||
|
// l'IDE reste accessible via la nav mais dégrade en panneau unique.
|
||||||
|
{ path: '/', redirect: () => (typeof window !== 'undefined' && window.innerWidth < 768 ? { name: 'dashboard' } : { name: 'ide' }) },
|
||||||
{ path: '/dashboard', name: 'dashboard', component: () => import('../views/DashboardView.vue'), meta: { layout: 'shell' } },
|
{ path: '/dashboard', name: 'dashboard', component: () => import('../views/DashboardView.vue'), meta: { layout: 'shell' } },
|
||||||
{ path: '/sessions', name: 'sessions', component: () => import('../views/SessionsListView.vue'), meta: { layout: 'shell' } },
|
{ path: '/sessions', name: 'sessions', component: () => import('../views/SessionsListView.vue'), meta: { layout: 'shell' } },
|
||||||
{ path: '/sessions/:id', name: 'session', component: () => import('../views/SessionView.vue'), meta: { layout: 'fullbleed' } },
|
{ path: '/sessions/:id', name: 'session', component: () => import('../views/SessionView.vue'), meta: { layout: 'fullbleed' } },
|
||||||
|
|||||||
Reference in New Issue
Block a user