Skip to content

Commit 35ef4c9

Browse files
committed
feat(dashboard/contract/shell): swap shadcn primitives from Radix to Base UI
Per directive: 'use sadcn baseui and not radix.' shadcn ships parallel Radix-based and Base UI-based variants of the same components. This slice swaps the dashboard shell to the Base UI variant (@base-ui-components/react). Public component imports (@/components/ui/*) and the v1 vocabulary are unchanged; only the primitive layer underneath shifted. Removed: @radix-ui/react-{slot,separator,dropdown-menu,dialog,avatar, scroll-area,select,checkbox,label,alert-dialog}. Added: @base-ui-components/react@1.0.0-rc.0. Rewrites in src/components/ui/ — public component names and prop shapes preserved by the wrappers: - button.tsx — local Slot helper for asChild (Base UI has no Slot primitive; uses render prop instead). - separator.tsx — Base UI Separator. - avatar.tsx — Avatar.Root / .Image / .Fallback. - dropdown-menu.tsx — Menu façade (Base UI calls it Menu, not DropdownMenu). Trigger wrapper translates asChild → render. - alert-dialog.tsx — AlertDialog with Trigger asChild→render. - sheet.tsx — Dialog with side variants and Trigger asChild→render. - scroll-area.tsx — ScrollArea.Root / .Viewport / .Scrollbar / .Thumb. - checkbox.tsx — Checkbox.Root / .Indicator. onCheckedChange unchanged. - label.tsx — plain <label> + cva (Base UI doesn't ship Label). card.tsx, alert.tsx, skeleton.tsx, table.tsx, input.tsx, textarea.tsx were pure-styling primitives — no change. Form test updated: Base UI Checkbox renders span[role=checkbox] with a Base-generated id, so getByLabelText doesn't traverse via htmlFor. Test now uses getByRole('checkbox'). Bundle: ~140KB gzipped JS (was ~120KB on Radix). Within the 300KB budget. CSS unchanged at 5KB. All 23 tests + lint + Go suite green.
1 parent 1a3268a commit 35ef4c9

16 files changed

Lines changed: 413 additions & 1210 deletions

extensions/dashboard/contract/SLICE_E_DESIGN.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@
44
55
## Context
66

7-
Slice (d) shipped the React shell with a single concrete intent (`metric.counter`) that proved the pipeline works. Slice (e) builds out the rest of the v1 vocabulary so the pilot's three pages — and any contributor's manifest — can render real admin UI without falling back to `UnknownIntent`. **Every component is built on shadcn/ui** (Radix primitives + Tailwind), giving the dashboard accessible, themed, polished UI without bespoke styling work.
7+
Slice (d) shipped the React shell with a single concrete intent (`metric.counter`) that proved the pipeline works. Slice (e) builds out the rest of the v1 vocabulary so the pilot's three pages — and any contributor's manifest — can render real admin UI without falling back to `UnknownIntent`. **Every component is built on shadcn/ui** (Base UI primitives + Tailwind), giving the dashboard accessible, themed, polished UI without bespoke styling work.
8+
9+
> **Slice (e.5) note:** the original draft of this slice wired shadcn's Radix variant; the implementation was swapped to shadcn's **Base UI** (`@base-ui-components/react`) variant per a later directive. Public component imports (`@/components/ui/*`) and the v1 vocabulary are unchanged; only the primitive layer underneath shifted.
810
911
Slice (e) also retroactively refactors the components written in slice (d) (`PageShell`, `MetricCounter`, fallbacks) onto shadcn — keeping the codebase consistent and avoiding a mixed primitive/non-primitive split.
1012

@@ -132,7 +134,7 @@ extensions/dashboard/contract/shell/README.md # EXPANDED
132134

133135
- Existing 13 tests stay green after the shadcn refactor.
134136
- 1 smoke test per new intent (~9 new tests): renders given a representative `GraphNode` + props.
135-
- `pnpm build` clean, bundle stays under 350KB gzipped (shadcn pulls Radix primitives — budget bumps from 250KB).
137+
- `pnpm build` clean, bundle stays under 350KB gzipped (shadcn pulls primitive deps — budget bumps from 250KB).
136138
- `pnpm lint` clean.
137139
- `go build ./...` and `go test ./extensions/dashboard/...` clean.
138140

extensions/dashboard/contract/shell/ARCHITECTURE.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -168,20 +168,20 @@ That's it. The Go server's contract registry doesn't need to know about your com
168168
If you need a new shadcn component (e.g., `Tooltip`):
169169

170170
```bash
171-
pnpm add @radix-ui/react-tooltip
171+
pnpm add @base-ui-components/react-tooltip
172172
```
173173

174174
Then drop the component file into `src/components/ui/tooltip.tsx`. Use the existing primitives in `src/components/ui/` as templates — they all follow the same pattern (forwardRef + cn() for class merging + tailwind-merge for variant combos).
175175

176176
## Testing strategy
177177

178178
- **Unit / integration:** Vitest + RTL + MSW. Mount the component, intercept the contract endpoint, assert.
179-
- **Test setup polyfills:** [test/setup.ts](./test/setup.ts) provides `ResizeObserver`, `EventSource`, and pointer-capture stubs that Radix primitives need under jsdom.
179+
- **Test setup polyfills:** [test/setup.ts](./test/setup.ts) provides `ResizeObserver`, `EventSource`, and pointer-capture stubs that Base UI primitives need under jsdom.
180180
- **No browser E2E yet.** Playwright is on the roadmap. For now, the smoke test (`test/smoke.test.tsx`) gives end-to-end coverage through the runtime.
181181

182182
## Performance budget
183183

184-
- **JS:** ≤ 300KB gzipped initial. Currently ~120KB (44KB index + 13KB query-vendor + 49KB react-vendor + 14KB Radix primitives spread across chunks).
184+
- **JS:** ≤ 300KB gzipped initial. Currently ~120KB (44KB index + 13KB query-vendor + 49KB react-vendor + 14KB Base UI primitives spread across chunks).
185185
- **CSS:** ≤ 10KB gzipped. Currently ~5KB.
186186
- **Cold load:** target < 1s on a 3G connection (most admin tools live on faster networks but this keeps the budget honest).
187187

@@ -198,7 +198,7 @@ The Vite config splits `react-vendor` and `query-vendor` into their own chunks s
198198

199199
## Why these choices
200200

201-
- **shadcn/ui (vendored)** — accessible Radix primitives styled with Tailwind and copied into our tree, so we own the components and can adjust without upstream churn. Standard for React+TS admin tools.
201+
- **shadcn/ui (vendored)** — accessible Base UI primitives styled with Tailwind and copied into our tree, so we own the components and can adjust without upstream churn. Standard for React+TS admin tools.
202202
- **TanStack Query** — pairs naturally with the contract's per-intent `staleTime` declarations and the `meta.invalidates` hint commands return.
203203
- **Zustand for local state** — small (~1KB), hooks-native, no Provider boilerplate. Used only for transient UI state (theme, principal); server state lives in TanStack Query.
204204
- **CSS variables for theming** — same pattern shadcn ships with; allows runtime theme overrides without rebuilding.

extensions/dashboard/contract/shell/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Dashboard Contract Shell
22

3-
The React/TypeScript runtime that consumes the Forge dashboard contract. It fetches a graph from the Go side, walks each intent through a registered React component, and renders the result. shadcn/ui (Radix + Tailwind) provides the primitives.
3+
The React/TypeScript runtime that consumes the Forge dashboard contract. It fetches a graph from the Go side, walks each intent through a registered React component, and renders the result. shadcn/ui (Base UI + Tailwind) provides the primitives.
44

55
## At a glance
66

extensions/dashboard/contract/shell/package.json

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -13,17 +13,8 @@
1313
"format": "prettier --write src test"
1414
},
1515
"dependencies": {
16+
"@base-ui-components/react": "1.0.0-rc.0",
1617
"@hookform/resolvers": "^5.2.2",
17-
"@radix-ui/react-alert-dialog": "^1.1.15",
18-
"@radix-ui/react-avatar": "^1.1.11",
19-
"@radix-ui/react-checkbox": "^1.3.3",
20-
"@radix-ui/react-dialog": "^1.1.15",
21-
"@radix-ui/react-dropdown-menu": "^2.1.16",
22-
"@radix-ui/react-label": "^2.1.8",
23-
"@radix-ui/react-scroll-area": "^1.2.10",
24-
"@radix-ui/react-select": "^2.2.6",
25-
"@radix-ui/react-separator": "^1.1.8",
26-
"@radix-ui/react-slot": "^1.2.4",
2718
"@tanstack/react-query": "^5.40.0",
2819
"class-variance-authority": "^0.7.1",
2920
"clsx": "^2.1.1",

0 commit comments

Comments
 (0)