<div align="center">

![@djangocfg/ui-core](https://raw.githubusercontent.com/markolofsen/assets/main/libs/djangocfg/ui-core.webp)

</div>

# @djangocfg/ui-core

Framework-agnostic React UI library: 70+ shadcn/Radix components on Tailwind v4, semantic theme tokens, palette hooks for Canvas/SVG, plus a router-adapter system that lets the same `<Link>` / `<Sidebar>` / `<SSRPagination>` work under Next.js, Vite, Electron, Wails, or plain React.

**Part of [DjangoCFG](https://djangocfg.com). [Live demo](https://djangocfg.com/demo/).**

## Install

```bash
pnpm add @djangocfg/ui-core
```

Works in any React host. Next.js apps import components from here directly and
add the Next router adapter from `@djangocfg/ui-core/adapters/nextjs` (see
**Router adapters** below); Next-specific *server* utilities (sitemap, health,
OG images, config) live in the separate [`@djangocfg/nextjs`](../nextjs) package.

Import styles once at the app root — use the golden path, which pins everything
to the right cascade layers so import order can't break layout utilities:

```css
/* FIRST style import in your app entry CSS */
@import "@djangocfg/ui-core/styles/full";   /* Tailwind v4 + tokens + base + utilities, layer-safe */
```

The plain `@djangocfg/ui-core/styles` entry does NOT import Tailwind and emits
its CSS unlayered — if you use it you own the layer ordering (import
`tailwindcss` first). See `src/styles/README.md` § App setup for why. Prefer
`…/styles/full`.

## Quick start

```tsx
import { UiProviders, Button, Card } from '@djangocfg/ui-core';

<UiProviders>
  <Card><Button>Hello</Button></Card>
</UiProviders>
```

`<UiProviders>` mounts Tooltip / Dialog / Toast in the right order **and a top-level error `Boundary`** (a crash shows a recoverable fallback, not a white screen). **Mount it once at the root** — library components (and everything in `@djangocfg/ui-tools`) trust it to be there and never nest their own (a second `TooltipProvider` is the canonical "Tooltip must be used within TooltipProvider" trap). Pass `onError` to forward crashes to your logger, `errorFallback` for a custom (e.g. i18n) crash screen, or `errorBoundary={false}` to opt out.

## Catalogue

| Group | Examples |
|---|---|
| `components/data/` | Avatar · Badge · Card · Table · BalancedText · Skeleton |
| `components/forms/` | Button · Input · Textarea · Select · Switch · Checkbox · Slider · Form |
| `components/feedback/` | Alert · Toast · Banner · Progress · Spinner |
| `components/overlay/` | Dialog · Drawer · Popover · Tooltip · HoverCard · Sheet · ContextMenu · DropdownMenu |
| `components/navigation/` | Sidebar · Tabs · Breadcrumb · Pagination · NavigationMenu · Command |
| `components/layout/` | Container · Grid · Stack · Separator · ScrollArea · Sticky |
| `components/select/` | Combobox · MultiSelect |
| `components/effects/` | Glass · Marquee · Backdrop |
| `components/specialized/` | Accordion · Collapsible · Toggle · Calendar · DatePicker |
| `components/boundary/` | ErrorBoundary |

Imports stay flat — group folders are organisational.

## Hooks (`/hooks`)

| Topic | Hooks |
|---|---|
| `dom/` | `useSize` · `useResizeObserver` · `useMeasure` · `useMutationObserver` · `useIntersection` |
| `device/` | `useIsMobile` · `useIsTouch` · `useMediaQuery` · `useOnline` · `useViewportSize` · `useOrientation` |
| `state/` | `useLocalStorage` · `useSessionStorage` · `useToggle` · `useCounter` · `useDebouncedValue` |
| `events/` | `useEventListener` · `useClickOutside` · `useKeyPress` · `useFocusWithin` |
| `theme/` | `useTheme` · `useResolvedTheme` · `useThemePreset` |
| `feedback/` | `useToast` · `useDialog` · `useClipboard` · `useConfirm` |
| `hotkey/` | `useHotkey` (single key + chord) |
| `audio/` | `useBeep` · `useSpeak` (Web Speech) |
| `tabs/` | `useCrossTab` (BroadcastChannel coordination) |
| `media/` | `useMediaPermissions` · `useUserMedia` · `useDevices` |
| `time/` | `useNow` · `useInterval` · `useTimeout` |
| `router/` | `useLink` · `useRouter` (router-adapter consumer) |

## Lib utilities (`/lib`)

- `cn(...)` — `clsx` + `tailwind-merge` shortcut
- `getIntensity(value, thresholds)` — quantise values into discrete bins (heatmaps, gauges)
- `createLogger()` — leveled console logger
- `dialog-service` — imperative `confirm()` / `alert()` / `prompt()` returning promises
- `persist` — typed localStorage / sessionStorage hooks
- `pretext` (subpath `@djangocfg/ui-core/lib/pretext`) — DOM-free text measurement via [@chenglou/pretext](https://github.com/chenglou/pretext); powers `<BalancedText>` and is the primitive for non-CSS line balancing

## Router adapters

The router-aware components (`Sidebar`, `Link`, `SSRPagination`) read the active router via `RouterAdapterProvider`. Ship the adapter that matches your host:

| Adapter | Source |
|---|---|
| Next.js App Router | `@djangocfg/ui-core/adapters/nextjs` |
| Plain `<a>` fallback | default (no adapter) |

## Theming (`/styles`)

Tailwind v4 with semantic tokens, not raw color scales:

```tsx
<Card className="bg-card border-border text-foreground" />
<Button className="bg-primary text-primary-foreground" />
<Alert className="bg-warning-background text-warning-foreground border-warning-border" />
```

Tokens live in `:root` / `.dark` as fully-wrapped CSS colors; `@theme inline` exposes them as `--color-X` references, so opacity modifiers (`bg-card/40`, `border-foreground/20`) resolve via `color-mix` for **every** semantic token.

`@custom-variant dark (&:where(.dark, .dark *))` binds the `dark:` variant to the `.dark` class on `<html>` (not `prefers-color-scheme`) — every theme-switcher in this monorepo toggles that class.

Tailwind's `text-*` size utilities are bridged to the preset-overridable `--font-size-*` scale, so overriding those vars (globally via `buildThemeStyleSheet({ vars })` or scoped on a selector) re-sizes all `text-*` at once — no per-component edits. See `src/styles/README.md` § Typography tokens.

**Programmatic theme colors** for Canvas / SVG / Mermaid:

```ts
import { useThemeColor, alpha, useStylePresets } from '@djangocfg/ui-core/styles/palette';

const primary = useThemeColor('primary');           // #00d9ff (hex, not oklch)
const dim = alpha(primary, 0.15);                   // 'rgba(0, 217, 255, 0.15)'
const { success, warning, danger } = useStylePresets();
```

Always hex-strings — `color-mix(...)` / `oklch(...)` syntax is rejected by Canvas2D fillStyle.

[Live playground](https://djangocfg.com/demo/) covers all tokens, presets, and dark-mode pairs.

## Maintenance rule

After any change to components or hooks — update this README and bump the package patch version. Consumers pin to npm versions; surface drift in this file is the canonical changelog.

## Requirements

- React 18 or 19
- Tailwind CSS v4 (host imports `@djangocfg/ui-core/styles`)

## License

MIT — © djangocfg.
