Designing the app-shell scaffold to the token standard.
Web · mix · nav Expanded · viewing Chat. The brand sits ONCE (the nav for web/Voice, the top bar for Android), never duplicated; bottom-nav is retired. Tab in, arrow-key the destinations, the facet drifts behind. Live.
Brand placement + platform presets (ratified)
The app shell is a pure composition of the corrected components (the real brand
lockup, the nav items, and the facet field), so every top-bar /
nav fix propagates here automatically. Two rules are ratified:
The brand appears exactly once. Never duplicate the logo / title between the top bar and
the left nav. The surface’s main nav owns the brand: the left nav on web and
Voice, the top bar on Android (whose top bar is the primary chrome).
Platform presets:Android = a top bar (brand) + a compact nav rail · Voice = the
left nav ONLY, no title bar · web = the mix (top bar + expanded nav). Switch them live above.
Bottom navigation is retired. The shell has two nav layouts (expanded sidebar or icon
rail), never a bottom bar.
Title shown in the top bar (the active destination on small widths).
destinations *
ShellDestination[]
n/a
Nav entries: { id, label, icon, dot? }. Each renders as one focusable button.
selected *
string
n/a
Active destination id. Drives the selected indicator + which content shows.
onSelect *
(id: string) => void
n/a
Fired when a destination is activated (click / Enter / Space).
preset
'web' | 'android' | 'voice'
'web'
Platform chrome preset. web: top bar + expanded nav · android: top bar (owns the brand) + icon rail · voice: left nav only, no title bar. Sets the default layout and where the brand sits.
layout
'expanded' | 'rail'
preset default
Nav presentation: full sidebar or icon rail. Defaults from the preset; bottom navigation is retired.
actions
ReactNode
n/a
Trailing top-bar controls (cluster, inbox, avatar). Rendered after the title.
children *
ReactNode
n/a
The content region, rendered transparent so the facet field shows behind it.
MainScaffold() takes only a modifier; it composes the regions internally and
hoists their view-models. The real parameters live on the region composables it
calls. Here are the most load-bearing ones:
Parameter
Type
Default
Description
MainScaffold.modifier
Modifier
Modifier
Outer modifier; applies fillMaxSize() + statusBarsPadding() internally. State (header collapse, sheets) is held inside.
Whether the project + session rows are collapsed up under the title bar.
TopChromeBoundary.onCollapsedChange *
(Boolean) -> Unit
n/a
Drag/tap the cyan boundary handle to collapse/expand the header chrome.
DashboardView is a self-contained scene root with no parameters: it reads the
shared DashboardRouter + SettingsStore. The shape is the configurable surface:
Style
Type
Default
Description
FacetBackground()
View
n/a
The pinned full-window geometric field (.ignoresSafeArea()), the animated ground under everything.
sidebar
View (216pt)
n/a
Translucent floating panel (canvas @ 0.3, no material) so the field reads through: brand at top, nav rows, settings pinned to bottom.
SidebarRow(section, isSelected, action)
View
n/a
One nav row. Selected → cyan label + cyan @0.16 fill + cyan @0.45 border; hover → onSurface @0.06.
detail
View
n/a
The selected page, full-bleed over the same field (switched on router.section).
router.section
DashboardSection
n/a
Active destination. Drives both the selected row and which detail pane shows.
Cross-platform drift (tracked for reconciliation)
All three shells share one anatomy (a top region, a nav region, and a content
region over the pinned animated facet field), but the form genuinely diverges:
Nav placement: Android stacks nav as top chrome (project + session rows
that collapse under the title bar); Voice + web use a left sidebar / rail.
Bottom navigation is retired: every shell is a sidebar or an icon rail.
Top bar: Android centers the logo with avatar/cluster/bell around it; Voice
has no title bar at all (the brand sits in the sidebar under the traffic
lights). Web carries a title + trailing actions.
Boundary: Android draws a 1.5dp cyan line + drag handle between chrome
and content; Voice/web separate regions with a hairline only.
The facet field is unified: the same low-poly triangle mesh + ~18s
diagonal tone-wave on all three (ChatFeedBackground.kt / FacetBackground.swift).