Skip to content

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 and text-xs (12px) for caps / hint text. Earlier waves shipped custom --text-body / --text-ui-sm / --text-ui-xs tokens at exact 14 / 13 / 11px from a density audit, but they were removed when tailwind-merge was found to silently drop them when combined with text-{color} classes in cn(...) 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 — use row-avatar instead.
    • row-avatar — 2.5rem (40px), for rows with avatars / file-type glyphs (mention rows, attachments, members, bots). Matches DenseRow's existing h-10 leading 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 existing surface-card / glass-panel utilities stay reachable as surface-card-legacy / glass-panel-legacy so 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 -legacy utilities get deleted in a sweep PR.
  • DaisyUI theme. Audit cmyk (default) and luxury for 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 componentExisting in @soma/uiPlan
PanelContainernoneNew. panels/panel-container.tsx.
PanelnoneNew. panels/panel.tsx.
SlashMenunone (closest precedent is overlays/command-palette.tsx)New. editor/slash-menu.tsx.
MentionPickernoneNew. editor/mention-picker.tsx.
SelectionBubbleoverlays/bubble-toolbar.tsx (65 lines)Refactor. Replace internals; rename to editor/selection-bubble.tsx.
BotListnone (closest: lists/roster-item.tsx)New. lists/bot-list.tsx. Built on DenseRow.
CapabilityFormnoneNew. forms/capability-form.tsx.
BackendSwitcherforms/ai-model-selector.tsx (107 lines)Refactor. Move to chat/backend-switcher.tsx; reshape into a composer-footer chip per refs main §5.
PeerAddressInputnoneNew. forms/peer-address-input.tsx.
SpacesRailnone explicit; layout shell in layout/desktop-shell.tsx may host oneNew. nav/spaces-rail.tsx.
CharDisplaytapia/char-display.tsxKeep. Extract any inline shadows; verify density tokens.
DensityProvidernoneNew. primitives/density-provider.tsx.
EmptynoneNew. primitives/empty.tsx with variant prop (full / compact / filter).
DenseRowpartial in lists/roster-item.tsxRefactor. Generalize roster-item's slot model into lists/dense-row.tsx.
CommandPaletteoverlays/command-palette.tsx (166 lines)Refactor. Re-section per refs space-lifecycle §4.8: Recent docs → Spaces → Documents → Commands.
TreePopovernoneNew. nav/tree-popover.tsx. Built on react-complex-tree — installed in Wave 2 alongside this component.
PopupShellprecedent: layout/window-chrome.tsxRefactor. Extract to popup/popup-shell.tsx; expose the 4-glyph cluster from refs popup-window §6.
SelectionAIBarnoneNew. editor/selection-ai-bar.tsx.
InlineAIAcceptBarnoneNew. editor/inline-ai-accept-bar.tsx.
NodeAIRegistrynoneNew. editor/node-ai-registry.ts (logic, no UI).
InlineAIStreamnoneNew. editor/inline-ai-stream.tsx.

Gaps found during this pass — proposed additions ​

ComponentWhyFile
SettingsTabsLocked by refs space-lifecycle §4.2 (horizontal pill/underline tabs) but missing from PRD §6. Reused by every settings page.nav/settings-tabs.tsx
PillStatus 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 ​

ItemDescription
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 fixDrop 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.
DensityProviderReact context exposing density: 'dense' | 'cozy'. v0 ships dense only; the provider exists so post-v0 cozy is a config flip.
i18n harnessPick + 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 docUpdate 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) ​

ComponentRefs anchor
Emptyrefs files-density §2
DenseRow (refactor from roster-item)refs files-density §4
Pill (new primitive)Status patterns in refs assistant-bots §3
PeerAddressInputrefs main §4 step 1
CharDisplay (extract inline styles)already locked in tapia/char-display.tsx
SpacesRailrefs 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) ​

ComponentRefs anchor
SlashMenurefs editor §1 + ADR-0005 §11 amendment
SelectionBubble (refactor from bubble-toolbar)refs editor §2
MentionPickerrefs main §3
BackendSwitcher (refactor from ai-model-selector)refs main §5
TreePopoverrefs space-lifecycle §3
CommandPalette (refactor existing)refs space-lifecycle §3
SettingsTabsrefs space-lifecycle §2
SelectionAIBarrefs editor-ai §1

Wave 3 — Composites (depend on Waves 1–2) ​

ComponentRefs anchor
PanelPRD §3 + refs main §2
PanelContainer(composes Panel)
BotListrefs assistant-bots §3
CapabilityFormrefs main §4 step 2
InlineAIAcceptBarrefs editor-ai §4
InlineAIStreamrefs editor-ai §5
PopupShell (refactor from window-chrome)refs popup-window §2

Wave 4 — Logic / glue ​

ComponentRefs 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 SlashMenu and SelectionAIBar consume the registry. To avoid blocking those, the registry is split:

  • Wave 1 ships the interface — editor/node-ai-registry.types.ts declaring NodeAIAction, NodeAIRegistry (the type), and a trivial in-memory createNodeAIRegistry() 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:

  1. 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.
  2. No inline shadow utilities. Only shadow-elevated from the token sweep is permitted, and only on modal + popup classes.
  3. DensityProvider aware. Reads density (even if v0 only emits dense) so post-v0 cozy is a switch, not a rewrite.
  4. 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.)
  5. Keyboard-traversable if the component is a list, menu, or has multiple targets. Arrow keys + Enter; Esc closes.
  6. Color contrast ≥ AA at body size (0.875rem / 14px @ 16px root). Run pnpm --filter @soma/ui lint and the existing visual-regression story snapshots; the Wave 0 Storybook contrast test-runner check is the regression gate.
  7. 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.
  8. 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):

  1. Space settings shell + Bots tab (PRD §4.2 + §4.4). Requires SettingsTabs, BotList, PeerAddressInput, CapabilityForm, Empty, DenseRow, Pill, DensityProvider. → Waves 0, 1, 2, 3.
  2. Assistant tab (PRD §4.3). Adds BackendSwitcher's settings counterpart. → reuses Wave 2.
  3. Right area panel stack + chat panel (PRD §3, §4.5). Requires Panel, PanelContainer, MentionPicker, refactored chat composer with BackendSwitcher in footer.
  4. Editor surfaces — SlashMenu, SelectionBubble, SelectionAIBar, InlineAIAcceptBar, InlineAIStream, NodeAIRegistry. Requires Waves 2–4.
  5. Document column header — breadcrumb + TreePopover. → Wave 2.
  6. Spaces rail refactor (PRD §4.8). → Wave 1.
  7. Command palette wiring (cross-cutting). → Wave 2.
  8. Typing popup (PRD §4.7). Requires PopupShell + the existing CharDisplay. → 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_bots on SomaHandle — 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-agentd ACP — 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. DensityProvider ships, but only emits dense.
  • Theme switcher UI. cmyk stays the default; luxury is 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-intl or i18next'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/soma screen — only the screens listed in §5 are in v0.

8. Open questions for the lock pass ​

  1. overlays/command-palette.tsx refactor scope. Should the cross-cutting palette and the TreePopover (anchored under breadcrumbs) share an implementation, or are they two surfaces that look similar but diverge? Recommend: shared Popover + shared RowList primitives, but distinct top-level components — sections and scopes differ enough that one prop-set would balloon.
  2. Pill API 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.
  3. PanelContainer split 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.
  4. InlineAIStream and ai-chat's existing streaming code. Today's chat/ai-message.tsx presumably handles streaming inside the chat. Confirm whether the new InlineAIStream shares its Thinking… pill subcomponent with the chat (it should, per refs editor-ai §5) or is built fresh.
  5. CharDisplay extraction work. Whether tapia/char-display.tsx needs 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.