@tangle-network/brand 1.27.0 → 1.28.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.
@@ -28,6 +28,15 @@
28
28
  --spacing-control-md: var(--control-height);
29
29
  --spacing-control-lg: var(--control-height-lg);
30
30
  --spacing-shell-header: var(--shell-header-height);
31
+ --color-brand: var(--brand-primary);
32
+ --color-brand-dim: var(--btn-primary-bg);
33
+ --color-brand-cool: var(--brand-cool);
34
+ --color-brand-glow: var(--brand-glow);
35
+ --color-brand-emerald: var(--brand-emerald);
36
+ --color-status-running: var(--status-running);
37
+ --color-status-creating: var(--status-creating);
38
+ --color-status-stopped: var(--status-stopped);
39
+ --color-status-error: var(--status-error);
31
40
  --color-scrim: var(--scrim);
32
41
  --color-scrim-strong: var(--scrim-strong);
33
42
  --color-on-media: var(--on-media);
@@ -230,31 +239,14 @@
230
239
  }
231
240
 
232
241
  /* ───────────────────────── Tangle — canonical dark spine ───────────────────
233
- * tangle-dark names the default :root/.dark spine explicitly. The named block
234
- * repeats its surfaces and ink so a dark island remains self-sufficient
235
- * inside a light-named wrapper. Indigo stays in the accent; status tokens inherit
236
- * the dark-tuned root palette.
242
+ * tangle-dark names the default :root/.dark spine explicitly, for a dark island
243
+ * inside a light-named wrapper or a picker's explicit choice. It is the
244
+ * canonical spine by construction, never a copy of it.
237
245
  */
238
246
  [data-theme="tangle-dark"] {
239
- --hsl-background: 240 33% 6%; --hsl-foreground: 0 0% 90.1961%;
240
- --hsl-card: 244 23% 12%; --hsl-card-foreground: 0 0% 90.1961%;
241
- --hsl-popover: 247 23% 21%; --hsl-popover-foreground: 0 0% 90.1961%;
242
- --hsl-primary: 246 54% 51%; --hsl-primary-foreground: 0 0% 100%;
243
- --hsl-secondary: 249 24% 16%; --hsl-secondary-foreground: 0 0% 90.1961%;
244
- --hsl-muted: 249 24% 16%; --hsl-muted-foreground: 240 5% 68%;
245
- --hsl-accent: 249 24% 16%; --hsl-accent-foreground: 0 0% 90.1961%;
246
- --hsl-destructive: 0 90% 77%; --hsl-destructive-foreground: 0 60% 10%;
247
- --hsl-border: 243 20% 20%; --hsl-input: 244 29% 9%;
248
- --hsl-ring: 233 100% 87%;
249
- --depth-1: #0a0a14; --depth-2: #191826; --depth-3: #221f33; --depth-4: #2c2942;
250
- --bg-root: #0a0a14; --bg-card: var(--depth-2); --bg-input: hsl(var(--hsl-input));
251
- --md3-surface-container-lowest: #040408; --md3-surface-container-low: #12111f;
252
- --md3-surface-container: #191826; --md3-surface-container-high: #221f33;
253
- --md3-surface-container-highest: #2c2942; --md3-outline-variant: #2a293d;
254
- --border-subtle: rgba(255, 255, 255, 0.10); --border-default: rgba(255, 255, 255, 0.16);
255
- --border-hover: rgba(255, 255, 255, 0.28); --accent-text: #A5B4FC;
256
- --brand-primary: #7C6CF6; --brand-glow: #A5AAFC;
257
- --text-primary: #e6e6e6; --text-secondary: #c7c7c7; --text-muted: #b5b5b5; --text-dim: #a5a5a5;
247
+ /* Nothing to declare: tokens.css seeds the complete dark spine on this
248
+ * selector, so a tangle-dark island carries every value itself and inherits
249
+ * nothing from a light wrapper. */
258
250
  }
259
251
  /* Its light mode. */
260
252
  [data-theme="tangle-light"] {
@@ -378,7 +370,7 @@
378
370
  --hsl-accent: 133 38.4% 88%; --hsl-accent-foreground: 143 25% 12%;
379
371
  --hsl-border: 107 8.7% 79.8%; --hsl-input: 0 0% 100%;
380
372
  --hsl-ring: 148 28% 43%;
381
- --hsl-destructive: 0 72% 41%;
373
+ --hsl-destructive: 0 73.7% 41.8%; /* the canonical light red, restated so the dark block below mirrors it */
382
374
  --hsl-raised: 110 18.7% 93.7%; --hsl-raised-2: 0 0% 100%;
383
375
  --sidebar-background: 108 21.7% 95.5%; --sidebar-foreground: 143 25% 12%;
384
376
  --sidebar-primary: 153 38% 25%; --sidebar-primary-foreground: 0 0% 100%;
@@ -11,6 +11,7 @@ import {
11
11
  hexRelativeLuminanceIn,
12
12
  hslIn,
13
13
  hslToRgb,
14
+ resolveIn,
14
15
  } from "./css-test-utils";
15
16
 
16
17
  /**
@@ -100,71 +101,15 @@ describe('named theme: [data-theme="tangle-dark"]', () => {
100
101
  );
101
102
  if (!light) throw new Error('missing theme block: [data-theme="tangle-light"]');
102
103
 
103
- it("IS the canonical dark spine, named — no drift from tokens.css", () => {
104
+ it("declares nothing: the attribute names the spine tokens.css seeds on it", () => {
104
105
  // tangle-dark exists so the attribute can NAME the default spine (a dark
105
- // island in a light-named wrapper, a picker's explicit selection). If its
106
- // values diverge from :root, "tangle dark" becomes two palettes.
107
- const hslTokens = [
108
- "hsl-background",
109
- "hsl-foreground",
110
- "hsl-card",
111
- "hsl-card-foreground",
112
- "hsl-popover",
113
- "hsl-popover-foreground",
114
- "hsl-primary",
115
- "hsl-primary-foreground",
116
- "hsl-secondary",
117
- "hsl-secondary-foreground",
118
- "hsl-muted",
119
- "hsl-muted-foreground",
120
- "hsl-accent",
121
- "hsl-accent-foreground",
122
- "hsl-destructive",
123
- "hsl-destructive-foreground",
124
- "hsl-border",
125
- "hsl-input",
126
- "hsl-ring",
127
- ];
128
- for (const token of hslTokens) {
129
- expect(hslIn(css, token), `--${token} must equal the :root dark spine`).toEqual(
130
- hslIn(CANON, token),
131
- );
132
- }
133
- const hexTokens = [
134
- "depth-1",
135
- "depth-2",
136
- "depth-3",
137
- "depth-4",
138
- "md3-surface-container-lowest",
139
- "md3-surface-container-low",
140
- "md3-surface-container",
141
- "md3-surface-container-high",
142
- "md3-surface-container-highest",
143
- "md3-outline-variant",
144
- "accent-text",
145
- "brand-primary",
146
- "brand-glow",
147
- "text-primary",
148
- "text-secondary",
149
- "text-muted",
150
- "text-dim",
151
- ];
152
- for (const token of hexTokens) {
153
- expect(hexIn(css, token), `--${token} must equal the :root dark spine`).toEqual(
154
- hexIn(CANON, token),
155
- );
156
- }
157
- // Aliased and rgba declarations don't fit hslIn/hexIn — compare them raw.
158
- for (const token of [
159
- "bg-root",
160
- "bg-card",
161
- "bg-input",
162
- "border-subtle",
163
- "border-default",
164
- "border-hover",
165
- ]) {
166
- expect(declIn(css, token), `--${token} must equal the :root dark spine`).toBe(
167
- declIn(CANON, token),
106
+ // island in a light-named wrapper, a picker's explicit selection). It is
107
+ // the canonical spine by construction: tokens.css declares every dark token
108
+ // on this selector, so a copy here could only drift from it.
109
+ expect(css.replace(/^[^{]*\{/, "").trim()).toBe("");
110
+ for (const token of ["hsl-background", "hsl-destructive", "accent-text", "text-dim"]) {
111
+ expect(resolveIn(css, token), `--${token} resolves to the :root dark spine`).toBe(
112
+ resolveIn(CANON, token),
168
113
  );
169
114
  }
170
115
  });
@@ -238,27 +183,14 @@ describe('named theme: [data-theme="tangle-dark"]', () => {
238
183
 
239
184
  it("is self-sufficient inside a light-named wrapper — the island it exists for", () => {
240
185
  // The feature's stated scenario: tangle-dark nested in a tangle-light
241
- // wrapper. The browser composes the two scopes, so a token the island omits
242
- // inherits the WRAPPER's light value, not :root's — the light group's
243
- // near-black secondary-foreground on the dark secondary measured ~1.5:1
244
- // before the block redeclared its foregrounds. Model that cascade: every
245
- // tangle-light rule (the shared light group AND the theme block), with the
246
- // island's declarations overriding.
247
- const wrapper = blocksIn(themes, '[data-theme="tangle-light"]').join("\n");
248
- const effective = (token: string) => {
249
- const read = (source: string) => {
250
- try {
251
- return hslIn(source, token);
252
- } catch {
253
- return null;
254
- }
255
- };
256
- const value = read(css) ?? read(wrapper);
257
- if (!value) {
258
- throw new Error(`--${token} resolves to nothing inside the island`);
259
- }
260
- return value;
261
- };
186
+ // wrapper. A token the island omitted would inherit the WRAPPER's light
187
+ // value (its near-black secondary-foreground on the dark secondary measured
188
+ // ~1.5:1 once), which is why tokens.css seeds the whole spine on the
189
+ // island's own selector.
190
+ // The island declares every dark token on itself (the baseline tokens.css
191
+ // seeds on its selector), so nothing falls through to the wrapper: the
192
+ // chain is the island's own block over the canonical dark spine.
193
+ const effective = (token: string) => hslIn(css, token);
262
194
  for (const [fgToken, bgToken] of [
263
195
  ["hsl-secondary-foreground", "hsl-secondary"],
264
196
  ["hsl-accent-foreground", "hsl-accent"],
@@ -304,14 +236,12 @@ describe("named themes: secondary text tiers are readable text", () => {
304
236
  ];
305
237
  for (const scope of scopes) {
306
238
  it(`[data-theme="${scope}"] holds AA for --text-muted and --text-dim`, () => {
307
- const css = blocksIn(themes, `[data-theme="${scope}"]`).find((b) =>
308
- /--text-dim:/.test(b),
309
- );
310
- expect(css, `${scope} declares its text ladder`).toBeDefined();
311
- const resolve = (token: string): ReturnType<typeof hexIn> => {
312
- const ref = css!.match(new RegExp(`--${token}:\\s*var\\(--([a-z0-9-]+)\\)`));
313
- return ref ? resolve(ref[1]) : hexIn(css!, token);
314
- };
239
+ // The theme block over the spine it inherits; tangle-dark declares nothing
240
+ // and resolves to the canonical dark spine.
241
+ const blocks = blocksIn(themes, `[data-theme="${scope}"]`);
242
+ const css = blocks.find((b) => /--hsl-background:/.test(b)) ?? blocks[0];
243
+ const chain = scope.endsWith("-light") ? [css, CANON_LIGHT] : [css];
244
+ const resolve = (token: string) => hexIn(chain, token);
315
245
  const planes = {
316
246
  canvas: resolve("bg-root"),
317
247
  card: resolve("bg-card"),
@@ -450,13 +380,16 @@ describe('named theme: [data-theme="hospitality"]', () => {
450
380
  // sage planes (surface-ladder.test.ts holds their hue and contrast), and the
451
381
  // dark destructive red is lightened to read on the lighter popover; it stays
452
382
  // red. The other status hues, info and every category stay canonical.
453
- for (const css of [light, dark]) {
383
+ for (const [css, spine] of [
384
+ [light, CANON_LIGHT],
385
+ [dark, CANON],
386
+ ] as const) {
454
387
  expect(css).not.toMatch(
455
388
  /--(hsl-)?(success|warning|info|error)\b|--destructive|--hsl-destructive-foreground|--surface-info-|--status-|--tone-|--run-mix-|--syntax-/,
456
389
  );
457
- expect(hslIn(css, "hsl-destructive").h, "destructive stays red").toBe(0);
390
+ expect(hslIn([css, spine], "hsl-destructive").h, "destructive stays red").toBe(0);
458
391
  }
459
- expect(declIn(light, "hsl-destructive")).toBe(declIn(CANON_LIGHT, "hsl-destructive"));
392
+ expect(hslIn(light, "hsl-destructive")).toEqual(hslIn(CANON_LIGHT, "hsl-destructive"));
460
393
  });
461
394
 
462
395
  it("keeps the canonical ladder roles: paper cards on a tinted canvas in light, rising planes in dark", () => {
@@ -680,8 +613,12 @@ describe("dark themes: destructive reads as text", () => {
680
613
  '[data-theme="agents"]',
681
614
  '.dark[data-theme="website"]',
682
615
  ].map((selector): [string, string] => {
683
- const css = blocksIn(themes, selector).find((b) => b.includes("--hsl-background"));
684
- if (!css) throw new Error(`missing dark theme block: ${selector}`);
616
+ // A selector can head two rules (tangle-light's shared status group and
617
+ // its theme block); the theme block is the one with a canvas. tangle-dark
618
+ // declares nothing and resolves to the spine it is seeded with.
619
+ const blocks = blocksIn(themes, selector);
620
+ const css = blocks.find((b) => b.includes("--hsl-background")) ?? (blocks.length === 1 ? blocks[0] : undefined);
621
+ if (css === undefined) throw new Error(`missing dark theme block: ${selector}`);
685
622
  return [selector, css];
686
623
  }),
687
624
  ];
@@ -305,6 +305,6 @@ describe("Inset workspace backdrop (canonical themes)", () => {
305
305
  expect(Math.abs(panel - backdrop)).toBeGreaterThanOrEqual(3);
306
306
  });
307
307
  it("keeps the dark floor tokens equal, as their comment says", () => {
308
- expect(declIn(DARK, "md3-surface-dim")).toBe(declIn(DARK, "md3-surface-container-lowest"));
308
+ expect(hexIn(DARK, "md3-surface-dim")).toEqual(hexIn(DARK, "md3-surface-container-lowest"));
309
309
  });
310
310
  });
@@ -18,8 +18,10 @@ const ladders = read("ladders.css");
18
18
 
19
19
  /** The spine block of a selector: the rule that declares the surface ladder. */
20
20
  function spine(source: string, selector: string): string {
21
- const block = blocksIn(source, selector).find((b) => b.includes("--hsl-background:"));
22
- if (!block) throw new Error(`missing spine block: ${selector}`);
21
+ const blocks = blocksIn(source, selector);
22
+ // A theme that declares nothing (tangle-dark) is the baseline its chain continues to.
23
+ const block = blocks.find((b) => b.includes("--hsl-background:")) ?? (blocks.length === 1 ? blocks[0] : undefined);
24
+ if (block === undefined) throw new Error(`missing spine block: ${selector}`);
23
25
  return block;
24
26
  }
25
27
 
@@ -36,16 +36,6 @@
36
36
  --radius-xl: 12px;
37
37
  --radius-full: 999px;
38
38
 
39
- --color-brand: #6366F1;
40
- --color-brand-dim: #4F46E5;
41
- --color-brand-cool: #818CF8;
42
- --color-brand-glow: #A5AAFC;
43
- --color-brand-emerald: #10B981;
44
- --color-status-running: #10B981;
45
- --color-status-creating: #9B93F0;
46
- --color-status-stopped: #FFB800;
47
- --color-status-error: #FF4D6D;
48
-
49
39
  --shadow-card: 0 1px 2px rgba(0, 0, 0, 0.3);
50
40
  --shadow-dropdown: 0 8px 30px rgba(0, 0, 0, 0.45);
51
41
  --shadow-glow: none;
@@ -60,6 +50,18 @@
60
50
  * Public HSL aliases remain HSL channel triples, never full CSS colors.
61
51
  */
62
52
  @theme inline {
53
+ /* Brand and status colours follow the runtime tokens, so bg-brand and
54
+ * text-status-running change with the mode and the named theme. */
55
+ --color-brand: var(--brand-primary);
56
+ --color-brand-dim: var(--btn-primary-bg);
57
+ --color-brand-cool: var(--brand-cool);
58
+ --color-brand-glow: var(--brand-glow);
59
+ --color-brand-emerald: var(--brand-emerald);
60
+ --color-status-running: var(--status-running);
61
+ --color-status-creating: var(--status-creating);
62
+ --color-status-stopped: var(--status-stopped);
63
+ --color-status-error: var(--status-error);
64
+
63
65
  --text-display: var(--font-size-display);
64
66
  --text-hero: var(--font-size-hero);
65
67
  --text-page: var(--font-size-3xl);