@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.
- package/README.md +1 -1
- package/package.json +19 -11
- package/src/styles/README.md +74 -510
- package/src/styles/{base.css → css/base.css} +1 -1
- package/src/styles/css/presets/default.css +1 -0
- package/src/styles/css/presets/dense.css +36 -0
- package/src/styles/css/presets/django-cfg.css +44 -0
- package/src/styles/css/presets/high-contrast.css +24 -0
- package/src/styles/css/presets/ios.css +120 -0
- package/src/styles/css/presets/macos.css +138 -0
- package/src/styles/css/presets/soft.css +52 -0
- package/src/styles/css/presets/windows.css +140 -0
- package/src/styles/{utilities → css/utilities}/glass.css +16 -0
- package/src/styles/presets/index.ts +2 -15
- package/src/styles/presets/presets.ts +0 -3
- package/src/styles/presets/types.ts +4 -148
- package/src/theme/README.md +36 -64
- package/src/theme/TROUBLESHOOTING.md +4 -0
- package/src/styles/presets/build.ts +0 -64
- package/src/styles/presets/themes/default.ts +0 -6
- package/src/styles/presets/themes/dense.ts +0 -31
- package/src/styles/presets/themes/django-cfg.ts +0 -62
- package/src/styles/presets/themes/high-contrast.ts +0 -35
- package/src/styles/presets/themes/index.ts +0 -24
- package/src/styles/presets/themes/ios.ts +0 -114
- package/src/styles/presets/themes/macos.ts +0 -190
- package/src/styles/presets/themes/soft.ts +0 -48
- package/src/styles/presets/themes/types.ts +0 -6
- package/src/styles/presets/themes/windows.ts +0 -154
- /package/src/styles/{full.css → css/full.css} +0 -0
- /package/src/styles/{globals.css → css/globals.css} +0 -0
- /package/src/styles/{index.css → css/index.css} +0 -0
- /package/src/styles/{sources.css → css/sources.css} +0 -0
- /package/src/styles/{theme → css/theme}/animations.css +0 -0
- /package/src/styles/{theme → css/theme}/dark.css +0 -0
- /package/src/styles/{theme → css/theme}/light.css +0 -0
- /package/src/styles/{theme → css/theme}/tokens.css +0 -0
- /package/src/styles/{theme.css → css/theme.css} +0 -0
- /package/src/styles/{utilities → css/utilities}/animations.css +0 -0
- /package/src/styles/{utilities → css/utilities}/controls.css +0 -0
- /package/src/styles/{utilities → css/utilities}/display.css +0 -0
- /package/src/styles/{utilities → css/utilities}/divider.css +0 -0
- /package/src/styles/{utilities → css/utilities}/marquee.css +0 -0
- /package/src/styles/{utilities → css/utilities}/overlay.css +0 -0
- /package/src/styles/{utilities → css/utilities}/step.css +0 -0
- /package/src/styles/{utilities.css → css/utilities.css} +0 -0
package/src/styles/README.md
CHANGED
|
@@ -1,542 +1,106 @@
|
|
|
1
|
-
#
|
|
1
|
+
# ui-core styles
|
|
2
2
|
|
|
3
|
-
|
|
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
|
-
##
|
|
7
|
+
## Layout
|
|
6
8
|
|
|
7
|
-
```
|
|
9
|
+
```text
|
|
8
10
|
styles/
|
|
9
|
-
├──
|
|
10
|
-
├──
|
|
11
|
-
├──
|
|
12
|
-
├──
|
|
13
|
-
├──
|
|
14
|
-
│
|
|
15
|
-
├──
|
|
16
|
-
├──
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
├──
|
|
25
|
-
├──
|
|
26
|
-
├──
|
|
27
|
-
└──
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
53
|
+
For a product-specific adjustment, add a small override after the preset:
|
|
353
54
|
|
|
354
55
|
```css
|
|
355
|
-
:root.
|
|
356
|
-
--
|
|
357
|
-
--
|
|
56
|
+
:root, .dark {
|
|
57
|
+
--font-size-base: 0.9375rem;
|
|
58
|
+
--font-size-sm: 0.875rem;
|
|
358
59
|
}
|
|
359
60
|
```
|
|
360
61
|
|
|
361
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
424
|
-
|
|
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
|
-
|
|
430
|
-
|
|
431
|
-
|
|
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
|
-
|
|
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
|
-
|
|
465
|
-
--font-
|
|
81
|
+
:root, .dark {
|
|
82
|
+
--font-size-base: 0.8125rem;
|
|
83
|
+
--font-size-sm: 0.75rem;
|
|
466
84
|
}
|
|
467
85
|
```
|
|
468
86
|
|
|
469
|
-
|
|
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
|
-
|
|
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
|
-
##
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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. */
|