@nationaldesignstudio/react 0.0.7 → 0.0.8

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 (170) hide show
  1. package/README.md +0 -4
  2. package/dist/assets/fonts/PPNeueMontreal-Variable.woff2 +0 -0
  3. package/dist/assets/react.svg +1 -0
  4. package/dist/components/atoms/accordion/accordion.d.ts +50 -0
  5. package/dist/components/{button → atoms/button}/button.d.ts +5 -4
  6. package/dist/components/atoms/pager-control/pager-control.d.ts +62 -0
  7. package/dist/components/{card → organisms/card}/card.d.ts +6 -2
  8. package/dist/components/{navbar → organisms/navbar}/navbar.d.ts +27 -1
  9. package/dist/components/sections/banner/banner.d.ts +64 -0
  10. package/dist/components/sections/card-grid/card-grid.d.ts +53 -0
  11. package/dist/components/sections/faq-section/faq-section.d.ts +44 -0
  12. package/dist/components/sections/hero/hero.d.ts +73 -0
  13. package/dist/components/sections/river/river.d.ts +63 -0
  14. package/dist/components/sections/tout/tout.d.ts +73 -0
  15. package/dist/components/sections/two-column-section/two-column-section.d.ts +58 -0
  16. package/dist/index.d.ts +28 -12
  17. package/dist/index.js +3185 -813
  18. package/dist/index.js.map +1 -1
  19. package/dist/tailwind.css +23 -0
  20. package/dist/tokens.css +2009 -103
  21. package/package.json +23 -5
  22. package/src/App.css +0 -0
  23. package/src/App.tsx +7 -0
  24. package/src/assets/fonts/PPNeueMontreal-Variable.woff2 +0 -0
  25. package/src/assets/react.svg +1 -0
  26. package/src/components/atoms/accordion/accordion.stories.tsx +228 -0
  27. package/src/components/atoms/accordion/accordion.tsx +137 -0
  28. package/src/components/atoms/accordion/index.ts +6 -0
  29. package/src/components/atoms/button/__screenshots__/button.visual.test.tsx/button-charcoal-chromium-darwin.png +0 -0
  30. package/src/components/atoms/button/__screenshots__/button.visual.test.tsx/button-charcoal-chromium-linux.png +0 -0
  31. package/src/components/atoms/button/__screenshots__/button.visual.test.tsx/button-charcoal-outline-chromium-darwin.png +0 -0
  32. package/src/components/atoms/button/__screenshots__/button.visual.test.tsx/button-charcoal-outline-chromium-linux.png +0 -0
  33. package/src/components/atoms/button/__screenshots__/button.visual.test.tsx/button-charcoal-outline-quiet-chromium-darwin.png +0 -0
  34. package/src/components/atoms/button/__screenshots__/button.visual.test.tsx/button-charcoal-outline-quiet-chromium-linux.png +0 -0
  35. package/src/components/atoms/button/__screenshots__/button.visual.test.tsx/button-disabled-chromium-darwin.png +0 -0
  36. package/src/components/atoms/button/__screenshots__/button.visual.test.tsx/button-disabled-chromium-linux.png +0 -0
  37. package/src/components/atoms/button/__screenshots__/button.visual.test.tsx/button-ivory-chromium-darwin.png +0 -0
  38. package/src/components/atoms/button/__screenshots__/button.visual.test.tsx/button-ivory-chromium-linux.png +0 -0
  39. package/src/components/atoms/button/__screenshots__/button.visual.test.tsx/button-ivory-outline-chromium-darwin.png +0 -0
  40. package/src/components/atoms/button/__screenshots__/button.visual.test.tsx/button-ivory-outline-chromium-linux.png +0 -0
  41. package/src/components/atoms/button/__screenshots__/button.visual.test.tsx/button-ivory-outline-quiet-chromium-darwin.png +0 -0
  42. package/src/components/atoms/button/__screenshots__/button.visual.test.tsx/button-ivory-outline-quiet-chromium-linux.png +0 -0
  43. package/src/components/atoms/button/__screenshots__/button.visual.test.tsx/button-size-large-chromium-darwin.png +0 -0
  44. package/src/components/atoms/button/__screenshots__/button.visual.test.tsx/button-size-large-chromium-linux.png +0 -0
  45. package/src/components/atoms/button/__screenshots__/button.visual.test.tsx/button-size-medium-chromium-darwin.png +0 -0
  46. package/src/components/atoms/button/__screenshots__/button.visual.test.tsx/button-size-medium-chromium-linux.png +0 -0
  47. package/src/components/atoms/button/__screenshots__/button.visual.test.tsx/button-size-small-chromium-darwin.png +0 -0
  48. package/src/components/atoms/button/__screenshots__/button.visual.test.tsx/button-size-small-chromium-linux.png +0 -0
  49. package/src/components/atoms/button/button.stories.tsx +93 -0
  50. package/src/components/atoms/button/button.test.tsx +141 -0
  51. package/src/components/atoms/button/button.tsx +95 -0
  52. package/src/components/atoms/button/button.visual.test.tsx +102 -0
  53. package/src/components/atoms/button/icon-button.stories.tsx +175 -0
  54. package/src/components/atoms/button/icon-button.tsx +90 -0
  55. package/src/components/atoms/button/index.ts +6 -0
  56. package/src/components/atoms/pager-control/index.ts +5 -0
  57. package/src/components/atoms/pager-control/pager-control.stories.tsx +212 -0
  58. package/src/components/atoms/pager-control/pager-control.test.tsx +149 -0
  59. package/src/components/atoms/pager-control/pager-control.tsx +328 -0
  60. package/src/components/organisms/card/__screenshots__/card.visual.test.tsx/card-default-vertical-chromium-darwin.png +0 -0
  61. package/src/components/organisms/card/__screenshots__/card.visual.test.tsx/card-default-vertical-chromium-linux.png +0 -0
  62. package/src/components/organisms/card/__screenshots__/card.visual.test.tsx/card-horizontal-chromium-darwin.png +0 -0
  63. package/src/components/organisms/card/__screenshots__/card.visual.test.tsx/card-horizontal-chromium-linux.png +0 -0
  64. package/src/components/organisms/card/__screenshots__/card.visual.test.tsx/card-minimal-chromium-darwin.png +0 -0
  65. package/src/components/organisms/card/__screenshots__/card.visual.test.tsx/card-minimal-chromium-linux.png +0 -0
  66. package/src/components/organisms/card/__screenshots__/card.visual.test.tsx/card-without-actions-chromium-darwin.png +0 -0
  67. package/src/components/organisms/card/__screenshots__/card.visual.test.tsx/card-without-actions-chromium-linux.png +0 -0
  68. package/src/components/organisms/card/__screenshots__/card.visual.test.tsx/card-without-eyebrow-chromium-darwin.png +0 -0
  69. package/src/components/organisms/card/__screenshots__/card.visual.test.tsx/card-without-eyebrow-chromium-linux.png +0 -0
  70. package/src/components/organisms/card/__screenshots__/card.visual.test.tsx/card-without-image-chromium-darwin.png +0 -0
  71. package/src/components/organisms/card/__screenshots__/card.visual.test.tsx/card-without-image-chromium-linux.png +0 -0
  72. package/src/components/organisms/card/card.stories.tsx +293 -0
  73. package/src/components/organisms/card/card.test.tsx +245 -0
  74. package/src/components/organisms/card/card.tsx +227 -0
  75. package/src/components/organisms/card/card.visual.test.tsx +197 -0
  76. package/src/components/organisms/card/index.ts +19 -0
  77. package/src/components/organisms/navbar/__screenshots__/navbar.visual.test.tsx/navbar-active-link-chromium-darwin.png +0 -0
  78. package/src/components/organisms/navbar/__screenshots__/navbar.visual.test.tsx/navbar-active-link-chromium-linux.png +0 -0
  79. package/src/components/organisms/navbar/__screenshots__/navbar.visual.test.tsx/navbar-brand-only-chromium-darwin.png +0 -0
  80. package/src/components/organisms/navbar/__screenshots__/navbar.visual.test.tsx/navbar-brand-only-chromium-linux.png +0 -0
  81. package/src/components/organisms/navbar/__screenshots__/navbar.visual.test.tsx/navbar-default-chromium-darwin.png +0 -0
  82. package/src/components/organisms/navbar/__screenshots__/navbar.visual.test.tsx/navbar-default-chromium-linux.png +0 -0
  83. package/src/components/organisms/navbar/index.ts +18 -0
  84. package/src/components/organisms/navbar/navbar.stories.tsx +313 -0
  85. package/src/components/organisms/navbar/navbar.test.tsx +190 -0
  86. package/src/components/organisms/navbar/navbar.tsx +365 -0
  87. package/src/components/organisms/navbar/navbar.visual.test.tsx +85 -0
  88. package/src/components/organisms/us-gov-banner/__screenshots__/us-gov-banner.visual.test.tsx/us-gov-banner-custom-icon-chromium-darwin.png +0 -0
  89. package/src/components/organisms/us-gov-banner/__screenshots__/us-gov-banner.visual.test.tsx/us-gov-banner-custom-icon-chromium-linux.png +0 -0
  90. package/src/components/organisms/us-gov-banner/__screenshots__/us-gov-banner.visual.test.tsx/us-gov-banner-custom-text-chromium-darwin.png +0 -0
  91. package/src/components/organisms/us-gov-banner/__screenshots__/us-gov-banner.visual.test.tsx/us-gov-banner-custom-text-chromium-linux.png +0 -0
  92. package/src/components/organisms/us-gov-banner/__screenshots__/us-gov-banner.visual.test.tsx/us-gov-banner-default-chromium-darwin.png +0 -0
  93. package/src/components/organisms/us-gov-banner/__screenshots__/us-gov-banner.visual.test.tsx/us-gov-banner-default-chromium-linux.png +0 -0
  94. package/src/components/organisms/us-gov-banner/index.ts +1 -0
  95. package/src/components/organisms/us-gov-banner/us-gov-banner.stories.tsx +35 -0
  96. package/src/components/organisms/us-gov-banner/us-gov-banner.test.tsx +107 -0
  97. package/src/components/organisms/us-gov-banner/us-gov-banner.tsx +73 -0
  98. package/src/components/organisms/us-gov-banner/us-gov-banner.visual.test.tsx +46 -0
  99. package/src/components/sections/banner/banner.stories.tsx +150 -0
  100. package/src/components/sections/banner/banner.test.tsx +185 -0
  101. package/src/components/sections/banner/banner.tsx +132 -0
  102. package/src/components/sections/banner/index.ts +2 -0
  103. package/src/components/sections/card-grid/card-grid.stories.tsx +351 -0
  104. package/src/components/sections/card-grid/card-grid.tsx +118 -0
  105. package/src/components/sections/card-grid/index.ts +1 -0
  106. package/src/components/sections/faq-section/faq-section.tsx +76 -0
  107. package/src/components/sections/faq-section/index.ts +2 -0
  108. package/src/components/sections/hero/__screenshots__/hero.visual.test.tsx/hero-a1-desktop-chromium-darwin.png +0 -0
  109. package/src/components/sections/hero/__screenshots__/hero.visual.test.tsx/hero-a1-desktop-chromium-linux.png +0 -0
  110. package/src/components/sections/hero/__screenshots__/hero.visual.test.tsx/hero-a1-mobile-chromium-darwin.png +0 -0
  111. package/src/components/sections/hero/__screenshots__/hero.visual.test.tsx/hero-a1-mobile-chromium-linux.png +0 -0
  112. package/src/components/sections/hero/__screenshots__/hero.visual.test.tsx/hero-a1-tablet-chromium-darwin.png +0 -0
  113. package/src/components/sections/hero/__screenshots__/hero.visual.test.tsx/hero-a1-tablet-chromium-linux.png +0 -0
  114. package/src/components/sections/hero/__screenshots__/hero.visual.test.tsx/hero-a2-desktop-chromium-darwin.png +0 -0
  115. package/src/components/sections/hero/__screenshots__/hero.visual.test.tsx/hero-a2-desktop-chromium-linux.png +0 -0
  116. package/src/components/sections/hero/__screenshots__/hero.visual.test.tsx/hero-a2-mobile-chromium-darwin.png +0 -0
  117. package/src/components/sections/hero/__screenshots__/hero.visual.test.tsx/hero-a2-mobile-chromium-linux.png +0 -0
  118. package/src/components/sections/hero/__screenshots__/hero.visual.test.tsx/hero-a2-tablet-chromium-darwin.png +0 -0
  119. package/src/components/sections/hero/__screenshots__/hero.visual.test.tsx/hero-a2-tablet-chromium-linux.png +0 -0
  120. package/src/components/sections/hero/__screenshots__/hero.visual.test.tsx/hero-a3-desktop-chromium-darwin.png +0 -0
  121. package/src/components/sections/hero/__screenshots__/hero.visual.test.tsx/hero-a3-desktop-chromium-linux.png +0 -0
  122. package/src/components/sections/hero/__screenshots__/hero.visual.test.tsx/hero-a3-mobile-chromium-darwin.png +0 -0
  123. package/src/components/sections/hero/__screenshots__/hero.visual.test.tsx/hero-a3-mobile-chromium-linux.png +0 -0
  124. package/src/components/sections/hero/__screenshots__/hero.visual.test.tsx/hero-a3-tablet-chromium-darwin.png +0 -0
  125. package/src/components/sections/hero/__screenshots__/hero.visual.test.tsx/hero-a3-tablet-chromium-linux.png +0 -0
  126. package/src/components/sections/hero/__screenshots__/hero.visual.test.tsx/hero-custom-class-chromium-darwin.png +0 -0
  127. package/src/components/sections/hero/__screenshots__/hero.visual.test.tsx/hero-custom-class-chromium-linux.png +0 -0
  128. package/src/components/sections/hero/__screenshots__/hero.visual.test.tsx/hero-default-chromium-linux.png +0 -0
  129. package/src/components/sections/hero/__screenshots__/hero.visual.test.tsx/hero-long-title-chromium-darwin.png +0 -0
  130. package/src/components/sections/hero/__screenshots__/hero.visual.test.tsx/hero-long-title-chromium-linux.png +0 -0
  131. package/src/components/sections/hero/hero.stories.tsx +145 -0
  132. package/src/components/sections/hero/hero.test.tsx +135 -0
  133. package/src/components/sections/hero/hero.tsx +190 -0
  134. package/src/components/sections/hero/hero.visual.test.tsx +140 -0
  135. package/src/components/sections/hero/index.ts +1 -0
  136. package/src/components/sections/prose/__screenshots__/prose.visual.test.tsx/prose-h3-heading-chromium-darwin.png +0 -0
  137. package/src/components/sections/prose/__screenshots__/prose.visual.test.tsx/prose-h3-heading-chromium-linux.png +0 -0
  138. package/src/components/sections/prose/__screenshots__/prose.visual.test.tsx/prose-multiple-paragraphs-chromium-darwin.png +0 -0
  139. package/src/components/sections/prose/__screenshots__/prose.visual.test.tsx/prose-multiple-paragraphs-chromium-linux.png +0 -0
  140. package/src/components/sections/prose/__screenshots__/prose.visual.test.tsx/prose-multiple-sections-chromium-darwin.png +0 -0
  141. package/src/components/sections/prose/__screenshots__/prose.visual.test.tsx/prose-multiple-sections-chromium-linux.png +0 -0
  142. package/src/components/sections/prose/__screenshots__/prose.visual.test.tsx/prose-single-section-chromium-darwin.png +0 -0
  143. package/src/components/sections/prose/__screenshots__/prose.visual.test.tsx/prose-single-section-chromium-linux.png +0 -0
  144. package/src/components/sections/prose/index.ts +6 -0
  145. package/src/components/sections/prose/prose.stories.tsx +144 -0
  146. package/src/components/sections/prose/prose.test.tsx +178 -0
  147. package/src/components/sections/prose/prose.tsx +88 -0
  148. package/src/components/sections/prose/prose.visual.test.tsx +105 -0
  149. package/src/components/sections/river/index.ts +1 -0
  150. package/src/components/sections/river/river.stories.tsx +237 -0
  151. package/src/components/sections/river/river.test.tsx +268 -0
  152. package/src/components/sections/river/river.tsx +175 -0
  153. package/src/components/sections/tout/index.ts +1 -0
  154. package/src/components/sections/tout/tout.stories.tsx +154 -0
  155. package/src/components/sections/tout/tout.test.tsx +242 -0
  156. package/src/components/sections/tout/tout.tsx +205 -0
  157. package/src/components/sections/two-column-section/index.ts +5 -0
  158. package/src/components/sections/two-column-section/two-column-section.stories.tsx +285 -0
  159. package/src/components/sections/two-column-section/two-column-section.tsx +155 -0
  160. package/src/index.ts +98 -0
  161. package/src/lib/utils.ts +6 -0
  162. package/src/main.tsx +13 -0
  163. package/src/stories/Introduction.mdx +114 -0
  164. package/src/stories/TokenShowcase.stories.tsx +92 -0
  165. package/src/stories/TokenShowcase.tsx +1352 -0
  166. package/src/styles.css +11 -0
  167. package/dist/components/hero/hero.d.ts +0 -17
  168. /package/dist/components/{button → atoms/button}/icon-button.d.ts +0 -0
  169. /package/dist/components/{us-gov-banner → organisms/us-gov-banner}/us-gov-banner.d.ts +0 -0
  170. /package/dist/components/{prose → sections/prose}/prose.d.ts +0 -0
@@ -0,0 +1,154 @@
1
+ import type { Meta, StoryObj } from "@storybook/react-vite";
2
+ import { Button } from "../../atoms/button";
3
+ import { Tout } from ".";
4
+
5
+ const meta: Meta<typeof Tout> = {
6
+ title: "Sections/Tout",
7
+ component: Tout,
8
+ parameters: {
9
+ layout: "fullscreen",
10
+ },
11
+ argTypes: {
12
+ headline: {
13
+ control: "text",
14
+ description: "The headline text",
15
+ },
16
+ body: {
17
+ control: "text",
18
+ description: "The body text",
19
+ },
20
+ },
21
+ } as Meta<typeof Tout>;
22
+
23
+ export default meta;
24
+ type Story = StoryObj<typeof Tout>;
25
+
26
+ const PlaceholderBackground = () => (
27
+ <div className="absolute inset-0 bg-gradient-to-br from-gray-300 to-gray-400" />
28
+ );
29
+
30
+ const ImageBackground = () => (
31
+ <img
32
+ src="https://images.unsplash.com/photo-1557804506-669a67965ba0?w=1600&h=900&fit=crop"
33
+ alt=""
34
+ className="absolute inset-0 w-full h-full object-cover"
35
+ />
36
+ );
37
+
38
+ export const Playground: Story = {
39
+ render: (args) => <Tout {...args} />,
40
+ };
41
+ Playground.args = {
42
+ headline: "Brand-Large/Headline/Small",
43
+ 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.",
44
+ primaryAction: <Button>Primary</Button>,
45
+ secondaryAction: <Button variant="charcoalOutline">Secondary</Button>,
46
+ backgroundMedia: <PlaceholderBackground />,
47
+ };
48
+
49
+ // =============================================================================
50
+ // Responsive Variants
51
+ // =============================================================================
52
+
53
+ export const Desktop: Story = {
54
+ render: () => (
55
+ <Tout
56
+ headline="Brand-Large/Headline/Small"
57
+ 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."
58
+ primaryAction={<Button>Primary</Button>}
59
+ secondaryAction={<Button variant="charcoalOutline">Secondary</Button>}
60
+ backgroundMedia={<PlaceholderBackground />}
61
+ />
62
+ ),
63
+ globals: {
64
+ viewport: { value: "lg", isRotated: false },
65
+ },
66
+ };
67
+
68
+ export const Tablet: Story = {
69
+ render: () => (
70
+ <Tout
71
+ headline="Brand-Large/Headline/Small"
72
+ 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."
73
+ primaryAction={<Button>Primary</Button>}
74
+ secondaryAction={<Button variant="charcoalOutline">Secondary</Button>}
75
+ backgroundMedia={<PlaceholderBackground />}
76
+ />
77
+ ),
78
+ globals: {
79
+ viewport: { value: "md", isRotated: false },
80
+ },
81
+ };
82
+
83
+ export const Mobile: Story = {
84
+ render: () => (
85
+ <Tout
86
+ headline="Brand-Large/Headline/Small"
87
+ 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."
88
+ primaryAction={<Button size="sm">Primary</Button>}
89
+ secondaryAction={
90
+ <Button size="sm" variant="charcoalOutline">
91
+ Secondary
92
+ </Button>
93
+ }
94
+ backgroundMedia={<PlaceholderBackground />}
95
+ />
96
+ ),
97
+ globals: {
98
+ viewport: { value: "sm", isRotated: false },
99
+ },
100
+ };
101
+
102
+ // =============================================================================
103
+ // Examples
104
+ // =============================================================================
105
+
106
+ /**
107
+ * With actual background image
108
+ */
109
+ export const WithImage: Story = {
110
+ render: () => (
111
+ <Tout
112
+ headline="Work with Purpose"
113
+ body="Join a team that's building the future of government services. We're looking for passionate individuals who want to make a difference."
114
+ primaryAction={<Button>View Careers</Button>}
115
+ secondaryAction={<Button variant="charcoalOutline">Learn More</Button>}
116
+ backgroundMedia={<ImageBackground />}
117
+ />
118
+ ),
119
+ };
120
+
121
+ /**
122
+ * Without secondary action
123
+ */
124
+ export const SingleAction: Story = {
125
+ render: () => (
126
+ <Tout
127
+ headline="Get Started Today"
128
+ body="Begin your journey with our comprehensive onboarding process designed to help you succeed from day one."
129
+ primaryAction={<Button>Start Now</Button>}
130
+ backgroundMedia={<PlaceholderBackground />}
131
+ />
132
+ ),
133
+ };
134
+
135
+ /**
136
+ * With video background (placeholder)
137
+ */
138
+ export const WithVideoPlaceholder: Story = {
139
+ render: () => (
140
+ <Tout
141
+ headline="Experience Innovation"
142
+ body="See how modern technology is transforming the way government serves its citizens."
143
+ primaryAction={<Button>Watch Video</Button>}
144
+ secondaryAction={<Button variant="charcoalOutline">Learn More</Button>}
145
+ backgroundMedia={
146
+ <div className="absolute inset-0 bg-gray-800 flex items-center justify-center">
147
+ <span className="text-gray-400 typography-body-small">
148
+ Video Background
149
+ </span>
150
+ </div>
151
+ }
152
+ />
153
+ ),
154
+ };
@@ -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,205 @@
1
+ import * as React from "react";
2
+ import { cn } from "@/lib/utils";
3
+
4
+ /**
5
+ * Tout component based on Figma BaseKit / Touts
6
+ *
7
+ * A full-bleed section with a background image and overlaid content.
8
+ * Content can be positioned on the left side or centered.
9
+ *
10
+ * Responsive behavior:
11
+ * - Mobile (sm): 600px height, 4 columns with gap-20, content spans all 4 cols
12
+ * - Tablet (md): 750px height, 12 columns with gap-20, content spans 9 cols (left) or centered
13
+ * - Desktop (lg): 900px height, 24 columns with gap-20, content spans 9 cols (left) or centered
14
+ *
15
+ * This component is self-contained - do NOT wrap in a grid-container.
16
+ */
17
+ export interface ToutProps extends React.HTMLAttributes<HTMLElement> {
18
+ /**
19
+ * The headline displayed in the tout
20
+ */
21
+ headline: React.ReactNode;
22
+ /**
23
+ * The body text displayed below the headline (optional)
24
+ */
25
+ body?: string;
26
+ /**
27
+ * Primary action button (required)
28
+ */
29
+ primaryAction: React.ReactNode;
30
+ /**
31
+ * Secondary action button (optional)
32
+ */
33
+ secondaryAction?: React.ReactNode;
34
+ /**
35
+ * Background media (image or video element)
36
+ * Should be a full-bleed element that covers the entire section
37
+ */
38
+ backgroundMedia: React.ReactNode;
39
+ /**
40
+ * Content alignment: left (default) or center
41
+ */
42
+ align?: "left" | "center";
43
+ /**
44
+ * Show National Design Studio footer link at the bottom of the section
45
+ */
46
+ ndstudioFooter?: boolean;
47
+ }
48
+
49
+ /**
50
+ * Tout component for hero-like sections with background media and overlaid content.
51
+ *
52
+ * This component is self-contained with its own grid.
53
+ * Grid setup:
54
+ * - Desktop (lg): 24 columns, gap-spacing-20, content spans 9 cols
55
+ * - Tablet (md): 12 columns, gap-spacing-20, content spans 9 cols
56
+ * - Mobile: 4 columns, gap-spacing-20, content spans all 4 cols
57
+ *
58
+ * @example
59
+ * ```tsx
60
+ * <Tout
61
+ * headline="Feature Headline"
62
+ * body="Description of the feature..."
63
+ * primaryAction={<Button>Primary</Button>}
64
+ * secondaryAction={<Button variant="charcoalOutline">Secondary</Button>}
65
+ * backgroundMedia={
66
+ * <img
67
+ * src="/background.jpg"
68
+ * alt=""
69
+ * className="absolute inset-0 w-full h-full object-cover"
70
+ * />
71
+ * }
72
+ * />
73
+ * ```
74
+ */
75
+ const Tout = React.forwardRef<HTMLElement, ToutProps>(
76
+ (
77
+ {
78
+ className,
79
+ headline,
80
+ body,
81
+ primaryAction,
82
+ secondaryAction,
83
+ backgroundMedia,
84
+ align = "left",
85
+ ndstudioFooter,
86
+ ...props
87
+ },
88
+ ref,
89
+ ) => {
90
+ const isCentered = align === "center";
91
+
92
+ return (
93
+ <section
94
+ ref={ref}
95
+ className={cn(
96
+ // Full width
97
+ "w-full",
98
+ // Positioning context for background
99
+ "relative overflow-hidden",
100
+ // Responsive height: 600px mobile, 750px tablet, 900px desktop
101
+ "h-[600px] md:h-[750px] lg:h-[900px]",
102
+ className,
103
+ )}
104
+ {...props}
105
+ >
106
+ {/* Background layer - full bleed */}
107
+ <div
108
+ aria-hidden="true"
109
+ className="absolute inset-0 pointer-events-none"
110
+ >
111
+ {/* Fallback background color */}
112
+ <div className="absolute inset-0 bg-gray-500" />
113
+ {/* Background media */}
114
+ {backgroundMedia}
115
+ </div>
116
+
117
+ {/* Inner grid for content alignment */}
118
+ <div
119
+ className={cn(
120
+ // Position above background
121
+ "relative z-10",
122
+ // Grid setup with responsive columns
123
+ "grid w-full h-full",
124
+ // Mobile: 4 columns with gap-20
125
+ "grid-cols-4 gap-spacing-20",
126
+ // Tablet (md): 12 columns
127
+ "md:grid-cols-12",
128
+ // Desktop (lg): 24 columns
129
+ "lg:grid-cols-24",
130
+ // Max width and centering like grid-container
131
+ "max-w-[var(--breakpoint-lg)] mx-auto",
132
+ // Responsive margins matching grid-container
133
+ "px-spacing-20 md:px-spacing-56 lg:px-spacing-72",
134
+ // Vertical padding to position content at bottom
135
+ "py-spacing-36 md:py-spacing-56 lg:py-spacing-72",
136
+ )}
137
+ >
138
+ {/* Content column - aligned to grid */}
139
+ <div
140
+ className={cn(
141
+ // Flex container for content
142
+ "flex flex-col",
143
+ isCentered ? "justify-start items-center" : "justify-end",
144
+ // Responsive gap between text and buttons
145
+ "gap-spacing-28 md:gap-spacing-36",
146
+ // Mobile: all 4 cols
147
+ "col-span-4",
148
+ // Tablet & Desktop: 9 cols left-aligned, full width centered
149
+ isCentered ? "md:col-span-12 lg:col-span-24" : "md:col-span-9",
150
+ )}
151
+ >
152
+ {/* Text content stack */}
153
+ <div
154
+ className={cn(
155
+ "flex flex-col gap-spacing-16",
156
+ isCentered && "items-center text-center",
157
+ )}
158
+ >
159
+ <h2 className="typography-headline-small text-gray-900">
160
+ {headline}
161
+ </h2>
162
+ {body && (
163
+ <p className="typography-body-small text-gray-800">{body}</p>
164
+ )}
165
+ </div>
166
+
167
+ {/* CTA buttons */}
168
+ <div
169
+ className={cn(
170
+ "flex flex-row",
171
+ isCentered ? "justify-center" : "items-start",
172
+ // Responsive gap between buttons
173
+ "gap-spacing-8 md:gap-spacing-12",
174
+ "[&>*]:flex-shrink-0",
175
+ )}
176
+ >
177
+ {primaryAction}
178
+ {secondaryAction}
179
+ </div>
180
+ </div>
181
+ </div>
182
+
183
+ {/* National Design Studio footer */}
184
+ {ndstudioFooter && (
185
+ <div className="absolute bottom-6 md:bottom-8 left-0 right-0 z-10 text-center">
186
+ <p className="typography-label-large uppercase text-ivory-alpha-75">
187
+ Designed and Engineered in DC by{" "}
188
+ <a
189
+ href="https://ndstudio.gov"
190
+ target="_blank"
191
+ rel="noopener noreferrer"
192
+ className="hover:underline"
193
+ >
194
+ National Design Studio
195
+ </a>
196
+ </p>
197
+ </div>
198
+ )}
199
+ </section>
200
+ );
201
+ },
202
+ );
203
+ Tout.displayName = "Tout";
204
+
205
+ export { Tout };
@@ -0,0 +1,5 @@
1
+ export {
2
+ TwoColumnSection,
3
+ type TwoColumnSectionProps,
4
+ twoColumnSectionVariants,
5
+ } from "./two-column-section";