App shell / scaffold
In reviewLive · web
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.
// Web: React, consuming @dizyx/nockerl-tokens. (Web is the laggard platform. Spec + live demo,// not yet exported from @dizyx/nockerl-react; promotion is tracked.)
export function App() { const [section, setSection] = useState('chat'); return ( <AppShell preset="web" // 'web' | 'android' | 'voice' (platform chrome preset) title="Chat" layout="expanded" // 'expanded' | 'rail'; bottom nav retired (preset sets the default) destinations={[ { id: 'chat', label: 'Chat', icon: <ChatIcon />, dot: 'streaming' }, { id: 'tasks', label: 'Tasks', icon: <TasksIcon />, dot: 'attention' }, { id: 'files', label: 'Files', icon: <FilesIcon /> }, { id: 'cluster', label: 'Cluster', icon: <ClusterIcon /> }, ]} selected={section} onSelect={setSection} actions={<><ClusterButton /><InboxButton count={3} /><NockerlAvatar /></>} > <SectionContent section={section} /> {/* the content region, over the facet field */} </AppShell> );}// Android: Jetpack Compose (canonical). com.nockerl.app.core.ui.MainScaffold// The shell is a Box: a PINNED ChatFeedBackground at the base, a stacked top// chrome (GlobalTopBar -> ProjectRow -> SessionChipsBar -> TopChromeBoundary)// that casts one soft shadow, then the transparent content (ChatScreen).import com.nockerl.app.core.ui.MainScaffold
// Hosted by the auth-gated NavHost once authenticated:NavHost(navController, startDestination = NockerlRoutes.MAIN) { composable(NockerlRoutes.MAIN) { MainScaffold() }}
// MainScaffold composes the regions itself:// GlobalTopBar(onAvatarClick, onInboxClick, onClusterClick, unreadCount, hasUnseen)// ProjectRow(projectName, isWorkspaceScoped, onOpenPicker, onOpenFiles, onOpenTasks)// TopChromeBoundary(collapsed = headerCollapsed, onCollapsedChange = { … }) // cyan line + grab handle// ChatScreen(modifier = Modifier.weight(1f)) // transparent content// macOS: SwiftUI (canonical). NockerlVoice/UI/DashboardView.swift// A ZStack: one full-window FacetBackground under EVERYTHING, a translucent// floating sidebar, and the selected page full-bleed over the same field.// No title bar (.windowStyle(.hiddenTitleBar) on the scene).struct DashboardView: View { @ObservedObject private var router = DashboardRouter.shared
var body: some View { ZStack(alignment: .topLeading) { FacetBackground().ignoresSafeArea() // pinned geometric ground HStack(spacing: 0) { sidebar // brand + nav items + settings detail.frame(maxWidth: .infinity, maxHeight: .infinity) } .ignoresSafeArea(.container, edges: .top) // flow under the traffic lights } .frame(minWidth: 860, minHeight: 580) .tint(NockerlTheme.accent) }}Parameters
Section titled “Parameters”| Prop | Type | Default | Description |
|---|---|---|---|
title | string | Title shown in the top bar (the active destination on small widths). | |
destinations * | ShellDestination[] | Nav entries: { id, label, icon, dot? }. Each renders as one focusable button. | |
selected * | string | Active destination id. Drives the selected indicator + which content shows. | |
onSelect * | (id: string) => void | 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 | Trailing top-bar controls (cluster, inbox, avatar). Rendered after the title. | |
children * | ReactNode | 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. |
GlobalTopBar.onAvatarClick / onInboxClick / onClusterClick | () -> Unit | {} | Top-bar actions that open the settings / inbox / cluster sheets. |
GlobalTopBar.unreadCount | Int | 0 | Inbox badge count (orange agent-family token); falls back to a dot via hasUnseen. |
ProjectRow.onOpenPicker / onOpenFiles / onOpenTasks * | () -> Unit | Project selector + Files / Tasks buttons → their bottom sheets. | |
TopChromeBoundary.collapsed * | Boolean | Whether the project + session rows are collapsed up under the title bar. | |
TopChromeBoundary.onCollapsedChange * | (Boolean) -> Unit | 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 | The pinned full-window geometric field (.ignoresSafeArea()), the animated ground under everything. | |
sidebar | View (216pt) | 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 | One nav row. Selected → cyan label + cyan @0.16 fill + cyan @0.45 border; hover → onSurface @0.06. | |
detail | View | The selected page, full-bleed over the same field (switched on router.section). | |
router.section | DashboardSection | Active destination. Drives both the selected row and which detail pane shows. |