@djangocfg/ui-core 2.1.426 → 2.1.428

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 (52) hide show
  1. package/README.md +5 -3
  2. package/package.json +4 -4
  3. package/src/components/data/badge/index.tsx +1 -1
  4. package/src/components/data/calendar/calendar.tsx +2 -2
  5. package/src/components/data/stat/index.tsx +5 -5
  6. package/src/components/data/status/index.tsx +3 -3
  7. package/src/components/data/table/index.tsx +30 -11
  8. package/src/components/feedback/banner/index.tsx +8 -4
  9. package/src/components/forms/button/index.tsx +15 -5
  10. package/src/components/forms/button-download/index.tsx +2 -2
  11. package/src/components/forms/checkbox/index.tsx +1 -1
  12. package/src/components/forms/editable/index.tsx +19 -19
  13. package/src/components/forms/input/index.tsx +44 -9
  14. package/src/components/forms/otp/index.tsx +1 -1
  15. package/src/components/forms/setting-row/index.tsx +363 -0
  16. package/src/components/forms/switch/index.tsx +1 -1
  17. package/src/components/forms/tags-input/index.tsx +1 -1
  18. package/src/components/forms/textarea/index.tsx +3 -8
  19. package/src/components/index.ts +2 -0
  20. package/src/components/navigation/dropdown-menu/index.tsx +3 -1
  21. package/src/components/navigation/menu/menu-builder.tsx +7 -2
  22. package/src/components/navigation/stepper/index.tsx +1 -1
  23. package/src/components/navigation/tabs/index.tsx +3 -3
  24. package/src/components/overlay/dialog/index.tsx +8 -3
  25. package/src/components/overlay/sheet/index.tsx +1 -1
  26. package/src/components/overlay/tooltip/index.tsx +4 -1
  27. package/src/components/select/multi-select-pro-async.tsx +2 -2
  28. package/src/components/select/multi-select-pro.tsx +2 -2
  29. package/src/components/specialized/copy/index.tsx +2 -2
  30. package/src/components/specialized/item/index.tsx +1 -1
  31. package/src/hooks/router/README.md +4 -1
  32. package/src/lib/env.ts +6 -1
  33. package/src/styles/README.md +115 -22
  34. package/src/styles/base.css +18 -1
  35. package/src/styles/presets/index.ts +1 -0
  36. package/src/styles/presets/themes/dense.ts +11 -0
  37. package/src/styles/presets/themes/django-cfg.ts +43 -2
  38. package/src/styles/presets/themes/high-contrast.ts +25 -9
  39. package/src/styles/presets/themes/ios.ts +32 -0
  40. package/src/styles/presets/themes/macos.ts +36 -0
  41. package/src/styles/presets/themes/soft.ts +13 -0
  42. package/src/styles/presets/themes/windows.ts +34 -0
  43. package/src/styles/presets/types.ts +36 -2
  44. package/src/styles/theme/dark.css +48 -32
  45. package/src/styles/theme/light.css +21 -13
  46. package/src/styles/theme/tokens.css +23 -0
  47. package/src/styles/utilities/controls.css +12 -0
  48. package/src/styles/utilities/divider.css +23 -0
  49. package/src/styles/utilities.css +2 -0
  50. package/src/theme/ThemeSegmented.tsx +73 -0
  51. package/src/theme/index.ts +2 -0
  52. package/src/types/index.ts +0 -0
@@ -9,8 +9,9 @@ styles/
9
9
  ├── full.css # Golden path (recommended) — Tailwind + tokens + base + utilities, cascade-layer-safe
10
10
  ├── index.css # Plain entry (no Tailwind, unlayered) — you own layer ordering
11
11
  ├── theme.css # Imports tokens.css → animations → light → dark
12
- ├── base.css # Resets + `*` border-color + body bg/color
13
- ├── utilities.css # Custom utilities (.glass-*, .step, animations)
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 · controls · step · animations · glass · marquee
14
15
  ├── sources.css # @source directives for monorepo class detection
15
16
  ├── palette/ # JS-readable color access (Canvas/SVG/Mermaid)
16
17
  └── theme/
@@ -53,17 +54,18 @@ This makes opacity modifiers (`bg-card/40`, `border-foreground/20`) resolve thro
53
54
  | `bg-card` / `text-card-foreground` | `--card` | Card surface (elevated over background) |
54
55
  | `bg-popover` / `text-popover-foreground` | `--popover` | Floating menus, tooltips, dropdowns |
55
56
  | `bg-muted` / `text-muted-foreground` | `--muted` | Subtle surface (input rest, chips, secondary text) |
56
- | `bg-accent` / `text-accent-foreground` | `--accent` | Hover surface |
57
- | `bg-primary` / `text-primary-foreground` | `--primary` | Brand CTA (filled buttons, links) |
57
+ | `bg-accent` / `text-accent-foreground` | `--accent` | Hover + selected surface — **neutral gray** (no brand tint), quiet like macOS/Claude |
58
+ | `bg-primary` / `text-primary-foreground` | `--primary` | Brand CTA (filled buttons, links) — cyan |
58
59
  | `bg-secondary` / `text-secondary-foreground` | `--secondary` | Neutral filled controls |
59
60
  | `bg-destructive` / `text-destructive-foreground` | `--destructive` | Error / delete filled controls |
60
- | `border-border` | `--border` | Dividers, card outlines, separators |
61
- | `bg-input` | `--input` | Input field border / fill (theme dependent) |
62
- | `ring-ring` | `--ring` | Focus rings, selected outlines |
61
+ | `border-border` | `--border` | Card outlines, control borders |
62
+ | `border-divider` / `.divider-b` | `--divider` | **Hairline between rows** deliberately *lighter than `--card`* so it stays visible on elevated surfaces (a `--border` line vanishes on a card) |
63
+ | `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` |
64
+ | `ring-ring` | `--ring` | Focus rings, selected outlines — **blue** (system-accent feel), independent of the cyan brand |
63
65
 
64
- ### Status surface tokens (default + dark themes only)
66
+ ### Status surface tokens (light + dark)
65
67
 
66
- Each status has 4 tokens for banners and alerts:
68
+ Each status has the full 4-token set in **both** themes for banners and alerts:
67
69
 
68
70
  | Role | Class | Token |
69
71
  |---|---|---|
@@ -72,7 +74,7 @@ Each status has 4 tokens for banners and alerts:
72
74
  | Readable text | `text-warning-foreground` | `--warning-foreground` |
73
75
  | Border ring | `border-warning-border` | `--warning-border` |
74
76
 
75
- Available statuses: **`warning`** · **`success`** · **`destructive`** · **`info`**.
77
+ Available statuses: **`warning`** · **`success`** · **`destructive`** · **`info`**. Reference consumers: `feedback/banner`, `specialized/copy`, `forms/button-download`, `data/status`, `data/stat`.
76
78
 
77
79
  ```tsx
78
80
  <div className="flex items-center gap-3 rounded-md border border-warning-border/40
@@ -82,7 +84,12 @@ Available statuses: **`warning`** · **`success`** · **`destructive`** · **`in
82
84
  </div>
83
85
  ```
84
86
 
85
- Most presets don't override `*-background` / `*-border` / `*-foreground`. For preset-agnostic banners, use opacity-derived surfaces:
87
+ The **brand presets** (`macos` / `ios` / `windows` / `django-cfg`) retint the full
88
+ status set to their own canvas, so banners read correctly on their custom
89
+ backgrounds. The **modifier presets** (`soft` / `dense` / `high-contrast`) and
90
+ `default` inherit the base status surfaces. Either way the four-token set is
91
+ always defined, so `bg-warning-background` etc. are safe everywhere. For a fully
92
+ preset-agnostic surface you can still derive from the base color with opacity:
86
93
 
87
94
  ```tsx
88
95
  <div className="rounded-md border border-warning/30 bg-warning/10 text-warning">…</div>
@@ -97,18 +104,99 @@ Most presets don't override `*-background` / `*-border` / `*-foreground`. For pr
97
104
  | `bg-sidebar` / `text-sidebar-foreground` / `border-sidebar-border` | App sidebar chrome |
98
105
  | `bg-sidebar-accent` / `text-sidebar-accent-foreground` | Sidebar hover state |
99
106
 
107
+ ### Chart tokens (categorical palette)
108
+
109
+ `--chart-1 … --chart-5` are the categorical series colors (chart-1 = brand hue).
110
+ Like every color token they are **fully-wrapped `hsl(...)`** and bound to
111
+ Tailwind via `--color-chart-*` in `tokens.css`, so the utilities work with
112
+ opacity modifiers:
113
+
114
+ ```tsx
115
+ <div className="bg-chart-1" /> {/* solid */}
116
+ <div className="bg-chart-3/40" /> {/* 40% via color-mix */}
117
+ <span className="text-chart-2" />
118
+ ```
119
+
120
+ For Recharts / SVG / Canvas, pass the variable directly — **never** wrap it:
121
+
122
+ ```tsx
123
+ <Bar fill="var(--chart-1)" /> {/* ✅ */}
124
+ <Bar fill="hsl(var(--chart-1))" /> {/* ❌ hsl(hsl(...)) — invalid */}
125
+ ```
126
+
127
+ > **JIT-scan gotcha (charts/status).** `bg-chart-${n}` / `bg-${status}-background`
128
+ > built from template literals are invisible to Tailwind's static scanner —
129
+ > only literal classes get a CSS rule. Use literal class names (or inline
130
+ > `style={{ background: 'var(--chart-N)' }}`) when the index is dynamic.
131
+
132
+ ### Typography tokens
133
+
134
+ `--font-sans` / `--font-mono` and the size scale (`--font-size-xs … -xl`,
135
+ `--line-height-base`, `--letter-spacing-base`) live in `base.css` and are
136
+ **overridable per preset** (e.g. `macos` → SF Pro, `windows` → Segoe UI
137
+ Variable). `tokens.css` bridges the size scale onto Tailwind's `--text-*` tokens,
138
+ so `font-sans` / `font-mono` and `text-xs … text-xl` follow the active preset
139
+ instead of Tailwind's hardcoded defaults. `body` applies font-sans + base
140
+ size/line-height/tracking directly.
141
+
142
+ ## Radius tokens
143
+
144
+ 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):
145
+
146
+ | Token | Value | Used by |
147
+ |---|---|---|
148
+ | `--radius-control` | `0.625rem` | **Interactive controls** (inputs, nav items, search, value chips) — one shared corner so they round consistently. Class: `.rounded-control` |
149
+ | `--radius-dialog` | `1rem` | Dialog panels. Applied at **all** sizes (was `sm:`-gated, which left phones square) |
150
+ | `--radius-popover` | `0.75rem` | Popovers / menus |
151
+
152
+ ## Focus rings (Vercel / Linear pattern)
153
+
154
+ 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.
155
+
156
+ ```
157
+ focus-visible:border-ring focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring
158
+ ```
159
+
160
+ The native browser focus outline (often white/auto, which used to pierce through on click) is reset app-wide in `base.css`:
161
+
162
+ ```css
163
+ :where(input, textarea, select, button, a, [tabindex], [role="button"], [contenteditable]):focus { outline: none; }
164
+ ```
165
+
166
+ 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.
167
+
168
+ ## Scan-independent utility classes
169
+
170
+ Some classes are authored as **plain CSS** in `utilities/*.css` (not Tailwind utilities):
171
+
172
+ | Class | File | Why plain CSS |
173
+ |---|---|---|
174
+ | `.divider-b` / `.divider-t` | `utilities/divider.css` | Hairline via `--divider` |
175
+ | `.rounded-control` | `utilities/controls.css` | Shared control radius |
176
+
177
+ > **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.
178
+
100
179
  ## Theme presets — 8 production-ready
101
180
 
102
- | ID | Use case |
103
- |---|---|
104
- | `default` | Default ui-core themecyan brand |
105
- | `django-cfg` | Brand override (deeper cyan-blue primary) |
106
- | `macos` | Pixel-accurate Apple HIG (Sequoia / Tahoe 26) — Wails/Electron desktop |
107
- | `ios` | iOS app feel 0.75rem radius, systemBlue |
108
- | `windows` | Microsoft Fluent 2 Segoe UI Variable, 0.375rem radius |
109
- | `soft` | Larger radius (1rem) friendly marketing surfaces |
110
- | `dense` | Smaller radius (0.25rem) — data-heavy admin UIs |
111
- | `high-contrast` | A11y boost stronger borders, harder text |
181
+ Two families, with different coverage by design:
182
+
183
+ - **Brand / OS presets** declare a *full* token set colors, sidebar, charts,
184
+ status surfaces, divider, and (macos/windows) typography so the identity is
185
+ self-contained and survives layering over any base.
186
+ - **Modifier presets** override only the chrome they're about (radius / borders /
187
+ contrast) and **inherit** brand colors, charts, and status from whatever is
188
+ active beneath them so they compose (`django-cfg` + `dense`, etc.).
189
+
190
+ | ID | Family | Use case |
191
+ |---|---|---|
192
+ | `default` | base | Default ui-core theme — cyan brand |
193
+ | `django-cfg` | brand | Brand identity — brand-washed accent/sidebar + on-brand `info` |
194
+ | `macos` | brand | Pixel-accurate Apple HIG (Sequoia / Tahoe 26) — SF Pro, Wails/Electron desktop |
195
+ | `ios` | brand | iOS app feel — 0.75rem radius, systemBlue, iOS status colors |
196
+ | `windows` | brand | Microsoft Fluent 2 — Segoe UI Variable, 0.375rem radius |
197
+ | `soft` | modifier | Larger radius (1rem) — friendly marketing surfaces |
198
+ | `dense` | modifier | Smaller radius (0.25rem) — data-heavy admin UIs |
199
+ | `high-contrast` | modifier | A11y boost — stronger borders, harder text, pure canvas |
112
200
 
113
201
  ### Apply a preset
114
202
 
@@ -258,7 +346,12 @@ Now any of these work:
258
346
 
259
347
  ## Theme Showcase story
260
348
 
261
- The `UI Core/Theme Showcase` story in djangocfg storybook renders every base token, status surface, button variant, card, form control, glass utility, and opacity sanity-check on one page. Switch the `preset` control to flip across all 8 themes; flip light/dark from the toolbar.
349
+ The `UI Core/Theme Showcase` story in djangocfg storybook renders every base
350
+ token (incl. `divider`), the real status surfaces (`*-background`/`*-foreground`/
351
+ `*-border`), the chart palette (solid + /40 opacity), the typography scale,
352
+ button variants, cards, form controls, glass utilities, and an opacity
353
+ sanity-check on one page. Switch the `preset` control to flip across all 8
354
+ themes; flip light/dark from the toolbar.
262
355
 
263
356
  Use it to validate any token change before publishing — every regression shows up on one screen.
264
357
 
@@ -8,8 +8,15 @@
8
8
  :root {
9
9
  --radius: 0.625rem;
10
10
  --radius-sm: 0.375rem;
11
+ /* Control corner — shared by interactive controls (nav items, inputs,
12
+ * search, value chips) so they round consistently. ~10px reads like
13
+ * Claude/macOS. Override per preset for a tighter/softer feel. */
14
+ --radius-control: 0.625rem;
11
15
  --radius-popover: 0.75rem;
12
- --radius-dialog: 1.5rem;
16
+ /* Dialog corner — tightened from 1.5rem to read like Claude/macOS panels
17
+ * (round, but not pill-soft). Default preset only; preset themes that set
18
+ * their own `radius` are unaffected. */
19
+ --radius-dialog: 1rem;
13
20
  --radius-tooltip: 0.375rem;
14
21
 
15
22
  /* Typography tokens — overridable per preset */
@@ -29,6 +36,16 @@
29
36
  border-color: var(--border);
30
37
  }
31
38
 
39
+ /* Suppress the native (often white/auto) focus outline app-wide — every
40
+ * interactive ui-core component provides its own `focus-visible:ring-*` in the
41
+ * blue `--ring` token. Without this, clicking certain inputs (where the browser
42
+ * applies `:focus` but not `:focus-visible`) shows an ugly white outline ON TOP
43
+ * of our ring. Keyboard accessibility is preserved via the ring utilities.
44
+ */
45
+ :where(input, textarea, select, button, a, [tabindex], [role="button"], [contenteditable]):focus {
46
+ outline: none;
47
+ }
48
+
32
49
  html {
33
50
  font-weight: 400;
34
51
  }
@@ -5,6 +5,7 @@ export type {
5
5
  ThemeCssVarKey,
6
6
  ThemeCssVarMap,
7
7
  ThemeCssVarSidebarKey,
8
+ ThemeCssVarStatusKey,
8
9
  ThemeCssVarTypographyKey,
9
10
  ThemeStyleConfig,
10
11
  ThemeStylePresetId,
@@ -1,3 +1,12 @@
1
+ /**
2
+ * dense — density modifier (NOT a full color theme).
3
+ *
4
+ * Tight radius (0.25rem) + slightly stronger neutral chrome for data-heavy
5
+ * admin tables/grids where rows need crisp separation. Overrides only neutral
6
+ * surfaces (radius/border/input/divider/muted/card/accent) and inherits brand
7
+ * colors from the active base/brand preset, so it composes with `django-cfg`.
8
+ * Light and dark are symmetric.
9
+ */
1
10
  import type { ThemePreset } from './types';
2
11
 
3
12
  export const densePreset: ThemePreset = {
@@ -5,6 +14,7 @@ export const densePreset: ThemePreset = {
5
14
  radius: '0.25rem',
6
15
  border: 'hsl(0 0% 82%)',
7
16
  input: 'hsl(0 0% 82%)',
17
+ divider: 'hsl(0 0% 88%)',
8
18
  muted: 'hsl(0 0% 94%)',
9
19
  card: 'hsl(0 0% 100%)',
10
20
  accent: 'hsl(0 0% 93%)',
@@ -13,6 +23,7 @@ export const densePreset: ThemePreset = {
13
23
  radius: '0.25rem',
14
24
  border: 'hsl(0 0% 24%)',
15
25
  input: 'hsl(0 0% 24%)',
26
+ divider: 'hsl(0 0% 18%)',
16
27
  muted: 'hsl(0 0% 12%)',
17
28
  card: 'hsl(0 0% 10%)',
18
29
  accent: 'hsl(0 0% 14%)',
@@ -1,21 +1,62 @@
1
+ /**
2
+ * django-cfg brand preset.
3
+ *
4
+ * The default ui-core theme already uses the django-cfg cyan family, so this
5
+ * preset is the *explicit, self-contained* brand declaration: it re-states the
6
+ * full brand-bearing token set (primary, ring, accent wash, sidebar, charts,
7
+ * info status) so the identity survives even when layered over a non-default
8
+ * base, and tints the neutral hover/accent surfaces with a faint brand wash
9
+ * instead of the base's pure grey.
10
+ *
11
+ * Brand cyan: #0989aa (light, `192 90% 35%`) / #00d9ff (dark, `189 100% 50%`).
12
+ * Only color tokens are set — radius/typography inherit base so the brand can
13
+ * be combined with `soft`/`dense` density modifiers downstream.
14
+ */
1
15
  import type { ThemePreset } from './types';
2
16
 
3
17
  export const djangoCfgPreset: ThemePreset = {
4
18
  light: {
5
19
  primary: 'hsl(192 90% 35%)',
6
20
  'primary-foreground': 'hsl(0 0% 100%)',
7
- ring: 'hsl(192 90% 35%)',
21
+ // Faint brand wash on hover/selected surfaces (vs base's neutral grey)
22
+ accent: 'hsl(192 60% 94%)',
23
+ 'accent-foreground': 'hsl(192 90% 22%)',
24
+ ring: 'hsl(192 90% 40%)',
8
25
  'sidebar-primary': 'hsl(192 90% 35%)',
9
- 'sidebar-ring': 'hsl(192 90% 35%)',
26
+ 'sidebar-primary-foreground': 'hsl(0 0% 100%)',
27
+ 'sidebar-accent': 'hsl(192 55% 93%)',
28
+ 'sidebar-accent-foreground': 'hsl(192 90% 22%)',
29
+ 'sidebar-ring': 'hsl(192 90% 40%)',
30
+ // Info status routed to the brand hue so banners read as on-brand
31
+ info: 'hsl(192 90% 35%)',
32
+ 'info-background': 'hsl(192 100% 96%)',
33
+ 'info-foreground': 'hsl(192 90% 22%)',
34
+ 'info-border': 'hsl(192 80% 78%)',
10
35
  'chart-1': 'hsl(192 90% 35%)',
36
+ 'chart-2': 'hsl(142 76% 36%)',
37
+ 'chart-3': 'hsl(262 83% 58%)',
38
+ 'chart-4': 'hsl(26 90% 57%)',
39
+ 'chart-5': 'hsl(346 77% 50%)',
11
40
  },
12
41
  dark: {
13
42
  primary: 'hsl(189 100% 50%)',
14
43
  'primary-foreground': 'hsl(0 0% 9%)',
44
+ accent: 'hsl(189 40% 18%)',
45
+ 'accent-foreground': 'hsl(189 100% 78%)',
15
46
  ring: 'hsl(189 100% 50%)',
16
47
  'sidebar-primary': 'hsl(189 100% 50%)',
17
48
  'sidebar-primary-foreground': 'hsl(0 0% 9%)',
49
+ 'sidebar-accent': 'hsl(189 35% 16%)',
50
+ 'sidebar-accent-foreground': 'hsl(189 100% 78%)',
18
51
  'sidebar-ring': 'hsl(189 100% 50%)',
52
+ info: 'hsl(189 100% 55%)',
53
+ 'info-background': 'hsl(189 70% 10%)',
54
+ 'info-foreground': 'hsl(189 90% 75%)',
55
+ 'info-border': 'hsl(189 50% 28%)',
19
56
  'chart-1': 'hsl(189 100% 50%)',
57
+ 'chart-2': 'hsl(142 76% 50%)',
58
+ 'chart-3': 'hsl(262 83% 65%)',
59
+ 'chart-4': 'hsl(26 90% 60%)',
60
+ 'chart-5': 'hsl(346 77% 58%)',
20
61
  },
21
62
  };
@@ -1,19 +1,35 @@
1
+ /**
2
+ * high-contrast — accessibility modifier (NOT a full color theme).
3
+ *
4
+ * Pushes border/divider/text contrast to meet stronger WCAG ratios: harder
5
+ * borders, darker (light) / brighter (dark) text, a stronger focus ring, and a
6
+ * pure white / near-black canvas so foreground separation is maximal. Brand
7
+ * colors (primary/charts/status) are inherited so the a11y boost stacks on top
8
+ * of any base or brand preset. Light and dark are symmetric.
9
+ */
1
10
  import type { ThemePreset } from './types';
2
11
 
3
12
  export const highContrastPreset: ThemePreset = {
4
13
  light: {
5
- border: 'hsl(0 0% 72%)',
6
- input: 'hsl(0 0% 72%)',
7
- 'muted-foreground': 'hsl(0 0% 32%)',
14
+ background: 'hsl(0 0% 100%)',
8
15
  foreground: 'hsl(0 0% 6%)',
9
- ring: 'hsl(0 0% 20%)',
16
+ card: 'hsl(0 0% 100%)',
17
+ 'card-foreground': 'hsl(0 0% 6%)',
18
+ border: 'hsl(0 0% 62%)',
19
+ input: 'hsl(0 0% 62%)',
20
+ divider: 'hsl(0 0% 68%)',
21
+ 'muted-foreground': 'hsl(0 0% 28%)',
22
+ ring: 'hsl(0 0% 12%)',
10
23
  },
11
24
  dark: {
12
- border: 'hsl(0 0% 38%)',
13
- input: 'hsl(0 0% 38%)',
14
- 'muted-foreground': 'hsl(0 0% 78%)',
25
+ background: 'hsl(0 0% 4%)',
15
26
  foreground: 'hsl(0 0% 100%)',
16
- background: 'hsl(0 0% 6%)',
17
- ring: 'hsl(0 0% 85%)',
27
+ card: 'hsl(0 0% 8%)',
28
+ 'card-foreground': 'hsl(0 0% 100%)',
29
+ border: 'hsl(0 0% 48%)',
30
+ input: 'hsl(0 0% 48%)',
31
+ divider: 'hsl(0 0% 42%)',
32
+ 'muted-foreground': 'hsl(0 0% 82%)',
33
+ ring: 'hsl(0 0% 92%)',
18
34
  },
19
35
  };
@@ -21,6 +21,7 @@ export const iosPreset: ThemePreset = {
21
21
  border: 'hsl(220 9% 88%)',
22
22
  input: 'hsl(220 9% 88%)',
23
23
  ring: 'hsl(211 100% 50%)',
24
+ divider: 'hsl(220 9% 84%)', // hairline, a touch lighter than border
24
25
  radius: '0.75rem',
25
26
  'sidebar-background': 'hsl(0 0% 99%)',
26
27
  'sidebar-foreground': 'hsl(220 9% 12%)',
@@ -35,6 +36,21 @@ export const iosPreset: ThemePreset = {
35
36
  'chart-3': 'hsl(262 83% 58%)',
36
37
  'chart-4': 'hsl(35 100% 50%)',
37
38
  'chart-5': 'hsl(346 77% 50%)',
39
+ // Status surfaces — iOS system colors
40
+ warning: 'hsl(35 100% 50%)',
41
+ 'warning-background': 'hsl(35 100% 95%)',
42
+ 'warning-foreground': 'hsl(28 80% 30%)',
43
+ 'warning-border': 'hsl(35 90% 78%)',
44
+ success: 'hsl(145 63% 42%)',
45
+ 'success-background': 'hsl(145 60% 95%)',
46
+ 'success-foreground': 'hsl(145 60% 24%)',
47
+ 'success-border': 'hsl(145 55% 76%)',
48
+ 'destructive-background': 'hsl(0 100% 96%)',
49
+ 'destructive-border': 'hsl(0 90% 80%)',
50
+ info: 'hsl(211 100% 50%)',
51
+ 'info-background': 'hsl(211 100% 96%)',
52
+ 'info-foreground': 'hsl(211 80% 30%)',
53
+ 'info-border': 'hsl(211 90% 80%)',
38
54
  },
39
55
  dark: {
40
56
  background: 'hsl(240 6% 10%)',
@@ -56,6 +72,7 @@ export const iosPreset: ThemePreset = {
56
72
  border: 'hsl(240 5% 22%)',
57
73
  input: 'hsl(240 5% 22%)',
58
74
  ring: 'hsl(211 100% 55%)',
75
+ divider: 'hsl(240 5% 18%)', // hairline, softer than border
59
76
  radius: '0.75rem',
60
77
  'sidebar-background': 'hsl(240 6% 8%)',
61
78
  'sidebar-foreground': 'hsl(0 0% 96%)',
@@ -70,5 +87,20 @@ export const iosPreset: ThemePreset = {
70
87
  'chart-3': 'hsl(262 83% 65%)',
71
88
  'chart-4': 'hsl(35 100% 58%)',
72
89
  'chart-5': 'hsl(346 77% 58%)',
90
+ // Status surfaces (dark)
91
+ warning: 'hsl(35 100% 58%)',
92
+ 'warning-background': 'hsl(35 90% 10%)',
93
+ 'warning-foreground': 'hsl(35 100% 72%)',
94
+ 'warning-border': 'hsl(35 80% 28%)',
95
+ success: 'hsl(145 65% 52%)',
96
+ 'success-background': 'hsl(145 60% 9%)',
97
+ 'success-foreground': 'hsl(145 60% 70%)',
98
+ 'success-border': 'hsl(145 50% 26%)',
99
+ 'destructive-background': 'hsl(0 80% 11%)',
100
+ 'destructive-border': 'hsl(0 70% 30%)',
101
+ info: 'hsl(211 100% 55%)',
102
+ 'info-background': 'hsl(211 90% 11%)',
103
+ 'info-foreground': 'hsl(211 100% 74%)',
104
+ 'info-border': 'hsl(211 80% 30%)',
73
105
  },
74
106
  };
@@ -56,6 +56,8 @@ export const macosPreset: ThemePreset = {
56
56
  // Kept intentionally light — Apple hairline, not a thick rule
57
57
  border: 'hsl(240 3% 78%)',
58
58
  input: 'hsl(240 8% 93%)',
59
+ // Row hairline — a touch lighter than border so it reads on white cards
60
+ divider: 'hsl(240 4% 85%)',
59
61
  ring: 'hsl(211 100% 50%)',
60
62
  radius: '0.625rem',
61
63
  // Sidebar: slighly darker than page, matches macOS sidebar material
@@ -73,6 +75,22 @@ export const macosPreset: ThemePreset = {
73
75
  'chart-3': 'hsl(262 60% 56%)',
74
76
  'chart-4': 'hsl(35 100% 48%)',
75
77
  'chart-5': 'hsl(2 100% 59%)',
78
+ // Status surfaces — Apple system colors (systemOrange/Green/Red/Blue) with
79
+ // faint tinted banner fills that sit on the #F2F2F7 grouped canvas.
80
+ warning: 'hsl(35 100% 48%)', // systemOrange #FF9500
81
+ 'warning-background': 'hsl(35 100% 95%)',
82
+ 'warning-foreground': 'hsl(28 80% 30%)',
83
+ 'warning-border': 'hsl(35 90% 78%)',
84
+ success: 'hsl(142 71% 45%)', // systemGreen #34C759
85
+ 'success-background': 'hsl(142 60% 95%)',
86
+ 'success-foreground': 'hsl(142 60% 24%)',
87
+ 'success-border': 'hsl(142 55% 76%)',
88
+ 'destructive-background': 'hsl(2 100% 96%)',
89
+ 'destructive-border': 'hsl(2 90% 80%)',
90
+ info: 'hsl(211 100% 50%)', // systemBlue #007AFF
91
+ 'info-background': 'hsl(211 100% 96%)',
92
+ 'info-foreground': 'hsl(211 80% 30%)',
93
+ 'info-border': 'hsl(211 90% 80%)',
76
94
  ...appleTypography,
77
95
  },
78
96
  dark: {
@@ -103,6 +121,8 @@ export const macosPreset: ThemePreset = {
103
121
  // Separator dark: rgba(84,84,88,0.36) — ultra-thin, almost invisible
104
122
  border: 'hsl(240 3% 22%)',
105
123
  input: 'hsl(240 3% 19%)',
124
+ // Row hairline — softer than border so dark rows don't read as a hard rule
125
+ divider: 'hsl(240 3% 18%)',
106
126
  ring: 'hsl(211 100% 58%)',
107
127
  radius: '0.625rem',
108
128
  // Sidebar: near-black floor — #0D0D0F
@@ -120,6 +140,22 @@ export const macosPreset: ThemePreset = {
120
140
  'chart-3': 'hsl(262 60% 65%)',
121
141
  'chart-4': 'hsl(35 100% 56%)',
122
142
  'chart-5': 'hsl(3 100% 62%)',
143
+ // Status surfaces (dark) — Apple dark system colors, dim banner fills on the
144
+ // #141414 canvas with raised foregrounds for contrast.
145
+ warning: 'hsl(35 100% 56%)', // systemOrange dark #FF9F0A
146
+ 'warning-background': 'hsl(35 70% 10%)',
147
+ 'warning-foreground': 'hsl(35 95% 70%)',
148
+ 'warning-border': 'hsl(35 50% 26%)',
149
+ success: 'hsl(142 60% 50%)', // systemGreen dark #30D158
150
+ 'success-background': 'hsl(142 50% 9%)',
151
+ 'success-foreground': 'hsl(142 65% 68%)',
152
+ 'success-border': 'hsl(142 40% 24%)',
153
+ 'destructive-background': 'hsl(3 70% 11%)',
154
+ 'destructive-border': 'hsl(3 55% 30%)',
155
+ info: 'hsl(211 100% 58%)', // systemBlue dark #0A84FF
156
+ 'info-background': 'hsl(211 70% 11%)',
157
+ 'info-foreground': 'hsl(211 90% 72%)',
158
+ 'info-border': 'hsl(211 50% 30%)',
123
159
  ...appleTypography,
124
160
  },
125
161
  };
@@ -1,3 +1,13 @@
1
+ /**
2
+ * soft — density/feel modifier (NOT a full color theme).
3
+ *
4
+ * Larger radius (1rem) + gently lifted neutral surfaces for friendly marketing
5
+ * UIs. It deliberately only overrides the neutral chrome (background/card/muted/
6
+ * accent/border/input/divider/radius + sidebar surfaces) and inherits brand
7
+ * colors (primary/ring/charts/status) from whatever base or brand preset is
8
+ * active, so `soft` composes with `django-cfg` etc. Light and dark are kept
9
+ * symmetric — every key set in one mode is set in the other.
10
+ */
1
11
  import type { ThemePreset } from './types';
2
12
 
3
13
  export const softPreset: ThemePreset = {
@@ -12,7 +22,9 @@ export const softPreset: ThemePreset = {
12
22
  'accent-foreground': 'hsl(240 6% 10%)',
13
23
  border: 'hsl(240 5% 91%)',
14
24
  input: 'hsl(240 5% 91%)',
25
+ divider: 'hsl(240 5% 93%)',
15
26
  radius: '1rem',
27
+ 'sidebar-background': 'hsl(0 0% 99%)',
16
28
  'sidebar-accent': 'hsl(240 5% 94%)',
17
29
  'sidebar-border': 'hsl(240 5% 91%)',
18
30
  },
@@ -27,6 +39,7 @@ export const softPreset: ThemePreset = {
27
39
  'accent-foreground': 'hsl(0 0% 96%)',
28
40
  border: 'hsl(240 5% 20%)',
29
41
  input: 'hsl(240 5% 20%)',
42
+ divider: 'hsl(240 5% 16%)',
30
43
  radius: '1rem',
31
44
  'sidebar-background': 'hsl(240 6% 6%)',
32
45
  'sidebar-accent': 'hsl(240 5% 14%)',
@@ -44,6 +44,8 @@ export const windowsPreset: ThemePreset = {
44
44
  // ControlStrokeColorDefault: rgba(0,0,0,0.0578)
45
45
  border: 'hsl(0 0% 87%)',
46
46
  input: 'hsl(0 0% 90%)',
47
+ // Hairline between rows — slightly lighter than border
48
+ divider: 'hsl(0 0% 84%)',
47
49
  ring: 'hsl(210 100% 45%)',
48
50
  // WinUI 3: 4px controls, 8px cards/dialogs
49
51
  radius: '0.375rem',
@@ -60,6 +62,21 @@ export const windowsPreset: ThemePreset = {
60
62
  'chart-3': 'hsl(262 83% 55%)',
61
63
  'chart-4': 'hsl(35 100% 48%)',
62
64
  'chart-5': 'hsl(346 77% 50%)',
65
+ // Status surfaces — Fluent system colors
66
+ warning: 'hsl(38 92% 50%)',
67
+ 'warning-background': 'hsl(38 100% 95%)',
68
+ 'warning-foreground': 'hsl(30 80% 28%)',
69
+ 'warning-border': 'hsl(38 85% 78%)',
70
+ success: 'hsl(120 78% 27%)',
71
+ 'success-background': 'hsl(120 55% 95%)',
72
+ 'success-foreground': 'hsl(120 60% 28%)',
73
+ 'success-border': 'hsl(120 45% 78%)',
74
+ 'destructive-background': 'hsl(0 90% 96%)',
75
+ 'destructive-border': 'hsl(0 85% 78%)',
76
+ info: 'hsl(210 100% 38%)',
77
+ 'info-background': 'hsl(210 100% 95%)',
78
+ 'info-foreground': 'hsl(210 90% 28%)',
79
+ 'info-border': 'hsl(210 80% 78%)',
63
80
  ...fluentTypography,
64
81
  },
65
82
  dark: {
@@ -86,6 +103,8 @@ export const windowsPreset: ThemePreset = {
86
103
  // ControlStrokeColorDefault dark: rgba(255,255,255,0.0837)
87
104
  border: 'hsl(0 0% 28%)',
88
105
  input: 'hsl(0 0% 24%)',
106
+ // Hairline between rows — softer than border
107
+ divider: 'hsl(0 0% 22%)',
89
108
  ring: 'hsl(200 100% 69%)',
90
109
  radius: '0.375rem',
91
110
  // NavigationView pane background
@@ -102,6 +121,21 @@ export const windowsPreset: ThemePreset = {
102
121
  'chart-3': 'hsl(262 83% 65%)',
103
122
  'chart-4': 'hsl(35 100% 58%)',
104
123
  'chart-5': 'hsl(346 77% 58%)',
124
+ // Status surfaces — Fluent system colors
125
+ warning: 'hsl(38 95% 58%)',
126
+ 'warning-background': 'hsl(38 60% 10%)',
127
+ 'warning-foreground': 'hsl(38 90% 72%)',
128
+ 'warning-border': 'hsl(38 50% 28%)',
129
+ success: 'hsl(120 60% 55%)',
130
+ 'success-background': 'hsl(120 40% 10%)',
131
+ 'success-foreground': 'hsl(120 55% 72%)',
132
+ 'success-border': 'hsl(120 35% 28%)',
133
+ 'destructive-background': 'hsl(0 60% 11%)',
134
+ 'destructive-border': 'hsl(0 50% 30%)',
135
+ info: 'hsl(200 100% 69%)',
136
+ 'info-background': 'hsl(200 60% 11%)',
137
+ 'info-foreground': 'hsl(200 90% 74%)',
138
+ 'info-border': 'hsl(200 50% 30%)',
105
139
  ...fluentTypography,
106
140
  },
107
141
  };
@@ -31,8 +31,35 @@ export type ThemeCssVarColorKey =
31
31
  | 'destructive'
32
32
  | 'destructive-foreground';
33
33
 
34
- /** Layout / focus tokens — `radius` is a CSS length, rest are HSL */
35
- export type ThemeCssVarChromeKey = 'border' | 'input' | 'ring' | 'radius';
34
+ /**
35
+ * Layout / focus tokens `radius` is a CSS length, the rest are wrapped colors.
36
+ * `divider` is the hairline-between-rows token (deliberately *lighter* than
37
+ * `--card` so it stays visible on elevated surfaces — see styles README).
38
+ */
39
+ export type ThemeCssVarChromeKey = 'border' | 'input' | 'divider' | 'ring' | 'radius';
40
+
41
+ /**
42
+ * Status surfaces — each role has a 4-token set (icon/accent, banner background,
43
+ * readable foreground, border ring) used by banners/alerts. Presets with a
44
+ * custom `background` should re-tint these so banners don't clash with the
45
+ * canvas; presets that keep the default canvas can omit them. All are
46
+ * **fully-wrapped CSS colors** (same rule as `ThemeCssVarColorKey`).
47
+ */
48
+ export type ThemeCssVarStatusKey =
49
+ | 'warning'
50
+ | 'warning-background'
51
+ | 'warning-foreground'
52
+ | 'warning-border'
53
+ | 'success'
54
+ | 'success-background'
55
+ | 'success-foreground'
56
+ | 'success-border'
57
+ | 'destructive-background'
58
+ | 'destructive-border'
59
+ | 'info'
60
+ | 'info-background'
61
+ | 'info-foreground'
62
+ | 'info-border';
36
63
 
37
64
  /**
38
65
  * Typography tokens — override system font stack and scale per preset.
@@ -59,12 +86,19 @@ export type ThemeCssVarSidebarKey =
59
86
  | 'sidebar-border'
60
87
  | 'sidebar-ring';
61
88
 
89
+ /**
90
+ * Categorical chart colors. **Fully-wrapped CSS colors** (`hsl(...)`) — same
91
+ * rule as every color key. Consume via the Tailwind utility (`bg-chart-1`,
92
+ * backed by `--color-chart-N` in tokens.css) or `var(--chart-N)`; never wrap as
93
+ * `hsl(var(--chart-N))` (that double-wraps to `hsl(hsl(...))` and is invalid).
94
+ */
62
95
  export type ThemeCssVarChartKey = 'chart-1' | 'chart-2' | 'chart-3' | 'chart-4' | 'chart-5';
63
96
 
64
97
  /** All keys that map to `--${key}` in ui-core CSS files */
65
98
  export type ThemeCssVarKey =
66
99
  | ThemeCssVarColorKey
67
100
  | ThemeCssVarChromeKey
101
+ | ThemeCssVarStatusKey
68
102
  | ThemeCssVarTypographyKey
69
103
  | ThemeCssVarSidebarKey
70
104
  | ThemeCssVarChartKey;