@tangle-network/brand 1.27.1 → 1.29.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,68 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.29.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 4678ab2: The canonical spine (`:root`, `.dark`, `.light`, `data-theme` `dark`, `light`
8
+ and `tangle-dark`) has a real surface ladder. `--hsl-raised` is now the spine's
9
+ L3 plane and `--hsl-raised-2` its L4 plane, instead of both repeating the card,
10
+ and every adjacent plane and well is at least 4.4 OKLCH L apart (gated in
11
+ `surface-ladder.test.ts`):
12
+
13
+ - Dark: page `#0a0a14`, card `#191826`, raised `#252237`, raised-2 and popover
14
+ `#312e47`. A field in a raised card takes the card colour instead of the page's
15
+ near-black well, and dark cards and menus carry the 1px top highlight
16
+ (`--edge-highlight`).
17
+ - Light: page `#eceef3`, card white, raised `#edeff4`, raised-2 white. A field in
18
+ a raised card is white.
19
+
20
+ `--hsl-secondary` and `--hsl-popover` moved with the ladder (dark `#221f33` to
21
+ `#252237`, `#2c2942` to `#312e47`; light `#f2f3f8` to `#edeff4`). Named themes
22
+ other than Hospitality keep their flat ladder.
23
+
24
+ One hairline: `--border-subtle` and `--border-default` now alias `--hsl-border`
25
+ in the spine, so every divider, card edge and table rule renders one colour. The
26
+ dark hairline is `#37364a` (1.5:1 on the card, as documented; it measured 1.2:1).
27
+ `--glass-border-hover` now follows `--border-hover`.
28
+
29
+ The `--font-size-xs`, `-sm`, `-base` and `-lg` tokens now equal Tailwind's
30
+ `text-xs`, `text-sm`, `text-base` and `text-lg` (12, 14, 16 and 18px; they were
31
+ 11, 12, 15 and 16px), so `sm` means 14px in CSS and in a class. `text-eyebrow`,
32
+ the `Heading` eyebrow and subsection variants and `.tangle-api-meta` keep their
33
+ rendered sizes on the new steps. Code that relied on the old values reads
34
+ `--font-size-xs` for 12px captions and `--font-size-base` for 16px emphasis.
35
+
36
+ ## 1.28.0
37
+
38
+ ### Minor Changes
39
+
40
+ - 069f422: Each colour in the canonical spine is declared once, as a role, and every other
41
+ name of that colour aliases it: planes derive from the `--hsl-*` channels, the
42
+ four indigos (`--hsl-primary` fill, `--brand-primary` mark, `--brand-cool` line,
43
+ `--accent-text` ink) carry every `brand-*`, `md3-primary*` and gradient name, and
44
+ one ink per status hue carries its `surface-*`, `status-*`, `md3-*`, `code-*`,
45
+ `syntax-*` and shadcn channel. Every public token name is kept.
46
+
47
+ Values that moved, all toward the hand-tuned inks: dark `--status-*`,
48
+ `--hsl-success`, `--hsl-warning`, `--hsl-info` and `--hsl-destructive` are the
49
+ pastel `--surface-*-text` inks instead of saturated fills (a status fill now
50
+ carries dark text, as the destructive one did); light `--hsl-warning` is the
51
+ `--surface-warning-text` orange instead of the 1.6:1 yellow; `--brand-glow` is
52
+ the accent ink (`#A5B4FC` dark, the bright indigo in light, so prose links read
53
+ on paper); `--brand-vibrant` is the brand mark; syntax and `--code-*` colours are
54
+ the status and tone inks, with keywords on the categorical violet instead of the
55
+ accent; `--hsl-primary` and `--btn-primary-bg` agree at `#4c3fc6`; the sidebar
56
+ hover and border are the spine's L3 plane and hairline; one-unit plane drifts
57
+ (`#12111f`, `#f6f7fa`, `#f1f2f7`, `#c7c6d6`) collapse onto their channels.
58
+
59
+ Tailwind's `bg-brand`, `text-brand-glow` and `bg-status-*` utilities now follow
60
+ the mode and the named theme instead of one static value. `tangle-dark` declares
61
+ nothing: `tokens.css` seeds the complete dark spine on its selector. A shared
62
+ `scripts/css-resolve.mjs` reads aliases for the generators and the style tests,
63
+ and `tokens.test.ts` holds a ratchet on hex literals per spine (28 dark, 25 light,
64
+ outside the generated tones).
65
+
3
66
  ## 1.27.1
4
67
 
5
68
  ### Patch Changes
package/dist/index.d.ts CHANGED
@@ -1,6 +1,5 @@
1
1
  import { knot, knotGradientIds, knotSvg } from "./knot.js";
2
2
  import * as React from "react";
3
- //#region src/logo.d.ts
4
3
  interface LogoProps {
5
4
  size?: "sm" | "md" | "lg" | "xl";
6
5
  variant?: "full" | "icon";
@@ -15,8 +14,6 @@ interface TangleKnotProps {
15
14
  }
16
15
  export declare function TangleKnot({ size, className, label }: TangleKnotProps): React.JSX.Element;
17
16
  export declare function Logo({ size, variant, suffix, className }: LogoProps): React.JSX.Element;
18
- //#endregion
19
- //#region src/palette.generated.d.ts
20
17
  /** A theme's resolved roles: hex strings, or rgba() where the token is translucent. */
21
18
  interface BrandPalette {
22
19
  canvas: string;
@@ -56,19 +53,19 @@ export declare const palettes: {
56
53
  readonly light: {
57
54
  readonly canvas: "#eceef3";
58
55
  readonly card: "#ffffff";
59
- readonly well: "#f6f7fa";
60
- readonly nested: "#f1f2f7";
56
+ readonly well: "#f5f6fa";
57
+ readonly nested: "#edeff4";
61
58
  readonly overlay: "#ffffff";
62
59
  readonly deepest: "#ffffff";
63
- readonly hairline: "#c7c6d6";
60
+ readonly hairline: "#c8c7d6";
64
61
  readonly ink: "#2d2d2d";
65
62
  readonly inkSecondary: "#3f3f3f";
66
63
  readonly inkMuted: "#4c4c4c";
67
- readonly accent: "#4c3fc5";
64
+ readonly accent: "#4c3fc6";
68
65
  readonly accentHover: "#5a50d6";
69
66
  readonly onAccent: "#ffffff";
70
67
  readonly accentText: "#3c32e9";
71
- readonly danger: "#b41d1d";
68
+ readonly danger: "#b91c1c";
72
69
  readonly successBg: "#ecfdf5";
73
70
  readonly successBorder: "#a7f3d0";
74
71
  readonly successText: "#047857";
@@ -83,25 +80,25 @@ export declare const palettes: {
83
80
  readonly infoText: "#0369a1";
84
81
  readonly brandVibrant: "#6366f1";
85
82
  readonly brandCool: "#818cf8";
86
- readonly brandGlow: "#a5aafc";
83
+ readonly brandGlow: "#3c32e9";
87
84
  readonly brandPurple: "#6366f1";
88
85
  };
89
86
  readonly dark: {
90
87
  readonly canvas: "#0a0a14";
91
88
  readonly card: "#191826";
92
- readonly well: "#12111f";
93
- readonly nested: "#221f33";
94
- readonly overlay: "#2c2942";
89
+ readonly well: "#11101e";
90
+ readonly nested: "#252237";
91
+ readonly overlay: "#312e47";
95
92
  readonly deepest: "#040408";
96
- readonly hairline: "#2a293d";
93
+ readonly hairline: "#37364a";
97
94
  readonly ink: "#e6e6e6";
98
95
  readonly inkSecondary: "#c7c7c7";
99
96
  readonly inkMuted: "#b5b5b5";
100
- readonly accent: "#4c3fc5";
97
+ readonly accent: "#4c3fc6";
101
98
  readonly accentHover: "#5a50d6";
102
99
  readonly onAccent: "#ffffff";
103
100
  readonly accentText: "#a5b4fc";
104
- readonly danger: "#f99090";
101
+ readonly danger: "#ffada9";
105
102
  readonly successBg: "#2f5146";
106
103
  readonly successBorder: "#3c7760";
107
104
  readonly successText: "#5ed7a4";
@@ -114,9 +111,9 @@ export declare const palettes: {
114
111
  readonly infoBg: "#2d4e67";
115
112
  readonly infoBorder: "#3d7292";
116
113
  readonly infoText: "#67cdff";
117
- readonly brandVibrant: "#6366f1";
114
+ readonly brandVibrant: "#7c6cf6";
118
115
  readonly brandCool: "#818cf8";
119
- readonly brandGlow: "#a5aafc";
116
+ readonly brandGlow: "#a5b4fc";
120
117
  readonly brandPurple: "#c7d2fe";
121
118
  };
122
119
  readonly websiteLight: {
@@ -130,11 +127,11 @@ export declare const palettes: {
130
127
  readonly ink: "#191c24";
131
128
  readonly inkSecondary: "#3a3a50";
132
129
  readonly inkMuted: "#5e5e7a";
133
- readonly accent: "#4c3fc5";
130
+ readonly accent: "#4c3fc6";
134
131
  readonly accentHover: "#5a50d6";
135
132
  readonly onAccent: "#ffffff";
136
133
  readonly accentText: "#3c32e9";
137
- readonly danger: "#b41d1d";
134
+ readonly danger: "#b91c1c";
138
135
  readonly successBg: "#ecfdf5";
139
136
  readonly successBorder: "#a7f3d0";
140
137
  readonly successText: "#047857";
@@ -149,7 +146,7 @@ export declare const palettes: {
149
146
  readonly infoText: "#0369a1";
150
147
  readonly brandVibrant: "#6366f1";
151
148
  readonly brandCool: "#818cf8";
152
- readonly brandGlow: "#a5aafc";
149
+ readonly brandGlow: "#3c32e9";
153
150
  readonly brandPurple: "#6366f1";
154
151
  };
155
152
  readonly websiteDark: {
@@ -163,11 +160,11 @@ export declare const palettes: {
163
160
  readonly ink: "#fdfdfd";
164
161
  readonly inkSecondary: "#aaa8b8";
165
162
  readonly inkMuted: "#9496bc";
166
- readonly accent: "#4c3fc5";
163
+ readonly accent: "#4c3fc6";
167
164
  readonly accentHover: "#5a50d6";
168
165
  readonly onAccent: "#ffffff";
169
166
  readonly accentText: "#a5b4fc";
170
- readonly danger: "#f99090";
167
+ readonly danger: "#ffada9";
171
168
  readonly successBg: "#2f5146";
172
169
  readonly successBorder: "#3c7760";
173
170
  readonly successText: "#5ed7a4";
@@ -180,11 +177,10 @@ export declare const palettes: {
180
177
  readonly infoBg: "#2d4e67";
181
178
  readonly infoBorder: "#3d7292";
182
179
  readonly infoText: "#67cdff";
183
- readonly brandVibrant: "#6366f1";
180
+ readonly brandVibrant: "#7c6cf6";
184
181
  readonly brandCool: "#818cf8";
185
- readonly brandGlow: "#a5aafc";
182
+ readonly brandGlow: "#a5b4fc";
186
183
  readonly brandPurple: "#c7d2fe";
187
184
  };
188
185
  };
189
- //#endregion
190
186
  export { type BrandPalette, type LogoProps, type TangleKnotProps, knot, knotGradientIds, knotSvg };
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { knot, knotGradientIds, knotSvg } from "./knot.js";
2
2
  import * as React from "react";
3
3
  import { jsx, jsxs } from "react/jsx-runtime";
4
- //#region src/logo.tsx
4
+ // src/logo.tsx
5
5
  const sizeConfig = {
6
6
  sm: {
7
7
  icon: 24,
@@ -88,26 +88,25 @@ function Logo({ size = "md", variant = "full", suffix = null, className }) {
88
88
  })]
89
89
  });
90
90
  }
91
- //#endregion
92
- //#region src/palette.generated.ts
91
+ // src/palette.generated.ts
93
92
  /** Resolved palettes for the canonical light and dark themes and the website named theme. */
94
93
  const palettes = {
95
94
  light: {
96
95
  canvas: "#eceef3",
97
96
  card: "#ffffff",
98
- well: "#f6f7fa",
99
- nested: "#f1f2f7",
97
+ well: "#f5f6fa",
98
+ nested: "#edeff4",
100
99
  overlay: "#ffffff",
101
100
  deepest: "#ffffff",
102
- hairline: "#c7c6d6",
101
+ hairline: "#c8c7d6",
103
102
  ink: "#2d2d2d",
104
103
  inkSecondary: "#3f3f3f",
105
104
  inkMuted: "#4c4c4c",
106
- accent: "#4c3fc5",
105
+ accent: "#4c3fc6",
107
106
  accentHover: "#5a50d6",
108
107
  onAccent: "#ffffff",
109
108
  accentText: "#3c32e9",
110
- danger: "#b41d1d",
109
+ danger: "#b91c1c",
111
110
  successBg: "#ecfdf5",
112
111
  successBorder: "#a7f3d0",
113
112
  successText: "#047857",
@@ -122,25 +121,25 @@ const palettes = {
122
121
  infoText: "#0369a1",
123
122
  brandVibrant: "#6366f1",
124
123
  brandCool: "#818cf8",
125
- brandGlow: "#a5aafc",
124
+ brandGlow: "#3c32e9",
126
125
  brandPurple: "#6366f1"
127
126
  },
128
127
  dark: {
129
128
  canvas: "#0a0a14",
130
129
  card: "#191826",
131
- well: "#12111f",
132
- nested: "#221f33",
133
- overlay: "#2c2942",
130
+ well: "#11101e",
131
+ nested: "#252237",
132
+ overlay: "#312e47",
134
133
  deepest: "#040408",
135
- hairline: "#2a293d",
134
+ hairline: "#37364a",
136
135
  ink: "#e6e6e6",
137
136
  inkSecondary: "#c7c7c7",
138
137
  inkMuted: "#b5b5b5",
139
- accent: "#4c3fc5",
138
+ accent: "#4c3fc6",
140
139
  accentHover: "#5a50d6",
141
140
  onAccent: "#ffffff",
142
141
  accentText: "#a5b4fc",
143
- danger: "#f99090",
142
+ danger: "#ffada9",
144
143
  successBg: "#2f5146",
145
144
  successBorder: "#3c7760",
146
145
  successText: "#5ed7a4",
@@ -153,9 +152,9 @@ const palettes = {
153
152
  infoBg: "#2d4e67",
154
153
  infoBorder: "#3d7292",
155
154
  infoText: "#67cdff",
156
- brandVibrant: "#6366f1",
155
+ brandVibrant: "#7c6cf6",
157
156
  brandCool: "#818cf8",
158
- brandGlow: "#a5aafc",
157
+ brandGlow: "#a5b4fc",
159
158
  brandPurple: "#c7d2fe"
160
159
  },
161
160
  websiteLight: {
@@ -169,11 +168,11 @@ const palettes = {
169
168
  ink: "#191c24",
170
169
  inkSecondary: "#3a3a50",
171
170
  inkMuted: "#5e5e7a",
172
- accent: "#4c3fc5",
171
+ accent: "#4c3fc6",
173
172
  accentHover: "#5a50d6",
174
173
  onAccent: "#ffffff",
175
174
  accentText: "#3c32e9",
176
- danger: "#b41d1d",
175
+ danger: "#b91c1c",
177
176
  successBg: "#ecfdf5",
178
177
  successBorder: "#a7f3d0",
179
178
  successText: "#047857",
@@ -188,7 +187,7 @@ const palettes = {
188
187
  infoText: "#0369a1",
189
188
  brandVibrant: "#6366f1",
190
189
  brandCool: "#818cf8",
191
- brandGlow: "#a5aafc",
190
+ brandGlow: "#3c32e9",
192
191
  brandPurple: "#6366f1"
193
192
  },
194
193
  websiteDark: {
@@ -202,11 +201,11 @@ const palettes = {
202
201
  ink: "#fdfdfd",
203
202
  inkSecondary: "#aaa8b8",
204
203
  inkMuted: "#9496bc",
205
- accent: "#4c3fc5",
204
+ accent: "#4c3fc6",
206
205
  accentHover: "#5a50d6",
207
206
  onAccent: "#ffffff",
208
207
  accentText: "#a5b4fc",
209
- danger: "#f99090",
208
+ danger: "#ffada9",
210
209
  successBg: "#2f5146",
211
210
  successBorder: "#3c7760",
212
211
  successText: "#5ed7a4",
@@ -219,11 +218,10 @@ const palettes = {
219
218
  infoBg: "#2d4e67",
220
219
  infoBorder: "#3d7292",
221
220
  infoText: "#67cdff",
222
- brandVibrant: "#6366f1",
221
+ brandVibrant: "#7c6cf6",
223
222
  brandCool: "#818cf8",
224
- brandGlow: "#a5aafc",
223
+ brandGlow: "#a5b4fc",
225
224
  brandPurple: "#c7d2fe"
226
225
  }
227
226
  };
228
- //#endregion
229
227
  export { Logo, TangleKnot, knot, knotGradientIds, knotSvg, palettes };
package/dist/knot.d.ts CHANGED
@@ -1,4 +1,3 @@
1
- //#region src/knot.d.ts
2
1
  /** Canonical geometry. This entry has no React or browser dependencies. */
3
2
  export declare const knot: {
4
3
  readonly viewBox: "20 45 156 140";
@@ -29,5 +28,4 @@ export declare function knotSvg({ id, size, label }: {
29
28
  id: string;
30
29
  size?: number;
31
30
  label?: string;
32
- }): string;
33
- //#endregion
31
+ }): string;
package/dist/knot.js CHANGED
@@ -1,4 +1,4 @@
1
- //#region src/knot.ts
1
+ // src/knot.ts
2
2
  /** Canonical geometry. This entry has no React or browser dependencies. */
3
3
  const knot = {
4
4
  viewBox: "20 45 156 140",
@@ -41,5 +41,4 @@ function knotSvg({ id, size = 32, label }) {
41
41
  const accessibility = label ? `role="img" aria-label="${escape(label)}"` : "aria-hidden=\"true\"";
42
42
  return `<svg xmlns="http://www.w3.org/2000/svg" width="${size}" height="${size}" viewBox="${knot.viewBox}" fill="none" focusable="false" ${accessibility}><defs>${knot.gradients.map((g, i) => `<linearGradient id="${ids[i]}" x1="${g.x1}" y1="${g.y1}" x2="${g.x2}" y2="${g.y2}" gradientUnits="userSpaceOnUse">${knot.stops.map((color, index) => `<stop offset="${index}" stop-color="${color}"/>`).join("")}</linearGradient>`).join("")}</defs>${knot.paths.map((path, i) => `<path d="${path.d}" opacity="${path.opacity}" fill="url(#${ids[i]})"/>`).join("")}</svg>`;
43
43
  }
44
- //#endregion
45
44
  export { knot, knotGradientIds, knotSvg };
package/dist/social.d.ts CHANGED
@@ -1,4 +1,3 @@
1
- //#region src/social.d.ts
2
1
  export interface SocialImage {
3
2
  url: string;
4
3
  type: "image/png" | "image/jpeg" | "image/webp";
@@ -36,5 +35,4 @@ export type SocialMetaTag = {
36
35
  export declare function socialMetadata(page: SocialPage): {
37
36
  canonicalUrl: string;
38
37
  tags: SocialMetaTag[];
39
- };
40
- //#endregion
38
+ };
package/dist/social.js CHANGED
@@ -1,4 +1,4 @@
1
- //#region src/social.ts
1
+ // src/social.ts
2
2
  function httpsUrl(value) {
3
3
  const url = new URL(value);
4
4
  if (url.protocol !== "https:" || url.username || url.password) throw new Error("Social metadata requires HTTPS URLs without credentials");
@@ -105,5 +105,4 @@ function socialMetadata(page) {
105
105
  ]
106
106
  };
107
107
  }
108
- //#endregion
109
108
  export { socialMetadata };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tangle-network/brand",
3
- "version": "1.27.1",
3
+ "version": "1.29.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",
@@ -73,10 +73,10 @@
73
73
  }
74
74
  },
75
75
  "devDependencies": {
76
- "@storybook/react": "^10.6.0",
76
+ "@storybook/react": "^10.6.3",
77
77
  "@types/react": "^19.3.0",
78
- "react": "^19.3.0",
79
- "tsdown": "^0.23.0",
78
+ "react": "^19.3.1",
79
+ "tsdown": "0.23.1",
80
80
  "typescript": "^7.0.2"
81
81
  },
82
82
  "keywords": [
@@ -42,19 +42,19 @@ export const palettes = {
42
42
  light: {
43
43
  canvas: "#eceef3",
44
44
  card: "#ffffff",
45
- well: "#f6f7fa",
46
- nested: "#f1f2f7",
45
+ well: "#f5f6fa",
46
+ nested: "#edeff4",
47
47
  overlay: "#ffffff",
48
48
  deepest: "#ffffff",
49
- hairline: "#c7c6d6",
49
+ hairline: "#c8c7d6",
50
50
  ink: "#2d2d2d",
51
51
  inkSecondary: "#3f3f3f",
52
52
  inkMuted: "#4c4c4c",
53
- accent: "#4c3fc5",
53
+ accent: "#4c3fc6",
54
54
  accentHover: "#5a50d6",
55
55
  onAccent: "#ffffff",
56
56
  accentText: "#3c32e9",
57
- danger: "#b41d1d",
57
+ danger: "#b91c1c",
58
58
  successBg: "#ecfdf5",
59
59
  successBorder: "#a7f3d0",
60
60
  successText: "#047857",
@@ -69,25 +69,25 @@ export const palettes = {
69
69
  infoText: "#0369a1",
70
70
  brandVibrant: "#6366f1",
71
71
  brandCool: "#818cf8",
72
- brandGlow: "#a5aafc",
72
+ brandGlow: "#3c32e9",
73
73
  brandPurple: "#6366f1",
74
74
  },
75
75
  dark: {
76
76
  canvas: "#0a0a14",
77
77
  card: "#191826",
78
- well: "#12111f",
79
- nested: "#221f33",
80
- overlay: "#2c2942",
78
+ well: "#11101e",
79
+ nested: "#252237",
80
+ overlay: "#312e47",
81
81
  deepest: "#040408",
82
- hairline: "#2a293d",
82
+ hairline: "#37364a",
83
83
  ink: "#e6e6e6",
84
84
  inkSecondary: "#c7c7c7",
85
85
  inkMuted: "#b5b5b5",
86
- accent: "#4c3fc5",
86
+ accent: "#4c3fc6",
87
87
  accentHover: "#5a50d6",
88
88
  onAccent: "#ffffff",
89
89
  accentText: "#a5b4fc",
90
- danger: "#f99090",
90
+ danger: "#ffada9",
91
91
  successBg: "#2f5146",
92
92
  successBorder: "#3c7760",
93
93
  successText: "#5ed7a4",
@@ -100,9 +100,9 @@ export const palettes = {
100
100
  infoBg: "#2d4e67",
101
101
  infoBorder: "#3d7292",
102
102
  infoText: "#67cdff",
103
- brandVibrant: "#6366f1",
103
+ brandVibrant: "#7c6cf6",
104
104
  brandCool: "#818cf8",
105
- brandGlow: "#a5aafc",
105
+ brandGlow: "#a5b4fc",
106
106
  brandPurple: "#c7d2fe",
107
107
  },
108
108
  websiteLight: {
@@ -116,11 +116,11 @@ export const palettes = {
116
116
  ink: "#191c24",
117
117
  inkSecondary: "#3a3a50",
118
118
  inkMuted: "#5e5e7a",
119
- accent: "#4c3fc5",
119
+ accent: "#4c3fc6",
120
120
  accentHover: "#5a50d6",
121
121
  onAccent: "#ffffff",
122
122
  accentText: "#3c32e9",
123
- danger: "#b41d1d",
123
+ danger: "#b91c1c",
124
124
  successBg: "#ecfdf5",
125
125
  successBorder: "#a7f3d0",
126
126
  successText: "#047857",
@@ -135,7 +135,7 @@ export const palettes = {
135
135
  infoText: "#0369a1",
136
136
  brandVibrant: "#6366f1",
137
137
  brandCool: "#818cf8",
138
- brandGlow: "#a5aafc",
138
+ brandGlow: "#3c32e9",
139
139
  brandPurple: "#6366f1",
140
140
  },
141
141
  websiteDark: {
@@ -149,11 +149,11 @@ export const palettes = {
149
149
  ink: "#fdfdfd",
150
150
  inkSecondary: "#aaa8b8",
151
151
  inkMuted: "#9496bc",
152
- accent: "#4c3fc5",
152
+ accent: "#4c3fc6",
153
153
  accentHover: "#5a50d6",
154
154
  onAccent: "#ffffff",
155
155
  accentText: "#a5b4fc",
156
- danger: "#f99090",
156
+ danger: "#ffada9",
157
157
  successBg: "#2f5146",
158
158
  successBorder: "#3c7760",
159
159
  successText: "#5ed7a4",
@@ -166,9 +166,9 @@ export const palettes = {
166
166
  infoBg: "#2d4e67",
167
167
  infoBorder: "#3d7292",
168
168
  infoText: "#67cdff",
169
- brandVibrant: "#6366f1",
169
+ brandVibrant: "#7c6cf6",
170
170
  brandCool: "#818cf8",
171
- brandGlow: "#a5aafc",
171
+ brandGlow: "#a5b4fc",
172
172
  brandPurple: "#c7d2fe",
173
173
  },
174
174
  } as const satisfies Record<string, BrandPalette>;
@@ -60,11 +60,12 @@ describe("hexRelativeLuminanceIn", () => {
60
60
  expect(card).toBeGreaterThan(canvas);
61
61
  });
62
62
 
63
- it("says a token is non-hex rather than claiming it is missing", () => {
64
- expect(() => hexRelativeLuminanceIn("--a: var(--b);", "a")).toThrow(
65
- /not a hex color/,
66
- );
67
- expect(() => hexRelativeLuminanceIn("--a: #fff;", "b")).toThrow(/missing/);
63
+ it("follows an alias to the colour it paints, and names an alias that leads nowhere", () => {
64
+ expect(hexRelativeLuminanceIn("--a: var(--b); --b: #fff;", "a")).toBe(1);
65
+ expect(hexRelativeLuminanceIn("--a: hsl(var(--b)); --b: 0 0% 100%;", "a")).toBe(1);
66
+ expect(() => hexRelativeLuminanceIn("--a: var(--b);", "a")).toThrow(/--b is not defined/);
67
+ expect(() => hexRelativeLuminanceIn("--a: rgba(0, 0, 0, 0.5);", "a")).toThrow(/not a hex color/);
68
+ expect(() => hexRelativeLuminanceIn("--a: #fff;", "b")).toThrow(/not defined/);
68
69
  });
69
70
  });
70
71
 
@@ -73,7 +74,9 @@ describe("hslIn", () => {
73
74
  expect(hslIn("--a: 240 33% 6%;", "a")).toEqual({ h: 240, s: 33, l: 6 });
74
75
  });
75
76
 
76
- it("throws when the slot is absent", () => {
77
- expect(() => hslIn("--a: 240 33% 6%;", "b")).toThrow(/missing HSL slot/);
77
+ it("follows an alias to its triple, and throws when the slot is absent or not a triple", () => {
78
+ expect(hslIn("--a: var(--b); --b: 240 33% 6%;", "a")).toEqual({ h: 240, s: 33, l: 6 });
79
+ expect(() => hslIn("--a: 240 33% 6%;", "b")).toThrow(/not defined/);
80
+ expect(() => hslIn("--a: #fff;", "a")).toThrow(/not an HSL triple/);
78
81
  });
79
82
  });
@@ -6,6 +6,32 @@
6
6
  * importing file's context.
7
7
  */
8
8
 
9
+ import { readFileSync } from "node:fs";
10
+ import path from "node:path";
11
+ import { block as resolverBlock, decls, resolveValue } from "../../scripts/css-resolve.mjs";
12
+
13
+ /** The canonical dark spine's declarations: what every other block inherits. */
14
+ const BASELINE: Record<string, string> = decls(
15
+ resolverBlock(
16
+ readFileSync(path.resolve(import.meta.dirname, "tokens.css"), "utf8"),
17
+ ".dark",
18
+ ),
19
+ );
20
+
21
+ /**
22
+ * A token's concrete value in a block, with aliases followed: `var(--x)`
23
+ * chains and `hsl(var(--x))` wrappers, each hop reading the block first and
24
+ * the canonical dark baseline second, which is how the browser resolves a
25
+ * custom property declared on the same element as the light override.
26
+ */
27
+ export function resolveIn(css: string | string[], token: string): string {
28
+ // A chain of blocks models the cascade on one element: the first block that
29
+ // declares a name wins, as a named theme's own rule beats the spine it sits on.
30
+ const sources = Array.isArray(css) ? css : [css];
31
+ const scope = Object.assign({}, ...sources.map(decls).reverse());
32
+ return resolveValue(scope, token, BASELINE);
33
+ }
34
+
9
35
  /** Escape every regex metacharacter, so a selector is matched literally. */
10
36
  function escapeRegex(literal: string): string {
11
37
  return literal.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
@@ -78,15 +104,12 @@ export function declIn(css: string, token: string): string {
78
104
 
79
105
  /** Read an `H S% L%` channel triple. */
80
106
  export function hslIn(
81
- css: string,
107
+ css: string | string[],
82
108
  token: string,
83
109
  ): { h: number; s: number; l: number } {
84
- const m = css.match(
85
- new RegExp(
86
- `--${token}:\\s*(\\d+(?:\\.\\d+)?)\\s+(\\d+(?:\\.\\d+)?)%\\s+(\\d+(?:\\.\\d+)?)%`,
87
- ),
88
- );
89
- if (!m) throw new Error(`missing HSL slot --${token}`);
110
+ const value = resolveIn(css, token);
111
+ const m = /^(\d+(?:\.\d+)?)\s+(\d+(?:\.\d+)?)%\s+(\d+(?:\.\d+)?)%$/.exec(value);
112
+ if (!m) throw new Error(`--${token} is not an HSL triple (found "${value}")`);
90
113
  return { h: Number(m[1]), s: Number(m[2]), l: Number(m[3]) };
91
114
  }
92
115
 
@@ -99,11 +122,13 @@ type Rgb = [number, number, number];
99
122
  * separate question, answered by `compositeOver`). A token that exists but is not
100
123
  * hex reports exactly that, rather than claiming it is missing.
101
124
  */
102
- export function hexIn(css: string, token: string): Rgb {
103
- const declaration = css.match(new RegExp(`--${token}:\\s*([^;]+)`));
104
- if (!declaration) throw new Error(`missing surface --${token}`);
105
-
106
- const value = declaration[1].trim();
125
+ export function hexIn(css: string | string[], token: string): Rgb {
126
+ const value = resolveIn(css, token);
127
+ // A plane authored as channels resolves to a triple; read it as the colour it paints.
128
+ const triple = /^(\d+(?:\.\d+)?)\s+(\d+(?:\.\d+)?)%\s+(\d+(?:\.\d+)?)%$/.exec(value);
129
+ if (triple) {
130
+ return hslToRgb({ h: Number(triple[1]), s: Number(triple[2]), l: Number(triple[3]) }).map(Math.round) as Rgb;
131
+ }
107
132
  const hex = /^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})\b/.exec(value);
108
133
  if (!hex) {
109
134
  throw new Error(`--${token} is not a hex color (found "${value}")`);
@@ -181,7 +206,7 @@ export function contrastRatio(a: Rgb, b: Rgb): number {
181
206
  * below" assertion made on HSL L would be measuring the wrong thing. Hex parsing
182
207
  * (and its error reporting) is `hexIn`'s.
183
208
  */
184
- export function hexRelativeLuminanceIn(css: string, token: string): number {
209
+ export function hexRelativeLuminanceIn(css: string | string[], token: string): number {
185
210
  return relativeLuminance(hexIn(css, token));
186
211
  }
187
212