Skip to content

apps/internal → @repo/ui Design-System Adoption

Status: In progress — foundation + pilot landed; phased rollout planned Author: David Boone (research-assisted) Date: 2026-07-02 Branch: claude/trusting-lamport-1f2vxc · PR base: staging


apps/internal barely uses the shared design system, and what it does use is silently broken:

  • Only ~6 files imported @repo/ui components (Button, toast, a few whole panels — AuditLogViewer, OrganizationMembers, LangfusePanel, SystemConfigEditor, UserSearch). Everything else is hand-rolled Tailwind.
  • Internal forked its own partial --om-* tokens (src/styles/tokens.css + an inline @layer base block) and never imported @repo/ui/tokens/tokens.css. Every DS token the components reference — --om-success-light, --om-panel-2, --om-radius-sm, --om-line, --om-orange-press, … — was undefined in internal. So the @repo/ui components internal already renders were showing with missing variables (transparent/invalid styles).
  • Internal’s global.css had no @source directive for packages/ui, so Tailwind never generated the DS components’ utility classes either.
  • Internal even maintains a parallel local mini-DS in src/components/ui/ (GapBtn, GapPill, GapStat, GapSection) that duplicates Button, StatusBadge, and StatTile.

The reference consumer, apps/webapp, does it correctly: it imports @repo/ui/tokens/tokens.css and @source-scans packages/ui. Internal simply never adopted the foundation. This is both a consistency gap and a latent rendering bug.

Make apps/internal render on the shared design system: canonical --om-* tokens as the single source of truth, @repo/ui primitives instead of hand-rolled Tailwind, and the local ui/ shims retired. One visual language across the platform, and the existing broken DS components fixed as a side effect.

Non-goals: redesigning any screen (this is a like-for-like re-skin onto the DS, not a UX change); touching @repo/ui itself beyond additive needs; changing behavior or copy.

Phase 0 — token foundation (src/styles/global.css):

  • @import '@repo/ui/tokens/tokens.css' (real path — Tailwind v4 ignores the export map) so the canonical --om-* tokens are defined and win for shared tokens; internal’s app-specific tokens are kept.
  • @source directives for the app and packages/ui/src so DS component classes are emitted.
  • Verified: astro build green; the previously-missing DS tokens are now in the emitted CSS bundle. This fixes internal’s existing @repo/ui usage too.

Phase 1 — pilot (the onboarding UI, as the reference pattern):

  • OnboardingChecklist.tsx: StatusBadge (intent-mapped), Button, Skeleton, Banner, DS token classes — replacing hand-rolled pills/buttons/gray palette.
  • NewOrgForm.tsx: Field + controlBase + Select/Textarea/Checkbox from @repo/ui/form; Banner for env warning + errors; Button for submit.

Caveat that governs the rest of the rollout: adopting the DS tokens restyles internal to the DS palette/spacing. A headless astro build validates that CSS compiles and bundles — it does not validate pixels. Every phase below needs a visual-QA pass (dev server or screenshot) before merge.

Hand-rolled pattern in internal Replace with Source
<span className="rounded … bg-*-100 text-*-800"> status pill StatusBadge (intent, dot, size) + statusToIntent @repo/ui
<button className="rounded bg-… "> Button (variant, size, loading, href) @repo/ui
local GapBtn / GapPill / GapStat Button / StatusBadge or Chip / StatTile @repo/ui
<div className="rounded border bg-white p-…"> card DS token classes (bg-[var(--om-panel)] border-[var(--om-line)]) or Surface @repo/ui
<input>/<select>/<textarea> + labels Field + controlBase / Select / Textarea / Checkbox / Switch @repo/ui/form
loading spinners / “Loading…” Skeleton @repo/ui
empty-state divs EmptyState @repo/ui
page-level notices / env banner Banner (inline for in-card) @repo/ui
ad-hoc modals / confirms (window.confirm) Modal / ConfirmDialog @repo/ui
tables built from <table> Table / DataTable @repo/ui
text-gray-* / bg-gray-* / bg-white --om-text* / --om-panel* / --om-bg tokens tokens
AdminPageHeader.astro PageHeader @repo/ui (nav)

4. Inventory (migration targets, measured)

Section titled “4. Inventory (migration targets, measured)”
  • 23 React components, 24 .astro files.
  • 12 files with raw <button>, 11 with hand-rolled status/badge divs, 16 with the hardcoded gray palette — the prime targets.
  • Local ui/ shims to retire: GapBtn, GapPill, GapStat, GapSection.
  • Biggest raw-Tailwind components (line count):
Component Lines Notes
OrganizationConnectionsPanel.tsx 779 The onboarding connections centerpiece — high value
OrganizationsList.tsx 759 Table + filters + status → DataTable/StatusBadge/FilterChip
admin/FeatureFlagDashboard.tsx 460 Already imports @repo/ui/form partially — finish it
OrganizationPipelineSection.tsx 399
billing/BillingSummary.tsx 376 (distinct from the @repo/ui BillingSummary)
billing/BillingConfigModal.tsx 251 → Modal / ConfirmDialog
OrganizationOverview.tsx 222
OrganizationPromptSection.tsx 217

Each phase is independently shippable, gated on astro build green and a visual-QA pass. Order by value ÷ risk.

  • P0 — Foundation. ✅ Done (tokens + @source).
  • P1 — Pilot. ✅ Done (onboarding UI).
  • P2 — Retire the local ui/ shims. Replace GapBtn/GapPill/GapStat/ GapSection usages with Button/StatusBadge/StatTile/Surface, then delete src/components/ui/. Removes the parallel mini-DS so there’s one way to do it. Small, high-consistency.
  • P3 — Shared primitives sweep (mechanical). Across all components: raw status pills → StatusBadge; raw <button> → Button; text-gray-*/ bg-gray-*/bg-white → tokens. Touches ~16 files but each change is local and low-risk; do a few files per PR with QA.
  • P4 — Structural components. The big ones (§4): OrganizationsList → DataTable/FilterChip; OrganizationConnectionsPanel → Field/Button/ StatusBadge/Modal (coordinate with the connections-panel honesty fixes from the onboarding plan); billing modals → Modal/ConfirmDialog; forms → @repo/ui/form. One component per PR.
  • P5 — Layout/nav. AdminPageHeader → PageHeader; consider AppShell/ RailNav for the sidebar; wrap the app in a Surface. Highest visual blast radius — do last, with the most QA.
  • Verification per PR: astro check (types) + astro build (CSS/Tailwind resolves and bundles) + a manual visual pass. The build is necessary but not sufficient — it cannot catch a wrong-but-valid color.
  • Import conventions (match webapp): display components from @repo/ui; form controls from @repo/ui/form (note: top-level Input is the Astro component — use controlBase on a native <input> inside Field, or @repo/ui/form).
  • Fanning out: P3 is a good candidate for a multi-agent migration workflow (one agent per file, mechanical pattern swaps, astro build gate) once the pattern is settled; P4/P5 are per-component and better done deliberately.
  • Token reconciliation (follow-up): internal’s tokens.css + inline @layer base still define a few --om-* overrides that now lose to the DS. Once the sweep confirms nothing depends on the fork values, thin that file to only genuinely internal-specific tokens (mirrors webapp’s “gradually thinned local block”).
  1. Whole-app restyle, unverifiable headlessly — the token adoption is the point, but pixels need human QA. Mitigation: per-phase QA gate; foundation is one revertible commit.
  2. Token cascade — DS vs. internal fork values resolve by import order (DS imported last → wins for shared tokens). If a screen looks off, it’s almost always a token override in the fork; reconcile per §6.
  3. @source cost — scanning packages/ui grows the Tailwind content set; build stayed fast (~13s), so no concern.
  4. Behavioral parity — DS controls have their own a11y/keyboard behavior; re-skinning must not change form semantics. Covered by visual + interaction QA.