@djangocfg/ui-core 2.1.419 → 2.1.421

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -6,9 +6,9 @@
6
6
 
7
7
  # @djangocfg/ui-core
8
8
 
9
- Framework-agnostic React UI library: 70+ components on Radix + Tailwind v4, plus a router-adapter system that lets the same `<Link>` / `<Sidebar>` / `<SSRPagination>` work under Next.js, Vite, Electron, Wails, or plain React.
9
+ 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.
10
10
 
11
- **Part of [DjangoCFG](https://djangocfg.com).** **[Live demo](https://djangocfg.com/demo/)**.
11
+ **Part of [DjangoCFG](https://djangocfg.com). [Live demo](https://djangocfg.com/demo/).**
12
12
 
13
13
  ## Install
14
14
 
@@ -16,314 +16,123 @@ Framework-agnostic React UI library: 70+ components on Radix + Tailwind v4, plus
16
16
  pnpm add @djangocfg/ui-core
17
17
  ```
18
18
 
19
- ## Imports
19
+ Next.js apps should import from [`@djangocfg/ui-nextjs`](../ui-nextjs) instead — it re-exports everything here plus Next-specific bindings.
20
20
 
21
- ```tsx
22
- import { Button, Card, Sidebar, SSRPagination } from '@djangocfg/ui-core/components';
23
- import { useIsMobile, useNavigate, useQueryParams } from '@djangocfg/ui-core/hooks';
24
- import { cn } from '@djangocfg/ui-core/lib';
25
- import { UiProviders } from '@djangocfg/ui-core/providers';
26
- ```
27
-
28
- ## Root providers
29
-
30
- Mount `<UiProviders>` once at the top of your app — it bundles the overlay
31
- and imperative services every ui-core / ui-tools component expects:
32
-
33
- ```tsx
34
- import { UiProviders } from '@djangocfg/ui-core/providers';
21
+ Import styles once at the app root — use the golden path, which pins everything
22
+ to the right cascade layers so import order can't break layout utilities:
35
23
 
36
- export function Root({ children }: { children: React.ReactNode }) {
37
- return <UiProviders>{children}</UiProviders>;
38
- }
24
+ ```css
25
+ /* FIRST style import in your app entry CSS */
26
+ @import "@djangocfg/ui-core/styles/full"; /* Tailwind v4 + tokens + base + utilities, layer-safe */
39
27
  ```
40
28
 
41
- Includes:
42
- - `<TooltipProvider>` (Radix tooltip root, `delayDuration={100}` by default)
43
- - `<DialogProvider>` (installs `window.dialog.*` + renders active dialogs)
44
- - `<Toaster>` (Sonner toast portal)
45
-
46
- Opt out per-service: `<UiProviders noToaster noDialogService>`.
47
-
48
- **Do not nest a second `<TooltipProvider>` inside library components** —
49
- that creates a separate context scope and breaks the tooltip↔provider link.
29
+ The plain `@djangocfg/ui-core/styles` entry does NOT import Tailwind and emits
30
+ its CSS unlayered if you use it you own the layer ordering (import
31
+ `tailwindcss` first). See `src/styles/README.md` § App setup for why. Prefer
32
+ `…/styles/full`.
50
33
 
51
- ## Components
52
-
53
- Organized in `components/` by category — everything re-exported from the root barrel.
54
-
55
- | Category | Examples |
56
- |---|---|
57
- | **Forms** | `Button`, `ButtonLink`, `ButtonGroup`, `Input`, `Textarea`, `Checkbox`, `RadioGroup`, `Switch`, `Slider`, `Label`, `Form`, `Field`, `InputOTP`, `PhoneInput`, `InputGroup`, `DownloadButton` |
58
- | **Select** | `Select`, `Combobox`, `ComboboxAsync`, `MultiSelect`, `MultiSelectPro`, `MultiSelectProAsync`, `CountrySelect`, `LanguageSelect` (all support icons + badges; `Select` accepts empty-string values; `ComboboxAsync` is the single-select counterpart to `MultiSelectProAsync` for server-side typeahead) |
59
- | **Overlay** | `Dialog`, `AlertDialog`, `Sheet`, `Drawer`, `Popover`, `Tooltip`, `HoverCard`, `ResponsiveSheet`, `SidePanel` |
60
- | **Navigation** | `Link`, `Breadcrumb`, `BreadcrumbNavigation`, `Pagination`, `StaticPagination`, `SSRPagination`, `Sidebar` (full shadcn primitives), `Tabs`, `Accordion`, `Collapsible`, `Command`, `DropdownMenu`, `ContextMenu`, `Menubar`, `NavigationMenu`, `MenuBuilder` (declarative data-driven menu) |
61
- | **Theme** | `ThemeProvider`, `ThemeToggle`, `ForceTheme`, `ThemeOverride`, `useThemeContext`, `useForcedTheme` — framework-agnostic theme runtime |
62
- | **Layout** | `Card`, `Section`, `Sticky`, `ScrollArea`, `Resizable`, `Separator`, `Skeleton`, `AspectRatio` |
63
- | **Data** | `Table`, `Badge`, `Avatar`, `Progress`, `Carousel`, `Calendar`, `DatePicker`, `DateRangePicker`, `Toggle`, `ToggleGroup`, `Chart*`, `BalancedText` (pretext-powered line balancing) |
64
- | **Feedback** | `Alert`, `Spinner`, `Empty`, `Preloader`, `Toaster` (Sonner) |
65
- | **Boundary** | `Boundary` (React error boundary with `silent`/`inline`/`card`/`fullscreen` variants, `resetKeys`, custom `fallback`) |
66
- | **Specialized** | `Kbd`, `CopyButton`, `CopyField`, `TokenIcon`, `Item`, `Portal`, `ImageWithFallback`, `Flag`, `LanguageFlag` |
67
- | **Effects** | `GlowBackground` |
68
-
69
- ### Boundary
34
+ ## Quick start
70
35
 
71
36
  ```tsx
72
- import { Boundary, useBoundary } from '@djangocfg/ui-core';
37
+ import { UiProviders, Button, Card } from '@djangocfg/ui-core';
73
38
 
74
- <Boundary variant="silent"><ChatLauncher /></Boundary>
75
- <Boundary variant="card" resetKeys={[pathname]}><Panel /></Boundary>
39
+ <UiProviders>
40
+ <Card><Button>Hello</Button></Card>
41
+ </UiProviders>
76
42
  ```
77
43
 
78
- Variants: `silent` / `inline` / `card` / `fullscreen`. Plus `useBoundary()` hook for async errors, `resetKeys`, `onReset`, custom `fallback` / `FallbackComponent`, and safe re-render guarantees.
44
+ `<UiProviders>` mounts Tooltip / Dialog / Toast / Theme provider in the right order. **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).
79
45
 
80
- Full docs and patterns: [`src/components/boundary/README.md`](./src/components/boundary/README.md).
46
+ ## Catalogue
81
47
 
82
- For automatic backend reporting use `MonitorBoundary` from `@djangocfg/layouts`.
83
-
84
-
85
- > Pagination, breadcrumb and sidebar live here (not in ui-nextjs). `SSRPagination` reads URL state through `useLocation` + `useQueryParams`, so it works under any router adapter.
86
-
87
- ## Hooks
88
-
89
- ```tsx
90
- import { useIsMobile, useMediaQuery, useShortcutModLabel } from '@djangocfg/ui-core/hooks';
91
- import { useNavigate, useLocation, useQueryParams, useRouter, useIsActive } from '@djangocfg/ui-core/hooks/router';
92
- ```
93
-
94
- | Group | Hooks |
48
+ | Group | Examples |
49
+ |---|---|
50
+ | `components/data/` | Avatar · Badge · Card · Table · BalancedText · Skeleton |
51
+ | `components/forms/` | Button · Input · Textarea · Select · Switch · Checkbox · Slider · Form |
52
+ | `components/feedback/` | Alert · Toast · Banner · Progress · Spinner |
53
+ | `components/overlay/` | Dialog · Drawer · Popover · Tooltip · HoverCard · Sheet · ContextMenu · DropdownMenu |
54
+ | `components/navigation/` | Sidebar · Tabs · Breadcrumb · Pagination · NavigationMenu · Command |
55
+ | `components/layout/` | Container · Grid · Stack · Separator · ScrollArea · Sticky |
56
+ | `components/select/` | Combobox · MultiSelect |
57
+ | `components/effects/` | Glass · Marquee · Backdrop |
58
+ | `components/specialized/` | Accordion · Collapsible · Toggle · Calendar · DatePicker |
59
+ | `components/boundary/` | ErrorBoundary |
60
+
61
+ Imports stay flat — group folders are organisational.
62
+
63
+ ## Hooks (`/hooks`)
64
+
65
+ | Topic | Hooks |
95
66
  |---|---|
96
- | **Router** (adapter-driven) | `useNavigate`, `useLocation`, `useLocationProperty`, `useQueryParams`, `useQueryState` (typed), `useRouter`, `useUrlBuilder`, `useSmartLink`, `useIsActive`, `useBackOrFallback` + parsers (`parseAsInteger`, `parseAsBoolean`, …) |
97
- | **Media** | `useIsMobile`, `useIsPhone`, `useIsTabletOrBelow`, `useMediaQuery` |
98
- | **Device** | `useDeviceDetect`, `useBrowserDetect`, `useShortcutModLabel` |
99
- | **State** | `useDebounce`, `useDebouncedCallback`, `useCountdown`, `useImageLoader`, `useMounted` |
100
- | **DOM** | `useEventListener`, `useResizeObserver` (shared ResizeObserver store, returns `{ width, height }`) |
101
- | **Theme** | `useResolvedTheme` (light/dark detection), `useThemeColor`, `useThemePalette` (palette-aware hex colors for Canvas/SVG) |
102
- | **Hotkey** | `useHotkey` (smart `inInput` + `preventDefault` policy), `useHotkeyChord` (sequences), `formatHotkey('mod+k') → ⌘K`, `useHotkeyHelp` (auto cheat-sheet) |
103
- | **Audio** | `createSoundBus`, `useNotificationSounds`, `useAudioPrefs`, `useSoundEffect` Safari unlock, mute persist, per-event toggles + volume scale, native-host bridge |
104
- | **Tabs** | `useActiveTab`, `useIsTabActive`, `useIsTabLeader` cross-tab focus + leader election via BroadcastChannel |
105
- | **Feedback** | `useToast`, `toast` (Sonner) |
106
- | **Debug** | `useDebugTools` |
67
+ | `dom/` | `useSize` · `useResizeObserver` · `useMeasure` · `useMutationObserver` · `useIntersection` |
68
+ | `device/` | `useIsMobile` · `useMediaQuery` · `useOnline` · `useViewportSize` · `useOrientation` |
69
+ | `state/` | `useLocalStorage` · `useSessionStorage` · `useToggle` · `useCounter` · `useDebouncedValue` |
70
+ | `events/` | `useEventListener` · `useClickOutside` · `useKeyPress` · `useFocusWithin` |
71
+ | `theme/` | `useTheme` · `useResolvedTheme` · `useThemePreset` |
72
+ | `feedback/` | `useToast` · `useDialog` · `useClipboard` · `useConfirm` |
73
+ | `hotkey/` | `useHotkey` (single key + chord) |
74
+ | `audio/` | `useBeep` · `useSpeak` (Web Speech) |
75
+ | `tabs/` | `useCrossTab` (BroadcastChannel coordination) |
76
+ | `media/` | `useMediaPermissions` · `useUserMedia` · `useDevices` |
77
+ | `time/` | `useNow` · `useInterval` · `useTimeout` |
78
+ | `router/` | `useLink` · `useRouter` (router-adapter consumer) |
79
+
80
+ ## Lib utilities (`/lib`)
81
+
82
+ - `cn(...)` — `clsx` + `tailwind-merge` shortcut
83
+ - `getIntensity(value, thresholds)` — quantise values into discrete bins (heatmaps, gauges)
84
+ - `createLogger()` — leveled console logger
85
+ - `dialog-service` — imperative `confirm()` / `alert()` / `prompt()` returning promises
86
+ - `persist` — typed localStorage / sessionStorage hooks
87
+ - `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
107
88
 
108
89
  ## Router adapters
109
90
 
110
- `<Link>`, `<Sidebar>`, `<SSRPagination>` and friends are wired through a small adapter system, so the same component renders correctly under any router.
111
-
112
- ```tsx
113
- // Next.js host (e.g. apps/demo)
114
- import { LinkProvider } from '@djangocfg/ui-core/components';
115
- import NextLink from 'next/link';
116
-
117
- <LinkProvider value={NextLink}>{children}</LinkProvider>
118
- ```
119
-
120
- When no adapter is mounted, `<Link>` falls back to a plain `<a>` and routes clicks through the History API. `@djangocfg/layouts/BaseApp` mounts the Next adapters by default.
121
-
122
- ## Hotkeys
123
-
124
- ```tsx
125
- import { useHotkey, formatHotkey, useHotkeyChord } from '@djangocfg/ui-core/hooks';
126
-
127
- // Smart defaults — auto-detects modifiers vs bare keys:
128
- useHotkey('mod+k', openPalette); // ⌘K everywhere, incl. inputs; preventDefault auto
129
- useHotkey('/', focusSearch); // bare key — skipped inside inputs
130
- useHotkey('escape', closeModal); // always fires in inputs (blur / close pattern)
131
- useHotkey('?', openHelp, { description: 'Show shortcuts' }); // registers in cheat-sheet
132
-
133
- // Linear-style chords:
134
- useHotkeyChord(['g', 't'], () => router.push('/tasks'));
135
-
136
- // OS-aware tooltips:
137
- formatHotkey('mod+/') // → '⌘/' on Mac, 'Ctrl+/' elsewhere
138
- formatHotkey('g t') // → 'G then T'
139
- ```
140
-
141
- Built on `react-hotkeys-hook` with an opinionated policy: modifier-combos and `escape` fire inside `<input>` / `<textarea>` / `[contenteditable]` by default; bare keys don't, so they won't hijack typing. Override per-call with `inInput: true | false`.
142
-
143
- `useHotkeyHelp()` reads a module-level registry populated by every `useHotkey(..., { description })` call — drop it into a `?` cheat-sheet dialog with two lines.
144
-
145
- ## Audio
91
+ The router-aware components (`Sidebar`, `Link`, `SSRPagination`) read the active router via `RouterAdapterProvider`. Ship the adapter that matches your host:
146
92
 
147
- Notification sounds with Safari unlock, persisted mute, and a side-channel for native hosts.
148
-
149
- ```tsx
150
- import { useNotificationSounds, useSoundEffect } from '@djangocfg/ui-core/hooks';
151
-
152
- // Map event → URL + persisted mute + per-event toggles
153
- type Event = 'received' | 'mention' | 'error';
154
- const sounds = useNotificationSounds<Event>({
155
- storageKey: 'myapp.audio',
156
- sounds: { received: '/sfx/r.mp3', mention: '/sfx/m.mp3', error: '/sfx/e.mp3' },
157
- });
158
-
159
- onMessage = (m) => sounds.play('received');
160
- toggle = () => sounds.toggleMute();
161
- sounds.muted // boolean, persisted
162
- sounds.isSilent // true if no sounds wired or silenced
163
-
164
- // One-shot single asset (no persistence, no toggles)
165
- const ding = useSoundEffect('/sfx/ding.mp3');
166
- <Button onClick={() => { ding.play(); submit(); }} />
167
- ```
168
-
169
- **Native-host bridge.** When a wrapper (Electron / Wails / Tauri) plays sounds outside the browser, set `silenced: true` and pipe `onSoundEvent` to your bridge — web playback stays silent while the backend gets the trigger:
170
-
171
- ```tsx
172
- useNotificationSounds({
173
- storageKey: 'cmdop.audio',
174
- silenced: true,
175
- onSoundEvent: (event) => window.go.playSound(event),
176
- });
177
- ```
178
-
179
- **Per-event volume scale.** Master volume is multiplied by an optional per-event factor so different sounds can be balanced without juggling source files:
180
-
181
- ```tsx
182
- useNotificationSounds<Event>({
183
- storageKey: 'myapp.audio',
184
- sounds: { received: '/r.mp3', error: '/e.mp3', mention: '/m.mp3' },
185
- eventVolumes: {
186
- error: 0.25, // soft ack — destructive UI is the loud signal
187
- mention: 1, // personal — louder than baseline
188
- received: 0.7,
189
- },
190
- });
191
- ```
192
-
193
- The bus respects `prefers-reduced-motion`, `prefers-reduced-data`, and `visibilityState === 'hidden'` by default (each is an opt-out). Multi-component sync: same `storageKey` from different surfaces shares the same store.
194
-
195
- Lower level: `createSoundBus<E>({ sounds, getMuted, getVolume, isEnabled })` exposes the raw bus if you need to wire your own React state. `getVolume(event)` receives the event so you can scale per-fire.
196
-
197
- For an opinionated, fully-wired example with bundled audio assets see `useChatAudio` in [`@djangocfg/ui-tools`](../../ui-tools/src/tools/Chat/README.md#audio) — it inlines six notification mp3s into the lazy chat chunk as `data:`-URLs so consumers need no asset setup.
198
-
199
- ## Cross-tab coordination
200
-
201
- ```tsx
202
- import { useActiveTab, useIsTabLeader } from '@djangocfg/ui-core/hooks';
203
-
204
- const { isActive, isLeader, tabId } = useActiveTab();
205
- // isActive — this tab has user focus (visibilitychange + focus/blur)
206
- // isLeader — elected leader among open tabs (stable; oldest tab wins)
207
- // tabId — stable per-tab id (sessionStorage, survives in-tab reload)
208
- ```
209
-
210
- Single shared coordinator over `BroadcastChannel` — leader election runs locally on every tab from the same peer-set view, no server. Use the leader flag to dedupe side-effects across tabs: only the leader mutates `document.title` / favicon, holds the websocket, plays a sound; followers stay silent but still read state. Zustand-backed, SSR-safe, single-tab fallback when `BroadcastChannel` is unavailable.
211
-
212
- ## Schema-driven configurators
213
-
214
- `@djangocfg/ui-core/lib` exports a portable JSON Schema 7 subset (`CustomJsonSchema7`, `CustomJsonUiSchema7`, `CustomJsonUiGroup`, `CustomJsonUiDisabledWhenRule`) for packages that ship configurator schemas without taking a runtime dependency on RJSF.
215
-
216
- ```tsx
217
- import type { CustomJsonSchema7, CustomJsonUiSchema7 } from '@djangocfg/ui-core/lib';
218
- ```
219
-
220
- `@djangocfg/ui-tools/json-form` accepts these directly (it's a union with `RJSFSchema`).
221
-
222
- ## Dialog service
223
-
224
- ```tsx
225
- import { DialogProvider, useDialog, dialog } from '@djangocfg/ui-core/lib/dialog-service';
226
-
227
- dialog.confirm({ title: 'Delete?', description: 'This is permanent.' });
228
- ```
229
-
230
- ## Logger
231
-
232
- ```tsx
233
- import { createLogger } from '@djangocfg/ui-core/lib';
234
- const log = createLogger('MyComponent');
235
- log.info('user logged in', { userId: 123 });
236
- ```
237
-
238
- ## Lib utilities
239
-
240
- ```tsx
241
- import { cn, createLogger, getIntensity } from '@djangocfg/ui-core/lib';
242
- ```
243
-
244
- | Utility | Purpose |
93
+ | Adapter | Source |
245
94
  |---|---|
246
- | `cn` | Tailwind class merger (`tailwind-merge` + `clsx`). |
247
- | `createLogger` | Tagged console logger. |
248
- | `getIntensity(value, max, thresholds?)` | Quantize a value to an integer bucket (0–N). Used for heatmaps, gauges, thermal-style visualizations. Default thresholds `[0.25, 0.5, 0.75]` map ratios to four non-zero buckets. |
249
- | `compose-refs`, `compose-event-handlers`, `get-element-ref` | Radix-style ref/handler helpers. |
250
- | `dialog-service`, `persist`, `env`, `logger` | See dedicated sections. |
251
-
252
- ### Pretext
253
-
254
- ```tsx
255
- import {
256
- usePretext,
257
- usePretextLayout,
258
- usePretextLines,
259
- useBalancedWidth,
260
- } from '@djangocfg/ui-core/lib/pretext';
261
- ```
262
-
263
- DOM-free text measurement hooks wrapping [`@chenglou/pretext`](https://github.com/chenglou/pretext) (optional peer dependency). `useBalancedWidth` powers `<BalancedText>`. Falls back gracefully on SSR or when `@chenglou/pretext` is not installed.
95
+ | Next.js App Router | `@djangocfg/ui-nextjs` (auto-wired) |
96
+ | Vite / SPA (`react-router`) | `@djangocfg/ui-core/adapters/react-router` |
97
+ | Plain `<a>` fallback | default (no adapter) |
264
98
 
265
- ## Styles & Theming
99
+ ## Theming (`/styles`)
266
100
 
267
- ```css
268
- /* In your app's globals.css — import BEFORE @import "tailwindcss" */
269
- @import '@djangocfg/ui-nextjs/styles'; /* re-exports ui-core styles */
270
- ```
271
-
272
- ### Semantic color tokens
273
-
274
- All colors are CSS custom properties registered in `@theme` so Tailwind generates utility classes.
275
- **Never use raw Tailwind color-scale classes** (`amber-500`, `green-100`) in components — use
276
- semantic tokens that adapt to both light and dark themes automatically.
277
-
278
- #### Status surfaces — banners and alerts
101
+ Tailwind v4 with semantic tokens, not raw color scales:
279
102
 
280
103
  ```tsx
281
- {/* Warning */}
282
- <div className="flex gap-2 rounded-md border border-warning-border/40
283
- bg-warning-background px-3 py-2 text-xs text-warning-foreground">
284
- <Icon className="text-warning" />
285
- <span>You're on a preview plan.</span>
286
- </div>
104
+ <Card className="bg-card border-border text-foreground" />
105
+ <Button className="bg-primary text-primary-foreground" />
106
+ <Alert className="bg-warning-background text-warning-foreground border-warning-border" />
287
107
  ```
288
108
 
289
- Each status (`warning` · `success` · `destructive` · `info`) exposes four classes:
109
+ 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.
290
110
 
291
- | Class | Role |
292
- |---|---|
293
- | `bg-*-background` | Banner fill |
294
- | `text-*-foreground` | Readable body text |
295
- | `border-*-border` | Border ring |
296
- | `text-*` / `bg-*` | Icon / accent color |
297
-
298
- #### Code surface
111
+ `@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.
299
112
 
300
- `bg-code` · `text-code-foreground` · `border-code-border` used by PrettyCode, MarkdownMessage code fences.
301
- `bg-code-inline` · `text-code-inline-foreground` — for inline `<code>` chips.
113
+ **Programmatic theme colors** for Canvas / SVG / Mermaid:
302
114
 
303
- ### Playground
115
+ ```ts
116
+ import { useThemeColor, alpha, useStylePresets } from '@djangocfg/ui-core/styles/palette';
304
117
 
305
- ```bash
306
- pnpm playground # opens component explorer with live theme preview
118
+ const primary = useThemeColor('primary'); // #00d9ff (hex, not oklch)
119
+ const dim = alpha(primary, 0.15); // 'rgba(0, 217, 255, 0.15)'
120
+ const { success, warning, danger } = useStylePresets();
307
121
  ```
308
122
 
309
- The **Theme / Tokens** story shows every semantic token in both themes — use it to validate
310
- changes before syncing CSS to consumers. See `src/styles/README.md` for the full token reference
311
- and the manual sync workflow for local package development.
123
+ Always hex-strings `color-mix(...)` / `oklch(...)` syntax is rejected by Canvas2D fillStyle.
312
124
 
313
- ### Programmatic colors (Canvas / SVG)
125
+ [Live playground](https://djangocfg.com/demo/) covers all tokens, presets, and dark-mode pairs.
314
126
 
315
- ```tsx
316
- import { useThemePalette, alpha } from '@djangocfg/ui-core/styles/palette';
127
+ ## Maintenance rule
317
128
 
318
- const palette = useThemePalette();
319
- ctx.fillStyle = alpha(palette.primary, 0.3); // hex → rgba, updates on theme switch
320
- ```
129
+ 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.
321
130
 
322
131
  ## Requirements
323
132
 
324
- - React 19
325
- - Tailwind CSS 4
133
+ - React 18 or 19
134
+ - Tailwind CSS v4 (host imports `@djangocfg/ui-core/styles`)
326
135
 
327
- ---
136
+ ## License
328
137
 
329
- **[djangocfg.com](https://djangocfg.com)**
138
+ MIT — © djangocfg.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@djangocfg/ui-core",
3
- "version": "2.1.419",
3
+ "version": "2.1.421",
4
4
  "description": "Pure React UI component library without Next.js dependencies - for Electron, Vite, CRA apps",
5
5
  "keywords": [
6
6
  "ui-components",
@@ -79,6 +79,7 @@
79
79
  "require": "./src/utils/index.ts"
80
80
  },
81
81
  "./styles": "./src/styles/index.css",
82
+ "./styles/full": "./src/styles/full.css",
82
83
  "./styles/globals": "./src/styles/globals.css",
83
84
  "./styles/theme": "./src/styles/theme.css",
84
85
  "./styles/base": "./src/styles/base.css",
@@ -105,7 +106,7 @@
105
106
  "check": "tsc --noEmit"
106
107
  },
107
108
  "peerDependencies": {
108
- "@djangocfg/i18n": "^2.1.419",
109
+ "@djangocfg/i18n": "^2.1.421",
109
110
  "consola": "^3.4.2",
110
111
  "lucide-react": "^0.545.0",
111
112
  "moment": "^2.30.1",
@@ -179,8 +180,8 @@
179
180
  "@chenglou/pretext": "*"
180
181
  },
181
182
  "devDependencies": {
182
- "@djangocfg/i18n": "^2.1.419",
183
- "@djangocfg/typescript-config": "^2.1.419",
183
+ "@djangocfg/i18n": "^2.1.421",
184
+ "@djangocfg/typescript-config": "^2.1.421",
184
185
  "@types/node": "^25.2.3",
185
186
  "@types/react": "^19.2.15",
186
187
  "@types/react-dom": "^19.2.3",
@@ -6,7 +6,8 @@ CSS architecture for `@djangocfg/ui-core` — **Tailwind v4** with semantic toke
6
6
 
7
7
  ```
8
8
  styles/
9
- ├── index.css # Main entryimports the chain below
9
+ ├── full.css # Golden path (recommended) Tailwind + tokens + base + utilities, cascade-layer-safe
10
+ ├── index.css # Plain entry (no Tailwind, unlayered) — you own layer ordering
10
11
  ├── theme.css # Imports tokens.css → animations → light → dark
11
12
  ├── base.css # Resets + `*` border-color + body bg/color
12
13
  ├── utilities.css # Custom utilities (.glass-*, .step, animations)
@@ -155,18 +156,55 @@ Each requires a **non-transparent parent** (something for the blur to chew on).
155
156
 
156
157
  ## App setup
157
158
 
158
- In your consuming app's `globals.css`:
159
+ ### Golden path (recommended) — one import, layer-safe
159
160
 
160
161
  ```css
161
- /* 1. ui-core theme tokens FIRST (they define every --color-*) */
162
- @import "@djangocfg/ui-core/styles";
162
+ /* Single line. Imports Tailwind + tokens + base + utilities in the
163
+ correct cascade layers. Put it FIRST; other package CSS after. */
164
+ @import "@djangocfg/ui-core/styles/full";
163
165
 
164
- /* 2. Other package styles after */
165
166
  @import "@djangocfg/layouts/styles";
166
167
  @import "@djangocfg/ui-tools/styles";
168
+ ```
167
169
 
168
- /* 3. Tailwind v4 core LAST so it sees the @theme tokens */
169
- @import "tailwindcss";
170
+ `…/styles/full` (`full.css`) pins ui-core's base resets to `@layer base`
171
+ and its custom utilities to `@layer utilities` via `@import "" layer(name)`.
172
+ A `layer()`-qualified import is folded into that layer **regardless of
173
+ import order or build tool**, so you cannot get the ordering wrong, and
174
+ you do not need to import `tailwindcss` yourself.
175
+
176
+ ### The cascade-layer rule (why ordering matters)
177
+
178
+ Tailwind v4 emits its utilities inside `@layer utilities`. **Unlayered
179
+ CSS beats any layered rule** in the cascade. So if a package's base
180
+ resets (here `* { border-color }` and the `body` background/font rules in
181
+ `base.css`) are emitted *unlayered*, they sit above `@layer utilities`
182
+ and silently defeat layout utilities (`gap`, `space-y`, `divide`, `flex`,
183
+ `border`, `padding`). Colors usually survive because they flow through
184
+ CSS vars (no cascade conflict), so the breakage is invisible in Chrome
185
+ but shows up in stricter engines (WKWebView).
186
+
187
+ Whether a *plain*, unlayered `@import` lands in a layer depends on its
188
+ position relative to `@import "tailwindcss"` **and** on the build tool
189
+ (Vite vs Next.js resolve `@import` differently). `full.css` removes that
190
+ dependency by binding each file to a layer explicitly. **Use `…/styles/full`
191
+ and this whole class of bug cannot occur.**
192
+
193
+ ### Manual ordering (only if you manage layers yourself)
194
+
195
+ The plain `@djangocfg/ui-core/styles` entry imports `theme.css` +
196
+ `sources.css` + `base.css` + `utilities.css` **unlayered** (it does not
197
+ import Tailwind). If you use it, you own the layer ordering. The Next.js
198
+ demo does this and works because PostCSS folds the trailing
199
+ `@import "tailwindcss"` such that the resets still end up benign — but a
200
+ Vite consumer that puts `@import "tailwindcss"` last hit exactly the bug
201
+ above. If you must hand-order, import `tailwindcss` **first**:
202
+
203
+ ```css
204
+ @import "tailwindcss"; /* establishes the layers first */
205
+ @import "@djangocfg/ui-core/styles";
206
+ @import "@djangocfg/layouts/styles";
207
+ @import "@djangocfg/ui-tools/styles";
170
208
  ```
171
209
 
172
210
  > **No `@plugin "tailwindcss-animate"` needed** in v4 — the keyframes ship via `theme/animations.css`. Use `tw-animate-css` instead if you need extra utilities.
@@ -0,0 +1,52 @@
1
+ /**
2
+ * @djangocfg/ui-core — golden-path style entry (cascade-layer-safe)
3
+ *
4
+ * Import THIS file (`@djangocfg/ui-core/styles/full`) as the SINGLE,
5
+ * FIRST style import in a consumer app and Tailwind cascade-layer
6
+ * ordering cannot go wrong — regardless of build tool (Vite or Next.js)
7
+ * or where the consumer's own `@import "tailwindcss"` ends up.
8
+ *
9
+ * Why this file exists — the cascade-layer trap it removes
10
+ * --------------------------------------------------------
11
+ * Tailwind v4 emits its utilities inside `@layer utilities`. Any CSS
12
+ * that lands OUTSIDE a cascade layer beats every layered rule, because
13
+ * unlayered styles always win over layered ones in the cascade. So if a
14
+ * package's base resets (e.g. ui-core's `* { border-color }` and the
15
+ * `body` background/font rules in base.css) are emitted unlayered, they
16
+ * sit above `@layer utilities` and silently defeat layout utilities
17
+ * (gap, space-y, divide, flex, border, padding). Colors usually survive
18
+ * because they resolve through CSS vars, which have no cascade conflict,
19
+ * so the breakage is invisible until a stricter engine (WKWebView) shows
20
+ * it. The plain `@djangocfg/ui-core/styles` entry imports base.css and
21
+ * utilities.css unlayered, which means their position relative to
22
+ * `@import "tailwindcss"` decides whether they get folded into a layer —
23
+ * a footgun for consumers.
24
+ *
25
+ * This entry removes the footgun by binding each package file to an
26
+ * explicit cascade layer at import time via `@import "..." layer(name)`.
27
+ * A `layer()`-qualified import is folded into that named layer no matter
28
+ * the import order or build tool, so the consumer can put their own
29
+ * `@import "tailwindcss"` anywhere (or omit it — this file imports it
30
+ * first) and the resets stay in `@layer base`, the custom utilities stay
31
+ * in `@layer utilities`, both correctly under Tailwind's own emissions.
32
+ *
33
+ * theme.css and sources.css are intentionally NOT layered: `@theme`
34
+ * token blocks and the `:root` / `.dark` CSS variables must stay global
35
+ * so Tailwind collects the tokens and the variables apply everywhere,
36
+ * and `@source` is a build directive, not a style rule.
37
+ */
38
+
39
+ /* Tailwind v4 core first — establishes the properties/theme/base/components/utilities layer order. */
40
+ @import "tailwindcss";
41
+
42
+ /* Theme tokens + CSS variables — must stay global (unlayered) so Tailwind reads @theme and :root/.dark apply everywhere. */
43
+ @import "./theme.css";
44
+
45
+ /* @source directives for monorepo class detection — build directive, not a style rule. */
46
+ @import "./sources.css";
47
+
48
+ /* Base resets pinned to @layer base so they never escape above Tailwind's utilities. */
49
+ @import "./base.css" layer(base);
50
+
51
+ /* Custom utilities pinned to @layer utilities to sit alongside Tailwind's own. */
52
+ @import "./utilities.css" layer(utilities);
@@ -1,25 +1,34 @@
1
1
  /**
2
- * Unrealon UI Styles
3
- * Main entry point for @djangocfg/ui package styles
2
+ * @djangocfg/ui-core styles — token + base + utilities chain.
4
3
  *
5
- * Import order (critical for Tailwind v4):
6
- * 1. Theme variables FIRST (so Tailwind can use them)
7
- * 2. Tailwind CSS v4 (compiles with access to theme variables)
8
- * 3. Animation utilities (tw-animate-css for Tailwind v4)
9
- * 4. Base styles (override Tailwind defaults)
10
- * 5. Custom utilities (extend Tailwind utilities)
4
+ * IMPORTANT this entry does NOT import Tailwind and does NOT wrap its
5
+ * CSS in cascade layers. base.css (the `*` border reset and the `body`
6
+ * background/font rules) and utilities.css are emitted UNLAYERED. In
7
+ * Tailwind v4 unlayered rules beat anything in `@layer utilities`, so
8
+ * whether these resets defeat layout utilities (gap, space-y, divide,
9
+ * flex, border, padding) depends entirely on where the consumer places
10
+ * its own `@import "tailwindcss"` and on the build tool — a real footgun
11
+ * (it broke a WKWebView/Vite consumer while rendering fine in Chrome).
11
12
  *
12
- * Usage: Import this file in your app's main CSS or _app.tsx
13
+ * Prefer the cascade-layer-safe golden path instead, which pins the
14
+ * resets/utilities to explicit layers so ordering can't go wrong:
15
+ *
16
+ * @import "@djangocfg/ui-core/styles/full"; // see full.css
17
+ *
18
+ * This `index.css` is kept for consumers that intentionally manage their
19
+ * own layer ordering (e.g. the Next.js demo, which imports tailwindcss
20
+ * itself AFTER this chain). theme.css must stay unlayered here so the
21
+ * `@theme` tokens and `:root`/`.dark` variables remain global.
13
22
  */
14
23
 
15
- /* 1. Theme variables MUST come first */
24
+ /* Theme variables + tokens (kept global so Tailwind reads @theme and :root/.dark apply everywhere). */
16
25
  @import "./theme.css";
17
26
 
18
- /* 2. Source detection for Tailwind v4 monorepo */
27
+ /* Source detection for Tailwind v4 monorepo. */
19
28
  @import "./sources.css";
20
29
 
21
- /* 3. Base styles */
30
+ /* Base styles — emitted unlayered; see header note. */
22
31
  @import "./base.css";
23
32
 
24
- /* 5. Custom utilities */
33
+ /* Custom utilities — emitted unlayered; see header note. */
25
34
  @import "./utilities.css";