@nationaldesignstudio/react 0.0.15 → 0.0.16
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/package.json +3 -2
- package/src/App.css +0 -0
- package/src/App.tsx +7 -0
- package/src/assets/fonts/PPNeueMontreal-Variable.woff2 +0 -0
- package/src/assets/react.svg +1 -0
- package/src/components/atoms/accordion/accordion.stories.tsx +228 -0
- package/src/components/atoms/accordion/accordion.tsx +219 -0
- package/src/components/atoms/accordion/index.ts +6 -0
- package/src/components/atoms/button/__screenshots__/button.visual.test.tsx/button-charcoal-chromium-darwin.png +0 -0
- package/src/components/atoms/button/__screenshots__/button.visual.test.tsx/button-charcoal-chromium-linux.png +0 -0
- package/src/components/atoms/button/__screenshots__/button.visual.test.tsx/button-charcoal-outline-chromium-darwin.png +0 -0
- package/src/components/atoms/button/__screenshots__/button.visual.test.tsx/button-charcoal-outline-chromium-linux.png +0 -0
- package/src/components/atoms/button/__screenshots__/button.visual.test.tsx/button-charcoal-outline-quiet-chromium-darwin.png +0 -0
- package/src/components/atoms/button/__screenshots__/button.visual.test.tsx/button-charcoal-outline-quiet-chromium-linux.png +0 -0
- package/src/components/atoms/button/__screenshots__/button.visual.test.tsx/button-disabled-chromium-darwin.png +0 -0
- package/src/components/atoms/button/__screenshots__/button.visual.test.tsx/button-disabled-chromium-linux.png +0 -0
- package/src/components/atoms/button/__screenshots__/button.visual.test.tsx/button-ivory-chromium-darwin.png +0 -0
- package/src/components/atoms/button/__screenshots__/button.visual.test.tsx/button-ivory-chromium-linux.png +0 -0
- package/src/components/atoms/button/__screenshots__/button.visual.test.tsx/button-ivory-outline-chromium-darwin.png +0 -0
- package/src/components/atoms/button/__screenshots__/button.visual.test.tsx/button-ivory-outline-chromium-linux.png +0 -0
- package/src/components/atoms/button/__screenshots__/button.visual.test.tsx/button-ivory-outline-quiet-chromium-darwin.png +0 -0
- package/src/components/atoms/button/__screenshots__/button.visual.test.tsx/button-ivory-outline-quiet-chromium-linux.png +0 -0
- package/src/components/atoms/button/__screenshots__/button.visual.test.tsx/button-size-large-chromium-darwin.png +0 -0
- package/src/components/atoms/button/__screenshots__/button.visual.test.tsx/button-size-large-chromium-linux.png +0 -0
- package/src/components/atoms/button/__screenshots__/button.visual.test.tsx/button-size-medium-chromium-darwin.png +0 -0
- package/src/components/atoms/button/__screenshots__/button.visual.test.tsx/button-size-medium-chromium-linux.png +0 -0
- package/src/components/atoms/button/__screenshots__/button.visual.test.tsx/button-size-small-chromium-darwin.png +0 -0
- package/src/components/atoms/button/__screenshots__/button.visual.test.tsx/button-size-small-chromium-linux.png +0 -0
- package/src/components/atoms/button/button.stories.tsx +102 -0
- package/src/components/atoms/button/button.test.tsx +135 -0
- package/src/components/atoms/button/button.tsx +139 -0
- package/src/components/atoms/button/button.visual.test.tsx +102 -0
- package/src/components/atoms/button/icon-button.stories.tsx +166 -0
- package/src/components/atoms/button/icon-button.tsx +120 -0
- package/src/components/atoms/button/index.ts +6 -0
- package/src/components/atoms/ndstudio-footer/index.ts +1 -0
- package/src/components/atoms/ndstudio-footer/ndstudio-footer.tsx +55 -0
- package/src/components/atoms/pager-control/index.ts +5 -0
- package/src/components/atoms/pager-control/pager-control.stories.tsx +209 -0
- package/src/components/atoms/pager-control/pager-control.test.tsx +130 -0
- package/src/components/atoms/pager-control/pager-control.tsx +329 -0
- package/src/components/dev-tools/dev-toolbar/dev-toolbar.stories.tsx +82 -0
- package/src/components/dev-tools/dev-toolbar/dev-toolbar.tsx +196 -0
- package/src/components/dev-tools/dev-toolbar/index.ts +1 -0
- package/src/components/dev-tools/grid-overlay/grid-overlay.tsx +41 -0
- package/src/components/dev-tools/grid-overlay/index.ts +1 -0
- package/src/components/dev-tools/index.ts +2 -0
- package/src/components/organisms/card/__screenshots__/card.visual.test.tsx/card-default-vertical-chromium-darwin.png +0 -0
- package/src/components/organisms/card/__screenshots__/card.visual.test.tsx/card-default-vertical-chromium-linux.png +0 -0
- package/src/components/organisms/card/__screenshots__/card.visual.test.tsx/card-horizontal-chromium-darwin.png +0 -0
- package/src/components/organisms/card/__screenshots__/card.visual.test.tsx/card-horizontal-chromium-linux.png +0 -0
- package/src/components/organisms/card/__screenshots__/card.visual.test.tsx/card-minimal-chromium-darwin.png +0 -0
- package/src/components/organisms/card/__screenshots__/card.visual.test.tsx/card-minimal-chromium-linux.png +0 -0
- package/src/components/organisms/card/__screenshots__/card.visual.test.tsx/card-without-actions-chromium-darwin.png +0 -0
- package/src/components/organisms/card/__screenshots__/card.visual.test.tsx/card-without-actions-chromium-linux.png +0 -0
- package/src/components/organisms/card/__screenshots__/card.visual.test.tsx/card-without-eyebrow-chromium-darwin.png +0 -0
- package/src/components/organisms/card/__screenshots__/card.visual.test.tsx/card-without-eyebrow-chromium-linux.png +0 -0
- package/src/components/organisms/card/__screenshots__/card.visual.test.tsx/card-without-image-chromium-darwin.png +0 -0
- package/src/components/organisms/card/__screenshots__/card.visual.test.tsx/card-without-image-chromium-linux.png +0 -0
- package/src/components/organisms/card/card.stories.tsx +293 -0
- package/src/components/organisms/card/card.test.tsx +245 -0
- package/src/components/organisms/card/card.tsx +225 -0
- package/src/components/organisms/card/card.visual.test.tsx +197 -0
- package/src/components/organisms/card/index.ts +19 -0
- package/src/components/organisms/navbar/__screenshots__/navbar.visual.test.tsx/navbar-active-link-chromium-darwin.png +0 -0
- package/src/components/organisms/navbar/__screenshots__/navbar.visual.test.tsx/navbar-active-link-chromium-linux.png +0 -0
- package/src/components/organisms/navbar/__screenshots__/navbar.visual.test.tsx/navbar-brand-only-chromium-darwin.png +0 -0
- package/src/components/organisms/navbar/__screenshots__/navbar.visual.test.tsx/navbar-brand-only-chromium-linux.png +0 -0
- package/src/components/organisms/navbar/__screenshots__/navbar.visual.test.tsx/navbar-default-chromium-darwin.png +0 -0
- package/src/components/organisms/navbar/__screenshots__/navbar.visual.test.tsx/navbar-default-chromium-linux.png +0 -0
- package/src/components/organisms/navbar/index.ts +18 -0
- package/src/components/organisms/navbar/navbar.stories.tsx +313 -0
- package/src/components/organisms/navbar/navbar.test.tsx +190 -0
- package/src/components/organisms/navbar/navbar.tsx +323 -0
- package/src/components/organisms/navbar/navbar.visual.test.tsx +85 -0
- package/src/components/organisms/us-gov-banner/__screenshots__/us-gov-banner.visual.test.tsx/us-gov-banner-custom-icon-chromium-darwin.png +0 -0
- package/src/components/organisms/us-gov-banner/__screenshots__/us-gov-banner.visual.test.tsx/us-gov-banner-custom-icon-chromium-linux.png +0 -0
- package/src/components/organisms/us-gov-banner/__screenshots__/us-gov-banner.visual.test.tsx/us-gov-banner-custom-text-chromium-darwin.png +0 -0
- package/src/components/organisms/us-gov-banner/__screenshots__/us-gov-banner.visual.test.tsx/us-gov-banner-custom-text-chromium-linux.png +0 -0
- package/src/components/organisms/us-gov-banner/__screenshots__/us-gov-banner.visual.test.tsx/us-gov-banner-default-chromium-darwin.png +0 -0
- package/src/components/organisms/us-gov-banner/__screenshots__/us-gov-banner.visual.test.tsx/us-gov-banner-default-chromium-linux.png +0 -0
- package/src/components/organisms/us-gov-banner/index.ts +1 -0
- package/src/components/organisms/us-gov-banner/us-gov-banner.stories.tsx +35 -0
- package/src/components/organisms/us-gov-banner/us-gov-banner.test.tsx +107 -0
- package/src/components/organisms/us-gov-banner/us-gov-banner.tsx +73 -0
- package/src/components/organisms/us-gov-banner/us-gov-banner.visual.test.tsx +46 -0
- package/src/components/sections/banner/banner.stories.tsx +150 -0
- package/src/components/sections/banner/banner.test.tsx +185 -0
- package/src/components/sections/banner/banner.tsx +130 -0
- package/src/components/sections/banner/index.ts +2 -0
- package/src/components/sections/card-grid/card-grid.stories.tsx +351 -0
- package/src/components/sections/card-grid/card-grid.tsx +116 -0
- package/src/components/sections/card-grid/index.ts +1 -0
- package/src/components/sections/faq-section/faq-section.stories.tsx +453 -0
- package/src/components/sections/faq-section/faq-section.tsx +84 -0
- package/src/components/sections/faq-section/index.ts +2 -0
- package/src/components/sections/hero/__screenshots__/hero.visual.test.tsx/hero-a1-desktop-chromium-darwin.png +0 -0
- package/src/components/sections/hero/__screenshots__/hero.visual.test.tsx/hero-a1-desktop-chromium-linux.png +0 -0
- package/src/components/sections/hero/__screenshots__/hero.visual.test.tsx/hero-a1-mobile-chromium-darwin.png +0 -0
- package/src/components/sections/hero/__screenshots__/hero.visual.test.tsx/hero-a1-mobile-chromium-linux.png +0 -0
- package/src/components/sections/hero/__screenshots__/hero.visual.test.tsx/hero-a1-tablet-chromium-darwin.png +0 -0
- package/src/components/sections/hero/__screenshots__/hero.visual.test.tsx/hero-a1-tablet-chromium-linux.png +0 -0
- package/src/components/sections/hero/__screenshots__/hero.visual.test.tsx/hero-a2-desktop-chromium-darwin.png +0 -0
- package/src/components/sections/hero/__screenshots__/hero.visual.test.tsx/hero-a2-desktop-chromium-linux.png +0 -0
- package/src/components/sections/hero/__screenshots__/hero.visual.test.tsx/hero-a2-mobile-chromium-darwin.png +0 -0
- package/src/components/sections/hero/__screenshots__/hero.visual.test.tsx/hero-a2-mobile-chromium-linux.png +0 -0
- package/src/components/sections/hero/__screenshots__/hero.visual.test.tsx/hero-a2-tablet-chromium-darwin.png +0 -0
- package/src/components/sections/hero/__screenshots__/hero.visual.test.tsx/hero-a2-tablet-chromium-linux.png +0 -0
- package/src/components/sections/hero/__screenshots__/hero.visual.test.tsx/hero-a3-desktop-chromium-darwin.png +0 -0
- package/src/components/sections/hero/__screenshots__/hero.visual.test.tsx/hero-a3-desktop-chromium-linux.png +0 -0
- package/src/components/sections/hero/__screenshots__/hero.visual.test.tsx/hero-a3-mobile-chromium-darwin.png +0 -0
- package/src/components/sections/hero/__screenshots__/hero.visual.test.tsx/hero-a3-mobile-chromium-linux.png +0 -0
- package/src/components/sections/hero/__screenshots__/hero.visual.test.tsx/hero-a3-tablet-chromium-darwin.png +0 -0
- package/src/components/sections/hero/__screenshots__/hero.visual.test.tsx/hero-a3-tablet-chromium-linux.png +0 -0
- package/src/components/sections/hero/__screenshots__/hero.visual.test.tsx/hero-custom-class-chromium-darwin.png +0 -0
- package/src/components/sections/hero/__screenshots__/hero.visual.test.tsx/hero-custom-class-chromium-linux.png +0 -0
- package/src/components/sections/hero/__screenshots__/hero.visual.test.tsx/hero-default-chromium-linux.png +0 -0
- package/src/components/sections/hero/__screenshots__/hero.visual.test.tsx/hero-long-title-chromium-darwin.png +0 -0
- package/src/components/sections/hero/__screenshots__/hero.visual.test.tsx/hero-long-title-chromium-linux.png +0 -0
- package/src/components/sections/hero/hero.stories.tsx +274 -0
- package/src/components/sections/hero/hero.test.tsx +135 -0
- package/src/components/sections/hero/hero.tsx +453 -0
- package/src/components/sections/hero/hero.visual.test.tsx +140 -0
- package/src/components/sections/hero/index.ts +10 -0
- package/src/components/sections/prose/__screenshots__/prose.visual.test.tsx/prose-h3-heading-chromium-darwin.png +0 -0
- package/src/components/sections/prose/__screenshots__/prose.visual.test.tsx/prose-h3-heading-chromium-linux.png +0 -0
- package/src/components/sections/prose/__screenshots__/prose.visual.test.tsx/prose-multiple-paragraphs-chromium-darwin.png +0 -0
- package/src/components/sections/prose/__screenshots__/prose.visual.test.tsx/prose-multiple-paragraphs-chromium-linux.png +0 -0
- package/src/components/sections/prose/__screenshots__/prose.visual.test.tsx/prose-multiple-sections-chromium-darwin.png +0 -0
- package/src/components/sections/prose/__screenshots__/prose.visual.test.tsx/prose-multiple-sections-chromium-linux.png +0 -0
- package/src/components/sections/prose/__screenshots__/prose.visual.test.tsx/prose-single-section-chromium-darwin.png +0 -0
- package/src/components/sections/prose/__screenshots__/prose.visual.test.tsx/prose-single-section-chromium-linux.png +0 -0
- package/src/components/sections/prose/index.ts +6 -0
- package/src/components/sections/prose/prose.stories.tsx +144 -0
- package/src/components/sections/prose/prose.test.tsx +178 -0
- package/src/components/sections/prose/prose.tsx +88 -0
- package/src/components/sections/prose/prose.visual.test.tsx +105 -0
- package/src/components/sections/river/index.ts +1 -0
- package/src/components/sections/river/river.stories.tsx +237 -0
- package/src/components/sections/river/river.test.tsx +268 -0
- package/src/components/sections/river/river.tsx +173 -0
- package/src/components/sections/tout/index.ts +1 -0
- package/src/components/sections/tout/tout.stories.tsx +171 -0
- package/src/components/sections/tout/tout.test.tsx +242 -0
- package/src/components/sections/tout/tout.tsx +270 -0
- package/src/components/sections/two-column-section/index.ts +5 -0
- package/src/components/sections/two-column-section/two-column-section.stories.tsx +285 -0
- package/src/components/sections/two-column-section/two-column-section.tsx +162 -0
- package/src/hooks/index.ts +1 -0
- package/src/hooks/use-event-listener.ts +73 -0
- package/src/index.ts +155 -0
- package/src/lib/theme.ts +1000 -0
- package/src/lib/utils.ts +6 -0
- package/src/main.tsx +13 -0
- package/src/stories/GridSystem.stories.tsx +84 -0
- package/src/stories/Introduction.mdx +114 -0
- package/src/stories/ThemeProvider.stories.tsx +357 -0
- package/src/stories/TokenShowcase.stories.tsx +92 -0
- package/src/stories/TokenShowcase.tsx +1429 -0
- package/src/styles.css +11 -0
- package/src/theme/ThemeProvider.tsx +297 -0
- package/src/theme/hooks.ts +40 -0
- package/src/theme/index.ts +43 -0
- package/src/theme/utils.ts +104 -0
|
@@ -0,0 +1,453 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { tv, type VariantProps } from "tailwind-variants";
|
|
3
|
+
import { type ComponentTheme, themeToStyleVars } from "@/lib/theme";
|
|
4
|
+
import { cn } from "@/lib/utils";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Hero variants based on Figma BaseKit / Heros
|
|
8
|
+
*
|
|
9
|
+
* Variants:
|
|
10
|
+
* - A1: Content aligned at bottom (default)
|
|
11
|
+
* - A2: Content aligned at top
|
|
12
|
+
* - A3: Content aligned at center
|
|
13
|
+
*
|
|
14
|
+
* Each variant is responsive across breakpoints:
|
|
15
|
+
* - sm (Mobile): 500px height, 20px padding
|
|
16
|
+
* - md (Tablet): 650px height, 56px padding
|
|
17
|
+
* - lg (Desktop): 700-850px height, 64-72px padding
|
|
18
|
+
*/
|
|
19
|
+
const heroVariants = tv({
|
|
20
|
+
base: [
|
|
21
|
+
"flex w-full",
|
|
22
|
+
// Mobile (sm) - uses primitive spacing tokens
|
|
23
|
+
"h-[500px] p-spacing-20",
|
|
24
|
+
// Tablet (md) - uses primitive spacing tokens
|
|
25
|
+
"md:h-[650px] md:p-spacing-56",
|
|
26
|
+
],
|
|
27
|
+
variants: {
|
|
28
|
+
variant: {
|
|
29
|
+
// A1: Content at bottom
|
|
30
|
+
A1: [
|
|
31
|
+
"items-end",
|
|
32
|
+
// Desktop (lg) - 800px height, uses primitive spacing tokens
|
|
33
|
+
"xl:h-[800px] xl:p-spacing-72",
|
|
34
|
+
],
|
|
35
|
+
// A2: Content at top
|
|
36
|
+
A2: [
|
|
37
|
+
"items-start",
|
|
38
|
+
// Desktop (lg) - 700px height, uses primitive spacing tokens
|
|
39
|
+
"xl:h-[700px] xl:p-spacing-64",
|
|
40
|
+
],
|
|
41
|
+
// A3: Content centered
|
|
42
|
+
A3: [
|
|
43
|
+
"items-center",
|
|
44
|
+
// Desktop (lg) - 800px height, uses primitive spacing tokens
|
|
45
|
+
"xl:h-[800px] xl:p-spacing-64",
|
|
46
|
+
],
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
defaultVariants: {
|
|
50
|
+
variant: "A1",
|
|
51
|
+
},
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
// =============================================================================
|
|
55
|
+
// HeroBackground Components
|
|
56
|
+
// =============================================================================
|
|
57
|
+
|
|
58
|
+
export interface HeroBackgroundImageProps
|
|
59
|
+
extends React.HTMLAttributes<HTMLDivElement> {
|
|
60
|
+
/**
|
|
61
|
+
* URL for the background image
|
|
62
|
+
*/
|
|
63
|
+
src: string;
|
|
64
|
+
/**
|
|
65
|
+
* CSS background-position value (default: "center")
|
|
66
|
+
*/
|
|
67
|
+
position?: string;
|
|
68
|
+
/**
|
|
69
|
+
* Alt text for accessibility (used in aria-label)
|
|
70
|
+
*/
|
|
71
|
+
alt?: string;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Background image component for Hero
|
|
76
|
+
*/
|
|
77
|
+
const HeroBackgroundImage = React.forwardRef<
|
|
78
|
+
HTMLDivElement,
|
|
79
|
+
HeroBackgroundImageProps
|
|
80
|
+
>(({ className, src, position = "center", alt, ...props }, ref) => (
|
|
81
|
+
<div
|
|
82
|
+
ref={ref}
|
|
83
|
+
role="img"
|
|
84
|
+
aria-label={alt}
|
|
85
|
+
aria-hidden={!alt}
|
|
86
|
+
className={cn("absolute inset-0 bg-cover", className)}
|
|
87
|
+
style={{
|
|
88
|
+
backgroundImage: `url(${src})`,
|
|
89
|
+
backgroundPosition: position,
|
|
90
|
+
}}
|
|
91
|
+
{...props}
|
|
92
|
+
/>
|
|
93
|
+
));
|
|
94
|
+
HeroBackgroundImage.displayName = "HeroBackground.Image";
|
|
95
|
+
|
|
96
|
+
export interface HeroBackgroundVideoProps
|
|
97
|
+
extends Omit<React.VideoHTMLAttributes<HTMLVideoElement>, "children"> {
|
|
98
|
+
/**
|
|
99
|
+
* URL for the video source
|
|
100
|
+
*/
|
|
101
|
+
src: string;
|
|
102
|
+
/**
|
|
103
|
+
* Video MIME type (default: auto-detected from src)
|
|
104
|
+
*/
|
|
105
|
+
type?: string;
|
|
106
|
+
/**
|
|
107
|
+
* Poster image URL shown before video loads
|
|
108
|
+
*/
|
|
109
|
+
poster?: string;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Background video component for Hero (HTML5 video)
|
|
114
|
+
*/
|
|
115
|
+
const HeroBackgroundVideo = React.forwardRef<
|
|
116
|
+
HTMLVideoElement,
|
|
117
|
+
HeroBackgroundVideoProps
|
|
118
|
+
>(
|
|
119
|
+
(
|
|
120
|
+
{
|
|
121
|
+
className,
|
|
122
|
+
src,
|
|
123
|
+
type,
|
|
124
|
+
poster,
|
|
125
|
+
autoPlay = true,
|
|
126
|
+
loop = true,
|
|
127
|
+
muted = true,
|
|
128
|
+
playsInline = true,
|
|
129
|
+
...props
|
|
130
|
+
},
|
|
131
|
+
ref,
|
|
132
|
+
) => (
|
|
133
|
+
<video
|
|
134
|
+
ref={ref}
|
|
135
|
+
autoPlay={autoPlay}
|
|
136
|
+
loop={loop}
|
|
137
|
+
muted={muted}
|
|
138
|
+
playsInline={playsInline}
|
|
139
|
+
poster={poster}
|
|
140
|
+
className={cn("absolute inset-0 h-full w-full object-cover", className)}
|
|
141
|
+
{...props}
|
|
142
|
+
>
|
|
143
|
+
<source src={src} type={type} />
|
|
144
|
+
</video>
|
|
145
|
+
),
|
|
146
|
+
);
|
|
147
|
+
HeroBackgroundVideo.displayName = "HeroBackground.Video";
|
|
148
|
+
|
|
149
|
+
export interface HeroBackgroundStreamProps
|
|
150
|
+
extends React.IframeHTMLAttributes<HTMLIFrameElement> {
|
|
151
|
+
/**
|
|
152
|
+
* Cloudflare Stream video ID
|
|
153
|
+
*/
|
|
154
|
+
videoId: string;
|
|
155
|
+
/**
|
|
156
|
+
* Poster image URL (Cloudflare Stream thumbnail or custom)
|
|
157
|
+
*/
|
|
158
|
+
poster?: string;
|
|
159
|
+
/**
|
|
160
|
+
* Whether the video should autoplay (default: true)
|
|
161
|
+
*/
|
|
162
|
+
autoplay?: boolean;
|
|
163
|
+
/**
|
|
164
|
+
* Whether the video should loop (default: true)
|
|
165
|
+
*/
|
|
166
|
+
loop?: boolean;
|
|
167
|
+
/**
|
|
168
|
+
* Whether the video should be muted (default: true)
|
|
169
|
+
*/
|
|
170
|
+
muted?: boolean;
|
|
171
|
+
/**
|
|
172
|
+
* Whether to show playback controls (default: false)
|
|
173
|
+
*/
|
|
174
|
+
controls?: boolean;
|
|
175
|
+
/**
|
|
176
|
+
* Custom Cloudflare customer subdomain (if using custom domains)
|
|
177
|
+
*/
|
|
178
|
+
customerSubdomain?: string;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Background video component for Hero using Cloudflare Stream
|
|
183
|
+
*
|
|
184
|
+
* @example
|
|
185
|
+
* ```tsx
|
|
186
|
+
* <HeroBackground.Stream videoId="5d5bc37ffcf54c9b82e996823bffbb81" />
|
|
187
|
+
*
|
|
188
|
+
* // With custom subdomain
|
|
189
|
+
* <HeroBackground.Stream
|
|
190
|
+
* videoId="5d5bc37ffcf54c9b82e996823bffbb81"
|
|
191
|
+
* customerSubdomain="customer-abc123"
|
|
192
|
+
* />
|
|
193
|
+
* ```
|
|
194
|
+
*/
|
|
195
|
+
const HeroBackgroundStream = React.forwardRef<
|
|
196
|
+
HTMLIFrameElement,
|
|
197
|
+
HeroBackgroundStreamProps
|
|
198
|
+
>(
|
|
199
|
+
(
|
|
200
|
+
{
|
|
201
|
+
className,
|
|
202
|
+
videoId,
|
|
203
|
+
poster,
|
|
204
|
+
autoplay = true,
|
|
205
|
+
loop = true,
|
|
206
|
+
muted = true,
|
|
207
|
+
controls = false,
|
|
208
|
+
customerSubdomain,
|
|
209
|
+
title = "Background video",
|
|
210
|
+
...props
|
|
211
|
+
},
|
|
212
|
+
ref,
|
|
213
|
+
) => {
|
|
214
|
+
// Build Cloudflare Stream embed URL
|
|
215
|
+
const baseUrl = customerSubdomain
|
|
216
|
+
? `https://${customerSubdomain}.cloudflarestream.com`
|
|
217
|
+
: "https://iframe.videodelivery.net";
|
|
218
|
+
|
|
219
|
+
const params = new URLSearchParams();
|
|
220
|
+
if (autoplay) params.set("autoplay", "true");
|
|
221
|
+
if (loop) params.set("loop", "true");
|
|
222
|
+
if (muted) params.set("muted", "true");
|
|
223
|
+
if (!controls) params.set("controls", "false");
|
|
224
|
+
if (poster) params.set("poster", poster);
|
|
225
|
+
// Preload for better performance
|
|
226
|
+
params.set("preload", "auto");
|
|
227
|
+
|
|
228
|
+
const streamUrl = `${baseUrl}/${videoId}?${params.toString()}`;
|
|
229
|
+
|
|
230
|
+
return (
|
|
231
|
+
<iframe
|
|
232
|
+
ref={ref}
|
|
233
|
+
src={streamUrl}
|
|
234
|
+
title={title}
|
|
235
|
+
allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture"
|
|
236
|
+
allowFullScreen
|
|
237
|
+
className={cn(
|
|
238
|
+
"absolute inset-0 h-full w-full border-0",
|
|
239
|
+
// Scale up to hide letterboxing if video aspect doesn't match
|
|
240
|
+
"scale-[1.5] object-cover",
|
|
241
|
+
className,
|
|
242
|
+
)}
|
|
243
|
+
{...props}
|
|
244
|
+
/>
|
|
245
|
+
);
|
|
246
|
+
},
|
|
247
|
+
);
|
|
248
|
+
HeroBackgroundStream.displayName = "HeroBackground.Stream";
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Compound component for Hero backgrounds
|
|
252
|
+
*
|
|
253
|
+
* Provides sub-components for different background types:
|
|
254
|
+
* - `HeroBackground.Image` - Static image backgrounds
|
|
255
|
+
* - `HeroBackground.Video` - HTML5 video backgrounds
|
|
256
|
+
* - `HeroBackground.Stream` - Cloudflare Stream video backgrounds
|
|
257
|
+
*
|
|
258
|
+
* @example
|
|
259
|
+
* ```tsx
|
|
260
|
+
* // Image background
|
|
261
|
+
* <Hero
|
|
262
|
+
* title="Welcome"
|
|
263
|
+
* background={<HeroBackground.Image src="/hero.jpg" />}
|
|
264
|
+
* />
|
|
265
|
+
*
|
|
266
|
+
* // Video background
|
|
267
|
+
* <Hero
|
|
268
|
+
* title="Welcome"
|
|
269
|
+
* background={<HeroBackground.Video src="/hero.mp4" />}
|
|
270
|
+
* />
|
|
271
|
+
*
|
|
272
|
+
* // Cloudflare Stream background
|
|
273
|
+
* <Hero
|
|
274
|
+
* title="Welcome"
|
|
275
|
+
* background={<HeroBackground.Stream videoId="abc123" />}
|
|
276
|
+
* />
|
|
277
|
+
* ```
|
|
278
|
+
*/
|
|
279
|
+
const HeroBackground = {
|
|
280
|
+
Image: HeroBackgroundImage,
|
|
281
|
+
Video: HeroBackgroundVideo,
|
|
282
|
+
Stream: HeroBackgroundStream,
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
// =============================================================================
|
|
286
|
+
// Hero Component
|
|
287
|
+
// =============================================================================
|
|
288
|
+
|
|
289
|
+
export interface HeroProps
|
|
290
|
+
extends React.HTMLAttributes<HTMLElement>,
|
|
291
|
+
VariantProps<typeof heroVariants> {
|
|
292
|
+
/**
|
|
293
|
+
* The title text displayed in the hero
|
|
294
|
+
*/
|
|
295
|
+
title: string;
|
|
296
|
+
/**
|
|
297
|
+
* Custom typography class for the title using primitive tokens.
|
|
298
|
+
* Use primitive typography classes like "text-128 leading-128 tracking-128"
|
|
299
|
+
* Default: "text-64 leading-64 tracking-64 md:text-128 md:leading-128 md:tracking-128 xl:text-192 xl:leading-192 xl:tracking-192"
|
|
300
|
+
*/
|
|
301
|
+
titleClassName?: string;
|
|
302
|
+
/**
|
|
303
|
+
* Background for the hero. Can be:
|
|
304
|
+
* - A color string (hex, rgb, etc.) for solid backgrounds
|
|
305
|
+
* - A ReactNode (use HeroBackground.Image, HeroBackground.Video, or HeroBackground.Stream)
|
|
306
|
+
*/
|
|
307
|
+
background?: React.ReactNode | string;
|
|
308
|
+
/**
|
|
309
|
+
* Opacity of the overlay (0-1, default: 0)
|
|
310
|
+
* Only applies when using a background slot
|
|
311
|
+
*/
|
|
312
|
+
overlayOpacity?: number;
|
|
313
|
+
/**
|
|
314
|
+
* Color of the overlay (default: "black")
|
|
315
|
+
*/
|
|
316
|
+
overlayColor?: string;
|
|
317
|
+
/**
|
|
318
|
+
* Theme overrides for component styling via CSS custom properties
|
|
319
|
+
*/
|
|
320
|
+
theme?: ComponentTheme;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Checks if the background prop is a color string
|
|
325
|
+
*/
|
|
326
|
+
function isColorString(
|
|
327
|
+
background: React.ReactNode | string | undefined,
|
|
328
|
+
): background is string {
|
|
329
|
+
return (
|
|
330
|
+
typeof background === "string" &&
|
|
331
|
+
(background.startsWith("#") ||
|
|
332
|
+
background.startsWith("rgb") ||
|
|
333
|
+
background.startsWith("hsl") ||
|
|
334
|
+
// Named colors or CSS variables
|
|
335
|
+
/^(var\(|[a-z]+$)/i.test(background))
|
|
336
|
+
);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Hero component for page headers with large display typography.
|
|
341
|
+
*
|
|
342
|
+
* Features responsive sizing across three variants:
|
|
343
|
+
* - A1: Content at bottom (default)
|
|
344
|
+
* - A2: Content at top
|
|
345
|
+
* - A3: Content centered
|
|
346
|
+
*
|
|
347
|
+
* Each variant responds to breakpoints:
|
|
348
|
+
* - Mobile: 500px height, 20px padding, 64px typography
|
|
349
|
+
* - Tablet (768px+): 650px height, 56px padding, 128-148px typography
|
|
350
|
+
* - Desktop (1440px+): 700-800px height, 64-72px padding, 148-192px typography
|
|
351
|
+
*
|
|
352
|
+
* @example
|
|
353
|
+
* ```tsx
|
|
354
|
+
* // Simple hero with solid color
|
|
355
|
+
* <Hero title="Welcome" variant="A1" background="#1a1a1a" />
|
|
356
|
+
*
|
|
357
|
+
* // With background image
|
|
358
|
+
* <Hero
|
|
359
|
+
* title="Welcome"
|
|
360
|
+
* variant="A1"
|
|
361
|
+
* background={<HeroBackground.Image src="/hero.jpg" />}
|
|
362
|
+
* overlayOpacity={0.4}
|
|
363
|
+
* />
|
|
364
|
+
*
|
|
365
|
+
* // With Cloudflare Stream video
|
|
366
|
+
* <Hero
|
|
367
|
+
* title="Welcome"
|
|
368
|
+
* variant="A1"
|
|
369
|
+
* background={<HeroBackground.Stream videoId="abc123" />}
|
|
370
|
+
* overlayOpacity={0.3}
|
|
371
|
+
* />
|
|
372
|
+
* ```
|
|
373
|
+
*/
|
|
374
|
+
/**
|
|
375
|
+
* Default responsive typography for hero title using primitive tokens
|
|
376
|
+
* Mobile: 64px, Tablet: 128px, Desktop: 192px
|
|
377
|
+
*/
|
|
378
|
+
const DEFAULT_TITLE_TYPOGRAPHY =
|
|
379
|
+
"text-64 leading-64 tracking-64 md:text-128 md:leading-128 md:tracking-128 xl:text-192 xl:leading-192 xl:tracking-192 font-medium";
|
|
380
|
+
|
|
381
|
+
const Hero = React.forwardRef<HTMLElement, HeroProps>(
|
|
382
|
+
(
|
|
383
|
+
{
|
|
384
|
+
className,
|
|
385
|
+
title,
|
|
386
|
+
titleClassName,
|
|
387
|
+
variant,
|
|
388
|
+
background,
|
|
389
|
+
overlayOpacity = 0,
|
|
390
|
+
overlayColor = "black",
|
|
391
|
+
theme,
|
|
392
|
+
style,
|
|
393
|
+
...props
|
|
394
|
+
},
|
|
395
|
+
ref,
|
|
396
|
+
) => {
|
|
397
|
+
const isColor = isColorString(background);
|
|
398
|
+
const hasMediaBackground = background && !isColor;
|
|
399
|
+
const themeStyles = themeToStyleVars(theme);
|
|
400
|
+
const combinedStyles = {
|
|
401
|
+
...themeStyles,
|
|
402
|
+
...(isColor ? { backgroundColor: background } : {}),
|
|
403
|
+
...style,
|
|
404
|
+
};
|
|
405
|
+
|
|
406
|
+
return (
|
|
407
|
+
<section
|
|
408
|
+
ref={ref}
|
|
409
|
+
className={cn(
|
|
410
|
+
heroVariants({ variant }),
|
|
411
|
+
hasMediaBackground && "relative overflow-hidden",
|
|
412
|
+
// Default background color when no background is provided
|
|
413
|
+
!background && "bg-gray-1000",
|
|
414
|
+
className,
|
|
415
|
+
)}
|
|
416
|
+
style={
|
|
417
|
+
Object.keys(combinedStyles).length > 0 ? combinedStyles : undefined
|
|
418
|
+
}
|
|
419
|
+
{...props}
|
|
420
|
+
>
|
|
421
|
+
{/* Background slot (image, video, or stream) */}
|
|
422
|
+
{hasMediaBackground && background}
|
|
423
|
+
|
|
424
|
+
{/* Overlay */}
|
|
425
|
+
{hasMediaBackground && overlayOpacity > 0 && (
|
|
426
|
+
<div
|
|
427
|
+
aria-hidden="true"
|
|
428
|
+
className="absolute inset-0"
|
|
429
|
+
style={{
|
|
430
|
+
backgroundColor: overlayColor,
|
|
431
|
+
opacity: overlayOpacity,
|
|
432
|
+
}}
|
|
433
|
+
/>
|
|
434
|
+
)}
|
|
435
|
+
|
|
436
|
+
{/* Content */}
|
|
437
|
+
<h1
|
|
438
|
+
className={cn(
|
|
439
|
+
// Use primitive tokens instead of semantic typography
|
|
440
|
+
titleClassName || DEFAULT_TITLE_TYPOGRAPHY,
|
|
441
|
+
"text-gray-50",
|
|
442
|
+
hasMediaBackground && "relative z-10",
|
|
443
|
+
)}
|
|
444
|
+
>
|
|
445
|
+
{title}
|
|
446
|
+
</h1>
|
|
447
|
+
</section>
|
|
448
|
+
);
|
|
449
|
+
},
|
|
450
|
+
);
|
|
451
|
+
Hero.displayName = "Hero";
|
|
452
|
+
|
|
453
|
+
export { Hero, HeroBackground, heroVariants, DEFAULT_TITLE_TYPOGRAPHY };
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { describe, expect, test } from "vitest";
|
|
2
|
+
import { page } from "vitest/browser";
|
|
3
|
+
import { render } from "vitest-browser-react";
|
|
4
|
+
import { Hero } from "./hero";
|
|
5
|
+
|
|
6
|
+
describe("Hero Visual Regression", () => {
|
|
7
|
+
// =========================================================================
|
|
8
|
+
// Variant A1 (Content at bottom) - Default
|
|
9
|
+
// =========================================================================
|
|
10
|
+
describe("Variant A1", () => {
|
|
11
|
+
test("A1 desktop renders correctly", async () => {
|
|
12
|
+
await page.viewport(1440, 900);
|
|
13
|
+
|
|
14
|
+
render(<Hero data-testid="hero" variant="A1" title="Hero A1" />);
|
|
15
|
+
|
|
16
|
+
await expect(page.getByTestId("hero")).toMatchScreenshot(
|
|
17
|
+
"hero-a1-desktop",
|
|
18
|
+
);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test("A1 tablet renders correctly", async () => {
|
|
22
|
+
await page.viewport(834, 700);
|
|
23
|
+
|
|
24
|
+
render(<Hero data-testid="hero" variant="A1" title="Hero A1" />);
|
|
25
|
+
|
|
26
|
+
await expect(page.getByTestId("hero")).toMatchScreenshot(
|
|
27
|
+
"hero-a1-tablet",
|
|
28
|
+
);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test("A1 mobile renders correctly", async () => {
|
|
32
|
+
await page.viewport(393, 600);
|
|
33
|
+
|
|
34
|
+
render(<Hero data-testid="hero" variant="A1" title="Hero A1" />);
|
|
35
|
+
|
|
36
|
+
await expect(page.getByTestId("hero")).toMatchScreenshot(
|
|
37
|
+
"hero-a1-mobile",
|
|
38
|
+
);
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
// =========================================================================
|
|
43
|
+
// Variant A2 (Content at top)
|
|
44
|
+
// =========================================================================
|
|
45
|
+
describe("Variant A2", () => {
|
|
46
|
+
test("A2 desktop renders correctly", async () => {
|
|
47
|
+
await page.viewport(1440, 900);
|
|
48
|
+
|
|
49
|
+
render(<Hero data-testid="hero" variant="A2" title="Hero A2" />);
|
|
50
|
+
|
|
51
|
+
await expect(page.getByTestId("hero")).toMatchScreenshot(
|
|
52
|
+
"hero-a2-desktop",
|
|
53
|
+
);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test("A2 tablet renders correctly", async () => {
|
|
57
|
+
await page.viewport(834, 700);
|
|
58
|
+
|
|
59
|
+
render(<Hero data-testid="hero" variant="A2" title="Hero A2" />);
|
|
60
|
+
|
|
61
|
+
await expect(page.getByTestId("hero")).toMatchScreenshot(
|
|
62
|
+
"hero-a2-tablet",
|
|
63
|
+
);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test("A2 mobile renders correctly", async () => {
|
|
67
|
+
await page.viewport(393, 600);
|
|
68
|
+
|
|
69
|
+
render(<Hero data-testid="hero" variant="A2" title="Hero A2" />);
|
|
70
|
+
|
|
71
|
+
await expect(page.getByTestId("hero")).toMatchScreenshot(
|
|
72
|
+
"hero-a2-mobile",
|
|
73
|
+
);
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
// =========================================================================
|
|
78
|
+
// Variant A3 (Content centered)
|
|
79
|
+
// =========================================================================
|
|
80
|
+
describe("Variant A3", () => {
|
|
81
|
+
test("A3 desktop renders correctly", async () => {
|
|
82
|
+
await page.viewport(1440, 900);
|
|
83
|
+
|
|
84
|
+
render(<Hero data-testid="hero" variant="A3" title="Hero A3" />);
|
|
85
|
+
|
|
86
|
+
await expect(page.getByTestId("hero")).toMatchScreenshot(
|
|
87
|
+
"hero-a3-desktop",
|
|
88
|
+
);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test("A3 tablet renders correctly", async () => {
|
|
92
|
+
await page.viewport(834, 700);
|
|
93
|
+
|
|
94
|
+
render(<Hero data-testid="hero" variant="A3" title="Hero A3" />);
|
|
95
|
+
|
|
96
|
+
await expect(page.getByTestId("hero")).toMatchScreenshot(
|
|
97
|
+
"hero-a3-tablet",
|
|
98
|
+
);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test("A3 mobile renders correctly", async () => {
|
|
102
|
+
await page.viewport(393, 600);
|
|
103
|
+
|
|
104
|
+
render(<Hero data-testid="hero" variant="A3" title="Hero A3" />);
|
|
105
|
+
|
|
106
|
+
await expect(page.getByTestId("hero")).toMatchScreenshot(
|
|
107
|
+
"hero-a3-mobile",
|
|
108
|
+
);
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
// =========================================================================
|
|
113
|
+
// Additional tests
|
|
114
|
+
// =========================================================================
|
|
115
|
+
test("hero with long title renders correctly", async () => {
|
|
116
|
+
await page.viewport(1280, 800);
|
|
117
|
+
|
|
118
|
+
render(
|
|
119
|
+
<Hero data-testid="hero" title="A Much Longer Hero Title That Wraps" />,
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
await expect(page.getByTestId("hero")).toMatchScreenshot("hero-long-title");
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
test("hero with custom className renders correctly", async () => {
|
|
126
|
+
await page.viewport(1280, 800);
|
|
127
|
+
|
|
128
|
+
render(
|
|
129
|
+
<Hero
|
|
130
|
+
data-testid="hero"
|
|
131
|
+
title="Custom Background"
|
|
132
|
+
className="bg-blue-600"
|
|
133
|
+
/>,
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
await expect(page.getByTestId("hero")).toMatchScreenshot(
|
|
137
|
+
"hero-custom-class",
|
|
138
|
+
);
|
|
139
|
+
});
|
|
140
|
+
});
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|