@tangle-network/brand 1.30.0 → 1.31.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/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.31.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 36a423d: Per-user accent scopes. `data-accent="violet" | "orange" | "teal" | "blue" | "pink" | "brown" | "cyan" | "lime"` on `<html>` or any theme boundary rebinds the four indigo roles (fill, mark, line, ink) and the ring to that category's hue, in both modes and inside every named theme; buttons, focus, selection, interaction states, wells, gradients and the chart accent follow, while planes, body ink and status stay the theme's. `gen-ladders.mjs` solves each hue's roles (`--tone-<hue>-{fill,ring,mark,line,ink}`): the fill carries white at 4.5:1, the ink clears 4.5:1 on the page, the card and every selected tint, the line and the focus border clear 3:1. No picker ships; a product sets the attribute.
8
+
9
+ `tangle-drift`'s `settings_layout` counts a settings page as composed when it binds the shared frame with `bindSettingsNav` from `@tangle-network/ui/presentation`, as a server-rendered host does, as well as when it imports `@tangle-network/agent-app/settings-react`.
10
+
3
11
  ## 1.30.0
4
12
 
5
13
  ### Minor Changes
package/README.md CHANGED
@@ -60,6 +60,7 @@ For finer control, use the exported filenames:
60
60
  | `.dark[data-theme="intelligence"]` | Existing dark-only product surface; `.light` keeps the identity and uses canonical light |
61
61
  | `[data-theme="hospitality"]` + `.dark[data-theme="hospitality"]` | Hospitality's sage-and-forest product theme in both modes. Set it on the document root. Without `.dark` it is a complete light scope, including before hydration; `.dark` selects the dark baseline and its dark ladder. Status tones stay canonical |
62
62
  | `.dark[data-theme="website"]` + `[data-theme="website"]` | The public site's indigo-night planes and indigo-paper light. Set `class="dark" data-theme="website"` on the document root; a section with `data-theme="website"` and no `.dark` is a complete light island. Accent, status, category and syntax tones stay canonical |
63
+ | `data-accent="violet"`, `"orange"`, `"teal"`, `"blue"`, `"pink"`, `"brown"`, `"cyan"` or `"lime"` | A per-user accent. Set it on `<html>` or any theme boundary, in either mode and inside any named theme. It rebinds the four indigo roles (`--hsl-primary` fill, `--brand-primary` mark, `--brand-cool` line, `--accent-text` ink) and `--hsl-ring` to that category's `--tone-<hue>-*` values, and everything the spine derives from them follows: buttons, focus, selection, interaction states, wells, gradients, chart accent. Planes, body ink and status stay the theme's. Generated by `scripts/gen-ladders.mjs`; no picker ships, the product stores and sets the attribute |
63
64
 
64
65
  Set one coherent mode on each boundary; do not author contradictory `.light`/`.dark`/named-mode markers. Named light variants keep their existing palettes; `tangle-light` is not silently retuned to the neutral base light palette. Each known boundary receives a complete baseline before named overrides, so nested scopes do not inherit opposite-mode syntax, status or already-resolved HSL aliases.
65
66
 
@@ -143,11 +143,12 @@ export const RULE_PATTERNS = {
143
143
  */
144
144
  export const PAGE_PATTERNS = {
145
145
  // A settings page drawn locally. agent-app's settings-react owns the settings composition: the
146
- // section nav, frame, rows and save behaviour.
146
+ // section nav, frame, rows and save behaviour. A server-rendered host binds the same frame with
147
+ // ui's bindSettingsNav.
147
148
  settings_layout: {
148
149
  name: "settings",
149
- composes: String.raw`(?:\bfrom|\bimport)\s*\(?\s*["']@tangle-network/agent-app/settings-react(?:/[^"']*)?["']`,
150
- reason: "imports nothing from @tangle-network/agent-app/settings-react",
150
+ composes: String.raw`(?:\bfrom|\bimport)\s*\(?\s*["']@tangle-network/agent-app/settings-react(?:/[^"']*)?["']|\bimport\s*\{[^}]*\bbindSettingsNav\b[^}]*\}\s*from\s*["']@tangle-network/ui/presentation["']`,
151
+ reason: "imports nothing from @tangle-network/agent-app/settings-react, nor bindSettingsNav from @tangle-network/ui/presentation",
151
152
  },
152
153
  };
153
154
  const PAGE_DIR = /(?:^|\/)(?:routes|pages|views)\//i;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tangle-network/brand",
3
- "version": "1.30.0",
3
+ "version": "1.31.0",
4
4
  "description": "Tangle brand system — design tokens, logo, Tailwind v4 theme. Single source of truth for every Tangle app.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -0,0 +1,54 @@
1
+ import type { Meta, StoryObj } from "@storybook/react";
2
+
3
+ // Each data-accent scope inside an explicit .dark and .light boundary and two
4
+ // named themes: fill, ink, focus border, selection, mark and line follow the
5
+ // hue while the card, body ink and status stay the theme's.
6
+ const HUES = ["violet", "orange", "teal", "blue", "pink", "brown", "cyan", "lime"] as const;
7
+
8
+ function Island({ accent, theme, label }: { accent?: string; theme?: string; label: string }) {
9
+ return (
10
+ <section data-accent={accent} data-theme={theme} className="rounded-[var(--radius-card)] border border-border bg-card p-3 text-foreground">
11
+ <p className="mb-2 text-xs text-muted-foreground">{label}</p>
12
+ <div className="flex flex-wrap items-center gap-2">
13
+ <button type="button" className="h-8 rounded-[var(--radius-control)] bg-primary px-3 text-sm font-medium text-primary-foreground">
14
+ Run
15
+ </button>
16
+ <span className="text-sm font-medium text-[var(--accent-text)]">Accent ink</span>
17
+ <span className="h-8 w-14 rounded-[var(--radius-control)] border border-[var(--focus-border)] shadow-[0_0_0_3px_var(--focus-halo)]" />
18
+ <span className="rounded-[var(--radius-pill)] bg-[var(--bg-selection)] px-2 py-0.5 text-xs">Selected</span>
19
+ <span className="size-4 rounded-full bg-[var(--brand-primary)]" />
20
+ <span className="w-8 border-t-2 border-[var(--brand-cool)]" />
21
+ <span className="text-xs text-[var(--surface-success-text)]">Done</span>
22
+ </div>
23
+ </section>
24
+ );
25
+ }
26
+
27
+ function Mode({ mode }: { mode: "dark" | "light" }) {
28
+ return (
29
+ <div className={`${mode} bg-background p-4`} data-mode={mode}>
30
+ <div className="grid gap-3 sm:grid-cols-3">
31
+ <Island label="indigo (no accent)" />
32
+ {HUES.map((hue) => (
33
+ <Island key={hue} accent={hue} label={hue} />
34
+ ))}
35
+ </div>
36
+ </div>
37
+ );
38
+ }
39
+
40
+ const meta: Meta = { title: "Brand/Accent scopes", parameters: { layout: "fullscreen" } };
41
+ export default meta;
42
+
43
+ export const AllHues: StoryObj = {
44
+ render: () => (
45
+ <div>
46
+ <Mode mode="dark" />
47
+ <Mode mode="light" />
48
+ <div className="grid gap-3 p-4 sm:grid-cols-2">
49
+ <Island theme="aubergine" accent="pink" label="pink inside aubergine" />
50
+ <Island theme="super-light" accent="teal" label="teal inside super-light" />
51
+ </div>
52
+ </div>
53
+ ),
54
+ };
@@ -1,6 +1,6 @@
1
1
  /* GENERATED by scripts/gen-compat.mjs. Do not edit.
2
2
  * Source: tokens.css + named-themes.css
3
- * SHA-256: 83dde77644ce502513ced146ea7ed7dad93fc736195530c0c2bef82849bf1f21
3
+ * SHA-256: 728da214076ca13bbac43529a3ba30c5349fdd796266ebe18b354598b58acc40
4
4
  * Light-default compatibility; all explicit modes remain canonical.
5
5
  */
6
6
  :where(:root),
@@ -239,6 +239,46 @@
239
239
  --surface-teal-bg: #18333b;
240
240
  --surface-teal-border: #207e73;
241
241
  --surface-teal-text: #0bd8b6;
242
+ --tone-violet-fill: 272 44.9% 50.2%;
243
+ --tone-violet-ring: 272 100% 80.8%;
244
+ --tone-violet-mark: #8e4ec6;
245
+ --tone-violet-line: #8e4ec6;
246
+ --tone-violet-ink: #d19dff;
247
+ --tone-orange-fill: 22 93.6% 36.9%;
248
+ --tone-orange-ring: 26 100% 67.1%;
249
+ --tone-orange-mark: #f76b15;
250
+ --tone-orange-line: #f76b15;
251
+ --tone-orange-ink: #ffa057;
252
+ --tone-teal-fill: 172 95% 23.7%;
253
+ --tone-teal-ring: 170 90.3% 44.5%;
254
+ --tone-teal-mark: #12a594;
255
+ --tone-teal-line: #12a594;
256
+ --tone-teal-ink: #0bd8b6;
257
+ --tone-blue-fill: 209 86.3% 40%;
258
+ --tone-blue-ring: 210 100% 72%;
259
+ --tone-blue-mark: #0090ff;
260
+ --tone-blue-line: #0090ff;
261
+ --tone-blue-ink: #70b8ff;
262
+ --tone-pink-fill: 322 65.1% 46.1%;
263
+ --tone-pink-ring: 327 100% 77.6%;
264
+ --tone-pink-mark: #d6409f;
265
+ --tone-pink-line: #d6409f;
266
+ --tone-pink-ink: #ff8dcc;
267
+ --tone-brown-fill: 24 29.6% 39%;
268
+ --tone-brown-ring: 28 49.7% 72%;
269
+ --tone-brown-mark: #ad7f58;
270
+ --tone-brown-line: #ad7f58;
271
+ --tone-brown-ink: #dbb594;
272
+ --tone-cyan-fill: 192 80.4% 30%;
273
+ --tone-cyan-ring: 190 75.5% 60%;
274
+ --tone-cyan-mark: #00a2c7;
275
+ --tone-cyan-line: #00a2c7;
276
+ --tone-cyan-ink: #4ccce6;
277
+ --tone-lime-fill: 84 44.2% 30.2%;
278
+ --tone-lime-ring: 80 69.9% 66.1%;
279
+ --tone-lime-mark: #bdee63;
280
+ --tone-lime-line: #bdee63;
281
+ --tone-lime-ink: #bde56c;
242
282
  --run-mix-succeeded: #76edb9;
243
283
  --run-mix-waiting: #e9b642;
244
284
  --run-mix-failed: #e1918e;
@@ -531,6 +571,46 @@
531
571
  --surface-teal-bg: #e0f8f3;
532
572
  --surface-teal-border: #a1ded2;
533
573
  --surface-teal-text: #017d6d;
574
+ --tone-violet-fill: 272 44.9% 50.2%;
575
+ --tone-violet-ring: 272 44.9% 50.2%;
576
+ --tone-violet-mark: #8e4ec6;
577
+ --tone-violet-line: #8e4ec6;
578
+ --tone-violet-ink: #753ea6;
579
+ --tone-orange-fill: 22 93.6% 36.9%;
580
+ --tone-orange-ring: 22 93.6% 36.9%;
581
+ --tone-orange-mark: #f76b15;
582
+ --tone-orange-line: #cc4e00;
583
+ --tone-orange-ink: #953d0e;
584
+ --tone-teal-fill: 172 95% 23.7%;
585
+ --tone-teal-ring: 172 95% 23.7%;
586
+ --tone-teal-mark: #12a594;
587
+ --tone-teal-line: #008573;
588
+ --tone-teal-ink: #066558;
589
+ --tone-blue-fill: 209 86.3% 40%;
590
+ --tone-blue-ring: 209 86.3% 40%;
591
+ --tone-blue-mark: #0090ff;
592
+ --tone-blue-line: #0588f0;
593
+ --tone-blue-ink: #0f59a3;
594
+ --tone-pink-fill: 322 65.1% 46.1%;
595
+ --tone-pink-ring: 322 65.1% 46.1%;
596
+ --tone-pink-mark: #d6409f;
597
+ --tone-pink-line: #d6409f;
598
+ --tone-pink-ink: #a02071;
599
+ --tone-brown-fill: 24 29.6% 39%;
600
+ --tone-brown-ring: 24 29.6% 39%;
601
+ --tone-brown-mark: #ad7f58;
602
+ --tone-brown-line: #ad7f58;
603
+ --tone-brown-ink: #725440;
604
+ --tone-cyan-fill: 192 80.4% 30%;
605
+ --tone-cyan-ring: 192 80.4% 30%;
606
+ --tone-cyan-mark: #00a2c7;
607
+ --tone-cyan-line: #107d98;
608
+ --tone-cyan-ink: #0f6277;
609
+ --tone-lime-fill: 84 44.2% 30.2%;
610
+ --tone-lime-ring: 84 44.2% 30.2%;
611
+ --tone-lime-mark: #bdee63;
612
+ --tone-lime-line: #5c7c2f;
613
+ --tone-lime-ink: #4b6126;
534
614
  --run-mix-succeeded: #3c9b78;
535
615
  --run-mix-waiting: #c16227;
536
616
  --run-mix-failed: #cd342f;
@@ -1608,3 +1688,139 @@
1608
1688
  --text-muted: #9496bc;
1609
1689
  --text-dim: #9496bc;
1610
1690
  }
1691
+ [data-accent="violet"],
1692
+ [data-accent="violet"]:is(.light, .dark, [data-theme], [data-sandbox-ui], [data-sandbox-theme]),
1693
+ [data-accent="violet"] :is(.light, .dark, [data-theme], [data-sandbox-ui], [data-sandbox-theme]) {
1694
+ --hsl-primary: var(--tone-violet-fill);
1695
+ --hsl-ring: var(--tone-violet-ring);
1696
+ --brand-primary: var(--tone-violet-mark);
1697
+ --brand-cool: var(--tone-violet-line);
1698
+ --accent-text: var(--tone-violet-ink);
1699
+ }
1700
+ [data-accent="orange"],
1701
+ [data-accent="orange"]:is(.light, .dark, [data-theme], [data-sandbox-ui], [data-sandbox-theme]),
1702
+ [data-accent="orange"] :is(.light, .dark, [data-theme], [data-sandbox-ui], [data-sandbox-theme]) {
1703
+ --hsl-primary: var(--tone-orange-fill);
1704
+ --hsl-ring: var(--tone-orange-ring);
1705
+ --brand-primary: var(--tone-orange-mark);
1706
+ --brand-cool: var(--tone-orange-line);
1707
+ --accent-text: var(--tone-orange-ink);
1708
+ }
1709
+ [data-accent="teal"],
1710
+ [data-accent="teal"]:is(.light, .dark, [data-theme], [data-sandbox-ui], [data-sandbox-theme]),
1711
+ [data-accent="teal"] :is(.light, .dark, [data-theme], [data-sandbox-ui], [data-sandbox-theme]) {
1712
+ --hsl-primary: var(--tone-teal-fill);
1713
+ --hsl-ring: var(--tone-teal-ring);
1714
+ --brand-primary: var(--tone-teal-mark);
1715
+ --brand-cool: var(--tone-teal-line);
1716
+ --accent-text: var(--tone-teal-ink);
1717
+ }
1718
+ [data-accent="blue"],
1719
+ [data-accent="blue"]:is(.light, .dark, [data-theme], [data-sandbox-ui], [data-sandbox-theme]),
1720
+ [data-accent="blue"] :is(.light, .dark, [data-theme], [data-sandbox-ui], [data-sandbox-theme]) {
1721
+ --hsl-primary: var(--tone-blue-fill);
1722
+ --hsl-ring: var(--tone-blue-ring);
1723
+ --brand-primary: var(--tone-blue-mark);
1724
+ --brand-cool: var(--tone-blue-line);
1725
+ --accent-text: var(--tone-blue-ink);
1726
+ }
1727
+ [data-accent="pink"],
1728
+ [data-accent="pink"]:is(.light, .dark, [data-theme], [data-sandbox-ui], [data-sandbox-theme]),
1729
+ [data-accent="pink"] :is(.light, .dark, [data-theme], [data-sandbox-ui], [data-sandbox-theme]) {
1730
+ --hsl-primary: var(--tone-pink-fill);
1731
+ --hsl-ring: var(--tone-pink-ring);
1732
+ --brand-primary: var(--tone-pink-mark);
1733
+ --brand-cool: var(--tone-pink-line);
1734
+ --accent-text: var(--tone-pink-ink);
1735
+ }
1736
+ [data-accent="brown"],
1737
+ [data-accent="brown"]:is(.light, .dark, [data-theme], [data-sandbox-ui], [data-sandbox-theme]),
1738
+ [data-accent="brown"] :is(.light, .dark, [data-theme], [data-sandbox-ui], [data-sandbox-theme]) {
1739
+ --hsl-primary: var(--tone-brown-fill);
1740
+ --hsl-ring: var(--tone-brown-ring);
1741
+ --brand-primary: var(--tone-brown-mark);
1742
+ --brand-cool: var(--tone-brown-line);
1743
+ --accent-text: var(--tone-brown-ink);
1744
+ }
1745
+ [data-accent="cyan"],
1746
+ [data-accent="cyan"]:is(.light, .dark, [data-theme], [data-sandbox-ui], [data-sandbox-theme]),
1747
+ [data-accent="cyan"] :is(.light, .dark, [data-theme], [data-sandbox-ui], [data-sandbox-theme]) {
1748
+ --hsl-primary: var(--tone-cyan-fill);
1749
+ --hsl-ring: var(--tone-cyan-ring);
1750
+ --brand-primary: var(--tone-cyan-mark);
1751
+ --brand-cool: var(--tone-cyan-line);
1752
+ --accent-text: var(--tone-cyan-ink);
1753
+ }
1754
+ [data-accent="lime"],
1755
+ [data-accent="lime"]:is(.light, .dark, [data-theme], [data-sandbox-ui], [data-sandbox-theme]),
1756
+ [data-accent="lime"] :is(.light, .dark, [data-theme], [data-sandbox-ui], [data-sandbox-theme]) {
1757
+ --hsl-primary: var(--tone-lime-fill);
1758
+ --hsl-ring: var(--tone-lime-ring);
1759
+ --brand-primary: var(--tone-lime-mark);
1760
+ --brand-cool: var(--tone-lime-line);
1761
+ --accent-text: var(--tone-lime-ink);
1762
+ }
1763
+ :is([data-accent="violet"], [data-accent="orange"], [data-accent="teal"], [data-accent="blue"], [data-accent="pink"], [data-accent="brown"], [data-accent="cyan"], [data-accent="lime"]), :is([data-accent="violet"], [data-accent="orange"], [data-accent="teal"], [data-accent="blue"], [data-accent="pink"], [data-accent="brown"], [data-accent="cyan"], [data-accent="lime"]):is(.light, .dark, [data-theme], [data-sandbox-ui], [data-sandbox-theme]), :is([data-accent="violet"], [data-accent="orange"], [data-accent="teal"], [data-accent="blue"], [data-accent="pink"], [data-accent="brown"], [data-accent="cyan"], [data-accent="lime"]) :is(.light, .dark, [data-theme], [data-sandbox-ui], [data-sandbox-theme]) {
1764
+ --btn-primary-hover: color-mix(in srgb, hsl(var(--hsl-primary)), white 10%);
1765
+ --brand-purple: light-dark(var(--brand-primary), color-mix(in srgb, var(--accent-text), white 35%));
1766
+ --brand-strong: color-mix(in srgb, hsl(var(--hsl-primary)), black 50%);
1767
+ --md3-primary-fixed: light-dark(color-mix(in srgb, var(--accent-text) 12%, white), var(--accent-text));
1768
+ --md3-inverse-primary: light-dark(color-mix(in srgb, var(--brand-primary), white 40%), var(--btn-primary-bg));
1769
+ --hsl-primary-foreground: 0 0% 100%;
1770
+ --btn-primary-text: #ffffff;
1771
+ --md3-on-primary: #ffffff;
1772
+ --brand-glow: var(--accent-text);
1773
+ --brand-vibrant: var(--brand-primary);
1774
+ --btn-primary-bg: hsl(var(--hsl-primary));
1775
+ --btn-cta-bg: var(--btn-primary-bg);
1776
+ --btn-cta-text: var(--btn-primary-text);
1777
+ --sidebar-primary: var(--hsl-primary);
1778
+ --sidebar-primary-foreground: var(--hsl-primary-foreground);
1779
+ --sidebar-ring: var(--hsl-ring);
1780
+ --md3-primary: var(--accent-text);
1781
+ --md3-primary-dim: light-dark(var(--btn-primary-bg), var(--brand-primary));
1782
+ --md3-primary-container: var(--btn-primary-bg);
1783
+ --md3-on-primary-container: light-dark(#ffffff, var(--brand-purple));
1784
+ --md3-primary-fixed-dim: light-dark(color-mix(in srgb, var(--md3-primary-fixed) 85%, var(--accent-text)), var(--brand-cool));
1785
+ --md3-secondary: light-dark(var(--accent-text), var(--brand-purple));
1786
+ --md3-secondary-dim: light-dark(var(--btn-primary-bg), var(--accent-text));
1787
+ --md3-secondary-container: light-dark(var(--md3-primary-fixed), var(--brand-strong));
1788
+ --md3-on-secondary: light-dark(#ffffff, var(--brand-purple));
1789
+ --md3-on-secondary-container: light-dark(var(--accent-text), var(--brand-purple));
1790
+ --md3-surface-tint: light-dark(var(--brand-primary), var(--accent-text));
1791
+ --accent-surface-soft: light-dark(color-mix(in srgb, var(--btn-primary-bg) 5%, transparent), color-mix(in srgb, var(--accent-text) 7%, transparent));
1792
+ --accent-surface-strong: light-dark(color-mix(in srgb, var(--btn-primary-bg) 13%, transparent), color-mix(in srgb, var(--btn-primary-bg) 30%, transparent));
1793
+ --bg-hover: var(--accent-surface-soft);
1794
+ --bg-selection: light-dark(color-mix(in srgb, var(--btn-primary-bg) 16%, transparent), color-mix(in srgb, var(--brand-primary) 28%, transparent));
1795
+ --border-accent: light-dark(color-mix(in srgb, var(--btn-primary-hover) 18%, transparent), color-mix(in srgb, var(--brand-cool) 35%, transparent));
1796
+ --border-accent-hover: light-dark(color-mix(in srgb, var(--btn-primary-hover) 35%, transparent), color-mix(in srgb, var(--brand-cool) 50%, transparent));
1797
+ --tangle-gradient: linear-gradient(135deg, var(--btn-primary-bg), light-dark(var(--btn-primary-hover), var(--accent-text)));
1798
+ --tangle-gradient-text: linear-gradient(135deg, light-dark(var(--btn-primary-bg), var(--brand-purple)), light-dark(var(--btn-primary-hover), var(--accent-text)));
1799
+ --accent-gradient-strong: linear-gradient(135deg, var(--btn-primary-bg), light-dark(var(--btn-primary-hover), var(--brand-primary)));
1800
+ --status-creating: var(--accent-text);
1801
+ --primary: var(--hsl-primary);
1802
+ --primary-foreground: var(--hsl-primary-foreground);
1803
+ --ring: var(--hsl-ring);
1804
+ --syntax-meta: var(--accent-text);
1805
+ --display-control-shadow: inset 0 1px 0 rgb(255 255 255 / 0.18), 0 4px 16px hsl(var(--primary) / 0.2);
1806
+ --display-gradient: linear-gradient(135deg, hsl(var(--foreground)) 10%, var(--accent-text) 70%);
1807
+ --focus-border: hsl(var(--hsl-ring) / 0.8);
1808
+ --focus-halo: hsl(var(--hsl-ring) / 0.18);
1809
+ --ix-hover: 0 0 0 1px hsl(var(--hsl-ring) / 0.45), 0 0 0 4px hsl(var(--hsl-ring) / 0.16);
1810
+ --ix-hover-border: hsl(var(--hsl-ring) / 0.7);
1811
+ --ix-press: 0 0 0 1px hsl(var(--hsl-ring) / 0.95), 0 0 0 4px hsl(var(--hsl-ring) / 0.3);
1812
+ --ix-after: 0 0 0 1px hsl(var(--hsl-ring) / 0.6), 0 0 0 3px hsl(var(--hsl-ring) / 0.14);
1813
+ --ix-sel-ring: hsl(var(--hsl-ring) / 0.85);
1814
+ --ix-sel-bg: light-dark(hsl(var(--hsl-ring) / 0.13), hsl(var(--hsl-ring) / 0.18));
1815
+ --ix-hover-bg: light-dark(hsl(var(--hsl-ring) / 0.07), hsl(var(--hsl-ring) / 0.08));
1816
+ --chart-shade: var(--bg-hover);
1817
+ --chart-accent: light-dark(hsl(var(--hsl-primary)), var(--brand-cool));
1818
+ --color-brand: var(--brand-primary);
1819
+ --color-brand-dim: var(--btn-primary-bg);
1820
+ --color-brand-cool: var(--brand-cool);
1821
+ --color-brand-glow: var(--brand-glow);
1822
+ --color-status-creating: var(--status-creating);
1823
+ --color-primary: hsl(var(--primary));
1824
+ --color-primary-foreground: hsl(var(--primary-foreground));
1825
+ --color-ring: hsl(var(--ring));
1826
+ }
@@ -629,3 +629,149 @@
629
629
  --border-hover: rgba(255, 255, 255, 0.28);
630
630
  --text-primary: #fdfdfd; --text-secondary: #aaa8b8; --text-muted: #9496bc; --text-dim: #9496bc;
631
631
  }
632
+
633
+ /* accent-scopes:begin — generated by scripts/gen-ladders.mjs from its CATEGORY map and the spine; edit there, not these lines.
634
+ * data-accent="<category>" on <html> or any theme boundary retints the
635
+ * interaction accent: the five roles take the hue's --tone-<hue>-* values and
636
+ * the shared rule re-declares what derives from them. Planes, body ink and
637
+ * status stay the theme's. Last in the stylesheet, so a scope wins over the
638
+ * named theme it sits on or in. */
639
+ [data-accent="violet"],
640
+ [data-accent="violet"]:is(.light, .dark, [data-theme], [data-sandbox-ui], [data-sandbox-theme]),
641
+ [data-accent="violet"] :is(.light, .dark, [data-theme], [data-sandbox-ui], [data-sandbox-theme]) {
642
+ --hsl-primary: var(--tone-violet-fill);
643
+ --hsl-ring: var(--tone-violet-ring);
644
+ --brand-primary: var(--tone-violet-mark);
645
+ --brand-cool: var(--tone-violet-line);
646
+ --accent-text: var(--tone-violet-ink);
647
+ }
648
+ [data-accent="orange"],
649
+ [data-accent="orange"]:is(.light, .dark, [data-theme], [data-sandbox-ui], [data-sandbox-theme]),
650
+ [data-accent="orange"] :is(.light, .dark, [data-theme], [data-sandbox-ui], [data-sandbox-theme]) {
651
+ --hsl-primary: var(--tone-orange-fill);
652
+ --hsl-ring: var(--tone-orange-ring);
653
+ --brand-primary: var(--tone-orange-mark);
654
+ --brand-cool: var(--tone-orange-line);
655
+ --accent-text: var(--tone-orange-ink);
656
+ }
657
+ [data-accent="teal"],
658
+ [data-accent="teal"]:is(.light, .dark, [data-theme], [data-sandbox-ui], [data-sandbox-theme]),
659
+ [data-accent="teal"] :is(.light, .dark, [data-theme], [data-sandbox-ui], [data-sandbox-theme]) {
660
+ --hsl-primary: var(--tone-teal-fill);
661
+ --hsl-ring: var(--tone-teal-ring);
662
+ --brand-primary: var(--tone-teal-mark);
663
+ --brand-cool: var(--tone-teal-line);
664
+ --accent-text: var(--tone-teal-ink);
665
+ }
666
+ [data-accent="blue"],
667
+ [data-accent="blue"]:is(.light, .dark, [data-theme], [data-sandbox-ui], [data-sandbox-theme]),
668
+ [data-accent="blue"] :is(.light, .dark, [data-theme], [data-sandbox-ui], [data-sandbox-theme]) {
669
+ --hsl-primary: var(--tone-blue-fill);
670
+ --hsl-ring: var(--tone-blue-ring);
671
+ --brand-primary: var(--tone-blue-mark);
672
+ --brand-cool: var(--tone-blue-line);
673
+ --accent-text: var(--tone-blue-ink);
674
+ }
675
+ [data-accent="pink"],
676
+ [data-accent="pink"]:is(.light, .dark, [data-theme], [data-sandbox-ui], [data-sandbox-theme]),
677
+ [data-accent="pink"] :is(.light, .dark, [data-theme], [data-sandbox-ui], [data-sandbox-theme]) {
678
+ --hsl-primary: var(--tone-pink-fill);
679
+ --hsl-ring: var(--tone-pink-ring);
680
+ --brand-primary: var(--tone-pink-mark);
681
+ --brand-cool: var(--tone-pink-line);
682
+ --accent-text: var(--tone-pink-ink);
683
+ }
684
+ [data-accent="brown"],
685
+ [data-accent="brown"]:is(.light, .dark, [data-theme], [data-sandbox-ui], [data-sandbox-theme]),
686
+ [data-accent="brown"] :is(.light, .dark, [data-theme], [data-sandbox-ui], [data-sandbox-theme]) {
687
+ --hsl-primary: var(--tone-brown-fill);
688
+ --hsl-ring: var(--tone-brown-ring);
689
+ --brand-primary: var(--tone-brown-mark);
690
+ --brand-cool: var(--tone-brown-line);
691
+ --accent-text: var(--tone-brown-ink);
692
+ }
693
+ [data-accent="cyan"],
694
+ [data-accent="cyan"]:is(.light, .dark, [data-theme], [data-sandbox-ui], [data-sandbox-theme]),
695
+ [data-accent="cyan"] :is(.light, .dark, [data-theme], [data-sandbox-ui], [data-sandbox-theme]) {
696
+ --hsl-primary: var(--tone-cyan-fill);
697
+ --hsl-ring: var(--tone-cyan-ring);
698
+ --brand-primary: var(--tone-cyan-mark);
699
+ --brand-cool: var(--tone-cyan-line);
700
+ --accent-text: var(--tone-cyan-ink);
701
+ }
702
+ [data-accent="lime"],
703
+ [data-accent="lime"]:is(.light, .dark, [data-theme], [data-sandbox-ui], [data-sandbox-theme]),
704
+ [data-accent="lime"] :is(.light, .dark, [data-theme], [data-sandbox-ui], [data-sandbox-theme]) {
705
+ --hsl-primary: var(--tone-lime-fill);
706
+ --hsl-ring: var(--tone-lime-ring);
707
+ --brand-primary: var(--tone-lime-mark);
708
+ --brand-cool: var(--tone-lime-line);
709
+ --accent-text: var(--tone-lime-ink);
710
+ }
711
+ :is([data-accent="violet"], [data-accent="orange"], [data-accent="teal"], [data-accent="blue"], [data-accent="pink"], [data-accent="brown"], [data-accent="cyan"], [data-accent="lime"]),
712
+ :is([data-accent="violet"], [data-accent="orange"], [data-accent="teal"], [data-accent="blue"], [data-accent="pink"], [data-accent="brown"], [data-accent="cyan"], [data-accent="lime"]):is(.light, .dark, [data-theme], [data-sandbox-ui], [data-sandbox-theme]),
713
+ :is([data-accent="violet"], [data-accent="orange"], [data-accent="teal"], [data-accent="blue"], [data-accent="pink"], [data-accent="brown"], [data-accent="cyan"], [data-accent="lime"]) :is(.light, .dark, [data-theme], [data-sandbox-ui], [data-sandbox-theme]) {
714
+ --btn-primary-hover: color-mix(in srgb, hsl(var(--hsl-primary)), white 10%);
715
+ --brand-purple: light-dark(var(--brand-primary), color-mix(in srgb, var(--accent-text), white 35%));
716
+ --brand-strong: color-mix(in srgb, hsl(var(--hsl-primary)), black 50%);
717
+ --md3-primary-fixed: light-dark(color-mix(in srgb, var(--accent-text) 12%, white), var(--accent-text));
718
+ --md3-inverse-primary: light-dark(color-mix(in srgb, var(--brand-primary), white 40%), var(--btn-primary-bg));
719
+ --hsl-primary-foreground: 0 0% 100%;
720
+ --btn-primary-text: #ffffff;
721
+ --md3-on-primary: #ffffff;
722
+ --brand-glow: var(--accent-text);
723
+ --brand-vibrant: var(--brand-primary);
724
+ --btn-primary-bg: hsl(var(--hsl-primary));
725
+ --btn-cta-bg: var(--btn-primary-bg);
726
+ --btn-cta-text: var(--btn-primary-text);
727
+ --sidebar-primary: var(--hsl-primary);
728
+ --sidebar-primary-foreground: var(--hsl-primary-foreground);
729
+ --sidebar-ring: var(--hsl-ring);
730
+ --md3-primary: var(--accent-text);
731
+ --md3-primary-dim: light-dark(var(--btn-primary-bg), var(--brand-primary));
732
+ --md3-primary-container: var(--btn-primary-bg);
733
+ --md3-on-primary-container: light-dark(#ffffff, var(--brand-purple));
734
+ --md3-primary-fixed-dim: light-dark(color-mix(in srgb, var(--md3-primary-fixed) 85%, var(--accent-text)), var(--brand-cool));
735
+ --md3-secondary: light-dark(var(--accent-text), var(--brand-purple));
736
+ --md3-secondary-dim: light-dark(var(--btn-primary-bg), var(--accent-text));
737
+ --md3-secondary-container: light-dark(var(--md3-primary-fixed), var(--brand-strong));
738
+ --md3-on-secondary: light-dark(#ffffff, var(--brand-purple));
739
+ --md3-on-secondary-container: light-dark(var(--accent-text), var(--brand-purple));
740
+ --md3-surface-tint: light-dark(var(--brand-primary), var(--accent-text));
741
+ --accent-surface-soft: light-dark(color-mix(in srgb, var(--btn-primary-bg) 5%, transparent), color-mix(in srgb, var(--accent-text) 7%, transparent));
742
+ --accent-surface-strong: light-dark(color-mix(in srgb, var(--btn-primary-bg) 13%, transparent), color-mix(in srgb, var(--btn-primary-bg) 30%, transparent));
743
+ --bg-hover: var(--accent-surface-soft);
744
+ --bg-selection: light-dark(color-mix(in srgb, var(--btn-primary-bg) 16%, transparent), color-mix(in srgb, var(--brand-primary) 28%, transparent));
745
+ --border-accent: light-dark(color-mix(in srgb, var(--btn-primary-hover) 18%, transparent), color-mix(in srgb, var(--brand-cool) 35%, transparent));
746
+ --border-accent-hover: light-dark(color-mix(in srgb, var(--btn-primary-hover) 35%, transparent), color-mix(in srgb, var(--brand-cool) 50%, transparent));
747
+ --tangle-gradient: linear-gradient(135deg, var(--btn-primary-bg), light-dark(var(--btn-primary-hover), var(--accent-text)));
748
+ --tangle-gradient-text: linear-gradient(135deg, light-dark(var(--btn-primary-bg), var(--brand-purple)), light-dark(var(--btn-primary-hover), var(--accent-text)));
749
+ --accent-gradient-strong: linear-gradient(135deg, var(--btn-primary-bg), light-dark(var(--btn-primary-hover), var(--brand-primary)));
750
+ --status-creating: var(--accent-text);
751
+ --primary: var(--hsl-primary);
752
+ --primary-foreground: var(--hsl-primary-foreground);
753
+ --ring: var(--hsl-ring);
754
+ --syntax-meta: var(--accent-text);
755
+ --display-control-shadow: inset 0 1px 0 rgb(255 255 255 / 0.18), 0 4px 16px hsl(var(--primary) / 0.2);
756
+ --display-gradient: linear-gradient(135deg, hsl(var(--foreground)) 10%, var(--accent-text) 70%);
757
+ --focus-border: hsl(var(--hsl-ring) / 0.8);
758
+ --focus-halo: hsl(var(--hsl-ring) / 0.18);
759
+ --ix-hover: 0 0 0 1px hsl(var(--hsl-ring) / 0.45), 0 0 0 4px hsl(var(--hsl-ring) / 0.16);
760
+ --ix-hover-border: hsl(var(--hsl-ring) / 0.7);
761
+ --ix-press: 0 0 0 1px hsl(var(--hsl-ring) / 0.95), 0 0 0 4px hsl(var(--hsl-ring) / 0.3);
762
+ --ix-after: 0 0 0 1px hsl(var(--hsl-ring) / 0.6), 0 0 0 3px hsl(var(--hsl-ring) / 0.14);
763
+ --ix-sel-ring: hsl(var(--hsl-ring) / 0.85);
764
+ --ix-sel-bg: light-dark(hsl(var(--hsl-ring) / 0.13), hsl(var(--hsl-ring) / 0.18));
765
+ --ix-hover-bg: light-dark(hsl(var(--hsl-ring) / 0.07), hsl(var(--hsl-ring) / 0.08));
766
+ --chart-shade: var(--bg-hover);
767
+ --chart-accent: light-dark(hsl(var(--hsl-primary)), var(--brand-cool));
768
+ --color-brand: var(--brand-primary);
769
+ --color-brand-dim: var(--btn-primary-bg);
770
+ --color-brand-cool: var(--brand-cool);
771
+ --color-brand-glow: var(--brand-glow);
772
+ --color-status-creating: var(--status-creating);
773
+ --color-primary: hsl(var(--primary));
774
+ --color-primary-foreground: hsl(var(--primary-foreground));
775
+ --color-ring: hsl(var(--ring));
776
+ }
777
+ /* accent-scopes:end */
@@ -80,6 +80,14 @@ export const scopes: Array<{ name: string; sources: string[] }> = [
80
80
  sources: [named('[data-theme="website"]:where(:not(.dark))'), LIGHT, DARK],
81
81
  },
82
82
  { name: "website dark", sources: [named('.dark[data-theme="website"]'), DARK] },
83
+ // Every data-accent scope on each canonical mode: its role rule, then the spine.
84
+ ...[...themes.matchAll(/^\[data-accent="([a-z]+)"\],$/gm)].flatMap(([, hue]) => {
85
+ const rule = blockIn(themes, `[data-accent="${hue}"] :is(.light, .dark, [data-theme], [data-sandbox-ui], [data-sandbox-theme])`);
86
+ return [
87
+ { name: `accent ${hue} on dark`, sources: [rule, DARK] },
88
+ { name: `accent ${hue} on light`, sources: [rule, LIGHT, DARK] },
89
+ ];
90
+ }),
83
91
  ];
84
92
 
85
93
  export type Rgb = [number, number, number];
@@ -310,6 +310,46 @@
310
310
  --surface-teal-bg: #18333b;
311
311
  --surface-teal-border: #207e73;
312
312
  --surface-teal-text: #0bd8b6;
313
+ --tone-violet-fill: 272 44.9% 50.2%;
314
+ --tone-violet-ring: 272 100% 80.8%;
315
+ --tone-violet-mark: #8e4ec6;
316
+ --tone-violet-line: #8e4ec6;
317
+ --tone-violet-ink: #d19dff;
318
+ --tone-orange-fill: 22 93.6% 36.9%;
319
+ --tone-orange-ring: 26 100% 67.1%;
320
+ --tone-orange-mark: #f76b15;
321
+ --tone-orange-line: #f76b15;
322
+ --tone-orange-ink: #ffa057;
323
+ --tone-teal-fill: 172 95% 23.7%;
324
+ --tone-teal-ring: 170 90.3% 44.5%;
325
+ --tone-teal-mark: #12a594;
326
+ --tone-teal-line: #12a594;
327
+ --tone-teal-ink: #0bd8b6;
328
+ --tone-blue-fill: 209 86.3% 40%;
329
+ --tone-blue-ring: 210 100% 72%;
330
+ --tone-blue-mark: #0090ff;
331
+ --tone-blue-line: #0090ff;
332
+ --tone-blue-ink: #70b8ff;
333
+ --tone-pink-fill: 322 65.1% 46.1%;
334
+ --tone-pink-ring: 327 100% 77.6%;
335
+ --tone-pink-mark: #d6409f;
336
+ --tone-pink-line: #d6409f;
337
+ --tone-pink-ink: #ff8dcc;
338
+ --tone-brown-fill: 24 29.6% 39%;
339
+ --tone-brown-ring: 28 49.7% 72%;
340
+ --tone-brown-mark: #ad7f58;
341
+ --tone-brown-line: #ad7f58;
342
+ --tone-brown-ink: #dbb594;
343
+ --tone-cyan-fill: 192 80.4% 30%;
344
+ --tone-cyan-ring: 190 75.5% 60%;
345
+ --tone-cyan-mark: #00a2c7;
346
+ --tone-cyan-line: #00a2c7;
347
+ --tone-cyan-ink: #4ccce6;
348
+ --tone-lime-fill: 84 44.2% 30.2%;
349
+ --tone-lime-ring: 80 69.9% 66.1%;
350
+ --tone-lime-mark: #bdee63;
351
+ --tone-lime-line: #bdee63;
352
+ --tone-lime-ink: #bde56c;
313
353
  /* tones:end */
314
354
  /* Run-mix ramp — the fills of a proportional status bar.
315
355
  *
@@ -715,6 +755,46 @@
715
755
  --surface-teal-bg: #e0f8f3;
716
756
  --surface-teal-border: #a1ded2;
717
757
  --surface-teal-text: #017d6d;
758
+ --tone-violet-fill: 272 44.9% 50.2%;
759
+ --tone-violet-ring: 272 44.9% 50.2%;
760
+ --tone-violet-mark: #8e4ec6;
761
+ --tone-violet-line: #8e4ec6;
762
+ --tone-violet-ink: #753ea6;
763
+ --tone-orange-fill: 22 93.6% 36.9%;
764
+ --tone-orange-ring: 22 93.6% 36.9%;
765
+ --tone-orange-mark: #f76b15;
766
+ --tone-orange-line: #cc4e00;
767
+ --tone-orange-ink: #953d0e;
768
+ --tone-teal-fill: 172 95% 23.7%;
769
+ --tone-teal-ring: 172 95% 23.7%;
770
+ --tone-teal-mark: #12a594;
771
+ --tone-teal-line: #008573;
772
+ --tone-teal-ink: #066558;
773
+ --tone-blue-fill: 209 86.3% 40%;
774
+ --tone-blue-ring: 209 86.3% 40%;
775
+ --tone-blue-mark: #0090ff;
776
+ --tone-blue-line: #0588f0;
777
+ --tone-blue-ink: #0f59a3;
778
+ --tone-pink-fill: 322 65.1% 46.1%;
779
+ --tone-pink-ring: 322 65.1% 46.1%;
780
+ --tone-pink-mark: #d6409f;
781
+ --tone-pink-line: #d6409f;
782
+ --tone-pink-ink: #a02071;
783
+ --tone-brown-fill: 24 29.6% 39%;
784
+ --tone-brown-ring: 24 29.6% 39%;
785
+ --tone-brown-mark: #ad7f58;
786
+ --tone-brown-line: #ad7f58;
787
+ --tone-brown-ink: #725440;
788
+ --tone-cyan-fill: 192 80.4% 30%;
789
+ --tone-cyan-ring: 192 80.4% 30%;
790
+ --tone-cyan-mark: #00a2c7;
791
+ --tone-cyan-line: #107d98;
792
+ --tone-cyan-ink: #0f6277;
793
+ --tone-lime-fill: 84 44.2% 30.2%;
794
+ --tone-lime-ring: 84 44.2% 30.2%;
795
+ --tone-lime-mark: #bdee63;
796
+ --tone-lime-line: #5c7c2f;
797
+ --tone-lime-ink: #4b6126;
718
798
  /* tones:end */
719
799
  /* Run-mix ramp — the fills of a proportional status bar.
720
800
  *
@@ -646,3 +646,39 @@ describe("shadow, radius and loop period are one role each", () => {
646
646
  expect(breathe, "a loop never moves or scales").not.toMatch(/transform|translate|scale|rotate/);
647
647
  });
648
648
  });
649
+
650
+ describe("data-accent scopes retint the accent and nothing else", () => {
651
+ // Palette plan item 6: a product sets data-accent="<category>" on <html> or a
652
+ // theme boundary. Each hue's roles are generated (gen-ladders.mjs) into the
653
+ // tone regions, which the ratchet above already counts as generated tones.
654
+ const themes = readFileSync(path.resolve(import.meta.dirname, "named-themes.css"), "utf8");
655
+ const region = themes.match(/\/\* accent-scopes:begin[\s\S]*?accent-scopes:end \*\//)?.[0] ?? "";
656
+ const hues = [...region.matchAll(/^\[data-accent="([a-z]+)"\],$/gm)].map((m) => m[1]);
657
+
658
+ it("scopes every categorical hue", () => {
659
+ expect(hues).toEqual(["violet", "orange", "teal", "blue", "pink", "brown", "cyan", "lime"]);
660
+ });
661
+
662
+ for (const [theme, spine] of [
663
+ ["dark", DARK],
664
+ ["light", LIGHT],
665
+ ] as const) {
666
+ it.each(hues)(`${theme} %s: the accent ink clears 4.5:1 on the page and the card`, (hue) => {
667
+ for (const plane of ["md3-surface", "md3-surface-container"]) {
668
+ expect(contrastRatio(hexIn(spine, `tone-${hue}-ink`), hexIn(spine, plane)), plane).toBeGreaterThanOrEqual(4.5);
669
+ }
670
+ });
671
+ }
672
+
673
+ it("maps the five roles to the hue and re-declares no plane, body ink or status", () => {
674
+ for (const hue of hues) {
675
+ const rule = blockIn(region, `[data-accent="${hue}"] :is(.light, .dark, [data-theme], [data-sandbox-ui], [data-sandbox-theme])`);
676
+ expect([...rule.matchAll(/--([a-z-]+): var\(--tone-([a-z]+)-([a-z]+)\);/g)].map((m) => `${m[1]}=${m[2]}-${m[3]}`)).toEqual([
677
+ `hsl-primary=${hue}-fill`, `hsl-ring=${hue}-ring`, `brand-primary=${hue}-mark`, `brand-cool=${hue}-line`, `accent-text=${hue}-ink`,
678
+ ]);
679
+ }
680
+ const declared = [...region.matchAll(/--([a-z0-9-]+):/g)].map((m) => m[1]);
681
+ const untouchable = /^(hsl-(background|card|popover|secondary|muted|accent|border|input|foreground|muted-foreground|success|warning|destructive|info)|text-|md3-(surface($|-(container|dim|bright|variant))|on-surface|outline)|depth-|bg-(root|card|elevated|dark|section|input)|surface-|status-(running|stopped|warm|cold|error|deleted))/;
682
+ expect(declared.filter((t) => untouchable.test(t))).toEqual([]);
683
+ });
684
+ });