@nous-research/ui 0.14.2 → 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,217 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react-vite'
|
|
2
|
+
|
|
3
|
+
import fillerBg from '../../assets/filler-bg0.webp'
|
|
4
|
+
import { TierCard } from './tier-card'
|
|
5
|
+
|
|
6
|
+
const SCOUT_SRC = fillerBg.src ?? (fillerBg as unknown as string)
|
|
7
|
+
|
|
8
|
+
// Same tier palette referenced in `Poster.stories.tsx` and originally from
|
|
9
|
+
// `nous-account-service/src/app/manage-subscription/_components/TierCard.tsx`.
|
|
10
|
+
// Keep the two in sync so a design review can compare the card layout and
|
|
11
|
+
// the bare poster side-by-side.
|
|
12
|
+
const TIERS = [
|
|
13
|
+
{
|
|
14
|
+
bullets: ['Free models only'],
|
|
15
|
+
label: 'Scout',
|
|
16
|
+
price: { primary: 'Free', primarySuffix: '/mo' },
|
|
17
|
+
src: SCOUT_SRC,
|
|
18
|
+
tint: '#88ccaa'
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
bullets: ['300+ models', 'Hosted tool usage', '$5 monthly credits'],
|
|
22
|
+
label: 'Visor',
|
|
23
|
+
price: { primary: '$5', primarySuffix: '/mo' },
|
|
24
|
+
src: '/img/hermes-2.png',
|
|
25
|
+
tint: '#99bbdd'
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
bullets: [
|
|
29
|
+
'300+ models',
|
|
30
|
+
'Hosted tool usage',
|
|
31
|
+
'$20 monthly credits',
|
|
32
|
+
'$40 rollover cap'
|
|
33
|
+
],
|
|
34
|
+
label: 'Angel',
|
|
35
|
+
price: { primary: '$20', primarySuffix: '/mo' },
|
|
36
|
+
src: '/img/hermes-3.jpg',
|
|
37
|
+
tint: '#ccaa88'
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
bullets: [
|
|
41
|
+
'300+ models',
|
|
42
|
+
'Hosted tool usage',
|
|
43
|
+
'$50 monthly credits',
|
|
44
|
+
'$100 rollover cap'
|
|
45
|
+
],
|
|
46
|
+
label: 'Herald',
|
|
47
|
+
price: { primary: '$50', primarySuffix: '/mo' },
|
|
48
|
+
src: '/img/hermes-4.png',
|
|
49
|
+
tint: '#dd8899'
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
bullets: [
|
|
53
|
+
'300+ models',
|
|
54
|
+
'Hosted tool usage',
|
|
55
|
+
'$150 monthly credits',
|
|
56
|
+
'$300 rollover cap'
|
|
57
|
+
],
|
|
58
|
+
label: 'Muse',
|
|
59
|
+
price: { primary: '$200', primarySuffix: '/mo' },
|
|
60
|
+
src: '/img/hermes-1.png',
|
|
61
|
+
tint: '#ccaa88'
|
|
62
|
+
}
|
|
63
|
+
] as const
|
|
64
|
+
|
|
65
|
+
const HIGHEST_OVERLAY = {
|
|
66
|
+
overlay: 'rgba(180, 30, 20, 1)',
|
|
67
|
+
tint: '#ff4444',
|
|
68
|
+
tintStrength: { active: 0.55, inactive: 0.35 }
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const meta = {
|
|
72
|
+
args: {
|
|
73
|
+
bullets: [...TIERS[2].bullets],
|
|
74
|
+
image: TIERS[2].src,
|
|
75
|
+
price: TIERS[2].price,
|
|
76
|
+
tint: TIERS[2].tint,
|
|
77
|
+
title: TIERS[2].label
|
|
78
|
+
},
|
|
79
|
+
argTypes: {
|
|
80
|
+
badge: { control: 'text' },
|
|
81
|
+
bullets: { control: 'object' },
|
|
82
|
+
className: { table: { disable: true } },
|
|
83
|
+
image: { control: 'text' },
|
|
84
|
+
isCurrent: { control: 'boolean' },
|
|
85
|
+
onSelect: { action: 'select' },
|
|
86
|
+
overlay: { control: 'color' },
|
|
87
|
+
price: { control: 'object' },
|
|
88
|
+
selected: { control: 'boolean' },
|
|
89
|
+
tint: { control: 'color' },
|
|
90
|
+
tintStrength: { control: 'object' },
|
|
91
|
+
title: { control: 'text' }
|
|
92
|
+
},
|
|
93
|
+
component: TierCard,
|
|
94
|
+
decorators: [
|
|
95
|
+
(Story, context) => {
|
|
96
|
+
// Stories that provide their own layout (e.g. `Row`) opt in via the
|
|
97
|
+
// `tierCardRaw` param. Everything else gets the compact 16rem preview
|
|
98
|
+
// frame on top of the dark background lens.
|
|
99
|
+
if (context.parameters?.tierCardRaw) {
|
|
100
|
+
return <Story />
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return (
|
|
104
|
+
<div
|
|
105
|
+
className="bg-background flex items-center justify-center p-8"
|
|
106
|
+
style={{ minHeight: '100dvh' }}
|
|
107
|
+
>
|
|
108
|
+
<div className="w-[22rem]">
|
|
109
|
+
<Story />
|
|
110
|
+
</div>
|
|
111
|
+
</div>
|
|
112
|
+
)
|
|
113
|
+
}
|
|
114
|
+
],
|
|
115
|
+
parameters: {
|
|
116
|
+
docs: {
|
|
117
|
+
description: {
|
|
118
|
+
component:
|
|
119
|
+
'Selectable subscription-tier card. Fully presentational: the consumer owns the data (tier schema, price formatting, imagery, tints). Toggle `selected` to see the `.arc-border` shimmer and `mix-blend-mode: plus-lighter` lift on the headline / price.'
|
|
120
|
+
}
|
|
121
|
+
},
|
|
122
|
+
layout: 'fullscreen'
|
|
123
|
+
},
|
|
124
|
+
title: 'Components/TierCard'
|
|
125
|
+
} satisfies Meta<typeof TierCard>
|
|
126
|
+
|
|
127
|
+
export default meta
|
|
128
|
+
|
|
129
|
+
type Story = StoryObj<typeof meta>
|
|
130
|
+
|
|
131
|
+
/** Default resting state. Hover to preview the arc-border shimmer. */
|
|
132
|
+
export const Idle: Story = {}
|
|
133
|
+
|
|
134
|
+
/** Selected state — arc-border, active distortion, lifted text. */
|
|
135
|
+
export const Selected: Story = {
|
|
136
|
+
args: { selected: true }
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** Current plan, not selected — subtle midground border hint. */
|
|
140
|
+
export const Current: Story = {
|
|
141
|
+
args: { badge: '(current)', isCurrent: true }
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** Current plan AND selected — both treatments compose. */
|
|
145
|
+
export const CurrentSelected: Story = {
|
|
146
|
+
args: { badge: '(current)', isCurrent: true, selected: true }
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** Highest tier red-overlay treatment. */
|
|
150
|
+
export const HighestTier: Story = {
|
|
151
|
+
args: {
|
|
152
|
+
...HIGHEST_OVERLAY,
|
|
153
|
+
bullets: [...TIERS[3].bullets],
|
|
154
|
+
image: TIERS[3].src,
|
|
155
|
+
price: { primary: '$200', primarySuffix: '/mo' },
|
|
156
|
+
selected: true,
|
|
157
|
+
title: 'Sovereign'
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** Struck-through comparison price (e.g. first-payment discount). */
|
|
162
|
+
export const WithDiscount: Story = {
|
|
163
|
+
args: {
|
|
164
|
+
bullets: [...TIERS[2].bullets],
|
|
165
|
+
image: TIERS[2].src,
|
|
166
|
+
price: {
|
|
167
|
+
primary: '$10',
|
|
168
|
+
primarySuffix: 'first payment',
|
|
169
|
+
secondary: '$20',
|
|
170
|
+
secondarySuffix: '/mo'
|
|
171
|
+
},
|
|
172
|
+
tint: TIERS[2].tint,
|
|
173
|
+
title: TIERS[2].label
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Full 5-card row approximating the live manage-subscription page, with
|
|
179
|
+
* the highest tier carrying the red overlay. Click any card to toggle
|
|
180
|
+
* selection — mirrors the interaction model in the consumer app.
|
|
181
|
+
*/
|
|
182
|
+
export const Row: StoryObj = {
|
|
183
|
+
// Opt out of the compact single-card wrapper (see the meta decorator)
|
|
184
|
+
// and supply a full-width grid instead.
|
|
185
|
+
decorators: [
|
|
186
|
+
Story => (
|
|
187
|
+
<div
|
|
188
|
+
className="bg-background flex items-center justify-center p-10"
|
|
189
|
+
style={{ minHeight: '100dvh' }}
|
|
190
|
+
>
|
|
191
|
+
<div className="grid w-full max-w-[90rem] grid-cols-2 gap-4 sm:grid-cols-3 lg:grid-cols-5">
|
|
192
|
+
<Story />
|
|
193
|
+
</div>
|
|
194
|
+
</div>
|
|
195
|
+
)
|
|
196
|
+
],
|
|
197
|
+
parameters: { layout: 'fullscreen', tierCardRaw: true },
|
|
198
|
+
render: () => (
|
|
199
|
+
<>
|
|
200
|
+
{TIERS.map((tier, i) => {
|
|
201
|
+
const isHighest = i === TIERS.length - 1
|
|
202
|
+
|
|
203
|
+
return (
|
|
204
|
+
<TierCard
|
|
205
|
+
bullets={[...tier.bullets]}
|
|
206
|
+
image={tier.src}
|
|
207
|
+
key={tier.label}
|
|
208
|
+
price={tier.price}
|
|
209
|
+
selected={i === 2}
|
|
210
|
+
title={tier.label}
|
|
211
|
+
{...(isHighest ? HIGHEST_OVERLAY : { tint: tier.tint })}
|
|
212
|
+
/>
|
|
213
|
+
)
|
|
214
|
+
})}
|
|
215
|
+
</>
|
|
216
|
+
)
|
|
217
|
+
}
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { cn } from '../../utils'
|
|
4
|
+
|
|
5
|
+
import { ImageDistortion } from './image-distortion'
|
|
6
|
+
import { Typography } from './typography'
|
|
7
|
+
import { Small } from './typography/small'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Selectable tier / pricing card. Full-bleed distorted image background,
|
|
11
|
+
* readable overlay text, and an animated `.arc-border` shimmer on the
|
|
12
|
+
* selected state. Fully presentational — the consumer owns the data
|
|
13
|
+
* (tier schema, price formatting, tier imagery / tints).
|
|
14
|
+
*
|
|
15
|
+
* Visual states:
|
|
16
|
+
* - `selected`: brightens the distortion, activates `.arc-border`, and
|
|
17
|
+
* composites the headline / price with `mix-blend-mode: plus-lighter`
|
|
18
|
+
* so the text lifts off the image regardless of tint.
|
|
19
|
+
* - `isCurrent`: subtle midground-tinted border hint (suppressed when
|
|
20
|
+
* `selected` wins).
|
|
21
|
+
* - `overlay`: optional top-layer color blended with `mix-blend-mode:
|
|
22
|
+
* color` — used for the "highest tier" red treatment on top of any
|
|
23
|
+
* base tint.
|
|
24
|
+
*/
|
|
25
|
+
export function TierCard({
|
|
26
|
+
badge,
|
|
27
|
+
bullets,
|
|
28
|
+
className,
|
|
29
|
+
image,
|
|
30
|
+
isCurrent = false,
|
|
31
|
+
onSelect,
|
|
32
|
+
overlay,
|
|
33
|
+
price,
|
|
34
|
+
selected = false,
|
|
35
|
+
tint,
|
|
36
|
+
tintStrength,
|
|
37
|
+
title
|
|
38
|
+
}: TierCardProps) {
|
|
39
|
+
return (
|
|
40
|
+
<button
|
|
41
|
+
className={cn(
|
|
42
|
+
'group relative flex w-full cursor-pointer flex-col border border-current/20',
|
|
43
|
+
'text-left transition-colors duration-300',
|
|
44
|
+
selected && 'border-midground/60',
|
|
45
|
+
isCurrent && !selected && 'border-midground/30',
|
|
46
|
+
className
|
|
47
|
+
)}
|
|
48
|
+
onClick={onSelect}
|
|
49
|
+
type="button"
|
|
50
|
+
>
|
|
51
|
+
<span
|
|
52
|
+
aria-hidden
|
|
53
|
+
className={cn(
|
|
54
|
+
'arc-border transition-opacity duration-200',
|
|
55
|
+
selected ? 'opacity-100' : 'opacity-0 group-hover:opacity-100'
|
|
56
|
+
)}
|
|
57
|
+
/>
|
|
58
|
+
|
|
59
|
+
<div
|
|
60
|
+
className="relative aspect-[3/4] min-h-0 w-full flex-1 overflow-hidden"
|
|
61
|
+
style={{ backgroundColor: 'var(--background)' }}
|
|
62
|
+
>
|
|
63
|
+
<ImageDistortion
|
|
64
|
+
active={selected}
|
|
65
|
+
src={image}
|
|
66
|
+
tint={tint}
|
|
67
|
+
tintStrength={tintStrength}
|
|
68
|
+
/>
|
|
69
|
+
|
|
70
|
+
{overlay && (
|
|
71
|
+
<div
|
|
72
|
+
className="pointer-events-none absolute inset-0"
|
|
73
|
+
style={{ backgroundColor: overlay, mixBlendMode: 'color' }}
|
|
74
|
+
/>
|
|
75
|
+
)}
|
|
76
|
+
|
|
77
|
+
<div className="pointer-events-none absolute inset-0 z-[1] flex flex-col justify-between p-3">
|
|
78
|
+
<div className="flex flex-col gap-0.5">
|
|
79
|
+
<Typography variant="sm"
|
|
80
|
+
className={cn(
|
|
81
|
+
'block drop-shadow-[0_1px_2px_rgba(0,0,0,0.5)] text-[1.2rem]',
|
|
82
|
+
'transition-colors',
|
|
83
|
+
selected && 'text-midground'
|
|
84
|
+
)}
|
|
85
|
+
style={selected ? { mixBlendMode: 'plus-lighter' } : undefined}
|
|
86
|
+
>
|
|
87
|
+
{title}
|
|
88
|
+
{badge && <span className="ml-1 opacity-50">{badge}</span>}
|
|
89
|
+
</Typography>
|
|
90
|
+
|
|
91
|
+
{price.secondary ? (
|
|
92
|
+
<>
|
|
93
|
+
<Typography
|
|
94
|
+
className="block text-md line-through opacity-50 drop-shadow-[0_1px_3px_rgba(0,0,0,0.6)]"
|
|
95
|
+
expanded
|
|
96
|
+
style={{ mixBlendMode: 'plus-lighter' }}
|
|
97
|
+
>
|
|
98
|
+
{price.secondary}
|
|
99
|
+
{price.secondarySuffix && (
|
|
100
|
+
<span className="text-[1rem]">
|
|
101
|
+
{price.secondarySuffix}
|
|
102
|
+
</span>
|
|
103
|
+
)}
|
|
104
|
+
</Typography>
|
|
105
|
+
|
|
106
|
+
<Typography
|
|
107
|
+
className="block text-xl font-bold drop-shadow-[0_1px_3px_rgba(0,0,0,0.6)]"
|
|
108
|
+
expanded
|
|
109
|
+
style={{ mixBlendMode: 'plus-lighter' }}
|
|
110
|
+
>
|
|
111
|
+
{price.primary}
|
|
112
|
+
{price.primarySuffix && (
|
|
113
|
+
<span className="text-[1rem] opacity-60">
|
|
114
|
+
{' '}
|
|
115
|
+
{price.primarySuffix}
|
|
116
|
+
</span>
|
|
117
|
+
)}
|
|
118
|
+
</Typography>
|
|
119
|
+
</>
|
|
120
|
+
) : (
|
|
121
|
+
<Typography
|
|
122
|
+
className="block text-xl font-bold drop-shadow-[0_1px_3px_rgba(0,0,0,0.6)]"
|
|
123
|
+
expanded
|
|
124
|
+
style={{ mixBlendMode: 'plus-lighter' }}
|
|
125
|
+
>
|
|
126
|
+
{price.primary}
|
|
127
|
+
{price.primarySuffix && (
|
|
128
|
+
<span className="text-[1rem] opacity-60">
|
|
129
|
+
{price.primarySuffix}
|
|
130
|
+
</span>
|
|
131
|
+
)}
|
|
132
|
+
</Typography>
|
|
133
|
+
)}
|
|
134
|
+
</div>
|
|
135
|
+
|
|
136
|
+
{bullets.length > 0 && (
|
|
137
|
+
<ul className="flex flex-col gap-1">
|
|
138
|
+
{bullets.map((bullet, i) => (
|
|
139
|
+
<li
|
|
140
|
+
className={cn(
|
|
141
|
+
'font-courier text-display text-[1rem] leading-tight tracking-tight',
|
|
142
|
+
'drop-shadow-[0_1px_2px_rgba(0,0,0,0.5)]',
|
|
143
|
+
)}
|
|
144
|
+
key={typeof bullet === 'string' ? bullet : i}
|
|
145
|
+
>
|
|
146
|
+
· {bullet}
|
|
147
|
+
</li>
|
|
148
|
+
))}
|
|
149
|
+
</ul>
|
|
150
|
+
)}
|
|
151
|
+
</div>
|
|
152
|
+
</div>
|
|
153
|
+
</button>
|
|
154
|
+
)
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export interface TierCardPrice {
|
|
158
|
+
/** Headline price, e.g. `"$20"` or `"Free"`. */
|
|
159
|
+
primary: string
|
|
160
|
+
/** Small suffix rendered after `primary`, e.g. `"/mo"` or `"first payment"`. */
|
|
161
|
+
primarySuffix?: string
|
|
162
|
+
/** Optional struck-through comparison price rendered above `primary`, e.g. `"$30"`. */
|
|
163
|
+
secondary?: string
|
|
164
|
+
/** Small suffix rendered after `secondary`. */
|
|
165
|
+
secondarySuffix?: string
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export interface TierCardProps {
|
|
169
|
+
/** Small annotation after the title, e.g. `"(current)"`. */
|
|
170
|
+
badge?: React.ReactNode
|
|
171
|
+
/** Feature list rendered under the price. */
|
|
172
|
+
bullets: React.ReactNode[]
|
|
173
|
+
className?: string
|
|
174
|
+
/** Background image URL. */
|
|
175
|
+
image: string
|
|
176
|
+
/** Applies the "current plan" border hint when not `selected`. */
|
|
177
|
+
isCurrent?: boolean
|
|
178
|
+
onSelect?: () => void
|
|
179
|
+
/** Color blended with `mix-blend-mode: color` over the image (used for the highest-tier red treatment). */
|
|
180
|
+
overlay?: string
|
|
181
|
+
price: TierCardPrice
|
|
182
|
+
/** Applies selected chrome (arc-border shimmer, active distortion, plus-lighter text blend). */
|
|
183
|
+
selected?: boolean
|
|
184
|
+
/** Shader tint passed through to `ImageDistortion`. */
|
|
185
|
+
tint?: string
|
|
186
|
+
/** Active / inactive tint strength passed through to `ImageDistortion`. */
|
|
187
|
+
tintStrength?: { active: number; inactive: number }
|
|
188
|
+
/** Tier name / headline. */
|
|
189
|
+
title: React.ReactNode
|
|
190
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react-vite'
|
|
2
|
+
import { Suspense } from 'react'
|
|
3
|
+
|
|
4
|
+
import { TV } from './tv'
|
|
5
|
+
|
|
6
|
+
const meta = {
|
|
7
|
+
component: TV,
|
|
8
|
+
parameters: {
|
|
9
|
+
docs: {
|
|
10
|
+
description: {
|
|
11
|
+
component:
|
|
12
|
+
'Animated WebGL brush inside an SVG television frame. Renders a fragment shader, so it only makes sense on the client.'
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
title: 'Components/TV'
|
|
17
|
+
} satisfies Meta<typeof TV>
|
|
18
|
+
|
|
19
|
+
export default meta
|
|
20
|
+
|
|
21
|
+
type Story = StoryObj<typeof meta>
|
|
22
|
+
|
|
23
|
+
export const Default: Story = {
|
|
24
|
+
render: () => (
|
|
25
|
+
<Suspense>
|
|
26
|
+
<TV className="h-64 w-64" />
|
|
27
|
+
</Suspense>
|
|
28
|
+
)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export const Large: Story = {
|
|
32
|
+
render: () => (
|
|
33
|
+
<Suspense>
|
|
34
|
+
<TV className="h-[28rem] w-[28rem]" />
|
|
35
|
+
</Suspense>
|
|
36
|
+
)
|
|
37
|
+
}
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useEffect, useRef } from 'react'
|
|
4
|
+
|
|
5
|
+
const VERT = /* glsl */ `attribute vec2 a;void main(){gl_Position=vec4(a,0,1);}`
|
|
6
|
+
|
|
7
|
+
const FRAG = /* glsl */ `precision highp float;
|
|
8
|
+
uniform float t;
|
|
9
|
+
uniform vec2 r;
|
|
10
|
+
|
|
11
|
+
const float FBM_STR = .08;
|
|
12
|
+
|
|
13
|
+
float h(vec2 p) { return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453); }
|
|
14
|
+
|
|
15
|
+
float n2(vec2 p) {
|
|
16
|
+
vec2 i = floor(p), f = fract(p);
|
|
17
|
+
f = f * f * (3. - 2. * f);
|
|
18
|
+
|
|
19
|
+
return mix(
|
|
20
|
+
mix(h(i), h(i + vec2(1, 0)), f.x),
|
|
21
|
+
mix(h(i + vec2(0, 1)), h(i + vec2(1, 1)), f.x),
|
|
22
|
+
f.y
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
float fbm(vec2 p) {
|
|
27
|
+
float v = 0., a = .5;
|
|
28
|
+
|
|
29
|
+
for (int i = 0; i < 4; i++) {
|
|
30
|
+
v += a * n2(p);
|
|
31
|
+
p *= 2.1;
|
|
32
|
+
a *= .45;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return v;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
float drift(float speed, float s) {
|
|
39
|
+
return fract(t * speed + s + .02 * sin(t * .4 + s * 3.));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
float brushAt(vec2 uv, float y, float th, float s) {
|
|
43
|
+
float hw = .34 + .08 * h(vec2(s, 77.));
|
|
44
|
+
float cx = .5;
|
|
45
|
+
float xn = (uv.x - (cx - hw)) / (2. * hw);
|
|
46
|
+
float env = smoothstep(0., .03, xn) * smoothstep(1., .97, xn);
|
|
47
|
+
float localTh = th * env;
|
|
48
|
+
|
|
49
|
+
if (localTh < .002) return 0.;
|
|
50
|
+
|
|
51
|
+
float morph = floor(t * 8.) * .7 + s;
|
|
52
|
+
float top = y - localTh * .5 + fbm(vec2(uv.x * 6., morph)) * FBM_STR;
|
|
53
|
+
float bot = y + localTh * .5 - fbm(vec2(uv.x * 6., morph + 30.)) * FBM_STR;
|
|
54
|
+
float x0 = cx - hw + fbm(vec2(uv.y * 8., morph + 60.)) * FBM_STR;
|
|
55
|
+
float x1 = cx + hw - fbm(vec2(uv.y * 8., morph + 90.)) * FBM_STR;
|
|
56
|
+
|
|
57
|
+
float dMin = min(min(uv.y - top, bot - uv.y), min(uv.x - x0, x1 - uv.x));
|
|
58
|
+
|
|
59
|
+
float bristle = n2(vec2(uv.x * 60., uv.y * 8. + s)) * .4
|
|
60
|
+
+ n2(vec2(uv.x * 25., (uv.y - y) * 120. + s)) * .35
|
|
61
|
+
+ n2(vec2(uv.x * 90., uv.y * 3. + s * 2.)) * .25;
|
|
62
|
+
|
|
63
|
+
float eaten = smoothstep(.03, 0., dMin) * (1. - smoothstep(.2, .5, bristle));
|
|
64
|
+
|
|
65
|
+
return clamp(smoothstep(0., .003, dMin) * (1. - eaten), 0., 1.);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
void main() {
|
|
69
|
+
vec2 uv = gl_FragCoord.xy / r;
|
|
70
|
+
uv = vec2(uv.x * cos(.095) - uv.y * sin(.095), uv.x * sin(.095) + uv.y * cos(.095));
|
|
71
|
+
uv += vec2(fbm(uv * 4. + t * .06), fbm(uv * 4. + 8. + t * .05)) * .012;
|
|
72
|
+
|
|
73
|
+
vec3 c = vec3(.992, .992, .051);
|
|
74
|
+
|
|
75
|
+
float smScroll = -drift(.04, 5.) * 2.;
|
|
76
|
+
float sm = 0.;
|
|
77
|
+
|
|
78
|
+
for (int i = 0; i < 20; i++) {
|
|
79
|
+
sm = max(sm, brushAt(uv, mod(float(i) * .1 + smScroll, 2.) - .5, .04, float(i) + 10.));
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
float d1 = drift(.15, 1.), d2 = drift(.15, 1.37), d3 = drift(.15, 1.58), d4 = drift(.15, 1.82);
|
|
83
|
+
float big = max(
|
|
84
|
+
max(brushAt(uv, 1.1 - d1 * 1.4, .28, 1.), brushAt(uv, 1.1 - d2 * 1.4, .18, 2.)),
|
|
85
|
+
max(brushAt(uv, 1.1 - d3 * 1.4, .3, 3.), brushAt(uv, 1.1 - d4 * 1.4, .15, 4.))
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
c = mix(c, vec3(0.), clamp(max(sm, big), 0., 1.));
|
|
89
|
+
c *= .94 + .06 * sin(uv.y * r.y * 6.283);
|
|
90
|
+
|
|
91
|
+
vec2 raw = gl_FragCoord.xy / r;
|
|
92
|
+
float dx = min(raw.x - .22, .90 - raw.x);
|
|
93
|
+
float dy = min(raw.y - .29, .86 - raw.y);
|
|
94
|
+
float cycle = floor(t * .4);
|
|
95
|
+
float edge = mix(smoothstep(.22, 0., max(min(dx, dy), 0.)), 1., step(.75, h(vec2(cycle, 13.))))
|
|
96
|
+
* smoothstep(.85, 1., sin(t * 2.5) * .5 + .5)
|
|
97
|
+
* (.7 + .3 * h(vec2(cycle, 7.)));
|
|
98
|
+
|
|
99
|
+
float scanY = floor(gl_FragCoord.y);
|
|
100
|
+
float rowNoise = h(vec2(scanY, floor(t * 30.)));
|
|
101
|
+
c *= 1. - edge * max(step(.45, rowNoise), step(.3, h(vec2(gl_FragCoord.x + scanY * 7., floor(t * 45.)))) * step(.2, rowNoise));
|
|
102
|
+
|
|
103
|
+
gl_FragColor = vec4(clamp(c, 0., 1.), 1.);
|
|
104
|
+
}`
|
|
105
|
+
|
|
106
|
+
function useGL(ref: React.RefObject<HTMLCanvasElement | null>) {
|
|
107
|
+
const raf = useRef(0)
|
|
108
|
+
|
|
109
|
+
useEffect(() => {
|
|
110
|
+
const c = ref.current
|
|
111
|
+
|
|
112
|
+
if (!c) {
|
|
113
|
+
return
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const gl = c.getContext('webgl')
|
|
117
|
+
|
|
118
|
+
if (!gl) {
|
|
119
|
+
return
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const sh = (type: number, src: string) => {
|
|
123
|
+
const s = gl.createShader(type)!
|
|
124
|
+
gl.shaderSource(s, src)
|
|
125
|
+
gl.compileShader(s)
|
|
126
|
+
|
|
127
|
+
return s
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const p = gl.createProgram()!
|
|
131
|
+
gl.attachShader(p, sh(gl.VERTEX_SHADER, VERT))
|
|
132
|
+
gl.attachShader(p, sh(gl.FRAGMENT_SHADER, FRAG))
|
|
133
|
+
gl.linkProgram(p)
|
|
134
|
+
gl.useProgram(p)
|
|
135
|
+
|
|
136
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, gl.createBuffer())
|
|
137
|
+
gl.bufferData(
|
|
138
|
+
gl.ARRAY_BUFFER,
|
|
139
|
+
new Float32Array([-1, -1, 1, -1, -1, 1, 1, 1]),
|
|
140
|
+
gl.STATIC_DRAW
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
const a = gl.getAttribLocation(p, 'a')
|
|
144
|
+
gl.enableVertexAttribArray(a)
|
|
145
|
+
gl.vertexAttribPointer(a, 2, gl.FLOAT, false, 0, 0)
|
|
146
|
+
|
|
147
|
+
const uT = gl.getUniformLocation(p, 't')
|
|
148
|
+
const uR = gl.getUniformLocation(p, 'r')
|
|
149
|
+
|
|
150
|
+
const resize = () => {
|
|
151
|
+
const rect = c.getBoundingClientRect()
|
|
152
|
+
const dpr = Math.min(devicePixelRatio, 2)
|
|
153
|
+
|
|
154
|
+
c.width = rect.width * dpr
|
|
155
|
+
c.height = rect.height * dpr
|
|
156
|
+
|
|
157
|
+
gl.viewport(0, 0, c.width, c.height)
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
resize()
|
|
161
|
+
|
|
162
|
+
const ro = new ResizeObserver(resize)
|
|
163
|
+
ro.observe(c)
|
|
164
|
+
|
|
165
|
+
const t0 = performance.now()
|
|
166
|
+
|
|
167
|
+
let visible = !document.hidden
|
|
168
|
+
let inView = true
|
|
169
|
+
let raf2 = 0
|
|
170
|
+
|
|
171
|
+
const tick = () => {
|
|
172
|
+
gl.uniform1f(uT, (performance.now() - t0) / 1e3)
|
|
173
|
+
gl.uniform2f(uR, c.width, c.height)
|
|
174
|
+
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4)
|
|
175
|
+
|
|
176
|
+
raf2 = requestAnimationFrame(tick)
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const start = () => {
|
|
180
|
+
if (visible && inView && !raf2) {
|
|
181
|
+
raf2 = requestAnimationFrame(tick)
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const stop = () => {
|
|
186
|
+
if (raf2) {
|
|
187
|
+
cancelAnimationFrame(raf2)
|
|
188
|
+
raf2 = 0
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const onVisibility = () => {
|
|
193
|
+
visible = !document.hidden
|
|
194
|
+
visible ? start() : stop()
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const io = new IntersectionObserver(
|
|
198
|
+
entries => {
|
|
199
|
+
inView = entries.some(e => e.isIntersecting)
|
|
200
|
+
inView ? start() : stop()
|
|
201
|
+
},
|
|
202
|
+
{ threshold: 0 }
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
io.observe(c)
|
|
206
|
+
document.addEventListener('visibilitychange', onVisibility)
|
|
207
|
+
|
|
208
|
+
start()
|
|
209
|
+
raf.current = raf2
|
|
210
|
+
|
|
211
|
+
return () => {
|
|
212
|
+
stop()
|
|
213
|
+
io.disconnect()
|
|
214
|
+
document.removeEventListener('visibilitychange', onVisibility)
|
|
215
|
+
ro.disconnect()
|
|
216
|
+
}
|
|
217
|
+
}, [ref])
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
export function TV({ className }: { className?: string }) {
|
|
221
|
+
const canvasRef = useRef<HTMLCanvasElement>(null)
|
|
222
|
+
useGL(canvasRef)
|
|
223
|
+
|
|
224
|
+
return (
|
|
225
|
+
<div className={['relative', className].filter(Boolean).join(' ')}>
|
|
226
|
+
<svg className="relative h-full w-full" fill="none" viewBox="0 0 210 173">
|
|
227
|
+
<path
|
|
228
|
+
d="M30.8342 2.44471 6.08268 36.683c-.24437.338-.38254.7412-.39689 1.158L1.57754 157.126c-.03891 1.129.82339 2.087 1.95096 2.167l162.4835 11.463c.433.031.866-.074 1.238-.3l35.718-21.69c.607-.369.986-1.02 1.008-1.73l4.102-130.9871c.035-1.1269-.826-2.0806-1.951-2.1604L32.6847 1.58029c-.7248-.05144-1.4247.27551-1.8505.86442Z"
|
|
229
|
+
fill="#FDFD0D"
|
|
230
|
+
stroke="#FDFD0D"
|
|
231
|
+
strokeWidth="3.15"
|
|
232
|
+
/>
|
|
233
|
+
|
|
234
|
+
<path
|
|
235
|
+
d="M203.09 17.1483 35.6844 5.83395l-4.2 121.94805 168.4906 13.076z"
|
|
236
|
+
fill="#000"
|
|
237
|
+
stroke="#FDFD0D"
|
|
238
|
+
strokeWidth="4.2"
|
|
239
|
+
/>
|
|
240
|
+
|
|
241
|
+
<path
|
|
242
|
+
d="M190.491 29.7483 48.2859 18.434l-4.2 98.848 143.2901 10.976z"
|
|
243
|
+
fill="#FDFD0D"
|
|
244
|
+
/>
|
|
245
|
+
</svg>
|
|
246
|
+
|
|
247
|
+
<canvas
|
|
248
|
+
className="absolute inset-0 h-full w-full"
|
|
249
|
+
ref={canvasRef}
|
|
250
|
+
style={{
|
|
251
|
+
clipPath:
|
|
252
|
+
'polygon(23% 10.65%, 90.71% 17.2%, 89.23% 74.13%, 20.99% 67.79%)'
|
|
253
|
+
}}
|
|
254
|
+
/>
|
|
255
|
+
</div>
|
|
256
|
+
)
|
|
257
|
+
}
|