From 3e35c7f2f379e70cd3aceaee742b660d7b2acf80 Mon Sep 17 00:00:00 2001
From: Stian Larsen <71408982+Stianlars1@users.noreply.github.com>
Date: Fri, 11 Sep 2026 11:07:39 +0200
Subject: [PATCH] feat(demo): redesign playground with shared glass controls
---
.../src/components/landing/MaterialStage.tsx | 69 ++-
.../demo/src/components/landing/PillLabel.tsx | 25 +
apps/demo/src/components/playground/App.tsx | 488 +++++-------------
.../playground/BackgroundPicker.tsx | 166 +++---
.../src/components/playground/CodePanel.tsx | 80 +++
.../components/playground/ControlMaterial.tsx | 26 +
.../src/components/playground/Controls.tsx | 52 +-
.../src/components/playground/CopyButton.tsx | 33 ++
.../src/components/playground/GlassButton.tsx | 81 +++
.../components/playground/GlassDetails.tsx | 44 ++
.../components/playground/GlassPreview.tsx | 271 ++++++++++
.../playground/SavedConfiguration.tsx | 99 +++-
.../components/playground/ShapeControls.tsx | 13 +-
.../demo/src/components/playground/Slider.tsx | 3 +-
apps/demo/src/components/playground/config.ts | 86 +--
apps/demo/src/components/playground/icons.tsx | 36 ++
.../components/playground/useCopyFeedback.ts | 41 ++
apps/demo/src/lib/design-system/landing.css | 20 +-
.../lib/design-system/pill-presentation.ts | 32 ++
.../demo/src/lib/design-system/playground.css | 4 +-
.../playground/control-transitions.css | 30 ++
.../lib/design-system/playground/controls.css | 249 ++++-----
.../design-system/playground/foundation.css | 191 +++----
.../playground/glass-controls.css | 373 +++++++++++++
.../design-system/playground/responsive.css | 459 +++++-----------
.../design-system/playground/workspace.css | 475 ++++++++++++-----
apps/demo/src/lib/landing-pill.ts | 15 +
apps/demo/src/lib/material-scenes.ts | 20 +
apps/demo/tests/config.test.mjs | 83 ++-
docs/design-locks/2026-09-09-demo.md | 22 +
30 files changed, 2339 insertions(+), 1247 deletions(-)
create mode 100644 apps/demo/src/components/landing/PillLabel.tsx
create mode 100644 apps/demo/src/components/playground/CodePanel.tsx
create mode 100644 apps/demo/src/components/playground/ControlMaterial.tsx
create mode 100644 apps/demo/src/components/playground/CopyButton.tsx
create mode 100644 apps/demo/src/components/playground/GlassButton.tsx
create mode 100644 apps/demo/src/components/playground/GlassDetails.tsx
create mode 100644 apps/demo/src/components/playground/GlassPreview.tsx
create mode 100644 apps/demo/src/components/playground/useCopyFeedback.ts
create mode 100644 apps/demo/src/lib/design-system/pill-presentation.ts
create mode 100644 apps/demo/src/lib/design-system/playground/control-transitions.css
create mode 100644 apps/demo/src/lib/design-system/playground/glass-controls.css
create mode 100644 apps/demo/src/lib/landing-pill.ts
create mode 100644 apps/demo/src/lib/material-scenes.ts
diff --git a/apps/demo/src/components/landing/MaterialStage.tsx b/apps/demo/src/components/landing/MaterialStage.tsx
index 30041b0..40aee9e 100644
--- a/apps/demo/src/components/landing/MaterialStage.tsx
+++ b/apps/demo/src/components/landing/MaterialStage.tsx
@@ -5,18 +5,22 @@ import type { SyntheticEvent } from "react";
import Image from "next/image";
import { LiquidGlass } from "react-glaze";
import { useGlassDrag } from "@/hooks/useGlassDrag";
+import { materialScenes } from "@/lib/material-scenes";
+import { landingPill } from "@/lib/landing-pill";
+import { pillStyle } from "@/lib/design-system/pill-presentation";
+import { PillLabel } from "./PillLabel";
-const scenes = [
- { id: "soft", label: "Soft forms", image: "/art/soft.png" },
- { id: "metal", label: "Hard light", image: "/art/metal.png" },
- { id: "garden", label: "Small world", image: "/art/garden.png" },
-] as const;
+const scenes = materialScenes;
type SceneImage = { index: number; request: number };
-const imageSizes = "(max-width: 760px) calc(100vw - 32px), (max-width: 1280px) 58vw, 760px";
+const imageSizes =
+ "(max-width: 760px) calc(100vw - 32px), (max-width: 1280px) 58vw, 760px";
export function MaterialStage() {
- const [displayed, setDisplayed] = useState({ index: 0, request: 0 });
+ const [displayed, setDisplayed] = useState({
+ index: 0,
+ request: 0,
+ });
const [pending, setPending] = useState(null);
const latestRequest = useRef(0);
const [frosted, setFrosted] = useState(false);
@@ -35,7 +39,8 @@ export function MaterialStage() {
const onGlassReady = useCallback(() => setGlassError(false), []);
const onGlassError = useCallback(() => setGlassError(true), []);
const chooseScene = (index: number) => {
- if (pending?.index === index || (!pending && displayed.index === index)) return;
+ if (pending?.index === index || (!pending && displayed.index === index))
+ return;
const request = ++latestRequest.current;
setFailedScene(null);
setPending(index === displayed.index ? null : { index, request });
@@ -71,7 +76,7 @@ export function MaterialStage() {
setFrosted((value) => !value)}
onReady={onGlassReady}
onError={onGlassError}
>
- A different feeling.
-
+
@@ -131,11 +118,19 @@ export function MaterialStage() {
Alt + arrows to move.
+
{scenes.map((option, index) => (
))}
diff --git a/apps/demo/src/components/landing/PillLabel.tsx b/apps/demo/src/components/landing/PillLabel.tsx
new file mode 100644
index 0000000..96aa9b6
--- /dev/null
+++ b/apps/demo/src/components/landing/PillLabel.tsx
@@ -0,0 +1,25 @@
+import { landingPillArrow, landingPillLabel } from "@/lib/landing-pill";
+
+export function PillLabel() {
+ return (
+ <>
+
{landingPillLabel}
+
+ >
+ );
+}
diff --git a/apps/demo/src/components/playground/App.tsx b/apps/demo/src/components/playground/App.tsx
index c478190..1cd57d6 100644
--- a/apps/demo/src/components/playground/App.tsx
+++ b/apps/demo/src/components/playground/App.tsx
@@ -1,50 +1,35 @@
-/* eslint-disable @next/next/no-img-element -- Preview uploaded and local background sources directly. */
-import { useCallback, useEffect, useRef, useState } from "react";
+import { ControlMaterial } from "./ControlMaterial";
+import { CopyButton } from "./CopyButton";
+import { useEffect, useRef, useState } from "react";
import Link from "next/link";
import { BrandIcon } from "@/components/brand/BrandIcon";
-import { trackDemoEvent } from "@/lib/analytics";
-import { LiquidGlass, PRESETS } from "react-glaze";
-import type { GlassMetrics } from "react-glaze";
-import {
- backgrounds,
- componentCode,
- componentProps,
- getExampleStyle,
- shareableState,
-} from "./config";
+import { PRESETS } from "react-glaze";
+import type { GlassPreset } from "react-glaze";
+import { backgrounds, initialState, shareableState } from "./config";
+import type { StudioState } from "./config";
import { Controls } from "./Controls";
import { SavedConfiguration } from "./SavedConfiguration";
import { readPlaygroundState } from "./saved-config";
-import { useGlassDrag } from "./useGlassDrag";
-import { CopyIcon } from "./icons";
-
-import { OpticalBackdrop } from "./OpticalBackdrop";
import { BackgroundPicker } from "./BackgroundPicker";
import { useCustomBackground } from "./useCustomBackground";
-import type { StudioState } from "./config";
-import type { GlassPreset } from "react-glaze";
+import { useCopyFeedback } from "./useCopyFeedback";
+import { GlassPreview } from "./GlassPreview";
+import { CodePanel } from "./CodePanel";
export function App() {
- const [state, setState] = useState(readPlaygroundState),
- [clicks, setClicks] = useState(0),
- [ready, setReady] = useState(false),
- [error, setError] = useState(""),
- [codeTab, setCodeTab] = useState<"jsx" | "json">("jsx"),
- [copied, setCopied] = useState(false);
- const {
- stage: stageRef,
- host: glassRef,
- position,
- dragging,
- reset: resetPosition,
- moveTo,
- bindings,
- } = useGlassDrag(state.example);
+ const [state, setState] = useState(readPlaygroundState);
+ const [previewRevision, setPreviewRevision] = useState(0);
const custom = useCustomBackground();
+ const linkCopy = useCopyFeedback();
const backgroundRevision = useRef(0);
const chooseBackground = (background: StudioState["background"]) => {
backgroundRevision.current++;
- setState((s) => ({ ...s, background }));
+ setState((s) => ({ ...s, background, referenceLayout: false }));
+ };
+ const replaceState = (next: StudioState) => {
+ backgroundRevision.current++;
+ setState(next);
+ setPreviewRevision((value) => value + 1);
};
const choosePreset = (preset: GlassPreset) =>
setState((s) => ({
@@ -55,352 +40,147 @@ export function App() {
enabled: s.settings.enabled,
shape: s.settings.shape,
radius: s.settings.radius,
+ ...(s.example === "landing-pill"
+ ? { lighting: s.settings.lighting }
+ : {}),
},
}));
- const matchOpticalLayout = useCallback(() => {
- const r = stageRef.current?.getBoundingClientRect();
- if (!r) return;
- const portrait = r.width < r.height,
- width = r.width * ((2 * 1.42 * (portrait ? 1.1 : 1.08)) / 9.7),
- height = width * (portrait ? 1.08 / 1.1 : 0.97 / 1.08);
- setState((s) =>
- Math.abs(Number(s.width) - width) < 0.01 &&
- Math.abs(Number(s.height) - height) < 0.01
- ? s
- : { ...s, width, height },
- );
- moveTo({
- x: 0,
- y: portrait ? r.height * 0.08 : (-r.width * 0.12) / 9.7,
- });
- }, [stageRef, moveTo]);
- const matchReference = () => {
- backgroundRevision.current++;
- setState((s) => ({
- ...s,
- preset: "optical-type",
- customShape: false,
- settings: { ...PRESETS["optical-type"], enabled: s.settings.enabled },
- rimLight: false,
- background: "optical-type",
- example: "empty",
- motion: false,
- referenceLayout: true,
- }));
- matchOpticalLayout();
- };
- useEffect(() => {
- if (!state.referenceLayout || !stageRef.current) return;
- const observer = new ResizeObserver(matchOpticalLayout);
- observer.observe(stageRef.current);
- matchOpticalLayout();
- return () => observer.disconnect();
- }, [state.referenceLayout, stageRef, matchOpticalLayout]);
- const metrics = useRef
(null);
- const onReady = useCallback(() => {
- setReady(true);
- setError("");
- }, []);
- const onError = useCallback((error: Error) => setError(error.message), []);
- const onMetrics = useCallback((value: GlassMetrics) => {
- metrics.current = value;
- (window as Window & { __glassMetrics?: GlassMetrics }).__glassMetrics =
- value;
- }, []);
- const background = backgrounds.find((b) => b.id === state.background);
+ const shareUrl = new URL(location.href);
+ shareUrl.searchParams.set("config", JSON.stringify(shareableState(state)));
+ const shareHref = shareUrl.href;
useEffect(() => {
- const url = new URL(location.href);
- url.searchParams.set("config", JSON.stringify(shareableState(state)));
- history.replaceState(null, "", url);
- }, [state]);
- const code =
- codeTab === "jsx"
- ? componentCode(state)
- : JSON.stringify(
- {
- component: componentProps(state),
- playground: {
- background: shareableState(state).background,
- motion: state.motion,
- example: state.example,
- },
- },
- null,
- 2,
- );
- const copy = async () => {
- let succeeded = false;
- try {
- await navigator.clipboard.writeText(code);
- succeeded = true;
- } catch {
- const textarea = document.createElement("textarea");
- textarea.value = code;
- document.body.append(textarea);
- textarea.select();
- succeeded = document.execCommand("copy");
- textarea.remove();
- }
- setCopied(succeeded);
- if (succeeded) {
- trackDemoEvent({
- name: "playground_code_copied",
- properties: { format: codeTab },
- });
- setTimeout(() => setCopied(false), 1800);
- }
- };
+ history.replaceState(null, "", shareHref);
+ }, [shareHref]);
const shuffle = () => {
- backgroundRevision.current++;
const choices = backgrounds.filter((b) => b.id !== state.background);
const value = new Uint32Array(1);
crypto.getRandomValues(value);
- setState((s) => ({
- ...s,
- background: choices[value[0] % choices.length].id,
- }));
+ chooseBackground(choices[value[0] % choices.length].id);
};
return (
-
-
-
-
+
+
+
+ Skip to playground
+
+
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
A different feeling. Your own.
+
Choose a scene. Shape the glass. Take it with you.
-
- {state.background === "optical-type" ? (
-
- ) : (
-

- )}
-
setClicks((n) => n + 1)
- : undefined
- }
- aria-label={
- state.example === "button" ? "Explore the glass" : undefined
+
+
Get the code
+
void linkCopy.copy(shareHref)}
+ />
+
- {state.example === "button" ? (
- "Explore the glass"
- ) : state.example === "card" ? (
-
-
A new perspective
-
- Your own HTML, seen through glass.
-
-
- ) : state.example === "type" ? (
-
- LOOK
-
- AGAIN.
-
- ) : null}
-
-
-
-
-
Drag the glass · Alt + arrow keys
-
-
-
-
-
- Original artwork, lens proportions and optical preset.
+
+ {linkCopy.status === "failed"
+ ? "Could not copy. Copy the URL from the address bar."
+ : linkCopy.status === "copied"
+ ? "Share link copied."
+ : ""}
+
-
+
+
{
- const selection = ++backgroundRevision.current;
- if (
- (await custom.load(file)) &&
- selection === backgroundRevision.current
- )
- setState((s) => ({ ...s, background: "custom" }));
- }}
- onRemove={() => {
+ onPreset={choosePreset}
+ onReference={() => {
backgroundRevision.current++;
- custom.remove();
- setState((s) => ({
- ...s,
- background:
- s.background === "custom" ? "spectrum" : s.background,
- }));
}}
- onShuffle={shuffle}
+ >
+ {
+ const selection = ++backgroundRevision.current;
+ if (
+ (await custom.load(file)) &&
+ selection === backgroundRevision.current
+ )
+ setState((s) => ({
+ ...s,
+ background: "custom",
+ referenceLayout: false,
+ }));
+ }}
+ onRemove={() => {
+ backgroundRevision.current++;
+ custom.remove();
+ setState((s) => ({
+ ...s,
+ background:
+ s.background === "custom" ? "soft" : s.background,
+ }));
+ }}
+ />
+
+
replaceState(initialState)}
/>
-
-
- {error
- ? "Could not render glass. Reload to try again."
- : !state.settings.enabled
- ? "Glass off · Same background and content."
- : ready
- ? "Glass on · Automatic background."
- : "Preparing glass…"}
-
-
- {state.example === "button"
- ? `Clicks: ${clicks}`
- : "Native HTML content"}
-
-
-
- {
- backgroundRevision.current++;
- setState(next);
- }} />
-
-
-
Code
-
-
-
-
-
-
-
- {code}
-
-
- The background is regular page HTML.{" "}
- Package: react-glaze.
-
-
-
+
+
+
+
+
+
+
+
+
+
);
}
diff --git a/apps/demo/src/components/playground/BackgroundPicker.tsx b/apps/demo/src/components/playground/BackgroundPicker.tsx
index 8a58138..cb7ba7e 100644
--- a/apps/demo/src/components/playground/BackgroundPicker.tsx
+++ b/apps/demo/src/components/playground/BackgroundPicker.tsx
@@ -1,9 +1,11 @@
+import { GlassDetails } from "./GlassDetails";
+import { GlassButton } from "./GlassButton";
/* eslint-disable @next/next/no-img-element -- Uploaded thumbnails use browser-local image sources. */
import { useRef } from "react";
import { backgrounds } from "./config";
import type { BackgroundId } from "./config";
import type { CustomBackground } from "./useCustomBackground";
-import { ShuffleIcon } from "./icons";
+import { materialScenes } from "@/lib/material-scenes";
interface Props {
selected: BackgroundId;
custom: CustomBackground | null;
@@ -27,77 +29,103 @@ export function BackgroundPicker({
const input = useRef(null);
return (
<>
-
-
- {backgrounds.map((b) => (
-
- ))}
- {custom && (
-
-
-
- Content and interaction
+
+
+
+
+
+
+
+
+
@@ -108,7 +122,8 @@ export function Controls({ state, setState, onPreset }: Props) {
}))
}
>
-
+
+
@@ -135,9 +150,8 @@ export function Controls({ state, setState, onPreset }: Props) {
experimental; hit areas still follow the original DOM layout.
-
-
- Advanced material
+
+
@@ -177,7 +191,7 @@ export function Controls({ state, setState, onPreset }: Props) {
model. Shape, proportions and background also affect the appearance.
-
+
);
}
diff --git a/apps/demo/src/components/playground/CopyButton.tsx b/apps/demo/src/components/playground/CopyButton.tsx
new file mode 100644
index 0000000..25c9fc2
--- /dev/null
+++ b/apps/demo/src/components/playground/CopyButton.tsx
@@ -0,0 +1,33 @@
+import type { MouseEventHandler } from "react";
+import { GlassButton } from "./GlassButton";
+import { CheckIcon, CopyIcon } from "./icons";
+
+export function CopyButton({
+ label,
+ copied,
+ onClick,
+ className = "",
+}: {
+ label: string;
+ copied: boolean;
+ onClick: MouseEventHandler
;
+ className?: string;
+}) {
+ return (
+
+
+
+
+
+
+
+
+
+ {label}
+
+ );
+}
diff --git a/apps/demo/src/components/playground/GlassButton.tsx b/apps/demo/src/components/playground/GlassButton.tsx
new file mode 100644
index 0000000..4bff03c
--- /dev/null
+++ b/apps/demo/src/components/playground/GlassButton.tsx
@@ -0,0 +1,81 @@
+import { useCallback, useState } from "react";
+import type { ComponentPropsWithRef, ReactNode } from "react";
+import { LiquidGlass } from "react-glaze";
+import type { GlassOwnProps } from "react-glaze";
+import { useControlMaterial } from "./ControlMaterial";
+import { CheckIcon } from "./icons";
+
+const controlMaterial = {
+ preset: "quiet",
+ contentMode: "sharp",
+ lighting: "studio",
+ thickness: 0.24,
+ depth: 0.16,
+ ior: 1.25,
+ roughness: 0.04,
+ attenuationColor: "#ffffff",
+ dispersion: 0,
+ envMapIntensity: 0.65,
+ shadowOpacity: 0.08,
+ maxDpr: 1.5,
+ rimLight: { mode: "static", strength: 0.3, width: 0.8 },
+} as const satisfies GlassOwnProps;
+
+function useMaterialStatus() {
+ const enabled = useControlMaterial();
+ const [failed, setFailed] = useState(false);
+ const onError = useCallback(() => setFailed(true), []);
+ return { enabled: enabled && !failed, onError };
+}
+
+export function GlassButton({
+ children,
+ className = "",
+ shape = "pill",
+ ...props
+}: Omit, "onError"> & {
+ shape?: "pill" | "rounded";
+}) {
+ const material = useMaterialStatus();
+ const pressed = props["aria-pressed"];
+ return (
+
+
+ {children}
+ {pressed !== undefined && (
+
+
+
+ )}
+
+
+
+ );
+}
+
+export function GlassSummary({ children }: { children: ReactNode }) {
+ const material = useMaterialStatus();
+ return (
+
+ {children}
+
+ );
+}
diff --git a/apps/demo/src/components/playground/GlassDetails.tsx b/apps/demo/src/components/playground/GlassDetails.tsx
new file mode 100644
index 0000000..f257b1c
--- /dev/null
+++ b/apps/demo/src/components/playground/GlassDetails.tsx
@@ -0,0 +1,44 @@
+import { useState } from "react";
+import type { ReactNode } from "react";
+import { GlassSummary } from "./GlassButton";
+import { ChevronIcon } from "./icons";
+
+export function GlassDetails({
+ title,
+ description,
+ className,
+ children,
+}: {
+ title: string;
+ description?: string;
+ className: string;
+ children: ReactNode;
+}) {
+ const [open, setOpen] = useState(false);
+ return (
+ setOpen(event.currentTarget.open)}
+ >
+
+
+ {title}
+ {description && {description}}
+
+
+
+
+
+
+
+
+
+
+ {children}
+
+ );
+}
diff --git a/apps/demo/src/components/playground/GlassPreview.tsx b/apps/demo/src/components/playground/GlassPreview.tsx
new file mode 100644
index 0000000..f73404f
--- /dev/null
+++ b/apps/demo/src/components/playground/GlassPreview.tsx
@@ -0,0 +1,271 @@
+import { GlassDetails } from "./GlassDetails";
+import { GlassButton } from "./GlassButton";
+import Image from "next/image";
+import { useCallback, useEffect, useRef, useState } from "react";
+import type { Dispatch, ReactNode, SetStateAction } from "react";
+import { LiquidGlass, PRESETS } from "react-glaze";
+import type { GlassMetrics, GlassPreset } from "react-glaze";
+import { PillLabel } from "../landing/PillLabel";
+import { backgrounds, componentProps, getExampleStyle } from "./config";
+import type { StudioState } from "./config";
+import type { CustomBackground } from "./useCustomBackground";
+import { useGlassDrag } from "./useGlassDrag";
+import { OpticalBackdrop } from "./OpticalBackdrop";
+
+export function GlassPreview({
+ state,
+ setState,
+ custom,
+ onPreset,
+ onReference,
+ children,
+}: {
+ state: StudioState;
+ setState: Dispatch>;
+ custom: CustomBackground | null;
+ onPreset: (preset: GlassPreset) => void;
+ onReference: () => void;
+ children: ReactNode;
+}) {
+ const [clicks, setClicks] = useState(0);
+ const [ready, setReady] = useState(false);
+ const [error, setError] = useState("");
+ const [loadedImage, setLoadedImage] = useState("");
+ const [failedImage, setFailedImage] = useState("");
+ const isLandingPill = state.example === "landing-pill";
+ const isButton = isLandingPill || state.example === "button";
+ const {
+ stage: stageRef,
+ host: glassRef,
+ position,
+ dragging,
+ reset: resetPosition,
+ moveTo,
+ bindings,
+ } = useGlassDrag(state.example);
+ const matchOpticalLayout = useCallback(() => {
+ const r = stageRef.current?.getBoundingClientRect();
+ if (!r) return;
+ const portrait = r.width < r.height,
+ width = r.width * ((2 * 1.42 * (portrait ? 1.1 : 1.08)) / 9.7),
+ height = width * (portrait ? 1.08 / 1.1 : 0.97 / 1.08);
+ setState((s) =>
+ Math.abs(Number(s.width) - width) < 0.01 &&
+ Math.abs(Number(s.height) - height) < 0.01
+ ? s
+ : { ...s, width, height },
+ );
+ moveTo({
+ x: 0,
+ y: portrait ? r.height * 0.08 : (-r.width * 0.12) / 9.7,
+ });
+ }, [stageRef, moveTo, setState]);
+ const matchReference = () => {
+ onReference();
+ setState((s) => ({
+ ...s,
+ preset: "optical-type",
+ customShape: false,
+ settings: { ...PRESETS["optical-type"], enabled: s.settings.enabled },
+ rimLight: false,
+ background: "optical-type",
+ example: "empty",
+ motion: false,
+ referenceLayout: true,
+ }));
+ matchOpticalLayout();
+ };
+ useEffect(() => {
+ if (!state.referenceLayout || !stageRef.current) return;
+ const observer = new ResizeObserver(matchOpticalLayout);
+ observer.observe(stageRef.current);
+ matchOpticalLayout();
+ return () => observer.disconnect();
+ }, [state.referenceLayout, stageRef, matchOpticalLayout]);
+ const metrics = useRef(null);
+ const onReady = useCallback(() => {
+ setReady(true);
+ setError("");
+ }, []);
+ const onError = useCallback((error: Error) => setError(error.message), []);
+ const onMetrics = useCallback((value: GlassMetrics) => {
+ metrics.current = value;
+ (window as Window & { __glassMetrics?: GlassMetrics }).__glassMetrics =
+ value;
+ }, []);
+ const background = backgrounds.find((b) => b.id === state.background);
+ const imageSrc =
+ (state.background === "custom" ? custom?.src : background?.src) ?? "";
+ const imageFailed = imageSrc !== "" && failedImage === imageSrc;
+ const imageLoading =
+ state.background !== "optical-type" && loadedImage !== imageSrc;
+ return (
+
+
+
+
+ setState((s) => ({
+ ...s,
+ settings: { ...s.settings, enabled: true },
+ }))
+ }
+ >
+ With glass
+
+
+ setState((s) => ({
+ ...s,
+ settings: { ...s.settings, enabled: false },
+ }))
+ }
+ >
+ Without glass
+
+
+
+
+
+ {state.background === "optical-type" ? (
+
+ ) : (
+
setFailedImage(imageSrc)}
+ onLoad={() => {
+ setLoadedImage(imageSrc);
+ setFailedImage("");
+ }}
+ src={imageSrc}
+ alt=""
+ aria-hidden="true"
+ />
+ )}
+ {
+ setClicks((n) => n + 1);
+ if (isLandingPill)
+ onPreset(
+ state.preset === "frosted" ? "reference" : "frosted",
+ );
+ }
+ : undefined
+ }
+ aria-label={
+ isLandingPill
+ ? "A different feeling. Toggle frosted glass"
+ : state.example === "button"
+ ? "Explore the glass"
+ : "Glass preview"
+ }
+ >
+ {isLandingPill ? (
+
+ ) : state.example === "button" ? (
+ "Explore the glass"
+ ) : state.example === "card" ? (
+
+
A new perspective
+
Your own HTML, seen through glass.
+
+ ) : state.example === "type" ? (
+
+ LOOK
+
+ AGAIN.
+
+ ) : null}
+
+
+
+
+
+ Drag to explore.{isLandingPill ? " Click to frost." : ""}
+ Alt + arrows to move.
+
+
+ Center
+
+
+
+
+ {imageFailed
+ ? "Image could not load. Choose another scene or reload."
+ : error
+ ? "Could not render glass. Reload to try again."
+ : imageLoading
+ ? "Loading scene..."
+ : !state.settings.enabled
+ ? "Glass off"
+ : ready
+ ? state.preset === "frosted"
+ ? "Frosted glass"
+ : "Glass ready"
+ : "Preparing glass..."}
+
+
+ {isButton ? `Clicks: ${clicks}` : "Native HTML content"}
+
+
+ {children}
+
+ {" "}
+
+ Match Optical Type
+ Original artwork, lens proportions and optical preset.
+
+
+
+ );
+}
diff --git a/apps/demo/src/components/playground/SavedConfiguration.tsx b/apps/demo/src/components/playground/SavedConfiguration.tsx
index 190de0e..b8e8d9c 100644
--- a/apps/demo/src/components/playground/SavedConfiguration.tsx
+++ b/apps/demo/src/components/playground/SavedConfiguration.tsx
@@ -1,51 +1,94 @@
-import { useEffect, useState } from 'react';
-import type { StudioState } from './config';
-import { SAVED_CONFIG_KEY, readSavedConfig, saveConfig, deleteSavedConfig } from './saved-config';
+import { GlassButton } from "./GlassButton";
+import { useEffect, useState } from "react";
+import type { StudioState } from "./config";
+import {
+ SAVED_CONFIG_KEY,
+ readSavedConfig,
+ saveConfig,
+ deleteSavedConfig,
+} from "./saved-config";
-export function SavedConfiguration({ state, onLoad }: {
+export function SavedConfiguration({
+ state,
+ onLoad,
+}: {
state: StudioState;
onLoad: (state: StudioState) => void;
}) {
const [saved, setSaved] = useState(readSavedConfig);
- const [message, setMessage] = useState('');
+ const [message, setMessage] = useState("");
useEffect(() => {
const refresh = (event: StorageEvent) => {
if (event.key === SAVED_CONFIG_KEY || event.key === null) {
setSaved(readSavedConfig());
- setMessage('');
+ setMessage("");
}
};
- window.addEventListener('storage', refresh);
- return () => window.removeEventListener('storage', refresh);
+ window.addEventListener("storage", refresh);
+ return () => window.removeEventListener("storage", refresh);
}, []);
return (
Saved configuration
-
Save for your next visit. Shared links take priority. Uploaded images are not saved.
+
+ Save for your next visit. Shared links take priority. Uploaded images
+ are not saved.
+
- {
- const error = saveConfig(state);
- if (!error) setSaved(readSavedConfig());
- setMessage(error ?? (state.background === 'custom'
- ? 'Configuration saved. The uploaded image is replaced by Spectrum when loaded.'
- : 'Configuration saved in this browser.'));
- }}>Save configuration
- {
- const next = readSavedConfig();
- setSaved(next);
- if (next.state) onLoad(next.state);
- setMessage(next.error ?? (next.state ? 'Saved configuration loaded.' : 'No saved configuration.'));
- }}>Load saved
- {
- const error = deleteSavedConfig();
- if (!error) setSaved({ state: null, exists: false, error: null });
- setMessage(error ?? 'Saved configuration deleted. Your current preview is unchanged.');
- }}>Delete saved
+ {
+ const error = saveConfig(state);
+ if (!error) setSaved(readSavedConfig());
+ setMessage(
+ error ??
+ (state.background === "custom"
+ ? "Configuration saved. The uploaded image is replaced by Spectrum when loaded."
+ : "Configuration saved in this browser."),
+ );
+ }}
+ >
+ Save configuration
+
+ {
+ const next = readSavedConfig();
+ setSaved(next);
+ if (next.state) onLoad(next.state);
+ setMessage(
+ next.error ??
+ (next.state
+ ? "Saved configuration loaded."
+ : "No saved configuration."),
+ );
+ }}
+ >
+ Load saved
+
+ {
+ const error = deleteSavedConfig();
+ if (!error) setSaved({ state: null, exists: false, error: null });
+ setMessage(
+ error ??
+ "Saved configuration deleted. Your current preview is unchanged.",
+ );
+ }}
+ >
+ Delete saved
+
- {message || saved.error || (saved.state ? 'A saved configuration is available.' : 'No saved configuration yet.')}
+
+ {message ||
+ saved.error ||
+ (saved.state
+ ? "A saved configuration is available."
+ : "No saved configuration yet.")}
+
);
}
diff --git a/apps/demo/src/components/playground/ShapeControls.tsx b/apps/demo/src/components/playground/ShapeControls.tsx
index 95c8930..5b25603 100644
--- a/apps/demo/src/components/playground/ShapeControls.tsx
+++ b/apps/demo/src/components/playground/ShapeControls.tsx
@@ -1,3 +1,4 @@
+import { GlassButton } from "./GlassButton";
import { hasFixedRadius, isRatioShape } from "react-glaze";
import type { GlassShape } from "react-glaze";
import type { Dispatch, SetStateAction } from "react";
@@ -40,7 +41,9 @@ function DimensionControl({
{typeof value === "number" && (
@@ -140,18 +144,18 @@ export function ShapeControls({
{hasFixedRadius(shape) ? (
This shape sets its corner radius.{" "}
- setShape("custom")}
>
Customize corners
-
+
) : (