@tangle-network/brand 1.30.0 → 1.31.1

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.
@@ -589,25 +589,46 @@ describe("categorical tones are a complete, legible family", () => {
589
589
  });
590
590
  });
591
591
 
592
- describe("each colour is declared once: a role, then aliases", () => {
593
- // The spine is authored as roles (planes, ink tiers, four indigos, one ink
594
- // per status hue) that every other name of the same colour aliases. A new
595
- // hex literal is a second declaration of a colour that already has a name,
596
- // so the count can only fall. The generated tone region is excluded: its
597
- // values are the generator's, solved from the planes declared here.
598
- const literalsOutsideTones = (spine: string) => {
599
- const set = new Set<string>();
600
- for (const [, name, value] of spine.matchAll(/--([a-z0-9-]+)\s*:\s*([^;]+);/g)) {
601
- if (/^(tone-|surface-(violet|orange|teal)-)/.test(name)) continue;
602
- for (const hex of value.match(/#[0-9a-fA-F]{6}\b/g) ?? []) set.add(hex.toLowerCase());
592
+ describe("syntax keywords use a text-safe category role", () => {
593
+ it.each([["dark", DARK], ["light", LIGHT]] as const)("%s keywords clear AA on every code plane", (_, spine) => {
594
+ const ink = hexIn(spine, "syntax-keyword");
595
+ for (const plane of ["hsl-background", "hsl-card", "hsl-popover"]) {
596
+ expect(contrastRatio(ink, hexIn(spine, plane)), `keyword on ${plane}`).toBeGreaterThanOrEqual(4.5);
603
597
  }
604
- return set;
605
- };
598
+ });
599
+ });
600
+
601
+ describe("text-bearing accent gradients keep AA at every stop", () => {
602
+ const named = readFileSync(path.resolve(import.meta.dirname, "named-themes.css"), "utf8");
603
+ const namedRules = [...named.replace(/\/\*[\s\S]*?\*\//g, "").matchAll(/([^{}]*)\{([^{}]*--hsl-background:[^{}]*)\}/g)];
604
+ const cases = [["dark", [DARK]], ["light", [LIGHT, DARK]], ...namedRules.map(([, selector, body]) => [selector.trim(), [body, /-light|not\(\.dark\)/.test(selector) ? LIGHT : DARK, DARK]])] as const;
605
+ for (const [name, chain] of cases) {
606
+ it(String(name), () => {
607
+ const sources = [...chain];
608
+ const gradient = resolveIn(sources, "accent-gradient-strong");
609
+ const stops = [...gradient.matchAll(/var\(--([a-z0-9-]+)\)/g)].map((match) => match[1]);
610
+ expect(stops.length).toBeGreaterThanOrEqual(2);
611
+ const label = hexIn(sources, "btn-primary-text");
612
+ for (const stop of stops) expect(contrastRatio(hexIn(sources, stop), label), stop).toBeGreaterThanOrEqual(4.5);
613
+ });
614
+ }
615
+ });
616
+
617
+ describe("the generated palette owns every colour", () => {
606
618
  it.each([
607
- ["dark", DARK, 28],
608
- ["light", LIGHT, 25],
609
- ] as const)("%s spine declares no new hex literal", (_, spine, ceiling) => {
610
- expect(literalsOutsideTones(spine).size).toBeLessThanOrEqual(ceiling);
619
+ ["dark", DARK],
620
+ ["light", LIGHT],
621
+ ] as const)("%s spine declares at most 30 distinct hex literals, including tones", (_, spine) => {
622
+ const literals = new Set((spine.match(/#[0-9a-fA-F]{6}\b/g) ?? []).map((hex) => hex.toLowerCase()));
623
+ expect(literals.size).toBeLessThanOrEqual(30);
624
+ });
625
+
626
+ it("has no authored hex outside generated blocks", () => {
627
+ const named = readFileSync(path.resolve(import.meta.dirname, "named-themes.css"), "utf8");
628
+ for (const css of [tokens, named]) {
629
+ expect(css).toContain("/* palette:begin");
630
+ expect(css.replace(/\/\* palette:begin[\s\S]*?\/\* palette:end \*\//g, "")).not.toMatch(/#[0-9a-fA-F]{3,8}\b/);
631
+ }
611
632
  });
612
633
  });
613
634
 
@@ -646,3 +667,39 @@ describe("shadow, radius and loop period are one role each", () => {
646
667
  expect(breathe, "a loop never moves or scales").not.toMatch(/transform|translate|scale|rotate/);
647
668
  });
648
669
  });
670
+
671
+ describe("data-accent scopes retint the accent and nothing else", () => {
672
+ // Palette plan item 6: a product sets data-accent="<category>" on <html> or a
673
+ // theme boundary. Each hue's roles are generated (gen-ladders.mjs) into the
674
+ // tone regions, which the ratchet above already counts as generated tones.
675
+ const themes = readFileSync(path.resolve(import.meta.dirname, "named-themes.css"), "utf8");
676
+ const region = themes.match(/\/\* accent-scopes:begin[\s\S]*?accent-scopes:end \*\//)?.[0] ?? "";
677
+ const hues = [...region.matchAll(/^\[data-accent="([a-z]+)"\],$/gm)].map((m) => m[1]);
678
+
679
+ it("scopes every categorical hue", () => {
680
+ expect(hues).toEqual(["violet", "orange", "teal", "blue", "pink", "brown", "cyan", "lime"]);
681
+ });
682
+
683
+ for (const [theme, spine] of [
684
+ ["dark", DARK],
685
+ ["light", LIGHT],
686
+ ] as const) {
687
+ it.each(hues)(`${theme} %s: the accent ink clears 4.5:1 on the page and the card`, (hue) => {
688
+ for (const plane of ["md3-surface", "md3-surface-container"]) {
689
+ expect(contrastRatio(hexIn(spine, `tone-${hue}-ink`), hexIn(spine, plane)), plane).toBeGreaterThanOrEqual(4.5);
690
+ }
691
+ });
692
+ }
693
+
694
+ it("maps the five roles to the hue and re-declares no plane, body ink or status", () => {
695
+ for (const hue of hues) {
696
+ const rule = blockIn(region, `[data-accent="${hue}"] :is(.light, .dark, [data-theme], [data-sandbox-ui], [data-sandbox-theme])`);
697
+ expect([...rule.matchAll(/--([a-z-]+): var\(--tone-([a-z]+)-([a-z]+)\);/g)].map((m) => `${m[1]}=${m[2]}-${m[3]}`)).toEqual([
698
+ `hsl-primary=${hue}-fill`, `hsl-ring=${hue}-ring`, `brand-primary=${hue}-mark`, `brand-cool=${hue}-line`, `accent-text=${hue}-ink`,
699
+ ]);
700
+ }
701
+ const declared = [...region.matchAll(/--([a-z0-9-]+):/g)].map((m) => m[1]);
702
+ 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))/;
703
+ expect(declared.filter((t) => untouchable.test(t))).toEqual([]);
704
+ });
705
+ });