@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.
Files changed (164) hide show
  1. package/package.json +3 -2
  2. package/src/App.css +0 -0
  3. package/src/App.tsx +7 -0
  4. package/src/assets/fonts/PPNeueMontreal-Variable.woff2 +0 -0
  5. package/src/assets/react.svg +1 -0
  6. package/src/components/atoms/accordion/accordion.stories.tsx +228 -0
  7. package/src/components/atoms/accordion/accordion.tsx +219 -0
  8. package/src/components/atoms/accordion/index.ts +6 -0
  9. package/src/components/atoms/button/__screenshots__/button.visual.test.tsx/button-charcoal-chromium-darwin.png +0 -0
  10. package/src/components/atoms/button/__screenshots__/button.visual.test.tsx/button-charcoal-chromium-linux.png +0 -0
  11. package/src/components/atoms/button/__screenshots__/button.visual.test.tsx/button-charcoal-outline-chromium-darwin.png +0 -0
  12. package/src/components/atoms/button/__screenshots__/button.visual.test.tsx/button-charcoal-outline-chromium-linux.png +0 -0
  13. package/src/components/atoms/button/__screenshots__/button.visual.test.tsx/button-charcoal-outline-quiet-chromium-darwin.png +0 -0
  14. package/src/components/atoms/button/__screenshots__/button.visual.test.tsx/button-charcoal-outline-quiet-chromium-linux.png +0 -0
  15. package/src/components/atoms/button/__screenshots__/button.visual.test.tsx/button-disabled-chromium-darwin.png +0 -0
  16. package/src/components/atoms/button/__screenshots__/button.visual.test.tsx/button-disabled-chromium-linux.png +0 -0
  17. package/src/components/atoms/button/__screenshots__/button.visual.test.tsx/button-ivory-chromium-darwin.png +0 -0
  18. package/src/components/atoms/button/__screenshots__/button.visual.test.tsx/button-ivory-chromium-linux.png +0 -0
  19. package/src/components/atoms/button/__screenshots__/button.visual.test.tsx/button-ivory-outline-chromium-darwin.png +0 -0
  20. package/src/components/atoms/button/__screenshots__/button.visual.test.tsx/button-ivory-outline-chromium-linux.png +0 -0
  21. package/src/components/atoms/button/__screenshots__/button.visual.test.tsx/button-ivory-outline-quiet-chromium-darwin.png +0 -0
  22. package/src/components/atoms/button/__screenshots__/button.visual.test.tsx/button-ivory-outline-quiet-chromium-linux.png +0 -0
  23. package/src/components/atoms/button/__screenshots__/button.visual.test.tsx/button-size-large-chromium-darwin.png +0 -0
  24. package/src/components/atoms/button/__screenshots__/button.visual.test.tsx/button-size-large-chromium-linux.png +0 -0
  25. package/src/components/atoms/button/__screenshots__/button.visual.test.tsx/button-size-medium-chromium-darwin.png +0 -0
  26. package/src/components/atoms/button/__screenshots__/button.visual.test.tsx/button-size-medium-chromium-linux.png +0 -0
  27. package/src/components/atoms/button/__screenshots__/button.visual.test.tsx/button-size-small-chromium-darwin.png +0 -0
  28. package/src/components/atoms/button/__screenshots__/button.visual.test.tsx/button-size-small-chromium-linux.png +0 -0
  29. package/src/components/atoms/button/button.stories.tsx +102 -0
  30. package/src/components/atoms/button/button.test.tsx +135 -0
  31. package/src/components/atoms/button/button.tsx +139 -0
  32. package/src/components/atoms/button/button.visual.test.tsx +102 -0
  33. package/src/components/atoms/button/icon-button.stories.tsx +166 -0
  34. package/src/components/atoms/button/icon-button.tsx +120 -0
  35. package/src/components/atoms/button/index.ts +6 -0
  36. package/src/components/atoms/ndstudio-footer/index.ts +1 -0
  37. package/src/components/atoms/ndstudio-footer/ndstudio-footer.tsx +55 -0
  38. package/src/components/atoms/pager-control/index.ts +5 -0
  39. package/src/components/atoms/pager-control/pager-control.stories.tsx +209 -0
  40. package/src/components/atoms/pager-control/pager-control.test.tsx +130 -0
  41. package/src/components/atoms/pager-control/pager-control.tsx +329 -0
  42. package/src/components/dev-tools/dev-toolbar/dev-toolbar.stories.tsx +82 -0
  43. package/src/components/dev-tools/dev-toolbar/dev-toolbar.tsx +196 -0
  44. package/src/components/dev-tools/dev-toolbar/index.ts +1 -0
  45. package/src/components/dev-tools/grid-overlay/grid-overlay.tsx +41 -0
  46. package/src/components/dev-tools/grid-overlay/index.ts +1 -0
  47. package/src/components/dev-tools/index.ts +2 -0
  48. package/src/components/organisms/card/__screenshots__/card.visual.test.tsx/card-default-vertical-chromium-darwin.png +0 -0
  49. package/src/components/organisms/card/__screenshots__/card.visual.test.tsx/card-default-vertical-chromium-linux.png +0 -0
  50. package/src/components/organisms/card/__screenshots__/card.visual.test.tsx/card-horizontal-chromium-darwin.png +0 -0
  51. package/src/components/organisms/card/__screenshots__/card.visual.test.tsx/card-horizontal-chromium-linux.png +0 -0
  52. package/src/components/organisms/card/__screenshots__/card.visual.test.tsx/card-minimal-chromium-darwin.png +0 -0
  53. package/src/components/organisms/card/__screenshots__/card.visual.test.tsx/card-minimal-chromium-linux.png +0 -0
  54. package/src/components/organisms/card/__screenshots__/card.visual.test.tsx/card-without-actions-chromium-darwin.png +0 -0
  55. package/src/components/organisms/card/__screenshots__/card.visual.test.tsx/card-without-actions-chromium-linux.png +0 -0
  56. package/src/components/organisms/card/__screenshots__/card.visual.test.tsx/card-without-eyebrow-chromium-darwin.png +0 -0
  57. package/src/components/organisms/card/__screenshots__/card.visual.test.tsx/card-without-eyebrow-chromium-linux.png +0 -0
  58. package/src/components/organisms/card/__screenshots__/card.visual.test.tsx/card-without-image-chromium-darwin.png +0 -0
  59. package/src/components/organisms/card/__screenshots__/card.visual.test.tsx/card-without-image-chromium-linux.png +0 -0
  60. package/src/components/organisms/card/card.stories.tsx +293 -0
  61. package/src/components/organisms/card/card.test.tsx +245 -0
  62. package/src/components/organisms/card/card.tsx +225 -0
  63. package/src/components/organisms/card/card.visual.test.tsx +197 -0
  64. package/src/components/organisms/card/index.ts +19 -0
  65. package/src/components/organisms/navbar/__screenshots__/navbar.visual.test.tsx/navbar-active-link-chromium-darwin.png +0 -0
  66. package/src/components/organisms/navbar/__screenshots__/navbar.visual.test.tsx/navbar-active-link-chromium-linux.png +0 -0
  67. package/src/components/organisms/navbar/__screenshots__/navbar.visual.test.tsx/navbar-brand-only-chromium-darwin.png +0 -0
  68. package/src/components/organisms/navbar/__screenshots__/navbar.visual.test.tsx/navbar-brand-only-chromium-linux.png +0 -0
  69. package/src/components/organisms/navbar/__screenshots__/navbar.visual.test.tsx/navbar-default-chromium-darwin.png +0 -0
  70. package/src/components/organisms/navbar/__screenshots__/navbar.visual.test.tsx/navbar-default-chromium-linux.png +0 -0
  71. package/src/components/organisms/navbar/index.ts +18 -0
  72. package/src/components/organisms/navbar/navbar.stories.tsx +313 -0
  73. package/src/components/organisms/navbar/navbar.test.tsx +190 -0
  74. package/src/components/organisms/navbar/navbar.tsx +323 -0
  75. package/src/components/organisms/navbar/navbar.visual.test.tsx +85 -0
  76. package/src/components/organisms/us-gov-banner/__screenshots__/us-gov-banner.visual.test.tsx/us-gov-banner-custom-icon-chromium-darwin.png +0 -0
  77. package/src/components/organisms/us-gov-banner/__screenshots__/us-gov-banner.visual.test.tsx/us-gov-banner-custom-icon-chromium-linux.png +0 -0
  78. package/src/components/organisms/us-gov-banner/__screenshots__/us-gov-banner.visual.test.tsx/us-gov-banner-custom-text-chromium-darwin.png +0 -0
  79. package/src/components/organisms/us-gov-banner/__screenshots__/us-gov-banner.visual.test.tsx/us-gov-banner-custom-text-chromium-linux.png +0 -0
  80. package/src/components/organisms/us-gov-banner/__screenshots__/us-gov-banner.visual.test.tsx/us-gov-banner-default-chromium-darwin.png +0 -0
  81. package/src/components/organisms/us-gov-banner/__screenshots__/us-gov-banner.visual.test.tsx/us-gov-banner-default-chromium-linux.png +0 -0
  82. package/src/components/organisms/us-gov-banner/index.ts +1 -0
  83. package/src/components/organisms/us-gov-banner/us-gov-banner.stories.tsx +35 -0
  84. package/src/components/organisms/us-gov-banner/us-gov-banner.test.tsx +107 -0
  85. package/src/components/organisms/us-gov-banner/us-gov-banner.tsx +73 -0
  86. package/src/components/organisms/us-gov-banner/us-gov-banner.visual.test.tsx +46 -0
  87. package/src/components/sections/banner/banner.stories.tsx +150 -0
  88. package/src/components/sections/banner/banner.test.tsx +185 -0
  89. package/src/components/sections/banner/banner.tsx +130 -0
  90. package/src/components/sections/banner/index.ts +2 -0
  91. package/src/components/sections/card-grid/card-grid.stories.tsx +351 -0
  92. package/src/components/sections/card-grid/card-grid.tsx +116 -0
  93. package/src/components/sections/card-grid/index.ts +1 -0
  94. package/src/components/sections/faq-section/faq-section.stories.tsx +453 -0
  95. package/src/components/sections/faq-section/faq-section.tsx +84 -0
  96. package/src/components/sections/faq-section/index.ts +2 -0
  97. package/src/components/sections/hero/__screenshots__/hero.visual.test.tsx/hero-a1-desktop-chromium-darwin.png +0 -0
  98. package/src/components/sections/hero/__screenshots__/hero.visual.test.tsx/hero-a1-desktop-chromium-linux.png +0 -0
  99. package/src/components/sections/hero/__screenshots__/hero.visual.test.tsx/hero-a1-mobile-chromium-darwin.png +0 -0
  100. package/src/components/sections/hero/__screenshots__/hero.visual.test.tsx/hero-a1-mobile-chromium-linux.png +0 -0
  101. package/src/components/sections/hero/__screenshots__/hero.visual.test.tsx/hero-a1-tablet-chromium-darwin.png +0 -0
  102. package/src/components/sections/hero/__screenshots__/hero.visual.test.tsx/hero-a1-tablet-chromium-linux.png +0 -0
  103. package/src/components/sections/hero/__screenshots__/hero.visual.test.tsx/hero-a2-desktop-chromium-darwin.png +0 -0
  104. package/src/components/sections/hero/__screenshots__/hero.visual.test.tsx/hero-a2-desktop-chromium-linux.png +0 -0
  105. package/src/components/sections/hero/__screenshots__/hero.visual.test.tsx/hero-a2-mobile-chromium-darwin.png +0 -0
  106. package/src/components/sections/hero/__screenshots__/hero.visual.test.tsx/hero-a2-mobile-chromium-linux.png +0 -0
  107. package/src/components/sections/hero/__screenshots__/hero.visual.test.tsx/hero-a2-tablet-chromium-darwin.png +0 -0
  108. package/src/components/sections/hero/__screenshots__/hero.visual.test.tsx/hero-a2-tablet-chromium-linux.png +0 -0
  109. package/src/components/sections/hero/__screenshots__/hero.visual.test.tsx/hero-a3-desktop-chromium-darwin.png +0 -0
  110. package/src/components/sections/hero/__screenshots__/hero.visual.test.tsx/hero-a3-desktop-chromium-linux.png +0 -0
  111. package/src/components/sections/hero/__screenshots__/hero.visual.test.tsx/hero-a3-mobile-chromium-darwin.png +0 -0
  112. package/src/components/sections/hero/__screenshots__/hero.visual.test.tsx/hero-a3-mobile-chromium-linux.png +0 -0
  113. package/src/components/sections/hero/__screenshots__/hero.visual.test.tsx/hero-a3-tablet-chromium-darwin.png +0 -0
  114. package/src/components/sections/hero/__screenshots__/hero.visual.test.tsx/hero-a3-tablet-chromium-linux.png +0 -0
  115. package/src/components/sections/hero/__screenshots__/hero.visual.test.tsx/hero-custom-class-chromium-darwin.png +0 -0
  116. package/src/components/sections/hero/__screenshots__/hero.visual.test.tsx/hero-custom-class-chromium-linux.png +0 -0
  117. package/src/components/sections/hero/__screenshots__/hero.visual.test.tsx/hero-default-chromium-linux.png +0 -0
  118. package/src/components/sections/hero/__screenshots__/hero.visual.test.tsx/hero-long-title-chromium-darwin.png +0 -0
  119. package/src/components/sections/hero/__screenshots__/hero.visual.test.tsx/hero-long-title-chromium-linux.png +0 -0
  120. package/src/components/sections/hero/hero.stories.tsx +274 -0
  121. package/src/components/sections/hero/hero.test.tsx +135 -0
  122. package/src/components/sections/hero/hero.tsx +453 -0
  123. package/src/components/sections/hero/hero.visual.test.tsx +140 -0
  124. package/src/components/sections/hero/index.ts +10 -0
  125. package/src/components/sections/prose/__screenshots__/prose.visual.test.tsx/prose-h3-heading-chromium-darwin.png +0 -0
  126. package/src/components/sections/prose/__screenshots__/prose.visual.test.tsx/prose-h3-heading-chromium-linux.png +0 -0
  127. package/src/components/sections/prose/__screenshots__/prose.visual.test.tsx/prose-multiple-paragraphs-chromium-darwin.png +0 -0
  128. package/src/components/sections/prose/__screenshots__/prose.visual.test.tsx/prose-multiple-paragraphs-chromium-linux.png +0 -0
  129. package/src/components/sections/prose/__screenshots__/prose.visual.test.tsx/prose-multiple-sections-chromium-darwin.png +0 -0
  130. package/src/components/sections/prose/__screenshots__/prose.visual.test.tsx/prose-multiple-sections-chromium-linux.png +0 -0
  131. package/src/components/sections/prose/__screenshots__/prose.visual.test.tsx/prose-single-section-chromium-darwin.png +0 -0
  132. package/src/components/sections/prose/__screenshots__/prose.visual.test.tsx/prose-single-section-chromium-linux.png +0 -0
  133. package/src/components/sections/prose/index.ts +6 -0
  134. package/src/components/sections/prose/prose.stories.tsx +144 -0
  135. package/src/components/sections/prose/prose.test.tsx +178 -0
  136. package/src/components/sections/prose/prose.tsx +88 -0
  137. package/src/components/sections/prose/prose.visual.test.tsx +105 -0
  138. package/src/components/sections/river/index.ts +1 -0
  139. package/src/components/sections/river/river.stories.tsx +237 -0
  140. package/src/components/sections/river/river.test.tsx +268 -0
  141. package/src/components/sections/river/river.tsx +173 -0
  142. package/src/components/sections/tout/index.ts +1 -0
  143. package/src/components/sections/tout/tout.stories.tsx +171 -0
  144. package/src/components/sections/tout/tout.test.tsx +242 -0
  145. package/src/components/sections/tout/tout.tsx +270 -0
  146. package/src/components/sections/two-column-section/index.ts +5 -0
  147. package/src/components/sections/two-column-section/two-column-section.stories.tsx +285 -0
  148. package/src/components/sections/two-column-section/two-column-section.tsx +162 -0
  149. package/src/hooks/index.ts +1 -0
  150. package/src/hooks/use-event-listener.ts +73 -0
  151. package/src/index.ts +155 -0
  152. package/src/lib/theme.ts +1000 -0
  153. package/src/lib/utils.ts +6 -0
  154. package/src/main.tsx +13 -0
  155. package/src/stories/GridSystem.stories.tsx +84 -0
  156. package/src/stories/Introduction.mdx +114 -0
  157. package/src/stories/ThemeProvider.stories.tsx +357 -0
  158. package/src/stories/TokenShowcase.stories.tsx +92 -0
  159. package/src/stories/TokenShowcase.tsx +1429 -0
  160. package/src/styles.css +11 -0
  161. package/src/theme/ThemeProvider.tsx +297 -0
  162. package/src/theme/hooks.ts +40 -0
  163. package/src/theme/index.ts +43 -0
  164. 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
+ });
@@ -0,0 +1,10 @@
1
+ export {
2
+ DEFAULT_TITLE_TYPOGRAPHY,
3
+ Hero,
4
+ HeroBackground,
5
+ type HeroBackgroundImageProps,
6
+ type HeroBackgroundStreamProps,
7
+ type HeroBackgroundVideoProps,
8
+ type HeroProps,
9
+ heroVariants,
10
+ } from "./hero";
@@ -0,0 +1,6 @@
1
+ export {
2
+ Prose,
3
+ type ProseProps,
4
+ ProseSection,
5
+ type ProseSectionProps,
6
+ } from "./prose";