MiHCM

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: none without a replacement).
  • 44 px touch target on lg sizes.
  • Color is never the only signal — pair red with the word "Delete," check marks with the word "Verified."
  • aria-* on web and accessibilityState / accessibilityRole on 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.