diff --git a/web/src/app.css b/web/src/app.css index 8040e649..5f0e7307 100644 --- a/web/src/app.css +++ b/web/src/app.css @@ -27,6 +27,8 @@ --status-archived: #444444; --topbar-height: 44px; + --bottom-nav-height: 56px; + --context-bar-height: 44px; --sidebar-width: 260px; --detail-panel-width: 300px; --content-max-width: 960px; @@ -956,3 +958,28 @@ dialog.attachment-image-lightbox .attachment-image-lightbox-close:hover { } } +/* + Mobile bottom-nav reflow (PLAN-1694). is rendered in the + workspace layout and uses position: fixed; bottom: 0 on mobile. It toggles + `body.has-bottom-nav` while mounted on mobile so the scroll container + (.main-content, defined in the root +layout.svelte) gets bottom padding and + content can't hide under the bar. Gated by the same ≤768px breakpoint + uiStore.isMobile uses, and by the body class so non-workspace shell pages + (which share .main-content but have no bottom nav) aren't padded. The + safe-area inset keeps the last row clear of the iOS home indicator. +*/ +@media (max-width: 768px) { + body.has-bottom-nav .main-content { + padding-bottom: calc(var(--bottom-nav-height) + env(safe-area-inset-bottom, 0px)); + } + /* + Contextual top bar on detail screens (MobileContextBar, PLAN-1694 + Phase 2). It's position: fixed; top: 0 and toggles body.has-context-bar + while shown, so pad the scroll container's top to match. Same body-class + pattern as the bottom-nav reflow above; safe-area inset clears the notch. + */ + body.has-context-bar .main-content { + padding-top: calc(var(--context-bar-height) + env(safe-area-inset-top, 0px)); + } +} + diff --git a/web/src/app.html b/web/src/app.html index 730a9448..c346a7af 100644 --- a/web/src/app.html +++ b/web/src/app.html @@ -2,7 +2,7 @@ - + diff --git a/web/src/lib/components/layout/BottomNav.svelte b/web/src/lib/components/layout/BottomNav.svelte new file mode 100644 index 00000000..1180d3c5 --- /dev/null +++ b/web/src/lib/components/layout/BottomNav.svelte @@ -0,0 +1,223 @@ + + + +{#if uiStore.isMobile && wsPrefix} + + + (workspaceOpen = false)} /> + (youOpen = false)} /> + + (captureOpen = false)} + {wsSlug} + {wsPrefix} + /> +{/if} + + diff --git a/web/src/lib/components/layout/DockedSheet.svelte b/web/src/lib/components/layout/DockedSheet.svelte new file mode 100644 index 00000000..1c217a3d --- /dev/null +++ b/web/src/lib/components/layout/DockedSheet.svelte @@ -0,0 +1,134 @@ + + + + + +{#if open} + + +
+ +{/if} + + diff --git a/web/src/lib/components/layout/MobileContextBar.svelte b/web/src/lib/components/layout/MobileContextBar.svelte new file mode 100644 index 00000000..4f171159 --- /dev/null +++ b/web/src/lib/components/layout/MobileContextBar.svelte @@ -0,0 +1,155 @@ + + + +{#if show} +
+ +

{title}

+ +
+{/if} + + diff --git a/web/src/lib/components/layout/QuickCaptureSheet.svelte b/web/src/lib/components/layout/QuickCaptureSheet.svelte new file mode 100644 index 00000000..09cb53aa --- /dev/null +++ b/web/src/lib/components/layout/QuickCaptureSheet.svelte @@ -0,0 +1,199 @@ + + + + +
+ + +
+ + +
+
+
+ + diff --git a/web/src/lib/components/layout/Sidebar.svelte b/web/src/lib/components/layout/Sidebar.svelte index bd539eaa..cb016f14 100644 --- a/web/src/lib/components/layout/Sidebar.svelte +++ b/web/src/lib/components/layout/Sidebar.svelte @@ -10,6 +10,7 @@ import { goto } from '$app/navigation'; import { api, isPlanLimitError, planLimitMessage } from '$lib/api/client'; import { parseSchema, parseSettings, itemUrlId } from '$lib/types'; + import { getActiveKey } from '$lib/nav/destinations'; import type { Collection } from '$lib/types'; import { toastStore } from '$lib/stores/toast.svelte'; import NotificationPanel from '$lib/components/common/NotificationPanel.svelte'; @@ -29,28 +30,19 @@ let wsUsername = $derived(workspaceStore.current?.owner_username ?? ''); let wsPrefix = $derived(wsUsername && wsSlug ? `/${wsUsername}/${wsSlug}` : ''); let isGuest = $derived(workspaceStore.current?.is_guest ?? false); - let isDashboardPage = $derived(wsPrefix ? page.url.pathname === wsPrefix : false); - let isInsightsPage = $derived(wsPrefix ? page.url.pathname === `${wsPrefix}/insights` : false); - let isRolesPage = $derived(wsPrefix ? page.url.pathname === `${wsPrefix}/roles` : false); - let isActivityPage = $derived(wsPrefix ? page.url.pathname === `${wsPrefix}/activity` : false); - let isStarredPage = $derived(wsPrefix ? page.url.pathname === `${wsPrefix}/starred` : false); - let isTagsPage = $derived( - wsPrefix - ? page.url.pathname === `${wsPrefix}/tags` || - page.url.pathname.startsWith(`${wsPrefix}/tags/`) - : false - ); + // Active-state derives from the shared nav source so the Sidebar and the + // mobile BottomNav/More sheet can't drift (PLAN-1694 DR-1). + let activeKey = $derived(getActiveKey(page.url.pathname, wsPrefix)); + let isDashboardPage = $derived(activeKey === 'dashboard'); + let isInsightsPage = $derived(activeKey === 'insights'); + let isRolesPage = $derived(activeKey === 'roles'); + let isActivityPage = $derived(activeKey === 'activity'); + let isStarredPage = $derived(activeKey === 'starred'); + let isTagsPage = $derived(activeKey === 'tags'); - let activeCollectionSlug = $derived.by(() => { - if (!wsPrefix) return null; - const prefix = `${wsPrefix}/`; - const path = page.url.pathname; - if (!path.startsWith(prefix)) return null; - const rest = path.slice(prefix.length); - const slug = rest.split('/')[0]; - if (slug === 'settings' || slug === 'new' || slug === 'library' || slug === 'activity' || slug === 'starred' || slug === 'tags' || slug === 'roles' || slug === 'insights' || slug === '') return null; - return slug; - }); + let activeCollectionSlug = $derived( + activeKey && activeKey.startsWith('collection:') ? activeKey.slice('collection:'.length) : null + ); let activeColl = $derived( activeCollectionSlug diff --git a/web/src/lib/components/layout/WorkspaceSheet.svelte b/web/src/lib/components/layout/WorkspaceSheet.svelte new file mode 100644 index 00000000..ab72c4ac --- /dev/null +++ b/web/src/lib/components/layout/WorkspaceSheet.svelte @@ -0,0 +1,362 @@ + + + + +
+ + + + {#if switching} +
+ {#each workspaceStore.workspaces as ws (ws.slug)} + + {/each} + +
+ {/if} + + +
Navigate
+
+ {#each navDestinations as dest (dest.key)} + + {/each} +
+ + + {#if regularCollections.length || agentCollections.length} +
Collections
+
+ {#each regularCollections as coll (coll.id)} + + {/each} + {#each agentCollections as coll (coll.id)} + + {/each} +
+ {/if} +
+
+ + diff --git a/web/src/lib/components/layout/YouSheet.svelte b/web/src/lib/components/layout/YouSheet.svelte new file mode 100644 index 00000000..6689d1fd --- /dev/null +++ b/web/src/lib/components/layout/YouSheet.svelte @@ -0,0 +1,308 @@ + + + + +
+ {#if authStore.user} +
+ + {avatarInitial(userName || userEmail)} + + + {userName} + {userEmail} + +
+ +
+ + + + + + Account settings + + + + + Workspaces + + + {#if authStore.cloudMode} + + + Billing + + + {/if} + {#if authStore.user?.role === 'admin'} + + + Admin + + + {/if} + {#if wsSlug} + + {/if} +
+ + +
+ +
+ + + {/if} +
+
+ +{#if wsSlug} + +{/if} + + diff --git a/web/src/lib/components/search/CommandPalette.svelte b/web/src/lib/components/search/CommandPalette.svelte index 6e313440..34ef9737 100644 --- a/web/src/lib/components/search/CommandPalette.svelte +++ b/web/src/lib/components/search/CommandPalette.svelte @@ -55,6 +55,26 @@ let searchTimeout: ReturnType; let inputEl = $state(); + // Mobile: swipe down on the grab handle to dismiss, matching the + // Workspace/You docked sheets (PLAN-1694). Desktop is unaffected — the grip + // is display:none above 768px. + let dragY = $state(0); + let dragging = $state(false); + let dragStartY = 0; + function gripStart(e: TouchEvent) { + dragStartY = e.touches[0].clientY; + dragging = true; + } + function gripMove(e: TouchEvent) { + if (!dragging) return; + dragY = Math.max(0, e.touches[0].clientY - dragStartY); + } + function gripEnd() { + dragging = false; + if (dragY > 90) uiStore.closeSearch(); + dragY = 0; + } + // Filters let filterCollection = $state(null); let filterStatus = $state(null); @@ -682,7 +702,26 @@
uiStore.closeSearch()}> -
e.stopPropagation()} onkeydown={handleKeydown}> +
e.stopPropagation()} + onkeydown={handleKeydown} + style:transform={dragY ? `translateY(${dragY}px)` : undefined} + style:transition={dragging ? 'none' : undefined} + > + + +
+ +