twico-ui 1.42.0 → 1.43.0

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/llms.txt CHANGED
@@ -234,6 +234,8 @@ Props:
234
234
  - `type` — `"button" | "submit" | "reset"` · default `"button"` — Native button type — use "submit" to submit a surrounding form.
235
235
  - `disabled` — `boolean` · default `false` — Disables the control — no interaction and a dimmed appearance.
236
236
  - `autoFocus` — `boolean` · default `false` — Focus the element automatically when it mounts.
237
+ - `id` — `string` — Id applied to <button>, handy for labels and aria wiring.
238
+ - `style` — `React.CSSProperties` — Inline styles merged onto <button> after the component's own.
237
239
 
238
240
  ```jsx
239
241
  import { Button } from "twico-ui";
@@ -268,6 +270,8 @@ Props:
268
270
  - `type` — `"button" | "submit" | "reset"` · default `"button"` — Native button type — use "submit" to submit a surrounding form.
269
271
  - `disabled` — `boolean` · default `false` — Disables the control — no interaction and a dimmed appearance.
270
272
  - `autoFocus` — `boolean` · default `false` — Focus the element automatically when it mounts.
273
+ - `id` — `string` — Id applied to <button>, handy for labels and aria wiring.
274
+ - `style` — `React.CSSProperties` — Inline styles merged onto <button> after the component's own.
271
275
 
272
276
  ```jsx
273
277
  import { IconButton } from "twico-ui";
@@ -424,7 +428,7 @@ Divider is a thin separator rule that visually splits content, either horizontal
424
428
  Props:
425
429
  - `orientation` — `"horizontal" | "vertical"` · default `"horizontal"` — Direction of the separator rule, either horizontal (default) to split stacked content or vertical for inline items.
426
430
  - `inset` — `boolean` · default `false` — Indents the start of a horizontal rule to align it with inset content like list item text.
427
- - `align` — `"left" | "center" | "right" | "start" | "end"` · default `"center"` — Label alignment when children are provided; logical start/end now accepted and map to left/right (additive — existing left/center/right unchanged).
431
+ - `align` — `"left" | "center" | "right" | "start" | "end"` · default `"center"` — Label alignment when children are provided; prefer the logical "start"/"end" so the label mirrors under dir="rtl" ("left"/"right" still accepted).
428
432
  - `children` — `React.ReactNode` — Optional label text rendered within the rule, such as "OR", to break content with a caption.
429
433
 
430
434
  ```jsx
@@ -523,7 +527,7 @@ Heading (h1–h6) with consistent token typography. Use it instead of bare headi
523
527
  Props:
524
528
  - `level` — `1 | 2 | 3 | 4 | 5 | 6` · default `2` — Chooses which h1-h6 tag renders and its default token size, keeping document hierarchy and typography consistent.
525
529
  - `size` — `string` — Overrides the font-size token suffix (e.g. "3xl") to decouple visual scale from the semantic heading level.
526
- - `align` — `string` — Sets the CSS text-align value, letting you left-align, center, or right-align the heading text.
530
+ - `align` — `string` — Sets the CSS text-align value; prefer the logical "start"/"end" over "left"/"right" so the heading mirrors under dir="rtl".
527
531
  - `as` — `ElementType` — Renders a different tag while preserving the level's size, useful when semantics and visual weight should differ.
528
532
  - `sx` — `Sx` — Style escape hatch: flat CSS goes inline (wins over base); nested selectors/at-rules like "&:hover" or "@media …" compile to a scoped stylesheet.
529
533
 
@@ -544,7 +548,7 @@ Props:
544
548
  - `size` — `"xs" | "sm" | "base" | "lg" | "xl"` · default `"base"` — Picks the font-size token from xs to xl, scaling the body text to fit its context, base by default.
545
549
  - `tone` — `"default" | "muted" | "subtle" | "primary" | "success" | "warning" | "danger" | "info" | "neutral"` · default `"default"` — Sets the text color — neutral roles (default, muted, subtle) plus the six semantic color intents.
546
550
  - `weight` — `string` — Sets the font-weight token suffix like semibold to adjust how heavy the text appears.
547
- - `align` — `string` — Sets the text-align value, controlling whether the text reads left, center, right, or justified.
551
+ - `align` — `string` — Sets the text-align value: "start", "center", "end" or "justify" — prefer the logical start/end so the text mirrors under dir="rtl".
548
552
  - `as` — `ElementType` · default `"p"` — Chooses the element or tag rendered, defaulting to p so you can swap in span or other tags.
549
553
  - `sx` — `Sx` — Style escape hatch: flat CSS goes inline (wins over base); nested selectors/at-rules like "&:hover" or "@media …" compile to a scoped stylesheet.
550
554
 
@@ -581,6 +585,7 @@ Props:
581
585
  - `readOnly` — `boolean` · default `false` — Makes the field non-editable while still focusable/selectable.
582
586
  - `required` — `boolean` · default `false` — Marks the field required for native form validation.
583
587
  - `autoComplete` — `string` — Browser autocomplete hint (e.g. "email", "off").
588
+ - `style` — `React.CSSProperties` — Inline styles merged onto <input> after the component's own.
584
589
 
585
590
  ```jsx
586
591
  import { Checkbox } from "twico-ui";
@@ -665,6 +670,8 @@ Props:
665
670
  - `name` — `string` — Form field name submitted with the value.
666
671
  - `readOnly` — `boolean` · default `false` — Makes the field non-editable while still focusable/selectable.
667
672
  - `autoComplete` — `string` — Browser autocomplete hint (e.g. "email", "off").
673
+ - `id` — `string` — Id applied to <input>, handy for labels and aria wiring.
674
+ - `style` — `React.CSSProperties` — Inline styles merged onto <input> after the component's own.
668
675
 
669
676
  ```jsx
670
677
  import { Combobox } from "twico-ui";
@@ -715,6 +722,8 @@ Props:
715
722
  - `disabled` — `boolean` · default `false` — Disables the control — no interaction and a dimmed appearance.
716
723
  - `readOnly` — `boolean` · default `false` — Makes the field non-editable while still focusable/selectable.
717
724
  - `autoComplete` — `string` — Browser autocomplete hint (e.g. "email", "off").
725
+ - `id` — `string` — Id applied to <input>, handy for labels and aria wiring.
726
+ - `style` — `React.CSSProperties` — Inline styles merged onto <input> after the component's own.
718
727
 
719
728
  ```jsx
720
729
  import { Currency } from "twico-ui";
@@ -753,6 +762,8 @@ Props:
753
762
  - `disabled` — `boolean` · default `false` — Disables the control — no interaction and a dimmed appearance.
754
763
  - `readOnly` — `boolean` · default `false` — Makes the field non-editable while still focusable/selectable.
755
764
  - `autoComplete` — `string` — Browser autocomplete hint (e.g. "email", "off").
765
+ - `id` — `string` — Id applied to <input>, handy for labels and aria wiring.
766
+ - `style` — `React.CSSProperties` — Inline styles merged onto <input> after the component's own.
756
767
 
757
768
  ```jsx
758
769
  import { CurrencyField } from "twico-ui";
@@ -1030,6 +1041,8 @@ Props:
1030
1041
  - `disabled` — `boolean` · default `false` — Disables the control — no interaction and a dimmed appearance.
1031
1042
  - `readOnly` — `boolean` · default `false` — Makes the field non-editable while still focusable/selectable.
1032
1043
  - `autoComplete` — `string` — Browser autocomplete hint (e.g. "email", "off").
1044
+ - `id` — `string` — Id applied to <input>, handy for labels and aria wiring.
1045
+ - `style` — `React.CSSProperties` — Inline styles merged onto <input> after the component's own.
1033
1046
 
1034
1047
  ```jsx
1035
1048
  import { Input } from "twico-ui";
@@ -1079,6 +1092,8 @@ Props:
1079
1092
  - `name` — `string` — Form field name submitted with the value.
1080
1093
  - `readOnly` — `boolean` · default `false` — Makes the field non-editable while still focusable/selectable.
1081
1094
  - `autoComplete` — `string` — Browser autocomplete hint (e.g. "email", "off").
1095
+ - `id` — `string` — Id applied to <input>, handy for labels and aria wiring.
1096
+ - `style` — `React.CSSProperties` — Inline styles merged onto <input> after the component's own.
1082
1097
 
1083
1098
  ```jsx
1084
1099
  import { MultiSelect } from "twico-ui";
@@ -1119,6 +1134,8 @@ Props:
1119
1134
  - `readOnly` — `boolean` · default `false` — Makes the field non-editable while still focusable/selectable.
1120
1135
  - `required` — `boolean` · default `false` — Marks the field required for native form validation.
1121
1136
  - `autoComplete` — `string` — Browser autocomplete hint (e.g. "email", "off").
1137
+ - `id` — `string` — Id applied to <input>, handy for labels and aria wiring.
1138
+ - `style` — `React.CSSProperties` — Inline styles merged onto <input> after the component's own.
1122
1139
 
1123
1140
  ```jsx
1124
1141
  import { Radio } from "twico-ui";
@@ -1169,7 +1186,7 @@ const [r, setR] = React.useState(0);
1169
1186
 
1170
1187
  #### Select
1171
1188
 
1172
- A custom select that replaces the native browser dropdown with a rounded, keyboard-navigable popover, supporting grouped options and two-line (title + subtitle) options. Use it when you need a styled single-select with optional in-popover search, grouping, or descriptions.
1189
+ A custom select that replaces the native browser dropdown with a rounded, keyboard-navigable popover, supporting grouped options and two-line (title + subtitle) options. Use it when you need a styled single-select with optional in-popover search, grouping, or descriptions. Its trigger is exposed as the WAI-ARIA select-only combobox (role="combobox" with aria-haspopup="listbox"), as MUI's Select also is, so query it by the combobox role rather than by button — and give it a label, since that role takes its name from the label rather than from the selected value.
1173
1190
 
1174
1191
  `import { Select } from "twico-ui"`
1175
1192
 
@@ -1288,6 +1305,8 @@ Props:
1288
1305
  - `readOnly` — `boolean` · default `false` — Makes the field non-editable while still focusable/selectable.
1289
1306
  - `required` — `boolean` · default `false` — Marks the field required for native form validation.
1290
1307
  - `autoComplete` — `string` — Browser autocomplete hint (e.g. "email", "off").
1308
+ - `id` — `string` — Id applied to <input>, handy for labels and aria wiring.
1309
+ - `style` — `React.CSSProperties` — Inline styles merged onto <input> after the component's own.
1291
1310
 
1292
1311
  ```jsx
1293
1312
  import { Switch } from "twico-ui";
@@ -1326,6 +1345,8 @@ Props:
1326
1345
  - `rows` — `number` — Visible number of text rows.
1327
1346
  - `disabled` — `boolean` · default `false` — Disables the control — no interaction and a dimmed appearance.
1328
1347
  - `readOnly` — `boolean` · default `false` — Makes the field non-editable while still focusable/selectable.
1348
+ - `id` — `string` — Id applied to <textarea>, handy for labels and aria wiring.
1349
+ - `style` — `React.CSSProperties` — Inline styles merged onto <textarea> after the component's own.
1329
1350
 
1330
1351
  ```jsx
1331
1352
  import { Textarea } from "twico-ui";
@@ -2671,6 +2692,8 @@ Props:
2671
2692
  - `open` — `string[]` — Controlled open panel values; pair with onOpenChange. When set, internal state is bypassed and defaultOpen is ignored.
2672
2693
  - `onOpenChange` — `(values: string[]) => void` — Called with the next open values whenever a panel trigger is toggled (fires in both controlled and uncontrolled modes).
2673
2694
  - `headingLevel` — `1 | 2 | 3 | 4 | 5 | 6` · default `3` — Heading level wrapping each trigger button (document-outline semantics).
2695
+ - `mountOnOpen` — `boolean` · default `false` — Render each panel's content only after its first open, then keep it mounted - cuts the DOM and hook cost of a page full of collapsed panels.
2696
+ - `unmountOnClose` — `boolean` · default `false` — Also unmount a panel's content when it closes, waiting out the collapse animation first. Implies mountOnOpen.
2674
2697
 
2675
2698
  ```jsx
2676
2699
  import { Accordion } from "twico-ui";
@@ -2741,7 +2764,7 @@ Props:
2741
2764
  import { Navbar } from "twico-ui";
2742
2765
 
2743
2766
  <Navbar
2744
- brand={<>Twico <span style={{ color: "var(--color-primary)" }}>UI</span></>}
2767
+ brand={<>Twico <span style={{ color: "var(--color-primary-subtle-fg)" }}>UI</span></>}
2745
2768
  links={[
2746
2769
  { label: "Dashboard", active: true },
2747
2770
  { label: "Projects" },
@@ -2779,7 +2802,7 @@ const MailIcon = () => (<svg width="18" height="18" viewBox="0 0 24 24" fill="no
2779
2802
  const CogIcon = () => (<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round"><circle cx="12" cy="12" r="3" /><path d="M12 3v3M12 18v3M3 12h3M18 12h3" /></svg>);
2780
2803
 
2781
2804
  <Sidebar
2782
- brand={<>Twico <span style={{ color: "var(--color-primary)" }}>UI</span></>}
2805
+ brand={<>Twico <span style={{ color: "var(--color-primary-subtle-fg)" }}>UI</span></>}
2783
2806
  items={[
2784
2807
  { section: "Main" },
2785
2808
  { label: "Dashboard", icon: <HomeIcon />, active: true },
@@ -3018,7 +3041,7 @@ Menu is a portaled dropdown menu that takes a clickable trigger and an array of
3018
3041
  `import { Menu } from "twico-ui"`
3019
3042
 
3020
3043
  Props:
3021
- - `trigger` — `React.ReactNode` · **required** — Defines the clickable element that opens the menu, typically a Button or IconButton the dropdown anchors to.
3044
+ - `trigger` — `React.ReactNode` · **required** — Defines the clickable element that opens the menu, typically a Button or IconButton the dropdown anchors to. Pass a single focusable control (a Button/IconButton): the element is cloned to carry tabIndex/aria-expanded, which are only valid on a control, and wrapping the real button to overlay a badge gives two tab stops - put the decoration outside. A wrapping Tooltip goes outside this component.
3022
3045
  - `items` — `MenuItemDef[]` · **required** — Lists the menu entries, where each can be an action item, a separator, or a section heading.
3023
3046
  - `align` — `"start" | "end"` · default `"start"` — Aligns the menu horizontally to the trigger's start or end edge so it sits flush in tight layouts; defaults to start.
3024
3047
  - `header` — `React.ReactNode` — Renders an optional rich node above the items, handy for surfacing user info or context at the top of the menu.
@@ -3050,7 +3073,7 @@ Popover is a click-triggered floating panel anchored to a trigger element, rende
3050
3073
  `import { Popover } from "twico-ui"`
3051
3074
 
3052
3075
  Props:
3053
- - `trigger` — `React.ReactNode` · **required** — The clickable element the panel anchors to; clicking it toggles the popover open and closed.
3076
+ - `trigger` — `React.ReactNode` · **required** — The clickable element the panel anchors to; clicking it toggles the popover open and closed. Pass a single focusable control (a Button/IconButton): the element is cloned to carry tabIndex/aria-expanded, which are only valid on a control, and wrapping the real button to overlay a badge gives two tab stops - put the decoration outside. A wrapping Tooltip goes outside this component.
3054
3077
  - `title` — `React.ReactNode` — Optional bold heading rendered at the top of the panel to introduce its interactive content.
3055
3078
  - `placement` — `"top" | "bottom" | "left" | "right"` · default `"bottom"` — Which side of the trigger the panel opens toward (top, bottom, left, or right), auto-flipping near viewport edges.
3056
3079
  - `align` — `"start" | "center" | "end"` · default `"center"` — Cross-axis alignment (start, center, or end) of the panel relative to the trigger for top and bottom placements.
@@ -3384,10 +3407,10 @@ export function useKeyPress( key: string | string[], handler: (event: KeyboardEv
3384
3407
 
3385
3408
  ### useLocalStorage
3386
3409
 
3387
- State persisted to localStorage (JSON-serialized), SSR-safe.
3410
+ State persisted to localStorage (JSON-serialized), SSR-safe: returns `initialValue` on the server AND on the first client render, then reads storage in a layout effect before paint (#466).
3388
3411
 
3389
3412
  ```ts
3390
- export function useLocalStorage<T>( key: string, initialValue: T ): [T, (value: T | ((prev: T) => T)) => void];
3413
+ export function useLocalStorage<T>( key: string, initialValue: T, options?: UseLocalStorageOptions ): [T, (value: T | ((prev: T) => T)) => void];
3391
3414
  ```
3392
3415
 
3393
3416
  ### useCopyToClipboard
@@ -3432,10 +3455,10 @@ export function useTimeout(callback: () => void, delay: number | null): void;
3432
3455
 
3433
3456
  ### useWindowSize
3434
3457
 
3435
- The current window dimensions (0×0 on the server).
3458
+ The current window dimensions. 0×0 on the server AND on the first client render, then measured in a layout effect before paint (#466) — so hydration never mismatches.
3436
3459
 
3437
3460
  ```ts
3438
- export function useWindowSize(): { width: number; height: number };
3461
+ export function useWindowSize(options?: UseWindowSizeOptions): { width: number; height: number };
3439
3462
  ```
3440
3463
 
3441
3464
  ### useHover
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "twico-ui",
3
- "version": "1.42.0",
3
+ "version": "1.43.0",
4
4
  "description": "Twico UI — a free, modern, themeable React + Tailwind-friendly component library. 61 components with dark mode, motion, and accessibility built in.",
5
5
  "license": "MIT",
6
6
  "author": "Twico UI",
@@ -78,6 +78,8 @@
78
78
  "build": "tsup",
79
79
  "build:css": "node scripts/build-css.mjs",
80
80
  "build:css:check": "node scripts/build-css.mjs --check",
81
+ "build:fonts": "node scripts/build-fonts.mjs",
82
+ "build:fonts:check": "node scripts/build-fonts.mjs --check",
81
83
  "verify:palette": "node scripts/verify-palette.mjs",
82
84
  "gen:llms": "node scripts/gen-llms.mjs",
83
85
  "gen:llms:check": "node scripts/gen-llms.mjs --check",
@@ -108,7 +110,7 @@
108
110
  "@arethetypeswrong/cli": "^0.18.5",
109
111
  "@semantic-release/changelog": "^7.0.0",
110
112
  "@semantic-release/git": "^11.0.1",
111
- "@size-limit/preset-small-lib": "^13.0.3",
113
+ "@size-limit/preset-small-lib": "^14.0.0",
112
114
  "@testing-library/jest-dom": "^7.0.1",
113
115
  "@testing-library/react": "^16.1.0",
114
116
  "@testing-library/user-event": "^14.6.3",
@@ -117,10 +119,11 @@
117
119
  "jsdom": "^30.0.1",
118
120
  "lucide-react": "^1.31.0",
119
121
  "semantic-release": "^25.0.9",
120
- "size-limit": "^13.0.3",
122
+ "size-limit": "^14.0.0",
121
123
  "tsup": "^8.3.5",
122
124
  "typescript": "^5.9.3",
123
- "vitest": "^5.0.0"
125
+ "vitest": "^5.0.0",
126
+ "wawoff2": "^2.0.1"
124
127
  },
125
128
  "size-limit": [
126
129
  {
@@ -13,11 +13,13 @@
13
13
  Twico UI — Webfonts (self-hosted, OFL licensed)
14
14
  Plus Jakarta Sans (UI) + JetBrains Mono (code).
15
15
  Variable fonts: one file covers weights 200–800.
16
+ #450: shipped as WOFF2 - brotli-compressed inside the container, so the saving no longer
17
+ depends on the host compressing font/ttf. Generated by `npm run build:fonts` (committed).
16
18
  ============================================================ */
17
19
 
18
20
  @font-face {
19
21
  font-family: "Plus Jakarta Sans";
20
- src: url("./fonts/PlusJakartaSans-Variable.ttf") format("truetype-variations");
22
+ src: url("./fonts/PlusJakartaSans-Variable.woff2") format("woff2");
21
23
  font-weight: 200 800;
22
24
  font-style: normal;
23
25
  font-display: swap;
@@ -25,7 +27,7 @@
25
27
 
26
28
  @font-face {
27
29
  font-family: "Plus Jakarta Sans";
28
- src: url("./fonts/PlusJakartaSans-Italic-Variable.ttf") format("truetype-variations");
30
+ src: url("./fonts/PlusJakartaSans-Italic-Variable.woff2") format("woff2");
29
31
  font-weight: 200 800;
30
32
  font-style: italic;
31
33
  font-display: swap;
@@ -33,7 +35,7 @@
33
35
 
34
36
  @font-face {
35
37
  font-family: "JetBrains Mono";
36
- src: url("./fonts/JetBrainsMono-Variable.ttf") format("truetype-variations");
38
+ src: url("./fonts/JetBrainsMono-Variable.woff2") format("woff2");
37
39
  font-weight: 100 800;
38
40
  font-style: normal;
39
41
  font-display: swap;
@@ -152,16 +154,38 @@
152
154
  /* Borders & dividers */
153
155
  --color-border: var(--slate-200);
154
156
  --color-border-strong: var(--slate-300);
157
+ /* #454: the unchecked boundary of Checkbox/Radio/Switch is the ONLY indication the control
158
+ exists, so SC 1.4.11 requires 3:1 against the surface. border-strong is 1.49:1. This step
159
+ clears it in both themes (4.76:1 on white, 6.96:1 on the dark surface) without touching
160
+ card/divider borders. */
161
+ --color-control-border: var(--slate-500);
162
+ /* #454 (review): a Switch track is NOT the same problem as a checkbox border. Its white thumb is
163
+ what identifies the control - except in light mode, where a white thumb on a white surface is
164
+ invisible and the track has to carry it. So light needs a dark track (4.76:1 on the surface, and
165
+ 4.76:1 against the thumb)... */
166
+ --color-control-track: var(--slate-500);
167
+ /* #454 (review): the OFF thumb, deliberately STATIC in both themes (like --color-chart-on-fill).
168
+ It used to be --_accent-fg, i.e. the tone's ON-state ink - which is designed to sit on the tone
169
+ FILL, not on a neutral track. For five of the six tones that ink is near-black (success #052e1d,
170
+ info sky-950, warning amber-950, danger's dark variant, and neutral = --color-surface), so on the
171
+ dark track nothing in the control cleared 3:1 and an unchecked success Switch simply vanished -
172
+ the very SC 1.4.11 failure #454 was filed for, reintroduced for 5/6 tones while fixing primary.
173
+ A fixed light thumb resolves against BOTH tracks (4.76:1 on slate-500, 10.35:1 on slate-700) for
174
+ every tone, and the ON state keeps --_accent-fg, which is token-paired with its own fill. */
175
+ --color-control-thumb: #ffffff;
155
176
  --color-divider: var(--slate-200);
156
177
 
157
178
  /* Text */
158
179
  --color-text: var(--slate-900);
159
180
  --color-text-muted: var(--slate-600);
160
- --color-text-subtle: var(--slate-400);
181
+ --color-text-subtle: var(--slate-500);
161
182
  --color-text-inverted: #ffffff;
162
183
 
163
184
  /* Data labels painted ON coloured chart marks (pie/donut slices, treemap tiles,
164
- funnel stages). The mark fills are fixed -500 primitives that DON'T flip with
185
+ funnel stages). #455 made the mark fills theme-aware (--color-chart-1..7), but these two stay
186
+ static on purpose: the fills still span a wide lightness range in BOTH themes, so a theme-aware
187
+ label colour would wash out on the light ones and invert on the dark ones. Historically the
188
+ comment here said the fills were fixed -500 primitives that DON'T flip with
165
189
  theme, so these must stay static too (they are intentionally NOT re-declared in
166
190
  .dark): a light fill + a translucent dark halo — applied via `paint-order:
167
191
  stroke` — keeps the text legible on any slice colour, including light ones like
@@ -186,26 +210,51 @@
186
210
  --color-success: var(--emerald-600);
187
211
  --color-success-fg: #ffffff;
188
212
  --color-success-subtle: var(--emerald-50);
189
- --color-success-subtle-fg: var(--emerald-600);
213
+ --color-success-subtle-fg: var(--emerald-700);
190
214
 
191
215
  /* Warning */
192
216
  --color-warning: var(--amber-500);
193
217
  --color-warning-fg: var(--amber-950); /* dark ink on amber-500 = 6.97:1 (white was 2.15:1) */
194
218
  --color-warning-subtle: var(--amber-50);
195
- --color-warning-subtle-fg: var(--amber-600);
219
+ --color-warning-subtle-fg: var(--amber-700);
196
220
 
197
221
  /* Danger */
198
222
  --color-danger: var(--rose-500);
199
223
  --color-danger-hover: var(--rose-600);
200
224
  --color-danger-fg: #ffffff;
201
225
  --color-danger-subtle: var(--rose-50);
202
- --color-danger-subtle-fg: var(--rose-600);
226
+ --color-danger-subtle-fg: var(--rose-700);
203
227
 
204
228
  /* Info */
205
229
  --color-info: var(--sky-500);
206
230
  --color-info-fg: var(--sky-950); /* dark ink on sky-500 (white was 2.77:1) */
207
231
  --color-info-subtle: var(--sky-50);
208
- --color-info-subtle-fg: var(--sky-600);
232
+ --color-info-subtle-fg: var(--sky-700);
233
+
234
+ /* #455: GRAPHIC tone colours — for a fill or stroke that is itself the information (a Progress
235
+ bar, a Toast edge stripe, a status icon). SC 1.4.11 wants 3:1 for those, and the light-mode
236
+ tone colours miss it: amber-500 is 1.96:1 on surface-sunken and sky-500 2.53:1. These steps
237
+ clear 3:1 on BOTH --color-surface (#fff) and --color-surface-sunken (slate-100) with margin
238
+ — 4.1:1 at worst — and keep the five tones at a consistent lightness so a tone switch does
239
+ not change the weight of the control. Use the plain tone for fills that carry text on top
240
+ (they are paired with --color-*-fg); use these when the colour stands alone. */
241
+ --color-primary-graphic: var(--brand-500); /* 4.47 / 4.08 — unchanged, listed for symmetry */
242
+ --color-success-graphic: var(--emerald-700); /* 5.48 / 5.01 */
243
+ --color-warning-graphic: var(--amber-700); /* 5.02 / 4.58 */
244
+ --color-danger-graphic: var(--rose-600); /* 4.70 / 4.29 */
245
+ --color-info-graphic: var(--sky-600); /* 4.10 / 3.74 */
246
+
247
+ /* #455: the chart series ramp. Was seven primitives inlined in _chart.js, where slots 1 and 6
248
+ were BOTH indigo-500 (--brand-500 aliases it) — two series rendered identically, 1.00:1. As
249
+ tokens they also flip per theme, so a series fill is never below 3:1 on its own surface:
250
+ light bottoms out at 4.08:1, and the closest pair is 23.8 ΔE76 apart. */
251
+ --color-chart-1: var(--indigo-500);
252
+ --color-chart-2: var(--sky-700);
253
+ --color-chart-3: var(--emerald-700);
254
+ --color-chart-4: var(--amber-700);
255
+ --color-chart-5: var(--rose-600);
256
+ --color-chart-6: var(--indigo-900);
257
+ --color-chart-7: var(--slate-500);
209
258
  }
210
259
 
211
260
  /* ============================================================
@@ -224,11 +273,19 @@
224
273
 
225
274
  --color-border: #25324a;
226
275
  --color-border-strong: var(--slate-700);
276
+ --color-control-border: var(--slate-400);
277
+ /* ...while dark must go the OTHER way: on a dark surface the white thumb already reads at 10.35:1
278
+ against this track and ~14:1 against the surface, so the control is unmistakable, and lightening
279
+ the track to match --color-control-border would have dropped thumb-vs-track to 2.56:1 - trading
280
+ one 1.4.11 failure for another. On/off is carried by thumb POSITION, so SC 1.4.1 is satisfied
281
+ without colour either way. */
282
+ --color-control-track: var(--slate-700);
283
+ --color-control-thumb: #ffffff; /* static: see the :root note */
227
284
  --color-divider: #1c2840;
228
285
 
229
286
  --color-text: var(--slate-50);
230
287
  --color-text-muted: var(--slate-300);
231
- --color-text-subtle: var(--slate-500);
288
+ --color-text-subtle: var(--slate-400);
232
289
  --color-text-inverted: var(--slate-900);
233
290
 
234
291
  --color-primary: var(--brand-500);
@@ -261,6 +318,33 @@
261
318
  --color-info-fg: #04212f;
262
319
  --color-info-subtle: rgb(14 165 233 / 0.15);
263
320
  --color-info-subtle-fg: var(--sky-400);
321
+
322
+ /* #455: dark already clears 3:1 for every tone on slate-900 (6.4:1 and up), so these are the tone
323
+ values themselves - but written as PRIMITIVES, not as var(--color-warning).
324
+ #455 (review): aliasing the tone here while light named a primitive made the family behave
325
+ differently per theme - a consumer overriding --color-warning saw the Progress bar follow in dark
326
+ and not in light. Naming primitives in both scopes puts -graphic on exactly the same footing as
327
+ -fg / -subtle / -subtle-fg / -border: one more member of the per-tone derivative family that a
328
+ re-skin overrides together (which is what the docs-site Theme builder demonstrates). */
329
+ --color-primary-graphic: var(--brand-500);
330
+ --color-success-graphic: var(--emerald-500);
331
+ --color-warning-graphic: var(--amber-500);
332
+ --color-danger-graphic: var(--rose-500);
333
+ --color-info-graphic: var(--sky-500);
334
+
335
+ /* #455: dark keeps the ORIGINAL 500 steps. Dark mode never had the contrast problem (3.07:1 at
336
+ worst against surface-raised, 6.4:1+ on the surface itself), and the on-slice data labels in
337
+ PieChart/DonutChart/Treemap/FunnelChart are a static white fill + dark halo tuned against these
338
+ exact fills (see docs/charts.md) - lightening them would weaken that for no gain. Only slot 6
339
+ changes, from the indigo-500 that duplicated slot 1 to indigo-300: 25.7 ΔE76 from slot 1, and a
340
+ white-on-slice ratio in line with the amber-500 slice the label design already handles. */
341
+ --color-chart-1: var(--indigo-500);
342
+ --color-chart-2: var(--sky-500);
343
+ --color-chart-3: var(--emerald-500);
344
+ --color-chart-4: var(--amber-500);
345
+ --color-chart-5: var(--rose-500);
346
+ --color-chart-6: var(--indigo-300);
347
+ --color-chart-7: var(--slate-500);
264
348
  }
265
349
 
266
350
  /* ===== tokens/typography.css ===== */
@@ -552,7 +636,7 @@ h1, h2, h3, h4, h5, h6 {
552
636
  p { margin: 0; }
553
637
 
554
638
  a {
555
- color: var(--color-primary);
639
+ color: var(--color-primary-subtle-fg);
556
640
  text-decoration: none;
557
641
  transition: color var(--duration-fast) var(--ease-standard);
558
642
  }