@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.
- package/CHANGELOG.md +58 -0
- package/dist/brand/index.d.ts +3 -1
- package/dist/brand/particle-logo.d.ts +50 -0
- package/dist/brand/particle-logo.js +445 -0
- package/dist/brand/text-scramble.d.ts +42 -0
- package/dist/brand/text-scramble.js +160 -0
- package/dist/chat/delegation-card.js +2 -1
- package/dist/chat/message.js +12 -13
- package/dist/color/color-area.js +2 -1
- package/dist/forms/_chassis.d.ts +1 -1
- package/dist/forms/_chassis.js +2 -2
- package/dist/forms/env-var-input.js +2 -1
- package/dist/forms/file-upload.js +2 -1
- package/dist/index.d.ts +5 -1
- package/dist/index.js +5 -1
- package/dist/instruments/data-table.js +3 -2
- package/dist/instruments/table.js +1 -1
- package/dist/layout/index.d.ts +2 -1
- package/dist/layout/index.js +2 -1
- package/dist/layout/reveal.d.ts +44 -0
- package/dist/layout/reveal.js +112 -0
- package/dist/lib/cn.js +23 -5
- package/dist/lib/particle-field-shaders.js +236 -0
- package/dist/lib/particle-field.js +210 -0
- package/dist/midi/spectrum-visualizer.js +2 -1
- package/dist/primitives/chip.js +4 -3
- package/dist/styles.css +12 -0
- package/dist/system/_style-field-chassis.js +14 -1
- package/dist/system/frame-clock.d.ts +33 -0
- package/dist/system/frame-clock.js +56 -0
- package/dist/system/index.d.ts +2 -1
- package/dist/system/index.js +2 -1
- package/dist/system/still.js +14 -1
- package/dist/system/theme-registry.d.ts +1 -1
- package/dist/system/theme-registry.js +1 -1
- package/manifest/components/autocomplete.json +1 -0
- package/manifest/components/bento.json +4 -4
- package/manifest/components/color-area.json +10 -0
- package/manifest/components/color-field.json +3 -0
- package/manifest/components/color-picker.json +3 -0
- package/manifest/components/combobox.json +1 -0
- package/manifest/components/composer.json +3 -0
- package/manifest/components/data-table-pagination.json +3 -0
- package/manifest/components/data-table-toolbar.json +3 -0
- package/manifest/components/date-field.json +3 -0
- package/manifest/components/date-picker.json +3 -0
- package/manifest/components/date-range-picker.json +3 -0
- package/manifest/components/delegation-card.json +6 -0
- package/manifest/components/env-var-input.json +3 -0
- package/manifest/components/file-upload.json +5 -0
- package/manifest/components/font-scope.json +3 -2
- package/manifest/components/grid.json +2 -2
- package/manifest/components/input-group.json +3 -0
- package/manifest/components/input.json +3 -0
- package/manifest/components/ledger.json +1 -1
- package/manifest/components/markdown-text.json +1 -1
- package/manifest/components/message.json +8 -0
- package/manifest/components/multi-select.json +1 -0
- package/manifest/components/musical-time-input.json +3 -0
- package/manifest/components/number-field.json +3 -0
- package/manifest/components/otp-field.json +3 -0
- package/manifest/components/particle-logo.json +140 -0
- package/manifest/components/password-input.json +3 -0
- package/manifest/components/preview.json +3 -2
- package/manifest/components/reveal.json +108 -0
- package/manifest/components/scrub-input.json +3 -0
- package/manifest/components/search-input.json +3 -0
- package/manifest/components/select.json +3 -0
- package/manifest/components/spectrum-visualizer.json +10 -0
- package/manifest/components/stack.json +1 -1
- package/manifest/components/table.json +1 -1
- package/manifest/components/tags-input.json +1 -0
- package/manifest/components/text-scramble.json +90 -0
- package/manifest/components/textarea.json +3 -0
- package/manifest/components/theme-provider.json +3 -2
- package/manifest/components/time-field.json +3 -0
- package/manifest/manifest.json +118 -42
- package/manifest/tokens.json +4 -2
- 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: {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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:
|
|
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",
|
package/dist/primitives/chip.js
CHANGED
|
@@ -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`
|
|
24
|
-
* see that as a conflict
|
|
25
|
-
*
|
|
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 };
|