From 059356cce7528239858e2f97e606cc5e65b38356 Mon Sep 17 00:00:00 2001 From: Naiyuan Qing <145280634+NevilleQingNY@users.noreply.github.com> Date: Thu, 23 Apr 2026 06:57:10 +0800 Subject: [PATCH] docs(claude-md): trim implementation archaeology, keep rules (#1540) CLAUDE.md is loaded into context every conversation; verbose race-condition post-mortems and code-organization rationales rot fast and crowd out the actionable rules they were meant to support. Strip the archaeology, keep the load-bearing constraints. - Workspace identity singleton + destructive ops (~22 -> 11 lines): keep the "must call setCurrentWorkspace(null, null) when leaving context" rule and the 4-step destructive order; drop the three-way race autopsy (already documented inline in workspace-tab.tsx where it belongs). - Drag region (~27 -> 3 lines): keep "every full-window desktop view must mount as first flex child"; drop hit-testing rationale, canonical-file inventory, and useImmersiveMode escape-hatch trivia. - UX vs platform chrome (~3 -> 0 lines): delete entirely. The rule duplicates "Cross-Platform Development Rules" above; the rest is purely why-we-organized-it-this-way narrative. Common Zustand footguns kept as-is - both items are real rules (stable selector references, hooks accepting wsId as parameter), not archaeology. Net: -36 lines, no rule lost. Co-authored-by: Claude Opus 4.7 (1M context) --- CLAUDE.md | 50 +++++++------------------------------------------- 1 file changed, 7 insertions(+), 43 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index bfc1fe6e1..1e5fc6218 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -191,64 +191,28 @@ Every path in the desktop app falls into exactly one category. Choosing the wron **Adding a new pre-workspace flow on desktop**: register a new `WindowOverlay` type in `stores/window-overlay-store.ts`. Do NOT add it to `routes.tsx`. If a shared view needs the flow on both platforms, add the route on web (`apps/web/app/(auth)/...`) AND the overlay type on desktop — the shared view component is identical. -### Workspace identity singleton +### Workspace context -`setCurrentWorkspace(slug, uuid)` in `@multica/core/platform` is the single source of truth for "which workspace is active right now". Three consumers depend on it: - -1. API client's `X-Workspace-Slug` header. -2. Zustand per-workspace storage namespace. -3. Chrome gating (`{slug && }` on desktop, similar on web). - -Normally set by `WorkspaceRouteLayout` when its route mounts. Critically: **unmount does NOT clear it.** Any code that leaves workspace context (leave workspace, delete workspace, force navigation to overlay) must call `setCurrentWorkspace(null, null)` explicitly — otherwise the realtime `workspace:deleted` handler races the mutation, chrome gating stays truthy while the workspace is gone from cache, and `useWorkspaceId` throws. +`setCurrentWorkspace(slug, uuid)` from `@multica/core/platform` is the single source of truth for the active workspace. `WorkspaceRouteLayout` sets it on mount; unmount does NOT clear it. Code that leaves workspace context (leave/delete workspace, force-navigate to overlay) must call `setCurrentWorkspace(null, null)` explicitly. ### Workspace destructive operations -Leave / Delete workspace flows must follow this order: +Leave / Delete workspace flows must follow this order, otherwise concurrent refetches race and the renderer hard-reloads: -1. Read destination from cached workspace list (no extra fetch). +1. Read destination from cached workspace list. 2. `setCurrentWorkspace(null, null)`. -3. `navigation.push(destination)` — switch to next workspace or open new-workspace overlay. +3. `navigation.push(destination)`. 4. THEN `await mutation.mutateAsync(workspaceId)`. -Reversing step 4 with steps 1–3 (mutate first, navigate after) causes a three-way race between the mutation's `onSettled` invalidate, the explicit `navigateAway`, and the realtime handler's `relocateAfterWorkspaceLoss` — all refetching the same `workspaces` query concurrently. One gets cancelled, bubbles as `CancelledError`, and triggers `window.location.assign` → full renderer reload / white screen. - ### Tab isolation Tabs are grouped per workspace in `stores/tab-store.ts`. The TabBar shows only the active workspace's tabs; cross-workspace tab leakage is impossible by construction (no flat global tabs array). Cross-workspace `push(path)` is detected by the navigation adapter (`platform/navigation.tsx`) and translated into `switchWorkspace(slug, targetPath)` — NOT a navigation within the current tab's router. Don't bypass the adapter; always go through `useNavigation()` from shared code. -### Drag region (macOS window-move) +### Drag region (macOS) -Every full-window desktop view (login, onboarding, new-workspace, invite, no-access, create-workspace modal) — i.e. anything that isn't inside the dashboard shell — needs a top drag strip so users can move the window. The native macOS traffic lights are **kept visible** for every such surface (Linear/Notion/Arc pattern); no `useImmersiveMode` by default. - -**Pattern**: use the shared `` from `@multica/views/platform` as the first flex child of the page root. It's a 48px transparent row with `-webkit-app-region: drag` — the parent's bg fills through it so the page reads edge-to-edge while the top 48px stays draggable under the traffic lights. - -```tsx -import { DragStrip } from "@multica/views/platform"; - -return ( -
- -
- {/* page content — interactive elements placed at y ≥ 48 clear the strip; - any element at y < 48 needs WebkitAppRegion: "no-drag" */} -
-
-); -``` - -Why flex, not absolute: the absolute-strip + `z-index` approach relies on stacking-context hit-testing, which isn't reliable for `-webkit-app-region`. A real flex row with no siblings at that pixel is unambiguous. Web browsers silently ignore `-webkit-app-region`, so shared views render the strip as a plain 48px spacer on web — safe cross-platform. - -**Horizontal clearance**: traffic lights occupy roughly x ∈ [16, 76] on macOS. Interactive UI (Back buttons, menus) should start at x ≥ 80 on desktop-sized viewports. The shared views default to sufficient `lg:px-20` padding; re-examine when laying out anything in the top-left corner. - -Canonical example: `packages/views/platform/drag-strip.tsx`. Used by `onboarding/steps/step-welcome.tsx` (per-column), `onboarding/onboarding-flow.tsx`, `workspace/new-workspace-page.tsx`, `invite/invite-page.tsx`, `workspace/no-access-page.tsx`, `modals/create-workspace.tsx`, and desktop's `pages/login.tsx`. - -**When to use `useImmersiveMode`**: only when a view must place interactive UI in the traffic-light hit-zone (y < 28 AND x < 80). For every current non-dashboard surface, buttons sit at y ≥ 48, so immersive mode is unnecessary. Hook is preserved as an escape hatch but has no callers. - -### UX vs platform chrome - -UX affordances (Back button, Log out button, welcome copy, invite card) belong in `packages/views/` so web and desktop render identical content. Platform chrome (tab system interaction, native-window IPC, `useImmersiveMode`) lives in desktop-only code. The `DragStrip` + `useImmersiveMode` primitives live in `packages/views/platform/` because they're cross-platform safe (web no-op) and need to be callable from shared views that own the page layout — keeping them in desktop-only would force every shared page to leave top-padding decisions to the platform shell, fragmenting the design. +Every full-window desktop view (anything outside the dashboard shell) must mount `` from `@multica/views/platform` as the first flex child of the page root, otherwise users can't drag the window. Interactive UI inside the top 48px needs `WebkitAppRegion: "no-drag"` to stay clickable. ## UI/UX Rules