@djangocfg/ui-core 2.1.460 → 2.1.463

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.
Files changed (46) hide show
  1. package/README.md +1 -1
  2. package/package.json +19 -11
  3. package/src/styles/README.md +74 -510
  4. package/src/styles/{base.css → css/base.css} +1 -1
  5. package/src/styles/css/presets/default.css +1 -0
  6. package/src/styles/css/presets/dense.css +36 -0
  7. package/src/styles/css/presets/django-cfg.css +44 -0
  8. package/src/styles/css/presets/high-contrast.css +24 -0
  9. package/src/styles/css/presets/ios.css +120 -0
  10. package/src/styles/css/presets/macos.css +138 -0
  11. package/src/styles/css/presets/soft.css +52 -0
  12. package/src/styles/css/presets/windows.css +140 -0
  13. package/src/styles/{utilities → css/utilities}/glass.css +16 -0
  14. package/src/styles/presets/index.ts +2 -15
  15. package/src/styles/presets/presets.ts +0 -3
  16. package/src/styles/presets/types.ts +4 -148
  17. package/src/theme/README.md +36 -64
  18. package/src/theme/TROUBLESHOOTING.md +4 -0
  19. package/src/styles/presets/build.ts +0 -64
  20. package/src/styles/presets/themes/default.ts +0 -6
  21. package/src/styles/presets/themes/dense.ts +0 -31
  22. package/src/styles/presets/themes/django-cfg.ts +0 -62
  23. package/src/styles/presets/themes/high-contrast.ts +0 -35
  24. package/src/styles/presets/themes/index.ts +0 -24
  25. package/src/styles/presets/themes/ios.ts +0 -114
  26. package/src/styles/presets/themes/macos.ts +0 -190
  27. package/src/styles/presets/themes/soft.ts +0 -48
  28. package/src/styles/presets/themes/types.ts +0 -6
  29. package/src/styles/presets/themes/windows.ts +0 -154
  30. /package/src/styles/{full.css → css/full.css} +0 -0
  31. /package/src/styles/{globals.css → css/globals.css} +0 -0
  32. /package/src/styles/{index.css → css/index.css} +0 -0
  33. /package/src/styles/{sources.css → css/sources.css} +0 -0
  34. /package/src/styles/{theme → css/theme}/animations.css +0 -0
  35. /package/src/styles/{theme → css/theme}/dark.css +0 -0
  36. /package/src/styles/{theme → css/theme}/light.css +0 -0
  37. /package/src/styles/{theme → css/theme}/tokens.css +0 -0
  38. /package/src/styles/{theme.css → css/theme.css} +0 -0
  39. /package/src/styles/{utilities → css/utilities}/animations.css +0 -0
  40. /package/src/styles/{utilities → css/utilities}/controls.css +0 -0
  41. /package/src/styles/{utilities → css/utilities}/display.css +0 -0
  42. /package/src/styles/{utilities → css/utilities}/divider.css +0 -0
  43. /package/src/styles/{utilities → css/utilities}/marquee.css +0 -0
  44. /package/src/styles/{utilities → css/utilities}/overlay.css +0 -0
  45. /package/src/styles/{utilities → css/utilities}/step.css +0 -0
  46. /package/src/styles/{utilities.css → css/utilities.css} +0 -0
@@ -1,542 +1,106 @@
1
- # Tailwind CSS v4 Styles Guide
1
+ # ui-core styles
2
2
 
3
- CSS architecture for `@djangocfg/ui-core` — **Tailwind v4** with semantic tokens, presets, and macOS-style glass utilities.
3
+ `@djangocfg/ui-core` owns the semantic CSS token contract used by every
4
+ frontend. CSS is the single source of truth for colors, typography, radius,
5
+ status surfaces, charts, and sidebar tokens.
4
6
 
5
- ## Directory layout
7
+ ## Layout
6
8
 
7
- ```
9
+ ```text
8
10
  styles/
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
11
- ├── theme.css # Imports tokens.css → animations → light → dark
12
- ├── base.css # Resets + `*` border-color + body bg/color + radius scale + native focus-outline reset
13
- ├── utilities.css # Custom utilities entry — imports utilities/*
14
- │ └── utilities/ # display · divider · overlay · controls · step · animations · glass · marquee
15
- ├── sources.css # @source directives for monorepo class detection
16
- ├── palette/ # JS-readable color access (Canvas/SVG/Mermaid)
17
- └── theme/
18
- ├── tokens.css # @theme inline { --color-X: var(--X) } + @theme { spacing/blur/z }
19
- ├── light.css # :root — full hsl(...) values
20
- ├── dark.css # .dark — full hsl(...) values
21
- └── animations.css # @keyframes (float, blob, morph, …)
22
-
23
- presets/
24
- ├── build.ts # buildThemeStyleSheet({preset}) → CSS string
25
- ├── types.ts # ThemeStylePresetId, ThemeCssVarMap
26
- ├── presets.ts # THEME_STYLE_PRESETS, THEME_STYLE_PRESET_ORDER
27
- └── themes/ # 8 preset definitions (see Presets section)
28
- ```
29
-
30
- ## Token format (Tailwind v4 `@theme inline` pattern)
31
-
32
- Tokens live in `:root` / `.dark` as **fully-wrapped CSS colors**, and `@theme inline` maps them by reference:
33
-
34
- ```css
35
- :root { --background: hsl(0 0% 94%); }
36
- .dark { --background: hsl(0 0% 4%); }
37
-
38
- @theme inline { --color-background: var(--background); }
39
- ```
40
-
41
- This makes opacity modifiers (`bg-card/40`, `border-foreground/20`) resolve through `color-mix(in oklab, …)` — they work for every semantic token, no helpers needed.
42
-
43
- > **Do NOT wrap tokens in `hsl(var(--X))`.** Tokens are already full colors, so `hsl(hsl(...))` is invalid and falls back to the default. Use `var(--X)` or `color-mix(in oklab, var(--X) N%, transparent)` for manual opacity.
44
-
45
- > **Do NOT set a token to a bare HSL triplet.** The utilities read the token
46
- > **raw** (`.bg-muted { background-color: var(--muted) }`), so `--muted: 0 0% 10%`
47
- > resolves to the *string* `"0 0% 10%"` — not a color — and the declaration is
48
- > silently dropped: **transparent fills + white-fallback borders.** Always write
49
- > the full color: `--muted: hsl(0 0% 10%)`. This bites most often when a component
50
- > overrides tokens **inline** (e.g. an old `ForceTheme` wrapper). If part of a page
51
- > has vanished backgrounds / white borders, that's this — see
52
- > [`../theme/TROUBLESHOOTING.md`](../theme/TROUBLESHOOTING.md) and prefer
53
- > `ThemeOverride` over inline token maps.
54
-
55
- ## Semantic tokens — never use raw color scales
56
-
57
- **Rule:** never write `bg-amber-500`, `text-green-700`, `border-gray-200`. Use semantic tokens — they adapt to both themes and to whatever preset is active.
58
-
59
- ### Base tokens
60
-
61
- | Class | Token | Purpose |
62
- |---|---|---|
63
- | `bg-background` / `text-foreground` | `--background` / `--foreground` | Page surface + body text |
64
- | `bg-card` / `text-card-foreground` | `--card` | Card surface (elevated over background) |
65
- | `bg-popover` / `text-popover-foreground` | `--popover` | Floating menus, tooltips, dropdowns |
66
- | `bg-muted` / `text-muted-foreground` | `--muted` | Subtle surface (input rest, chips, secondary text) |
67
- | `bg-accent` / `text-accent-foreground` | `--accent` | Hover + selected surface — **neutral gray** (no brand tint), quiet like macOS/Claude |
68
- | `bg-primary` / `text-primary-foreground` | `--primary` | Brand CTA (filled buttons, links) — cyan |
69
- | `bg-secondary` / `text-secondary-foreground` | `--secondary` | Neutral filled controls |
70
- | `bg-destructive` / `text-destructive-foreground` | `--destructive` | Error / delete filled controls |
71
- | `border-border` | `--border` | **Outlines only** — card frames, control/input edges. *Not* for separators (see Presets § Border vs divider) |
72
- | `border-divider` / `.divider-b` / `bg-divider` | `--divider` | **All separators** — columns, header `border-b`, list rows, resize handles. A soft hairline (translucent on dark themes) so the chrome never reads as a heavy grid |
73
- | `bg-overlay` | `--overlay` | Modal scrim / backdrop behind dialogs, drawers, sheets — black scrim in both themes (the token owns the opacity), **plus** a `backdrop-filter` frost via `--overlay-blur` (`utilities/overlay.css` augments this same class) |
74
- | `bg-input` | `--input` | Input **fill** — a notch off the panel so fields read as real controls (not flush holes). The input *border* uses `--border`, not `--input` |
75
- | `ring-ring` | `--ring` | Focus rings, selected outlines — **blue** (system-accent feel), independent of the cyan brand |
76
-
77
- ### Status surface tokens (light + dark)
78
-
79
- Each status has the full 4-token set in **both** themes for banners and alerts:
80
-
81
- | Role | Class | Token |
82
- |---|---|---|
83
- | Icon / accent | `text-warning` | `--warning` |
84
- | Banner background | `bg-warning-background` | `--warning-background` |
85
- | Readable text | `text-warning-foreground` | `--warning-foreground` |
86
- | Border ring | `border-warning-border` | `--warning-border` |
87
-
88
- Available statuses: **`warning`** · **`success`** · **`destructive`** · **`info`**. Reference consumers: `feedback/banner`, `specialized/copy`, `forms/button-download`, `data/status`, `data/stat`.
89
-
90
- ```tsx
91
- <div className="flex items-center gap-3 rounded-md border border-warning-border/40
92
- bg-warning-background px-3 py-2 text-xs text-warning-foreground">
93
- <Icon className="h-4 w-4 text-warning" />
94
- <span>You're on a preview plan.</span>
95
- </div>
96
- ```
97
-
98
- #### On-fill text — `*-foreground` vs `on-*` (read this before styling a badge)
99
-
100
- There are **two** text tokens per status and they are NOT interchangeable:
101
-
102
- | Token | Class | Sits on | Use for |
103
- |---|---|---|---|
104
- | `--{status}-foreground` | `text-success-foreground` | the tinted **`*-background`** | banner / alert copy (a *colored* tint) |
105
- | `--on-{status}` | `text-on-success` | the **solid `*` fill** | text/icon on a filled badge, unread pill, filled chip |
106
-
107
- `*-foreground` is a *status-colored* tint tuned for the faint banner surface — on the solid fill it produces **green-text-on-green-fill** (unreadable). `on-*` is a near-black / near-white contrast ink (the WhatsApp/Telegram pattern: dark text on the green pill) that meets WCAG AA against the fill in both themes.
108
-
109
- ```tsx
110
- {/* ✅ unread count pill — dark ink on the green fill */}
111
- <span className="rounded-full bg-success px-2 py-0.5 text-xs font-semibold text-on-success">
112
- {unread}
113
- </span>
114
-
115
- {/* ❌ green-on-green — *-foreground is for banners, not the fill */}
116
- <span className="bg-success text-success-foreground">{unread}</span>
117
- ```
118
-
119
- On-fill tokens exist for **`success`** · **`warning`** · **`info`** · **`destructive`** in both themes. (`--primary-foreground` / `--secondary-foreground` / `--destructive-foreground` already play the on-fill role for those base fills — they're genuine contrast colors, not tints, so no `on-*` is needed for them.) The base `on-*` is a near-black; a preset only re-declares it when it retints a fill dark enough that near-black fails — e.g. **`windows`** light mode sets `on-success` / `on-info` to white because Fluent's light green/blue fills are very dark.
120
-
121
- The **brand presets** (`macos` / `ios` / `windows` / `django-cfg`) retint the full
122
- status set to their own canvas, so banners read correctly on their custom
123
- backgrounds. The **modifier presets** (`soft` / `dense` / `high-contrast`) and
124
- `default` inherit the base status surfaces. Either way the four-token set is
125
- always defined, so `bg-warning-background` etc. are safe everywhere. For a fully
126
- preset-agnostic surface you can still derive from the base color with opacity:
127
-
128
- ```tsx
129
- <div className="rounded-md border border-warning/30 bg-warning/10 text-warning">…</div>
130
- ```
131
-
132
- ### Code & sidebar tokens
133
-
134
- | Class | Purpose |
135
- |---|---|
136
- | `bg-code` / `text-code-foreground` / `border-code-border` | Code block panels (markdown fences, terminal blocks) |
137
- | `bg-code-inline` / `text-code-inline-foreground` | Inline `<code>` chips |
138
- | `bg-sidebar` / `text-sidebar-foreground` / `border-sidebar-border` | App sidebar chrome |
139
- | `bg-sidebar-accent` / `text-sidebar-accent-foreground` | Sidebar hover state |
140
-
141
- ### Chart tokens (categorical palette)
142
-
143
- `--chart-1 … --chart-5` are the categorical series colors (chart-1 = brand hue).
144
- Like every color token they are **fully-wrapped `hsl(...)`** and bound to
145
- Tailwind via `--color-chart-*` in `tokens.css`, so the utilities work with
146
- opacity modifiers:
147
-
148
- ```tsx
149
- <div className="bg-chart-1" /> {/* solid */}
150
- <div className="bg-chart-3/40" /> {/* 40% via color-mix */}
151
- <span className="text-chart-2" />
152
- ```
153
-
154
- For Recharts / SVG / Canvas, pass the variable directly — **never** wrap it:
155
-
156
- ```tsx
157
- <Bar fill="var(--chart-1)" /> {/* ✅ */}
158
- <Bar fill="hsl(var(--chart-1))" /> {/* ❌ hsl(hsl(...)) — invalid */}
159
- ```
160
-
161
- > **JIT-scan gotcha (charts/status).** `bg-chart-${n}` / `bg-${status}-background`
162
- > built from template literals are invisible to Tailwind's static scanner —
163
- > only literal classes get a CSS rule. Use literal class names (or inline
164
- > `style={{ background: 'var(--chart-N)' }}`) when the index is dynamic.
165
-
166
- ### Typography tokens
167
-
168
- `--font-sans` / `--font-mono` and the size scale (`--font-size-xs … -xl`,
169
- `--line-height-base`, `--letter-spacing-base`) live in `base.css` and are
170
- **overridable per preset** (e.g. `macos` → SF Pro, `windows` → Segoe UI
171
- Variable). `tokens.css` bridges the size scale onto Tailwind's `--text-*` tokens,
172
- so `font-sans` / `font-mono` and `text-xs … text-xl` follow the active preset
173
- instead of Tailwind's hardcoded defaults. `body` applies font-sans + base
174
- size/line-height/tracking directly.
175
-
176
- #### The font-size scale → `text-*` bridge (one source of truth)
177
-
178
- The size tokens are plain rem values. The `macos` preset
179
- (`presets/themes/macos.ts`) sets them to the Apple HIG point scale:
180
-
181
- | Token | macos value | px (@1×) | Used for |
182
- |---|---|---|---|
183
- | `--font-size-xs` | `0.6875rem` | 11px | captions, timestamps |
184
- | `--font-size-sm` | `0.75rem` | 12px | footnotes, secondary labels |
185
- | `--font-size-base` | `0.8125rem` | 13px | HIG default body |
186
- | `--font-size-lg` | `0.9375rem` | 15px | subheadings |
187
- | `--font-size-xl` | `1.0625rem` | 17px | titles, nav bar |
188
-
189
- **The key fact:** Tailwind's `text-*` utilities don't read their own hardcoded
190
- sizes — they're bridged to these vars in `tokens.css` via `@theme inline`:
191
-
192
- ```css
193
- @theme inline {
194
- --text-xs: var(--font-size-xs);
195
- --text-sm: var(--font-size-sm);
196
- --text-base: var(--font-size-base);
197
- --text-lg: var(--font-size-lg);
198
- --text-xl: var(--font-size-xl);
199
- }
200
- ```
201
-
202
- So `--font-size-*` is the **single source of truth** for text sizing: change one
203
- var and **every** `text-sm` / `text-base` / … in that scope re-sizes uniformly —
204
- no per-component edits, no chasing `text-[15px]` literals across the tree.
205
-
206
- #### Override recipe (a) — global bump via `buildThemeStyleSheet`
207
-
208
- To lift the whole UI a notch (e.g. a desktop consumer that wants 15px body),
209
- pass `vars` alongside the preset — they merge on top of the preset's values per
210
- mode (`buildThemeStyleSheet` → `mergeLayer`), emitting `:root` (light) and
211
- `.dark` blocks. Every `text-*` utility moves with them:
212
-
213
- ```ts
214
- import { buildThemeStyleSheet } from '@djangocfg/ui-core/styles/presets';
215
-
216
- const css = buildThemeStyleSheet({
217
- preset: 'macos',
218
- vars: {
219
- light: { 'font-size-base': '0.9375rem', 'font-size-sm': '0.8125rem' },
220
- dark: { 'font-size-base': '0.9375rem', 'font-size-sm': '0.8125rem' },
221
- },
222
- });
223
- // inject `css` after ui-core/styles (cmdop does exactly this)
224
- ```
225
-
226
- > Keys are the bare token name (no `--` prefix); `buildThemeStyleSheet` adds it.
227
-
228
- #### Override recipe (b) — scoped bump on a selector
229
-
230
- To re-size only a subtree, re-declare the font-size vars on a selector. The
231
- bridge re-points `text-*` for everything inside it — no preset rebuild:
232
-
233
- ```css
234
- .compact-panel {
235
- --font-size-base: 0.75rem; /* 12px */
236
- --font-size-sm: 0.6875rem; /* 11px */
237
- }
238
- ```
239
-
240
- **Prefer either recipe over per-component `text-[15px]` hacks** — those drift
241
- from the scale and don't follow the preset or theme.
242
-
243
- ## Radius tokens
244
-
245
- The radius **scale** (`--radius`, `--radius-sm`, …) is theme-independent and lives in `base.css`; presets that set a semantic `radius` regenerate the scale via `presets/build.ts`. A few named radii are fixed defaults (default preset only):
246
-
247
- | Token | Value | Used by |
248
- |---|---|---|
249
- | `--radius-control` | `0.625rem` | **Interactive controls** (inputs, nav items, search, value chips) — one shared corner so they round consistently. Class: `.rounded-control` |
250
- | `--radius-dialog` | `1rem` | Dialog panels. Applied at **all** sizes (was `sm:`-gated, which left phones square) |
251
- | `--radius-popover` | `0.75rem` | Popovers / menus |
252
-
253
- ## Focus rings (Vercel / Linear pattern)
254
-
255
- Inputs use a **crisp thin accent outline**, not a blurry halo: the border turns `--ring` (blue) plus a tight `ring-1 ring-ring`, `:focus-visible` only.
256
-
257
- ```
258
- focus-visible:border-ring focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring
259
- ```
260
-
261
- The native browser focus outline (often white/auto, which used to pierce through on click) is reset app-wide in `base.css`:
11
+ ├── css/ # complete CSS layer
12
+ │ ├── full.css # Tailwind v4 + tokens + base + utilities
13
+ │ ├── index.css # unlayered compatibility entry
14
+ │ ├── globals.css # backwards-compatible global entry
15
+ │ ├── base.css # resets and document defaults
16
+ │ ├── sources.css # Tailwind @source directives
17
+ │ ├── theme.css # token/animation aggregator
18
+ │ ├── theme/ # tokens, light, dark, animations
19
+ │ ├── utilities.css # ui-core utility aggregator
20
+ │ ├── utilities/ # focused utility modules
21
+ │ └── presets/ # authoritative static preset token sheets
22
+ │ ├── default.css
23
+ │ ├── django-cfg.css
24
+ │ ├── ios.css
25
+ │ ├── macos.css
26
+ │ ├── windows.css
27
+ │ ├── soft.css
28
+ │ ├── dense.css
29
+ │ └── high-contrast.css
30
+ ├── palette/ # runtime readers of computed CSS variables
31
+ └── presets/ # TypeScript names/order only
32
+ ├── index.ts
33
+ ├── presets.ts
34
+ └── types.ts
35
+ ```
36
+
37
+ There are deliberately no TypeScript color maps and no runtime CSS generator.
38
+ Do not add a second palette in React or TypeScript.
39
+
40
+ ## Consumer contract
41
+
42
+ Import the golden path first, then exactly one product preset:
262
43
 
263
44
  ```css
264
- :where(input, textarea, select, button, a, [tabindex], [role="button"], [contenteditable]):focus { outline: none; }
265
- ```
266
-
267
- So every interactive control relies on its own `focus-visible:ring-*` — keyboard a11y is preserved, the stray native outline is gone. The single source for input styling is `INPUT_CLASS` / `inputClass(size)` exported from `components/forms/input` and **reused by `Editable`**, so standalone inputs and inline-edits focus identically.
268
-
269
- ## Scan-independent utility classes
270
-
271
- Some classes are authored as **plain CSS** in `utilities/*.css` (not Tailwind utilities):
272
-
273
- | Class | File | Why plain CSS |
274
- |---|---|---|
275
- | `.divider-b` / `.divider-t` | `utilities/divider.css` | Hairline via `--divider` |
276
- | `.bg-overlay` (frost) | `utilities/overlay.css` | Adds `backdrop-filter: blur(var(--overlay-blur))` onto Tailwind's own `bg-overlay` so modal backdrops frost the page. Tune via `--overlay-blur` (base.css, default 2px; `0px` disables). Attached to `.bg-overlay` — not a new class — so overlays and host forks that render `bg-overlay` all get it |
277
- | `.rounded-control` | `utilities/controls.css` | Shared control radius |
278
-
279
- > **JIT-scan gotcha.** Tailwind's content scan covers `ui-core/src` but **not always every consumer package** (e.g. the `layouts` package source isn't always scanned by Storybook/apps). A *new* arbitrary Tailwind class used **only** in a consumer (`border-zinc-500/25`, `border-foreground/[0.12]`, `rounded-[var(--x)]`) then produces **no CSS rule** and silently falls back to the global `* { border-color: var(--border) }` — the class is in the DOM but the computed color is wrong. **Fix pattern:** for any token-driven visual that consumers need, add a plain `.class` in `ui-core/styles/utilities/*` and `@import` it, then use that class downstream. Don't invent new Tailwind classes in the `layouts` package.
280
-
281
- ## Theme presets — 8 production-ready
282
-
283
- Two families, with different coverage by design:
284
-
285
- - **Brand / OS presets** declare a *full* token set — colors, sidebar, charts,
286
- status surfaces, divider, and (macos/windows) typography — so the identity is
287
- self-contained and survives layering over any base.
288
- - **Modifier presets** override only the chrome they're about (radius / borders /
289
- contrast) and **inherit** brand colors, charts, and status from whatever is
290
- active beneath them — so they compose (`django-cfg` + `dense`, etc.).
291
-
292
- | ID | Family | Use case |
293
- |---|---|---|
294
- | `default` | base | Default ui-core theme — cyan brand |
295
- | `django-cfg` | brand | Brand identity — brand-washed accent/sidebar + on-brand `info` |
296
- | `macos` | brand | Pixel-accurate Apple HIG (Sequoia / Tahoe 26) — SF Pro, Wails/Electron desktop |
297
- | `ios` | brand | iOS app feel — 0.75rem radius, systemBlue, iOS status colors |
298
- | `windows` | brand | Microsoft Fluent 2 — Segoe UI Variable, 0.375rem radius |
299
- | `soft` | modifier | Larger radius (1rem) — friendly marketing surfaces |
300
- | `dense` | modifier | Smaller radius (0.25rem) — data-heavy admin UIs |
301
- | `high-contrast` | modifier | A11y boost — stronger borders, harder text, pure canvas |
302
-
303
- #### Border vs divider — outline weight vs translucent hairline
304
-
305
- `--border` and `--divider` are **two different roles**, not two shades of one:
306
-
307
- - **`--border`** — an OUTLINE: opaque, control-weight. Card frames, input
308
- edges, panel outlines. Use `border-border` / `border border-border`.
309
- - **`--divider`** — a SEPARATOR: a soft hairline between things. Shell columns,
310
- header `border-b` rules, list rows, resize handles. Use `border-divider` /
311
- `.divider-b` / `bg-divider`, or `divide-*` on a `divide-y` stack.
312
-
313
- On the **dark** themes the separator token is now a **translucent** hairline —
314
- `hsl(H S 46-48% / 0.18)` — mirroring Apple's own `rgba(84,84,88,0.36)` approach:
315
- a light grey at low alpha that *dissolves into whatever sits behind it* instead
316
- of a fixed opaque line. This is deliberate. An opaque separator (even a dim one)
317
- read as a hard grid on a near-black canvas — every column edge and header rule
318
- looked heavy. A translucent hairline self-adapts: heavier where it crosses an
319
- elevated card, lighter on the page. Every dark preset carries it (`macos`,
320
- `ios`, `windows`, `dense`, `soft`, and the base `dark.css` behind
321
- `default`/`django-cfg`); **`high-contrast` keeps an opaque divider on purpose**
322
- (a11y wants the harder line). Light themes keep an opaque divider too — a
323
- translucent line on white gains nothing.
324
-
325
- The alpha was tuned by eye in a live cmdop-web session (0.18 is quiet-but-present;
326
- lower starts to vanish on the page).
327
-
328
- **If a dark surface looks too contrasty**, the culprit is almost always a
329
- column/row/header separator wrongly drawn with `--border` (the outline weight)
330
- instead of `--divider` — move it to `border-divider` / `var(--divider)`.
331
-
332
- ### Apply a preset
333
-
334
- ```tsx
335
- import { buildThemeStyleSheet } from '@djangocfg/ui-core/styles/presets';
336
-
337
- // Static: bake into your app's CSS at build time
338
- const css = buildThemeStyleSheet({ preset: 'macos' });
339
- // then write `css` to a file and `@import` it after ui-core/styles
340
-
341
- // Runtime: inject into <head> (use case: settings picker)
342
- useEffect(() => {
343
- const el = document.createElement('style');
344
- el.textContent = buildThemeStyleSheet({ preset });
345
- document.head.appendChild(el);
346
- return () => el.remove();
347
- }, [preset]);
45
+ @import "@djangocfg/ui-core/styles/full";
46
+ @import "@djangocfg/ui-core/styles/presets/macos";
348
47
  ```
349
48
 
350
- ### Override individual tokens
49
+ The preset export resolves to `src/styles/css/presets/macos.css`; consumers do
50
+ not depend on the physical path. The preset defines both `:root` (light) and
51
+ `.dark` (dark) values. `ThemeProvider` only controls the `html.dark` class.
351
52
 
352
- To re-tint only a couple of variables without copying a whole preset:
53
+ For a product-specific adjustment, add a small override after the preset:
353
54
 
354
55
  ```css
355
- :root.my-brand {
356
- --primary: hsl(280 80% 55%);
357
- --ring: hsl(280 80% 55%);
56
+ :root, .dark {
57
+ --font-size-base: 0.9375rem;
58
+ --font-size-sm: 0.875rem;
358
59
  }
359
60
  ```
360
61
 
361
- Toggle `<html class="my-brand">` and the override applies on top of whichever preset is active.
362
-
363
- ## Glass utilities — macOS / Windows / Liquid
364
-
365
- `utilities.css` ships four backdrop-blur classes. Each is built with `color-mix` over a token, so they work in any preset / theme.
366
-
367
- | Class | Recipe | When |
368
- |---|---|---|
369
- | `.glass-macos` | `blur(20px) saturate(180%)` · 72% `--background` | Sidebar, popovers, sheet headers |
370
- | `.glass-liquid` | `blur(12px) saturate(180%)` · 60% `--card` + inner highlight + border | Floating chrome (macOS 26 Dock, FAB) |
371
- | `.glass-header` | `blur(8px) saturate(160%)` · 80% `--background` | Nav bars, status strips |
372
- | `.glass-win11` | `blur(60px) saturate(125%)` · 85% `--background` | Windows Mica / Acrylic |
373
-
374
- Each requires a **non-transparent parent** (something for the blur to chew on). Avoid stacking — blur compounds.
375
-
376
- ## App setup
377
-
378
- ### Golden path (recommended) — one import, layer-safe
379
-
380
- ```css
381
- /* Single line. Imports Tailwind + tokens + base + utilities in the
382
- correct cascade layers. Put it FIRST; other package CSS after. */
383
- @import "@djangocfg/ui-core/styles/full";
384
-
385
- @import "@djangocfg/layouts/styles";
386
- @import "@djangocfg/ui-tools/styles";
387
- ```
388
-
389
- `…/styles/full` (`full.css`) pins ui-core's base resets to `@layer base`
390
- and its custom utilities to `@layer utilities` via `@import "…" layer(name)`.
391
- A `layer()`-qualified import is folded into that layer **regardless of
392
- import order or build tool**, so you cannot get the ordering wrong, and
393
- you do not need to import `tailwindcss` yourself.
394
-
395
- ### The cascade-layer rule (why ordering matters)
396
-
397
- Tailwind v4 emits its utilities inside `@layer utilities`. **Unlayered
398
- CSS beats any layered rule** in the cascade. So if a package's base
399
- resets (here `* { border-color }` and the `body` background/font rules in
400
- `base.css`) are emitted *unlayered*, they sit above `@layer utilities`
401
- and silently defeat layout utilities (`gap`, `space-y`, `divide`, `flex`,
402
- `border`, `padding`). Colors usually survive because they flow through
403
- CSS vars (no cascade conflict), so the breakage is invisible in Chrome
404
- but shows up in stricter engines (WKWebView).
62
+ Do not create a `theme-preset.ts`, append a `<style>` tag, or call a token
63
+ builder during application startup.
405
64
 
406
- Whether a *plain*, unlayered `@import` lands in a layer depends on its
407
- position relative to `@import "tailwindcss"` **and** on the build tool
408
- (Vite vs Next.js resolve `@import` differently). `full.css` removes that
409
- dependency by binding each file to a layer explicitly. **Use `…/styles/full`
410
- and this whole class of bug cannot occur.**
65
+ ## Token format
411
66
 
412
- ### Manual ordering (only if you manage layers yourself)
413
-
414
- The plain `@djangocfg/ui-core/styles` entry imports `theme.css` +
415
- `sources.css` + `base.css` + `utilities.css` **unlayered** (it does not
416
- import Tailwind). If you use it, you own the layer ordering. The Next.js
417
- demo does this and works because PostCSS folds the trailing
418
- `@import "tailwindcss"` such that the resets still end up benign — but a
419
- Vite consumer that puts `@import "tailwindcss"` last hit exactly the bug
420
- above. If you must hand-order, import `tailwindcss` **first**:
67
+ Color variables are complete CSS colors, never bare HSL triplets:
421
68
 
422
69
  ```css
423
- @import "tailwindcss"; /* establishes the layers first */
424
- @import "@djangocfg/ui-core/styles";
425
- @import "@djangocfg/layouts/styles";
426
- @import "@djangocfg/ui-tools/styles";
70
+ :root { --background: hsl(240 17% 97%); }
71
+ .dark { --background: hsl(240 5% 8%); }
427
72
  ```
428
73
 
429
- > **No `@plugin "tailwindcss-animate"` needed** in v4 — the keyframes ship via `theme/animations.css`. Use `tw-animate-css` instead if you need extra utilities.
430
-
431
- ### Why `@source` is required
432
-
433
- Tailwind v4 doesn't scan across npm packages automatically. Each consumer needs either a `@source` directive or to import a `sources.css` from the package — `@djangocfg/ui-core/styles` already chains its own `sources.css`, so importing `…/styles` is enough.
434
-
435
- ### Wiring a custom display font (consumer recipe)
436
-
437
- `ui-core` ships `.font-display` and a display type ramp (`.text-display-xl` / `.text-display-lg` / `.text-display`) that read from a `--font-display` CSS variable. The variable itself is **not** set — apps pick the font.
438
-
439
- In a Next.js app with `next/font`:
74
+ Tailwind maps semantic utilities to these variables through `@theme inline`.
75
+ Use `var(--background)` or `bg-background`; do not write
76
+ `hsl(var(--background))`.
440
77
 
441
- ```tsx
442
- // app/layout.tsx
443
- import { Plus_Jakarta_Sans } from 'next/font/google';
444
-
445
- const display = Plus_Jakarta_Sans({
446
- subsets: ['latin'],
447
- weight: ['700', '800'],
448
- variable: '--font-display', // exposes as CSS var
449
- display: 'swap',
450
- });
451
-
452
- export default function RootLayout({ children }: { children: React.ReactNode }) {
453
- return (
454
- <html lang="en" className={display.variable}>
455
- <body>{children}</body>
456
- </html>
457
- );
458
- }
459
- ```
460
-
461
- Then in `globals.css` register it as a Tailwind token so the `font-display` utility class works:
78
+ Typography variables are ordinary CSS values:
462
79
 
463
80
  ```css
464
- @theme {
465
- --font-display: var(--font-display), ui-sans-serif, system-ui, sans-serif;
81
+ :root, .dark {
82
+ --font-size-base: 0.8125rem;
83
+ --font-size-sm: 0.75rem;
466
84
  }
467
85
  ```
468
86
 
469
- Now any of these work:
470
-
471
- ```tsx
472
- <h1 className="text-7xl text-display-xl">Hero title</h1>
473
- <h2 className="text-4xl text-display-lg">Section title</h2>
474
- <h3 className="text-xl text-display">Card title</h3>
475
- <span className="font-display">Plain display family</span>
476
- ```
477
-
478
- ## Theme Showcase story
479
-
480
- The `UI Core/Theme Showcase` story in djangocfg storybook renders every base
481
- token (incl. `divider`), the real status surfaces (`*-background`/`*-foreground`/
482
- `*-border`), the chart palette (solid + /40 opacity), the typography scale,
483
- button variants, cards, form controls, glass utilities, and an opacity
484
- sanity-check on one page. Switch the `preset` control to flip across all 8
485
- themes; flip light/dark from the toolbar.
87
+ ## Preset rules
486
88
 
487
- Use it to validate any token change before publishing — every regression shows up on one screen.
89
+ - Presets are authored directly in `presets/css/`.
90
+ - Every preset must define valid `hsl(...)`, `color-mix(...)`, or other complete
91
+ CSS color values.
92
+ - Keep light and dark pairs together in the same file.
93
+ - A preset may omit tokens that intentionally inherit from `theme.css`.
94
+ - Changes to a preset require checking both light and dark modes.
95
+ - Product density overrides belong in the consuming app's CSS, not in a copied
96
+ TypeScript map.
488
97
 
489
- ## Local edits → consumer sync (debugging only)
490
-
491
- These packages are usually consumed via pinned npm versions. To hot-test a change without publishing:
98
+ ## Verification
492
99
 
493
100
  ```bash
494
- CONSUMER=/path/to/consumer/frontend/node_modules/.pnpm/@djangocfg+ui-core@…/node_modules/@djangocfg/ui-core/src/styles/theme
495
- SRC=./src/styles/theme
496
-
497
- cp "$SRC/light.css" "$CONSUMER/light.css"
498
- cp "$SRC/dark.css" "$CONSUMER/dark.css"
499
- cp "$SRC/tokens.css" "$CONSUMER/tokens.css"
500
- ```
501
-
502
- Restart the consumer's dev server (Next.js / Vite cache module resolution). Revert before committing — `file:` paths in `package.json` cause `@types/react` dedup failures.
503
-
504
- ## Gotchas
505
-
506
- ### Arbitrary Tailwind values
507
-
508
- `h-[80px]`, `z-[100]` etc. may not work in v4. Prefer scaled tokens (`h-20`, `z-100`) — they're already registered in `tokens.css` under `--spacing-*` / `--z-index-*`.
509
-
510
- ### Opacity in arbitrary classes
511
-
512
- `shadow-[0_0_0_1px_var(--ring)]` works (underscores get parsed as spaces). For nested `color-mix`, use underscores everywhere:
513
-
514
- ```tsx
515
- className="bg-[color-mix(in_oklab,var(--primary)_30%,transparent)]"
516
- ```
517
-
518
- ### Glass over transparent background
519
-
520
- `.glass-*` needs an opaque parent to blur. On a fully transparent canvas (e.g. Wails translucent window) put a base layer first or the blur is invisible.
521
-
522
- ## Programmatic palette access (JS / Canvas / SVG)
523
-
524
- For contexts that can't read CSS vars (Canvas 2D, Mermaid, react-pdf, SVG fill attrs):
525
-
526
- ```tsx
527
- import {
528
- useThemePalette,
529
- useStylePresets,
530
- useBoxColors,
531
- alpha,
532
- } from '@djangocfg/ui-core/styles/palette';
533
-
534
- const palette = useThemePalette();
535
- ctx.fillStyle = palette.primary; // '#0989aa'
536
- ctx.fillStyle = alpha(palette.warning, 0.15); // 'rgba(…, 0.15)'
537
-
538
- const presets = useStylePresets();
539
- presets.success // { fill: '#…', stroke: '#…', color: '#fff' }
101
+ pnpm check
540
102
  ```
541
103
 
542
- These hooks resolve the live CSS values via `getComputedStyle` and re-run on theme changes.
104
+ At the consumer, build the app that imports the preset and verify both modes.
105
+ The important invariant is that changing a preset requires editing one CSS
106
+ file, and no React runtime code is involved in applying it.
@@ -23,7 +23,7 @@
23
23
  * / sheet) blurs the page behind it. Theme-agnostic (same both modes);
24
24
  * a preset can override for a heavier/lighter frost, or set `0px` to disable
25
25
  * the blur while keeping the `--overlay` dimming scrim. 2px = a subtle frost. */
26
- --overlay-blur: 2px;
26
+ --overlay-blur: 5px;
27
27
 
28
28
  /* Typography tokens — overridable per preset */
29
29
  --font-sans: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
@@ -0,0 +1 @@
1
+ /* Authoritative static preset: default. */