Appearance
UI Revamp v0 — @soma/ui Scaffolding Plan
Companion to ui-revamp-v0.md (PRD), the seven refs docs in this folder, and ADR-0005. This doc turns the PRD §6 component table into a sequenced, dependency-aware build plan: which components are new vs. refactors of existing code, what depends on what, the order in which they ship to @soma/ui, and the criteria a component must meet before any desktop/soma consumer wires it in.
PRD §7 step 5 is the deliverable; this is the spec for how step 5 runs.
1. Pre-flight: the token sweep
Before any component work, the design token system from ADR-0005 §7 and refs §3 lands in desktop/desktop-ui/src/styles.css. Concretely:
- Font sizes (Tailwind-native). Use
text-sm(14px) for body + dense UI rows andtext-xs(12px) for caps / hint text. Earlier waves shipped custom--text-body / --text-ui-sm / --text-ui-xstokens at exact 14 / 13 / 11px from a density audit, but they were removed when tailwind-merge was found to silently drop them when combined withtext-{color}classes incn(...)compositions. The 1px upward adjustment on the densest rows is accepted as a tradeoff for the simpler scale. - Line-heights.
--leading-body: 1.5,--leading-ui: 1.2. (Bumped from PRD's original 1.45.) - Row heights. Utility classes for three tiers, sized by content:
row-text— 2rem (32px), for text-only or icon-leading rows. Does not fit a 40px avatar — userow-avatarinstead.row-avatar— 2.5rem (40px), for rows with avatars / file-type glyphs (mention rows, attachments, members, bots). MatchesDenseRow's existingh-10leading slot.row-card— 3.25rem (52px), for rows with two-line content (primary + sub).
- Border + shadow. Establish exactly one shadow token (
--shadow-elevated), used by modal + popup only. The existingsurface-card/glass-panelutilities stay reachable assurface-card-legacy/glass-panel-legacyso non-revamped screens keep their depth during the incremental cutover. New revamped surfaces use the simple names (surface-card,glass-panel) backed by border-only / single-shadow-token rules. Once every screen is on revamped components (cutover §5 complete), the-legacyutilities get deleted in a sweep PR. - DaisyUI theme. Audit
cmyk(default) andluxuryfor shadow + radius. The default theme should ship with shadow disabled at the component level.
Token sweep is the only PR that needs zero new components — it cleans the foundation so every wave that follows compiles into a consistent visual system without breaking non-revamped screens mid-cutover.
2. Mapping PRD §6 to current @soma/ui reality
PRD §6 lists 20 components for v0. Many already exist in desktop/desktop-ui/src/components/ as starting points. The scaffold work is: refactor where there is precedent, build where there is none.
Audit override. A Storybook reality-check (ui-revamp-v0-audit.md) walked every existing story and found that several "refactor target" components are actually unverified (no story) or visibly broken; some of the verdicts below are downgraded to rebuild or story-first-refactor. Read the audit alongside this table.
| PRD §6 component | Existing in @soma/ui | Plan |
|---|---|---|
PanelContainer | none | New. panels/panel-container.tsx. |
Panel | none | New. panels/panel.tsx. |
SlashMenu | none (closest precedent is overlays/command-palette.tsx) | New. editor/slash-menu.tsx. |
MentionPicker | none | New. editor/mention-picker.tsx. |
SelectionBubble | overlays/bubble-toolbar.tsx (65 lines) | Refactor. Replace internals; rename to editor/selection-bubble.tsx. |
BotList | none (closest: lists/roster-item.tsx) | New. lists/bot-list.tsx. Built on DenseRow. |
CapabilityForm | none | New. forms/capability-form.tsx. |
BackendSwitcher | forms/ai-model-selector.tsx (107 lines) | Refactor. Move to chat/backend-switcher.tsx; reshape into a composer-footer chip per refs main §5. |
PeerAddressInput | none | New. forms/peer-address-input.tsx. |
SpacesRail | none explicit; layout shell in layout/desktop-shell.tsx may host one | New. nav/spaces-rail.tsx. |
CharDisplay | tapia/char-display.tsx | Keep. Extract any inline shadows; verify density tokens. |
DensityProvider | none | New. primitives/density-provider.tsx. |
Empty | none | New. primitives/empty.tsx with variant prop (full / compact / filter). |
DenseRow | partial in lists/roster-item.tsx | Refactor. Generalize roster-item's slot model into lists/dense-row.tsx. |
CommandPalette | overlays/command-palette.tsx (166 lines) | Refactor. Re-section per refs space-lifecycle §4.8: Recent docs → Spaces → Documents → Commands. |
TreePopover | none | New. nav/tree-popover.tsx. Built on react-complex-tree — installed in Wave 2 alongside this component. |
PopupShell | precedent: layout/window-chrome.tsx | Refactor. Extract to popup/popup-shell.tsx; expose the 4-glyph cluster from refs popup-window §6. |
SelectionAIBar | none | New. editor/selection-ai-bar.tsx. |
InlineAIAcceptBar | none | New. editor/inline-ai-accept-bar.tsx. |
NodeAIRegistry | none | New. editor/node-ai-registry.ts (logic, no UI). |
InlineAIStream | none | New. editor/inline-ai-stream.tsx. |
Gaps found during this pass — proposed additions
| Component | Why | File |
|---|---|---|
SettingsTabs | Locked by refs space-lifecycle §4.2 (horizontal pill/underline tabs) but missing from PRD §6. Reused by every settings page. | nav/settings-tabs.tsx |
Pill | Status pills (bot pending/active/failed; default backend; capability scope counters) are everywhere. Tailwind-only is fine, but a typed primitive locks the variants and the color → semantic mapping. | primitives/pill.tsx |
These two should be added to PRD §6 in a small follow-up edit; for this scaffold plan I treat them as in-scope.
3. Build waves
The waves below are dependency-ordered. Within a wave, components are independent and can be built in parallel.
Wave 0 — Foundation
| Item | Description |
|---|---|
| Token sweep (§1) | rem-based density tokens land in styles.css; revamped utilities use the simple names, legacy utilities (surface-card-legacy, glass-panel-legacy) preserve depth on non-revamped screens during cutover. |
| Storybook decorator fix | Drop the hardcoded #0f172a background; add parameters.theme knob for cmyk / luxury explicit testing; add a Storybook test-runner contrast assertion so dark-on-dark can't regress. |
DensityProvider | React context exposing density: 'dense' | 'cozy'. v0 ships dense only; the provider exists so post-v0 cozy is a config flip. |
| i18n harness | Pick + wire one of react-intl / i18next (decision happens in this PR). Ships an English-only catalog; every new component in waves 1–3 routes user-facing strings through the harness (acceptance criterion §4.7). Cost-cheap insurance against a future locale retrofit. |
| Storybook conventions doc | Update docs/src/development/ui-components.md with the story shape every component must follow (states matrix, hover/focus/disabled/empty/error, dark theme variant, i18n string usage). |
Wave 1 — Primitives (depend only on Wave 0)
| Component | Refs anchor |
|---|---|
Empty | refs files-density §2 |
DenseRow (refactor from roster-item) | refs files-density §4 |
Pill (new primitive) | Status patterns in refs assistant-bots §3 |
PeerAddressInput | refs main §4 step 1 |
CharDisplay (extract inline styles) | already locked in tapia/char-display.tsx |
SpacesRail | refs space-lifecycle §3 |
NodeAIRegistry (types + in-memory impl) | refs editor-ai §3. Interface only — editor/node-ai-registry.types.ts + a trivial createNodeAIRegistry() Wave 2 can target. TipTap wiring lands in Wave 4. |
Wave 2 — Popovers and dropdowns (depend on Wave 1)
| Component | Refs anchor |
|---|---|
SlashMenu | refs editor §1 + ADR-0005 §11 amendment |
SelectionBubble (refactor from bubble-toolbar) | refs editor §2 |
MentionPicker | refs main §3 |
BackendSwitcher (refactor from ai-model-selector) | refs main §5 |
TreePopover | refs space-lifecycle §3 |
CommandPalette (refactor existing) | refs space-lifecycle §3 |
SettingsTabs | refs space-lifecycle §2 |
SelectionAIBar | refs editor-ai §1 |
Wave 3 — Composites (depend on Waves 1–2)
| Component | Refs anchor |
|---|---|
Panel | PRD §3 + refs main §2 |
PanelContainer | (composes Panel) |
BotList | refs assistant-bots §3 |
CapabilityForm | refs main §4 step 2 |
InlineAIAcceptBar | refs editor-ai §4 |
InlineAIStream | refs editor-ai §5 |
PopupShell (refactor from window-chrome) | refs popup-window §2 |
Wave 4 — Logic / glue
| Component | Refs anchor |
|---|---|
NodeAIRegistry (TipTap integration) | refs editor-ai §3. The TipTap extension half of the registry — wires per-node-type AI action declarations into editor commands, owns context resolution (which node, which selection, which document). No UI. The interface for this lives in Wave 1 (see below) so Wave 2 components don't block on Wave 4. |
Note on the dependency inversion. Wave 2's
SlashMenuandSelectionAIBarconsume the registry. To avoid blocking those, the registry is split:
- Wave 1 ships the interface —
editor/node-ai-registry.types.tsdeclaringNodeAIAction,NodeAIRegistry(the type), and a trivial in-memorycreateNodeAIRegistry()implementation Wave 2 components can target and Storybook stories can mock.- Wave 4 ships the TipTap integration — wires the registry into editor commands and node introspection. Wave 2 components don't change when this lands; they just receive a real registry instead of the in-memory one.
4. Acceptance criteria — when is a component done?
Before a desktop/soma screen wires a new (or refactored) component, the component must satisfy:
- Storybook story exists at
src/stories/<component>.stories.tsx. The story must cover, at minimum: default, hover, focus-visible, disabled, empty (if applicable), error (if applicable), and dark theme. For list rows / popovers also: keyboard navigation. - No inline shadow utilities. Only
shadow-elevatedfrom the token sweep is permitted, and only on modal + popup classes. DensityProvideraware. Reads density (even if v0 only emitsdense) so post-v0 cozy is a switch, not a rewrite.- Inline failure surfaces. Any primary action inside the component (form submit, dispatch, copy, etc.) renders failures inline. No
toast(...)calls for these paths. (Cross-cutting ADR-0005 §6.) - Keyboard-traversable if the component is a list, menu, or has multiple targets. Arrow keys + Enter; Esc closes.
- Color contrast ≥ AA at body size (
0.875rem/ 14px @ 16px root). Runpnpm --filter @soma/ui lintand the existing visual-regression story snapshots; the Wave 0 Storybook contrast test-runner check is the regression gate. - User-facing strings route through the i18n harness — no hardcoded English in JSX text or
aria-label. Use the translation-key pattern established in Wave 0. - Refs anchor cited in the component file's JSDoc header, so a future reader can find the lock pattern in one hop.
Wave 4's NodeAIRegistry substitutes "Storybook story" with "TipTap extension test exercising registration + action resolution for at least three node types".
5. Cutover order into desktop/soma
Once a wave's components meet acceptance, the dependent desktop/soma screens swap from their existing implementation in a separate PR per screen — branch is the gate, no feature flag.
Screen-cutover sequence, ordered by v0 product priority (PRD §7 step 6):
- Space settings shell + Bots tab (PRD §4.2 + §4.4). Requires
SettingsTabs,BotList,PeerAddressInput,CapabilityForm,Empty,DenseRow,Pill,DensityProvider. → Waves 0, 1, 2, 3. - Assistant tab (PRD §4.3). Adds
BackendSwitcher's settings counterpart. → reuses Wave 2. - Right area panel stack + chat panel (PRD §3, §4.5). Requires
Panel,PanelContainer,MentionPicker, refactored chat composer withBackendSwitcherin footer. - Editor surfaces —
SlashMenu,SelectionBubble,SelectionAIBar,InlineAIAcceptBar,InlineAIStream,NodeAIRegistry. Requires Waves 2–4. - Document column header — breadcrumb +
TreePopover. → Wave 2. - Spaces rail refactor (PRD §4.8). → Wave 1.
- Command palette wiring (cross-cutting). → Wave 2.
- Typing popup (PRD §4.7). Requires
PopupShell+ the existingCharDisplay. → Wave 3.
This sequence keeps "add a bot" — the v0 product priority — at the front. The first end-user-visible improvement is space settings, not the editor revamp.
6. Backend items that must land in parallel
Several frontend cutovers depend on the embedded daemon (backend/crates/daemon, backend/crates/agentd, backend/crates/soma-node) gaining new surface area:
list_space_botsonSomaHandle— required by space settings Bots tab (cutover 1).- Bot handshake / capability issue / registration through the napi facade — required by
CapabilityForm. - Bot status stream over the existing event channel — required for live status pills.
- ACP backend management (add, mark default, validate) — required by Assistant tab (cutover 2).
- ACP chat completion + streaming to a per-space chat thread — required by chat panel (cutover 3).
@bot:dispatch routing — required by §4.6.- Per-node AI action invocation routed through
soma-agentdACP — required by editor surfaces (cutover 4).
These are tracked separately. The scaffold plan assumes the daemon team confirms what already exists vs. what needs new endpoints; gaps are filed as backend issues, not blockers to wave-0/1/2 UI work. UI work in waves 0–2 has no daemon dependency and can begin immediately.
7. Out of scope for v0 scaffold
- Cozy / oversized density variants.
DensityProviderships, but only emits dense. - Theme switcher UI.
cmykstays the default;luxuryis exercised in Storybook for contrast testing only. - Localized strings beyond English. Only English is shipped in v0 — but the pattern is established now. Wave 0 lands a thin i18n harness (translation keys + a single English catalog;
react-intlori18next's minimal config — decision deferred to the Wave 0 PR, just pick one and commit). Every new component in waves 1–3 routes user-facing strings through the harness. The cost is small per component and avoids a costly retrofit when the second locale lands. - Visual regression infrastructure beyond what already exists in Storybook.
- Migration of every existing
desktop/somascreen — only the screens listed in §5 are in v0.
8. Open questions for the lock pass
overlays/command-palette.tsxrefactor scope. Should the cross-cutting palette and theTreePopover(anchored under breadcrumbs) share an implementation, or are they two surfaces that look similar but diverge? Recommend: sharedPopover+ sharedRowListprimitives, but distinct top-level components — sections and scopes differ enough that one prop-set would balloon.PillAPI breadth. Two variant axes — semantic (status/count/meta/default-marker) and tone (neutral/info/success/warn/error). Lock the matrix before the component lands so consumers don't invent variants ad-hoc.PanelContainersplit gesture. Drag-to-split is in the PRD but has no clean refero analog. Keyboard fallback (⌘\\to split the focused panel) might be the safer v0 path with drag added in v0.1.InlineAIStreamandai-chat's existing streaming code. Today'schat/ai-message.tsxpresumably handles streaming inside the chat. Confirm whether the newInlineAIStreamshares itsThinking…pill subcomponent with the chat (it should, per refs editor-ai §5) or is built fresh.- CharDisplay extraction work. Whether
tapia/char-display.tsxneeds any genuine change or just an audit. If just an audit, drop it from Wave 1 and treat it as already done.
Once this scaffold is locked, the next deliverable is Wave 0 — the token sweep PR — followed by Wave 1 component PRs in parallel.