Skip to content

Commit dfcfac3

Browse files
authored
feat(web): workspace switcher button in every shell posture (0280) (#411)
Follow-up to #409: the workspace switcher verb (`workspace.switch`) was only reachable from the calm shell's ModeSwitch. Now every shell posture has the pointer road: - **Workbench (bench)** — a Workspaces button on the Rail, above the identity/settings footer. - **Quiet (minimal)** — a Workspaces glyph in the top-left corner cluster, between Search and Settings. - **Calm** — unchanged (already had it). And **Settings → Appearance** reflects the 0280 defaults: the Layout row now picks between the three presets (Quiet / Calm / Bench via `applyPreset`, active state derived from the tree's provenance), plus a Workspaces row with a shortcut into the switcher. Verified live in the worktree preview (flag on): the button opens the switcher in all three postures, and the settings preset row applies and highlights correctly. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
2 parents 136d2ac + faf2f31 commit dfcfac3

4 files changed

Lines changed: 56 additions & 10 deletions

File tree

‎apps/web/src/routes/settings.tsx‎

Lines changed: 33 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
*/
99
import { createFileRoute } from '@tanstack/react-router'
1010
import { serializeShare } from '@xnetjs/identity'
11-
import { deleteDay, leaveWithEverything } from '@xnetjs/plugins'
11+
import { deleteDay, getCommandRegistry, leaveWithEverything } from '@xnetjs/plugins'
1212
import { useIdentity } from '@xnetjs/react'
1313
import { SettingRow, SettingsGroup, SettingsPanel, SettingToggle, useTheme } from '@xnetjs/ui'
1414
import { MeetingEngineSettings } from '@xnetjs/views'
@@ -17,8 +17,10 @@ import {
1717
Database,
1818
Mic,
1919
Info,
20+
Layers,
2021
LayoutGrid,
2122
MessageSquare,
23+
Square,
2224
Sun,
2325
Moon,
2426
Monitor,
@@ -52,6 +54,7 @@ import { createLeavePorts, downloadLeaveBundle, type LeaveDeps } from '../lib/le
5254
import { isSentryConfigured } from '../lib/sentry'
5355
import { useConsent } from '../lib/use-consent'
5456
import { WINDDOWN_DURATION_CHOICES, useWinddownPreferences } from '../lib/winddown'
57+
import { presetForWorkspaceId } from '../workbench/layout-tree'
5558
import { useWorkbench } from '../workbench/state'
5659

5760
/** Marketing + dashboard origins for xNet Cloud (managed hub hosting). */
@@ -194,28 +197,49 @@ function SettingsPage() {
194197
function AppearanceSettings() {
195198
const { theme, setTheme, variant, setVariant, density, setDensity } = useTheme()
196199
const winddown = useWinddownPreferences()
197-
const layout = useWorkbench((state) => state.layout)
198-
const setLayout = useWorkbench((state) => state.setLayout)
200+
// The shell defaults are presets over the layout tree (0280): the active
201+
// one is derived from the tree's provenance; a saved workspace shows none.
202+
const activePreset = useWorkbench((state) => presetForWorkspaceId(state.tree.workspaceId))
203+
const applyPreset = useWorkbench((state) => state.applyPreset)
199204

200205
return (
201206
<SettingsPanel title="Appearance" description="Customize how xNet looks">
202207
<SettingsGroup>
203208
<SettingRow
204209
label="Layout"
205-
description="Calm puts a conversation, a list and a contextual canvas up front; Workbench is the multi-pane editor grid"
210+
description="Quiet is the bare surface with chrome at the edges; Calm puts a conversation, a list and a contextual canvas up front; Bench is the multi-pane editor grid"
206211
>
207212
<div className="flex gap-1.5">
213+
<ThemeButton
214+
icon={<Square size={14} strokeWidth={1.5} />}
215+
label="Quiet"
216+
active={activePreset === 'quiet'}
217+
onClick={() => applyPreset('quiet')}
218+
/>
208219
<ThemeButton
209220
icon={<MessageSquare size={14} strokeWidth={1.5} />}
210221
label="Calm"
211-
active={layout === 'calm'}
212-
onClick={() => setLayout('calm')}
222+
active={activePreset === 'calm'}
223+
onClick={() => applyPreset('calm')}
213224
/>
214225
<ThemeButton
215226
icon={<LayoutGrid size={14} strokeWidth={1.5} />}
216-
label="Workbench"
217-
active={layout === 'workbench'}
218-
onClick={() => setLayout('workbench')}
227+
label="Bench"
228+
active={activePreset === 'bench'}
229+
onClick={() => applyPreset('bench')}
230+
/>
231+
</div>
232+
</SettingRow>
233+
<SettingRow
234+
label="Workspaces"
235+
description="Rearranged panels can be saved as named workspaces — switch, share or reset from the switcher (also on the rail and in ⌘K)"
236+
>
237+
<div className="flex gap-1.5">
238+
<ThemeButton
239+
icon={<Layers size={14} strokeWidth={1.5} />}
240+
label="Switch workspace…"
241+
active={false}
242+
onClick={() => void getCommandRegistry().runCommand('workspace.switch')}
219243
/>
220244
</div>
221245
</SettingRow>

‎apps/web/src/workbench/Rail.tsx‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
Compass,
1515
Contact,
1616
Files,
17+
Layers,
1718
MessageSquare,
1819
Network,
1920
Puzzle,
@@ -159,6 +160,14 @@ export function Rail() {
159160

160161
<div className="flex-1" />
161162

163+
{/* Workspace switcher (0280): same verb as the calm ModeSwitch button. */}
164+
<RailButton
165+
label="Workspaces"
166+
icon={Layers}
167+
coachId="workspace.switch"
168+
onClick={() => void getCommandRegistry().runCommand('workspace.switch')}
169+
/>
170+
162171
{identity && (
163172
<div
164173
title={identity.did}

‎apps/web/src/workbench/calm/QuietChrome.tsx‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import { Link, useLocation, useNavigate } from '@tanstack/react-router'
1616
import { getCommandRegistry } from '@xnetjs/plugins'
1717
import { useIdentity } from '@xnetjs/react'
1818
import { PopoverContent, PopoverRoot, PopoverTrigger, Sheet, SheetContent } from '@xnetjs/ui'
19-
import { Search, Settings, type LucideIcon } from 'lucide-react'
19+
import { Layers, Search, Settings, type LucideIcon } from 'lucide-react'
2020
import { useEffect, useLayoutEffect, useRef, useState, type ReactNode } from 'react'
2121
import { contributeTips } from '../../coachmarks'
2222
import { slotsIn } from '../layout-tree'
@@ -203,6 +203,11 @@ function CornerGlyphs({ lit, activeMode }: { lit: boolean; activeMode: CalmMode
203203
icon={Search}
204204
onClick={() => void getCommandRegistry().runCommand('search.open')}
205205
/>
206+
<GlyphButton
207+
label="Workspaces"
208+
icon={Layers}
209+
onClick={() => void getCommandRegistry().runCommand('workspace.switch')}
210+
/>
206211
<Link
207212
to="/settings"
208213
title="Settings"
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"id": "2026-07-08-the-workspace-switcher-is-now-one-tap-aw",
3+
"date": "July 8, 2026",
4+
"title": "The workspace switcher is now one tap away in every shell",
5+
"summary": "The Workspaces button now appears on the workbench rail and in the quiet shell's corner glyphs, not just the calm mode switch — and Settings → Appearance picks between the three layout presets (Quiet, Calm, Bench) with a shortcut to the workspace switcher.",
6+
"highlights": [],
7+
"tags": ["app"]
8+
}

0 commit comments

Comments
 (0)