Instruction file imported from dragoscv/mmo (
.github/instructions/base-ui.instructions.md). Copyright stays with the author.
Base UI primitives (@mmo/ui, ADR-0008 D6)
Rules
- Import primitives from
@mmo/ui(tsconfig path alias), never from@radix-ui/*orradix-ui. Web'scomponents/ui/*are thin wrappers (withAsChildHOC,NativeSelectfor<option>callers) — extend them, don't fork. - Composition:
render={<Link href="…" />}(Base UIuseRender), NOTasChild. - State attributes:
data-open,data-checked,data-highlighted,data-starting-style,data-ending-style. Never select ondata-state="open". - Callbacks carry a second arg:
onOpenChange(open, eventDetails),onValueChange(value, eventDetails). - Families and names are shadcn-compatible: layout (
AppShell,Sidebar*,BottomTabBar,Page*), feedback (Skeleton*,EmptyState+NotSignedInState/NoCompanionState/ErrorState/NoResultsState,Progress,ProgressJob,Toaster), data (DataTableon TanStack Table 9), overlays (Dialog,Sheet,AlertDialog,Popover,Tooltip,DropdownMenu,ContextMenu,CommandDialog,ShortcutsOverlay), forms (Input,Select,Checkbox,Switch,Slider,RadioGroup,ToggleGroup,Field*), theme (ThemeProvider,useThemePrefs,ThemeSettings), motion presets, hooks, shortcuts registry. - Motion:
motion13 presets (fade,rise,scale,slideUp,stagger) + React 19.3<ViewTransition>on web routes;<PageTransition>in mixai/companion. Never addframer-motion. - One shortcuts registry per app:
registerShortcut/useRegisterShortcut. Web layout already mounts ONEShortcutsOverlay(idshortcuts-overlay) — trigger viagetShortcuts().find(id).handler(), never mount a seconduseShortcutsOverlay(duplicate registry id). DataTable: TanStack Table v9 —tableFeatures({...})+useTable({ features, columns, data }); there is NOuseReactTable/getCoreRowModel. State is a TanStack Store (table.state.x).cmdk1.1: root isCommand, sub-partsCommand.Input/List/Item/Group/Empty; items exposedata-selected="true"; async results must passkeywords={[query]}or the client filter hides them.
Gotchas (exact strings)
Cannot read properties of null (reading 'useState')→ duplicated React. Dedupereact,react-dom,motion,@base-ui/react,lucide-reactto the consuming app'snode_modules(web:next.config.tsturbopack.resolveAlias+vitest.config.tsresolve.dedupe; Vite apps:resolve.dedupe+ alias). Inserver/ui/tsconfig.jsonmap"react": ["../node_modules/@types/react"](mapping to the JS package → TS7016 "no declaration file" across packages/ui).- Tests in jsdom: Base UI
Select/Menudo NOT open onfireEvent.click(trigger)→ render withdefaultOpen;Select.Itemcommits only onkeyDown Enterafter focus. cmdk group headings are notrole=heading→ query[cmdk-group-heading].ChoiceCardswith description have name "Label Description". - ESLint
react-hooks/set-state-in-effectandreact-hooks/purityare ERRORS on web: reset derived state in the change handler, noDate.now()in render (compute in SQL or pass from caller). packages/uihas its ownnode_modules/react; do not import types from it in consumers.
Verify
pnpm -C packages/ui typecheckandpnpm -C packages/ui test(RTL + jsdom) via a hiddenStart-Process pwsh -File .copilot-tmp/ui-verify.ps1— the shared terminal prints only "RUN" then dies.- Consumer typecheck:
pnpm -C apps/web typecheck/pnpm -C apps/mixai typecheck/pnpm -C server ui:typecheck. - Visual:
/dev/uicatalog on web (MIXAI_DEV_UI=1in prod),pnpm -C apps/web e2especsui-catalog,theme-matrix,a11y.