@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,171 @@
1
+ import type { Meta, StoryObj } from "@storybook/react-vite";
2
+ import { Button } from "../../atoms/button";
3
+ import { NdstudioFooter } from "../../atoms/ndstudio-footer";
4
+ import { Tout } from ".";
5
+
6
+ const meta: Meta<typeof Tout> = {
7
+ title: "Sections/Tout",
8
+ component: Tout,
9
+ parameters: {
10
+ layout: "fullscreen",
11
+ },
12
+ argTypes: {
13
+ headline: {
14
+ control: "text",
15
+ description: "The headline text",
16
+ },
17
+ body: {
18
+ control: "text",
19
+ description: "The body text",
20
+ },
21
+ },
22
+ } as Meta<typeof Tout>;
23
+
24
+ export default meta;
25
+ type Story = StoryObj<typeof Tout>;
26
+
27
+ const PlaceholderBackground = () => (
28
+ <div className="absolute inset-0 bg-gradient-to-br from-gray-300 to-gray-400" />
29
+ );
30
+
31
+ const ImageBackground = () => (
32
+ <img
33
+ src="https://images.unsplash.com/photo-1557804506-669a67965ba0?w=1600&h=900&fit=crop"
34
+ alt=""
35
+ className="absolute inset-0 w-full h-full object-cover"
36
+ />
37
+ );
38
+
39
+ export const Playground: Story = {
40
+ render: (args) => <Tout {...args} />,
41
+ };
42
+ Playground.args = {
43
+ headline: "Brand-Large/Headline/Small",
44
+ body: "A river pattern stacks content in a simple vertical flow: one clear heading, a short block of copy, then the next step. It's ideal for guiding citizens through a process or story, keeping focus moving straight down the page with minimal choices and well-timed calls to action.",
45
+ primaryAction: <Button>Primary</Button>,
46
+ secondaryAction: <Button variant="charcoalOutline">Secondary</Button>,
47
+ backgroundMedia: <PlaceholderBackground />,
48
+ };
49
+
50
+ // =============================================================================
51
+ // Responsive Variants
52
+ // =============================================================================
53
+
54
+ export const Desktop: Story = {
55
+ render: () => (
56
+ <Tout
57
+ headline="Brand-Large/Headline/Small"
58
+ body="A river pattern stacks content in a simple vertical flow: one clear heading, a short block of copy, then the next step. It's ideal for guiding citizens through a process or story, keeping focus moving straight down the page with minimal choices and well-timed calls to action."
59
+ primaryAction={<Button>Primary</Button>}
60
+ secondaryAction={<Button variant="charcoalOutline">Secondary</Button>}
61
+ backgroundMedia={<PlaceholderBackground />}
62
+ />
63
+ ),
64
+ globals: {
65
+ viewport: { value: "lg", isRotated: false },
66
+ },
67
+ };
68
+
69
+ export const Tablet: Story = {
70
+ render: () => (
71
+ <Tout
72
+ headline="Brand-Large/Headline/Small"
73
+ body="A river pattern stacks content in a simple vertical flow: one clear heading, a short block of copy, then the next step. It's ideal for guiding citizens through a process or story, keeping focus moving straight down the page with minimal choices and well-timed calls to action."
74
+ primaryAction={<Button>Primary</Button>}
75
+ secondaryAction={<Button variant="charcoalOutline">Secondary</Button>}
76
+ backgroundMedia={<PlaceholderBackground />}
77
+ />
78
+ ),
79
+ globals: {
80
+ viewport: { value: "md", isRotated: false },
81
+ },
82
+ };
83
+
84
+ export const Mobile: Story = {
85
+ render: () => (
86
+ <Tout
87
+ headline="Brand-Large/Headline/Small"
88
+ body="A river pattern stacks content in a simple vertical flow: one clear heading, a short block of copy, then the next step. It's ideal for guiding citizens through a process or story, keeping focus moving straight down the page with minimal choices and well-timed calls to action."
89
+ primaryAction={<Button size="sm">Primary</Button>}
90
+ secondaryAction={
91
+ <Button size="sm" variant="charcoalOutline">
92
+ Secondary
93
+ </Button>
94
+ }
95
+ backgroundMedia={<PlaceholderBackground />}
96
+ />
97
+ ),
98
+ globals: {
99
+ viewport: { value: "sm", isRotated: false },
100
+ },
101
+ };
102
+
103
+ // =============================================================================
104
+ // Examples
105
+ // =============================================================================
106
+
107
+ /**
108
+ * With actual background image
109
+ */
110
+ export const WithImage: Story = {
111
+ render: () => (
112
+ <Tout
113
+ headline="Work with Purpose"
114
+ body="Join a team that's building the future of government services. We're looking for passionate individuals who want to make a difference."
115
+ primaryAction={<Button>View Careers</Button>}
116
+ secondaryAction={<Button variant="charcoalOutline">Learn More</Button>}
117
+ backgroundMedia={<ImageBackground />}
118
+ />
119
+ ),
120
+ };
121
+
122
+ /**
123
+ * Without secondary action
124
+ */
125
+ export const SingleAction: Story = {
126
+ render: () => (
127
+ <Tout
128
+ headline="Get Started Today"
129
+ body="Begin your journey with our comprehensive onboarding process designed to help you succeed from day one."
130
+ primaryAction={<Button>Start Now</Button>}
131
+ backgroundMedia={<PlaceholderBackground />}
132
+ />
133
+ ),
134
+ };
135
+
136
+ /**
137
+ * With video background (placeholder)
138
+ */
139
+ export const WithVideoPlaceholder: Story = {
140
+ render: () => (
141
+ <Tout
142
+ headline="Experience Innovation"
143
+ body="See how modern technology is transforming the way government serves its citizens."
144
+ primaryAction={<Button>Watch Video</Button>}
145
+ secondaryAction={<Button variant="charcoalOutline">Learn More</Button>}
146
+ backgroundMedia={
147
+ <div className="absolute inset-0 bg-gray-800 flex items-center justify-center">
148
+ <span className="text-gray-400 typography-body-small">
149
+ Video Background
150
+ </span>
151
+ </div>
152
+ }
153
+ />
154
+ ),
155
+ };
156
+
157
+ /**
158
+ * With NdstudioFooter - shows the National Design Studio branding at the bottom
159
+ */
160
+ export const WithNdstudioFooter: Story = {
161
+ render: () => (
162
+ <Tout
163
+ headline="Work with Purpose"
164
+ body="Join a team that's building the future of government services. We're looking for passionate individuals who want to make a difference."
165
+ primaryAction={<Button>View Careers</Button>}
166
+ secondaryAction={<Button variant="charcoalOutline">Learn More</Button>}
167
+ backgroundMedia={<ImageBackground />}
168
+ footer={<NdstudioFooter />}
169
+ />
170
+ ),
171
+ };
@@ -0,0 +1,242 @@
1
+ import { describe, expect, test } from "vitest";
2
+ import { page } from "vitest/browser";
3
+ import { render } from "vitest-browser-react";
4
+ import { Tout } from "./tout";
5
+
6
+ const PlaceholderBackground = () => (
7
+ <div data-testid="background" className="absolute inset-0 bg-gray-300" />
8
+ );
9
+
10
+ describe("Tout", () => {
11
+ describe("Accessibility", () => {
12
+ test("renders as section landmark", async () => {
13
+ render(
14
+ <Tout
15
+ headline="Test Headline"
16
+ body="Test body"
17
+ primaryAction={<button type="button">Action</button>}
18
+ backgroundMedia={<PlaceholderBackground />}
19
+ data-testid="tout"
20
+ />,
21
+ );
22
+
23
+ const tout = page.getByTestId("tout");
24
+ await expect.element(tout).toBeInTheDocument();
25
+ });
26
+
27
+ test("headline renders as h2", async () => {
28
+ render(
29
+ <Tout
30
+ headline="Tout Headline"
31
+ body="Test body"
32
+ primaryAction={<button type="button">Action</button>}
33
+ backgroundMedia={<PlaceholderBackground />}
34
+ />,
35
+ );
36
+
37
+ await expect
38
+ .element(page.getByRole("heading", { level: 2, name: "Tout Headline" }))
39
+ .toBeInTheDocument();
40
+ });
41
+
42
+ test("body text is accessible", async () => {
43
+ render(
44
+ <Tout
45
+ headline="Test"
46
+ body="Accessible body text"
47
+ primaryAction={<button type="button">Action</button>}
48
+ backgroundMedia={<PlaceholderBackground />}
49
+ />,
50
+ );
51
+
52
+ await expect
53
+ .element(page.getByText("Accessible body text"))
54
+ .toBeInTheDocument();
55
+ });
56
+
57
+ test("background is hidden from screen readers", async () => {
58
+ render(
59
+ <Tout
60
+ headline="Test"
61
+ body="Test"
62
+ primaryAction={<button type="button">Action</button>}
63
+ backgroundMedia={<PlaceholderBackground />}
64
+ data-testid="tout"
65
+ />,
66
+ );
67
+
68
+ const tout = page.getByTestId("tout");
69
+ await expect.element(tout).toBeInTheDocument();
70
+ });
71
+ });
72
+
73
+ describe("Props", () => {
74
+ test("renders with required props", async () => {
75
+ render(
76
+ <Tout
77
+ headline="Required Headline"
78
+ body="Required body"
79
+ primaryAction={<button type="button">Primary</button>}
80
+ backgroundMedia={<PlaceholderBackground />}
81
+ />,
82
+ );
83
+
84
+ await expect
85
+ .element(page.getByText("Required Headline"))
86
+ .toBeInTheDocument();
87
+ await expect.element(page.getByText("Required body")).toBeInTheDocument();
88
+ await expect
89
+ .element(page.getByRole("button", { name: "Primary" }))
90
+ .toBeInTheDocument();
91
+ await expect.element(page.getByTestId("background")).toBeInTheDocument();
92
+ });
93
+
94
+ test("renders secondary action when provided", async () => {
95
+ render(
96
+ <Tout
97
+ headline="Test"
98
+ body="Test"
99
+ primaryAction={<button type="button">Primary</button>}
100
+ secondaryAction={<button type="button">Secondary</button>}
101
+ backgroundMedia={<PlaceholderBackground />}
102
+ />,
103
+ );
104
+
105
+ await expect
106
+ .element(page.getByRole("button", { name: "Primary" }))
107
+ .toBeInTheDocument();
108
+ await expect
109
+ .element(page.getByRole("button", { name: "Secondary" }))
110
+ .toBeInTheDocument();
111
+ });
112
+
113
+ test("does not render secondary action when not provided", async () => {
114
+ render(
115
+ <Tout
116
+ headline="Test"
117
+ body="Test"
118
+ primaryAction={<button type="button">Primary</button>}
119
+ backgroundMedia={<PlaceholderBackground />}
120
+ />,
121
+ );
122
+
123
+ await expect
124
+ .element(page.getByRole("button", { name: "Primary" }))
125
+ .toBeInTheDocument();
126
+ // Should only have one button
127
+ const buttons = page.getByRole("button");
128
+ await expect.element(buttons).toBeInTheDocument();
129
+ });
130
+
131
+ test("supports custom className", async () => {
132
+ render(
133
+ <Tout
134
+ headline="Test"
135
+ body="Test"
136
+ primaryAction={<button type="button">Action</button>}
137
+ backgroundMedia={<PlaceholderBackground />}
138
+ className="custom-class"
139
+ data-testid="tout"
140
+ />,
141
+ );
142
+
143
+ const tout = page.getByTestId("tout");
144
+ await expect.element(tout).toHaveClass(/custom-class/);
145
+ });
146
+
147
+ test("spreads additional props to section element", async () => {
148
+ render(
149
+ <Tout
150
+ headline="Test"
151
+ body="Test"
152
+ primaryAction={<button type="button">Action</button>}
153
+ backgroundMedia={<PlaceholderBackground />}
154
+ data-testid="tout"
155
+ aria-label="Tout section"
156
+ />,
157
+ );
158
+
159
+ const tout = page.getByTestId("tout");
160
+ await expect.element(tout).toHaveAttribute("aria-label", "Tout section");
161
+ });
162
+ });
163
+
164
+ describe("Styling", () => {
165
+ test("applies full width class", async () => {
166
+ render(
167
+ <Tout
168
+ headline="Test"
169
+ body="Test"
170
+ primaryAction={<button type="button">Action</button>}
171
+ backgroundMedia={<PlaceholderBackground />}
172
+ data-testid="tout"
173
+ />,
174
+ );
175
+
176
+ const tout = page.getByTestId("tout");
177
+ await expect.element(tout).toHaveClass(/w-full/);
178
+ });
179
+
180
+ test("applies responsive height classes", async () => {
181
+ render(
182
+ <Tout
183
+ headline="Test"
184
+ body="Test"
185
+ primaryAction={<button type="button">Action</button>}
186
+ backgroundMedia={<PlaceholderBackground />}
187
+ data-testid="tout"
188
+ />,
189
+ );
190
+
191
+ const tout = page.getByTestId("tout");
192
+ await expect.element(tout).toHaveClass(/h-\[600px\]/);
193
+ });
194
+
195
+ test("applies relative positioning for z-index context", async () => {
196
+ render(
197
+ <Tout
198
+ headline="Test"
199
+ body="Test"
200
+ primaryAction={<button type="button">Action</button>}
201
+ backgroundMedia={<PlaceholderBackground />}
202
+ data-testid="tout"
203
+ />,
204
+ );
205
+
206
+ const tout = page.getByTestId("tout");
207
+ await expect.element(tout).toHaveClass(/relative/);
208
+ });
209
+ });
210
+
211
+ describe("Content", () => {
212
+ test("headline has correct styling", async () => {
213
+ render(
214
+ <Tout
215
+ headline="Styled Headline"
216
+ body="Test"
217
+ primaryAction={<button type="button">Action</button>}
218
+ backgroundMedia={<PlaceholderBackground />}
219
+ />,
220
+ );
221
+
222
+ const headline = page.getByRole("heading", { level: 2 });
223
+ await expect.element(headline).toHaveClass(/typography-headline-small/);
224
+ await expect.element(headline).toHaveClass(/text-gray-900/);
225
+ });
226
+
227
+ test("body has correct styling", async () => {
228
+ render(
229
+ <Tout
230
+ headline="Test"
231
+ body="Styled body text"
232
+ primaryAction={<button type="button">Action</button>}
233
+ backgroundMedia={<PlaceholderBackground />}
234
+ />,
235
+ );
236
+
237
+ const body = page.getByText("Styled body text");
238
+ await expect.element(body).toHaveClass(/typography-body-small/);
239
+ await expect.element(body).toHaveClass(/text-gray-800/);
240
+ });
241
+ });
242
+ });
@@ -0,0 +1,270 @@
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
+ * Tout variants for background and content styling
8
+ */
9
+ const toutVariants = tv({
10
+ base: [
11
+ // Full width
12
+ "w-full",
13
+ // Positioning context for background
14
+ "relative overflow-hidden",
15
+ // Responsive height: 600px mobile, 750px tablet, 900px desktop
16
+ "h-[600px] md:h-[750px] lg:h-[900px]",
17
+ ],
18
+ variants: {
19
+ variant: {
20
+ // Default light content styling
21
+ light: "",
22
+ // Dark content styling
23
+ dark: "",
24
+ },
25
+ align: {
26
+ left: "",
27
+ center: "",
28
+ },
29
+ },
30
+ defaultVariants: {
31
+ variant: "light",
32
+ align: "left",
33
+ },
34
+ });
35
+
36
+ /**
37
+ * Tout component based on Figma BaseKit / Touts
38
+ *
39
+ * A full-bleed section with a background image and overlaid content.
40
+ * Content can be positioned on the left side or centered.
41
+ *
42
+ * Variants:
43
+ * - light: Light text styling (default)
44
+ * - dark: Dark text styling
45
+ *
46
+ * Alignment:
47
+ * - left: Content aligned to the left (default)
48
+ * - center: Content centered
49
+ *
50
+ * Responsive behavior:
51
+ * - Mobile (sm): 600px height, 4 columns with gap-20, content spans all 4 cols
52
+ * - Tablet (md): 750px height, 12 columns with gap-20, content spans 9 cols (left) or centered
53
+ * - Desktop (lg): 900px height, 24 columns with gap-20, content spans 9 cols (left) or centered
54
+ *
55
+ * This component is self-contained - do NOT wrap in a grid-container.
56
+ */
57
+ export interface ToutProps
58
+ extends React.HTMLAttributes<HTMLElement>,
59
+ VariantProps<typeof toutVariants> {
60
+ /**
61
+ * The headline displayed in the tout
62
+ */
63
+ headline: React.ReactNode;
64
+ /**
65
+ * The body text displayed below the headline (optional)
66
+ */
67
+ body?: string;
68
+ /**
69
+ * Primary action button (required)
70
+ */
71
+ primaryAction: React.ReactNode;
72
+ /**
73
+ * Secondary action button (optional)
74
+ */
75
+ secondaryAction?: React.ReactNode;
76
+ /**
77
+ * Background media (image or video element)
78
+ * Should be a full-bleed element that covers the entire section
79
+ */
80
+ backgroundMedia: React.ReactNode;
81
+ /**
82
+ * Optional footer content to display at the bottom of the section.
83
+ * Use with NdstudioFooter component for the branded footer.
84
+ */
85
+ footer?: React.ReactNode;
86
+ /**
87
+ * Component-level theme overrides.
88
+ * Allows customization of colors, spacing, and surface properties.
89
+ */
90
+ theme?: ComponentTheme;
91
+ }
92
+
93
+ /**
94
+ * Tout component for hero-like sections with background media and overlaid content.
95
+ *
96
+ * This component is self-contained with its own grid.
97
+ * Grid setup:
98
+ * - Desktop (lg): 24 columns, gap-spacing-20, content spans 9 cols
99
+ * - Tablet (md): 12 columns, gap-spacing-20, content spans 9 cols
100
+ * - Mobile: 4 columns, gap-spacing-20, content spans all 4 cols
101
+ *
102
+ * @example
103
+ * ```tsx
104
+ * <Tout
105
+ * headline="Feature Headline"
106
+ * body="Description of the feature..."
107
+ * primaryAction={<Button>Primary</Button>}
108
+ * secondaryAction={<Button variant="charcoalOutline">Secondary</Button>}
109
+ * backgroundMedia={
110
+ * <img
111
+ * src="/background.jpg"
112
+ * alt=""
113
+ * className="absolute inset-0 w-full h-full object-cover"
114
+ * />
115
+ * }
116
+ * footer={<NdstudioFooter />}
117
+ * />
118
+ * ```
119
+ */
120
+ const Tout = React.forwardRef<HTMLElement, ToutProps>(
121
+ (
122
+ {
123
+ className,
124
+ variant = "light",
125
+ align = "left",
126
+ headline,
127
+ body,
128
+ primaryAction,
129
+ secondaryAction,
130
+ backgroundMedia,
131
+ footer,
132
+ theme,
133
+ style,
134
+ ...props
135
+ },
136
+ ref,
137
+ ) => {
138
+ const isCentered = align === "center";
139
+ const isDark = variant === "dark";
140
+ const themeStyles = themeToStyleVars(theme);
141
+
142
+ return (
143
+ <section
144
+ ref={ref}
145
+ className={toutVariants({ variant, align, class: className })}
146
+ style={{ ...themeStyles, ...style }}
147
+ {...props}
148
+ >
149
+ {/* Background layer - full bleed */}
150
+ <div
151
+ aria-hidden="true"
152
+ className="absolute inset-0 pointer-events-none"
153
+ >
154
+ {/* Fallback background color */}
155
+ <div className="absolute inset-0 bg-gray-500" />
156
+ {/* Background media */}
157
+ {backgroundMedia}
158
+ </div>
159
+
160
+ {/* Inner grid for content alignment - uses primitive spacing tokens */}
161
+ <div
162
+ className={cn(
163
+ // Position above background
164
+ "relative z-10",
165
+ // Grid setup with responsive columns
166
+ "grid w-full h-full",
167
+ // Mobile: 4 columns with gap-20
168
+ "grid-cols-4 gap-spacing-20",
169
+ // Tablet (md): 12 columns
170
+ "md:grid-cols-12",
171
+ // Desktop (lg): 24 columns
172
+ "lg:grid-cols-24",
173
+ // Max width and centering like grid-container
174
+ "max-w-[var(--breakpoint-lg)] mx-auto",
175
+ // Responsive margins matching grid-container - uses primitive spacing tokens
176
+ "px-spacing-20 md:px-spacing-56 lg:px-spacing-72",
177
+ // Vertical padding to position content at bottom - uses primitive spacing tokens
178
+ "py-spacing-36 md:py-spacing-56 lg:py-spacing-72",
179
+ )}
180
+ style={{
181
+ // Grid spacing theme overrides
182
+ ...(theme?.spatial?.gridSmallMargin && {
183
+ paddingLeft: "var(--theme-grid-small-margin)",
184
+ paddingRight: "var(--theme-grid-small-margin)",
185
+ }),
186
+ ...(theme?.spatial?.gridSmallGutter && {
187
+ gap: "var(--theme-grid-small-gutter)",
188
+ }),
189
+ }}
190
+ >
191
+ {/* Content column - aligned to grid */}
192
+ <div
193
+ className={cn(
194
+ // Flex container for content
195
+ "flex flex-col",
196
+ isCentered ? "justify-start items-center" : "justify-end",
197
+ // Responsive gap between text and buttons - uses primitive spacing tokens
198
+ "gap-spacing-28 md:gap-spacing-36",
199
+ // Mobile: all 4 cols
200
+ "col-span-4",
201
+ // Tablet & Desktop: 9 cols left-aligned, full width centered
202
+ isCentered ? "md:col-span-12 lg:col-span-24" : "md:col-span-9",
203
+ )}
204
+ >
205
+ {/* Text content stack - uses primitive spacing tokens */}
206
+ <div
207
+ className={cn(
208
+ "flex flex-col gap-spacing-16",
209
+ isCentered && "items-center text-center",
210
+ )}
211
+ >
212
+ <h2
213
+ className={cn(
214
+ "typography-headline-small",
215
+ isDark ? "text-gray-100" : "text-gray-900",
216
+ )}
217
+ style={{
218
+ color: theme?.colors?.textPrimary
219
+ ? "var(--theme-text-primary)"
220
+ : undefined,
221
+ }}
222
+ >
223
+ {headline}
224
+ </h2>
225
+ {body && (
226
+ <p
227
+ className={cn(
228
+ "typography-body-small",
229
+ isDark ? "text-gray-400" : "text-gray-800",
230
+ )}
231
+ style={{
232
+ color: theme?.colors?.textSecondary
233
+ ? "var(--theme-text-secondary)"
234
+ : undefined,
235
+ }}
236
+ >
237
+ {body}
238
+ </p>
239
+ )}
240
+ </div>
241
+
242
+ {/* CTA buttons - uses primitive spacing tokens */}
243
+ <div
244
+ className={cn(
245
+ "flex flex-row",
246
+ isCentered ? "justify-center" : "items-start",
247
+ // Responsive gap between buttons
248
+ "gap-spacing-8 md:gap-spacing-12",
249
+ "[&>*]:flex-shrink-0",
250
+ )}
251
+ >
252
+ {primaryAction}
253
+ {secondaryAction}
254
+ </div>
255
+ </div>
256
+ </div>
257
+
258
+ {/* Footer slot */}
259
+ {footer && (
260
+ <div className="absolute bottom-6 md:bottom-8 left-0 right-0 z-10">
261
+ {footer}
262
+ </div>
263
+ )}
264
+ </section>
265
+ );
266
+ },
267
+ );
268
+ Tout.displayName = "Tout";
269
+
270
+ export { Tout, toutVariants };
@@ -0,0 +1,5 @@
1
+ export {
2
+ TwoColumnSection,
3
+ type TwoColumnSectionProps,
4
+ twoColumnSectionVariants,
5
+ } from "./two-column-section";