← 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

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

Consequences

Easier:

Harder:

When we’d revisit this

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

Alternatives considered

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