@djangocfg/ui-core 2.1.419 → 2.1.420
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 +84 -275
- package/package.json +5 -4
- package/src/styles/README.md +45 -7
- package/src/styles/full.css +52 -0
- package/src/styles/index.css +22 -13
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
|
|
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)
|
|
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
|
-
|
|
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
|
-
|
|
22
|
-
|
|
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
|
-
|
|
37
|
-
|
|
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
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
-
##
|
|
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 {
|
|
37
|
+
import { UiProviders, Button, Card } from '@djangocfg/ui-core';
|
|
73
38
|
|
|
74
|
-
<
|
|
75
|
-
<
|
|
39
|
+
<UiProviders>
|
|
40
|
+
<Card><Button>Hello</Button></Card>
|
|
41
|
+
</UiProviders>
|
|
76
42
|
```
|
|
77
43
|
|
|
78
|
-
|
|
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
|
-
|
|
46
|
+
## Catalogue
|
|
81
47
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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
|
-
|
|
|
97
|
-
|
|
|
98
|
-
|
|
|
99
|
-
|
|
|
100
|
-
|
|
|
101
|
-
|
|
|
102
|
-
|
|
|
103
|
-
|
|
|
104
|
-
|
|
|
105
|
-
|
|
|
106
|
-
|
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
|
247
|
-
| `
|
|
248
|
-
|
|
|
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
|
-
##
|
|
99
|
+
## Theming (`/styles`)
|
|
266
100
|
|
|
267
|
-
|
|
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
|
-
|
|
282
|
-
<
|
|
283
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
301
|
-
`bg-code-inline` · `text-code-inline-foreground` — for inline `<code>` chips.
|
|
113
|
+
**Programmatic theme colors** for Canvas / SVG / Mermaid:
|
|
302
114
|
|
|
303
|
-
|
|
115
|
+
```ts
|
|
116
|
+
import { useThemeColor, alpha, useStylePresets } from '@djangocfg/ui-core/styles/palette';
|
|
304
117
|
|
|
305
|
-
|
|
306
|
-
|
|
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
|
-
|
|
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
|
-
|
|
125
|
+
[Live playground](https://djangocfg.com/demo/) covers all tokens, presets, and dark-mode pairs.
|
|
314
126
|
|
|
315
|
-
|
|
316
|
-
import { useThemePalette, alpha } from '@djangocfg/ui-core/styles/palette';
|
|
127
|
+
## Maintenance rule
|
|
317
128
|
|
|
318
|
-
|
|
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
|
|
325
|
-
- Tailwind CSS
|
|
133
|
+
- React 18 or 19
|
|
134
|
+
- Tailwind CSS v4 (host imports `@djangocfg/ui-core/styles`)
|
|
326
135
|
|
|
327
|
-
|
|
136
|
+
## License
|
|
328
137
|
|
|
329
|
-
|
|
138
|
+
MIT — © djangocfg.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@djangocfg/ui-core",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.420",
|
|
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.
|
|
109
|
+
"@djangocfg/i18n": "^2.1.420",
|
|
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.
|
|
183
|
-
"@djangocfg/typescript-config": "^2.1.
|
|
183
|
+
"@djangocfg/i18n": "^2.1.420",
|
|
184
|
+
"@djangocfg/typescript-config": "^2.1.420",
|
|
184
185
|
"@types/node": "^25.2.3",
|
|
185
186
|
"@types/react": "^19.2.15",
|
|
186
187
|
"@types/react-dom": "^19.2.3",
|
package/src/styles/README.md
CHANGED
|
@@ -6,7 +6,8 @@ CSS architecture for `@djangocfg/ui-core` — **Tailwind v4** with semantic toke
|
|
|
6
6
|
|
|
7
7
|
```
|
|
8
8
|
styles/
|
|
9
|
-
├──
|
|
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
|
-
|
|
159
|
+
### Golden path (recommended) — one import, layer-safe
|
|
159
160
|
|
|
160
161
|
```css
|
|
161
|
-
/*
|
|
162
|
-
|
|
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
|
-
|
|
169
|
-
|
|
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);
|
package/src/styles/index.css
CHANGED
|
@@ -1,25 +1,34 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
* Main entry point for @djangocfg/ui package styles
|
|
2
|
+
* @djangocfg/ui-core styles — token + base + utilities chain.
|
|
4
3
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
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
|
-
*
|
|
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
|
-
/*
|
|
24
|
+
/* Theme variables + tokens (kept global so Tailwind reads @theme and :root/.dark apply everywhere). */
|
|
16
25
|
@import "./theme.css";
|
|
17
26
|
|
|
18
|
-
/*
|
|
27
|
+
/* Source detection for Tailwind v4 monorepo. */
|
|
19
28
|
@import "./sources.css";
|
|
20
29
|
|
|
21
|
-
/*
|
|
30
|
+
/* Base styles — emitted unlayered; see header note. */
|
|
22
31
|
@import "./base.css";
|
|
23
32
|
|
|
24
|
-
/*
|
|
33
|
+
/* Custom utilities — emitted unlayered; see header note. */
|
|
25
34
|
@import "./utilities.css";
|