diff --git a/web/src/lib/components/layout/TopBar.svelte b/web/src/lib/components/layout/TopBar.svelte index 2cbf7193..fe509378 100644 --- a/web/src/lib/components/layout/TopBar.svelte +++ b/web/src/lib/components/layout/TopBar.svelte @@ -8,6 +8,7 @@ import type { Workspace } from '$lib/types'; import { onMount } from 'svelte'; import PadLogo from '$lib/components/layout/PadLogo.svelte'; + import WorkspaceSwitcher from '$lib/components/layout/WorkspaceSwitcher.svelte'; let { mobile = false }: { mobile?: boolean } = $props(); @@ -15,19 +16,17 @@ let currentTheme = $state<'dark' | 'light'>('dark'); let currentSlug = $derived(workspaceStore.current?.slug ?? ''); - let currentUsername = $derived(workspaceStore.current?.owner_username ?? ''); - // DnD state — local copy of workspaces for reordering + // DnD state — local copy of workspaces for reordering (desktop only; + // mobile uses WorkspaceSwitcher which doesn't expose reorder — users + // who want to reorder can do it on desktop). let dndWorkspaces: Workspace[] = $state([]); let isDragging = $state(false); const flipDurationMs = 150; - // Mobile edit mode - let mobileEditMode = $state(false); - // Sync from store when not actively reordering $effect(() => { - if (!isDragging && !mobileEditMode) { + if (!isDragging) { dndWorkspaces = [...workspaceStore.workspaces]; } }); @@ -44,16 +43,6 @@ await saveOrder(); } - // Mobile edit mode: svelte-dnd-action handlers (touch drag works fine - // when items are buttons, not links) - function handleMobileConsider(e: CustomEvent>) { - dndWorkspaces = e.detail.items; - } - - async function handleMobileFinalize(e: CustomEvent>) { - dndWorkspaces = e.detail.items; - } - async function saveOrder() { const updates = dndWorkspaces.map((ws, i) => ({ slug: ws.slug, @@ -67,16 +56,6 @@ } } - function enterEditMode() { - mobileEditMode = true; - dndWorkspaces = [...workspaceStore.workspaces]; - } - - async function exitEditMode() { - mobileEditMode = false; - await saveOrder(); - } - // Color palette for workspace circles const colors = [ '#4a9eff', '#4ade80', '#a78bfa', '#fbbf24', @@ -229,47 +208,25 @@ {:else} +
-
- {#each workspaceStore.workspaces as ws (ws.id)} - uiStore.onNavigate()} - > - - {wsInitial(ws.name)} - - {ws.name} - - {/each} +
+ +
- - {#if workspaceStore.workspaces.length > 1} - - {/if} {#if authStore.user}
- - - {#if mobileEditMode} -
-
-

Reorder Workspaces

- -
- -
- {#each dndWorkspaces as ws (ws.id)} -
- - - {wsInitial(ws.name)} - - {ws.name} -
- {/each} -
-
- {/if} {/if} diff --git a/web/src/lib/components/layout/WorkspaceSwitcher.svelte b/web/src/lib/components/layout/WorkspaceSwitcher.svelte index 3a2f95fd..8c1a1727 100644 --- a/web/src/lib/components/layout/WorkspaceSwitcher.svelte +++ b/web/src/lib/components/layout/WorkspaceSwitcher.svelte @@ -4,19 +4,35 @@ import { uiStore } from '$lib/stores/ui.svelte'; import BottomSheet from '$lib/components/common/BottomSheet.svelte'; + interface Props { + /** + * Force the mobile (BottomSheet) branch regardless of the internal + * viewport detection. Use this when an ancestor component already + * owns the mobile/desktop decision (e.g. TopBar branches on + * `uiStore.isMobile` at ≤768px but this component's own breakpoint + * is ≤639.98px — without the override, 640–768px would show the + * desktop dropdown inside a mobile layout). + */ + mobile?: boolean; + } + + let { mobile }: Props = $props(); + let open = $state(false); // ── Viewport detection ─────────────────────────────────────────────── // Track mobile viewport so we can swap the absolute-positioned dropdown // for a full-width BottomSheet that reads better when workspace names - // are long or the list is deep. - let isMobile = $state(false); + // are long or the list is deep. Skipped when the caller passes an + // explicit `mobile` prop. + let detectedMobile = $state(false); $effect(() => { + if (mobile !== undefined) return; if (typeof window === 'undefined') return; const mq = window.matchMedia('(max-width: 639.98px)'); - isMobile = mq.matches; + detectedMobile = mq.matches; const onChange = (e: MediaQueryListEvent) => { - isMobile = e.matches; + detectedMobile = e.matches; // Close the sheet if the viewport crosses above mobile while it's // open (e.g. rotation) so returning to mobile doesn't reopen it. if (!e.matches) { @@ -27,13 +43,29 @@ return () => mq.removeEventListener('change', onChange); }); + // Also close the sheet if an ancestor-driven `mobile` prop flips off + // while the sheet is open — same rotation-reopen guard as the internal + // detection path. + $effect(() => { + if (mobile === false) { + open = false; + } + }); + + let isMobile = $derived(mobile ?? detectedMobile); + function select(ws: { slug: string; owner_username?: string }) { open = false; + // Close the mobile sidebar if open — the TopBar's previous inline + // workspace links did this on click, so preserve the behavior now + // that the switcher is the mobile nav entry point. + uiStore.onNavigate(); goto(`/${ws.owner_username}/${ws.slug}`); } function openCreateModal() { open = false; + uiStore.onNavigate(); uiStore.openCreateWorkspace(); }