@cueplusplus/ui 0.19.0 → 0.20.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.
Files changed (79) hide show
  1. package/CHANGELOG.md +58 -0
  2. package/dist/brand/index.d.ts +3 -1
  3. package/dist/brand/particle-logo.d.ts +50 -0
  4. package/dist/brand/particle-logo.js +445 -0
  5. package/dist/brand/text-scramble.d.ts +42 -0
  6. package/dist/brand/text-scramble.js +160 -0
  7. package/dist/chat/delegation-card.js +2 -1
  8. package/dist/chat/message.js +12 -13
  9. package/dist/color/color-area.js +2 -1
  10. package/dist/forms/_chassis.d.ts +1 -1
  11. package/dist/forms/_chassis.js +2 -2
  12. package/dist/forms/env-var-input.js +2 -1
  13. package/dist/forms/file-upload.js +2 -1
  14. package/dist/index.d.ts +5 -1
  15. package/dist/index.js +5 -1
  16. package/dist/instruments/data-table.js +3 -2
  17. package/dist/instruments/table.js +1 -1
  18. package/dist/layout/index.d.ts +2 -1
  19. package/dist/layout/index.js +2 -1
  20. package/dist/layout/reveal.d.ts +44 -0
  21. package/dist/layout/reveal.js +112 -0
  22. package/dist/lib/cn.js +23 -5
  23. package/dist/lib/particle-field-shaders.js +236 -0
  24. package/dist/lib/particle-field.js +210 -0
  25. package/dist/midi/spectrum-visualizer.js +2 -1
  26. package/dist/primitives/chip.js +4 -3
  27. package/dist/styles.css +12 -0
  28. package/dist/system/_style-field-chassis.js +14 -1
  29. package/dist/system/frame-clock.d.ts +33 -0
  30. package/dist/system/frame-clock.js +56 -0
  31. package/dist/system/index.d.ts +2 -1
  32. package/dist/system/index.js +2 -1
  33. package/dist/system/still.js +14 -1
  34. package/dist/system/theme-registry.d.ts +1 -1
  35. package/dist/system/theme-registry.js +1 -1
  36. package/manifest/components/autocomplete.json +1 -0
  37. package/manifest/components/bento.json +4 -4
  38. package/manifest/components/color-area.json +10 -0
  39. package/manifest/components/color-field.json +3 -0
  40. package/manifest/components/color-picker.json +3 -0
  41. package/manifest/components/combobox.json +1 -0
  42. package/manifest/components/composer.json +3 -0
  43. package/manifest/components/data-table-pagination.json +3 -0
  44. package/manifest/components/data-table-toolbar.json +3 -0
  45. package/manifest/components/date-field.json +3 -0
  46. package/manifest/components/date-picker.json +3 -0
  47. package/manifest/components/date-range-picker.json +3 -0
  48. package/manifest/components/delegation-card.json +6 -0
  49. package/manifest/components/env-var-input.json +3 -0
  50. package/manifest/components/file-upload.json +5 -0
  51. package/manifest/components/font-scope.json +3 -2
  52. package/manifest/components/grid.json +2 -2
  53. package/manifest/components/input-group.json +3 -0
  54. package/manifest/components/input.json +3 -0
  55. package/manifest/components/ledger.json +1 -1
  56. package/manifest/components/markdown-text.json +1 -1
  57. package/manifest/components/message.json +8 -0
  58. package/manifest/components/multi-select.json +1 -0
  59. package/manifest/components/musical-time-input.json +3 -0
  60. package/manifest/components/number-field.json +3 -0
  61. package/manifest/components/otp-field.json +3 -0
  62. package/manifest/components/particle-logo.json +140 -0
  63. package/manifest/components/password-input.json +3 -0
  64. package/manifest/components/preview.json +3 -2
  65. package/manifest/components/reveal.json +108 -0
  66. package/manifest/components/scrub-input.json +3 -0
  67. package/manifest/components/search-input.json +3 -0
  68. package/manifest/components/select.json +3 -0
  69. package/manifest/components/spectrum-visualizer.json +10 -0
  70. package/manifest/components/stack.json +1 -1
  71. package/manifest/components/table.json +1 -1
  72. package/manifest/components/tags-input.json +1 -0
  73. package/manifest/components/text-scramble.json +90 -0
  74. package/manifest/components/textarea.json +3 -0
  75. package/manifest/components/theme-provider.json +3 -2
  76. package/manifest/components/time-field.json +3 -0
  77. package/manifest/manifest.json +118 -42
  78. package/manifest/tokens.json +4 -2
  79. package/package.json +6 -4
@@ -0,0 +1,112 @@
1
+ "use client";
2
+ import { cn } from "../lib/cn.js";
3
+ import { useStill } from "../system/still.js";
4
+ import * as React from "react";
5
+ import { jsx } from "react/jsx-runtime";
6
+ //#region src/layout/reveal.tsx
7
+ /** framer-motion's `easeOut`, the curve cueplusplus.com's entrances use. */
8
+ const EASE_OUT = "cubic-bezier(0, 0, 0.58, 1)";
9
+ /**
10
+ * Content that rises into place the first time it scrolls into view —
11
+ * cueplusplus.com's section entrance, ported (spec 2026-10-04-cue-site-verbatim
12
+ * §4.3): framer-motion's `whileInView` with `viewport: { once: true }`, from
13
+ * `opacity: 0` and `distance` px down, over `duration` ms `easeOut`.
14
+ *
15
+ * No motion library: an `IntersectionObserver` and two inline styles. It
16
+ * watches once and lets go. Held still, or for a reader who prefers reduced
17
+ * motion, the content is simply there and nothing is observed — as the site's
18
+ * `initial={reduce ? false : …}` does.
19
+ *
20
+ * @example
21
+ * <Reveal distance={22} rootMargin="-15% 0px" stagger={80}>
22
+ * <div data-reveal-item>Stance</div>
23
+ * <div data-reveal-item>Reason</div>
24
+ * </Reveal>
25
+ */
26
+ const Reveal = React.forwardRef(function Reveal({ distance = 28, rootMargin = "-12% 0px", duration = 500, stagger = 0, className, style, children, ...props }, ref) {
27
+ const box = React.useRef(null);
28
+ const still = useStill();
29
+ const [mounted, setMounted] = React.useState(false);
30
+ const entered = React.useRef(false);
31
+ const targets = React.useCallback(() => {
32
+ const element = box.current;
33
+ if (element === null) return [];
34
+ return stagger > 0 ? [...element.querySelectorAll("[data-reveal-item]")] : [element];
35
+ }, [stagger]);
36
+ React.useLayoutEffect(() => {
37
+ if (!still && !entered.current) for (const target of targets()) {
38
+ target.style.opacity = "0";
39
+ target.style.transform = `translateY(${distance}px)`;
40
+ }
41
+ setMounted(true);
42
+ }, [
43
+ still,
44
+ distance,
45
+ targets
46
+ ]);
47
+ React.useEffect(() => {
48
+ if (still) {
49
+ for (const target of targets()) {
50
+ target.style.opacity = "";
51
+ target.style.transform = "";
52
+ }
53
+ return;
54
+ }
55
+ if (entered.current) return;
56
+ const reveal = () => {
57
+ entered.current = true;
58
+ targets().forEach((target, index) => {
59
+ target.style.transition = `opacity ${duration}ms ${EASE_OUT}, transform ${duration}ms ${EASE_OUT}`;
60
+ if (stagger > 0) target.style.transitionDelay = `${index * stagger}ms`;
61
+ target.style.opacity = "1";
62
+ target.style.transform = "none";
63
+ });
64
+ };
65
+ const element = box.current;
66
+ if (element === null || typeof IntersectionObserver === "undefined") {
67
+ reveal();
68
+ return;
69
+ }
70
+ const stop = () => {
71
+ line.disconnect();
72
+ whole.disconnect();
73
+ };
74
+ const line = new IntersectionObserver((entries) => {
75
+ if (!entries.some((entry) => entry.isIntersecting)) return;
76
+ reveal();
77
+ stop();
78
+ }, { rootMargin });
79
+ const whole = new IntersectionObserver((entries) => {
80
+ if (!entries.some((entry) => entry.isIntersecting && entry.intersectionRatio >= .99)) return;
81
+ reveal();
82
+ stop();
83
+ }, { threshold: 1 });
84
+ line.observe(element);
85
+ whole.observe(element);
86
+ return stop;
87
+ }, [
88
+ still,
89
+ rootMargin,
90
+ duration,
91
+ stagger,
92
+ targets
93
+ ]);
94
+ return /* @__PURE__ */ jsx("div", {
95
+ ref: (node) => {
96
+ box.current = node;
97
+ if (typeof ref === "function") ref(node);
98
+ else if (ref !== null) ref.current = node;
99
+ },
100
+ "data-slot": "reveal",
101
+ "data-reveal-stagger": stagger > 0 ? "" : void 0,
102
+ className: cn(!still && !mounted && "cue-reveal-pending", className),
103
+ style: !still && !mounted ? {
104
+ ...style,
105
+ "--cue-reveal-distance": `${distance}px`
106
+ } : style,
107
+ ...props,
108
+ children
109
+ });
110
+ });
111
+ //#endregion
112
+ export { Reveal };
package/dist/lib/cn.js CHANGED
@@ -22,12 +22,30 @@ import { extendTailwindMerge } from "tailwind-merge";
22
22
  * own `radius` theme scale, which is also what makes `rounded-t-control`,
23
23
  * `rounded-br-overlay` and the rest of the per-corner rungs resolve against
24
24
  * the same conflict group as their bare form and each other.
25
+ *
26
+ * The bridge's spacing names are the same defect on another axis:
27
+ * `h-control-md`, `size-icon-sm`, `min-h-control-lg` and their kin read as
28
+ * unknown classes, so a Button's size height and a caller's `h-auto` both
29
+ * stayed on the element and the bridge utility won by CSS order — the CUE++
30
+ * site's hero actions drew 34px tall against the site's 48px (2026-10-04).
31
+ * They are members of Tailwind's `spacing` scale, which every sizing and
32
+ * spacing utility reads, so that is where they are taught.
25
33
  */
26
- const twMergeCue = extendTailwindMerge({ extend: { theme: { radius: [
27
- "control",
28
- "surface",
29
- "overlay"
30
- ] } } });
34
+ const twMergeCue = extendTailwindMerge({ extend: { theme: {
35
+ radius: [
36
+ "control",
37
+ "surface",
38
+ "overlay"
39
+ ],
40
+ spacing: [
41
+ "control-sm",
42
+ "control-md",
43
+ "control-lg",
44
+ "chip",
45
+ "icon-sm",
46
+ "icon-md"
47
+ ]
48
+ } } });
31
49
  /**
32
50
  * Join class names and resolve conflicting Tailwind utilities, last one wins.
33
51
  *
@@ -0,0 +1,236 @@
1
+ //#region src/lib/particle-field-shaders.ts
2
+ /**
3
+ * cueplusplus.com's particle-logo shaders, verbatim (`site/src/components/particle-logo/shaders.ts`
4
+ * at 08c6f26), as GLSL ES 1.00 with `highp`, which WebGL2 compiles unchanged.
5
+ *
6
+ * One deviation (spec 2026-10-04-cue-site-verbatim §9, D2): the two `smoothstep` calls the source
7
+ * wrote with their edges reversed — undefined in GLSL — are written as `1.0 - smoothstep(lo, hi, x)`,
8
+ * which is the value every GPU the site was seen on already computed for them.
9
+ *
10
+ * @internal Not part of the public API.
11
+ */
12
+ const VERTEX_SHADER = `
13
+ precision highp float;
14
+
15
+ // Built-in uniforms (must be declared for RawShaderMaterial)
16
+ uniform mat4 modelViewMatrix;
17
+ uniform mat4 projectionMatrix;
18
+
19
+ // Custom attributes
20
+ attribute vec3 position;
21
+ attribute vec2 uv;
22
+ attribute float pindex;
23
+ attribute vec3 offset;
24
+ attribute float angle;
25
+
26
+ // Custom uniforms
27
+ uniform float uTime;
28
+ uniform float uRandom;
29
+ uniform float uDepth;
30
+ uniform float uSize;
31
+ uniform vec2 uTextureSize;
32
+ uniform sampler2D uTexture;
33
+ uniform sampler2D uTouch;
34
+ uniform vec2 uWaveDirection;
35
+ uniform float uWavePosition;
36
+ uniform float uWaveTime;
37
+ uniform vec3 uWaveColor;
38
+ uniform vec3 uPrevColor;
39
+
40
+ varying vec2 vPUv;
41
+ varying vec2 vUv;
42
+ varying float vTouch;
43
+ varying float vFlipProgress;
44
+ varying float vIsFlipping;
45
+
46
+ // Simplex 2D noise
47
+ vec3 permute(vec3 x) { return mod(((x*34.0)+1.0)*x, 289.0); }
48
+
49
+ float snoise(vec2 v) {
50
+ const vec4 C = vec4(0.211324865405187, 0.366025403784439,
51
+ -0.577350269189626, 0.024390243902439);
52
+ vec2 i = floor(v + dot(v, C.yy) );
53
+ vec2 x0 = v - i + dot(i, C.xx);
54
+ vec2 i1;
55
+ i1 = (x0.x > x0.y) ? vec2(1.0, 0.0) : vec2(0.0, 1.0);
56
+ vec4 x12 = x0.xyxy + C.xxzz;
57
+ x12.xy -= i1;
58
+ i = mod(i, 289.0);
59
+ vec3 p = permute( permute( i.y + vec3(0.0, i1.y, 1.0 ))
60
+ + i.x + vec3(0.0, i1.x, 1.0 ));
61
+ vec3 m = max(0.5 - vec3(dot(x0,x0), dot(x12.xy,x12.xy),
62
+ dot(x12.zw,x12.zw)), 0.0);
63
+ m = m*m ;
64
+ m = m*m ;
65
+ vec3 x = 2.0 * fract(p * C.www) - 1.0;
66
+ vec3 h = abs(x) - 0.5;
67
+ vec3 ox = floor(x + 0.5);
68
+ vec3 a0 = x - ox;
69
+ m *= 1.79284291400159 - 0.85373472095314 * ( a0*a0 + h*h );
70
+ vec3 g;
71
+ g.x = a0.x * x0.x + h.x * x0.y;
72
+ g.yz = a0.yz * x12.xz + h.yz * x12.yw;
73
+ return 130.0 * dot(m, g);
74
+ }
75
+
76
+ float random(float n) {
77
+ return fract(sin(n) * 43758.5453123);
78
+ }
79
+
80
+ void main() {
81
+ vUv = uv;
82
+
83
+ // particle uv
84
+ vec2 puv = offset.xy / uTextureSize;
85
+ vPUv = puv;
86
+
87
+ // pixel color
88
+ vec4 colA = texture2D(uTexture, puv);
89
+ float grey = colA.r * 0.21 + colA.g * 0.71 + colA.b * 0.07;
90
+
91
+ // displacement
92
+ vec3 displaced = offset;
93
+
94
+ // Random base offset - scattered positioning
95
+ float rndX = random(pindex) - 0.5;
96
+ float rndY = random(offset.x + pindex) - 0.5;
97
+ displaced.xy += vec2(rndX, rndY) * uRandom;
98
+
99
+ float rndz = (random(pindex) + snoise(vec2(pindex * 0.1, uTime * 0.1)));
100
+ displaced.z += rndz * (random(pindex) * 2.0 * uDepth);
101
+
102
+ // Constant atom-like wiggle animation (always active)
103
+ float wiggleSpeed = 2.0 + random(pindex) * 2.0; // Varying speeds per particle
104
+ float wiggleAmount = 1.5 + random(pindex + 100.0) * 1.0; // Varying amplitudes
105
+ float wigglePhase = random(pindex * 2.0) * 6.28; // Random phase offset
106
+
107
+ displaced.x += sin(uTime * wiggleSpeed + wigglePhase) * wiggleAmount;
108
+ displaced.y += cos(uTime * wiggleSpeed * 0.8 + wigglePhase) * wiggleAmount;
109
+ displaced.z += sin(uTime * wiggleSpeed * 0.6 + wigglePhase * 0.5) * wiggleAmount * 0.5;
110
+
111
+ // center
112
+ displaced.xy -= uTextureSize * 0.5;
113
+
114
+ // Jagged wave effect - sweeps from a random direction with noise
115
+ // Project particle position onto wave direction
116
+ float particlePos = dot(displaced.xy, uWaveDirection);
117
+
118
+ // Add noise to create jagged wave front based on particle's perpendicular position
119
+ float perpPos = dot(displaced.xy, vec2(-uWaveDirection.y, uWaveDirection.x));
120
+ float noiseOffset = snoise(vec2(perpPos * 0.02, pindex * 0.1)) * 0.15;
121
+ noiseOffset += snoise(vec2(pindex * 0.05, perpPos * 0.01)) * 0.1;
122
+
123
+ // Normalize to 0-1 range based on texture size
124
+ float maxDist = length(uTextureSize) * 0.6;
125
+ float normalizedPos = (particlePos + maxDist) / (maxDist * 2.0);
126
+
127
+ // Add the noise offset to create jagged edge
128
+ normalizedPos += noiseOffset;
129
+
130
+ // Wave front position (0 to 1 sweep across)
131
+ float waveFront = uWavePosition;
132
+ float waveWidth = 0.08; // Narrower transition
133
+
134
+ // Has the wave passed this particle? (with noise for randomness)
135
+ float hasFlipped = 1.0 - smoothstep(-waveWidth, waveWidth, normalizedPos - waveFront);
136
+
137
+ // Flip animation for particles in transition zone
138
+ float flipZone = 1.0 - smoothstep(0.0, waveWidth * 2.0, abs(normalizedPos - waveFront));
139
+
140
+ // Subtle pop effect when flipping
141
+ float popEffect = flipZone * 3.0;
142
+ displaced.z += popEffect;
143
+
144
+ // Pass to fragment shader
145
+ vFlipProgress = flipZone;
146
+ vIsFlipping = hasFlipped;
147
+
148
+ // touch - dramatic explosion effect
149
+ float t = texture2D(uTouch, puv).r;
150
+ vTouch = t; // Pass to fragment shader
151
+ float touchStrength = t * 60.0; // Strong displacement on hover
152
+
153
+ // Explosive scatter in all directions
154
+ displaced.z += touchStrength * rndz * 2.0;
155
+ displaced.x += cos(angle) * touchStrength * (0.4 + rndz * 0.8);
156
+ displaced.y += sin(angle) * touchStrength * (0.4 + rndz * 0.8);
157
+
158
+ // Add swirl effect on touch
159
+ displaced.x += sin(uTime * 2.0 + pindex * 0.02) * touchStrength * 0.2;
160
+ displaced.y += cos(uTime * 2.0 + pindex * 0.02) * touchStrength * 0.2;
161
+
162
+ // particle size - grows when touched for dramatic effect
163
+ float psize = (snoise(vec2(uTime, pindex) * 0.5) + 2.0);
164
+ psize *= max(grey, 0.4);
165
+ psize *= uSize;
166
+
167
+ // Particles grow slightly when touched
168
+ psize *= (1.0 + t * 0.8);
169
+
170
+ // final position
171
+ vec4 mvPosition = modelViewMatrix * vec4(displaced, 1.0);
172
+ mvPosition.xyz += position * psize;
173
+ vec4 finalPosition = projectionMatrix * mvPosition;
174
+
175
+ gl_Position = finalPosition;
176
+ }
177
+ `;
178
+ /** @internal */
179
+ const FRAGMENT_SHADER = `
180
+ precision highp float;
181
+
182
+ uniform sampler2D uTexture;
183
+ uniform vec3 uWaveColor;
184
+ uniform vec3 uPrevColor;
185
+ uniform float uColorFade;
186
+
187
+ varying vec2 vPUv;
188
+ varying vec2 vUv;
189
+ varying float vTouch;
190
+ varying float vFlipProgress;
191
+ varying float vIsFlipping;
192
+
193
+ void main() {
194
+ vec2 uv = vUv;
195
+ vec2 puv = vPUv;
196
+
197
+ // pixel color - white for the logo
198
+ vec4 colA = texture2D(uTexture, puv);
199
+ float grey = colA.r * 0.21 + colA.g * 0.71 + colA.b * 0.07;
200
+
201
+ // Circle shape for particles
202
+ float border = 0.3;
203
+ float radius = 0.5;
204
+ float dist = radius - distance(uv, vec2(0.5));
205
+ float t = smoothstep(0.0, border, dist);
206
+
207
+ float brightness = max(grey, 0.3);
208
+
209
+ // Color transition: smoothly flip between previous and new color
210
+ // vIsFlipping smoothly transitions from 0 to 1 as wave passes
211
+ vec3 currentColor = mix(uPrevColor, uWaveColor, vIsFlipping);
212
+
213
+ // During active flip zone, flash slightly brighter
214
+ float flipBrightness = 1.0 + vFlipProgress * 0.3;
215
+
216
+ // Fade back to white over time
217
+ vec3 white = vec3(1.0, 1.0, 1.0);
218
+ vec3 particleColor = mix(currentColor, white, uColorFade);
219
+
220
+ // Apply brightness
221
+ vec3 finalColor = particleColor * brightness * flipBrightness;
222
+
223
+ // Touch glow - bright white flash
224
+ finalColor = mix(finalColor, white, vTouch * 0.8);
225
+
226
+ // Brightness boost on touch
227
+ finalColor *= (1.0 + vTouch * 0.5);
228
+
229
+ vec4 color = vec4(finalColor, 1.0);
230
+ color.a = t * brightness * (1.0 + vTouch * 0.3);
231
+
232
+ gl_FragColor = color;
233
+ }
234
+ `;
235
+ //#endregion
236
+ export { FRAGMENT_SHADER, VERTEX_SHADER };
@@ -0,0 +1,210 @@
1
+ //#region src/lib/particle-field.ts
2
+ /** A pixel is a particle when its alpha or its red is over this. */
3
+ const THRESHOLD = 200;
4
+ /**
5
+ * The source's sampling, exactly: every `skip`-th pixel of the **flattened** raster, kept when
6
+ * `alpha > 200 || red > 200`. The source rasterises its logo onto opaque black, so every sampled
7
+ * pixel passes the alpha test and the black ground becomes a field of dim particles around the
8
+ * mark — which is what the site looks like, and is kept.
9
+ */
10
+ function sampleParticles({ data, width, height }, skip, random = Math.random) {
11
+ const total = width * height;
12
+ let count = 0;
13
+ for (let i = 0; i < total; i += 1) {
14
+ if (i % skip !== 0) continue;
15
+ if (data[i * 4 + 3] > THRESHOLD || data[i * 4] > THRESHOLD) count += 1;
16
+ }
17
+ const indices = new Uint16Array(count);
18
+ const offsets = new Float32Array(count * 3);
19
+ const angles = new Float32Array(count);
20
+ for (let i = 0, j = 0; i < total; i += 1) {
21
+ if (i % skip !== 0) continue;
22
+ if (data[i * 4 + 3] <= THRESHOLD && data[i * 4] <= THRESHOLD) continue;
23
+ offsets[j * 3] = i % width;
24
+ offsets[j * 3 + 1] = Math.floor(i / width);
25
+ offsets[j * 3 + 2] = 0;
26
+ indices[j] = i;
27
+ angles[j] = random() * Math.PI * 2;
28
+ j += 1;
29
+ }
30
+ return {
31
+ count,
32
+ indices,
33
+ offsets,
34
+ angles
35
+ };
36
+ }
37
+ /**
38
+ * An sRGB hex colour in linear light, as three's `Color` converts it with colour management on
39
+ * (r152+): a `RawShaderMaterial` uniform receives these values, and the shader writes them out
40
+ * unconverted. Ported so a hex reaches the GPU exactly as it did on the site.
41
+ */
42
+ function srgbToLinear(hex) {
43
+ const value = Number.parseInt(hex.replace("#", ""), 16);
44
+ const channel = (c) => {
45
+ const s = c / 255;
46
+ return s <= .04045 ? s / 12.92 : ((s + .055) / 1.055) ** 2.4;
47
+ };
48
+ return [
49
+ channel(value >> 16 & 255),
50
+ channel(value >> 8 & 255),
51
+ channel(value & 255)
52
+ ];
53
+ }
54
+ const same = (a, b) => a[0] === b[0] && a[1] === b[1] && a[2] === b[2];
55
+ /**
56
+ * The next wave's colour: a random one that is not the current colour, as the source's
57
+ * `do … while` picks it — guarded, so a palette with nothing different to offer returns at once
58
+ * instead of looping for ever.
59
+ */
60
+ function pickWaveColor(palette, current, random = Math.random) {
61
+ if (!palette.some((colour) => !same(colour, current))) return palette[0] ?? current;
62
+ let next;
63
+ do
64
+ next = palette[Math.floor(random() * palette.length)];
65
+ while (same(next, current) && palette.length > 1);
66
+ return next;
67
+ }
68
+ const easeOutSine = (t) => Math.sin(t * Math.PI / 2);
69
+ /**
70
+ * The source's `TouchTexture` trail, without its canvas: points are added once per rendered
71
+ * update, aged before they are drawn, and dropped once their age passes 80. A point's force is how
72
+ * far the pointer moved since the last one, squared and scaled, `min((dx² + dy²) × 10000, 1)`; the
73
+ * first point has none.
74
+ */
75
+ var TouchTrail = class TouchTrail {
76
+ static SIZE = 64;
77
+ static MAX_AGE = 80;
78
+ static RADIUS = .25;
79
+ points = [];
80
+ add(x, y) {
81
+ const last = this.points[this.points.length - 1];
82
+ let force = 0;
83
+ if (last !== void 0) {
84
+ const dx = last.x - x;
85
+ const dy = last.y - y;
86
+ force = Math.min((dx * dx + dy * dy) * 1e4, 1);
87
+ }
88
+ this.points.push({
89
+ x,
90
+ y,
91
+ age: 0,
92
+ force
93
+ });
94
+ }
95
+ age() {
96
+ this.points = this.points.filter((point) => {
97
+ point.age += 1;
98
+ return point.age <= TouchTrail.MAX_AGE;
99
+ });
100
+ }
101
+ /** A point's intensity at an age: rising over the first 30% of its life, falling over the rest. */
102
+ static intensity(age) {
103
+ const rise = TouchTrail.MAX_AGE * .3;
104
+ if (age < rise) return easeOutSine(age / rise);
105
+ return easeOutSine(1 - (age - rise) / (TouchTrail.MAX_AGE * .7));
106
+ }
107
+ /** Paint the trail onto its 64px canvas, as the source's `drawTouch` does. */
108
+ draw(context) {
109
+ const size = TouchTrail.SIZE;
110
+ context.fillStyle = "black";
111
+ context.fillRect(0, 0, size, size);
112
+ for (const point of this.points) {
113
+ const x = point.x * size;
114
+ const y = (1 - point.y) * size;
115
+ const radius = size * TouchTrail.RADIUS * TouchTrail.intensity(point.age) * point.force;
116
+ if (radius <= 0) continue;
117
+ const gradient = context.createRadialGradient(x, y, radius * .25, x, y, radius);
118
+ gradient.addColorStop(0, "rgba(255, 255, 255, 0.8)");
119
+ gradient.addColorStop(1, "rgba(0, 0, 0, 0.0)");
120
+ context.beginPath();
121
+ context.fillStyle = gradient;
122
+ context.arc(x, y, radius, 0, Math.PI * 2);
123
+ context.fill();
124
+ }
125
+ }
126
+ };
127
+ /** R3F 9.6.1's default clipping planes, which the source inherits. */
128
+ const NEAR = .1;
129
+ const FAR = 1e3;
130
+ /** The world-space size the camera sees at z = 0, as R3F's `viewport` reports it. */
131
+ function viewportAt(aspect) {
132
+ const height = 600 * Math.tan(25 * Math.PI / 180);
133
+ return {
134
+ width: height * aspect,
135
+ height
136
+ };
137
+ }
138
+ /** A column-major perspective matrix, as three's `PerspectiveCamera` builds it. */
139
+ function perspective(fov, aspect, near, far) {
140
+ const f = 1 / Math.tan(fov / 2 * Math.PI / 180);
141
+ const m = /* @__PURE__ */ new Float32Array(16);
142
+ m[0] = f / aspect;
143
+ m[5] = f;
144
+ m[10] = (far + near) / (near - far);
145
+ m[11] = -1;
146
+ m[14] = 2 * far * near / (near - far);
147
+ return m;
148
+ }
149
+ /** The model-view matrix: the mesh scaled `[s, s, 1]`, seen from `z = 300`. */
150
+ function modelView(scale) {
151
+ const m = /* @__PURE__ */ new Float32Array(16);
152
+ m[0] = scale;
153
+ m[5] = scale;
154
+ m[10] = 1;
155
+ m[14] = -300;
156
+ m[15] = 1;
157
+ return m;
158
+ }
159
+ /** The mesh's scale, as the source computes it for each fit. */
160
+ function logoScale(fit, aspect, width, height, containFill) {
161
+ const viewport = viewportAt(aspect);
162
+ if (fit === "contain") return Math.min(viewport.width / width, viewport.height / height) * containFill;
163
+ return viewport.height * .26 / height;
164
+ }
165
+ /** The raster the source samples: 600px wide, at its image's aspect. */
166
+ const TARGET_WIDTH = 600;
167
+ /** Rasterise an image as the source does: twice the size with high-quality smoothing, then down onto opaque black. */
168
+ function rasterise(image) {
169
+ const scale = TARGET_WIDTH / image.width;
170
+ const width = Math.round(image.width * scale);
171
+ const height = Math.round(image.height * scale);
172
+ const large = document.createElement("canvas");
173
+ large.width = width * 2;
174
+ large.height = height * 2;
175
+ const big = large.getContext("2d", { willReadFrequently: true });
176
+ big.imageSmoothingEnabled = true;
177
+ big.imageSmoothingQuality = "high";
178
+ big.fillStyle = "black";
179
+ big.fillRect(0, 0, large.width, large.height);
180
+ big.drawImage(image, 0, 0, large.width, large.height);
181
+ const small = document.createElement("canvas");
182
+ small.width = width;
183
+ small.height = height;
184
+ const context = small.getContext("2d", { willReadFrequently: true });
185
+ context.imageSmoothingEnabled = true;
186
+ context.imageSmoothingQuality = "high";
187
+ context.fillStyle = "black";
188
+ context.fillRect(0, 0, width, height);
189
+ context.drawImage(large, 0, 0, width, height);
190
+ const { data } = context.getImageData(0, 0, width, height);
191
+ return {
192
+ data,
193
+ width,
194
+ height
195
+ };
196
+ }
197
+ /**
198
+ * A CSS colour as a `#rrggbb` string, normalised through a canvas — the one parser every
199
+ * browser ships for whatever a theme writes (hex, `rgb()`, `oklch()`, `color-mix()`).
200
+ */
201
+ function normaliseColour(colour) {
202
+ const context = document.createElement("canvas").getContext("2d");
203
+ if (context === null) return null;
204
+ context.fillStyle = "#000000";
205
+ context.fillStyle = colour;
206
+ const out = String(context.fillStyle);
207
+ return /^#[0-9a-f]{6}$/iu.test(out) ? out : null;
208
+ }
209
+ //#endregion
210
+ export { FAR, NEAR, TouchTrail, logoScale, modelView, normaliseColour, perspective, pickWaveColor, rasterise, sampleParticles, srgbToLinear, viewportAt };
@@ -1,5 +1,6 @@
1
1
  "use client";
2
2
  import { cn } from "../lib/cn.js";
3
+ import { FIELD_RADIUS } from "../system/_style-field-chassis.js";
3
4
  import { useIsomorphicLayoutEffect } from "../system/use-isomorphic-layout-effect.js";
4
5
  import { useComposedRefs } from "../lib/compose.js";
5
6
  import { toRem } from "../instruments/_rem.js";
@@ -141,7 +142,7 @@ const SpectrumVisualizer = React.forwardRef(function SpectrumVisualizer({ classN
141
142
  "aria-label": summary,
142
143
  "data-slot": "spectrum-visualizer-canvas",
143
144
  "data-mode": mode,
144
- className: "block w-full rounded-control border border-border bg-(--cue-data-ground)",
145
+ className: `block w-full ${FIELD_RADIUS} border border-border bg-(--cue-data-ground)`,
145
146
  style: { height: toRem(height) }
146
147
  }), labels ? /* @__PURE__ */ jsx("div", {
147
148
  "data-slot": "spectrum-visualizer-legend",
@@ -20,9 +20,10 @@ import { jsx } from "react/jsx-runtime";
20
20
  * two competing utilities in the class string, and `chipVariants` is exported —
21
21
  * a caller who uses it without `cn` gets a raw string where the winner is
22
22
  * decided by stylesheet order rather than by which utility was written last.
23
- * `h-chip h-4` proves the point at the other end: `tailwind-merge` cannot even
24
- * see that as a conflict, because `chip` is a token name and not a scale step.
25
- * So nothing here cancels anything. What varies moves into `variants` or
23
+ * `h-chip h-4` proved the point at the other end: `tailwind-merge` could not
24
+ * even see that as a conflict until `cn` was taught the bridge's spacing names
25
+ * (2026-10-04), and a caller who skips `cn` still cannot. So nothing here
26
+ * cancels anything. What varies moves into `variants` or
26
27
  * `compoundVariants` until only one of it is ever emitted.
27
28
  *
28
29
  * That is why `neutral` carries no rim of its own. The other six tones rim
package/dist/styles.css CHANGED
@@ -115,6 +115,18 @@
115
115
  clip-path: none;
116
116
  }
117
117
 
118
+ /* `Reveal`'s server render (`layout/reveal.tsx`): pending content is hidden before hydration, so
119
+ * it is never shown visible and then hidden by the client. Only where scripting is on — a reader
120
+ * with no JavaScript sees it — and where motion is welcome, as the client's own entrance is. A
121
+ * staggered block stays visible and hides the items it marks, as the client does. */
122
+ @media (scripting: enabled) and (prefers-reduced-motion: no-preference) {
123
+ .cue-reveal-pending:not([data-reveal-stagger]),
124
+ .cue-reveal-pending[data-reveal-stagger] [data-reveal-item] {
125
+ opacity: 0;
126
+ transform: translateY(var(--cue-reveal-distance, 28px));
127
+ }
128
+ }
129
+
118
130
  @media (forced-colors: active) {
119
131
  .cue-cut,
120
132
  .cue-cut-secondary {
@@ -24,6 +24,19 @@ const FIELD_PAD_SM = "px-[calc(var(--cue-space-3)*var(--cue-style-field-pad,1))]
24
24
  const FIELD_PAD_MD = "px-[calc(var(--cue-space-4)*var(--cue-style-field-pad,1))]";
25
25
  /** The `lg` rung's inline padding. */
26
26
  const FIELD_PAD_LG = "px-[calc(var(--cue-space-5)*var(--cue-style-field-pad,1))]";
27
+ /**
28
+ * A text-entry field's corner (2026-10-04): `style-field-radius`, and the
29
+ * control's own radius — a set's `style-control-radius` or the density's rung,
30
+ * times the scale, exactly what `rounded-control` resolves to — when a set names
31
+ * none. A set whose buttons are pills (Prism, Ledger) names it so its fields,
32
+ * textareas, drop zones and data panes are rounded rectangles rather than pills;
33
+ * every other set draws what it drew before. Spelled through `--cue-*` names,
34
+ * as `_style-check.ts`'s corner is, and never through a `--radius-*` reader.
35
+ */
36
+ const FIELD_RADIUS = "rounded-[var(--cue-style-field-radius,calc(var(--cue-style-control-radius,var(--cue-radius-control))*var(--cue-radius-scale)))]";
37
+ /** The same corner on one side, for a shape that tightens only its tail (a message bubble). */
38
+ const FIELD_RADIUS_BR = "rounded-br-[var(--cue-style-field-radius,calc(var(--cue-style-control-radius,var(--cue-radius-control))*var(--cue-radius-scale)))]";
39
+ const FIELD_RADIUS_BL = "rounded-bl-[var(--cue-style-field-radius,calc(var(--cue-style-control-radius,var(--cue-radius-control))*var(--cue-radius-scale)))]";
27
40
  /** The edge under a pointer that can hover. */
28
41
  const FIELD_HOVER_EDGE = "hover:border-[color:var(--cue-style-field-hover-edge,transparent)]";
29
42
  /** An invalid field's edge: `danger`, the class the chassis drew before round 6. */
@@ -35,4 +48,4 @@ const INVALID_EDGE = "data-[invalid]:border-danger";
35
48
  */
36
49
  const SELECT_HOVER_EDGE = "hover:not-data-disabled:border-[color:var(--cue-style-field-hover-edge,var(--cue-border-strong))]";
37
50
  //#endregion
38
- export { FIELD_HOVER_EDGE, FIELD_PAD_LG, FIELD_PAD_MD, FIELD_PAD_SM, INVALID_EDGE, SELECT_HOVER_EDGE };
51
+ export { FIELD_HOVER_EDGE, FIELD_PAD_LG, FIELD_PAD_MD, FIELD_PAD_SM, FIELD_RADIUS, FIELD_RADIUS_BL, FIELD_RADIUS_BR, INVALID_EDGE, SELECT_HOVER_EDGE };