@cueplusplus/ui 0.18.2 → 0.20.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 (189) hide show
  1. package/CHANGELOG.md +100 -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 +8 -1
  8. package/dist/brand/motif.d.ts +32 -0
  9. package/dist/brand/motif.js +71 -0
  10. package/dist/brand/particle-logo.d.ts +50 -0
  11. package/dist/brand/particle-logo.js +437 -0
  12. package/dist/brand/particle-mark.d.ts +29 -0
  13. package/dist/brand/particle-mark.js +137 -0
  14. package/dist/brand/ribbon.d.ts +20 -0
  15. package/dist/brand/ribbon.js +54 -0
  16. package/dist/brand/text-scramble.d.ts +42 -0
  17. package/dist/brand/text-scramble.js +160 -0
  18. package/dist/chat/delegation-card.js +2 -1
  19. package/dist/chat/message.js +12 -13
  20. package/dist/chrome/announcement-bar.d.ts +30 -0
  21. package/dist/chrome/announcement-bar.js +38 -0
  22. package/dist/chrome/app-bar.d.ts +4 -1
  23. package/dist/chrome/app-bar.js +7 -2
  24. package/dist/chrome/app-shell.js +8 -1
  25. package/dist/chrome/band.d.ts +3 -1
  26. package/dist/chrome/band.js +4 -3
  27. package/dist/chrome/hero.d.ts +61 -0
  28. package/dist/chrome/hero.js +108 -0
  29. package/dist/chrome/index.d.ts +6 -1
  30. package/dist/chrome/index.js +6 -1
  31. package/dist/chrome/keyword.d.ts +22 -0
  32. package/dist/chrome/keyword.js +28 -0
  33. package/dist/chrome/logo-strip.d.ts +23 -0
  34. package/dist/chrome/logo-strip.js +43 -0
  35. package/dist/chrome/page-shell.js +13 -1
  36. package/dist/chrome/stat-band.d.ts +32 -0
  37. package/dist/chrome/stat-band.js +39 -0
  38. package/dist/color/color-area.js +2 -1
  39. package/dist/dmx/channel-matrix.js +5 -3
  40. package/dist/dmx/dmx-strip.js +5 -3
  41. package/dist/elements/animated-number.js +4 -1
  42. package/dist/elements/replay.d.ts +1 -1
  43. package/dist/elements/replay.js +5 -2
  44. package/dist/elements/scroll-anchor.js +4 -1
  45. package/dist/forms/_chassis.d.ts +1 -1
  46. package/dist/forms/_chassis.js +2 -2
  47. package/dist/forms/env-var-input.js +2 -1
  48. package/dist/forms/file-upload.js +2 -1
  49. package/dist/index.d.ts +17 -1
  50. package/dist/index.js +18 -2
  51. package/dist/instruments/data-table.js +3 -2
  52. package/dist/instruments/index.js +1 -1
  53. package/dist/instruments/stat.js +1 -1
  54. package/dist/instruments/table.js +1 -1
  55. package/dist/instruments/terminal-frame.js +5 -2
  56. package/dist/layout/grid.js +4 -2
  57. package/dist/layout/index.d.ts +2 -1
  58. package/dist/layout/index.js +2 -1
  59. package/dist/layout/preview.js +4 -188
  60. package/dist/layout/reveal.d.ts +44 -0
  61. package/dist/layout/reveal.js +106 -0
  62. package/dist/layout/sidebar.js +88 -52
  63. package/dist/lib/cn.js +23 -5
  64. package/dist/lib/particle-field-shaders.js +236 -0
  65. package/dist/lib/particle-field.js +210 -0
  66. package/dist/midi/spectrum-visualizer.js +2 -1
  67. package/dist/overlays/_surface.d.ts +2 -2
  68. package/dist/primitives/button.js +22 -3
  69. package/dist/primitives/chip.d.ts +1 -1
  70. package/dist/primitives/chip.js +63 -6
  71. package/dist/styles.css +26 -0
  72. package/dist/system/_scoped-pairing.js +18 -0
  73. package/dist/system/_stage-scope.js +199 -0
  74. package/dist/system/_style-check.js +1 -1
  75. package/dist/system/_style-chip.js +1 -1
  76. package/dist/system/_style-field-chassis.js +14 -1
  77. package/dist/system/_style-group-label.js +7 -3
  78. package/dist/system/_style-hero-title.js +41 -0
  79. package/dist/system/_style-kbd.js +1 -1
  80. package/dist/system/_style-label-ink.js +12 -5
  81. package/dist/system/_style-menu.js +1 -1
  82. package/dist/system/_style-page-title.js +1 -1
  83. package/dist/system/_style-segmented-control.js +1 -1
  84. package/dist/system/_style-skeleton.js +1 -1
  85. package/dist/system/_style-tab.js +1 -1
  86. package/dist/system/font-scope.d.ts +38 -0
  87. package/dist/system/font-scope.js +69 -0
  88. package/dist/system/frame-clock.d.ts +33 -0
  89. package/dist/system/frame-clock.js +55 -0
  90. package/dist/system/index.d.ts +4 -1
  91. package/dist/system/index.js +4 -1
  92. package/dist/system/portal.js +7 -1
  93. package/dist/system/still.d.ts +32 -0
  94. package/dist/system/still.js +60 -0
  95. package/manifest/components/alert-dialog.json +1 -1
  96. package/manifest/components/announcement-bar.json +82 -0
  97. package/manifest/components/app-bar.json +3 -2
  98. package/manifest/components/app-shell.json +3 -0
  99. package/manifest/components/autocomplete.json +2 -0
  100. package/manifest/components/band.json +2 -1
  101. package/manifest/components/bento.json +13 -13
  102. package/manifest/components/button.json +1 -0
  103. package/manifest/components/channel-matrix.json +1 -0
  104. package/manifest/components/chart-tooltip-content.json +1 -0
  105. package/manifest/components/checkbox-group.json +1 -1
  106. package/manifest/components/checkbox.json +1 -1
  107. package/manifest/components/chip.json +1 -0
  108. package/manifest/components/color-area.json +10 -0
  109. package/manifest/components/color-field.json +3 -0
  110. package/manifest/components/color-picker.json +3 -0
  111. package/manifest/components/combobox.json +2 -0
  112. package/manifest/components/command-palette.json +1 -0
  113. package/manifest/components/composer.json +3 -0
  114. package/manifest/components/context-menu.json +1 -1
  115. package/manifest/components/data-table-pagination.json +3 -0
  116. package/manifest/components/data-table-toolbar.json +3 -0
  117. package/manifest/components/date-field.json +3 -0
  118. package/manifest/components/date-picker.json +3 -0
  119. package/manifest/components/date-range-picker.json +3 -0
  120. package/manifest/components/delegation-card.json +6 -0
  121. package/manifest/components/density.json +6 -2
  122. package/manifest/components/dialog.json +1 -1
  123. package/manifest/components/dmx-bar.json +1 -1
  124. package/manifest/components/dmx-strip.json +1 -1
  125. package/manifest/components/drawer.json +1 -1
  126. package/manifest/components/dropdown-menu.json +1 -0
  127. package/manifest/components/env-var-input.json +3 -0
  128. package/manifest/components/file-upload.json +5 -0
  129. package/manifest/components/font-scope.json +82 -0
  130. package/manifest/components/footer.json +0 -1
  131. package/manifest/components/gloss-object.json +81 -0
  132. package/manifest/components/grid.json +6 -6
  133. package/manifest/components/halftone.json +79 -0
  134. package/manifest/components/hero.json +204 -0
  135. package/manifest/components/hover-card.json +1 -1
  136. package/manifest/components/input-group.json +3 -0
  137. package/manifest/components/input.json +3 -0
  138. package/manifest/components/kbd.json +1 -0
  139. package/manifest/components/keyword.json +67 -0
  140. package/manifest/components/ledger.json +5 -5
  141. package/manifest/components/logo-strip.json +93 -0
  142. package/manifest/components/markdown-text.json +3 -3
  143. package/manifest/components/menubar.json +1 -1
  144. package/manifest/components/message.json +8 -0
  145. package/manifest/components/motif.json +90 -0
  146. package/manifest/components/multi-select.json +2 -0
  147. package/manifest/components/musical-time-input.json +3 -0
  148. package/manifest/components/navigation-menu.json +1 -0
  149. package/manifest/components/number-field.json +3 -0
  150. package/manifest/components/otp-field.json +3 -0
  151. package/manifest/components/page-shell.json +5 -0
  152. package/manifest/components/particle-logo.json +140 -0
  153. package/manifest/components/particle-mark.json +75 -0
  154. package/manifest/components/password-input.json +3 -0
  155. package/manifest/components/popover.json +1 -1
  156. package/manifest/components/preview.json +10 -4
  157. package/manifest/components/radio-group.json +0 -1
  158. package/manifest/components/radio.json +0 -1
  159. package/manifest/components/reveal.json +106 -0
  160. package/manifest/components/ribbon.json +65 -0
  161. package/manifest/components/scrollable-tabs-list.json +1 -0
  162. package/manifest/components/scrub-input.json +3 -0
  163. package/manifest/components/search-input.json +3 -0
  164. package/manifest/components/segmented-control.json +1 -0
  165. package/manifest/components/select.json +3 -0
  166. package/manifest/components/sheet.json +1 -1
  167. package/manifest/components/sidebar.json +1 -0
  168. package/manifest/components/skeleton.json +1 -0
  169. package/manifest/components/spectrum-visualizer.json +10 -0
  170. package/manifest/components/stack.json +4 -4
  171. package/manifest/components/stat-band.json +68 -0
  172. package/manifest/components/stat.json +1 -1
  173. package/manifest/components/still-provider.json +60 -0
  174. package/manifest/components/switch.json +0 -1
  175. package/manifest/components/table.json +2 -2
  176. package/manifest/components/tabs.json +1 -0
  177. package/manifest/components/tags-input.json +2 -0
  178. package/manifest/components/text-scramble.json +90 -0
  179. package/manifest/components/textarea.json +3 -0
  180. package/manifest/components/theme-configurator.json +1 -0
  181. package/manifest/components/theme-provider.json +10 -4
  182. package/manifest/components/time-field.json +3 -0
  183. package/manifest/components/toast.json +1 -0
  184. package/manifest/components/tooltip.json +1 -1
  185. package/manifest/fixtures.json +18 -0
  186. package/manifest/manifest.json +368 -80
  187. package/manifest/tokens.json +93 -3
  188. package/package.json +6 -6
  189. package/dist/system/_paint-ground-pattern.js +0 -30
@@ -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",
@@ -117,9 +117,9 @@ declare const overlayFooterClasses = "flex items-center justify-end gap-(--cue-s
117
117
  * and the highlight are the style set's, with those as their defaults
118
118
  * (`_style-menu.ts`).
119
119
  */
120
- declare const menuItemClasses = "flex h-control-sm cursor-default items-center gap-(--cue-space-2) rounded-[var(--cue-style-menu-item-radius,calc(var(--cue-radius-control)*var(--cue-radius-scale)))] px-(--cue-space-3) font-[family-name:var(--cue-style-control-face,var(--cue-font-mono))] text-(length:--cue-text-ui) text-[color:var(--cue-style-quiet-ink,var(--cue-fg-muted))] data-[highlighted]:bg-[color:var(--cue-style-menu-highlight,var(--cue-accent-soft))] outline-none select-none data-[highlighted]:text-fg data-[disabled]:pointer-events-none data-[disabled]:opacity-[var(--cue-style-row-disabled-opacity,0.4)]";
120
+ declare const menuItemClasses = "flex h-control-sm cursor-default items-center gap-(--cue-space-2) rounded-[var(--cue-style-menu-item-radius,calc(var(--cue-style-control-radius,var(--cue-radius-control))*var(--cue-radius-scale)))] px-(--cue-space-3) font-[family-name:var(--cue-style-control-face,var(--cue-font-mono))] text-(length:--cue-text-ui) text-[color:var(--cue-style-quiet-ink,var(--cue-fg-muted))] data-[highlighted]:bg-[color:var(--cue-style-menu-highlight,var(--cue-accent-soft))] outline-none select-none data-[highlighted]:text-fg data-[disabled]:pointer-events-none data-[disabled]:opacity-[var(--cue-style-row-disabled-opacity,0.4)]";
121
121
  /** The uppercase mono caption over a group of menu rows — by default. */
122
- declare const menuLabelClasses = "flex h-control-sm items-center px-(--cue-space-3) py-[var(--cue-style-group-label-pad,0px)] font-[family-name:var(--cue-style-label-face,var(--cue-font-mono))] text-[length:var(--cue-style-group-label-size,var(--cue-style-label-size,var(--cue-text-label)))] tracking-[var(--cue-style-label-tracking,var(--cue-tracking-caps))] text-[color:var(--cue-style-group-label-ink,var(--cue-style-label-ink,var(--cue-fg-subtle)))] font-[number:var(--cue-style-label-weight,inherit)] [text-transform:var(--cue-style-label-case,uppercase)] select-none";
122
+ declare const menuLabelClasses = "flex h-control-sm items-center px-(--cue-space-3) py-[var(--cue-style-group-label-pad,0px)] font-[family-name:var(--cue-style-label-face,var(--cue-font-mono))] text-[length:var(--cue-style-group-label-size,var(--cue-style-label-size,var(--cue-text-label)))] tracking-[var(--cue-style-label-tracking,var(--cue-tracking-caps))] text-[color:var(--cue-style-group-label-ink,var(--cue-style-label-ink,var(--cue-fg-muted)))] font-[number:var(--cue-style-label-weight,inherit)] [text-transform:var(--cue-style-label-case,uppercase)] select-none";
123
123
  /** The hairline between menu groups. */
124
124
  declare const menuSeparatorClasses = "my-(--cue-space-1) mx-[var(--cue-style-menu-separator-inset,0px)] h-px bg-border";
125
125
  /** Padding of a menu popup — a single space unit so rows reach the rim. */
@@ -40,6 +40,25 @@ import { useRender } from "@base-ui/react/use-render";
40
40
  */
41
41
  const buttonChassis = `inline-flex select-none items-center justify-center ${CONTROL_GAP} whitespace-nowrap rounded-control ${CONTROL_FACE} ${CONTROL_WEIGHT} ${CONTROL_TRACKING} ${CONTROL_TRIM} ${CONTROL_CASE} transition-colors duration-150 outline-none ${FOCUS_RING_PAINT} focus-visible:outline-offset-[var(--cue-style-focus-offset,1px)] disabled:pointer-events-none ${DISABLED_OPACITY}`;
42
42
  /**
43
+ * A style set's chevron cut (structural Looks, spec 2026-10-02 §3.3), for the tones
44
+ * that are a filled shape: primary and danger always, secondary only where the set
45
+ * fills its ground (the build derives `--cue-style-secondary-cut` as `none` for a set
46
+ * whose secondary ground is `transparent`). Outline, ghost and danger-outline are
47
+ * drawn by their edge or their ink, and a hollow cut is geometry this chassis does
48
+ * not take on, so they keep the rectangle.
49
+ *
50
+ * The cut is a `clip-path` on the element: a filled tone's border is transparent,
51
+ * so nothing is cut but fill and exterior glow. Two things give way, on purpose:
52
+ * the glow outside a cut button is clipped (a set with a cut is worn with a pack
53
+ * that has none), and under `:focus-visible` and in forced colours the clip is
54
+ * removed, so the focus ring and the system border are never cut. The element stays
55
+ * a rectangle throughout: hit area, ring, `render`, refs, sizing and keyboard.
56
+ * Unset, the clip is `none` and the button renders exactly as before. The rules
57
+ * live in `styles.css` (`.cue-cut`, `.cue-cut-secondary`): as arbitrary utilities
58
+ * they were ~105 characters on every button a page renders.
59
+ */
60
+ const CUT_FILLED = "cue-cut";
61
+ /**
43
62
  * The shared colour vocabulary. `accent` is theme-controlled, so `primary`
44
63
  * reads as white-on-black in `cue` and cyan-on-near-black in `terminal` with no
45
64
  * per-theme branching here.
@@ -57,11 +76,11 @@ const buttonChassis = `inline-flex select-none items-center justify-center ${CON
57
76
  * @internal Not part of the public API; imported by `icon-button.tsx`.
58
77
  */
59
78
  const buttonTones = {
60
- primary: `bg-accent-fill text-accent-fill-fg hover:bg-accent-fill-hover border border-transparent ${GLOW_CLASSES} ${DISABLED_TONES.primary}`,
61
- secondary: `border ${SECONDARY_EDGE} ${SECONDARY_INK} hover:text-[color:var(--cue-style-secondary-hover-ink,var(--cue-fg))] hover:border-[color:var(--cue-style-secondary-hover-edge,var(--cue-border-strong))] ${SECONDARY_GROUND} hover:bg-[color:var(--cue-style-secondary-hover-ground,var(--cue-style-secondary-ground,var(--cue-surface-2)))] ${DISABLED_TONES.secondary}`,
79
+ primary: `bg-accent-fill [background-image:var(--cue-style-primary-fill,none)] text-accent-fill-fg hover:bg-accent-fill-hover hover:[background-image:none] disabled:[background-image:none] border border-transparent ${GLOW_CLASSES} ${CUT_FILLED} ${DISABLED_TONES.primary}`,
80
+ secondary: `border ${SECONDARY_EDGE} ${SECONDARY_INK} hover:text-[color:var(--cue-style-secondary-hover-ink,var(--cue-fg))] hover:border-[color:var(--cue-style-secondary-hover-edge,var(--cue-border-strong))] ${SECONDARY_GROUND} hover:bg-[color:var(--cue-style-secondary-hover-ground,var(--cue-style-secondary-ground,var(--cue-surface-2)))] cue-cut-secondary ${DISABLED_TONES.secondary}`,
62
81
  ghost: `${GHOST_INK} hover:text-fg hover:bg-surface-2 border border-transparent ${DISABLED_TONES.ghost}`,
63
82
  outline: `border ${OUTLINE_EDGE} ${OUTLINE_INK} ${OUTLINE_HOVER_EDGE} hover:text-accent bg-transparent ${OUTLINE_HOVER_GROUND} ${REST_SHADE_CLASS} ${DISABLED_TONES.outline}`,
64
- danger: `bg-danger text-danger-fg hover:opacity-90 border border-transparent ${GLOW_DANGER_CLASSES} ${DISABLED_TONES.danger}`,
83
+ danger: `bg-danger text-danger-fg hover:opacity-90 border border-transparent ${GLOW_DANGER_CLASSES} ${CUT_FILLED} ${DISABLED_TONES.danger}`,
65
84
  "danger-outline": `border text-danger bg-transparent hover:bg-danger/10 border-danger/40 ${DISABLED_TONES["danger-outline"]}`
66
85
  };
67
86
  /**
@@ -2,7 +2,7 @@ import { ClassProp, VariantProps } from "../lib/cva.js";
2
2
  import * as React from "react";
3
3
  //#region src/primitives/chip.d.ts
4
4
  declare const chipVariants: (props?: ({
5
- tone?: "danger" | "info" | "warn" | "accent" | "ok" | "busy" | "neutral" | null | undefined;
5
+ tone?: "info" | "warn" | "danger" | "accent" | "ok" | "busy" | "neutral" | null | undefined;
6
6
  variant?: "outline" | "tinted" | "tag" | null | undefined;
7
7
  interactive?: boolean | null | undefined;
8
8
  } & ClassProp) | undefined) => string;
@@ -7,16 +7,73 @@ import { LABEL_FACE } from "../system/_style-label-face.js";
7
7
  import { LABEL_WEIGHT } from "../system/_style-label-weight.js";
8
8
  import * as React from "react";
9
9
  import { jsx } from "react/jsx-runtime";
10
+ //#region src/primitives/chip.tsx
11
+ /**
12
+ * The tinted-chip recipe: `bg-<tone>/10 border-<tone>/35` and the tone's ink
13
+ * (mixed toward `fg`, see `CHIP_TONE_INK`), and the
14
+ * two quieter readings of it — `outline`, which drops the fill, and `tag`,
15
+ * which drops the fill and the voice with it.
16
+ *
17
+ * Every property that varies is emitted from exactly one place: one rim, one
18
+ * ink, one fill, one gutter, one type size, always. Emitting `bg-<tone>/10`
19
+ * from the tone map and then cancelling it with `bg-transparent` would leave
20
+ * two competing utilities in the class string, and `chipVariants` is exported —
21
+ * a caller who uses it without `cn` gets a raw string where the winner is
22
+ * decided by stylesheet order rather than by which utility was written last.
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
27
+ * `compoundVariants` until only one of it is ever emitted.
28
+ *
29
+ * That is why `neutral` carries no rim of its own. The other six tones rim
30
+ * themselves at `/35` in all three variants, but the neutral line has two
31
+ * weights in this system: `--cue-border` is a box edge, and a chip is a box;
32
+ * `--cue-hair-strong` is the rule between things, and a tag is a mark on the
33
+ * page rather than a container. The rim is therefore a compound of tone and
34
+ * variant, exactly as the fill is.
35
+ *
36
+ * **The recipe reads the style set** (spec 2026-09-26-looks-fidelity §3.6,
37
+ * `system/_style-chip.ts`): the six tones' fill and rim are
38
+ * `color-mix(in oklab, var(--cue-<tone>) var(--cue-style-chip-fill, 10%), transparent)`
39
+ * and the same with `--cue-style-chip-edge` at `35%` — exactly `bg-<tone>/10`
40
+ * and `border-<tone>/35` under no set, and a set's own shares inside one. The
41
+ * case, tracking, radius, size and weight read `--cue-style-chip-case`,
42
+ * `-tracking`, `-radius`, `-size` and `--cue-style-label-weight` the same way.
43
+ *
44
+ * @example
45
+ * <span className={chipVariants({ tone: "danger", variant: "outline" })}>fault</span>
46
+ * @example
47
+ * <span className={chipVariants({ variant: "tag" })}>mcp</span>
48
+ */
49
+ /**
50
+ * A toned chip's ink: its tone, mixed 30% toward the page's own `fg`.
51
+ *
52
+ * A status ink is cut to be seen as a dot or a rim (3:1), and a chip sets it
53
+ * as text: small, uppercase and on its own 10% tint, where the bare tone fell
54
+ * to 4.2–4.4:1 in light under most themes and 3.6:1 under Kiln (structural
55
+ * Looks §4.1's rendered sweep, phase 5). Mixing toward `fg` raises it in both
56
+ * modes, because `fg` is the ink furthest from the ground in either, and
57
+ * keeps the hue a reader reads the tone by.
58
+ */
59
+ const CHIP_TONE_INK = {
60
+ accent: "text-[color:color-mix(in_oklch,var(--cue-accent)_70%,var(--cue-fg))]",
61
+ ok: "text-[color:color-mix(in_oklch,var(--cue-ok)_70%,var(--cue-fg))]",
62
+ busy: "text-[color:color-mix(in_oklch,var(--cue-busy)_70%,var(--cue-fg))]",
63
+ warn: "text-[color:color-mix(in_oklch,var(--cue-warn)_70%,var(--cue-fg))]",
64
+ danger: "text-[color:color-mix(in_oklch,var(--cue-danger)_70%,var(--cue-fg))]",
65
+ info: "text-[color:color-mix(in_oklch,var(--cue-info)_70%,var(--cue-fg))]"
66
+ };
10
67
  const chipVariants = cva(`inline-flex ${CHIP_HEIGHT} select-none items-center justify-center ${CHIP_GAP} whitespace-nowrap ${CHIP_RADIUS} border ${LABEL_FACE} ${LABEL_WEIGHT} ${CHIP_CASE} outline-none ${FOCUS_RING}`, {
11
68
  variants: {
12
69
  tone: {
13
70
  neutral: CHIP_INK_NEUTRAL,
14
- accent: `${CHIP_EDGE.accent} text-accent`,
15
- ok: `${CHIP_EDGE.ok} text-ok`,
16
- busy: `${CHIP_EDGE.busy} text-busy`,
17
- warn: `${CHIP_EDGE.warn} text-warn`,
18
- danger: `${CHIP_EDGE.danger} text-danger`,
19
- info: `${CHIP_EDGE.info} text-info`
71
+ accent: `${CHIP_EDGE.accent} ${CHIP_TONE_INK.accent}`,
72
+ ok: `${CHIP_EDGE.ok} ${CHIP_TONE_INK.ok}`,
73
+ busy: `${CHIP_EDGE.busy} ${CHIP_TONE_INK.busy}`,
74
+ warn: `${CHIP_EDGE.warn} ${CHIP_TONE_INK.warn}`,
75
+ danger: `${CHIP_EDGE.danger} ${CHIP_TONE_INK.danger}`,
76
+ info: `${CHIP_EDGE.info} ${CHIP_TONE_INK.info}`
20
77
  },
21
78
  variant: {
22
79
  tinted: `${CHIP_PAD} ${CHIP_SIZE} ${CHIP_LEADING} ${CHIP_TRACKING}`,
package/dist/styles.css CHANGED
@@ -95,3 +95,29 @@
95
95
  animation: none;
96
96
  }
97
97
  }
98
+
99
+ /* A style set's chevron cut on filled buttons (structural Looks, spec 2026-10-02
100
+ * §3.3), as two bespoke classes for the plussie's reason and one more: written as
101
+ * Tailwind arbitrary properties it was some 105 characters on every button a page
102
+ * renders, and `/kitchen-sink`'s document grew 1.5% for it. Unset, the clip is
103
+ * `none` and nothing changes. Focus and forced colours drop the clip, so the ring
104
+ * and the system border are never cut; unlayered, so a utility cannot undo it. */
105
+ .cue-cut {
106
+ clip-path: var(--cue-style-control-cut-clip, none);
107
+ }
108
+
109
+ .cue-cut-secondary {
110
+ clip-path: var(--cue-style-secondary-cut, none);
111
+ }
112
+
113
+ .cue-cut:focus-visible,
114
+ .cue-cut-secondary:focus-visible {
115
+ clip-path: none;
116
+ }
117
+
118
+ @media (forced-colors: active) {
119
+ .cue-cut,
120
+ .cue-cut-secondary {
121
+ clip-path: none;
122
+ }
123
+ }
@@ -0,0 +1,18 @@
1
+ import * as React from "react";
2
+ //#region src/system/_scoped-pairing.ts
3
+ /**
4
+ * Whether the subtree's pairing is a scope's own rather than the root's.
5
+ *
6
+ * A portal mounts on `<body>`, whose `font-family` was computed from the root's
7
+ * pairing, and a computed family does not follow a token. So a portal opened
8
+ * under a scope that names a pairing asks for the family again
9
+ * (`portal.tsx`), and one opened anywhere else leaves it alone. Set by
10
+ * `StageScope` (`_stage-scope.tsx`) for `Preview` and `FontScope`. Its own
11
+ * module, with nothing but React behind it, so the portal can read it without
12
+ * importing the provider it is stamped from.
13
+ *
14
+ * @internal Not part of the public API.
15
+ */
16
+ const ScopedPairingContext = React.createContext(false);
17
+ //#endregion
18
+ export { ScopedPairingContext };