@boyernick/standard-ui-react 0.4.2-canary.60 → 0.4.2-canary.61
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/package.json +1 -1
- package/src/carousel.tsx +1 -13
- package/src/glass.tsx +22 -41
- package/src/lib/lens-glass/config.ts +225 -0
- package/src/lib/lens-glass/renderer.ts +586 -0
- package/src/lib/lens-glass/shader.ts +138 -0
- package/src/lib/media-glass.ts +11 -13
- package/src/modal.tsx +2 -2
- package/src/lib/liquidglass/NOTICE.md +0 -24
- package/src/lib/liquidglass/index.d.ts +0 -45
- package/src/lib/liquidglass/index.js +0 -2862
package/package.json
CHANGED
package/src/carousel.tsx
CHANGED
|
@@ -562,19 +562,7 @@ export const CarouselDots = ({ className, ...props }: CarouselDotsProps) => {
|
|
|
562
562
|
<Glass
|
|
563
563
|
data-glass-radius="css"
|
|
564
564
|
className="glass-optical flex items-center gap-1 rounded-full px-3 py-2.5"
|
|
565
|
-
config={{
|
|
566
|
-
blurAmount: 0,
|
|
567
|
-
refraction: 0.12,
|
|
568
|
-
zRadius: 3,
|
|
569
|
-
chromAberration: 0,
|
|
570
|
-
tintStrength: 0,
|
|
571
|
-
edgeHighlight: 0.045,
|
|
572
|
-
specular: 0.02,
|
|
573
|
-
fresnel: 1,
|
|
574
|
-
shadowOpacity: 0.065,
|
|
575
|
-
shadowSpread: 12,
|
|
576
|
-
shadowOffsetY: 3,
|
|
577
|
-
}}
|
|
565
|
+
config={{ shadow: 0.06 }}
|
|
578
566
|
>
|
|
579
567
|
{scrollSnaps.map((_, index) => {
|
|
580
568
|
const isSelected = index === selectedIndex
|
package/src/glass.tsx
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import * as React from "react";
|
|
4
|
-
import type { GlassConfig } from "./lib/
|
|
4
|
+
import type { GlassConfig } from "./lib/lens-glass/config";
|
|
5
5
|
import { cn } from "./lib/cn";
|
|
6
6
|
import { scheduleGlassRenderer } from "./lib/glass-lifecycle";
|
|
7
7
|
import { attachGlassLighting } from "./lib/glass-lighting";
|
|
@@ -10,19 +10,19 @@ import { Button, type ButtonProps } from "./button";
|
|
|
10
10
|
export type GlassState = "pending" | "live" | "off";
|
|
11
11
|
|
|
12
12
|
export interface GlassRootProps extends React.ComponentPropsWithoutRef<"div"> {
|
|
13
|
-
/** Default
|
|
14
|
-
defaults?:
|
|
13
|
+
/** Default glass material configuration shared by direct Glass children. */
|
|
14
|
+
defaults?: GlassConfig;
|
|
15
15
|
/** Disable WebGL rendering while preserving the underlying DOM. */
|
|
16
16
|
disabled?: boolean;
|
|
17
17
|
/** Move glass-optical reflections with the pointer; respects reduced motion. */
|
|
18
18
|
interactiveLighting?: boolean;
|
|
19
|
-
/** Called when the
|
|
19
|
+
/** Called when the glass renderer changes state. */
|
|
20
20
|
onGlassStateChange?: (state: GlassState) => void;
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
export interface GlassProps extends React.ComponentPropsWithoutRef<"div"> {
|
|
24
|
-
/** Per-pane
|
|
25
|
-
config?:
|
|
24
|
+
/** Per-pane glass material configuration. */
|
|
25
|
+
config?: GlassConfig;
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
function setForwardedRef<T>(ref: React.ForwardedRef<T>, value: T | null) {
|
|
@@ -34,11 +34,13 @@ function setForwardedRef<T>(ref: React.ForwardedRef<T>, value: T | null) {
|
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
/**
|
|
37
|
-
* A positioned rendering root for
|
|
37
|
+
* A positioned rendering root for the glass material.
|
|
38
38
|
*
|
|
39
|
-
* Glass panes must be direct children of this element.
|
|
40
|
-
*
|
|
41
|
-
*
|
|
39
|
+
* Glass panes must be direct children of this element. Each pane refracts the
|
|
40
|
+
* images, video and canvases inside the root that are behind it, drawn where
|
|
41
|
+
* they are each frame, over the root's children's backgrounds and a ground
|
|
42
|
+
* colour: `--glass-ground` on the root, else the nearest opaque ancestor
|
|
43
|
+
* background.
|
|
42
44
|
*/
|
|
43
45
|
export const GlassRoot = React.forwardRef<HTMLDivElement, GlassRootProps>(
|
|
44
46
|
function GlassRoot(
|
|
@@ -101,27 +103,18 @@ export const GlassRoot = React.forwardRef<HTMLDivElement, GlassRootProps>(
|
|
|
101
103
|
}
|
|
102
104
|
|
|
103
105
|
report("pending");
|
|
104
|
-
let renderer: { markChanged(): void } | undefined;
|
|
105
|
-
// Image load does not bubble and is not a DOM mutation. Lazy-loaded
|
|
106
|
-
// images otherwise leave the renderer's initial empty scene cached.
|
|
107
|
-
const refresh = () => renderer?.markChanged();
|
|
108
|
-
root.addEventListener("load", refresh, true);
|
|
109
106
|
|
|
110
107
|
const lifecycle = scheduleGlassRenderer(
|
|
111
108
|
startupRef.current,
|
|
112
109
|
async () => {
|
|
113
|
-
const {
|
|
114
|
-
|
|
115
|
-
return LiquidGlass.init({
|
|
110
|
+
const { createLensGlass } = await import("./lib/lens-glass/renderer");
|
|
111
|
+
return createLensGlass({
|
|
116
112
|
root,
|
|
117
113
|
glassElements,
|
|
118
|
-
defaults: JSON.parse(defaultsKey) as
|
|
114
|
+
defaults: JSON.parse(defaultsKey) as GlassConfig,
|
|
119
115
|
});
|
|
120
116
|
},
|
|
121
|
-
(
|
|
122
|
-
renderer = instance;
|
|
123
|
-
report("live");
|
|
124
|
-
},
|
|
117
|
+
() => report("live"),
|
|
125
118
|
(error) => {
|
|
126
119
|
console.warn("GlassRoot: renderer initialization failed.", error);
|
|
127
120
|
report("off");
|
|
@@ -129,11 +122,7 @@ export const GlassRoot = React.forwardRef<HTMLDivElement, GlassRootProps>(
|
|
|
129
122
|
);
|
|
130
123
|
startupRef.current = lifecycle.settled;
|
|
131
124
|
|
|
132
|
-
return () =>
|
|
133
|
-
root.removeEventListener("load", refresh, true);
|
|
134
|
-
renderer = undefined;
|
|
135
|
-
lifecycle.dispose();
|
|
136
|
-
};
|
|
125
|
+
return () => lifecycle.dispose();
|
|
137
126
|
}, [defaultsKey, disabled]);
|
|
138
127
|
|
|
139
128
|
return (
|
|
@@ -150,8 +139,8 @@ export const GlassRoot = React.forwardRef<HTMLDivElement, GlassRootProps>(
|
|
|
150
139
|
);
|
|
151
140
|
|
|
152
141
|
/**
|
|
153
|
-
* A
|
|
154
|
-
* `config`
|
|
142
|
+
* A glass pane. It must be rendered as a direct child of GlassRoot.
|
|
143
|
+
* `config` is serialized to the pane's `data-config` attribute.
|
|
155
144
|
*/
|
|
156
145
|
export const Glass = React.forwardRef<HTMLDivElement, GlassProps>(function Glass(
|
|
157
146
|
{ className, config, children, ...props },
|
|
@@ -174,7 +163,7 @@ GlassRoot.displayName = "GlassRoot";
|
|
|
174
163
|
Glass.displayName = "Glass";
|
|
175
164
|
|
|
176
165
|
export interface GlassButtonProps extends Omit<ButtonProps, "variant"> {
|
|
177
|
-
config?:
|
|
166
|
+
config?: GlassConfig;
|
|
178
167
|
}
|
|
179
168
|
|
|
180
169
|
/** A native StandardUI button, rendered as a direct child of GlassRoot. */
|
|
@@ -185,17 +174,9 @@ export function GlassButton({ config, ...props }: GlassButtonProps) {
|
|
|
185
174
|
variant="glass"
|
|
186
175
|
data-slot="glass"
|
|
187
176
|
data-glass-radius="css"
|
|
188
|
-
data-config={JSON.stringify({
|
|
189
|
-
blurAmount: 0.12,
|
|
190
|
-
refraction: 0.25,
|
|
191
|
-
zRadius: 6,
|
|
192
|
-
chromAberration: 0.02,
|
|
193
|
-
shadowOpacity: 0.15,
|
|
194
|
-
...config,
|
|
195
|
-
button: true,
|
|
196
|
-
})}
|
|
177
|
+
data-config={JSON.stringify({ ...config, button: true })}
|
|
197
178
|
/>
|
|
198
179
|
);
|
|
199
180
|
}
|
|
200
181
|
|
|
201
|
-
export type { GlassConfig } from "./lib/
|
|
182
|
+
export type { GlassConfig } from "./lib/lens-glass/config";
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The StandardUI glass material: a lens with a refracting bezel, colour
|
|
3
|
+
* dispersion, a lit rim, frost and grain, over a dimmed ground.
|
|
4
|
+
*
|
|
5
|
+
* Every length is in CSS pixels. Unset values scale with the pane, so the
|
|
6
|
+
* same material reads on a 36px icon button and a 400px panel.
|
|
7
|
+
*/
|
|
8
|
+
export interface GlassConfig {
|
|
9
|
+
/** Width of the refracting band inside the edge. Default: 30% of the short side, at most 48. */
|
|
10
|
+
bezel?: number;
|
|
11
|
+
/** How far the bezel bends the ground, at the rim. Default: ¾ of the bezel. */
|
|
12
|
+
bend?: number;
|
|
13
|
+
/** Red channel's share of the bend (colour dispersion). Default 1.2. */
|
|
14
|
+
dispersionRed?: number;
|
|
15
|
+
/** Blue channel's share of the bend. Default 0.8. */
|
|
16
|
+
dispersionBlue?: number;
|
|
17
|
+
/** Rim light intensity. Default 0.5. */
|
|
18
|
+
rim?: number;
|
|
19
|
+
/** Rim light width. Default 3.5, less on small panes. */
|
|
20
|
+
rimWidth?: number;
|
|
21
|
+
/** Specular sheen on the lit side of the bezel. Default 0.35. */
|
|
22
|
+
specular?: number;
|
|
23
|
+
/** Frost blur, as a Gaussian sigma. Default 3. */
|
|
24
|
+
frost?: number;
|
|
25
|
+
/** Film grain. Default 0.07. */
|
|
26
|
+
grain?: number;
|
|
27
|
+
/** Multiplier on the ground seen through the glass (1 = undimmed). Default 0.8. */
|
|
28
|
+
dim?: number;
|
|
29
|
+
/** Fraction of the height where the glass starts fading out. Default 1 (no fade). */
|
|
30
|
+
fadeStart?: number;
|
|
31
|
+
/** Fraction of the height where the glass has faded out. Default 1 (no fade). */
|
|
32
|
+
fadeEnd?: number;
|
|
33
|
+
/** Contact shadow under the pane. Default 0.18. */
|
|
34
|
+
shadow?: number;
|
|
35
|
+
/**
|
|
36
|
+
* Corner radius. Default: the element's computed border radius.
|
|
37
|
+
*/
|
|
38
|
+
cornerRadius?: number;
|
|
39
|
+
/**
|
|
40
|
+
* Corner shape: 2 is a circular arc, ~4.2 a continuous "squircle" corner.
|
|
41
|
+
* Fully rounded panes (circles and pills) are always circular. Default 4.2.
|
|
42
|
+
*/
|
|
43
|
+
cornerPower?: number;
|
|
44
|
+
/** Hover brightens the rim; pressing flattens the bend. Default false. */
|
|
45
|
+
button?: boolean;
|
|
46
|
+
|
|
47
|
+
/** @deprecated LiquidGlass option, ignored by the lens material. */
|
|
48
|
+
blurAmount?: number;
|
|
49
|
+
/** @deprecated LiquidGlass option, ignored by the lens material. */
|
|
50
|
+
refraction?: number;
|
|
51
|
+
/** @deprecated LiquidGlass option, ignored by the lens material. */
|
|
52
|
+
chromAberration?: number;
|
|
53
|
+
/** @deprecated LiquidGlass option, ignored by the lens material. */
|
|
54
|
+
edgeHighlight?: number;
|
|
55
|
+
/** @deprecated LiquidGlass option, ignored by the lens material. */
|
|
56
|
+
fresnel?: number;
|
|
57
|
+
/** @deprecated LiquidGlass option, ignored by the lens material. */
|
|
58
|
+
distortion?: number;
|
|
59
|
+
/** @deprecated LiquidGlass option, ignored by the lens material. */
|
|
60
|
+
zRadius?: number;
|
|
61
|
+
/** @deprecated LiquidGlass option, ignored by the lens material. */
|
|
62
|
+
opacity?: number;
|
|
63
|
+
/** @deprecated LiquidGlass option, ignored by the lens material. */
|
|
64
|
+
saturation?: number;
|
|
65
|
+
/** @deprecated LiquidGlass option, ignored by the lens material. */
|
|
66
|
+
tintStrength?: number;
|
|
67
|
+
/** @deprecated LiquidGlass option, ignored by the lens material. */
|
|
68
|
+
brightness?: number;
|
|
69
|
+
/** @deprecated LiquidGlass option, ignored by the lens material. */
|
|
70
|
+
shadowOpacity?: number;
|
|
71
|
+
/** @deprecated LiquidGlass option, ignored by the lens material. */
|
|
72
|
+
shadowSpread?: number;
|
|
73
|
+
/** @deprecated LiquidGlass option, ignored by the lens material. */
|
|
74
|
+
shadowOffsetY?: number;
|
|
75
|
+
/** @deprecated LiquidGlass option, ignored by the lens material. */
|
|
76
|
+
floating?: boolean;
|
|
77
|
+
/** @deprecated LiquidGlass option, ignored by the lens material. */
|
|
78
|
+
bevelMode?: number;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Everything the shader needs for one pane, fully resolved. */
|
|
82
|
+
export interface ResolvedGlass {
|
|
83
|
+
width: number;
|
|
84
|
+
height: number;
|
|
85
|
+
radius: number;
|
|
86
|
+
power: number;
|
|
87
|
+
bezel: number;
|
|
88
|
+
bend: number;
|
|
89
|
+
dispersionRed: number;
|
|
90
|
+
dispersionBlue: number;
|
|
91
|
+
rim: number;
|
|
92
|
+
rimWidth: number;
|
|
93
|
+
specular: number;
|
|
94
|
+
frost: number;
|
|
95
|
+
grain: number;
|
|
96
|
+
dim: number;
|
|
97
|
+
fadeStart: number;
|
|
98
|
+
fadeEnd: number;
|
|
99
|
+
shadow: number;
|
|
100
|
+
button: boolean;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** How far outside the pane the ground must be sampled. */
|
|
104
|
+
export function samplePadding(glass: ResolvedGlass) {
|
|
105
|
+
return Math.ceil(glass.bend * Math.max(glass.dispersionRed, 1) + glass.frost * 2 + 2);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const finite = (value: unknown): value is number =>
|
|
109
|
+
typeof value === "number" && Number.isFinite(value);
|
|
110
|
+
|
|
111
|
+
const pick = (value: unknown, fallback: number, min: number, max: number) =>
|
|
112
|
+
finite(value) ? Math.min(max, Math.max(min, value)) : fallback;
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Resolve a pane's configuration against its size and computed radius.
|
|
116
|
+
* Unset lengths scale with the short side, so small controls keep the
|
|
117
|
+
* material's proportions rather than a 48px bezel that swallows them.
|
|
118
|
+
*/
|
|
119
|
+
export function resolveGlass(
|
|
120
|
+
config: GlassConfig,
|
|
121
|
+
width: number,
|
|
122
|
+
height: number,
|
|
123
|
+
cssRadius: number,
|
|
124
|
+
): ResolvedGlass {
|
|
125
|
+
const short = Math.max(1, Math.min(width, height));
|
|
126
|
+
const radius = pick(config.cornerRadius, cssRadius, 0, short / 2);
|
|
127
|
+
// A pane rounded all the way is a circle or a pill: keep it one.
|
|
128
|
+
const fullyRounded = radius >= short / 2 - 0.5;
|
|
129
|
+
const power = fullyRounded ? 2 : pick(config.cornerPower, 4.2, 2, 8);
|
|
130
|
+
const bezel = pick(config.bezel, Math.min(48, short * 0.3), 0, short / 2);
|
|
131
|
+
return {
|
|
132
|
+
width,
|
|
133
|
+
height,
|
|
134
|
+
radius: fullyRounded ? short / 2 : radius,
|
|
135
|
+
power,
|
|
136
|
+
bezel,
|
|
137
|
+
bend: pick(config.bend, bezel * 0.75, 0, 96),
|
|
138
|
+
dispersionRed: pick(config.dispersionRed, 1.2, 1, 2),
|
|
139
|
+
dispersionBlue: pick(config.dispersionBlue, 0.8, 0, 1),
|
|
140
|
+
rim: pick(config.rim, 0.5, 0, 2),
|
|
141
|
+
rimWidth: pick(config.rimWidth, Math.min(3.5, Math.max(1.25, short * 0.04)), 0.5, 12),
|
|
142
|
+
specular: pick(config.specular, 0.35, 0, 2),
|
|
143
|
+
frost: pick(config.frost, 3, 0, 16),
|
|
144
|
+
grain: pick(config.grain, 0.07, 0, 0.5),
|
|
145
|
+
dim: pick(config.dim, 0.8, 0, 1.5),
|
|
146
|
+
fadeStart: pick(config.fadeStart, 1, 0, 1),
|
|
147
|
+
fadeEnd: pick(config.fadeEnd, 1, 0, 1),
|
|
148
|
+
shadow: pick(config.shadow, 0.18, 0, 1),
|
|
149
|
+
button: config.button === true,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** Parse a `data-config` attribute; anything but a JSON object is ignored. */
|
|
154
|
+
export function parseConfig(value: string | null | undefined): GlassConfig {
|
|
155
|
+
if (!value) return {};
|
|
156
|
+
try {
|
|
157
|
+
const parsed: unknown = JSON.parse(value);
|
|
158
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed)
|
|
159
|
+
? (parsed as GlassConfig)
|
|
160
|
+
: {};
|
|
161
|
+
} catch {
|
|
162
|
+
return {};
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export interface Box {
|
|
167
|
+
x: number;
|
|
168
|
+
y: number;
|
|
169
|
+
width: number;
|
|
170
|
+
height: number;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export function intersect(a: Box, b: Box): Box | null {
|
|
174
|
+
const x = Math.max(a.x, b.x);
|
|
175
|
+
const y = Math.max(a.y, b.y);
|
|
176
|
+
const right = Math.min(a.x + a.width, b.x + b.width);
|
|
177
|
+
const bottom = Math.min(a.y + a.height, b.y + b.height);
|
|
178
|
+
return right > x && bottom > y ? { x, y, width: right - x, height: bottom - y } : null;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/** A CSS `object-position` component: a percentage, a length, or a keyword. */
|
|
182
|
+
function position(value: string | undefined, free: number) {
|
|
183
|
+
const token = (value ?? "50%").trim();
|
|
184
|
+
if (token === "left" || token === "top") return 0;
|
|
185
|
+
if (token === "right" || token === "bottom") return free;
|
|
186
|
+
if (token === "center") return free / 2;
|
|
187
|
+
if (token.endsWith("%")) return (parseFloat(token) / 100) * free;
|
|
188
|
+
const px = parseFloat(token);
|
|
189
|
+
return Number.isFinite(px) ? px : free / 2;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Where a replaced element's content is painted inside its box, per CSS
|
|
194
|
+
* `object-fit` and `object-position`. Returns the content rectangle in the
|
|
195
|
+
* box's coordinate space (it may overflow the box; callers clip).
|
|
196
|
+
*/
|
|
197
|
+
export function objectFitRect(
|
|
198
|
+
box: { width: number; height: number },
|
|
199
|
+
natural: { width: number; height: number },
|
|
200
|
+
fit: string,
|
|
201
|
+
objectPosition: string,
|
|
202
|
+
): Box {
|
|
203
|
+
const { width: bw, height: bh } = box;
|
|
204
|
+
const nw = natural.width || bw;
|
|
205
|
+
const nh = natural.height || bh;
|
|
206
|
+
let width = bw;
|
|
207
|
+
let height = bh;
|
|
208
|
+
if (fit === "contain" || fit === "cover" || fit === "scale-down") {
|
|
209
|
+
const scale =
|
|
210
|
+
fit === "cover" ? Math.max(bw / nw, bh / nh) : Math.min(bw / nw, bh / nh);
|
|
211
|
+
const clamped = fit === "scale-down" ? Math.min(1, scale) : scale;
|
|
212
|
+
width = nw * clamped;
|
|
213
|
+
height = nh * clamped;
|
|
214
|
+
} else if (fit === "none") {
|
|
215
|
+
width = nw;
|
|
216
|
+
height = nh;
|
|
217
|
+
}
|
|
218
|
+
const [px, py] = objectPosition.split(/\s+/);
|
|
219
|
+
return {
|
|
220
|
+
x: position(px, bw - width),
|
|
221
|
+
y: position(py, bh - height),
|
|
222
|
+
width,
|
|
223
|
+
height,
|
|
224
|
+
};
|
|
225
|
+
}
|