@nous-research/ui 0.3.0 → 0.5.0
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/dist/hooks/use-smooth-controls.d.ts.map +1 -1
- package/dist/hooks/use-smooth-controls.js +16 -0
- package/dist/hooks/use-smooth-controls.js.map +1 -1
- package/dist/index.d.ts +11 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -6
- package/dist/index.js.map +1 -1
- package/dist/ui/components/command-block.d.ts +25 -0
- package/dist/ui/components/command-block.d.ts.map +1 -0
- package/dist/ui/components/command-block.js +27 -0
- package/dist/ui/components/command-block.js.map +1 -0
- package/dist/ui/components/grid/grid.css +2 -1
- package/dist/ui/components/icons/hamburger.d.ts +7 -0
- package/dist/ui/components/icons/hamburger.d.ts.map +1 -0
- package/dist/ui/components/icons/hamburger.js +6 -0
- package/dist/ui/components/icons/hamburger.js.map +1 -0
- package/dist/ui/components/icons/index.d.ts +1 -0
- package/dist/ui/components/icons/index.d.ts.map +1 -1
- package/dist/ui/components/icons/index.js +1 -0
- package/dist/ui/components/icons/index.js.map +1 -1
- package/dist/ui/components/image-distortion.d.ts +22 -0
- package/dist/ui/components/image-distortion.d.ts.map +1 -0
- package/dist/ui/components/image-distortion.js +336 -0
- package/dist/ui/components/image-distortion.js.map +1 -0
- package/dist/ui/components/overlays/index.d.ts +17 -3
- package/dist/ui/components/overlays/index.d.ts.map +1 -1
- package/dist/ui/components/overlays/index.js +48 -28
- package/dist/ui/components/overlays/index.js.map +1 -1
- package/dist/ui/components/poster.d.ts +63 -0
- package/dist/ui/components/poster.d.ts.map +1 -0
- package/dist/ui/components/poster.js +102 -0
- package/dist/ui/components/poster.js.map +1 -0
- package/dist/ui/components/terminal-demo.d.ts +33 -0
- package/dist/ui/components/terminal-demo.d.ts.map +1 -0
- package/dist/ui/components/terminal-demo.js +79 -0
- package/dist/ui/components/terminal-demo.js.map +1 -0
- package/dist/ui/components/theme-toggle.d.ts +7 -0
- package/dist/ui/components/theme-toggle.d.ts.map +1 -0
- package/dist/ui/components/theme-toggle.js +10 -0
- package/dist/ui/components/theme-toggle.js.map +1 -0
- package/dist/ui/components/tier-card.d.ts +54 -0
- package/dist/ui/components/tier-card.d.ts.map +1 -0
- package/dist/ui/components/tier-card.js +26 -0
- package/dist/ui/components/tier-card.js.map +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import type { AutoPlayPattern } from './image-distortion';
|
|
2
|
+
/**
|
|
3
|
+
* Social-ready glitchy card built around the haptic-distortion image
|
|
4
|
+
* component. The poster runs the sword-guy distortion on an auto-animated
|
|
5
|
+
* slash pattern so it can be screen-recorded as a GIF without a human
|
|
6
|
+
* moving a cursor.
|
|
7
|
+
*
|
|
8
|
+
* Two variants, matching actual use cases:
|
|
9
|
+
* - `'vibe'` (default): full-bleed distorted image with just registration
|
|
10
|
+
* marks and a tiny "Hermes Agent" mark in the corner — mirrors the
|
|
11
|
+
* overlay on the Hermes agent website.
|
|
12
|
+
* - `'dispatch'`: broadcast-card layout with sidebar copy, numbered tags,
|
|
13
|
+
* and chrome — for when the poster needs to carry information.
|
|
14
|
+
*/
|
|
15
|
+
export declare function Poster({ aspect, autoPlay, body, border, channel, children, className, cornerMarks, eyebrow, headline, layout, scale, seal, signature, src, tags, tint, tintStrength, variant, ...rest }: PosterProps): import("react/jsx-runtime").JSX.Element;
|
|
16
|
+
export type PosterAspect = 'landscape' | 'portrait' | 'square' | 'story' | 'wide';
|
|
17
|
+
export type PosterVariant = 'dispatch' | 'vibe';
|
|
18
|
+
export interface PosterProps {
|
|
19
|
+
/** Output aspect ratio. Picks sensible defaults for common social formats. */
|
|
20
|
+
aspect?: PosterAspect;
|
|
21
|
+
/** Distortion choreography pattern. Default: `'slash'`. */
|
|
22
|
+
autoPlay?: AutoPlayPattern;
|
|
23
|
+
/** (`dispatch` only) Descriptive copy under the headline. */
|
|
24
|
+
body?: React.ReactNode;
|
|
25
|
+
/** Show the thin outer frame around the poster. Default `true`. */
|
|
26
|
+
border?: boolean;
|
|
27
|
+
/** Tiny broadcast-station label. Optional in `vibe`; shown in header in `dispatch`. */
|
|
28
|
+
channel?: React.ReactNode;
|
|
29
|
+
/** (`dispatch` only) Override the sidebar content (takes precedence over headline/body). */
|
|
30
|
+
children?: React.ReactNode;
|
|
31
|
+
className?: string;
|
|
32
|
+
/** Show the small `+` die-line registration marks in the image corners. Default `true`. */
|
|
33
|
+
cornerMarks?: boolean;
|
|
34
|
+
/** (`dispatch` only) Small tagline above the headline. */
|
|
35
|
+
eyebrow?: React.ReactNode;
|
|
36
|
+
/** (`dispatch` only) Big expanded-typography headline. Pass an array of strings to stack lines. */
|
|
37
|
+
headline?: string[] | string;
|
|
38
|
+
/** (`dispatch` only) Force stacked vs split layout. Default inferred from `aspect`. */
|
|
39
|
+
layout?: 'split' | 'stacked';
|
|
40
|
+
/** Render scale. 1 = full canvas (1080px+ base width). */
|
|
41
|
+
scale?: number;
|
|
42
|
+
/** (`dispatch` only) Small legal / signature line at the bottom-right. */
|
|
43
|
+
seal?: React.ReactNode;
|
|
44
|
+
/**
|
|
45
|
+
* Signature mark. In `vibe` this is the small "Hermes Agent" overlay in the
|
|
46
|
+
* bottom-right. In `dispatch` this is the URL / CTA in the footer.
|
|
47
|
+
*/
|
|
48
|
+
signature?: React.ReactNode;
|
|
49
|
+
/** Override the poster image. Defaults to the Hermes "filler-bg0" asset. */
|
|
50
|
+
src?: string;
|
|
51
|
+
/** (`dispatch` only) Ranked list of features / pricing tiers rendered as a numbered sidebar list. */
|
|
52
|
+
tags?: string[];
|
|
53
|
+
/** Shader tint overlay. Great for tier-colored variants. */
|
|
54
|
+
tint?: string;
|
|
55
|
+
/** Active / inactive tint strength — defaults match `ImageDistortion`. */
|
|
56
|
+
tintStrength?: {
|
|
57
|
+
active: number;
|
|
58
|
+
inactive: number;
|
|
59
|
+
};
|
|
60
|
+
/** Layout variant. `'vibe'` (default) is full-bleed image; `'dispatch'` is the broadcast-card with sidebar copy. */
|
|
61
|
+
variant?: PosterVariant;
|
|
62
|
+
}
|
|
63
|
+
//# sourceMappingURL=poster.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"poster.d.ts","sourceRoot":"","sources":["../../../src/ui/components/poster.tsx"],"names":[],"mappings":"AAYA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAA;AAoEzD;;;;;;;;;;;;GAYG;AACH,wBAAgB,MAAM,CAAC,EACrB,MAAiB,EACjB,QAAkB,EAClB,IAAI,EACJ,MAAa,EACb,OAAO,EACP,QAAQ,EACR,SAAS,EACT,WAAkB,EAClB,OAAO,EACP,QAAkD,EAClD,MAAM,EACN,KAAS,EACT,IAAmB,EACnB,SAAS,EACT,GAAiB,EACjB,IAAI,EACJ,IAAI,EACJ,YAAY,EACZ,OAAgB,EAChB,GAAG,IAAI,EACR,EAAE,WAAW,2CAyKb;AA6ED,MAAM,MAAM,YAAY,GACpB,WAAW,GACX,UAAU,GACV,QAAQ,GACR,OAAO,GACP,MAAM,CAAA;AAEV,MAAM,MAAM,aAAa,GAAG,UAAU,GAAG,MAAM,CAAA;AAE/C,MAAM,WAAW,WAAW;IAC1B,8EAA8E;IAC9E,MAAM,CAAC,EAAE,YAAY,CAAA;IACrB,2DAA2D;IAC3D,QAAQ,CAAC,EAAE,eAAe,CAAA;IAC1B,6DAA6D;IAC7D,IAAI,CAAC,EAAE,KAAK,CAAC,SAAS,CAAA;IACtB,mEAAmE;IACnE,MAAM,CAAC,EAAE,OAAO,CAAA;IAChB,uFAAuF;IACvF,OAAO,CAAC,EAAE,KAAK,CAAC,SAAS,CAAA;IACzB,4FAA4F;IAC5F,QAAQ,CAAC,EAAE,KAAK,CAAC,SAAS,CAAA;IAC1B,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,2FAA2F;IAC3F,WAAW,CAAC,EAAE,OAAO,CAAA;IACrB,0DAA0D;IAC1D,OAAO,CAAC,EAAE,KAAK,CAAC,SAAS,CAAA;IACzB,mGAAmG;IACnG,QAAQ,CAAC,EAAE,MAAM,EAAE,GAAG,MAAM,CAAA;IAC5B,uFAAuF;IACvF,MAAM,CAAC,EAAE,OAAO,GAAG,SAAS,CAAA;IAC5B,0DAA0D;IAC1D,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,0EAA0E;IAC1E,IAAI,CAAC,EAAE,KAAK,CAAC,SAAS,CAAA;IACtB;;;OAGG;IACH,SAAS,CAAC,EAAE,KAAK,CAAC,SAAS,CAAA;IAC3B,4EAA4E;IAC5E,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,qGAAqG;IACrG,IAAI,CAAC,EAAE,MAAM,EAAE,CAAA;IACf,4DAA4D;IAC5D,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,0EAA0E;IAC1E,YAAY,CAAC,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAA;IACnD,oHAAoH;IACpH,OAAO,CAAC,EAAE,aAAa,CAAA;CACxB"}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
import { useEffect, useState } from 'react';
|
|
4
|
+
import fillerBg from '../../assets/filler-bg0.jpg';
|
|
5
|
+
import { cn } from '../../utils';
|
|
6
|
+
import { Blink } from './blink';
|
|
7
|
+
import { ImageDistortion } from './image-distortion';
|
|
8
|
+
import { Typography } from './typography';
|
|
9
|
+
import { Small } from './typography/small';
|
|
10
|
+
const ASPECT_CONFIG = {
|
|
11
|
+
landscape: { defaultLayout: 'split', height: 1080, width: 1920 },
|
|
12
|
+
portrait: { defaultLayout: 'split', height: 1350, width: 1080 },
|
|
13
|
+
square: { defaultLayout: 'split', height: 1080, width: 1080 },
|
|
14
|
+
story: { defaultLayout: 'stacked', height: 1920, width: 1080 },
|
|
15
|
+
wide: { defaultLayout: 'split', height: 900, width: 1600 }
|
|
16
|
+
};
|
|
17
|
+
const DEFAULT_SRC = fillerBg.src ?? fillerBg;
|
|
18
|
+
function useUtcClock() {
|
|
19
|
+
const [now, setNow] = useState(null);
|
|
20
|
+
useEffect(() => {
|
|
21
|
+
setNow(new Date());
|
|
22
|
+
const id = setInterval(() => setNow(new Date()), 1000);
|
|
23
|
+
return () => clearInterval(id);
|
|
24
|
+
}, []);
|
|
25
|
+
return now ? now.toISOString().slice(11, 19) : '--:--:--';
|
|
26
|
+
}
|
|
27
|
+
function CornerMark({ className }) {
|
|
28
|
+
return (_jsxs("span", { "aria-hidden": true, className: cn('pointer-events-none absolute block size-4 opacity-50', className), children: [_jsx("span", { className: "absolute top-1/2 left-0 h-px w-full -translate-y-1/2 bg-current" }), _jsx("span", { className: "absolute top-0 left-1/2 h-full w-px -translate-x-1/2 bg-current" })] }));
|
|
29
|
+
}
|
|
30
|
+
function ChannelDot() {
|
|
31
|
+
return (_jsxs("span", { className: "flex items-center gap-1.5", children: [_jsx("span", { className: "bg-midground size-1.5 animate-pulse rounded-full" }), _jsx(Small, { className: "opacity-70", children: "REC" })] }));
|
|
32
|
+
}
|
|
33
|
+
function ScanlineOverlay() {
|
|
34
|
+
return (_jsx("div", { "aria-hidden": true, className: "pointer-events-none absolute inset-0 opacity-20 mix-blend-overlay", style: {
|
|
35
|
+
backgroundImage: 'repeating-linear-gradient(0deg, transparent 0, transparent 2px, rgba(255,255,255,0.08) 2px, rgba(255,255,255,0.08) 3px)'
|
|
36
|
+
} }));
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Social-ready glitchy card built around the haptic-distortion image
|
|
40
|
+
* component. The poster runs the sword-guy distortion on an auto-animated
|
|
41
|
+
* slash pattern so it can be screen-recorded as a GIF without a human
|
|
42
|
+
* moving a cursor.
|
|
43
|
+
*
|
|
44
|
+
* Two variants, matching actual use cases:
|
|
45
|
+
* - `'vibe'` (default): full-bleed distorted image with just registration
|
|
46
|
+
* marks and a tiny "Hermes Agent" mark in the corner — mirrors the
|
|
47
|
+
* overlay on the Hermes agent website.
|
|
48
|
+
* - `'dispatch'`: broadcast-card layout with sidebar copy, numbered tags,
|
|
49
|
+
* and chrome — for when the poster needs to carry information.
|
|
50
|
+
*/
|
|
51
|
+
export function Poster({ aspect = 'square', autoPlay = 'slash', body, border = true, channel, children, className, cornerMarks = true, eyebrow, headline = ['An Agent', 'That Grows', 'With You.'], layout, scale = 1, seal = 'MIT · 2026', signature, src = DEFAULT_SRC, tags, tint, tintStrength, variant = 'vibe', ...rest }) {
|
|
52
|
+
const config = ASPECT_CONFIG[aspect];
|
|
53
|
+
const resolvedLayout = layout ?? config.defaultLayout;
|
|
54
|
+
// Use aspect-ratio + max-width/height so the poster fluidly fits any parent
|
|
55
|
+
// (storybook iframe, a tweet preview, an embed) without getting clipped,
|
|
56
|
+
// but caps at the intended export width for screen-recording. `maxHeight`
|
|
57
|
+
// uses an absolute `dvh`-based value rather than `%` because `%` inside a
|
|
58
|
+
// flex container can cause the browser to clamp height without re-running
|
|
59
|
+
// aspect-ratio on width, producing a subtly wrong shape. An absolute cap
|
|
60
|
+
// leaves aspect-ratio fully in charge: once the height binds, width is
|
|
61
|
+
// re-derived correctly. `calc(100dvh - 8rem)` = viewport minus a typical
|
|
62
|
+
// host's vertical padding (e.g. Storybook's `p-8` = 4rem on each side),
|
|
63
|
+
// so the poster + padding fit within the viewport without ever producing
|
|
64
|
+
// scrollbars. Container queries tie all internal typography to the
|
|
65
|
+
// actual rendered width so headline/metadata scales along with the canvas.
|
|
66
|
+
const outerProps = {
|
|
67
|
+
// `text-midground` (not `text-foreground`) is the readable on-canvas
|
|
68
|
+
// color across every lens. `--foreground` is really the lens's inversion
|
|
69
|
+
// layer color: on dark lenses it has `fgOpacity: 0` and resolves to
|
|
70
|
+
// fully-transparent via `color-mix`, which would make text invisible.
|
|
71
|
+
// `--midground` always has opacity 1 and picks up each lens's accent.
|
|
72
|
+
className: cn('text-midground relative overflow-hidden font-sans', border && 'border border-current/25', className),
|
|
73
|
+
style: {
|
|
74
|
+
aspectRatio: `${config.width} / ${config.height}`,
|
|
75
|
+
background: 'var(--background)',
|
|
76
|
+
containerType: 'inline-size',
|
|
77
|
+
fontSize: `${(16 / config.width) * 100}cqi`,
|
|
78
|
+
maxHeight: 'calc(100dvh - 8rem)',
|
|
79
|
+
maxWidth: '100%',
|
|
80
|
+
width: `${config.width * scale}px`
|
|
81
|
+
},
|
|
82
|
+
...rest
|
|
83
|
+
};
|
|
84
|
+
if (variant === 'vibe') {
|
|
85
|
+
return (_jsx("div", { ...outerProps, children: _jsx(VibeContent, { autoPlay: autoPlay, channel: channel, cornerMarks: cornerMarks, signature: signature, src: src, tint: tint, tintStrength: tintStrength }) }));
|
|
86
|
+
}
|
|
87
|
+
const headlineLines = Array.isArray(headline) ? headline : [headline];
|
|
88
|
+
return (_jsxs("div", { ...outerProps, className: cn('flex flex-col', outerProps.className), children: [_jsx(DispatchHeader, { channel: channel }), _jsxs("div", { className: cn('relative min-h-0 min-w-0 flex-1', resolvedLayout === 'split'
|
|
89
|
+
? 'grid grid-cols-[3fr_2fr]'
|
|
90
|
+
: 'grid grid-rows-[3fr_2fr]'), children: [_jsxs("div", { className: cn('relative overflow-hidden border-current/20', resolvedLayout === 'split' ? 'border-r' : 'border-b'), style: { backgroundColor: 'var(--background)' }, children: [_jsx(ImageDistortion, { autoPlay: autoPlay, src: src, tint: tint, tintStrength: tintStrength }), cornerMarks && (_jsxs(_Fragment, { children: [_jsx(CornerMark, { className: "top-3 left-3" }), _jsx(CornerMark, { className: "top-3 right-3" }), _jsx(CornerMark, { className: "bottom-3 left-3" }), _jsx(CornerMark, { className: "right-3 bottom-3" })] })), _jsx(ScanlineOverlay, {}), _jsx(Small, { className: "absolute bottom-4 left-4 z-1 opacity-80", children: "Hermes Agent" })] }), _jsxs("aside", { className: "relative flex min-w-0 flex-col justify-between gap-8 p-8", children: [_jsxs("div", { className: "flex flex-col gap-5", children: [eyebrow && (_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("span", { className: "bg-midground/80 h-px flex-1" }), _jsx(Small, { className: "opacity-80", children: eyebrow })] })), children ?? (_jsxs(_Fragment, { children: [_jsx(Typography, { as: "h1", className: "text-[2.75em] leading-[0.95] font-bold tracking-[-0.01em]", expanded: true, children: headlineLines.map((line, i) => (_jsx("span", { className: "block", children: line }, `${line}-${i}`))) }), body && (_jsx("p", { className: "text-[1.0625em] leading-[1.5] tracking-normal normal-case opacity-60", children: body }))] }))] }), tags && tags.length > 0 && (_jsx("ul", { className: "flex flex-col gap-2 border-t border-current/15 pt-4", children: tags.map((tag, i) => (_jsxs("li", { className: "flex items-baseline justify-between gap-3", children: [_jsx(Small, { className: "font-courier opacity-40", children: String(i + 1).padStart(3, '0') }), _jsx(Small, { className: "opacity-80", children: tag }), _jsx("span", { className: "mx-1 h-px flex-1 translate-y-[-3px] border-b border-dotted border-current/25" }), _jsxs(Small, { className: "font-courier opacity-40", children: [String(i + 1).padStart(2, '0'), "/", String(tags.length).padStart(2, '0')] })] }, `${tag}-${i}`))) }))] })] }), _jsxs("footer", { className: "flex items-center justify-between gap-4 border-t border-current/20 px-6 py-3", children: [_jsxs(Small, { className: "opacity-70", children: [signature, _jsx(Blink, {})] }), _jsx(Small, { className: "font-courier opacity-40", children: seal })] })] }));
|
|
91
|
+
}
|
|
92
|
+
function DispatchHeader({ channel }) {
|
|
93
|
+
const clock = useUtcClock();
|
|
94
|
+
return (_jsxs("header", { className: "flex items-center justify-between gap-4 border-b border-current/20 px-6 py-3", children: [_jsxs("div", { className: "flex items-center gap-3", children: [_jsx("span", { className: "bg-midground size-2 rounded-sm opacity-70" }), _jsx(Small, { className: "opacity-70", children: channel })] }), _jsxs("div", { className: "flex items-center gap-4", children: [_jsx(ChannelDot, {}), _jsxs(Small, { className: "font-courier opacity-50", children: [clock, " UTC"] })] })] }));
|
|
95
|
+
}
|
|
96
|
+
function VibeContent({ autoPlay, channel, cornerMarks, signature, src, tint, tintStrength }) {
|
|
97
|
+
// Absolute-inset-0 guarantees this fills the poster even when the outer
|
|
98
|
+
// container uses aspect-ratio-derived height in a browser that doesn't
|
|
99
|
+
// propagate that as a definite height for percentage-based children.
|
|
100
|
+
return (_jsxs("div", { className: "absolute inset-0", children: [_jsx(ImageDistortion, { autoPlay: autoPlay, src: src, tint: tint, tintStrength: tintStrength }), cornerMarks && (_jsxs(_Fragment, { children: [_jsx(CornerMark, { className: "top-5 left-5" }), _jsx(CornerMark, { className: "top-5 right-5" }), _jsx(CornerMark, { className: "bottom-5 left-5" }), _jsx(CornerMark, { className: "right-5 bottom-5" })] })), _jsx(ScanlineOverlay, {}), channel && (_jsx(Small, { className: "absolute top-5 left-10 z-1 text-[0.75em] opacity-70", children: channel })), _jsx(Small, { className: "absolute right-10 bottom-5 z-1 text-[0.75em] opacity-80", children: signature })] }));
|
|
101
|
+
}
|
|
102
|
+
//# sourceMappingURL=poster.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"poster.js","sourceRoot":"","sources":["../../../src/ui/components/poster.tsx"],"names":[],"mappings":"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","sourcesContent":["'use client'\n\nimport { useEffect, useState } from 'react'\n\nimport fillerBg from '../../assets/filler-bg0.jpg'\nimport { cn } from '../../utils'\n\nimport { Blink } from './blink'\nimport { ImageDistortion } from './image-distortion'\nimport { Typography } from './typography'\nimport { Small } from './typography/small'\n\nimport type { AutoPlayPattern } from './image-distortion'\n\nconst ASPECT_CONFIG: Record<\n PosterAspect,\n { defaultLayout: 'split' | 'stacked'; height: number; width: number }\n> = {\n landscape: { defaultLayout: 'split', height: 1080, width: 1920 },\n portrait: { defaultLayout: 'split', height: 1350, width: 1080 },\n square: { defaultLayout: 'split', height: 1080, width: 1080 },\n story: { defaultLayout: 'stacked', height: 1920, width: 1080 },\n wide: { defaultLayout: 'split', height: 900, width: 1600 }\n}\n\nconst DEFAULT_SRC =\n (fillerBg as { src?: string }).src ?? (fillerBg as unknown as string)\n\nfunction useUtcClock() {\n const [now, setNow] = useState<Date | null>(null)\n\n useEffect(() => {\n setNow(new Date())\n const id = setInterval(() => setNow(new Date()), 1000)\n\n return () => clearInterval(id)\n }, [])\n\n return now ? now.toISOString().slice(11, 19) : '--:--:--'\n}\n\nfunction CornerMark({ className }: { className?: string }) {\n return (\n <span\n aria-hidden\n className={cn(\n 'pointer-events-none absolute block size-4 opacity-50',\n className\n )}\n >\n <span className=\"absolute top-1/2 left-0 h-px w-full -translate-y-1/2 bg-current\" />\n\n <span className=\"absolute top-0 left-1/2 h-full w-px -translate-x-1/2 bg-current\" />\n </span>\n )\n}\n\nfunction ChannelDot() {\n return (\n <span className=\"flex items-center gap-1.5\">\n <span className=\"bg-midground size-1.5 animate-pulse rounded-full\" />\n\n <Small className=\"opacity-70\">REC</Small>\n </span>\n )\n}\n\nfunction ScanlineOverlay() {\n return (\n <div\n aria-hidden\n className=\"pointer-events-none absolute inset-0 opacity-20 mix-blend-overlay\"\n style={{\n backgroundImage:\n 'repeating-linear-gradient(0deg, transparent 0, transparent 2px, rgba(255,255,255,0.08) 2px, rgba(255,255,255,0.08) 3px)'\n }}\n />\n )\n}\n\n/**\n * Social-ready glitchy card built around the haptic-distortion image\n * component. The poster runs the sword-guy distortion on an auto-animated\n * slash pattern so it can be screen-recorded as a GIF without a human\n * moving a cursor.\n *\n * Two variants, matching actual use cases:\n * - `'vibe'` (default): full-bleed distorted image with just registration\n * marks and a tiny \"Hermes Agent\" mark in the corner — mirrors the\n * overlay on the Hermes agent website.\n * - `'dispatch'`: broadcast-card layout with sidebar copy, numbered tags,\n * and chrome — for when the poster needs to carry information.\n */\nexport function Poster({\n aspect = 'square',\n autoPlay = 'slash',\n body,\n border = true,\n channel,\n children,\n className,\n cornerMarks = true,\n eyebrow,\n headline = ['An Agent', 'That Grows', 'With You.'],\n layout,\n scale = 1,\n seal = 'MIT · 2026',\n signature,\n src = DEFAULT_SRC,\n tags,\n tint,\n tintStrength,\n variant = 'vibe',\n ...rest\n}: PosterProps) {\n const config = ASPECT_CONFIG[aspect]\n const resolvedLayout = layout ?? config.defaultLayout\n\n // Use aspect-ratio + max-width/height so the poster fluidly fits any parent\n // (storybook iframe, a tweet preview, an embed) without getting clipped,\n // but caps at the intended export width for screen-recording. `maxHeight`\n // uses an absolute `dvh`-based value rather than `%` because `%` inside a\n // flex container can cause the browser to clamp height without re-running\n // aspect-ratio on width, producing a subtly wrong shape. An absolute cap\n // leaves aspect-ratio fully in charge: once the height binds, width is\n // re-derived correctly. `calc(100dvh - 8rem)` = viewport minus a typical\n // host's vertical padding (e.g. Storybook's `p-8` = 4rem on each side),\n // so the poster + padding fit within the viewport without ever producing\n // scrollbars. Container queries tie all internal typography to the\n // actual rendered width so headline/metadata scales along with the canvas.\n const outerProps = {\n // `text-midground` (not `text-foreground`) is the readable on-canvas\n // color across every lens. `--foreground` is really the lens's inversion\n // layer color: on dark lenses it has `fgOpacity: 0` and resolves to\n // fully-transparent via `color-mix`, which would make text invisible.\n // `--midground` always has opacity 1 and picks up each lens's accent.\n className: cn(\n 'text-midground relative overflow-hidden font-sans',\n border && 'border border-current/25',\n className\n ),\n style: {\n aspectRatio: `${config.width} / ${config.height}`,\n background: 'var(--background)',\n containerType: 'inline-size' as const,\n fontSize: `${(16 / config.width) * 100}cqi`,\n maxHeight: 'calc(100dvh - 8rem)',\n maxWidth: '100%',\n width: `${config.width * scale}px`\n },\n ...rest\n }\n\n if (variant === 'vibe') {\n return (\n <div {...outerProps}>\n <VibeContent\n autoPlay={autoPlay}\n channel={channel}\n cornerMarks={cornerMarks}\n signature={signature}\n src={src}\n tint={tint}\n tintStrength={tintStrength}\n />\n </div>\n )\n }\n\n const headlineLines = Array.isArray(headline) ? headline : [headline]\n\n return (\n <div {...outerProps} className={cn('flex flex-col', outerProps.className)}>\n <DispatchHeader channel={channel} />\n\n <div\n className={cn(\n 'relative min-h-0 min-w-0 flex-1',\n resolvedLayout === 'split'\n ? 'grid grid-cols-[3fr_2fr]'\n : 'grid grid-rows-[3fr_2fr]'\n )}\n >\n <div\n className={cn(\n 'relative overflow-hidden border-current/20',\n resolvedLayout === 'split' ? 'border-r' : 'border-b'\n )}\n style={{ backgroundColor: 'var(--background)' }}\n >\n <ImageDistortion\n autoPlay={autoPlay}\n src={src}\n tint={tint}\n tintStrength={tintStrength}\n />\n\n {cornerMarks && (\n <>\n <CornerMark className=\"top-3 left-3\" />\n <CornerMark className=\"top-3 right-3\" />\n <CornerMark className=\"bottom-3 left-3\" />\n <CornerMark className=\"right-3 bottom-3\" />\n </>\n )}\n\n <ScanlineOverlay />\n\n <Small className=\"absolute bottom-4 left-4 z-1 opacity-80\">\n Hermes Agent\n </Small>\n </div>\n\n <aside className=\"relative flex min-w-0 flex-col justify-between gap-8 p-8\">\n <div className=\"flex flex-col gap-5\">\n {eyebrow && (\n <div className=\"flex items-center gap-2\">\n <span className=\"bg-midground/80 h-px flex-1\" />\n\n <Small className=\"opacity-80\">{eyebrow}</Small>\n </div>\n )}\n\n {children ?? (\n <>\n <Typography\n as=\"h1\"\n className=\"text-[2.75em] leading-[0.95] font-bold tracking-[-0.01em]\"\n expanded\n >\n {headlineLines.map((line, i) => (\n <span className=\"block\" key={`${line}-${i}`}>\n {line}\n </span>\n ))}\n </Typography>\n\n {body && (\n <p className=\"text-[1.0625em] leading-[1.5] tracking-normal normal-case opacity-60\">\n {body}\n </p>\n )}\n </>\n )}\n </div>\n\n {tags && tags.length > 0 && (\n <ul className=\"flex flex-col gap-2 border-t border-current/15 pt-4\">\n {tags.map((tag, i) => (\n <li\n className=\"flex items-baseline justify-between gap-3\"\n key={`${tag}-${i}`}\n >\n <Small className=\"font-courier opacity-40\">\n {String(i + 1).padStart(3, '0')}\n </Small>\n\n <Small className=\"opacity-80\">{tag}</Small>\n\n <span className=\"mx-1 h-px flex-1 translate-y-[-3px] border-b border-dotted border-current/25\" />\n\n <Small className=\"font-courier opacity-40\">\n {String(i + 1).padStart(2, '0')}/\n {String(tags.length).padStart(2, '0')}\n </Small>\n </li>\n ))}\n </ul>\n )}\n </aside>\n </div>\n\n <footer className=\"flex items-center justify-between gap-4 border-t border-current/20 px-6 py-3\">\n <Small className=\"opacity-70\">\n {signature}\n\n <Blink />\n </Small>\n\n <Small className=\"font-courier opacity-40\">{seal}</Small>\n </footer>\n </div>\n )\n}\n\nfunction DispatchHeader({ channel }: { channel: React.ReactNode }) {\n const clock = useUtcClock()\n\n return (\n <header className=\"flex items-center justify-between gap-4 border-b border-current/20 px-6 py-3\">\n <div className=\"flex items-center gap-3\">\n <span className=\"bg-midground size-2 rounded-sm opacity-70\" />\n\n <Small className=\"opacity-70\">{channel}</Small>\n </div>\n\n <div className=\"flex items-center gap-4\">\n <ChannelDot />\n\n <Small className=\"font-courier opacity-50\">{clock} UTC</Small>\n </div>\n </header>\n )\n}\n\ninterface VibeContentProps {\n autoPlay: AutoPlayPattern\n channel: React.ReactNode\n cornerMarks: boolean\n signature: React.ReactNode\n src: string\n tint?: string\n tintStrength?: { active: number; inactive: number }\n}\n\nfunction VibeContent({\n autoPlay,\n channel,\n cornerMarks,\n signature,\n src,\n tint,\n tintStrength\n}: VibeContentProps) {\n // Absolute-inset-0 guarantees this fills the poster even when the outer\n // container uses aspect-ratio-derived height in a browser that doesn't\n // propagate that as a definite height for percentage-based children.\n return (\n <div className=\"absolute inset-0\">\n <ImageDistortion\n autoPlay={autoPlay}\n src={src}\n tint={tint}\n tintStrength={tintStrength}\n />\n\n {cornerMarks && (\n <>\n <CornerMark className=\"top-5 left-5\" />\n <CornerMark className=\"top-5 right-5\" />\n <CornerMark className=\"bottom-5 left-5\" />\n <CornerMark className=\"right-5 bottom-5\" />\n </>\n )}\n\n <ScanlineOverlay />\n\n {channel && (\n <Small className=\"absolute top-5 left-10 z-1 text-[0.75em] opacity-70\">\n {channel}\n </Small>\n )}\n\n <Small className=\"absolute right-10 bottom-5 z-1 text-[0.75em] opacity-80\">\n {signature}\n </Small>\n </div>\n )\n}\n\nexport type PosterAspect =\n | 'landscape'\n | 'portrait'\n | 'square'\n | 'story'\n | 'wide'\n\nexport type PosterVariant = 'dispatch' | 'vibe'\n\nexport interface PosterProps {\n /** Output aspect ratio. Picks sensible defaults for common social formats. */\n aspect?: PosterAspect\n /** Distortion choreography pattern. Default: `'slash'`. */\n autoPlay?: AutoPlayPattern\n /** (`dispatch` only) Descriptive copy under the headline. */\n body?: React.ReactNode\n /** Show the thin outer frame around the poster. Default `true`. */\n border?: boolean\n /** Tiny broadcast-station label. Optional in `vibe`; shown in header in `dispatch`. */\n channel?: React.ReactNode\n /** (`dispatch` only) Override the sidebar content (takes precedence over headline/body). */\n children?: React.ReactNode\n className?: string\n /** Show the small `+` die-line registration marks in the image corners. Default `true`. */\n cornerMarks?: boolean\n /** (`dispatch` only) Small tagline above the headline. */\n eyebrow?: React.ReactNode\n /** (`dispatch` only) Big expanded-typography headline. Pass an array of strings to stack lines. */\n headline?: string[] | string\n /** (`dispatch` only) Force stacked vs split layout. Default inferred from `aspect`. */\n layout?: 'split' | 'stacked'\n /** Render scale. 1 = full canvas (1080px+ base width). */\n scale?: number\n /** (`dispatch` only) Small legal / signature line at the bottom-right. */\n seal?: React.ReactNode\n /**\n * Signature mark. In `vibe` this is the small \"Hermes Agent\" overlay in the\n * bottom-right. In `dispatch` this is the URL / CTA in the footer.\n */\n signature?: React.ReactNode\n /** Override the poster image. Defaults to the Hermes \"filler-bg0\" asset. */\n src?: string\n /** (`dispatch` only) Ranked list of features / pricing tiers rendered as a numbered sidebar list. */\n tags?: string[]\n /** Shader tint overlay. Great for tier-colored variants. */\n tint?: string\n /** Active / inactive tint strength — defaults match `ImageDistortion`. */\n tintStrength?: { active: number; inactive: number }\n /** Layout variant. `'vibe'` (default) is full-bleed image; `'dispatch'` is the broadcast-card with sidebar copy. */\n variant?: PosterVariant\n}\n"]}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export declare function TerminalDemo({ ariaLabel, className, height, label, loopDelayMs, outputLineDelayMs, sequence }: TerminalDemoProps): import("react/jsx-runtime").JSX.Element;
|
|
2
|
+
interface ClearStep {
|
|
3
|
+
type: 'clear';
|
|
4
|
+
}
|
|
5
|
+
interface OutputStep {
|
|
6
|
+
lines: string[];
|
|
7
|
+
type: 'output';
|
|
8
|
+
}
|
|
9
|
+
interface PauseStep {
|
|
10
|
+
ms: number;
|
|
11
|
+
type: 'pause';
|
|
12
|
+
}
|
|
13
|
+
interface PromptStep {
|
|
14
|
+
text: string;
|
|
15
|
+
type: 'prompt';
|
|
16
|
+
}
|
|
17
|
+
interface TerminalDemoProps {
|
|
18
|
+
ariaLabel?: string;
|
|
19
|
+
className?: string;
|
|
20
|
+
height?: number | string;
|
|
21
|
+
label?: string;
|
|
22
|
+
loopDelayMs?: number;
|
|
23
|
+
outputLineDelayMs?: number;
|
|
24
|
+
sequence: TerminalDemoStep[];
|
|
25
|
+
}
|
|
26
|
+
export type TerminalDemoStep = ClearStep | OutputStep | PauseStep | PromptStep | TypeStep;
|
|
27
|
+
interface TypeStep {
|
|
28
|
+
delay?: number;
|
|
29
|
+
text: string;
|
|
30
|
+
type: 'type';
|
|
31
|
+
}
|
|
32
|
+
export {};
|
|
33
|
+
//# sourceMappingURL=terminal-demo.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"terminal-demo.d.ts","sourceRoot":"","sources":["../../../src/ui/components/terminal-demo.tsx"],"names":[],"mappings":"AAUA,wBAAgB,YAAY,CAAC,EAC3B,SAA2B,EAC3B,SAAS,EACT,MAAY,EACZ,KAAkB,EAClB,WAAkB,EAClB,iBAAsB,EACtB,QAAQ,EACT,EAAE,iBAAiB,2CAgInB;AAED,UAAU,SAAS;IACjB,IAAI,EAAE,OAAO,CAAA;CACd;AAED,UAAU,UAAU;IAClB,KAAK,EAAE,MAAM,EAAE,CAAA;IACf,IAAI,EAAE,QAAQ,CAAA;CACf;AAED,UAAU,SAAS;IACjB,EAAE,EAAE,MAAM,CAAA;IACV,IAAI,EAAE,OAAO,CAAA;CACd;AAED,UAAU,UAAU;IAClB,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,EAAE,QAAQ,CAAA;CACf;AAED,UAAU,iBAAiB;IACzB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,CAAA;IACxB,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,iBAAiB,CAAC,EAAE,MAAM,CAAA;IAC1B,QAAQ,EAAE,gBAAgB,EAAE,CAAA;CAC7B;AAED,MAAM,MAAM,gBAAgB,GACxB,SAAS,GACT,UAAU,GACV,SAAS,GACT,UAAU,GACV,QAAQ,CAAA;AAEZ,UAAU,QAAQ;IAChB,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,EAAE,MAAM,CAAA;CACb"}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
4
|
+
import { cn } from '../../utils';
|
|
5
|
+
function sleep(ms) {
|
|
6
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
7
|
+
}
|
|
8
|
+
export function TerminalDemo({ ariaLabel = 'Terminal Demo', className, height = 320, label = 'Terminal', loopDelayMs = 1000, outputLineDelayMs = 50, sequence }) {
|
|
9
|
+
const bodyRef = useRef(null);
|
|
10
|
+
const startedRef = useRef(false);
|
|
11
|
+
const [html, setHtml] = useState('');
|
|
12
|
+
const runDemo = useCallback(async () => {
|
|
13
|
+
if (startedRef.current) {
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
startedRef.current = true;
|
|
17
|
+
let content = '';
|
|
18
|
+
const render = (h) => {
|
|
19
|
+
content = h;
|
|
20
|
+
setHtml(h);
|
|
21
|
+
};
|
|
22
|
+
for (;;) {
|
|
23
|
+
for (const step of sequence) {
|
|
24
|
+
switch (step.type) {
|
|
25
|
+
case 'clear':
|
|
26
|
+
content = '';
|
|
27
|
+
render('');
|
|
28
|
+
break;
|
|
29
|
+
case 'output':
|
|
30
|
+
for (const line of step.lines) {
|
|
31
|
+
render(content + '\n' + line);
|
|
32
|
+
await sleep(outputLineDelayMs);
|
|
33
|
+
}
|
|
34
|
+
break;
|
|
35
|
+
case 'pause':
|
|
36
|
+
await sleep(step.ms);
|
|
37
|
+
break;
|
|
38
|
+
case 'prompt':
|
|
39
|
+
render(content + `<span class="text-midground">${step.text}</span>`);
|
|
40
|
+
break;
|
|
41
|
+
case 'type':
|
|
42
|
+
for (const char of step.text) {
|
|
43
|
+
render(content + char);
|
|
44
|
+
await sleep(step.delay ?? 30);
|
|
45
|
+
}
|
|
46
|
+
break;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
content = '';
|
|
50
|
+
render('');
|
|
51
|
+
await sleep(loopDelayMs);
|
|
52
|
+
}
|
|
53
|
+
}, [loopDelayMs, outputLineDelayMs, sequence]);
|
|
54
|
+
useEffect(() => {
|
|
55
|
+
const el = bodyRef.current?.closest('[data-demo-root]');
|
|
56
|
+
if (!el) {
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
const observer = new IntersectionObserver(entries => {
|
|
60
|
+
entries.forEach(entry => {
|
|
61
|
+
if (entry.isIntersecting) {
|
|
62
|
+
runDemo();
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
}, { threshold: 0.3 });
|
|
66
|
+
observer.observe(el);
|
|
67
|
+
return () => observer.disconnect();
|
|
68
|
+
}, [runDemo]);
|
|
69
|
+
useEffect(() => {
|
|
70
|
+
if (bodyRef.current) {
|
|
71
|
+
bodyRef.current.scrollTop = bodyRef.current.scrollHeight;
|
|
72
|
+
}
|
|
73
|
+
}, [html]);
|
|
74
|
+
return (_jsxs("div", { "aria-label": ariaLabel, className: cn('border-4 border-double border-inherit', className), "data-demo-root": true, role: "img", children: [_jsxs("div", { className: "flex items-center gap-3 border-b border-current/10 px-3 py-2", children: [_jsxs("div", { className: "flex gap-1.5", children: [_jsx("span", { className: "bg-midground size-2 rounded-full", style: { mixBlendMode: 'plus-lighter' } }), _jsx("span", { className: "bg-midground/60 size-2 rounded-full" }), _jsx("span", { className: "bg-midground/30 size-2 rounded-full" })] }), _jsx("span", { className: "font-courier text-[0.625rem] tracking-widest uppercase opacity-50", children: label })] }), _jsx("div", { className: cn('overflow-x-hidden overflow-y-auto whitespace-pre-wrap', 'font-courier p-4 text-[0.75rem] leading-[1.7] normal-case'), dangerouslySetInnerHTML: {
|
|
75
|
+
__html: html +
|
|
76
|
+
'<span class="blink inline-block dither ml-0.5 h-[1em] w-[1ch]"></span>'
|
|
77
|
+
}, ref: bodyRef, style: { height } })] }));
|
|
78
|
+
}
|
|
79
|
+
//# sourceMappingURL=terminal-demo.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"terminal-demo.js","sourceRoot":"","sources":["../../../src/ui/components/terminal-demo.tsx"],"names":[],"mappings":"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","sourcesContent":["'use client'\n\nimport { useCallback, useEffect, useRef, useState } from 'react'\n\nimport { cn } from '../../utils'\n\nfunction sleep(ms: number) {\n return new Promise<void>(resolve => setTimeout(resolve, ms))\n}\n\nexport function TerminalDemo({\n ariaLabel = 'Terminal Demo',\n className,\n height = 320,\n label = 'Terminal',\n loopDelayMs = 1000,\n outputLineDelayMs = 50,\n sequence\n}: TerminalDemoProps) {\n const bodyRef = useRef<HTMLDivElement>(null)\n const startedRef = useRef(false)\n const [html, setHtml] = useState('')\n\n const runDemo = useCallback(async () => {\n if (startedRef.current) {\n return\n }\n\n startedRef.current = true\n let content = ''\n\n const render = (h: string) => {\n content = h\n setHtml(h)\n }\n\n for (;;) {\n for (const step of sequence) {\n switch (step.type) {\n case 'clear':\n content = ''\n render('')\n\n break\n\n case 'output':\n for (const line of step.lines) {\n render(content + '\\n' + line)\n await sleep(outputLineDelayMs)\n }\n\n break\n\n case 'pause':\n await sleep(step.ms)\n\n break\n\n case 'prompt':\n render(content + `<span class=\"text-midground\">${step.text}</span>`)\n\n break\n\n case 'type':\n for (const char of step.text) {\n render(content + char)\n await sleep(step.delay ?? 30)\n }\n\n break\n }\n }\n\n content = ''\n render('')\n await sleep(loopDelayMs)\n }\n }, [loopDelayMs, outputLineDelayMs, sequence])\n\n useEffect(() => {\n const el = bodyRef.current?.closest('[data-demo-root]')\n\n if (!el) {\n return\n }\n\n const observer = new IntersectionObserver(\n entries => {\n entries.forEach(entry => {\n if (entry.isIntersecting) {\n runDemo()\n }\n })\n },\n { threshold: 0.3 }\n )\n\n observer.observe(el)\n\n return () => observer.disconnect()\n }, [runDemo])\n\n useEffect(() => {\n if (bodyRef.current) {\n bodyRef.current.scrollTop = bodyRef.current.scrollHeight\n }\n }, [html])\n\n return (\n <div\n aria-label={ariaLabel}\n className={cn('border-4 border-double border-inherit', className)}\n data-demo-root\n role=\"img\"\n >\n <div className=\"flex items-center gap-3 border-b border-current/10 px-3 py-2\">\n <div className=\"flex gap-1.5\">\n <span\n className=\"bg-midground size-2 rounded-full\"\n style={{ mixBlendMode: 'plus-lighter' }}\n />\n\n <span className=\"bg-midground/60 size-2 rounded-full\" />\n <span className=\"bg-midground/30 size-2 rounded-full\" />\n </div>\n\n <span className=\"font-courier text-[0.625rem] tracking-widest uppercase opacity-50\">\n {label}\n </span>\n </div>\n\n <div\n className={cn(\n 'overflow-x-hidden overflow-y-auto whitespace-pre-wrap',\n 'font-courier p-4 text-[0.75rem] leading-[1.7] normal-case'\n )}\n dangerouslySetInnerHTML={{\n __html:\n html +\n '<span class=\"blink inline-block dither ml-0.5 h-[1em] w-[1ch]\"></span>'\n }}\n ref={bodyRef}\n style={{ height }}\n />\n </div>\n )\n}\n\ninterface ClearStep {\n type: 'clear'\n}\n\ninterface OutputStep {\n lines: string[]\n type: 'output'\n}\n\ninterface PauseStep {\n ms: number\n type: 'pause'\n}\n\ninterface PromptStep {\n text: string\n type: 'prompt'\n}\n\ninterface TerminalDemoProps {\n ariaLabel?: string\n className?: string\n height?: number | string\n label?: string\n loopDelayMs?: number\n outputLineDelayMs?: number\n sequence: TerminalDemoStep[]\n}\n\nexport type TerminalDemoStep =\n | ClearStep\n | OutputStep\n | PauseStep\n | PromptStep\n | TypeStep\n\ninterface TypeStep {\n delay?: number\n text: string\n type: 'type'\n}\n"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"theme-toggle.d.ts","sourceRoot":"","sources":["../../../src/ui/components/theme-toggle.tsx"],"names":[],"mappings":"AAQA,wBAAgB,WAAW,CAAC,EAAE,SAAS,EAAE,KAAK,EAAE,EAAE,gBAAgB,2CAoDjE;AAED,UAAU,gBAAgB;IACxB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,KAAK,CAAC,EAAE,KAAK,CAAC,aAAa,CAAA;CAC5B"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useStore } from '@nanostores/react';
|
|
4
|
+
import { cn } from '../../utils';
|
|
5
|
+
import { $lightMode, toggleLens } from './overlays';
|
|
6
|
+
export function ThemeToggle({ className, style }) {
|
|
7
|
+
const light = useStore($lightMode);
|
|
8
|
+
return (_jsxs("button", { "aria-label": light ? 'Switch to dark mode' : 'Switch to light mode', className: cn('relative flex h-6 w-11 shrink-0 cursor-pointer items-center rounded-full', 'border border-current/25 bg-current/8 transition-colors', 'hover:bg-current/15', className), onClick: toggleLens, style: style, type: "button", children: [_jsxs("svg", { className: "absolute left-1 size-3.5 opacity-40", fill: "none", stroke: "currentColor", strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, viewBox: "0 0 24 24", children: [_jsx("circle", { cx: 12, cy: 12, r: 5 }), _jsx("path", { d: "M12 1v2M12 21v2M4.22 4.22l1.42 1.42M18.36 18.36l1.42 1.42M1 12h2M21 12h2M4.22 19.78l1.42-1.42M18.36 5.64l1.42-1.42" })] }), _jsx("svg", { className: "absolute right-1 size-3.5 opacity-40", fill: "none", stroke: "currentColor", strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, viewBox: "0 0 24 24", children: _jsx("path", { d: "M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z" }) }), _jsx("span", { "aria-hidden": true, className: cn('bg-midground absolute size-4 rounded-full', 'transition-transform duration-200 ease-out'), style: { transform: `translateX(${light ? 2 : 22}px)` } })] }));
|
|
9
|
+
}
|
|
10
|
+
//# sourceMappingURL=theme-toggle.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"theme-toggle.js","sourceRoot":"","sources":["../../../src/ui/components/theme-toggle.tsx"],"names":[],"mappings":"AAAA,YAAY,CAAA;;AAEZ,OAAO,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAA;AAE5C,OAAO,EAAE,EAAE,EAAE,MAAM,aAAa,CAAA;AAEhC,OAAO,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,YAAY,CAAA;AAEnD,MAAM,UAAU,WAAW,CAAC,EAAE,SAAS,EAAE,KAAK,EAAoB;IAChE,MAAM,KAAK,GAAG,QAAQ,CAAC,UAAU,CAAC,CAAA;IAElC,OAAO,CACL,gCACc,KAAK,CAAC,CAAC,CAAC,qBAAqB,CAAC,CAAC,CAAC,sBAAsB,EAClE,SAAS,EAAE,EAAE,CACX,0EAA0E,EAC1E,yDAAyD,EACzD,qBAAqB,EACrB,SAAS,CACV,EACD,OAAO,EAAE,UAAU,EACnB,KAAK,EAAE,KAAK,EACZ,IAAI,EAAC,QAAQ,aAEb,eACE,SAAS,EAAC,qCAAqC,EAC/C,IAAI,EAAC,MAAM,EACX,MAAM,EAAC,cAAc,EACrB,aAAa,EAAC,OAAO,EACrB,cAAc,EAAC,OAAO,EACtB,WAAW,EAAE,CAAC,EACd,OAAO,EAAC,WAAW,aAEnB,iBAAQ,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,CAAC,EAAE,CAAC,GAAI,EAEhC,eAAM,CAAC,EAAC,oHAAoH,GAAG,IAC3H,EAEN,cACE,SAAS,EAAC,sCAAsC,EAChD,IAAI,EAAC,MAAM,EACX,MAAM,EAAC,cAAc,EACrB,aAAa,EAAC,OAAO,EACrB,cAAc,EAAC,OAAO,EACtB,WAAW,EAAE,CAAC,EACd,OAAO,EAAC,WAAW,YAEnB,eAAM,CAAC,EAAC,iDAAiD,GAAG,GACxD,EAEN,oCAEE,SAAS,EAAE,EAAE,CACX,2CAA2C,EAC3C,4CAA4C,CAC7C,EACD,KAAK,EAAE,EAAE,SAAS,EAAE,cAAc,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,GACvD,IACK,CACV,CAAA;AACH,CAAC","sourcesContent":["'use client'\n\nimport { useStore } from '@nanostores/react'\n\nimport { cn } from '../../utils'\n\nimport { $lightMode, toggleLens } from './overlays'\n\nexport function ThemeToggle({ className, style }: ThemeToggleProps) {\n const light = useStore($lightMode)\n\n return (\n <button\n aria-label={light ? 'Switch to dark mode' : 'Switch to light mode'}\n className={cn(\n 'relative flex h-6 w-11 shrink-0 cursor-pointer items-center rounded-full',\n 'border border-current/25 bg-current/8 transition-colors',\n 'hover:bg-current/15',\n className\n )}\n onClick={toggleLens}\n style={style}\n type=\"button\"\n >\n <svg\n className=\"absolute left-1 size-3.5 opacity-40\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n viewBox=\"0 0 24 24\"\n >\n <circle cx={12} cy={12} r={5} />\n\n <path d=\"M12 1v2M12 21v2M4.22 4.22l1.42 1.42M18.36 18.36l1.42 1.42M1 12h2M21 12h2M4.22 19.78l1.42-1.42M18.36 5.64l1.42-1.42\" />\n </svg>\n\n <svg\n className=\"absolute right-1 size-3.5 opacity-40\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n viewBox=\"0 0 24 24\"\n >\n <path d=\"M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z\" />\n </svg>\n\n <span\n aria-hidden\n className={cn(\n 'bg-midground absolute size-4 rounded-full',\n 'transition-transform duration-200 ease-out'\n )}\n style={{ transform: `translateX(${light ? 2 : 22}px)` }}\n />\n </button>\n )\n}\n\ninterface ThemeToggleProps {\n className?: string\n style?: React.CSSProperties\n}\n"]}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Selectable tier / pricing card. Full-bleed distorted image background,
|
|
3
|
+
* readable overlay text, and an animated `.arc-border` shimmer on the
|
|
4
|
+
* selected state. Fully presentational — the consumer owns the data
|
|
5
|
+
* (tier schema, price formatting, tier imagery / tints).
|
|
6
|
+
*
|
|
7
|
+
* Visual states:
|
|
8
|
+
* - `selected`: brightens the distortion, activates `.arc-border`, and
|
|
9
|
+
* composites the headline / price with `mix-blend-mode: plus-lighter`
|
|
10
|
+
* so the text lifts off the image regardless of tint.
|
|
11
|
+
* - `isCurrent`: subtle midground-tinted border hint (suppressed when
|
|
12
|
+
* `selected` wins).
|
|
13
|
+
* - `overlay`: optional top-layer color blended with `mix-blend-mode:
|
|
14
|
+
* color` — used for the "highest tier" red treatment on top of any
|
|
15
|
+
* base tint.
|
|
16
|
+
*/
|
|
17
|
+
export declare function TierCard({ badge, bullets, className, image, isCurrent, onSelect, overlay, price, selected, tint, tintStrength, title }: TierCardProps): import("react/jsx-runtime").JSX.Element;
|
|
18
|
+
export interface TierCardPrice {
|
|
19
|
+
/** Headline price, e.g. `"$20"` or `"Free"`. */
|
|
20
|
+
primary: string;
|
|
21
|
+
/** Small suffix rendered after `primary`, e.g. `"/mo"` or `"first payment"`. */
|
|
22
|
+
primarySuffix?: string;
|
|
23
|
+
/** Optional struck-through comparison price rendered above `primary`, e.g. `"$30"`. */
|
|
24
|
+
secondary?: string;
|
|
25
|
+
/** Small suffix rendered after `secondary`. */
|
|
26
|
+
secondarySuffix?: string;
|
|
27
|
+
}
|
|
28
|
+
export interface TierCardProps {
|
|
29
|
+
/** Small annotation after the title, e.g. `"(current)"`. */
|
|
30
|
+
badge?: React.ReactNode;
|
|
31
|
+
/** Feature list rendered under the price. */
|
|
32
|
+
bullets: React.ReactNode[];
|
|
33
|
+
className?: string;
|
|
34
|
+
/** Background image URL. */
|
|
35
|
+
image: string;
|
|
36
|
+
/** Applies the "current plan" border hint when not `selected`. */
|
|
37
|
+
isCurrent?: boolean;
|
|
38
|
+
onSelect?: () => void;
|
|
39
|
+
/** Color blended with `mix-blend-mode: color` over the image (used for the highest-tier red treatment). */
|
|
40
|
+
overlay?: string;
|
|
41
|
+
price: TierCardPrice;
|
|
42
|
+
/** Applies selected chrome (arc-border shimmer, active distortion, plus-lighter text blend). */
|
|
43
|
+
selected?: boolean;
|
|
44
|
+
/** Shader tint passed through to `ImageDistortion`. */
|
|
45
|
+
tint?: string;
|
|
46
|
+
/** Active / inactive tint strength passed through to `ImageDistortion`. */
|
|
47
|
+
tintStrength?: {
|
|
48
|
+
active: number;
|
|
49
|
+
inactive: number;
|
|
50
|
+
};
|
|
51
|
+
/** Tier name / headline. */
|
|
52
|
+
title: React.ReactNode;
|
|
53
|
+
}
|
|
54
|
+
//# sourceMappingURL=tier-card.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tier-card.d.ts","sourceRoot":"","sources":["../../../src/ui/components/tier-card.tsx"],"names":[],"mappings":"AAQA;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,QAAQ,CAAC,EACvB,KAAK,EACL,OAAO,EACP,SAAS,EACT,KAAK,EACL,SAAiB,EACjB,QAAQ,EACR,OAAO,EACP,KAAK,EACL,QAAgB,EAChB,IAAI,EACJ,YAAY,EACZ,KAAK,EACN,EAAE,aAAa,2CAsHf;AAED,MAAM,WAAW,aAAa;IAC5B,gDAAgD;IAChD,OAAO,EAAE,MAAM,CAAA;IACf,gFAAgF;IAChF,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,uFAAuF;IACvF,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,+CAA+C;IAC/C,eAAe,CAAC,EAAE,MAAM,CAAA;CACzB;AAED,MAAM,WAAW,aAAa;IAC5B,4DAA4D;IAC5D,KAAK,CAAC,EAAE,KAAK,CAAC,SAAS,CAAA;IACvB,6CAA6C;IAC7C,OAAO,EAAE,KAAK,CAAC,SAAS,EAAE,CAAA;IAC1B,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,4BAA4B;IAC5B,KAAK,EAAE,MAAM,CAAA;IACb,kEAAkE;IAClE,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAA;IACrB,2GAA2G;IAC3G,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,KAAK,EAAE,aAAa,CAAA;IACpB,gGAAgG;IAChG,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,uDAAuD;IACvD,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,2EAA2E;IAC3E,YAAY,CAAC,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAA;IACnD,4BAA4B;IAC5B,KAAK,EAAE,KAAK,CAAC,SAAS,CAAA;CACvB"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
import { cn } from '../../utils';
|
|
4
|
+
import { ImageDistortion } from './image-distortion';
|
|
5
|
+
import { Typography } from './typography';
|
|
6
|
+
import { Small } from './typography/small';
|
|
7
|
+
/**
|
|
8
|
+
* Selectable tier / pricing card. Full-bleed distorted image background,
|
|
9
|
+
* readable overlay text, and an animated `.arc-border` shimmer on the
|
|
10
|
+
* selected state. Fully presentational — the consumer owns the data
|
|
11
|
+
* (tier schema, price formatting, tier imagery / tints).
|
|
12
|
+
*
|
|
13
|
+
* Visual states:
|
|
14
|
+
* - `selected`: brightens the distortion, activates `.arc-border`, and
|
|
15
|
+
* composites the headline / price with `mix-blend-mode: plus-lighter`
|
|
16
|
+
* so the text lifts off the image regardless of tint.
|
|
17
|
+
* - `isCurrent`: subtle midground-tinted border hint (suppressed when
|
|
18
|
+
* `selected` wins).
|
|
19
|
+
* - `overlay`: optional top-layer color blended with `mix-blend-mode:
|
|
20
|
+
* color` — used for the "highest tier" red treatment on top of any
|
|
21
|
+
* base tint.
|
|
22
|
+
*/
|
|
23
|
+
export function TierCard({ badge, bullets, className, image, isCurrent = false, onSelect, overlay, price, selected = false, tint, tintStrength, title }) {
|
|
24
|
+
return (_jsxs("button", { className: cn('group relative flex w-full cursor-pointer flex-col border border-current/20', 'text-left transition-colors duration-300', selected && 'border-midground/60', isCurrent && !selected && 'border-midground/30', className), onClick: onSelect, type: "button", children: [_jsx("span", { "aria-hidden": true, className: cn('arc-border transition-opacity duration-200', selected ? 'opacity-100' : 'opacity-0 group-hover:opacity-100') }), _jsxs("div", { className: "relative aspect-[3/4] min-h-0 w-full flex-1 overflow-hidden", style: { backgroundColor: 'var(--background)' }, children: [_jsx(ImageDistortion, { active: selected, src: image, tint: tint, tintStrength: tintStrength }), overlay && (_jsx("div", { className: "pointer-events-none absolute inset-0", style: { backgroundColor: overlay, mixBlendMode: 'color' } })), _jsxs("div", { className: "pointer-events-none absolute inset-0 z-[1] flex flex-col justify-between p-3", children: [_jsxs("div", { className: "flex flex-col gap-0.5", children: [_jsxs(Small, { className: cn('block drop-shadow-[0_1px_2px_rgba(0,0,0,0.5)]', 'transition-colors', selected && 'text-midground'), style: selected ? { mixBlendMode: 'plus-lighter' } : undefined, children: [title, badge && _jsx("span", { className: "ml-1 opacity-50", children: badge })] }), price.secondary ? (_jsxs(_Fragment, { children: [_jsxs(Typography, { className: "block text-sm line-through opacity-50 drop-shadow-[0_1px_3px_rgba(0,0,0,0.6)]", expanded: true, style: { mixBlendMode: 'plus-lighter' }, children: [price.secondary, price.secondarySuffix && (_jsx("span", { className: "text-[0.625rem]", children: price.secondarySuffix }))] }), _jsxs(Typography, { className: "block text-lg font-bold drop-shadow-[0_1px_3px_rgba(0,0,0,0.6)]", expanded: true, style: { mixBlendMode: 'plus-lighter' }, children: [price.primary, price.primarySuffix && (_jsxs("span", { className: "text-[0.625rem] opacity-60", children: [' ', price.primarySuffix] }))] })] })) : (_jsxs(Typography, { className: "block text-lg font-bold drop-shadow-[0_1px_3px_rgba(0,0,0,0.6)]", expanded: true, style: { mixBlendMode: 'plus-lighter' }, children: [price.primary, price.primarySuffix && (_jsx("span", { className: "text-[0.625rem] opacity-60", children: price.primarySuffix }))] }))] }), bullets.length > 0 && (_jsx("ul", { className: "flex flex-col gap-1", children: bullets.map((bullet, i) => (_jsxs("li", { className: cn('font-courier text-[0.6875rem] leading-tight tracking-wider uppercase', 'drop-shadow-[0_1px_2px_rgba(0,0,0,0.5)]', 'opacity-70'), children: ["\u00B7 ", bullet] }, typeof bullet === 'string' ? bullet : i))) }))] })] })] }));
|
|
25
|
+
}
|
|
26
|
+
//# sourceMappingURL=tier-card.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tier-card.js","sourceRoot":"","sources":["../../../src/ui/components/tier-card.tsx"],"names":[],"mappings":"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","sourcesContent":["'use client'\n\nimport { cn } from '../../utils'\n\nimport { ImageDistortion } from './image-distortion'\nimport { Typography } from './typography'\nimport { Small } from './typography/small'\n\n/**\n * Selectable tier / pricing card. Full-bleed distorted image background,\n * readable overlay text, and an animated `.arc-border` shimmer on the\n * selected state. Fully presentational — the consumer owns the data\n * (tier schema, price formatting, tier imagery / tints).\n *\n * Visual states:\n * - `selected`: brightens the distortion, activates `.arc-border`, and\n * composites the headline / price with `mix-blend-mode: plus-lighter`\n * so the text lifts off the image regardless of tint.\n * - `isCurrent`: subtle midground-tinted border hint (suppressed when\n * `selected` wins).\n * - `overlay`: optional top-layer color blended with `mix-blend-mode:\n * color` — used for the \"highest tier\" red treatment on top of any\n * base tint.\n */\nexport function TierCard({\n badge,\n bullets,\n className,\n image,\n isCurrent = false,\n onSelect,\n overlay,\n price,\n selected = false,\n tint,\n tintStrength,\n title\n}: TierCardProps) {\n return (\n <button\n className={cn(\n 'group relative flex w-full cursor-pointer flex-col border border-current/20',\n 'text-left transition-colors duration-300',\n selected && 'border-midground/60',\n isCurrent && !selected && 'border-midground/30',\n className\n )}\n onClick={onSelect}\n type=\"button\"\n >\n <span\n aria-hidden\n className={cn(\n 'arc-border transition-opacity duration-200',\n selected ? 'opacity-100' : 'opacity-0 group-hover:opacity-100'\n )}\n />\n\n <div\n className=\"relative aspect-[3/4] min-h-0 w-full flex-1 overflow-hidden\"\n style={{ backgroundColor: 'var(--background)' }}\n >\n <ImageDistortion\n active={selected}\n src={image}\n tint={tint}\n tintStrength={tintStrength}\n />\n\n {overlay && (\n <div\n className=\"pointer-events-none absolute inset-0\"\n style={{ backgroundColor: overlay, mixBlendMode: 'color' }}\n />\n )}\n\n <div className=\"pointer-events-none absolute inset-0 z-[1] flex flex-col justify-between p-3\">\n <div className=\"flex flex-col gap-0.5\">\n <Small\n className={cn(\n 'block drop-shadow-[0_1px_2px_rgba(0,0,0,0.5)]',\n 'transition-colors',\n selected && 'text-midground'\n )}\n style={selected ? { mixBlendMode: 'plus-lighter' } : undefined}\n >\n {title}\n {badge && <span className=\"ml-1 opacity-50\">{badge}</span>}\n </Small>\n\n {price.secondary ? (\n <>\n <Typography\n className=\"block text-sm line-through opacity-50 drop-shadow-[0_1px_3px_rgba(0,0,0,0.6)]\"\n expanded\n style={{ mixBlendMode: 'plus-lighter' }}\n >\n {price.secondary}\n {price.secondarySuffix && (\n <span className=\"text-[0.625rem]\">\n {price.secondarySuffix}\n </span>\n )}\n </Typography>\n\n <Typography\n className=\"block text-lg font-bold drop-shadow-[0_1px_3px_rgba(0,0,0,0.6)]\"\n expanded\n style={{ mixBlendMode: 'plus-lighter' }}\n >\n {price.primary}\n {price.primarySuffix && (\n <span className=\"text-[0.625rem] opacity-60\">\n {' '}\n {price.primarySuffix}\n </span>\n )}\n </Typography>\n </>\n ) : (\n <Typography\n className=\"block text-lg font-bold drop-shadow-[0_1px_3px_rgba(0,0,0,0.6)]\"\n expanded\n style={{ mixBlendMode: 'plus-lighter' }}\n >\n {price.primary}\n {price.primarySuffix && (\n <span className=\"text-[0.625rem] opacity-60\">\n {price.primarySuffix}\n </span>\n )}\n </Typography>\n )}\n </div>\n\n {bullets.length > 0 && (\n <ul className=\"flex flex-col gap-1\">\n {bullets.map((bullet, i) => (\n <li\n className={cn(\n 'font-courier text-[0.6875rem] leading-tight tracking-wider uppercase',\n 'drop-shadow-[0_1px_2px_rgba(0,0,0,0.5)]',\n 'opacity-70'\n )}\n key={typeof bullet === 'string' ? bullet : i}\n >\n · {bullet}\n </li>\n ))}\n </ul>\n )}\n </div>\n </div>\n </button>\n )\n}\n\nexport interface TierCardPrice {\n /** Headline price, e.g. `\"$20\"` or `\"Free\"`. */\n primary: string\n /** Small suffix rendered after `primary`, e.g. `\"/mo\"` or `\"first payment\"`. */\n primarySuffix?: string\n /** Optional struck-through comparison price rendered above `primary`, e.g. `\"$30\"`. */\n secondary?: string\n /** Small suffix rendered after `secondary`. */\n secondarySuffix?: string\n}\n\nexport interface TierCardProps {\n /** Small annotation after the title, e.g. `\"(current)\"`. */\n badge?: React.ReactNode\n /** Feature list rendered under the price. */\n bullets: React.ReactNode[]\n className?: string\n /** Background image URL. */\n image: string\n /** Applies the \"current plan\" border hint when not `selected`. */\n isCurrent?: boolean\n onSelect?: () => void\n /** Color blended with `mix-blend-mode: color` over the image (used for the highest-tier red treatment). */\n overlay?: string\n price: TierCardPrice\n /** Applies selected chrome (arc-border shimmer, active distortion, plus-lighter text blend). */\n selected?: boolean\n /** Shader tint passed through to `ImageDistortion`. */\n tint?: string\n /** Active / inactive tint strength passed through to `ImageDistortion`. */\n tintStrength?: { active: number; inactive: number }\n /** Tier name / headline. */\n title: React.ReactNode\n}\n"]}
|