@nous-research/ui 0.15.0 → 0.17.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 +266 -0
- package/README.md +24 -4
- package/dist/fonts.js +1 -0
- package/dist/hooks/use-below-breakpoint.d.ts +2 -0
- package/dist/hooks/use-below-breakpoint.js +17 -0
- package/dist/hooks/use-capped-frame.js +1 -0
- package/dist/hooks/use-confirm-delete.d.ts +10 -0
- package/dist/hooks/use-confirm-delete.js +35 -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/hooks/use-toast.d.ts +7 -0
- package/dist/hooks/use-toast.js +21 -0
- package/dist/index.d.ts +11 -1
- package/dist/index.js +23 -1
- 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/bottom-sheet.d.ts +15 -0
- package/dist/ui/components/bottom-sheet.js +192 -0
- package/dist/ui/components/button.js +2 -1
- package/dist/ui/components/card.d.ts +5 -0
- package/dist/ui/components/card.js +74 -0
- package/dist/ui/components/checkbox.d.ts +1 -1
- package/dist/ui/components/checkbox.js +2 -1
- package/dist/ui/components/command-block.js +4 -3
- package/dist/ui/components/confirm-dialog.d.ts +13 -0
- package/dist/ui/components/confirm-dialog.js +113 -0
- package/dist/ui/components/cursor.js +1 -0
- package/dist/ui/components/dialog.d.ts +15 -0
- package/dist/ui/components/dialog.js +171 -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/input.d.ts +1 -0
- package/dist/ui/components/input.js +21 -0
- package/dist/ui/components/label.d.ts +1 -0
- package/dist/ui/components/label.js +18 -0
- package/dist/ui/components/leva-client.js +1 -0
- package/dist/ui/components/list-item.js +3 -2
- 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/separator.d.ts +5 -0
- package/dist/ui/components/separator.js +22 -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/toast.d.ts +8 -0
- package/dist/ui/components/toast.js +39 -0
- 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 +47 -3
- 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 +5 -3
- 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-below-breakpoint.ts +21 -0
- package/src/hooks/use-capped-frame.ts +18 -0
- package/src/hooks/use-confirm-delete.ts +43 -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/hooks/use-toast.ts +29 -0
- package/src/index.ts +130 -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/bottom-sheet.stories.tsx +43 -0
- package/src/ui/components/bottom-sheet.tsx +227 -0
- package/src/ui/components/button.stories.tsx +68 -0
- package/src/ui/components/button.tsx +170 -0
- package/src/ui/components/card.stories.tsx +63 -0
- package/src/ui/components/card.tsx +85 -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/confirm-dialog.stories.tsx +91 -0
- package/src/ui/components/confirm-dialog.tsx +130 -0
- package/src/ui/components/cursor.tsx +115 -0
- package/src/ui/components/dialog.stories.tsx +169 -0
- package/src/ui/components/dialog.tsx +177 -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/forms.stories.tsx +173 -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/input.stories.tsx +39 -0
- package/src/ui/components/input.tsx +20 -0
- package/src/ui/components/label.stories.tsx +26 -0
- package/src/ui/components/label.tsx +16 -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/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/separator.stories.tsx +33 -0
- package/src/ui/components/separator.tsx +24 -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/toast.stories.tsx +55 -0
- package/src/ui/components/toast.tsx +49 -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 +395 -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
- package/dist/ui/components/modal/index.d.ts +0 -8
- package/dist/ui/components/modal/index.js +0 -34
- package/dist/ui/components/modal/modal.css +0 -36
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useStore } from '@nanostores/react'
|
|
4
|
+
|
|
5
|
+
import { cn } from '../../utils'
|
|
6
|
+
|
|
7
|
+
import { $lightMode, toggleLens } from './overlays'
|
|
8
|
+
|
|
9
|
+
export function ThemeToggle({ className, style }: ThemeToggleProps) {
|
|
10
|
+
const light = useStore($lightMode)
|
|
11
|
+
|
|
12
|
+
return (
|
|
13
|
+
<button
|
|
14
|
+
aria-label={light ? 'Switch to dark mode' : 'Switch to light mode'}
|
|
15
|
+
className={cn(
|
|
16
|
+
'relative flex h-6 w-11 shrink-0 cursor-pointer items-center rounded-full',
|
|
17
|
+
'border border-current/25 bg-current/8 transition-colors',
|
|
18
|
+
'hover:bg-current/15',
|
|
19
|
+
className
|
|
20
|
+
)}
|
|
21
|
+
onClick={toggleLens}
|
|
22
|
+
style={style}
|
|
23
|
+
type="button"
|
|
24
|
+
>
|
|
25
|
+
<svg
|
|
26
|
+
className="absolute left-1 size-3.5 opacity-40"
|
|
27
|
+
fill="none"
|
|
28
|
+
stroke="currentColor"
|
|
29
|
+
strokeLinecap="round"
|
|
30
|
+
strokeLinejoin="round"
|
|
31
|
+
strokeWidth={2}
|
|
32
|
+
viewBox="0 0 24 24"
|
|
33
|
+
>
|
|
34
|
+
<circle cx={12} cy={12} r={5} />
|
|
35
|
+
|
|
36
|
+
<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" />
|
|
37
|
+
</svg>
|
|
38
|
+
|
|
39
|
+
<svg
|
|
40
|
+
className="absolute right-1 size-3.5 opacity-40"
|
|
41
|
+
fill="none"
|
|
42
|
+
stroke="currentColor"
|
|
43
|
+
strokeLinecap="round"
|
|
44
|
+
strokeLinejoin="round"
|
|
45
|
+
strokeWidth={2}
|
|
46
|
+
viewBox="0 0 24 24"
|
|
47
|
+
>
|
|
48
|
+
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z" />
|
|
49
|
+
</svg>
|
|
50
|
+
|
|
51
|
+
<span
|
|
52
|
+
aria-hidden
|
|
53
|
+
className={cn(
|
|
54
|
+
'bg-midground absolute size-4 rounded-full',
|
|
55
|
+
'transition-transform duration-200 ease-out'
|
|
56
|
+
)}
|
|
57
|
+
style={{ transform: `translateX(${light ? 2 : 22}px)` }}
|
|
58
|
+
/>
|
|
59
|
+
</button>
|
|
60
|
+
)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
interface ThemeToggleProps {
|
|
64
|
+
className?: string
|
|
65
|
+
style?: React.CSSProperties
|
|
66
|
+
}
|
|
@@ -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/Data Display/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,55 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react-vite'
|
|
2
|
+
|
|
3
|
+
import { useToast } from '../../hooks/use-toast'
|
|
4
|
+
import { Button } from './button'
|
|
5
|
+
import { Toast } from './toast'
|
|
6
|
+
|
|
7
|
+
const meta: Meta<typeof Toast> = {
|
|
8
|
+
component: Toast,
|
|
9
|
+
title: 'Components/Feedback/Toast'
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export default meta
|
|
13
|
+
|
|
14
|
+
type Story = StoryObj<typeof Toast>
|
|
15
|
+
|
|
16
|
+
export const Success: Story = {
|
|
17
|
+
render: () => {
|
|
18
|
+
function Demo() {
|
|
19
|
+
const { showToast, toast } = useToast()
|
|
20
|
+
|
|
21
|
+
return (
|
|
22
|
+
<>
|
|
23
|
+
<Button onClick={() => showToast('Operation succeeded', 'success')}>
|
|
24
|
+
Show success toast
|
|
25
|
+
</Button>
|
|
26
|
+
<Toast toast={toast} />
|
|
27
|
+
</>
|
|
28
|
+
)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return <Demo />
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export const Error: Story = {
|
|
36
|
+
render: () => {
|
|
37
|
+
function Demo() {
|
|
38
|
+
const { showToast, toast } = useToast()
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
<>
|
|
42
|
+
<Button
|
|
43
|
+
destructive
|
|
44
|
+
onClick={() => showToast('Something went wrong', 'error')}
|
|
45
|
+
>
|
|
46
|
+
Show error toast
|
|
47
|
+
</Button>
|
|
48
|
+
<Toast toast={toast} />
|
|
49
|
+
</>
|
|
50
|
+
)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return <Demo />
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState } from 'react'
|
|
4
|
+
import { createPortal } from 'react-dom'
|
|
5
|
+
|
|
6
|
+
import { cn } from '../../utils'
|
|
7
|
+
|
|
8
|
+
export function Toast({ toast }: ToastProps) {
|
|
9
|
+
const [visible, setVisible] = useState(false)
|
|
10
|
+
const [current, setCurrent] = useState(toast)
|
|
11
|
+
|
|
12
|
+
useEffect(() => {
|
|
13
|
+
if (toast) {
|
|
14
|
+
setCurrent(toast)
|
|
15
|
+
setVisible(true)
|
|
16
|
+
} else {
|
|
17
|
+
setVisible(false)
|
|
18
|
+
const timer = setTimeout(() => setCurrent(null), 200)
|
|
19
|
+
return () => clearTimeout(timer)
|
|
20
|
+
}
|
|
21
|
+
}, [toast])
|
|
22
|
+
|
|
23
|
+
if (!current || typeof document === 'undefined') return null
|
|
24
|
+
|
|
25
|
+
return createPortal(
|
|
26
|
+
<div
|
|
27
|
+
aria-live="polite"
|
|
28
|
+
className={cn(
|
|
29
|
+
'fixed top-16 right-4 z-50 border px-4 py-2.5 font-courier text-xs tracking-wider uppercase backdrop-blur-sm',
|
|
30
|
+
current.type === 'success'
|
|
31
|
+
? 'bg-success/15 text-success border-success/30'
|
|
32
|
+
: 'bg-destructive/15 text-destructive border-destructive/30'
|
|
33
|
+
)}
|
|
34
|
+
role="status"
|
|
35
|
+
style={{
|
|
36
|
+
animation: visible
|
|
37
|
+
? 'toast-in 200ms ease-out forwards'
|
|
38
|
+
: 'toast-out 200ms ease-in forwards'
|
|
39
|
+
}}
|
|
40
|
+
>
|
|
41
|
+
{current.message}
|
|
42
|
+
</div>,
|
|
43
|
+
document.body
|
|
44
|
+
)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
interface ToastProps {
|
|
48
|
+
toast: { message: string; type: 'error' | 'success' } | null
|
|
49
|
+
}
|
|
@@ -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/Effects/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
|
+
}
|