Must know
Five things that catch most people on their first build. Read this before you reach for a primitive.
1 · Subpath imports
Always import per-component. The package's exports map routes each subpath to the correct platform variant.
// ✅ tree-shakes; ships only Button
import { Button } from '@mihcm/ui/Button';
// ❌ pulls the barrel; ships every primitive
import { Button } from '@mihcm/ui';2 · One prop API, two runtimes
The same source line works on web and on native. The bundler picks Button.tsx for web builds and Button.native.tsx for React Native via the react-native export condition. You never reach for a .web. or .android. file directly.
// Same import on Next.js, Vite, and Expo.
import { Button } from '@mihcm/ui/Button';The web variant uses Radix primitives + Tailwind 4 classes. The native variant uses @rn-primitives/* + NativeWind. Same props, same accessibility contract, same visual treatment.
In Next.js App Router, these web entrypoints are client boundaries. You can
render static component trees from a Server Component, but event handlers,
controlled state, render functions, icon component maps, table columns, chart
formatters, and browser APIs belong in a small 'use client' wrapper.
Full guide: Client and Server Components.
3 · Theming with .dark
Dark mode is a class on <html>. Wrap your app in <ThemeProvider> on native; on the web, any theme-toggle library works as long as it adds and removes .dark on the document element.
Render a pre-paint bootstrap script in your root layout so the first frame is in the right theme — the canonical pattern reads localStorage, falls back to prefers-color-scheme, and adds .dark to the <html> element before paint. (See the docs site's own src/apps/docs/app/layout.tsx for the exact snippet.)
Tokens defined inside .dark { ... } (and only there — no competing @media (prefers-color-scheme: dark)) take over. The system reaches dark mode through one switch, never two.
4 · Accessibility is a release blocker
Every primitive must satisfy WCAG 2.1 AA. The audit checklist on each component page is the gate:
- 4.5:1 contrast on every variant in light and dark.
- Visible focus indicator (never
outline: nonewithout a replacement). - 44 px touch target on
lgsizes. - Color is never the only signal — pair red with the word "Delete," check marks with the word "Verified."
aria-*on web andaccessibilityState/accessibilityRoleon native, set by the component, not the consumer.
If a primitive fails any of these, it doesn't ship. No exceptions.
5 · Generative UI is sandboxed
@mihcm/ai-ui accepts JSON descriptors from a language model and renders them as real components — only after every descriptor passes Zod, the component name appears in an explicit allowlist, and the recursion depth check holds (max 5). No dynamic code evaluation, no raw HTML injection, ever. The threat model lives in docs/security-playbook.md.
Versioning
Every package follows semver, driven by changesets. Breaking changes ship as a major and are deprecated in the prior minor with a console warning. New publishable packages, including @mihcm/mcp, start with a changeset and receive their generated CHANGELOG.md entry in the Version Packages PR. The Changelog lists released package trains and any active release-train notes.