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:
- Tokens. The structural CSS (
variables.css) and the semantic theme (themes/default.css) live inpackages/ui/src/styles/. Bothapps/adminandapps/siteimport the same token bundle (@ark/ui/styles/global.css). When tokens drift, they drift in one place. - 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.
- 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 separateadminItemsslot. 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 throughadminItems, 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/uimakes 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.
Action-link UX
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 alice@example.com” 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.
References
docs/superpowers/plans/2026-05-07-phase-2-foundation-eval-and-plan.md— analysis and locked decisions backing this ADRdocs/superpowers/plans/2026-05-07-phase-2a-design-system.md— the implementation plan executed under this decision- ADR 0011 — internalize is reference, not exemplar (the discipline applied to lifts)