@nous-research/ui 0.15.0 → 0.16.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/CHANGELOG.md +227 -0
- package/README.md +24 -4
- package/dist/fonts.js +1 -0
- package/dist/hooks/use-capped-frame.js +1 -0
- package/dist/hooks/use-css-var-dims.js +1 -0
- package/dist/hooks/use-gpu-tier.js +1 -0
- package/dist/hooks/use-render-loop.js +1 -0
- package/dist/hooks/use-smooth-controls.js +1 -0
- package/dist/index.js +1 -0
- package/dist/ui/basic-page.js +1 -0
- package/dist/ui/components/animated-count.js +1 -0
- package/dist/ui/components/ascii.js +1 -0
- package/dist/ui/components/badge.js +2 -1
- package/dist/ui/components/badges/nous-girl.js +1 -0
- package/dist/ui/components/blend-mode.js +1 -0
- package/dist/ui/components/blink.js +1 -0
- package/dist/ui/components/button.js +2 -1
- package/dist/ui/components/checkbox.js +1 -0
- package/dist/ui/components/command-block.js +4 -3
- package/dist/ui/components/cursor.js +1 -0
- package/dist/ui/components/dropdown-menu.js +1 -0
- package/dist/ui/components/fit-text/index.js +1 -0
- package/dist/ui/components/graphs/bar-chart.js +1 -0
- package/dist/ui/components/graphs/index.js +1 -0
- package/dist/ui/components/graphs/line-chart.js +1 -0
- package/dist/ui/components/graphs/utils.js +1 -0
- package/dist/ui/components/grid/index.js +1 -0
- package/dist/ui/components/hover-bg.js +1 -0
- package/dist/ui/components/icons/arrow.js +1 -0
- package/dist/ui/components/icons/check.js +1 -0
- package/dist/ui/components/icons/chevron.js +1 -0
- package/dist/ui/components/icons/discord.js +1 -0
- package/dist/ui/components/icons/eye.js +1 -0
- package/dist/ui/components/icons/gear.js +1 -0
- package/dist/ui/components/icons/github.js +1 -0
- package/dist/ui/components/icons/hamburger.js +1 -0
- package/dist/ui/components/icons/heart.js +1 -0
- package/dist/ui/components/icons/index.js +1 -0
- package/dist/ui/components/icons/link.js +1 -0
- package/dist/ui/components/icons/minus.js +1 -0
- package/dist/ui/components/icons/search.js +1 -0
- package/dist/ui/components/image-distortion.js +1 -0
- package/dist/ui/components/leva-client.js +1 -0
- package/dist/ui/components/list-item.js +3 -2
- package/dist/ui/components/modal/index.js +1 -0
- package/dist/ui/components/modal/modal.css +1 -1
- package/dist/ui/components/overlays/blend-modes.js +1 -0
- package/dist/ui/components/overlays/glitch.js +1 -0
- package/dist/ui/components/overlays/greys.js +1 -0
- package/dist/ui/components/overlays/index.js +1 -0
- package/dist/ui/components/overlays/lens-layers.js +1 -0
- package/dist/ui/components/overlays/lens.js +1 -0
- package/dist/ui/components/overlays/noise.js +1 -0
- package/dist/ui/components/overlays/vignette.js +1 -0
- package/dist/ui/components/poster.js +1 -0
- package/dist/ui/components/progress.js +1 -0
- package/dist/ui/components/scene-canvas.js +1 -0
- package/dist/ui/components/scramble.js +1 -0
- package/dist/ui/components/segmented.js +5 -4
- package/dist/ui/components/select.js +1 -0
- package/dist/ui/components/selection-switcher.js +1 -0
- package/dist/ui/components/shader.js +1 -0
- package/dist/ui/components/socials.js +1 -0
- package/dist/ui/components/spinner.js +1 -0
- package/dist/ui/components/stats.js +2 -1
- package/dist/ui/components/switch.js +1 -0
- package/dist/ui/components/tabs.js +4 -3
- package/dist/ui/components/terminal-demo.js +2 -1
- package/dist/ui/components/theme-toggle.js +1 -0
- package/dist/ui/components/tier-card.js +2 -1
- package/dist/ui/components/tv.js +1 -0
- package/dist/ui/components/typography/h1.js +1 -0
- package/dist/ui/components/typography/h2.js +1 -0
- package/dist/ui/components/typography/index.js +1 -0
- package/dist/ui/components/typography/legend.js +1 -0
- package/dist/ui/components/typography/small.js +1 -0
- package/dist/ui/components/watchlist.js +2 -1
- package/dist/ui/footer.js +1 -0
- package/dist/ui/globals.css +33 -1
- package/dist/ui/header.js +1 -0
- package/dist/ui/layout-wrapper.js +2 -1
- package/dist/utils/color.js +1 -0
- package/dist/utils/index.js +1 -0
- package/dist/utils/poly.js +1 -0
- package/package.json +4 -2
- package/src/assets/filler-bg0.webp +0 -0
- package/src/assets.d.ts +38 -0
- package/src/fonts/Collapse-Bold.woff2 +0 -0
- package/src/fonts/Collapse-BoldItalic.woff2 +0 -0
- package/src/fonts/Collapse-Italic.woff2 +0 -0
- package/src/fonts/Collapse-Light.woff2 +0 -0
- package/src/fonts/Collapse-LightItalic.woff2 +0 -0
- package/src/fonts/Collapse-Regular.woff2 +0 -0
- package/src/fonts/Collapse-Thin.woff2 +0 -0
- package/src/fonts/Collapse-ThinItalic.woff2 +0 -0
- package/src/fonts/Mondwest-Regular.woff2 +0 -0
- package/src/fonts/Neuebit-Bold.woff2 +0 -0
- package/src/fonts/RulesCompressed-Medium.woff2 +0 -0
- package/src/fonts/RulesCompressed-Regular.woff2 +0 -0
- package/src/fonts/RulesExpanded-Bold.woff2 +0 -0
- package/src/fonts/RulesExpanded-Regular.woff2 +0 -0
- package/src/fonts.ts +6 -0
- package/src/hooks/use-capped-frame.ts +18 -0
- package/src/hooks/use-css-var-dims.ts +39 -0
- package/src/hooks/use-gpu-tier.ts +165 -0
- package/src/hooks/use-render-loop.ts +121 -0
- package/src/hooks/use-smooth-controls.ts +318 -0
- package/src/index.ts +109 -0
- package/src/ui/basic-page.tsx +34 -0
- package/src/ui/build.css +4 -0
- package/src/ui/components/animated-count.stories.tsx +67 -0
- package/src/ui/components/animated-count.tsx +168 -0
- package/src/ui/components/ascii.stories.tsx +30 -0
- package/src/ui/components/ascii.tsx +110 -0
- package/src/ui/components/badge.stories.tsx +31 -0
- package/src/ui/components/badge.tsx +60 -0
- package/src/ui/components/badges/nous-girl.tsx +52 -0
- package/src/ui/components/blend-mode.stories.tsx +33 -0
- package/src/ui/components/blend-mode.tsx +129 -0
- package/src/ui/components/blink.stories.tsx +32 -0
- package/src/ui/components/blink.tsx +21 -0
- package/src/ui/components/button.stories.tsx +68 -0
- package/src/ui/components/button.tsx +170 -0
- package/src/ui/components/checkbox.stories.tsx +113 -0
- package/src/ui/components/checkbox.tsx +36 -0
- package/src/ui/components/command-block.stories.tsx +52 -0
- package/src/ui/components/command-block.tsx +86 -0
- package/src/ui/components/cursor.tsx +115 -0
- package/src/ui/components/dropdown-menu.stories.tsx +52 -0
- package/src/ui/components/dropdown-menu.tsx +117 -0
- package/src/ui/components/fit-text/fit-text.css +42 -0
- package/src/ui/components/fit-text/index.stories.tsx +33 -0
- package/src/ui/components/fit-text/index.tsx +45 -0
- package/src/ui/components/graphs/bar-chart.tsx +153 -0
- package/src/ui/components/graphs/index.stories.tsx +64 -0
- package/src/ui/components/graphs/index.tsx +4 -0
- package/src/ui/components/graphs/line-chart.tsx +213 -0
- package/src/ui/components/graphs/utils.tsx +265 -0
- package/src/ui/components/grid/grid.css +79 -0
- package/src/ui/components/grid/index.tsx +19 -0
- package/src/ui/components/hover-bg.stories.tsx +29 -0
- package/src/ui/components/hover-bg.tsx +15 -0
- package/src/ui/components/icons/arrow.tsx +42 -0
- package/src/ui/components/icons/check.tsx +14 -0
- package/src/ui/components/icons/chevron.tsx +45 -0
- package/src/ui/components/icons/discord.tsx +16 -0
- package/src/ui/components/icons/eye.tsx +12 -0
- package/src/ui/components/icons/gear.tsx +51 -0
- package/src/ui/components/icons/github.tsx +16 -0
- package/src/ui/components/icons/hamburger.tsx +52 -0
- package/src/ui/components/icons/heart.tsx +12 -0
- package/src/ui/components/icons/index.ts +12 -0
- package/src/ui/components/icons/link.tsx +14 -0
- package/src/ui/components/icons/minus.tsx +14 -0
- package/src/ui/components/icons/search.tsx +28 -0
- package/src/ui/components/image-distortion.stories.tsx +120 -0
- package/src/ui/components/image-distortion.tsx +498 -0
- package/src/ui/components/leva-client.tsx +14 -0
- package/src/ui/components/list-item.stories.tsx +83 -0
- package/src/ui/components/list-item.tsx +37 -0
- package/src/ui/components/modal/index.stories.tsx +46 -0
- package/src/ui/components/modal/index.tsx +48 -0
- package/src/ui/components/modal/modal.css +36 -0
- package/src/ui/components/overlays/blend-modes.ts +13 -0
- package/src/ui/components/overlays/glitch.tsx +243 -0
- package/src/ui/components/overlays/greys.tsx +386 -0
- package/src/ui/components/overlays/index.tsx +47 -0
- package/src/ui/components/overlays/lens-layers.tsx +119 -0
- package/src/ui/components/overlays/lens.ts +91 -0
- package/src/ui/components/overlays/noise.tsx +174 -0
- package/src/ui/components/overlays/vignette.tsx +60 -0
- package/src/ui/components/poster.stories.tsx +513 -0
- package/src/ui/components/poster.tsx +411 -0
- package/src/ui/components/progress.stories.tsx +48 -0
- package/src/ui/components/progress.tsx +56 -0
- package/src/ui/components/scene-canvas.tsx +254 -0
- package/src/ui/components/scramble.stories.tsx +49 -0
- package/src/ui/components/scramble.tsx +95 -0
- package/src/ui/components/segmented.stories.tsx +101 -0
- package/src/ui/components/segmented.tsx +81 -0
- package/src/ui/components/select.stories.tsx +88 -0
- package/src/ui/components/select.tsx +267 -0
- package/src/ui/components/selection-switcher.tsx +44 -0
- package/src/ui/components/shader.tsx +83 -0
- package/src/ui/components/socials.tsx +42 -0
- package/src/ui/components/spinner.stories.tsx +101 -0
- package/src/ui/components/spinner.tsx +60 -0
- package/src/ui/components/stats.stories.tsx +24 -0
- package/src/ui/components/stats.tsx +53 -0
- package/src/ui/components/switch.stories.tsx +77 -0
- package/src/ui/components/switch.tsx +48 -0
- package/src/ui/components/tabs.stories.tsx +101 -0
- package/src/ui/components/tabs.tsx +66 -0
- package/src/ui/components/terminal-demo.stories.tsx +67 -0
- package/src/ui/components/terminal-demo.tsx +189 -0
- package/src/ui/components/theme-toggle.stories.tsx +47 -0
- package/src/ui/components/theme-toggle.tsx +66 -0
- package/src/ui/components/tier-card.stories.tsx +217 -0
- package/src/ui/components/tier-card.tsx +190 -0
- package/src/ui/components/tv.stories.tsx +37 -0
- package/src/ui/components/tv.tsx +257 -0
- package/src/ui/components/typography/h1.tsx +18 -0
- package/src/ui/components/typography/h2.tsx +18 -0
- package/src/ui/components/typography/index.tsx +54 -0
- package/src/ui/components/typography/legend.tsx +24 -0
- package/src/ui/components/typography/small.tsx +11 -0
- package/src/ui/components/watchlist.stories.tsx +33 -0
- package/src/ui/components/watchlist.tsx +105 -0
- package/src/ui/fonts.css +63 -0
- package/src/ui/footer.tsx +111 -0
- package/src/ui/globals.css +383 -0
- package/src/ui/header.tsx +398 -0
- package/src/ui/layout-wrapper.tsx +11 -0
- package/src/utils/color.ts +21 -0
- package/src/utils/index.ts +62 -0
- package/src/utils/poly.ts +26 -0
|
@@ -0,0 +1,411 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState } from 'react'
|
|
4
|
+
|
|
5
|
+
import fillerBg from '../../assets/filler-bg0.webp'
|
|
6
|
+
import { cn } from '../../utils'
|
|
7
|
+
|
|
8
|
+
import { Blink } from './blink'
|
|
9
|
+
import { ImageDistortion } from './image-distortion'
|
|
10
|
+
import { Typography } from './typography'
|
|
11
|
+
import { Small } from './typography/small'
|
|
12
|
+
|
|
13
|
+
import type { AutoPlayPattern } from './image-distortion'
|
|
14
|
+
|
|
15
|
+
const ASPECT_CONFIG: Record<
|
|
16
|
+
PosterAspect,
|
|
17
|
+
{ defaultLayout: 'split' | 'stacked'; height: number; width: number }
|
|
18
|
+
> = {
|
|
19
|
+
landscape: { defaultLayout: 'split', height: 1080, width: 1920 },
|
|
20
|
+
portrait: { defaultLayout: 'split', height: 1350, width: 1080 },
|
|
21
|
+
square: { defaultLayout: 'split', height: 1080, width: 1080 },
|
|
22
|
+
story: { defaultLayout: 'stacked', height: 1920, width: 1080 },
|
|
23
|
+
wide: { defaultLayout: 'split', height: 900, width: 1600 }
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const DEFAULT_SRC =
|
|
27
|
+
(fillerBg as { src?: string }).src ?? (fillerBg as unknown as string)
|
|
28
|
+
|
|
29
|
+
function useUtcClock() {
|
|
30
|
+
const [now, setNow] = useState<Date | null>(null)
|
|
31
|
+
|
|
32
|
+
useEffect(() => {
|
|
33
|
+
setNow(new Date())
|
|
34
|
+
const id = setInterval(() => setNow(new Date()), 1000)
|
|
35
|
+
|
|
36
|
+
return () => clearInterval(id)
|
|
37
|
+
}, [])
|
|
38
|
+
|
|
39
|
+
return now ? now.toISOString().slice(11, 19) : '--:--:--'
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function CornerMark({ className }: { className?: string }) {
|
|
43
|
+
return (
|
|
44
|
+
<span
|
|
45
|
+
aria-hidden
|
|
46
|
+
className={cn(
|
|
47
|
+
'pointer-events-none absolute block size-4 opacity-50',
|
|
48
|
+
className
|
|
49
|
+
)}
|
|
50
|
+
>
|
|
51
|
+
<span className="absolute top-1/2 left-0 h-px w-full -translate-y-1/2 bg-current" />
|
|
52
|
+
|
|
53
|
+
<span className="absolute top-0 left-1/2 h-full w-px -translate-x-1/2 bg-current" />
|
|
54
|
+
</span>
|
|
55
|
+
)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function ChannelDot() {
|
|
59
|
+
return (
|
|
60
|
+
<span className="flex items-center gap-1.5">
|
|
61
|
+
<span className="bg-midground size-1.5 animate-pulse rounded-full" />
|
|
62
|
+
|
|
63
|
+
<Small className="opacity-70">REC</Small>
|
|
64
|
+
</span>
|
|
65
|
+
)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function ScanlineOverlay() {
|
|
69
|
+
return (
|
|
70
|
+
<div
|
|
71
|
+
aria-hidden
|
|
72
|
+
className="pointer-events-none absolute inset-0 opacity-20 mix-blend-overlay"
|
|
73
|
+
style={{
|
|
74
|
+
backgroundImage:
|
|
75
|
+
'repeating-linear-gradient(0deg, transparent 0, transparent 2px, rgba(255,255,255,0.08) 2px, rgba(255,255,255,0.08) 3px)'
|
|
76
|
+
}}
|
|
77
|
+
/>
|
|
78
|
+
)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Social-ready glitchy card built around the haptic-distortion image
|
|
83
|
+
* component. The poster runs the sword-guy distortion on an auto-animated
|
|
84
|
+
* slash pattern so it can be screen-recorded as a GIF without a human
|
|
85
|
+
* moving a cursor.
|
|
86
|
+
*
|
|
87
|
+
* Two variants, matching actual use cases:
|
|
88
|
+
* - `'vibe'` (default): full-bleed distorted image with just registration
|
|
89
|
+
* marks and a tiny "Hermes Agent" mark in the corner — mirrors the
|
|
90
|
+
* overlay on the Hermes agent website.
|
|
91
|
+
* - `'dispatch'`: broadcast-card layout with sidebar copy, numbered tags,
|
|
92
|
+
* and chrome — for when the poster needs to carry information.
|
|
93
|
+
*/
|
|
94
|
+
export function Poster({
|
|
95
|
+
aspect = 'square',
|
|
96
|
+
autoPlay = 'slash',
|
|
97
|
+
body,
|
|
98
|
+
border = true,
|
|
99
|
+
channel,
|
|
100
|
+
children,
|
|
101
|
+
className,
|
|
102
|
+
cornerMarks = true,
|
|
103
|
+
eyebrow,
|
|
104
|
+
headline = ['An Agent', 'That Grows', 'With You.'],
|
|
105
|
+
layout,
|
|
106
|
+
scale = 1,
|
|
107
|
+
seal = 'MIT · 2026',
|
|
108
|
+
signature,
|
|
109
|
+
src = DEFAULT_SRC,
|
|
110
|
+
tags,
|
|
111
|
+
tint,
|
|
112
|
+
tintStrength,
|
|
113
|
+
variant = 'vibe',
|
|
114
|
+
...rest
|
|
115
|
+
}: PosterProps) {
|
|
116
|
+
const config = ASPECT_CONFIG[aspect]
|
|
117
|
+
const resolvedLayout = layout ?? config.defaultLayout
|
|
118
|
+
|
|
119
|
+
// Use aspect-ratio + max-width/height so the poster fluidly fits any parent
|
|
120
|
+
// (storybook iframe, a tweet preview, an embed) without getting clipped,
|
|
121
|
+
// but caps at the intended export width for screen-recording. `maxHeight`
|
|
122
|
+
// uses an absolute `dvh`-based value rather than `%` because `%` inside a
|
|
123
|
+
// flex container can cause the browser to clamp height without re-running
|
|
124
|
+
// aspect-ratio on width, producing a subtly wrong shape. An absolute cap
|
|
125
|
+
// leaves aspect-ratio fully in charge: once the height binds, width is
|
|
126
|
+
// re-derived correctly. `calc(100dvh - 8rem)` = viewport minus a typical
|
|
127
|
+
// host's vertical padding (e.g. Storybook's `p-8` = 4rem on each side),
|
|
128
|
+
// so the poster + padding fit within the viewport without ever producing
|
|
129
|
+
// scrollbars. Container queries tie all internal typography to the
|
|
130
|
+
// actual rendered width so headline/metadata scales along with the canvas.
|
|
131
|
+
const outerProps = {
|
|
132
|
+
// `text-midground` (not `text-foreground`) is the readable on-canvas
|
|
133
|
+
// color across every lens. `--foreground` is really the lens's inversion
|
|
134
|
+
// layer color: on dark lenses it has `fgOpacity: 0` and resolves to
|
|
135
|
+
// fully-transparent via `color-mix`, which would make text invisible.
|
|
136
|
+
// `--midground` always has opacity 1 and picks up each lens's accent.
|
|
137
|
+
className: cn(
|
|
138
|
+
'text-midground relative overflow-hidden font-sans',
|
|
139
|
+
border && 'border border-current/25',
|
|
140
|
+
className
|
|
141
|
+
),
|
|
142
|
+
style: {
|
|
143
|
+
aspectRatio: `${config.width} / ${config.height}`,
|
|
144
|
+
background: 'var(--background)',
|
|
145
|
+
containerType: 'inline-size' as const,
|
|
146
|
+
fontSize: `${(16 / config.width) * 100}cqi`,
|
|
147
|
+
maxHeight: 'calc(100dvh - 8rem)',
|
|
148
|
+
maxWidth: '100%',
|
|
149
|
+
width: `${config.width * scale}px`
|
|
150
|
+
},
|
|
151
|
+
...rest
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (variant === 'vibe') {
|
|
155
|
+
return (
|
|
156
|
+
<div {...outerProps}>
|
|
157
|
+
<VibeContent
|
|
158
|
+
autoPlay={autoPlay}
|
|
159
|
+
channel={channel}
|
|
160
|
+
cornerMarks={cornerMarks}
|
|
161
|
+
signature={signature}
|
|
162
|
+
src={src}
|
|
163
|
+
tint={tint}
|
|
164
|
+
tintStrength={tintStrength}
|
|
165
|
+
/>
|
|
166
|
+
</div>
|
|
167
|
+
)
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const headlineLines = Array.isArray(headline) ? headline : [headline]
|
|
171
|
+
|
|
172
|
+
return (
|
|
173
|
+
<div {...outerProps} className={cn('flex flex-col', outerProps.className)}>
|
|
174
|
+
<DispatchHeader channel={channel} />
|
|
175
|
+
|
|
176
|
+
<div
|
|
177
|
+
className={cn(
|
|
178
|
+
'relative min-h-0 min-w-0 flex-1',
|
|
179
|
+
resolvedLayout === 'split'
|
|
180
|
+
? 'grid grid-cols-[3fr_2fr]'
|
|
181
|
+
: 'grid grid-rows-[3fr_2fr]'
|
|
182
|
+
)}
|
|
183
|
+
>
|
|
184
|
+
<div
|
|
185
|
+
className={cn(
|
|
186
|
+
'relative overflow-hidden border-current/20',
|
|
187
|
+
resolvedLayout === 'split' ? 'border-r' : 'border-b'
|
|
188
|
+
)}
|
|
189
|
+
style={{ backgroundColor: 'var(--background)' }}
|
|
190
|
+
>
|
|
191
|
+
<ImageDistortion
|
|
192
|
+
autoPlay={autoPlay}
|
|
193
|
+
src={src}
|
|
194
|
+
tint={tint}
|
|
195
|
+
tintStrength={tintStrength}
|
|
196
|
+
/>
|
|
197
|
+
|
|
198
|
+
{cornerMarks && (
|
|
199
|
+
<>
|
|
200
|
+
<CornerMark className="top-3 left-3" />
|
|
201
|
+
<CornerMark className="top-3 right-3" />
|
|
202
|
+
<CornerMark className="bottom-3 left-3" />
|
|
203
|
+
<CornerMark className="right-3 bottom-3" />
|
|
204
|
+
</>
|
|
205
|
+
)}
|
|
206
|
+
|
|
207
|
+
<ScanlineOverlay />
|
|
208
|
+
|
|
209
|
+
<Small className="absolute bottom-4 left-4 z-1 opacity-80">
|
|
210
|
+
Hermes Agent
|
|
211
|
+
</Small>
|
|
212
|
+
</div>
|
|
213
|
+
|
|
214
|
+
<aside className="relative flex min-w-0 flex-col justify-between gap-8 p-8">
|
|
215
|
+
<div className="flex flex-col gap-5">
|
|
216
|
+
{eyebrow && (
|
|
217
|
+
<div className="flex items-center gap-2">
|
|
218
|
+
<span className="bg-midground/80 h-px flex-1" />
|
|
219
|
+
|
|
220
|
+
<Small className="opacity-80">{eyebrow}</Small>
|
|
221
|
+
</div>
|
|
222
|
+
)}
|
|
223
|
+
|
|
224
|
+
{children ?? (
|
|
225
|
+
<>
|
|
226
|
+
<Typography
|
|
227
|
+
as="h1"
|
|
228
|
+
className="text-[2.75em] leading-[0.95] font-bold tracking-[-0.01em]"
|
|
229
|
+
expanded
|
|
230
|
+
>
|
|
231
|
+
{headlineLines.map((line, i) => (
|
|
232
|
+
<span className="block" key={`${line}-${i}`}>
|
|
233
|
+
{line}
|
|
234
|
+
</span>
|
|
235
|
+
))}
|
|
236
|
+
</Typography>
|
|
237
|
+
|
|
238
|
+
{body && (
|
|
239
|
+
<p className="text-[1.0625em] leading-[1.5] tracking-normal normal-case opacity-60">
|
|
240
|
+
{body}
|
|
241
|
+
</p>
|
|
242
|
+
)}
|
|
243
|
+
</>
|
|
244
|
+
)}
|
|
245
|
+
</div>
|
|
246
|
+
|
|
247
|
+
{tags && tags.length > 0 && (
|
|
248
|
+
<ul className="flex flex-col gap-2 border-t border-current/15 pt-4">
|
|
249
|
+
{tags.map((tag, i) => (
|
|
250
|
+
<li
|
|
251
|
+
className="flex items-baseline justify-between gap-3"
|
|
252
|
+
key={`${tag}-${i}`}
|
|
253
|
+
>
|
|
254
|
+
<Small className="font-courier opacity-40">
|
|
255
|
+
{String(i + 1).padStart(3, '0')}
|
|
256
|
+
</Small>
|
|
257
|
+
|
|
258
|
+
<Small className="opacity-80">{tag}</Small>
|
|
259
|
+
|
|
260
|
+
<span className="mx-1 h-px flex-1 translate-y-[-3px] border-b border-dotted border-current/25" />
|
|
261
|
+
|
|
262
|
+
<Small className="font-courier opacity-40">
|
|
263
|
+
{String(i + 1).padStart(2, '0')}/
|
|
264
|
+
{String(tags.length).padStart(2, '0')}
|
|
265
|
+
</Small>
|
|
266
|
+
</li>
|
|
267
|
+
))}
|
|
268
|
+
</ul>
|
|
269
|
+
)}
|
|
270
|
+
</aside>
|
|
271
|
+
</div>
|
|
272
|
+
|
|
273
|
+
<footer className="flex items-center justify-between gap-4 border-t border-current/20 px-6 py-3">
|
|
274
|
+
<Small className="opacity-70">
|
|
275
|
+
{signature}
|
|
276
|
+
|
|
277
|
+
<Blink />
|
|
278
|
+
</Small>
|
|
279
|
+
|
|
280
|
+
<Small className="font-courier opacity-40">{seal}</Small>
|
|
281
|
+
</footer>
|
|
282
|
+
</div>
|
|
283
|
+
)
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function DispatchHeader({ channel }: { channel: React.ReactNode }) {
|
|
287
|
+
const clock = useUtcClock()
|
|
288
|
+
|
|
289
|
+
return (
|
|
290
|
+
<header className="flex items-center justify-between gap-4 border-b border-current/20 px-6 py-3">
|
|
291
|
+
<div className="flex items-center gap-3">
|
|
292
|
+
<span className="bg-midground size-2 rounded-sm opacity-70" />
|
|
293
|
+
|
|
294
|
+
<Small className="opacity-70">{channel}</Small>
|
|
295
|
+
</div>
|
|
296
|
+
|
|
297
|
+
<div className="flex items-center gap-4">
|
|
298
|
+
<ChannelDot />
|
|
299
|
+
|
|
300
|
+
<Small className="font-courier opacity-50">{clock} UTC</Small>
|
|
301
|
+
</div>
|
|
302
|
+
</header>
|
|
303
|
+
)
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
interface VibeContentProps {
|
|
307
|
+
autoPlay: AutoPlayPattern
|
|
308
|
+
channel: React.ReactNode
|
|
309
|
+
cornerMarks: boolean
|
|
310
|
+
signature: React.ReactNode
|
|
311
|
+
src: string
|
|
312
|
+
tint?: string
|
|
313
|
+
tintStrength?: { active: number; inactive: number }
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function VibeContent({
|
|
317
|
+
autoPlay,
|
|
318
|
+
channel,
|
|
319
|
+
cornerMarks,
|
|
320
|
+
signature,
|
|
321
|
+
src,
|
|
322
|
+
tint,
|
|
323
|
+
tintStrength
|
|
324
|
+
}: VibeContentProps) {
|
|
325
|
+
// Absolute-inset-0 guarantees this fills the poster even when the outer
|
|
326
|
+
// container uses aspect-ratio-derived height in a browser that doesn't
|
|
327
|
+
// propagate that as a definite height for percentage-based children.
|
|
328
|
+
return (
|
|
329
|
+
<div className="absolute inset-0">
|
|
330
|
+
<ImageDistortion
|
|
331
|
+
autoPlay={autoPlay}
|
|
332
|
+
src={src}
|
|
333
|
+
tint={tint}
|
|
334
|
+
tintStrength={tintStrength}
|
|
335
|
+
/>
|
|
336
|
+
|
|
337
|
+
{cornerMarks && (
|
|
338
|
+
<>
|
|
339
|
+
<CornerMark className="top-5 left-5" />
|
|
340
|
+
<CornerMark className="top-5 right-5" />
|
|
341
|
+
<CornerMark className="bottom-5 left-5" />
|
|
342
|
+
<CornerMark className="right-5 bottom-5" />
|
|
343
|
+
</>
|
|
344
|
+
)}
|
|
345
|
+
|
|
346
|
+
<ScanlineOverlay />
|
|
347
|
+
|
|
348
|
+
{channel && (
|
|
349
|
+
<Small className="absolute top-5 left-10 z-1 text-[0.75em] opacity-70">
|
|
350
|
+
{channel}
|
|
351
|
+
</Small>
|
|
352
|
+
)}
|
|
353
|
+
|
|
354
|
+
<Small className="absolute right-10 bottom-5 z-1 text-[0.75em] opacity-80">
|
|
355
|
+
{signature}
|
|
356
|
+
</Small>
|
|
357
|
+
</div>
|
|
358
|
+
)
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
export type PosterAspect =
|
|
362
|
+
| 'landscape'
|
|
363
|
+
| 'portrait'
|
|
364
|
+
| 'square'
|
|
365
|
+
| 'story'
|
|
366
|
+
| 'wide'
|
|
367
|
+
|
|
368
|
+
export type PosterVariant = 'dispatch' | 'vibe'
|
|
369
|
+
|
|
370
|
+
export interface PosterProps {
|
|
371
|
+
/** Output aspect ratio. Picks sensible defaults for common social formats. */
|
|
372
|
+
aspect?: PosterAspect
|
|
373
|
+
/** Distortion choreography pattern. Default: `'slash'`. */
|
|
374
|
+
autoPlay?: AutoPlayPattern
|
|
375
|
+
/** (`dispatch` only) Descriptive copy under the headline. */
|
|
376
|
+
body?: React.ReactNode
|
|
377
|
+
/** Show the thin outer frame around the poster. Default `true`. */
|
|
378
|
+
border?: boolean
|
|
379
|
+
/** Tiny broadcast-station label. Optional in `vibe`; shown in header in `dispatch`. */
|
|
380
|
+
channel?: React.ReactNode
|
|
381
|
+
/** (`dispatch` only) Override the sidebar content (takes precedence over headline/body). */
|
|
382
|
+
children?: React.ReactNode
|
|
383
|
+
className?: string
|
|
384
|
+
/** Show the small `+` die-line registration marks in the image corners. Default `true`. */
|
|
385
|
+
cornerMarks?: boolean
|
|
386
|
+
/** (`dispatch` only) Small tagline above the headline. */
|
|
387
|
+
eyebrow?: React.ReactNode
|
|
388
|
+
/** (`dispatch` only) Big expanded-typography headline. Pass an array of strings to stack lines. */
|
|
389
|
+
headline?: string[] | string
|
|
390
|
+
/** (`dispatch` only) Force stacked vs split layout. Default inferred from `aspect`. */
|
|
391
|
+
layout?: 'split' | 'stacked'
|
|
392
|
+
/** Render scale. 1 = full canvas (1080px+ base width). */
|
|
393
|
+
scale?: number
|
|
394
|
+
/** (`dispatch` only) Small legal / signature line at the bottom-right. */
|
|
395
|
+
seal?: React.ReactNode
|
|
396
|
+
/**
|
|
397
|
+
* Signature mark. In `vibe` this is the small "Hermes Agent" overlay in the
|
|
398
|
+
* bottom-right. In `dispatch` this is the URL / CTA in the footer.
|
|
399
|
+
*/
|
|
400
|
+
signature?: React.ReactNode
|
|
401
|
+
/** Override the poster image. Defaults to the Hermes "filler-bg0" asset. */
|
|
402
|
+
src?: string
|
|
403
|
+
/** (`dispatch` only) Ranked list of features / pricing tiers rendered as a numbered sidebar list. */
|
|
404
|
+
tags?: string[]
|
|
405
|
+
/** Shader tint overlay. Great for tier-colored variants. */
|
|
406
|
+
tint?: string
|
|
407
|
+
/** Active / inactive tint strength — defaults match `ImageDistortion`. */
|
|
408
|
+
tintStrength?: { active: number; inactive: number }
|
|
409
|
+
/** Layout variant. `'vibe'` (default) is full-bleed image; `'dispatch'` is the broadcast-card with sidebar copy. */
|
|
410
|
+
variant?: PosterVariant
|
|
411
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react-vite'
|
|
2
|
+
import { useState } from 'react'
|
|
3
|
+
|
|
4
|
+
import { Button } from './button'
|
|
5
|
+
import { Progress } from './progress'
|
|
6
|
+
|
|
7
|
+
const meta: Meta<typeof Progress> = {
|
|
8
|
+
args: { animate: true, speed: 0.4, value: 42 },
|
|
9
|
+
component: Progress,
|
|
10
|
+
title: 'Components/Progress'
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export default meta
|
|
14
|
+
|
|
15
|
+
type Story = StoryObj<typeof Progress>
|
|
16
|
+
|
|
17
|
+
export const Playground: Story = {
|
|
18
|
+
args: { children: '42%' }
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const Stages: Story = {
|
|
22
|
+
render: () => (
|
|
23
|
+
<div className="flex flex-col gap-3">
|
|
24
|
+
<Progress value={15} />
|
|
25
|
+
<Progress value={42}>42%</Progress>
|
|
26
|
+
<Progress value={75}>75%</Progress>
|
|
27
|
+
<Progress value={100}>Complete</Progress>
|
|
28
|
+
</div>
|
|
29
|
+
)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export const Interactive: Story = {
|
|
33
|
+
render: () => {
|
|
34
|
+
const [value, setValue] = useState(42)
|
|
35
|
+
|
|
36
|
+
return (
|
|
37
|
+
<div className="flex flex-col gap-4">
|
|
38
|
+
<Progress value={value}>{value}%</Progress>
|
|
39
|
+
|
|
40
|
+
<div className="flex gap-2">
|
|
41
|
+
<Button onClick={() => setValue(v => Math.max(0, v - 10))}>-10</Button>
|
|
42
|
+
|
|
43
|
+
<Button onClick={() => setValue(v => Math.min(100, v + 10))}>+10</Button>
|
|
44
|
+
</div>
|
|
45
|
+
</div>
|
|
46
|
+
)
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { cn } from '../../utils'
|
|
2
|
+
|
|
3
|
+
import { Typography, type TypographyProps } from './typography'
|
|
4
|
+
|
|
5
|
+
export const Progress = ({
|
|
6
|
+
animate = true,
|
|
7
|
+
barProps,
|
|
8
|
+
children,
|
|
9
|
+
className,
|
|
10
|
+
speed = 0.4,
|
|
11
|
+
value,
|
|
12
|
+
...props
|
|
13
|
+
}: ProgressProps) => (
|
|
14
|
+
<div
|
|
15
|
+
className={cn(
|
|
16
|
+
'relative flex min-h-[2.3rem] min-w-0 flex-1 items-stretch overflow-hidden',
|
|
17
|
+
className
|
|
18
|
+
)}
|
|
19
|
+
{...props}
|
|
20
|
+
>
|
|
21
|
+
<Typography
|
|
22
|
+
{...barProps}
|
|
23
|
+
className={cn(
|
|
24
|
+
'shrink-0 translate-y-0.5 truncate py-2',
|
|
25
|
+
'bg-midground/20',
|
|
26
|
+
children ? 'px-2' : 'px-0',
|
|
27
|
+
barProps?.className
|
|
28
|
+
)}
|
|
29
|
+
mono
|
|
30
|
+
style={{
|
|
31
|
+
...(animate && { transition: `width ${speed}s steps(10, end)` }),
|
|
32
|
+
width: `${value}%`,
|
|
33
|
+
...barProps?.style
|
|
34
|
+
}}
|
|
35
|
+
>
|
|
36
|
+
{children}
|
|
37
|
+
</Typography>
|
|
38
|
+
|
|
39
|
+
<div
|
|
40
|
+
className="flex-1"
|
|
41
|
+
style={
|
|
42
|
+
{
|
|
43
|
+
'--x': '.5rem',
|
|
44
|
+
backgroundImage: `repeating-linear-gradient(to right, transparent 0 var(--x), color-mix(in srgb, var(--color-midground) 17%, transparent) var(--x) calc(var(--x) + 1px))`
|
|
45
|
+
} as React.CSSProperties
|
|
46
|
+
}
|
|
47
|
+
/>
|
|
48
|
+
</div>
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
interface ProgressProps extends React.ComponentProps<'div'> {
|
|
52
|
+
animate?: boolean
|
|
53
|
+
barProps?: TypographyProps<'span'>
|
|
54
|
+
speed?: number
|
|
55
|
+
value: number
|
|
56
|
+
}
|