@cueplusplus/ui 0.18.2 → 0.19.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.
Files changed (136) hide show
  1. package/CHANGELOG.md +61 -0
  2. package/dist/brand/_motif-seed.js +20 -0
  3. package/dist/brand/gloss-object.d.ts +23 -0
  4. package/dist/brand/gloss-object.js +108 -0
  5. package/dist/brand/halftone.d.ts +23 -0
  6. package/dist/brand/halftone.js +54 -0
  7. package/dist/brand/index.d.ts +6 -1
  8. package/dist/brand/motif.d.ts +32 -0
  9. package/dist/brand/motif.js +71 -0
  10. package/dist/brand/particle-mark.d.ts +29 -0
  11. package/dist/brand/particle-mark.js +137 -0
  12. package/dist/brand/ribbon.d.ts +20 -0
  13. package/dist/brand/ribbon.js +54 -0
  14. package/dist/chrome/announcement-bar.d.ts +30 -0
  15. package/dist/chrome/announcement-bar.js +38 -0
  16. package/dist/chrome/app-bar.d.ts +4 -1
  17. package/dist/chrome/app-bar.js +7 -2
  18. package/dist/chrome/app-shell.js +8 -1
  19. package/dist/chrome/band.d.ts +3 -1
  20. package/dist/chrome/band.js +4 -3
  21. package/dist/chrome/hero.d.ts +61 -0
  22. package/dist/chrome/hero.js +108 -0
  23. package/dist/chrome/index.d.ts +6 -1
  24. package/dist/chrome/index.js +6 -1
  25. package/dist/chrome/keyword.d.ts +22 -0
  26. package/dist/chrome/keyword.js +28 -0
  27. package/dist/chrome/logo-strip.d.ts +23 -0
  28. package/dist/chrome/logo-strip.js +43 -0
  29. package/dist/chrome/page-shell.js +13 -1
  30. package/dist/chrome/stat-band.d.ts +32 -0
  31. package/dist/chrome/stat-band.js +39 -0
  32. package/dist/dmx/channel-matrix.js +5 -3
  33. package/dist/dmx/dmx-strip.js +5 -3
  34. package/dist/elements/animated-number.js +4 -1
  35. package/dist/elements/replay.d.ts +1 -1
  36. package/dist/elements/replay.js +5 -2
  37. package/dist/elements/scroll-anchor.js +4 -1
  38. package/dist/index.d.ts +13 -1
  39. package/dist/index.js +14 -2
  40. package/dist/instruments/index.js +1 -1
  41. package/dist/instruments/stat.js +1 -1
  42. package/dist/instruments/terminal-frame.js +5 -2
  43. package/dist/layout/grid.js +4 -2
  44. package/dist/layout/preview.js +4 -188
  45. package/dist/layout/sidebar.js +88 -52
  46. package/dist/overlays/_surface.d.ts +2 -2
  47. package/dist/primitives/button.js +22 -3
  48. package/dist/primitives/chip.js +62 -6
  49. package/dist/styles.css +26 -0
  50. package/dist/system/_scoped-pairing.js +18 -0
  51. package/dist/system/_stage-scope.js +199 -0
  52. package/dist/system/_style-check.js +1 -1
  53. package/dist/system/_style-chip.js +1 -1
  54. package/dist/system/_style-group-label.js +7 -3
  55. package/dist/system/_style-hero-title.js +41 -0
  56. package/dist/system/_style-kbd.js +1 -1
  57. package/dist/system/_style-label-ink.js +12 -5
  58. package/dist/system/_style-menu.js +1 -1
  59. package/dist/system/_style-page-title.js +1 -1
  60. package/dist/system/_style-segmented-control.js +1 -1
  61. package/dist/system/_style-skeleton.js +1 -1
  62. package/dist/system/_style-tab.js +1 -1
  63. package/dist/system/font-scope.d.ts +38 -0
  64. package/dist/system/font-scope.js +69 -0
  65. package/dist/system/index.d.ts +3 -1
  66. package/dist/system/index.js +3 -1
  67. package/dist/system/portal.js +7 -1
  68. package/dist/system/still.d.ts +32 -0
  69. package/dist/system/still.js +47 -0
  70. package/manifest/components/alert-dialog.json +1 -1
  71. package/manifest/components/announcement-bar.json +82 -0
  72. package/manifest/components/app-bar.json +3 -2
  73. package/manifest/components/app-shell.json +3 -0
  74. package/manifest/components/autocomplete.json +1 -0
  75. package/manifest/components/band.json +2 -1
  76. package/manifest/components/bento.json +12 -12
  77. package/manifest/components/button.json +1 -0
  78. package/manifest/components/channel-matrix.json +1 -0
  79. package/manifest/components/chart-tooltip-content.json +1 -0
  80. package/manifest/components/checkbox-group.json +1 -1
  81. package/manifest/components/checkbox.json +1 -1
  82. package/manifest/components/chip.json +1 -0
  83. package/manifest/components/combobox.json +1 -0
  84. package/manifest/components/command-palette.json +1 -0
  85. package/manifest/components/context-menu.json +1 -1
  86. package/manifest/components/density.json +6 -2
  87. package/manifest/components/dialog.json +1 -1
  88. package/manifest/components/dmx-bar.json +1 -1
  89. package/manifest/components/dmx-strip.json +1 -1
  90. package/manifest/components/drawer.json +1 -1
  91. package/manifest/components/dropdown-menu.json +1 -0
  92. package/manifest/components/font-scope.json +81 -0
  93. package/manifest/components/footer.json +0 -1
  94. package/manifest/components/gloss-object.json +81 -0
  95. package/manifest/components/grid.json +6 -6
  96. package/manifest/components/halftone.json +79 -0
  97. package/manifest/components/hero.json +204 -0
  98. package/manifest/components/hover-card.json +1 -1
  99. package/manifest/components/kbd.json +1 -0
  100. package/manifest/components/keyword.json +67 -0
  101. package/manifest/components/ledger.json +4 -4
  102. package/manifest/components/logo-strip.json +93 -0
  103. package/manifest/components/markdown-text.json +2 -2
  104. package/manifest/components/menubar.json +1 -1
  105. package/manifest/components/motif.json +90 -0
  106. package/manifest/components/multi-select.json +1 -0
  107. package/manifest/components/navigation-menu.json +1 -0
  108. package/manifest/components/page-shell.json +5 -0
  109. package/manifest/components/particle-mark.json +75 -0
  110. package/manifest/components/popover.json +1 -1
  111. package/manifest/components/preview.json +9 -4
  112. package/manifest/components/radio-group.json +0 -1
  113. package/manifest/components/radio.json +0 -1
  114. package/manifest/components/ribbon.json +65 -0
  115. package/manifest/components/scrollable-tabs-list.json +1 -0
  116. package/manifest/components/segmented-control.json +1 -0
  117. package/manifest/components/sheet.json +1 -1
  118. package/manifest/components/sidebar.json +1 -0
  119. package/manifest/components/skeleton.json +1 -0
  120. package/manifest/components/stack.json +4 -4
  121. package/manifest/components/stat-band.json +68 -0
  122. package/manifest/components/stat.json +1 -1
  123. package/manifest/components/still-provider.json +60 -0
  124. package/manifest/components/switch.json +0 -1
  125. package/manifest/components/table.json +1 -1
  126. package/manifest/components/tabs.json +1 -0
  127. package/manifest/components/tags-input.json +1 -0
  128. package/manifest/components/theme-configurator.json +1 -0
  129. package/manifest/components/theme-provider.json +9 -4
  130. package/manifest/components/toast.json +1 -0
  131. package/manifest/components/tooltip.json +1 -1
  132. package/manifest/fixtures.json +18 -0
  133. package/manifest/manifest.json +267 -55
  134. package/manifest/tokens.json +91 -3
  135. package/package.json +4 -4
  136. package/dist/system/_paint-ground-pattern.js +0 -30
package/CHANGELOG.md CHANGED
@@ -1,5 +1,66 @@
1
1
  # @cueplusplus/ui
2
2
 
3
+ ## 0.19.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 8cf9ebb: Five page blocks join `chrome` — `Hero`, `StatBand`, `LogoStrip`, `AnnouncementBar` and `Keyword` — and four motifs join `brand` behind one `Motif`: `Ribbon`, `Halftone`, `GlossObject` and `ParticleMark`. A style set chooses a hero's arrangement, a stat band's rule and numeral ink, a keyword's fill and which motif shows, through new optional tokens that fall back to today's rendering while unset; `AppBar.Nav`, `Sidebar` and `Stat` read the same set for their alignment, numbered rail and numeral ink. `StillProvider` and `useStill` hold a subtree's animation still, and `ParticleMark` draws nothing while hidden, off screen, in a background tab, held still or under reduced motion. `Hero surface="inverse"` draws a dark card on any page, and `axes.css` restates each set's and pack's inks on `[data-cue-rescope]` so the card is right before it hydrates. `Hero` takes a `level` (1 by default) for a hero that opens a section or sits in a preview under a page's own heading, `AnnouncementBar`'s link is the exported `AnnouncementBarAction` type, and `StillProvider`'s `children` are optional, as `ThemeProvider`'s are. The package manifest now lists a numeric union's members in ascending order — `Grid`'s `cols` reads 1 to 6 — rather than in the order the type checker happened to create them.
8
+ - 1f539dc: The five measured style sets — `graphite`, `tender`, `zine`, `kiln` and `porcelain` — now carry their sources' page structure, not only their voices, re-measured on 2026-10-03:
9
+
10
+ - **`graphite`:** column rules at the content's edges, and a one-column hero centred over a soft info-ink glow, with its title at its own 56px size. The nav's links sit beside the brand.
11
+ - **`tender`:** column rules, and an editorial hero: the words left, the ribbon right, and a muted second title line. The links sit beside the brand.
12
+ - **`zine`:** filled calls to action cut with a chevron, dashed frame lines round the content over a 24px dotted ground, and a two-column hero with the halftone right. The nav is centred, and a focused field's ring sits 1px outside its edge, as its buttons' already does.
13
+ - **`kiln`:** a centred one-column hero with its media below. The links sit beside the brand.
14
+ - **`porcelain`:** column rules, and a one-column hero aligned to the start.
15
+
16
+ A set that does not declare these names draws as before. None of the five sets authors a corner, because each source's measured corners are already its theme's density ladder, which keeps scaling them.
17
+
18
+ **`Hero`'s title now speaks in the display voice.** It reads the set's display rung (face, size, weight, tracking and leading), falling back name by name to the page title's voice, so a set with no display rung draws the hero it drew before. One new optional name, `style-hero-title-size`, sits in front of the size, for a source whose hero is larger than its section headings.
19
+
20
+ **Two shipped sets change on purpose.** `editorial` and `marketing` declare a display size and no page-title size, so their heroes now grow to their display rung:
21
+
22
+ | set | at scale 1 | at a 1.15 scale |
23
+ | ----------- | ---------- | --------------- |
24
+ | `editorial` | 22 → 52px | 25.3 → 59.8px |
25
+ | `marketing` | 22 → 64px | 25.3 → 73.6px |
26
+
27
+ - 27887a6: Style sets may now shape controls and surfaces, cut filled buttons into chevrons, and draw page and band grounds and frame or grid lines, each falling back to today's rendering while unset. Glows and the shadows some packs draw in the foreground ink now scale by a per-palette glow gain — 1 in light, 0.35 in dark — so packs no longer glow in dark mode; every theme regenerates with the gain. The chevron cut is two stylesheet classes, `cue-cut` and `cue-cut-secondary`, shipped in `styles.css`.
28
+ - aa3056c: `FontScope` sets a subtree in another type pairing and carries it to every dialog and menu opened inside it, without moving the root's pairing: a `ThemeProvider`'s `font` stays root-level. A portal opened under a scope that names a pairing, `FontScope` or a `Preview` stage, now asks for the family again, so its unclassed text follows the pairing rather than the family `<body>` computed. Every component that schedules its own frames, `ChannelMatrix`, `DmxStrip`, `AnimatedNumber`, `Replay`, `TerminalFrame` and `ScrollAnchor`, now takes its static path under `StillProvider still` as it does under reduced motion.
29
+ - b09a663: Four style sets — `ledger`, `command`, `monolith` and `prism` — and a thirteenth font pairing, `fraunces-mono`. Each set carries a whole page structure, not only a voice: its controls' and cards' shape (pills, 8px, squares, 24px glass), a page or band ground drawn from the theme's own inks, the hero's columns, alignment and art, the stat band's rule and numerals, the navigation's alignment, and which motif shows. `fraunces-mono` sets everything in JetBrains Mono and routes a set's display rung to Fraunces, a variable serif.
30
+
31
+ Three style names are new, each optional and falling back to what an unset page renders: `style-display-case` uppercases a page title or a hero's title; `style-primary-fill` paints a primary button's resting fill with a gradient of the theme's own inks, which gives way to the solid hover fill under the pointer and to the disabled fill when disabled; and a set whose `style-hero-title-ink-2` names the keyword fill now has `Hero`'s second title line painted with that gradient through `background-clip: text`, with the accent as the colour a browser that cannot clip — or forced colours — keeps. Before this, the keyword fill there was an invalid colour and the line fell back to the inherited ink.
32
+
33
+ `<Motif>` takes no room under a set that shows no motif: the build derives `style-motif-shown` from the four motif names, and the container's own display reads it. A stage that turns a motif on with an inline variable sets `--cue-style-motif-shown: block` beside it. `ParticleMark` fits its word to the canvas's width as well as its height, so a long word or a wide monospace face is no longer cut at the edges.
34
+
35
+ The four new sets rule their links in `currentColor`, as every measured set does, so an in-copy link keeps a visible underline in its own ink. `@cueplusplus/theme-base`'s manifest schema knows `fraunces-mono` as a base pairing: a theme may retune it under `fontPairings.overrides`, and may not add a pairing of that name.
36
+
37
+ - 439a832: Phase 5 measured the structural Looks on twelve new reference pages, on rendered pixels. Every fix below was made where the problem came from, not on a page.
38
+
39
+ **Status chips clear 4.5:1 in every theme.** A toned chip used to set its status ink as small text on its own 10% tint, which measured 4.2–4.4:1 in light under most themes and 3.6:1 under Kiln. The ink is now the tone mixed 30% toward `fg`. That raises it in both modes and keeps the hue the tone is read by.
40
+
41
+ **Labels fall back to `fg-muted`, not `fg-subtle`.** A label names what sits beside it, so it is held to 4.5:1 like body text. Ten readers used to fall back to `fg-subtle` when no style set gave a label ink: `Stat`'s label, a table head and group row, a `Sidebar` section label, a menu or command palette group label, the labels of `CodeBlock`, `TerminalFrame` and `Frames`, and `Footer`'s meta row. Under the default theme that ink is 2.4:1 in dark and 3.1:1 in light. They now read `fg-muted`, so they are brighter wherever no set declares `--cue-style-label-ink`. A set that declares it is unchanged, and `console` still renders exactly what no set renders.
42
+
43
+ **`LogoStrip` follows the set's label ink.** Its caption used to hard-code `fg-subtle`, and its names `fg-muted`.
44
+
45
+ **`Grid` with `minItemWidth` no longer overflows a narrower column.** The track's minimum is capped at the grid's own width, so a 22rem minimum on a 390px phone draws one full-width column. It used to draw one that ran 2px past its container. Wide layouts are unchanged.
46
+
47
+ **The structural sets declare inks their fallbacks failed with:**
48
+ - `zine`, `ledger`, `command` and `monolith` take `fg-muted` for labels, and `prism` takes `fg`.
49
+ - `prism`, `command` and `tender` mix the accent 40% toward `fg` for a selected row, a pressed segment and (prism, command) an eyebrow.
50
+ - `prism`'s icy band tints a quarter of its busy ink at its foot, where it used to tint half.
51
+
52
+ **`command`'s rhythm is measured.** Its source stacks its panels 10–13px apart, so its `band-y` is a fifth of `space-7`. The unmeasured clamp it replaces drew bands 64px apart.
53
+
54
+ ### Patch Changes
55
+
56
+ - Updated dependencies [8cf9ebb]
57
+ - Updated dependencies [1f539dc]
58
+ - Updated dependencies [27887a6]
59
+ - Updated dependencies [b09a663]
60
+ - Updated dependencies [439a832]
61
+ - @cueplusplus/tokens@0.19.0
62
+ - @cueplusplus/theme-base@1.2.0
63
+
3
64
  ## 0.18.2
4
65
 
5
66
  ### Patch Changes
@@ -0,0 +1,20 @@
1
+ //#region src/brand/_motif-seed.ts
2
+ /**
3
+ * A seeded random stream (mulberry32): the same seed draws the same picture on the
4
+ * server and in the browser, which is what keeps a motif's markup identical across
5
+ * hydration (spec 2026-10-02 §3.4).
6
+ */
7
+ function seeded(seed) {
8
+ let a = seed >>> 0;
9
+ return () => {
10
+ a = a + 1831565813 >>> 0;
11
+ let t = a;
12
+ t = Math.imul(t ^ t >>> 15, t | 1);
13
+ t ^= t + Math.imul(t ^ t >>> 7, t | 61);
14
+ return ((t ^ t >>> 14) >>> 0) / 4294967296;
15
+ };
16
+ }
17
+ /** A number rounded to two decimals, so path data stays short and stable. */
18
+ const r2 = (n) => Math.round(n * 100) / 100;
19
+ //#endregion
20
+ export { r2, seeded };
@@ -0,0 +1,23 @@
1
+ import * as React from "react";
2
+ //#region src/brand/gloss-object.d.ts
3
+ interface GlossObjectProps extends React.SVGProps<SVGSVGElement> {
4
+ /** The object: a ring, a knot of two rings, or a rounded mark. Defaults to `"torus"`. */
5
+ shape?: "torus" | "knot" | "mark";
6
+ /** Tilts the highlight. Defaults to `1`. */
7
+ seed?: number;
8
+ }
9
+ /**
10
+ * A glossy, iridescent object for a marketing hero — the chrome-ring and gloss
11
+ * lettermark look (spec 2026-10-02 §3.4). Drawn with layered radial and linear
12
+ * gradients of the theme's accent against the foreground's mix, plus a specular
13
+ * highlight in the accent's own ink (`--cue-accent-fg`), so it is the Look's own colour and never a copied render.
14
+ * Decorative: hidden from assistive technology.
15
+ *
16
+ * Static markup — no `"use client"`.
17
+ *
18
+ * @example
19
+ * <GlossObject shape="knot" className="h-48 w-48" />
20
+ */
21
+ declare function GlossObject({ shape, seed, ...svgProps }: GlossObjectProps): React.JSX.Element;
22
+ //#endregion
23
+ export { GlossObject, GlossObjectProps };
@@ -0,0 +1,108 @@
1
+ import * as React from "react";
2
+ import { jsx, jsxs } from "react/jsx-runtime";
3
+ //#region src/brand/gloss-object.tsx
4
+ /**
5
+ * A glossy, iridescent object for a marketing hero — the chrome-ring and gloss
6
+ * lettermark look (spec 2026-10-02 §3.4). Drawn with layered radial and linear
7
+ * gradients of the theme's accent against the foreground's mix, plus a specular
8
+ * highlight in the accent's own ink (`--cue-accent-fg`), so it is the Look's own colour and never a copied render.
9
+ * Decorative: hidden from assistive technology.
10
+ *
11
+ * Static markup — no `"use client"`.
12
+ *
13
+ * @example
14
+ * <GlossObject shape="knot" className="h-48 w-48" />
15
+ */
16
+ function GlossObject({ shape = "torus", seed = 1, ...svgProps }) {
17
+ const id = React.useId().replace(/:/gu, "");
18
+ const tilt = seed * 37 % 90 - 45;
19
+ const ring = (cx, cy, key) => /* @__PURE__ */ jsxs("g", { children: [/* @__PURE__ */ jsx("circle", {
20
+ cx,
21
+ cy,
22
+ r: "28",
23
+ fill: "none",
24
+ stroke: `url(#${id}-body)`,
25
+ strokeWidth: "16"
26
+ }), /* @__PURE__ */ jsx("circle", {
27
+ cx,
28
+ cy,
29
+ r: "28",
30
+ fill: "none",
31
+ stroke: `url(#${id}-sheen)`,
32
+ strokeWidth: "6",
33
+ strokeDasharray: "40 136",
34
+ transform: `rotate(${tilt} ${cx} ${cy})`
35
+ })] }, key);
36
+ return /* @__PURE__ */ jsxs("svg", {
37
+ viewBox: "0 0 100 100",
38
+ "aria-hidden": "true",
39
+ focusable: "false",
40
+ ...svgProps,
41
+ children: [
42
+ /* @__PURE__ */ jsxs("defs", { children: [/* @__PURE__ */ jsxs("linearGradient", {
43
+ id: `${id}-body`,
44
+ x1: "0",
45
+ y1: "0",
46
+ x2: "1",
47
+ y2: "1",
48
+ children: [
49
+ /* @__PURE__ */ jsx("stop", {
50
+ offset: "0%",
51
+ stopColor: "color-mix(in oklch, var(--cue-accent) 70%, var(--cue-accent-fg))"
52
+ }),
53
+ /* @__PURE__ */ jsx("stop", {
54
+ offset: "50%",
55
+ stopColor: "var(--cue-accent)"
56
+ }),
57
+ /* @__PURE__ */ jsx("stop", {
58
+ offset: "100%",
59
+ stopColor: "color-mix(in oklch, var(--cue-accent) 55%, var(--cue-fg))"
60
+ })
61
+ ]
62
+ }), /* @__PURE__ */ jsxs("linearGradient", {
63
+ id: `${id}-sheen`,
64
+ x1: "0",
65
+ y1: "0",
66
+ x2: "1",
67
+ y2: "0",
68
+ children: [
69
+ /* @__PURE__ */ jsx("stop", {
70
+ offset: "0%",
71
+ stopColor: "var(--cue-accent-fg)",
72
+ stopOpacity: "0"
73
+ }),
74
+ /* @__PURE__ */ jsx("stop", {
75
+ offset: "50%",
76
+ stopColor: "var(--cue-accent-fg)",
77
+ stopOpacity: "0.85"
78
+ }),
79
+ /* @__PURE__ */ jsx("stop", {
80
+ offset: "100%",
81
+ stopColor: "var(--cue-accent-fg)",
82
+ stopOpacity: "0"
83
+ })
84
+ ]
85
+ })] }),
86
+ shape === "knot" ? [ring(40, 50, "a"), ring(60, 50, "b")] : null,
87
+ shape === "torus" ? ring(50, 50, "a") : null,
88
+ shape === "mark" ? /* @__PURE__ */ jsxs("g", { children: [/* @__PURE__ */ jsx("rect", {
89
+ x: "18",
90
+ y: "18",
91
+ width: "64",
92
+ height: "64",
93
+ rx: "18",
94
+ fill: `url(#${id}-body)`
95
+ }), /* @__PURE__ */ jsx("rect", {
96
+ x: "24",
97
+ y: "22",
98
+ width: "40",
99
+ height: "10",
100
+ rx: "5",
101
+ fill: `url(#${id}-sheen)`,
102
+ transform: `rotate(${tilt / 4} 44 27)`
103
+ })] }) : null
104
+ ]
105
+ });
106
+ }
107
+ //#endregion
108
+ export { GlossObject };
@@ -0,0 +1,23 @@
1
+ import * as React from "react";
2
+ //#region src/brand/halftone.d.ts
3
+ interface HalftoneProps extends React.SVGProps<SVGSVGElement> {
4
+ /** The generated form the dots describe. Defaults to `"ridge"`. */
5
+ shape?: "wave" | "orb" | "ridge";
6
+ /** Which variant of the form. Defaults to `1`. */
7
+ seed?: number;
8
+ }
9
+ /**
10
+ * A halftone dot screen over a generated form — the engraved, printed look of a
11
+ * developer-tool landing page (spec 2026-10-02 §3.4). Every dot's size is the
12
+ * form's density at that cell, so the picture is drawn by the screen itself
13
+ * rather than filtered from an image, and nothing is fetched. Dots are the
14
+ * foreground ink. Decorative: hidden from assistive technology.
15
+ *
16
+ * Static markup — no `"use client"`.
17
+ *
18
+ * @example
19
+ * <Halftone shape="orb" className="h-72 w-72" />
20
+ */
21
+ declare function Halftone({ shape, seed, ...svgProps }: HalftoneProps): React.JSX.Element;
22
+ //#endregion
23
+ export { Halftone, HalftoneProps };
@@ -0,0 +1,54 @@
1
+ import { seeded } from "./_motif-seed.js";
2
+ import "react";
3
+ import { jsx } from "react/jsx-runtime";
4
+ //#region src/brand/halftone.tsx
5
+ /**
6
+ * A halftone dot screen over a generated form — the engraved, printed look of a
7
+ * developer-tool landing page (spec 2026-10-02 §3.4). Every dot's size is the
8
+ * form's density at that cell, so the picture is drawn by the screen itself
9
+ * rather than filtered from an image, and nothing is fetched. Dots are the
10
+ * foreground ink. Decorative: hidden from assistive technology.
11
+ *
12
+ * Static markup — no `"use client"`.
13
+ *
14
+ * @example
15
+ * <Halftone shape="orb" className="h-72 w-72" />
16
+ */
17
+ function Halftone({ shape = "ridge", seed = 1, ...svgProps }) {
18
+ const phase = seeded(seed)() * Math.PI * 2;
19
+ const density = (x, y) => {
20
+ if (shape === "orb") {
21
+ const d = Math.hypot(x - .5, y - .5) / .5;
22
+ return Math.max(0, 1 - d) * (.6 + .4 * (1 - y));
23
+ }
24
+ if (shape === "wave") return Math.max(0, Math.min(1, .5 + .45 * Math.sin(x * 9 + phase) * (1 - y) + (y - .5) * .6));
25
+ const ridge = .55 + .25 * Math.sin(x * 5 + phase) + .1 * Math.sin(x * 13 + phase * 2);
26
+ return y > ridge ? Math.min(1, (y - ridge) * 2.4) : 0;
27
+ };
28
+ const step = 4;
29
+ const sizes = [
30
+ [],
31
+ [],
32
+ [],
33
+ []
34
+ ];
35
+ for (let y = step / 2; y < 100; y += step) for (let x = step / 2; x < 100; x += step) {
36
+ const size = Math.ceil(density(x / 100, y / 100) * 4);
37
+ if (size >= 1) sizes[size - 1].push(`M${x} ${y}h0`);
38
+ }
39
+ return /* @__PURE__ */ jsx("svg", {
40
+ viewBox: "0 0 100 100",
41
+ "aria-hidden": "true",
42
+ focusable: "false",
43
+ ...svgProps,
44
+ children: sizes.map((dots, index) => dots.length === 0 ? null : /* @__PURE__ */ jsx("path", {
45
+ d: dots.join(""),
46
+ fill: "none",
47
+ stroke: "var(--cue-fg)",
48
+ strokeLinecap: "round",
49
+ strokeWidth: (index + 1) * step / 4
50
+ }, index))
51
+ });
52
+ }
53
+ //#endregion
54
+ export { Halftone };
@@ -2,4 +2,9 @@ import { BrandAssetProps, BrandSize, BrandTone } from "./_chassis.js";
2
2
  import { CueLogotype, CueLogotypeProps } from "./cue-logotype.js";
3
3
  import { CueMark, CueMarkProps } from "./cue-mark.js";
4
4
  import { Plussie, PlussieExpression, PlussieProps } from "./plussie.js";
5
- export { type BrandAssetProps, type BrandSize, type BrandTone, CueLogotype, type CueLogotypeProps, CueMark, type CueMarkProps, Plussie, type PlussieExpression, type PlussieProps };
5
+ import { GlossObject, GlossObjectProps } from "./gloss-object.js";
6
+ import { Halftone, HalftoneProps } from "./halftone.js";
7
+ import { Motif, MotifProps, MotifVariant } from "./motif.js";
8
+ import { ParticleMark, ParticleMarkProps } from "./particle-mark.js";
9
+ import { Ribbon, RibbonProps } from "./ribbon.js";
10
+ export { type BrandAssetProps, type BrandSize, type BrandTone, CueLogotype, type CueLogotypeProps, CueMark, type CueMarkProps, GlossObject, type GlossObjectProps, Halftone, type HalftoneProps, Motif, type MotifProps, type MotifVariant, ParticleMark, type ParticleMarkProps, Plussie, type PlussieExpression, type PlussieProps, Ribbon, type RibbonProps };
@@ -0,0 +1,32 @@
1
+ import * as React from "react";
2
+ //#region src/brand/motif.d.ts
3
+ type MotifVariant = "ribbon" | "halftone" | "gloss" | "particles";
4
+ interface MotifProps extends React.ComponentPropsWithoutRef<"div"> {
5
+ /** The variants this page offers. The Look shows at most one of them. Defaults to all four. */
6
+ variants?: readonly MotifVariant[];
7
+ /** The same seed draws the same picture, on the server and in the browser. Defaults to `1`. */
8
+ seed?: number;
9
+ /** The word the particle variant draws. Defaults to `"CUE"`. */
10
+ word?: string;
11
+ }
12
+ /**
13
+ * The art a Look draws in a hero: a ribbon, a halftone, a gloss object or a
14
+ * particle word, whichever the style set chooses (spec 2026-10-02 §3.4).
15
+ *
16
+ * **The Look chooses, at the CSS layer.** Every offered variant is rendered, each
17
+ * in a wrapper whose `display` is the set's `style-motif-*` token, `none` unless
18
+ * the set turns it on. So switching the Look switches the art with no re-render,
19
+ * the server and the browser draw the same markup, and the paint pack `none` stays
20
+ * empty. A Look that turns none on draws nothing here and takes no room: the
21
+ * container's own `display` is `style-motif-shown`, which the build derives as
22
+ * `block` for a set that shows a motif and which is `none` everywhere else. The
23
+ * particle variant animates only while it is the one displayed.
24
+ *
25
+ * Static markup — no `"use client"`; the particle variant is its own client island.
26
+ *
27
+ * @example
28
+ * <Hero title="Payments infrastructure" media={<Motif seed={2} />} />
29
+ */
30
+ declare const Motif: React.ForwardRefExoticComponent<MotifProps & React.RefAttributes<HTMLDivElement>>;
31
+ //#endregion
32
+ export { Motif, MotifProps, MotifVariant };
@@ -0,0 +1,71 @@
1
+ import { cn } from "../lib/cn.js";
2
+ import { GlossObject } from "./gloss-object.js";
3
+ import { Halftone } from "./halftone.js";
4
+ import { ParticleMark } from "./particle-mark.js";
5
+ import { Ribbon } from "./ribbon.js";
6
+ import * as React from "react";
7
+ import { jsx, jsxs } from "react/jsx-runtime";
8
+ //#region src/brand/motif.tsx
9
+ const DISPLAY = {
10
+ ribbon: "[display:var(--cue-style-motif-ribbon,none)]",
11
+ halftone: "[display:var(--cue-style-motif-halftone,none)]",
12
+ gloss: "[display:var(--cue-style-motif-gloss,none)]",
13
+ particles: "[display:var(--cue-style-motif-particles,none)]"
14
+ };
15
+ /**
16
+ * The art a Look draws in a hero: a ribbon, a halftone, a gloss object or a
17
+ * particle word, whichever the style set chooses (spec 2026-10-02 §3.4).
18
+ *
19
+ * **The Look chooses, at the CSS layer.** Every offered variant is rendered, each
20
+ * in a wrapper whose `display` is the set's `style-motif-*` token, `none` unless
21
+ * the set turns it on. So switching the Look switches the art with no re-render,
22
+ * the server and the browser draw the same markup, and the paint pack `none` stays
23
+ * empty. A Look that turns none on draws nothing here and takes no room: the
24
+ * container's own `display` is `style-motif-shown`, which the build derives as
25
+ * `block` for a set that shows a motif and which is `none` everywhere else. The
26
+ * particle variant animates only while it is the one displayed.
27
+ *
28
+ * Static markup — no `"use client"`; the particle variant is its own client island.
29
+ *
30
+ * @example
31
+ * <Hero title="Payments infrastructure" media={<Motif seed={2} />} />
32
+ */
33
+ const Motif = React.forwardRef(function Motif({ className, variants = [
34
+ "ribbon",
35
+ "halftone",
36
+ "gloss",
37
+ "particles"
38
+ ], seed = 1, word = "CUE", ...elementProps }, ref) {
39
+ return /* @__PURE__ */ jsx("div", {
40
+ ref,
41
+ "data-slot": "motif",
42
+ "aria-hidden": "true",
43
+ className: cn("relative aspect-[4/3] w-full [display:var(--cue-style-motif-shown,none)]", className),
44
+ ...elementProps,
45
+ children: variants.map((variant) => /* @__PURE__ */ jsxs("div", {
46
+ "data-motif": variant,
47
+ className: cn("absolute inset-0", DISPLAY[variant]),
48
+ children: [
49
+ variant === "ribbon" ? /* @__PURE__ */ jsx(Ribbon, {
50
+ seed,
51
+ className: "size-full"
52
+ }) : null,
53
+ variant === "halftone" ? /* @__PURE__ */ jsx(Halftone, {
54
+ seed,
55
+ className: "size-full"
56
+ }) : null,
57
+ variant === "gloss" ? /* @__PURE__ */ jsx(GlossObject, {
58
+ seed,
59
+ className: "size-full"
60
+ }) : null,
61
+ variant === "particles" ? /* @__PURE__ */ jsx(ParticleMark, {
62
+ word,
63
+ seed,
64
+ className: "size-full"
65
+ }) : null
66
+ ]
67
+ }, variant))
68
+ });
69
+ });
70
+ //#endregion
71
+ export { Motif };
@@ -0,0 +1,29 @@
1
+ import * as React from "react";
2
+ //#region src/brand/particle-mark.d.ts
3
+ interface ParticleMarkProps extends React.ComponentPropsWithoutRef<"canvas"> {
4
+ /** The word the dots draw, short: a name or a mark. */
5
+ word: string;
6
+ /** Which scatter. Defaults to `1`. */
7
+ seed?: number;
8
+ }
9
+ /**
10
+ * A word drawn as a field of dots that drift and settle — a studio site's mark
11
+ * (spec 2026-10-02 §3.4). The dots are the accent, read from the canvas's own
12
+ * computed style, so they follow the Look.
13
+ *
14
+ * **It costs nothing at rest.** It animates only while its element is displayed,
15
+ * on screen and in a visible tab, and while {@link useStill} is false (a still
16
+ * thumbnail, or a reader who prefers reduced motion). Otherwise it draws one
17
+ * still frame. That frame is redrawn whenever any theme axis changes anywhere above
18
+ * it, the system colour scheme flips, a face finishes loading, or it resizes, so a
19
+ * paused mark is never left in an old palette. Unmounting disconnects every
20
+ * observer and cancels every frame.
21
+ *
22
+ * Decorative: hidden from assistive technology.
23
+ *
24
+ * @example
25
+ * <ParticleMark word="CUE" className="h-40 w-full" />
26
+ */
27
+ declare function ParticleMark({ word, seed, ...canvasProps }: ParticleMarkProps): React.JSX.Element;
28
+ //#endregion
29
+ export { ParticleMark, ParticleMarkProps };
@@ -0,0 +1,137 @@
1
+ "use client";
2
+ import { seeded } from "./_motif-seed.js";
3
+ import { usePrefersReducedMotion } from "../instruments/_motion.js";
4
+ import { useStill } from "../system/still.js";
5
+ import * as React from "react";
6
+ import { jsx } from "react/jsx-runtime";
7
+ //#region src/brand/particle-mark.tsx
8
+ /** The six axes a motif's colours follow (spec 2026-10-02 §3.4). */
9
+ const AXES = [
10
+ "data-theme",
11
+ "data-font",
12
+ "data-mode",
13
+ "data-style",
14
+ "data-paint",
15
+ "data-density"
16
+ ];
17
+ /**
18
+ * A word drawn as a field of dots that drift and settle — a studio site's mark
19
+ * (spec 2026-10-02 §3.4). The dots are the accent, read from the canvas's own
20
+ * computed style, so they follow the Look.
21
+ *
22
+ * **It costs nothing at rest.** It animates only while its element is displayed,
23
+ * on screen and in a visible tab, and while {@link useStill} is false (a still
24
+ * thumbnail, or a reader who prefers reduced motion). Otherwise it draws one
25
+ * still frame. That frame is redrawn whenever any theme axis changes anywhere above
26
+ * it, the system colour scheme flips, a face finishes loading, or it resizes, so a
27
+ * paused mark is never left in an old palette. Unmounting disconnects every
28
+ * observer and cancels every frame.
29
+ *
30
+ * Decorative: hidden from assistive technology.
31
+ *
32
+ * @example
33
+ * <ParticleMark word="CUE" className="h-40 w-full" />
34
+ */
35
+ function ParticleMark({ word, seed = 1, ...canvasProps }) {
36
+ const ref = React.useRef(null);
37
+ const held = useStill();
38
+ const reduced = usePrefersReducedMotion();
39
+ const still = held || reduced;
40
+ React.useEffect(() => {
41
+ const canvas = ref.current;
42
+ if (canvas === null) return;
43
+ const ctx = canvas.getContext("2d");
44
+ if (ctx === null) return;
45
+ let frame = 0;
46
+ let visible = false;
47
+ let points = [];
48
+ const layout = () => {
49
+ const rect = canvas.getBoundingClientRect();
50
+ const scale = window.devicePixelRatio || 1;
51
+ canvas.width = Math.max(1, Math.round(rect.width * scale));
52
+ canvas.height = Math.max(1, Math.round(rect.height * scale));
53
+ const off = document.createElement("canvas");
54
+ off.width = canvas.width;
55
+ off.height = canvas.height;
56
+ const o = off.getContext("2d");
57
+ if (o === null) return;
58
+ o.fillStyle = getComputedStyle(canvas).getPropertyValue("--cue-accent").trim() || "currentColor";
59
+ const family = getComputedStyle(canvas).fontFamily;
60
+ o.font = `700 100px ${family}`;
61
+ const advance = o.measureText(word).width / 100;
62
+ const size = Math.min(canvas.height * .62, advance > 0 ? canvas.width * .9 / advance : canvas.height * .62);
63
+ o.font = `700 ${Math.round(size)}px ${family}`;
64
+ o.textAlign = "center";
65
+ o.textBaseline = "middle";
66
+ o.fillText(word, canvas.width / 2, canvas.height / 2);
67
+ const data = o.getImageData(0, 0, off.width, off.height).data;
68
+ const rand = seeded(seed);
69
+ const step = Math.max(3, Math.round(4 * scale));
70
+ points = [];
71
+ for (let y = 0; y < off.height; y += step) for (let x = 0; x < off.width; x += step) if (data[(y * off.width + x) * 4 + 3] > 128) points.push({
72
+ x,
73
+ y,
74
+ dx: (rand() - .5) * step,
75
+ dy: (rand() - .5) * step
76
+ });
77
+ };
78
+ const draw = (t) => {
79
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
80
+ ctx.fillStyle = getComputedStyle(canvas).getPropertyValue("--cue-accent").trim() || "currentColor";
81
+ const size = Math.max(1, (window.devicePixelRatio || 1) * 1.4);
82
+ const k = still ? 0 : Math.sin(t / 900);
83
+ for (const p of points) ctx.fillRect(p.x + p.dx * k, p.y + p.dy * k, size, size);
84
+ };
85
+ const rendered = () => typeof canvas.checkVisibility === "function" ? canvas.checkVisibility() : canvas.getClientRects().length > 0;
86
+ const shown = () => rendered() && visible && document.visibilityState === "visible";
87
+ const tick = (t) => {
88
+ draw(t);
89
+ frame = shown() && !still ? requestAnimationFrame(tick) : 0;
90
+ };
91
+ const refresh = () => {
92
+ cancelAnimationFrame(frame);
93
+ frame = 0;
94
+ layout();
95
+ draw(0);
96
+ if (shown() && !still) frame = requestAnimationFrame(tick);
97
+ };
98
+ const intersection = new IntersectionObserver(([entry]) => {
99
+ visible = entry?.isIntersecting ?? false;
100
+ refresh();
101
+ });
102
+ intersection.observe(canvas);
103
+ const resize = new ResizeObserver(refresh);
104
+ resize.observe(canvas);
105
+ const axes = new MutationObserver(refresh);
106
+ axes.observe(document.documentElement, {
107
+ subtree: true,
108
+ attributes: true,
109
+ attributeFilter: AXES
110
+ });
111
+ const scheme = window.matchMedia("(prefers-color-scheme: dark)");
112
+ scheme.addEventListener("change", refresh);
113
+ document.addEventListener("visibilitychange", refresh);
114
+ document.fonts?.addEventListener?.("loadingdone", refresh);
115
+ refresh();
116
+ return () => {
117
+ cancelAnimationFrame(frame);
118
+ intersection.disconnect();
119
+ resize.disconnect();
120
+ axes.disconnect();
121
+ scheme.removeEventListener("change", refresh);
122
+ document.removeEventListener("visibilitychange", refresh);
123
+ document.fonts?.removeEventListener?.("loadingdone", refresh);
124
+ };
125
+ }, [
126
+ word,
127
+ seed,
128
+ still
129
+ ]);
130
+ return /* @__PURE__ */ jsx("canvas", {
131
+ ref,
132
+ "aria-hidden": "true",
133
+ ...canvasProps
134
+ });
135
+ }
136
+ //#endregion
137
+ export { ParticleMark };
@@ -0,0 +1,20 @@
1
+ import * as React from "react";
2
+ //#region src/brand/ribbon.d.ts
3
+ interface RibbonProps extends React.SVGProps<SVGSVGElement> {
4
+ /** Which ribbon: the same seed draws the same bands. Defaults to `1`. */
5
+ seed?: number;
6
+ }
7
+ /**
8
+ * A flowing ribbon of layered bands in the theme's own inks — the payments-page
9
+ * motif (spec 2026-10-02 §3.4). Five bezier bands cross the frame on a diagonal,
10
+ * each a gradient from the accent to the foreground's mix, so it reads in every
11
+ * theme and both modes. Decorative: hidden from assistive technology.
12
+ *
13
+ * Static markup — no `"use client"`.
14
+ *
15
+ * @example
16
+ * <Ribbon seed={3} className="h-64 w-full" />
17
+ */
18
+ declare function Ribbon({ seed, ...svgProps }: RibbonProps): React.JSX.Element;
19
+ //#endregion
20
+ export { Ribbon, RibbonProps };
@@ -0,0 +1,54 @@
1
+ import { r2, seeded } from "./_motif-seed.js";
2
+ import * as React from "react";
3
+ import { jsx, jsxs } from "react/jsx-runtime";
4
+ //#region src/brand/ribbon.tsx
5
+ /**
6
+ * A flowing ribbon of layered bands in the theme's own inks — the payments-page
7
+ * motif (spec 2026-10-02 §3.4). Five bezier bands cross the frame on a diagonal,
8
+ * each a gradient from the accent to the foreground's mix, so it reads in every
9
+ * theme and both modes. Decorative: hidden from assistive technology.
10
+ *
11
+ * Static markup — no `"use client"`.
12
+ *
13
+ * @example
14
+ * <Ribbon seed={3} className="h-64 w-full" />
15
+ */
16
+ function Ribbon({ seed = 1, ...svgProps }) {
17
+ const id = React.useId().replace(/:/gu, "");
18
+ const rand = seeded(seed);
19
+ const bands = Array.from({ length: 5 }, (_, i) => {
20
+ const y0 = r2(40 + i * 14 + rand() * 10);
21
+ const c1 = r2(rand() * 60 - 10);
22
+ const c2 = r2(70 + rand() * 40);
23
+ const y1 = r2(i * 8 + rand() * 12);
24
+ const w = r2(10 + rand() * 8);
25
+ return `M0 ${y0} C 30 ${c1} 60 ${c2} 100 ${y1} L 100 ${r2(y1 + w)} C 60 ${r2(c2 + w)} 30 ${r2(c1 + w)} 0 ${r2(y0 + w)} Z`;
26
+ });
27
+ return /* @__PURE__ */ jsxs("svg", {
28
+ viewBox: "0 0 100 100",
29
+ preserveAspectRatio: "none",
30
+ "aria-hidden": "true",
31
+ focusable: "false",
32
+ ...svgProps,
33
+ children: [/* @__PURE__ */ jsx("defs", { children: /* @__PURE__ */ jsxs("linearGradient", {
34
+ id: `${id}-g`,
35
+ x1: "0",
36
+ y1: "0",
37
+ x2: "1",
38
+ y2: "1",
39
+ children: [/* @__PURE__ */ jsx("stop", {
40
+ offset: "0%",
41
+ stopColor: "var(--cue-accent)"
42
+ }), /* @__PURE__ */ jsx("stop", {
43
+ offset: "100%",
44
+ stopColor: "color-mix(in oklch, var(--cue-accent) 40%, var(--cue-fg))"
45
+ })]
46
+ }) }), bands.map((d, i) => /* @__PURE__ */ jsx("path", {
47
+ d,
48
+ fill: `url(#${id}-g)`,
49
+ opacity: r2(.35 + i * .12)
50
+ }, i))]
51
+ });
52
+ }
53
+ //#endregion
54
+ export { Ribbon };