@boyernick/standard-ui-react 0.4.2-canary.57 → 0.4.2-canary.59
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 +5 -1
- package/src/button.tsx +6 -1
- package/src/glass.tsx +201 -0
- package/src/index.ts +5 -0
- package/src/lib/glass-lifecycle.ts +33 -0
- package/src/lib/glass-lighting.ts +33 -0
- package/src/lib/liquidglass/NOTICE.md +24 -0
- package/src/lib/liquidglass/index.d.ts +45 -0
- package/src/lib/liquidglass/index.js +2862 -0
- package/src/video-player.tsx +206 -222
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@boyernick/standard-ui-react",
|
|
3
|
-
"version": "0.4.2-canary.
|
|
3
|
+
"version": "0.4.2-canary.59",
|
|
4
4
|
"description": "React components for StandardUI",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -14,6 +14,10 @@
|
|
|
14
14
|
"types": "./src/index.ts",
|
|
15
15
|
"default": "./src/index.ts"
|
|
16
16
|
},
|
|
17
|
+
"./glass": {
|
|
18
|
+
"types": "./src/glass.tsx",
|
|
19
|
+
"default": "./src/glass.tsx"
|
|
20
|
+
},
|
|
17
21
|
"./package.json": "./package.json"
|
|
18
22
|
},
|
|
19
23
|
"scripts": {
|
package/src/button.tsx
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { cva, type VariantProps } from "class-variance-authority"
|
|
2
|
-
import type { ButtonHTMLAttributes, ReactNode } from "react"
|
|
2
|
+
import type { ButtonHTMLAttributes, ReactNode, Ref } from "react"
|
|
3
3
|
import { cn } from "./lib/cn"
|
|
4
4
|
import { focusRing, focusRingDestructive } from "./lib/focus"
|
|
5
5
|
|
|
@@ -8,6 +8,10 @@ const buttonVariants = cva(
|
|
|
8
8
|
{
|
|
9
9
|
variants: {
|
|
10
10
|
variant: {
|
|
11
|
+
glass: cn(
|
|
12
|
+
"relative isolate border border-transparent bg-transparent text-fg-scrim",
|
|
13
|
+
focusRing,
|
|
14
|
+
),
|
|
11
15
|
primary: cn(
|
|
12
16
|
"border border-brand-primary-border bg-brand-primary text-brand-foreground inset-shadow-solid-top hover:bg-brand-primary-hover active:bg-brand-primary-active",
|
|
13
17
|
focusRing,
|
|
@@ -83,6 +87,7 @@ export type ButtonProps = Omit<
|
|
|
83
87
|
"prefix"
|
|
84
88
|
> &
|
|
85
89
|
VariantProps<typeof buttonVariants> & {
|
|
90
|
+
ref?: Ref<HTMLButtonElement>
|
|
86
91
|
/** Shows a spinner, sets aria-busy, and disables the button */
|
|
87
92
|
loading?: boolean
|
|
88
93
|
/** Square control sized to the height of `size` — pass an icon as children */
|
package/src/glass.tsx
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import type { GlassConfig } from "./lib/liquidglass/index.js";
|
|
5
|
+
import { cn } from "./lib/cn";
|
|
6
|
+
import { scheduleGlassRenderer } from "./lib/glass-lifecycle";
|
|
7
|
+
import { attachGlassLighting } from "./lib/glass-lighting";
|
|
8
|
+
import { Button, type ButtonProps } from "./button";
|
|
9
|
+
|
|
10
|
+
export type GlassState = "pending" | "live" | "off";
|
|
11
|
+
|
|
12
|
+
export interface GlassRootProps extends React.ComponentPropsWithoutRef<"div"> {
|
|
13
|
+
/** Default LiquidGlass shader configuration shared by direct Glass children. */
|
|
14
|
+
defaults?: Partial<GlassConfig>;
|
|
15
|
+
/** Disable WebGL rendering while preserving the underlying DOM. */
|
|
16
|
+
disabled?: boolean;
|
|
17
|
+
/** Move glass-optical reflections with the pointer; respects reduced motion. */
|
|
18
|
+
interactiveLighting?: boolean;
|
|
19
|
+
/** Called when the LiquidGlass renderer changes state. */
|
|
20
|
+
onGlassStateChange?: (state: GlassState) => void;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface GlassProps extends React.ComponentPropsWithoutRef<"div"> {
|
|
24
|
+
/** Per-pane LiquidGlass shader configuration. */
|
|
25
|
+
config?: Partial<GlassConfig>;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function setForwardedRef<T>(ref: React.ForwardedRef<T>, value: T | null) {
|
|
29
|
+
if (typeof ref === "function") {
|
|
30
|
+
ref(value);
|
|
31
|
+
} else if (ref) {
|
|
32
|
+
ref.current = value;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* A positioned rendering root for LiquidGlass.
|
|
38
|
+
*
|
|
39
|
+
* Glass panes must be direct children of this element. Background content must
|
|
40
|
+
* also live inside the root because LiquidGlass samples the root's children,
|
|
41
|
+
* not the root's own background.
|
|
42
|
+
*/
|
|
43
|
+
export const GlassRoot = React.forwardRef<HTMLDivElement, GlassRootProps>(
|
|
44
|
+
function GlassRoot(
|
|
45
|
+
{
|
|
46
|
+
className,
|
|
47
|
+
defaults,
|
|
48
|
+
disabled = false,
|
|
49
|
+
interactiveLighting = false,
|
|
50
|
+
onGlassStateChange,
|
|
51
|
+
children,
|
|
52
|
+
...props
|
|
53
|
+
},
|
|
54
|
+
forwardedRef,
|
|
55
|
+
) {
|
|
56
|
+
const rootRef = React.useRef<HTMLDivElement | null>(null);
|
|
57
|
+
const startupRef = React.useRef<Promise<void>>(Promise.resolve());
|
|
58
|
+
const stateCallbackRef = React.useRef(onGlassStateChange);
|
|
59
|
+
React.useEffect(() => {
|
|
60
|
+
stateCallbackRef.current = onGlassStateChange;
|
|
61
|
+
}, [onGlassStateChange]);
|
|
62
|
+
|
|
63
|
+
// GlassConfig is a flat JSON-safe object. A serialized key means callers
|
|
64
|
+
// can pass an inline object without tearing down WebGL on every render.
|
|
65
|
+
const defaultsKey = JSON.stringify(defaults ?? {});
|
|
66
|
+
|
|
67
|
+
React.useEffect(() => {
|
|
68
|
+
if (interactiveLighting && rootRef.current && !disabled) {
|
|
69
|
+
return attachGlassLighting(rootRef.current);
|
|
70
|
+
}
|
|
71
|
+
}, [interactiveLighting, disabled]);
|
|
72
|
+
|
|
73
|
+
const setRootRef = React.useCallback(
|
|
74
|
+
(node: HTMLDivElement | null) => {
|
|
75
|
+
rootRef.current = node;
|
|
76
|
+
setForwardedRef(forwardedRef, node);
|
|
77
|
+
},
|
|
78
|
+
[forwardedRef],
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
React.useEffect(() => {
|
|
82
|
+
const root = rootRef.current;
|
|
83
|
+
const report = (state: GlassState) => {
|
|
84
|
+
root?.setAttribute("data-glass-state", state);
|
|
85
|
+
stateCallbackRef.current?.(state);
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
if (!root || disabled) {
|
|
89
|
+
report("off");
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const glassElements = Array.from(root.children).filter(
|
|
94
|
+
(element): element is HTMLElement =>
|
|
95
|
+
element instanceof HTMLElement && element.dataset.slot === "glass",
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
if (glassElements.length === 0) {
|
|
99
|
+
report("off");
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
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
|
+
|
|
110
|
+
const lifecycle = scheduleGlassRenderer(
|
|
111
|
+
startupRef.current,
|
|
112
|
+
async () => {
|
|
113
|
+
const { LiquidGlass } = await import("./lib/liquidglass/index.js");
|
|
114
|
+
await document.fonts.ready;
|
|
115
|
+
return LiquidGlass.init({
|
|
116
|
+
root,
|
|
117
|
+
glassElements,
|
|
118
|
+
defaults: JSON.parse(defaultsKey) as Partial<GlassConfig>,
|
|
119
|
+
});
|
|
120
|
+
},
|
|
121
|
+
(instance) => {
|
|
122
|
+
renderer = instance;
|
|
123
|
+
report("live");
|
|
124
|
+
},
|
|
125
|
+
(error) => {
|
|
126
|
+
console.warn("GlassRoot: renderer initialization failed.", error);
|
|
127
|
+
report("off");
|
|
128
|
+
},
|
|
129
|
+
);
|
|
130
|
+
startupRef.current = lifecycle.settled;
|
|
131
|
+
|
|
132
|
+
return () => {
|
|
133
|
+
root.removeEventListener("load", refresh, true);
|
|
134
|
+
renderer = undefined;
|
|
135
|
+
lifecycle.dispose();
|
|
136
|
+
};
|
|
137
|
+
}, [defaultsKey, disabled]);
|
|
138
|
+
|
|
139
|
+
return (
|
|
140
|
+
<div
|
|
141
|
+
ref={setRootRef}
|
|
142
|
+
data-slot="glass-root"
|
|
143
|
+
className={cn("relative", className)}
|
|
144
|
+
{...props}
|
|
145
|
+
>
|
|
146
|
+
{children}
|
|
147
|
+
</div>
|
|
148
|
+
);
|
|
149
|
+
},
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* A LiquidGlass pane. It must be rendered as a direct child of GlassRoot.
|
|
154
|
+
* `config` maps directly to LiquidGlass's per-element `data-config` JSON.
|
|
155
|
+
*/
|
|
156
|
+
export const Glass = React.forwardRef<HTMLDivElement, GlassProps>(function Glass(
|
|
157
|
+
{ className, config, children, ...props },
|
|
158
|
+
ref,
|
|
159
|
+
) {
|
|
160
|
+
return (
|
|
161
|
+
<div
|
|
162
|
+
ref={ref}
|
|
163
|
+
data-slot="glass"
|
|
164
|
+
data-config={config ? JSON.stringify(config) : undefined}
|
|
165
|
+
className={cn("relative isolate", className)}
|
|
166
|
+
{...props}
|
|
167
|
+
>
|
|
168
|
+
{children}
|
|
169
|
+
</div>
|
|
170
|
+
);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
GlassRoot.displayName = "GlassRoot";
|
|
174
|
+
Glass.displayName = "Glass";
|
|
175
|
+
|
|
176
|
+
export interface GlassButtonProps extends Omit<ButtonProps, "variant"> {
|
|
177
|
+
config?: Partial<GlassConfig>;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** A native StandardUI button, rendered as a direct child of GlassRoot. */
|
|
181
|
+
export function GlassButton({ config, ...props }: GlassButtonProps) {
|
|
182
|
+
return (
|
|
183
|
+
<Button
|
|
184
|
+
{...props}
|
|
185
|
+
variant="glass"
|
|
186
|
+
data-slot="glass"
|
|
187
|
+
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
|
+
})}
|
|
197
|
+
/>
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export type { GlassConfig } from "./lib/liquidglass/index.js";
|
package/src/index.ts
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
export { Button, buttonVariants, type ButtonProps } from "./button";
|
|
2
|
+
export {
|
|
3
|
+
Glass, GlassRoot, GlassButton,
|
|
4
|
+
type GlassProps, type GlassRootProps, type GlassButtonProps,
|
|
5
|
+
type GlassConfig, type GlassState,
|
|
6
|
+
} from "./glass";
|
|
2
7
|
export { Input, inputVariants, type InputProps } from "./input";
|
|
3
8
|
export {
|
|
4
9
|
Kbd,
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/** Serialize renderer ownership of a root, including asynchronous startup. */
|
|
2
|
+
export function scheduleGlassRenderer<T extends { destroy(): void }>(
|
|
3
|
+
previous: Promise<void>,
|
|
4
|
+
initialize: () => Promise<T>,
|
|
5
|
+
ready: (instance: T) => void,
|
|
6
|
+
failed: (error: unknown) => void,
|
|
7
|
+
) {
|
|
8
|
+
let cancelled = false;
|
|
9
|
+
let instance: T | undefined;
|
|
10
|
+
const settled = previous.then(async () => {
|
|
11
|
+
if (cancelled) return;
|
|
12
|
+
try {
|
|
13
|
+
instance = await initialize();
|
|
14
|
+
if (cancelled) {
|
|
15
|
+
instance.destroy();
|
|
16
|
+
instance = undefined;
|
|
17
|
+
} else {
|
|
18
|
+
ready(instance);
|
|
19
|
+
}
|
|
20
|
+
} catch (error) {
|
|
21
|
+
if (!cancelled) failed(error);
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
return {
|
|
26
|
+
settled,
|
|
27
|
+
dispose() {
|
|
28
|
+
cancelled = true;
|
|
29
|
+
instance?.destroy();
|
|
30
|
+
instance = undefined;
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/** Update CSS-only reflections without re-rendering React or the WebGL scene. */
|
|
2
|
+
export function attachGlassLighting(root: HTMLElement) {
|
|
3
|
+
const target = root.closest("a, button") ?? root
|
|
4
|
+
const reducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)")
|
|
5
|
+
let frame = 0
|
|
6
|
+
const reset = () => {
|
|
7
|
+
cancelAnimationFrame(frame)
|
|
8
|
+
root.style.removeProperty("--glass-light-angle")
|
|
9
|
+
root.style.removeProperty("--glass-light-x")
|
|
10
|
+
root.style.removeProperty("--glass-light-y")
|
|
11
|
+
}
|
|
12
|
+
const move = (event: Event) => {
|
|
13
|
+
if (!(event instanceof PointerEvent) || reducedMotion.matches) return
|
|
14
|
+
const bounds = target.getBoundingClientRect()
|
|
15
|
+
const x = Math.min(1, Math.max(0, (event.clientX - bounds.left) / bounds.width))
|
|
16
|
+
const y = Math.min(1, Math.max(0, (event.clientY - bounds.top) / bounds.height))
|
|
17
|
+
cancelAnimationFrame(frame)
|
|
18
|
+
frame = requestAnimationFrame(() => {
|
|
19
|
+
root.style.setProperty("--glass-light-angle", `${125 + x * 35 + y * 10}deg`)
|
|
20
|
+
root.style.setProperty("--glass-light-x", `${20 + x * 40}%`)
|
|
21
|
+
root.style.setProperty("--glass-light-y", `${y * 35}%`)
|
|
22
|
+
})
|
|
23
|
+
}
|
|
24
|
+
target.addEventListener("pointermove", move)
|
|
25
|
+
target.addEventListener("pointerleave", reset)
|
|
26
|
+
reducedMotion.addEventListener("change", reset)
|
|
27
|
+
return () => {
|
|
28
|
+
reset()
|
|
29
|
+
target.removeEventListener("pointermove", move)
|
|
30
|
+
target.removeEventListener("pointerleave", reset)
|
|
31
|
+
reducedMotion.removeEventListener("change", reset)
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# LiquidGlass
|
|
2
|
+
|
|
3
|
+
Vendored from `@ybouane/liquidglass@1.0.3`.
|
|
4
|
+
|
|
5
|
+
Source: https://github.com/ybouane/liquidglass
|
|
6
|
+
License: MIT (as declared by the upstream package).
|
|
7
|
+
|
|
8
|
+
`index.js` is the package's published bundled WebGL renderer. StandardUI
|
|
9
|
+
vendors the build because version 1.0.3 publishes a `postinstall`
|
|
10
|
+
command whose `patch-package` executable is not a runtime dependency.
|
|
11
|
+
|
|
12
|
+
Local patch: `LiquidGlass.init()` destroys a partially initialized instance
|
|
13
|
+
before rethrowing a startup error, releasing its canvas, listeners and WebGL
|
|
14
|
+
resources.
|
|
15
|
+
|
|
16
|
+
StandardUI's `data-glass-radius="css"` panes derive the shader radius from the
|
|
17
|
+
computed CSS radius. Disabled buttons suppress shader hover/press feedback.
|
|
18
|
+
|
|
19
|
+
Responsive images are decoded from `currentSrc` without `srcset` before canvas
|
|
20
|
+
sampling. This keeps object-fit crop coordinates in resource pixels instead of
|
|
21
|
+
mixing density-corrected `naturalWidth` with full-resolution canvas coordinates.
|
|
22
|
+
|
|
23
|
+
Injected canvases compensate for the host's border so the rendered lens stays
|
|
24
|
+
centered on bordered controls and their icons.
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Type surface for the vendored @ybouane/liquidglass 1.0.3 renderer.
|
|
3
|
+
* Source: https://github.com/ybouane/liquidglass
|
|
4
|
+
* License: MIT
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export interface GlassConfig {
|
|
8
|
+
blurAmount: number;
|
|
9
|
+
refraction: number;
|
|
10
|
+
chromAberration: number;
|
|
11
|
+
edgeHighlight: number;
|
|
12
|
+
specular: number;
|
|
13
|
+
fresnel: number;
|
|
14
|
+
distortion: number;
|
|
15
|
+
cornerRadius: number;
|
|
16
|
+
zRadius: number;
|
|
17
|
+
opacity: number;
|
|
18
|
+
saturation: number;
|
|
19
|
+
tintStrength: number;
|
|
20
|
+
brightness: number;
|
|
21
|
+
shadowOpacity: number;
|
|
22
|
+
shadowSpread: number;
|
|
23
|
+
shadowOffsetY: number;
|
|
24
|
+
floating: boolean;
|
|
25
|
+
button: boolean;
|
|
26
|
+
bevelMode: number;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface LiquidGlassOptions {
|
|
30
|
+
root: HTMLElement;
|
|
31
|
+
glassElements?: NodeListOf<HTMLElement> | HTMLElement[];
|
|
32
|
+
defaults?: Partial<GlassConfig>;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export declare class LiquidGlass {
|
|
36
|
+
static init(options: LiquidGlassOptions): Promise<LiquidGlass>;
|
|
37
|
+
readonly root: HTMLElement;
|
|
38
|
+
readonly defaults: GlassConfig;
|
|
39
|
+
fps: number;
|
|
40
|
+
destroy(): void;
|
|
41
|
+
markChanged(element?: HTMLElement): void;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export declare const DEFAULTS: GlassConfig;
|
|
45
|
+
export declare function invalidateFontEmbedCache(): void;
|