Skip to content

App shell / scaffold

In review
Live · web
Platform
Nav
Surface
NockerlDashboard

dizyx · nockerl-design

Streaming · 2 tools running

nockerl-design · docs site

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.
// 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>
);
}
PropTypeDefaultDescription
titlestringTitle 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 *stringActive destination id. Drives the selected indicator + which content shows.
onSelect *(id: string) => voidFired 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 defaultNav presentation: full sidebar or icon rail. Defaults from the preset; bottom navigation is retired.
actionsReactNodeTrailing top-bar controls (cluster, inbox, avatar). Rendered after the title.
children *ReactNodeThe content region, rendered transparent so the facet field shows behind it.
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).