← All decisions

Design system in `packages/ui`, mobile-first member-first shell, modal-for-edit, plain-text first

accepted

0013 — Design system in packages/ui, mobile-first member-first shell, modal-for-edit, plain-text first

  • Status: accepted
  • Date: 2026-05-07
  • Deciders: Derek

Context

After Phase 1.5 we had 6 admin pages, each implementing layout, forms, errors, and “loading…” markers from scratch. The class-shape duplication across *.module.css was already visible (.field, .item, .error, .meta repeating with subtle drift). Continuing feature-first guarantees that: (a) buttons stay unstyled because the next sprint always feels more important; (b) the app shell gets bolted on under deadline at feature ~6; (c) tenant theming becomes hard to retrofit because the delivery of a tenant’s accent has nowhere to land.

The remediation is a shared design system and a shell — both of which need to land before any further feature lift if we want feature velocity to compound rather than slow.

Decision

We ship a design system in a new workspace package, @ark/ui, with three responsibilities:

  1. Tokens. The structural CSS (variables.css) and the semantic theme (themes/default.css) live in packages/ui/src/styles/. Both apps/admin and apps/site import the same token bundle (@ark/ui/styles/global.css). When tokens drift, they drift in one place.
  2. Primitives. A small but curated set of React components — buttons, fields, cards, lists, modals, toasts, etc. Each ships with a Vitest behavior spec and a Ladle story. Promotion threshold: a CSS or layout shape becomes a primitive on its third actual appearance in the codebase, not the second, and not on the first time someone predicts a third. “This will come up again” is a PR note, not a green light to promote. Premature promotion is a tax we won’t charge.
  3. App shell. A single <AppShell> component drives navigation: bottom-tab on mobile (≤768px), topbar on desktop. The shell is mobile-first and member-first. The primary affordances are the things the average member uses (forum, calendar, profile); admin features sit behind progressive disclosure via the shell’s separate adminItems slot. This intentionally inverts the usual enterprise default — ark serves arts-org members who are more likely to interact via phone than dashboard. Discipline rule: admin-only features go through adminItems, never inline with member nav. Mixing them erodes the member-first surface under feature pressure; the slot exists to prevent that.

We adopt modal-for-edit, page-for-view. Edit affordances open in a <Modal> so the user keeps their context. View pages stay routable so they get share-link affordances (<ShareLink>) and survive deep linking.

We adopt plain text first, no markdown. Forum bodies, wiki bodies, and reply bodies are plain <textarea> until WYSIWYG arrives in a later phase. Markdown is excluded — too high a learning curve for non-technical members.

For visual review we use Ladle, not Storybook. Lean, zero-config, fits the “just enough” posture.

What this decision does not include

  • Tenant branding schema delivery (organizations.brand_tokens, runtime theme injection). Future ADR (Phase 2b workstream D).
  • Capacitor mobile shell. Future ADR (Phase 2c).
  • Public-site customization layer (cargo-style layout builder). Deferred to Phase X.
  • Visual regression / screenshot diffs. Separate ADR when we have evidence the behavior tests aren’t enough.
  • A <Composer> / mention autocomplete component. Deferred until WYSIWYG.

Consequences

Easier:

  • Every new feature page starts from primitives, not blank <main> tags.
  • Theme tweaks happen in one file, not N module CSS files.
  • A junior contributor (or AI agent) can scaffold a feature page in a day rather than re-deriving styles.
  • Mobile push notifications (Phase 2c) land on top of an already-mobile-first shell, not a desktop UX retrofitted onto a phone.

Harder:

  • The next 1–2 weeks of work is design-system scaffolding, not user-visible features. Slower-feeling feature delivery in the short term to enable faster delivery for the year.
  • We have to resist promoting primitives on second appearance (“this might come up again”). Three-occurrence rule is the discipline.

When we’d revisit this

We re-open this decision if any of these happen:

  • The shared component package starts dragging admin down. Specifically, if installing @ark/ui makes the admin bundle measurably bigger than what’s actually used — that means we’re shipping components nobody asked for, and the package needs trimming or splitting.
  • The local design-review tool becomes slow to start. Ladle is meant to be friction-free; if it takes more than a few seconds to boot once a tenant has a lot of stories, we switch to a per-component model so each story loads on demand instead of all at once.
  • A tenant asks for a public-site layout we can’t build with what we have. That isn’t a failure of this decision — it’s the trigger to start the deferred Phase X work (the public-site layout builder). When that request comes, we’ll know we’re ready for it.

Alternatives considered

  • Tailwind. Rejected per recorded user preference. Token-based CSS Modules give us the same level-of-abstraction benefit without inheriting Tailwind’s aesthetic and learning-curve costs.
  • Storybook. Rejected for Ladle. Storybook is more powerful but the configuration footprint is real; Ladle does what we need now.
  • Continue feature-first, primitives later. Rejected. The duplication trajectory at 6 pages already demonstrates this scales sub-linearly; the cost of retrofitting at 12 pages is much higher.
  • Desktop-first nav. Rejected. Per locked decisions in 2026-05-07-phase-2-foundation-eval-and-plan.md, the member surface is the primary surface, and members are mobile-first. Inverting enterprise defaults is a deliberate product choice.

When the user produces a value they need to share or copy elsewhere (an invitation link, a deploy webhook URL, a generated API key), use <Modal> + <ShareLink> rather than rendering the value inline. The modal makes the result deliberate (the user has to actively dismiss it), gives ShareLink the visual prominence it needs, and provides a stable slot for context like “sent to [email protected]” or “expires in 7 days.”

The shared <InvitationLinkModal> in apps/admin/src/components/ is the current first-and-only consumer of this pattern. Per the third-appearance promotion rule, when this shape lands a third time, it lifts to @ark/ui as a generic <ActionLinkModal> primitive.

Addendum 2026-05-21 — Nav refresh: UserMenu + AppShell reshape

The nav refresh (see docs/superpowers/specs/2026-05-21-nav-refresh-design.md) introduced one new primitive and reshaped one existing one. Both are now part of the design-system contract:

  • UserMenu primitive (packages/ui/src/primitives/UserMenu/) — avatar trigger + dropdown panel (header with name + email, item list, divider, sign-out footer). Composes the existing Avatar for the trigger; no new initials logic. Click-outside + Escape dismiss; aria-haspopup="menu". The first consumer is apps/admin/src/components/UserMenuSlot.tsx; future apps (mobile shell, etc.) reuse the primitive.
  • AppShell props reshaped: the previous adminItems: NavItem[] prop has been replaced by a single adminLink?: NavItem — the new IA has one top-level Admin entry that drills into a dedicated /admin destination with its own sub-tabs. A new userMenuSlot?: ReactNode is rendered in both desktop + mobile top bars. The existing userSlot (🔔 + View-site cluster) now hides at mobile widths via CSS; consumers are expected to fold those entries into the user menu on narrow screens. The mobile bottom-tabbar drops the admin entry entirely — admins reach /admin via the user menu on mobile, satisfying the mobile-first/member-first tenet.

The “Admin destination” (horizontal sub-tabs Members / CMS / Pages / Branding / Publishing / Integrations) lives in apps/admin/src/components/AdminShell.tsx — app-local for now, per the third-appearance promotion rule (only one consumer).

References

  • docs/superpowers/plans/2026-05-07-phase-2-foundation-eval-and-plan.md — analysis and locked decisions backing this ADR
  • docs/superpowers/plans/2026-05-07-phase-2a-design-system.md — the implementation plan executed under this decision
  • docs/superpowers/specs/2026-05-21-nav-refresh-design.md — nav refresh design
  • docs/superpowers/plans/2026-05-21-nav-refresh.md — nav refresh implementation plan
  • ADR 0011 — internalize is reference, not exemplar (the discipline applied to lifts)