From 28cd757a345f28e72e110f2b61f78b00b61e9a8d Mon Sep 17 00:00:00 2001 From: Michael Ramos Date: Fri, 9 Oct 2026 10:32:36 -0400 Subject: [PATCH 1/2] =?UTF-8?q?Inbox:=20the=20Workspaces=20sidebar=20(clos?= =?UTF-8?q?e,=20open,=20peek,=20=E2=8C=98B)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- AGENTS.md | 2 + bun.lock | 2 + packages/inbox/App.tsx | 45 +-- packages/inbox/components/Decisions.tsx | 2 + packages/inbox/components/EmptyState.tsx | 2 + packages/inbox/components/InboxList.tsx | 2 + packages/inbox/components/Settings.tsx | 6 +- packages/inbox/components/Sidebar.tsx | 19 +- packages/inbox/inbox.css | 60 +++- packages/inbox/package.json | 2 + packages/inbox/shell/CollapsibleEdge.tsx | 109 +++++++ packages/inbox/shell/SidebarPeek.tsx | 128 ++++++++ packages/inbox/shell/SidebarShell.tsx | 116 +++++++ packages/inbox/shell/motion-style-div.tsx | 96 ++++++ packages/inbox/shell/shell-animate.ts | 40 +++ packages/inbox/shell/shell-motion.ts | 182 +++++++++++ packages/inbox/shell/sidebar.tsx | 356 ++++++++++++++++++++++ packages/inbox/shell/useResizablePanel.ts | 289 ++++++++++++++++++ packages/ui/HANDOFF.md | 4 + packages/ui/config/settings.ts | 14 + tests/e2e/inbox-sidebar.spec.ts | 295 ++++++++++++++++++ tests/e2e/playwright.config.ts | 4 +- 22 files changed, 1746 insertions(+), 29 deletions(-) create mode 100644 packages/inbox/shell/CollapsibleEdge.tsx create mode 100644 packages/inbox/shell/SidebarPeek.tsx create mode 100644 packages/inbox/shell/SidebarShell.tsx create mode 100644 packages/inbox/shell/motion-style-div.tsx create mode 100644 packages/inbox/shell/shell-animate.ts create mode 100644 packages/inbox/shell/shell-motion.ts create mode 100644 packages/inbox/shell/sidebar.tsx create mode 100644 packages/inbox/shell/useResizablePanel.ts create mode 100644 tests/e2e/inbox-sidebar.spec.ts diff --git a/AGENTS.md b/AGENTS.md index 5d094bec20..59fa19ff12 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1894,6 +1894,7 @@ Sorting and the rest: - Decisions, with the count of questions that record a decision once sent. - Settings. - "A new version is ready, Restart" when health reports `update`. Restart is `POST /api/inbox/restart`: the server stops, the binary on disk starts detached, and the page reloads on the new `serverSession`. + - The frame is Workspaces' sidebar primitive, copied by the owner's word (2026-10-09) into `packages/inbox/shell/` (each file names its Workspaces source and commit): the toggle at the head of each page and ⌘B / Ctrl+B close and open it on Workspaces' one spring (`shell-motion.ts`, MotionValues from `motion`, reduced motion lands without travel); the state is ui's `inboxSidebarOpen` setting (cookie `plannotator-inbox-sidebar-open`, as `gridEnabled`). Closed, a 600 ms rest on the left edge peeks it (1024 px and wider). The edge collapses on a click and resizes on a drag (200 to 520 px, 232 by default, cookie `plannotator-inbox-sidebar-width`), closing past 100 px and reopening if the held pointer comes back. Below 768 px it is a sheet. A file beside a thread closes it on the same spring without changing the stored choice. - A wide row shows: - the project label - the badge ("Stopped", "Holds up N") @@ -2099,6 +2100,7 @@ Sorting and the rest: - `inbox-attachments.spec.ts` - `inbox-guides.spec.ts` - `inbox-order.spec.ts` (three agents' questions read newest first in Waiting on you; a reply moves its row to Sent) + - `inbox-sidebar.spec.ts` (the sidebar's close and open sampled every frame, ⌘B and Ctrl+B, the reload, the peek, the edge's click, drag, snap-close and keyboard, reduced motion, the phone sheet; its PNGs, video and curves in `.local/proof/sidebar/`) - `inbox-phones.spec.ts` (Pair a phone, the device list and Remove; Reach from this Wi-Fi with a pinned phone pairing over the LAN, the no-Bonjour note and `openssl` failing; with `PLANNOTATOR_E2E_TAILNET=1` on a Mac signed in to Tailscale, the door at the MagicDNS name on 8443) PNGs land in `.local/proof/`. diff --git a/bun.lock b/bun.lock index 3dc3ea59e8..17ca0a7b47 100644 --- a/bun.lock +++ b/bun.lock @@ -278,10 +278,12 @@ "name": "@plannotator/inbox", "version": "0.0.1", "dependencies": { + "@base-ui/react": "^1.7.0", "@plannotator/core": "workspace:*", "@plannotator/guide-viewer": "workspace:*", "@plannotator/review-editor": "workspace:*", "@plannotator/ui": "workspace:*", + "motion": "^12.38.0", "react": "^19.2.8", "react-dom": "^19.2.8", "uqr": "^0.1.3", diff --git a/packages/inbox/App.tsx b/packages/inbox/App.tsx index c2b655de6c..4fe8397652 100644 --- a/packages/inbox/App.tsx +++ b/packages/inbox/App.tsx @@ -13,6 +13,7 @@ import { useCallback, useEffect, useMemo, useRef, useState, type ReactNode } from 'react'; import { ThemeProvider } from '@plannotator/ui/components/ThemeProvider'; import { configurePlannotatorUI } from '@plannotator/ui/configure'; +import { configStore, useConfigValue } from '@plannotator/ui/config'; import type { InboxDecision, InboxHealth, InboxListRow, InboxListSection, InboxQuestion, InboxThread } from '@plannotator/core/inbox-types'; import { InboxApiError, @@ -28,6 +29,7 @@ import { filterSections, heldNotice, refreshHeldRows, waitingCount } from './hel import { tildePath } from './format'; import type { ConnectContext } from './harnesses'; import { Sidebar } from './components/Sidebar'; +import { SidebarShell } from './shell/SidebarShell'; import { InboxList } from './components/InboxList'; import { ThreadPane } from './components/Thread'; import { EmptyState } from './components/EmptyState'; @@ -399,6 +401,8 @@ function Inbox() { const filterProject = route.page === 'inbox' && route.project ? projects.find((p) => p.id === route.project) ?? null : null; const notice = shown && latest ? heldNotice(filterSections(shown, route.project), filterSections(latest.sections, route.project)).text : null; const firstRun = latest !== null && projects.length === 0; + // Open or closed as the person left it (⌘B or the toggle), remembered in a cookie like the Grid/Clean look. + const sidebarOpen = useConfigValue('inboxSidebarOpen'); // A file beside the thread takes the list's and the sidebar's room (record 2.2). const fileOpen = route.page === 'inbox' && !!route.thread && !!route.file; const guideMessage = @@ -477,24 +481,31 @@ function Inbox() { return (
-
- go({ page: 'inbox', project: null, thread: null })} - onProject={(projectId) => go({ page: 'inbox', project: projectId, thread: null })} - onSettings={() => go({ page: 'settings', project: null, thread: null })} - onDecisions={() => go({ page: 'decisions', project: null, thread: null })} - onRestart={() => void restart()} - /> + {/* A file beside the thread takes the sidebar's room (record 2.2): it closes on the same spring, and the person's choice stays as it was. */} + configStore.set('inboxSidebarOpen', next)} + navigation={ + go({ page: 'inbox', project: null, thread: null })} + onProject={(projectId) => go({ page: 'inbox', project: projectId, thread: null })} + onSettings={() => go({ page: 'settings', project: null, thread: null })} + onDecisions={() => go({ page: 'decisions', project: null, thread: null })} + onRestart={() => void restart()} + /> + } + >
{main}
-
+ {guideMessage && thread && (
+

Decisions