@jasonbelmonti/signal-ui 0.9.0 → 0.10.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.
Files changed (41) hide show
  1. package/README.md +12 -0
  2. package/dist/src/components/FullScreenWipe.d.ts +13 -0
  3. package/dist/src/components/FullScreenWipe.js +38 -0
  4. package/dist/src/components/GlitchGhost.d.ts +24 -0
  5. package/dist/src/components/GlitchGhost.js +120 -0
  6. package/dist/src/components/Panel.d.ts +15 -1
  7. package/dist/src/components/Panel.js +73 -11
  8. package/dist/src/components/fullScreenWipe/renderProceduralPixelWipeBuffer.d.ts +14 -0
  9. package/dist/src/components/fullScreenWipe/renderProceduralPixelWipeBuffer.js +65 -0
  10. package/dist/src/components/fullScreenWipe/useFullScreenWipeState.d.ts +14 -0
  11. package/dist/src/components/fullScreenWipe/useFullScreenWipeState.js +47 -0
  12. package/dist/src/components/fullScreenWipe/useProceduralPixelWipeCanvas.d.ts +12 -0
  13. package/dist/src/components/fullScreenWipe/useProceduralPixelWipeCanvas.js +147 -0
  14. package/dist/src/components/glitchGhost/hasCanvasSnapshotSource.d.ts +3 -0
  15. package/dist/src/components/glitchGhost/hasCanvasSnapshotSource.js +3 -0
  16. package/dist/src/components/glitchGhost/hasCustomGhostContent.d.ts +2 -0
  17. package/dist/src/components/glitchGhost/hasCustomGhostContent.js +13 -0
  18. package/dist/src/components/glitchGhost/rewriteGhostReferenceValue.d.ts +1 -0
  19. package/dist/src/components/glitchGhost/rewriteGhostReferenceValue.js +39 -0
  20. package/dist/src/components/glitchGhost/snapshot.d.ts +5 -0
  21. package/dist/src/components/glitchGhost/snapshot.js +130 -0
  22. package/dist/src/components/panel/PanelRevealCanvas.d.ts +8 -0
  23. package/dist/src/components/panel/PanelRevealCanvas.js +12 -0
  24. package/dist/src/components/panel/renderPanelRevealBuffer.d.ts +13 -0
  25. package/dist/src/components/panel/renderPanelRevealBuffer.js +129 -0
  26. package/dist/src/components/panel/usePanelCursorTilt.d.ts +14 -0
  27. package/dist/src/components/panel/usePanelCursorTilt.js +91 -0
  28. package/dist/src/components/panel/usePanelRevealCanvas.d.ts +10 -0
  29. package/dist/src/components/panel/usePanelRevealCanvas.js +139 -0
  30. package/dist/src/components/panel/usePanelRevealState.d.ts +17 -0
  31. package/dist/src/components/panel/usePanelRevealState.js +90 -0
  32. package/dist/src/components/panel/usePanelShellRevealState.d.ts +15 -0
  33. package/dist/src/components/panel/usePanelShellRevealState.js +84 -0
  34. package/dist/src/components/signalChat/signalChatTheme.d.ts +1 -1
  35. package/dist/src/index.d.ts +3 -1
  36. package/dist/src/index.js +1 -0
  37. package/dist/styles/fullScreenWipe.css +316 -0
  38. package/dist/styles/panel.css +254 -0
  39. package/dist/styles/theme.css +1133 -77
  40. package/dist/styles.css +2 -0
  41. package/package.json +1 -1
package/README.md CHANGED
@@ -11,10 +11,22 @@ bun run typecheck
11
11
  bun run build
12
12
  ```
13
13
 
14
+ The Storybook catalog is organized in this order:
15
+
16
+ 1. `Overview` – landing, getting started, and catalog context.
17
+ 2. `Foundations` – baseline visual and token references.
18
+ 3. `Components` – primary reusable components.
19
+ 4. `Recipes` – recommended patterns and composition examples for real product flows.
20
+ 5. `Lab` – secondary experimental or edge-case surfaces.
21
+
14
22
  Pushes to `main` also publish the static Storybook to GitHub Pages via
15
23
  `.github/workflows/storybook-pages.yml`, so the theme can be shared without a local build.
16
24
  The default URL for this repository is `https://jasonbelmonti.github.io/signal-ui/`.
17
25
 
26
+ `Recipes` and `Lab` remain in the catalog intentionally, but they are secondary to
27
+ `Foundations` and `Components` so designers and integrators can quickly separate stable
28
+ reference surfaces from exploratory patterns.
29
+
18
30
  `bun run build` emits the consumable package contract in `dist/`:
19
31
 
20
32
  - `dist/index.js`
@@ -0,0 +1,13 @@
1
+ import type { HTMLAttributes, ReactNode } from "react";
2
+ export type FullScreenWipeVariant = "flat-iris" | "procedural-pixel";
3
+ export type FullScreenWipeState = "open" | "closed";
4
+ export interface FullScreenWipeProps extends HTMLAttributes<HTMLDivElement> {
5
+ accentColor?: string;
6
+ children?: ReactNode;
7
+ coverColor?: string;
8
+ durationMs?: number;
9
+ overlayLabel?: string;
10
+ state?: FullScreenWipeState;
11
+ variant?: FullScreenWipeVariant;
12
+ }
13
+ export declare function FullScreenWipe({ accentColor, children, className, coverColor, durationMs, overlayLabel, state, style, variant, ...divProps }: FullScreenWipeProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,38 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useRef } from "react";
3
+ import { joinClassNames } from "../utils/joinClassNames.js";
4
+ import { getFullScreenWipePhaseDurationMs, useFullScreenWipeState } from "./fullScreenWipe/useFullScreenWipeState.js";
5
+ import { useProceduralPixelWipeCanvas } from "./fullScreenWipe/useProceduralPixelWipeCanvas.js";
6
+ const DEFAULT_DURATION_MS = 140;
7
+ export function FullScreenWipe({ accentColor = "var(--signal-ui-primary)", children, className, coverColor = "rgb(4 5 8 / 0.98)", durationMs = DEFAULT_DURATION_MS, overlayLabel, state = "open", style, variant = "flat-iris", ...divProps }) {
8
+ const canvasRef = useRef(null);
9
+ const { blocksInteraction, phase, reducedMotion, renderedState } = useFullScreenWipeState({
10
+ durationMs,
11
+ state,
12
+ });
13
+ const openingDurationMs = getFullScreenWipePhaseDurationMs(durationMs, "opening");
14
+ const closingDurationMs = getFullScreenWipePhaseDurationMs(durationMs, "closing");
15
+ const phaseDurationMs = phase ? getFullScreenWipePhaseDurationMs(durationMs, phase) : durationMs;
16
+ useProceduralPixelWipeCanvas({
17
+ canvasRef,
18
+ durationMs: phaseDurationMs,
19
+ phase: variant === "procedural-pixel" ? phase : undefined,
20
+ reducedMotion,
21
+ state: variant === "procedural-pixel" ? state : "open",
22
+ });
23
+ const rootStyle = {
24
+ ...style,
25
+ "--signal-ui-full-screen-wipe-accent": accentColor,
26
+ "--signal-ui-full-screen-wipe-closing-duration": `${closingDurationMs}ms`,
27
+ "--signal-ui-full-screen-wipe-cover": coverColor,
28
+ "--signal-ui-full-screen-wipe-duration": `${durationMs}ms`,
29
+ "--signal-ui-full-screen-wipe-opening-duration": `${openingDurationMs}ms`,
30
+ };
31
+ return (_jsxs("div", { ...divProps, className: joinClassNames("signal-ui-full-screen-wipe", className), "data-signal-ui-full-screen-wipe-blocking": blocksInteraction ? "true" : "false", "data-signal-ui-full-screen-wipe-phase": phase, "data-signal-ui-full-screen-wipe-rendered-state": renderedState, "data-signal-ui-full-screen-wipe-state": state, "data-signal-ui-full-screen-wipe-variant": variant, style: rootStyle, children: [_jsx("div", { "aria-hidden": blocksInteraction, className: "signal-ui-full-screen-wipe__content", inert: blocksInteraction ? true : undefined, children: children }), _jsxs("div", { "aria-hidden": "true", className: "signal-ui-full-screen-wipe__overlay", children: [_jsx("div", { className: "signal-ui-full-screen-wipe__scan" }), variant === "flat-iris" ? (_jsxs("div", { className: "signal-ui-full-screen-wipe__flat-iris", children: [flatIrisPanelClassNames.map((panelClassName) => (_jsx("span", { className: joinClassNames("signal-ui-full-screen-wipe__panel", panelClassName) }, panelClassName))), _jsx("span", { className: "signal-ui-full-screen-wipe__iris-core" })] })) : (_jsx("div", { className: "signal-ui-full-screen-wipe__procedural", children: _jsx("canvas", { className: "signal-ui-full-screen-wipe__canvas", ref: canvasRef }) })), overlayLabel ? (_jsx("div", { className: "signal-ui-full-screen-wipe__label", children: _jsx("span", { children: overlayLabel }) })) : null] })] }));
32
+ }
33
+ const flatIrisPanelClassNames = [
34
+ "signal-ui-full-screen-wipe__panel--northwest",
35
+ "signal-ui-full-screen-wipe__panel--northeast",
36
+ "signal-ui-full-screen-wipe__panel--southwest",
37
+ "signal-ui-full-screen-wipe__panel--southeast",
38
+ ];
@@ -0,0 +1,24 @@
1
+ import type { ComponentPropsWithoutRef, ReactNode } from "react";
2
+ export type GlitchGhostMask = "signal" | "lattice";
3
+ export type GlitchGhostBlendMode = "screen" | "plus-lighter" | "exclusion";
4
+ type CssLength = number | string;
5
+ type CssAngle = number | string;
6
+ export interface GlitchGhostProps extends Omit<ComponentPropsWithoutRef<"div">, "children"> {
7
+ animated?: boolean;
8
+ blendMode?: GlitchGhostBlendMode;
9
+ blur?: CssLength;
10
+ children: ReactNode;
11
+ depth?: CssLength;
12
+ ghost?: ReactNode;
13
+ ghostColor?: string;
14
+ ghostCount?: 1 | 2 | 3;
15
+ ghostOpacity?: number;
16
+ mask?: GlitchGhostMask;
17
+ offsetX?: CssLength;
18
+ offsetY?: CssLength;
19
+ perspective?: CssLength;
20
+ tiltX?: CssAngle;
21
+ tiltY?: CssAngle;
22
+ }
23
+ export declare function GlitchGhost({ animated, blendMode, blur, children, className, depth, ghost, ghostColor, ghostCount, ghostOpacity, mask, offsetX, offsetY, perspective, style, tiltX, tiltY, ...props }: GlitchGhostProps): import("react/jsx-runtime").JSX.Element;
24
+ export {};
@@ -0,0 +1,120 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useEffect, useEffectEvent, useRef } from "react";
3
+ import { hasCanvasSnapshotSource } from "./glitchGhost/hasCanvasSnapshotSource.js";
4
+ import { hasCustomGhostContent } from "./glitchGhost/hasCustomGhostContent.js";
5
+ import { syncGhostSnapshotLayers } from "./glitchGhost/snapshot.js";
6
+ import { joinClassNames } from "../utils/joinClassNames.js";
7
+ const glitchGhostLayers = [
8
+ "signal-ui-glitch-ghost__ghost--lead",
9
+ "signal-ui-glitch-ghost__ghost--trail",
10
+ "signal-ui-glitch-ghost__ghost--echo",
11
+ ];
12
+ function toCssLength(value, fallback) {
13
+ if (value === undefined) {
14
+ return fallback;
15
+ }
16
+ return typeof value === "number" ? `${value}px` : value;
17
+ }
18
+ function toCssAngle(value, fallback) {
19
+ if (value === undefined) {
20
+ return fallback;
21
+ }
22
+ return typeof value === "number" ? `${value}deg` : value;
23
+ }
24
+ export function GlitchGhost({ animated = true, blendMode = "screen", blur = 0.85, children, className, depth = 18, ghost, ghostColor = "rgb(var(--signal-ui-primary-rgb) / 0.74)", ghostCount = 3, ghostOpacity = 0.42, mask = "signal", offsetX = 12, offsetY = 7, perspective = 580, style, tiltX = 7, tiltY = -8, ...props }) {
25
+ const mainRef = useRef(null);
26
+ const ghostCopyRefs = useRef([]);
27
+ const hasCustomGhost = hasCustomGhostContent(ghost);
28
+ const ghostStyle = {
29
+ ...style,
30
+ "--signal-ui-glitch-ghost-blend-mode": blendMode,
31
+ "--signal-ui-glitch-ghost-blur": toCssLength(blur, "0.85px"),
32
+ "--signal-ui-glitch-ghost-color": ghostColor,
33
+ "--signal-ui-glitch-ghost-depth": toCssLength(depth, "18px"),
34
+ "--signal-ui-glitch-ghost-offset-x": toCssLength(offsetX, "12px"),
35
+ "--signal-ui-glitch-ghost-offset-y": toCssLength(offsetY, "7px"),
36
+ "--signal-ui-glitch-ghost-opacity": ghostOpacity,
37
+ "--signal-ui-glitch-ghost-perspective": toCssLength(perspective, "580px"),
38
+ "--signal-ui-glitch-ghost-tilt-x": toCssAngle(tiltX, "7deg"),
39
+ "--signal-ui-glitch-ghost-tilt-y": toCssAngle(tiltY, "-8deg"),
40
+ };
41
+ const syncGhostSnapshots = useEffectEvent(() => {
42
+ if (hasCustomGhost) {
43
+ return;
44
+ }
45
+ const mainElement = mainRef.current;
46
+ const targetElements = ghostCopyRefs.current
47
+ .slice(0, ghostCount)
48
+ .filter((element) => element !== null);
49
+ if (!mainElement || targetElements.length === 0) {
50
+ return;
51
+ }
52
+ syncGhostSnapshotLayers(mainElement, targetElements);
53
+ });
54
+ useEffect(() => {
55
+ ghostCopyRefs.current.length = ghostCount;
56
+ if (hasCustomGhost || typeof window === "undefined") {
57
+ return undefined;
58
+ }
59
+ const mainElement = mainRef.current;
60
+ if (!mainElement) {
61
+ return undefined;
62
+ }
63
+ let animationFrameId = null;
64
+ let continuousSyncFrameId = null;
65
+ const stopContinuousSync = () => {
66
+ if (continuousSyncFrameId === null) {
67
+ return;
68
+ }
69
+ window.cancelAnimationFrame(continuousSyncFrameId);
70
+ continuousSyncFrameId = null;
71
+ };
72
+ const updateContinuousSync = () => {
73
+ if (!hasCanvasSnapshotSource(mainElement)) {
74
+ stopContinuousSync();
75
+ return;
76
+ }
77
+ if (continuousSyncFrameId !== null) {
78
+ return;
79
+ }
80
+ continuousSyncFrameId = window.requestAnimationFrame(function syncCanvasGhostFrame() {
81
+ continuousSyncFrameId = null;
82
+ syncGhostSnapshots();
83
+ updateContinuousSync();
84
+ });
85
+ };
86
+ const scheduleSnapshotSync = () => {
87
+ if (animationFrameId !== null) {
88
+ return;
89
+ }
90
+ animationFrameId = window.requestAnimationFrame(() => {
91
+ animationFrameId = null;
92
+ syncGhostSnapshots();
93
+ updateContinuousSync();
94
+ });
95
+ };
96
+ scheduleSnapshotSync();
97
+ const observer = new MutationObserver(() => {
98
+ scheduleSnapshotSync();
99
+ });
100
+ observer.observe(mainElement, {
101
+ attributes: true,
102
+ characterData: true,
103
+ childList: true,
104
+ subtree: true,
105
+ });
106
+ return () => {
107
+ observer.disconnect();
108
+ stopContinuousSync();
109
+ if (animationFrameId !== null) {
110
+ window.cancelAnimationFrame(animationFrameId);
111
+ }
112
+ for (const ghostCopyElement of ghostCopyRefs.current) {
113
+ ghostCopyElement?.replaceChildren();
114
+ }
115
+ };
116
+ }, [ghostCount, hasCustomGhost, syncGhostSnapshots]);
117
+ return (_jsxs("div", { className: joinClassNames("signal-ui-glitch-ghost", animated && "signal-ui-glitch-ghost--animated", className), "data-mask": mask, style: ghostStyle, ...props, children: [_jsx("div", { "aria-hidden": "true", className: "signal-ui-glitch-ghost__layers", children: glitchGhostLayers.slice(0, ghostCount).map((layerClassName, index) => (_jsx("div", { inert: true, className: joinClassNames("signal-ui-glitch-ghost__ghost", layerClassName), children: _jsx("div", { ref: (element) => {
118
+ ghostCopyRefs.current[index] = element;
119
+ }, className: "signal-ui-glitch-ghost__copy", children: hasCustomGhost ? ghost : null }) }, layerClassName))) }), _jsx("div", { ref: mainRef, className: "signal-ui-glitch-ghost__main", children: children })] }));
120
+ }
@@ -2,6 +2,11 @@ import type { CardProps } from "antd";
2
2
  export type PanelCutCorner = "accent" | "notch";
3
3
  export type PanelCutCornerPreset = "tactical" | "architectural";
4
4
  export type PanelFrame = "reticle";
5
+ export type PanelSurface = "glass";
6
+ export type PanelReveal = "holographic";
7
+ export type PanelRevealIntro = "point";
8
+ export type PanelRevealOutro = "point";
9
+ export type PanelRevealState = "open" | "closed" | "hidden";
5
10
  export type PanelCutCornerPlacement = "top-left" | "top-right" | "bottom-left" | "bottom-right";
6
11
  export interface PanelProps extends CardProps {
7
12
  cutCornerPreset?: PanelCutCornerPreset;
@@ -12,6 +17,15 @@ export interface PanelProps extends CardProps {
12
17
  frame?: PanelFrame;
13
18
  frameColor?: string;
14
19
  frameSize?: number | string;
20
+ surface?: PanelSurface;
21
+ surfaceColor?: string;
22
+ surfaceBlur?: number | string;
23
+ reveal?: PanelReveal;
24
+ revealColor?: string;
25
+ revealIntro?: PanelRevealIntro;
26
+ revealOutro?: PanelRevealOutro;
27
+ revealState?: PanelRevealState;
28
+ cursorTilt?: boolean;
15
29
  }
16
30
  export declare const panelCutCornerPresets: {
17
31
  tactical: {
@@ -27,4 +41,4 @@ export declare const panelCutCornerPresets: {
27
41
  cutCornerSize: number;
28
42
  };
29
43
  };
30
- export declare function Panel({ className, cutCorner, cutCornerColor, cutCornerPreset, cutCornerPlacement, cutCornerSize, frame, frameColor, frameSize, style, ...cardProps }: PanelProps): import("react/jsx-runtime").JSX.Element;
44
+ export declare function Panel({ className, cursorTilt, cutCorner, cutCornerColor, cutCornerPreset, cutCornerPlacement, cutCornerSize, frame, frameColor, frameSize, surface, surfaceColor, surfaceBlur, reveal, revealColor, revealIntro, revealOutro, revealState, style, onPointerCancel, onPointerLeave, onPointerMove, ...cardProps }: PanelProps): import("react/jsx-runtime").JSX.Element;
@@ -1,5 +1,10 @@
1
- import { jsx as _jsx } from "react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { Card } from "antd";
3
+ import { joinClassNames } from "../utils/joinClassNames.js";
4
+ import { PanelRevealCanvas } from "./panel/PanelRevealCanvas.js";
5
+ import { usePanelCursorTilt } from "./panel/usePanelCursorTilt.js";
6
+ import { usePanelRevealState as usePanelPixelRevealState } from "./panel/usePanelRevealState.js";
7
+ import { usePanelShellRevealState } from "./panel/usePanelShellRevealState.js";
3
8
  export const panelCutCornerPresets = {
4
9
  tactical: {
5
10
  cutCorner: "accent",
@@ -14,21 +19,33 @@ export const panelCutCornerPresets = {
14
19
  cutCornerSize: 24,
15
20
  },
16
21
  };
17
- function toCssLength(value) {
18
- if (value === undefined) {
19
- return undefined;
20
- }
21
- return typeof value === "number" ? `${value}px` : value;
22
- }
23
- function joinClassNames(...classNames) {
24
- return classNames.filter(Boolean).join(" ");
22
+ function getPanelRevealCssVariables(style) {
23
+ return Object.fromEntries(Object.entries(style).filter(([propertyName]) => propertyName.startsWith("--signal-ui-panel-reveal-")));
25
24
  }
26
- export function Panel({ className, cutCorner, cutCornerColor, cutCornerPreset, cutCornerPlacement, cutCornerSize, frame, frameColor, frameSize, style, ...cardProps }) {
25
+ export function Panel({ className, cursorTilt, cutCorner, cutCornerColor, cutCornerPreset, cutCornerPlacement, cutCornerSize, frame, frameColor, frameSize, surface, surfaceColor, surfaceBlur, reveal, revealColor, revealIntro, revealOutro, revealState = "open", style, onPointerCancel, onPointerLeave, onPointerMove, ...cardProps }) {
27
26
  const preset = cutCornerPreset ? panelCutCornerPresets[cutCornerPreset] : undefined;
28
27
  const resolvedCutCorner = cutCorner ?? preset?.cutCorner;
29
28
  const resolvedCutCornerColor = cutCornerColor ?? preset?.cutCornerColor ?? "var(--signal-ui-primary)";
30
29
  const resolvedCutCornerPlacement = cutCornerPlacement ?? preset?.cutCornerPlacement ?? "top-right";
31
30
  const resolvedCutCornerSize = cutCornerSize ?? preset?.cutCornerSize ?? 26;
31
+ const resolvedSurfaceColor = surfaceColor ?? frameColor ?? resolvedCutCornerColor;
32
+ const resolvedRevealColor = revealColor ?? resolvedSurfaceColor;
33
+ const { renderedRevealState, revealPhase } = usePanelShellRevealState({
34
+ reveal,
35
+ revealIntro,
36
+ revealOutro,
37
+ revealState,
38
+ });
39
+ const shouldRenderPixelReveal = reveal === "holographic" && surface === "glass";
40
+ const { renderedRevealState: renderedPixelRevealState, revealPhase: pixelRevealPhase } = usePanelPixelRevealState({
41
+ reveal: shouldRenderPixelReveal ? reveal : undefined,
42
+ revealIntro,
43
+ revealOutro,
44
+ revealState,
45
+ });
46
+ const { surfacePointerHandlers, surfaceRef } = usePanelCursorTilt({
47
+ enabled: cursorTilt,
48
+ });
32
49
  const panelStyle = {
33
50
  ...style,
34
51
  ...(resolvedCutCorner
@@ -43,6 +60,51 @@ export function Panel({ className, cutCorner, cutCornerColor, cutCornerPreset, c
43
60
  "--signal-ui-panel-reticle-size": toCssLength(frameSize) ?? "28px",
44
61
  }
45
62
  : {}),
63
+ ...(surface === "glass"
64
+ ? {
65
+ "--signal-ui-panel-surface-color": resolvedSurfaceColor,
66
+ "--signal-ui-panel-surface-blur": toCssLength(surfaceBlur) ?? "10px",
67
+ }
68
+ : {}),
69
+ ...(reveal === "holographic"
70
+ ? {
71
+ "--signal-ui-panel-reveal-color": resolvedRevealColor,
72
+ }
73
+ : {}),
74
+ };
75
+ const panelCard = (_jsx(Card, { ...cardProps, onPointerCancel: reveal === "holographic"
76
+ ? onPointerCancel
77
+ : cursorTilt
78
+ ? composeEventHandlers(onPointerCancel, surfacePointerHandlers.onPointerCancel)
79
+ : onPointerCancel, onPointerLeave: reveal === "holographic"
80
+ ? onPointerLeave
81
+ : cursorTilt
82
+ ? composeEventHandlers(onPointerLeave, surfacePointerHandlers.onPointerLeave)
83
+ : onPointerLeave, onPointerMove: reveal === "holographic"
84
+ ? onPointerMove
85
+ : cursorTilt
86
+ ? composeEventHandlers(onPointerMove, surfacePointerHandlers.onPointerMove)
87
+ : onPointerMove, className: joinClassNames("signal-ui-panel", cursorTilt && reveal !== "holographic" ? "signal-ui-panel--cursor-tilt" : undefined, frame && `signal-ui-panel--frame-${frame}`, surface && `signal-ui-panel--surface-${surface}`, resolvedCutCorner && `signal-ui-panel--cut-${resolvedCutCorner}`, resolvedCutCorner && `signal-ui-panel--corner-${resolvedCutCornerPlacement}`, reveal && `signal-ui-panel--reveal-${reveal}`, className), ref: cursorTilt && reveal !== "holographic" ? surfaceRef : undefined, style: panelStyle }));
88
+ if (reveal !== "holographic") {
89
+ return panelCard;
90
+ }
91
+ const shellStyle = {
92
+ ...getPanelRevealCssVariables(panelStyle),
93
+ "--signal-ui-panel-surface-blur": panelStyle["--signal-ui-panel-surface-blur"],
94
+ "--signal-ui-panel-surface-color": panelStyle["--signal-ui-panel-surface-color"],
46
95
  };
47
- return (_jsx(Card, { ...cardProps, className: joinClassNames("signal-ui-panel", frame ? `signal-ui-panel--frame-${frame}` : undefined, resolvedCutCorner ? `signal-ui-panel--cut-${resolvedCutCorner}` : undefined, resolvedCutCorner ? `signal-ui-panel--corner-${resolvedCutCornerPlacement}` : undefined, className), style: panelStyle }));
96
+ return (_jsxs("div", { onPointerCancel: cursorTilt ? surfacePointerHandlers.onPointerCancel : undefined, onPointerLeave: cursorTilt ? surfacePointerHandlers.onPointerLeave : undefined, onPointerMove: cursorTilt ? surfacePointerHandlers.onPointerMove : undefined, ref: cursorTilt ? surfaceRef : undefined, className: joinClassNames("signal-ui-panel-shell", "signal-ui-panel-shell--reveal-holographic", surface === "glass" ? "signal-ui-panel-shell--surface-glass" : undefined, shouldRenderPixelReveal && "signal-ui-panel-shell--pixel-reveal", cursorTilt ? "signal-ui-panel-shell--cursor-tilt" : undefined), "data-signal-ui-panel-reveal-phase": revealPhase, "data-signal-ui-panel-reveal-state": renderedRevealState, style: shellStyle, children: [_jsx("div", { "aria-hidden": "true", className: "signal-ui-panel-shell__pixels" }), _jsx("div", { "aria-hidden": "true", className: "signal-ui-panel-shell__beam" }), _jsx("div", { "aria-hidden": "true", className: "signal-ui-panel-shell__edges" }), _jsxs("div", { "aria-hidden": renderedRevealState !== "open", className: "signal-ui-panel-shell__stage", inert: renderedRevealState !== "open" ? true : undefined, children: [shouldRenderPixelReveal && renderedRevealState !== "hidden" ? (_jsx("div", { "aria-hidden": "true", className: "signal-ui-panel-shell__pixel-layer", children: _jsx(PanelRevealCanvas, { revealPhase: pixelRevealPhase, revealState: renderedPixelRevealState }) })) : undefined, panelCard] })] }));
97
+ }
98
+ function composeEventHandlers(...handlers) {
99
+ return (event) => {
100
+ handlers.forEach((handler) => {
101
+ handler?.(event);
102
+ });
103
+ };
104
+ }
105
+ function toCssLength(value) {
106
+ if (value === undefined) {
107
+ return undefined;
108
+ }
109
+ return typeof value === "number" ? `${value}px` : value;
48
110
  }
@@ -0,0 +1,14 @@
1
+ import { type RgbChannels } from "../signalButton/utils.js";
2
+ import type { FullScreenWipePhase } from "./useFullScreenWipeState.js";
3
+ type RenderProceduralPixelWipeBufferOptions = {
4
+ accent: RgbChannels;
5
+ cols: number;
6
+ cover: RgbChannels;
7
+ ctx: CanvasRenderingContext2D;
8
+ phase: FullScreenWipePhase | "closed";
9
+ progress: number;
10
+ rows: number;
11
+ timeMs: number;
12
+ };
13
+ export declare function renderProceduralPixelWipeBuffer({ accent, cols, cover, ctx, phase, progress, rows, timeMs, }: RenderProceduralPixelWipeBufferOptions): void;
14
+ export {};
@@ -0,0 +1,65 @@
1
+ import { drawPixel } from "../signalButton/color.js";
2
+ import { hash } from "../signalButton/noise.js";
3
+ import { clamp } from "../signalButton/utils.js";
4
+ export function renderProceduralPixelWipeBuffer({ accent, cols, cover, ctx, phase, progress, rows, timeMs, }) {
5
+ const easedProgress = phase === "opening"
6
+ ? 1 - easeOutQuint(progress)
7
+ : phase === "closing"
8
+ ? easeInOutCubic(progress)
9
+ : 1;
10
+ const timePhase = timeMs * 0.001;
11
+ const diagonalSpan = Math.max(cols + rows - 2, 1);
12
+ const frontierWidth = 0.14;
13
+ const travelOvershoot = 0.26;
14
+ const frontierPosition = lerp(-travelOvershoot, 1 + travelOvershoot, easedProgress);
15
+ ctx.clearRect(0, 0, cols, rows);
16
+ for (let y = 0; y < rows; y += 1) {
17
+ for (let x = 0; x < cols; x += 1) {
18
+ const diagonal = (x + y) / diagonalSpan;
19
+ const stableNoise = hash(x * 9 + 11, y * 13 + 7, 23);
20
+ const pulseWave = 0.5 +
21
+ 0.5 *
22
+ Math.sin(timePhase * 4.8 + x * 0.58 - y * 0.3 + stableNoise * Math.PI * 2);
23
+ const driftWave = 0.5 + 0.5 * Math.sin(timePhase * 2.7 - diagonal * 10.4 + stableNoise * Math.PI * 1.5);
24
+ const diagonalNoise = (stableNoise - 0.5) * 0.22 + (pulseWave - 0.5) * 0.08 + (driftWave - 0.5) * 0.06;
25
+ const threshold = diagonal + diagonalNoise;
26
+ const coverage = clamp((frontierPosition - threshold + frontierWidth) / (frontierWidth * 2), 0, 1);
27
+ if (coverage <= 0.02) {
28
+ continue;
29
+ }
30
+ const frontier = clamp(1 - Math.abs(frontierPosition - threshold) / frontierWidth, 0, 1);
31
+ const alpha = 0.12 + coverage * 0.86;
32
+ const coverLift = 0.02 + frontier * 0.08 + pulseWave * 0.04;
33
+ const coverSaturation = 0.08 + driftWave * 0.16;
34
+ ctx.fillStyle = rgba(cover, coverSaturation, coverLift, clamp(alpha + (phase === "closed" ? 0.04 : 0), 0, 1));
35
+ ctx.fillRect(x, y, 1, 1);
36
+ const accentThreshold = 0.46 - frontier * 0.12;
37
+ const accentNoise = hash(x * 7 + 17, y * 5 + 29, 41);
38
+ if (frontier > 0.12) {
39
+ drawPixel(ctx, x, y, accent, 0.88 + frontier * 0.12, 0.08 + frontier * 0.18 + pulseWave * 0.08, 0.06 + frontier * 0.34 + pulseWave * 0.06);
40
+ }
41
+ else if (coverage > 0.82 && accentNoise > accentThreshold) {
42
+ drawPixel(ctx, x, y, accent, 0.62 + pulseWave * 0.18, 0.03 + driftWave * 0.08, 0.03 + accentNoise * 0.08);
43
+ }
44
+ }
45
+ }
46
+ }
47
+ function rgba(channels, saturation, lift, alpha) {
48
+ const red = liftChannel(channels[0], saturation, lift);
49
+ const green = liftChannel(channels[1], saturation, lift);
50
+ const blue = liftChannel(channels[2], saturation, lift);
51
+ return `rgba(${red}, ${green}, ${blue}, ${clamp(alpha, 0, 1)})`;
52
+ }
53
+ function liftChannel(channel, saturation, lift) {
54
+ const scaled = channel * saturation;
55
+ return Math.round(clamp(scaled + (255 - scaled) * lift, 0, 255));
56
+ }
57
+ function easeInOutCubic(value) {
58
+ return value < 0.5 ? 4 * value * value * value : 1 - Math.pow(-2 * value + 2, 3) / 2;
59
+ }
60
+ function easeOutQuint(value) {
61
+ return 1 - Math.pow(1 - value, 5);
62
+ }
63
+ function lerp(start, end, amount) {
64
+ return start + (end - start) * amount;
65
+ }
@@ -0,0 +1,14 @@
1
+ import type { FullScreenWipeState } from "../FullScreenWipe.js";
2
+ export type FullScreenWipePhase = "opening" | "closing";
3
+ export declare function getFullScreenWipePhaseDurationMs(durationMs: number, _phase: FullScreenWipePhase): number;
4
+ type UseFullScreenWipeStateOptions = {
5
+ durationMs: number;
6
+ state: FullScreenWipeState;
7
+ };
8
+ export declare function useFullScreenWipeState({ durationMs, state, }: UseFullScreenWipeStateOptions): {
9
+ blocksInteraction: boolean;
10
+ phase: FullScreenWipePhase | undefined;
11
+ renderedState: FullScreenWipeState;
12
+ reducedMotion: boolean;
13
+ };
14
+ export {};
@@ -0,0 +1,47 @@
1
+ import { useEffect, useLayoutEffect, useRef, useState } from "react";
2
+ export function getFullScreenWipePhaseDurationMs(durationMs, _phase) {
3
+ return durationMs;
4
+ }
5
+ export function useFullScreenWipeState({ durationMs, state, }) {
6
+ const reducedMotion = prefersReducedMotion();
7
+ const [visualState, setVisualState] = useState({
8
+ phase: undefined,
9
+ renderedState: state,
10
+ });
11
+ const previousStateRef = useRef(state);
12
+ useIsomorphicLayoutEffect(() => {
13
+ const previousState = previousStateRef.current;
14
+ previousStateRef.current = state;
15
+ if (reducedMotion || previousState === state) {
16
+ setVisualState({
17
+ phase: undefined,
18
+ renderedState: state,
19
+ });
20
+ return undefined;
21
+ }
22
+ const nextPhase = state === "open" ? "opening" : "closing";
23
+ setVisualState({
24
+ phase: nextPhase,
25
+ renderedState: previousState,
26
+ });
27
+ const timeoutId = window.setTimeout(() => {
28
+ setVisualState({
29
+ phase: undefined,
30
+ renderedState: state,
31
+ });
32
+ }, getFullScreenWipePhaseDurationMs(durationMs, nextPhase));
33
+ return () => {
34
+ window.clearTimeout(timeoutId);
35
+ };
36
+ }, [durationMs, reducedMotion, state]);
37
+ return {
38
+ blocksInteraction: reducedMotion ? state === "closed" : state === "closed" || visualState.phase !== undefined,
39
+ phase: visualState.phase,
40
+ renderedState: visualState.renderedState,
41
+ reducedMotion,
42
+ };
43
+ }
44
+ function prefersReducedMotion() {
45
+ return typeof window !== "undefined" && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
46
+ }
47
+ const useIsomorphicLayoutEffect = typeof window === "undefined" ? useEffect : useLayoutEffect;
@@ -0,0 +1,12 @@
1
+ import type { RefObject } from "react";
2
+ import type { FullScreenWipeState } from "../FullScreenWipe.js";
3
+ import type { FullScreenWipePhase } from "./useFullScreenWipeState.js";
4
+ type UseProceduralPixelWipeCanvasOptions = {
5
+ canvasRef: RefObject<HTMLCanvasElement | null>;
6
+ durationMs: number;
7
+ phase?: FullScreenWipePhase;
8
+ reducedMotion: boolean;
9
+ state: FullScreenWipeState;
10
+ };
11
+ export declare function useProceduralPixelWipeCanvas({ canvasRef, durationMs, phase, reducedMotion, state, }: UseProceduralPixelWipeCanvasOptions): void;
12
+ export {};