@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,46 @@
1
+ import { describe, expect, test } from "vitest";
2
+ import { page } from "vitest/browser";
3
+ import { render } from "vitest-browser-react";
4
+ import { USGovBanner } from "./us-gov-banner";
5
+
6
+ describe("USGovBanner Visual Regression", () => {
7
+ test("default banner renders correctly", async () => {
8
+ await page.viewport(1280, 800);
9
+
10
+ render(<USGovBanner data-testid="banner" />);
11
+
12
+ await expect(page.getByTestId("banner")).toMatchScreenshot(
13
+ "us-gov-banner-default",
14
+ );
15
+ });
16
+
17
+ test("banner with custom text renders correctly", async () => {
18
+ await page.viewport(1280, 800);
19
+
20
+ render(
21
+ <USGovBanner
22
+ data-testid="banner"
23
+ text="An official website of the State of California"
24
+ />,
25
+ );
26
+
27
+ await expect(page.getByTestId("banner")).toMatchScreenshot(
28
+ "us-gov-banner-custom-text",
29
+ );
30
+ });
31
+
32
+ test("banner with custom flag icon renders correctly", async () => {
33
+ await page.viewport(1280, 800);
34
+
35
+ render(
36
+ <USGovBanner
37
+ data-testid="banner"
38
+ flagIcon={<span aria-hidden="true">🏛️</span>}
39
+ />,
40
+ );
41
+
42
+ await expect(page.getByTestId("banner")).toMatchScreenshot(
43
+ "us-gov-banner-custom-icon",
44
+ );
45
+ });
46
+ });
@@ -0,0 +1,150 @@
1
+ import type { Meta, StoryObj } from "@storybook/react-vite";
2
+ import { Button } from "../../atoms/button";
3
+ import { Banner } from ".";
4
+
5
+ const meta: Meta<typeof Banner> = {
6
+ title: "Sections/Banner",
7
+ component: Banner,
8
+ parameters: {
9
+ layout: "fullscreen",
10
+ },
11
+ argTypes: {
12
+ heading: {
13
+ control: "text",
14
+ description: "The heading text displayed in the banner",
15
+ },
16
+ description: {
17
+ control: "text",
18
+ description: "The description text displayed below the heading",
19
+ },
20
+ },
21
+ } as Meta<typeof Banner>;
22
+
23
+ export default meta;
24
+ type Story = StoryObj<typeof Banner>;
25
+
26
+ export const Playground: Story = {
27
+ render: (args) => <Banner {...args} />,
28
+ };
29
+ Playground.args = {
30
+ heading: "Important Update",
31
+ description:
32
+ "Use banners to surface short, important updates or a single key action without disrupting the main page content.",
33
+ action: <Button>Learn More</Button>,
34
+ };
35
+
36
+ // =============================================================================
37
+ // Default
38
+ // =============================================================================
39
+
40
+ /**
41
+ * Default banner with heading, description, and action button.
42
+ * Responsive across all breakpoints.
43
+ */
44
+ export const Default: Story = {
45
+ render: () => (
46
+ <Banner
47
+ heading="Important Update"
48
+ description="Use banners to surface short, important updates or a single key action without disrupting the main page content."
49
+ action={<Button>Learn More</Button>}
50
+ />
51
+ ),
52
+ };
53
+
54
+ /**
55
+ * Banner without an action button.
56
+ */
57
+ export const WithoutAction: Story = {
58
+ render: () => (
59
+ <Banner
60
+ heading="Announcement"
61
+ description="Use banners to surface short, important updates or a single key action without disrupting the main page content."
62
+ />
63
+ ),
64
+ };
65
+
66
+ // =============================================================================
67
+ // Responsive Variants
68
+ // =============================================================================
69
+
70
+ /**
71
+ * Desktop viewport (lg breakpoint, 1440px)
72
+ * Horizontal layout with 72px padding
73
+ */
74
+ export const Desktop: Story = {
75
+ render: () => (
76
+ <Banner
77
+ heading="Desktop Banner"
78
+ description="Use banners to surface short, important updates or a single key action without disrupting the main page content."
79
+ action={<Button>Learn More</Button>}
80
+ />
81
+ ),
82
+ globals: {
83
+ viewport: { value: "lg", isRotated: false },
84
+ },
85
+ };
86
+
87
+ /**
88
+ * Tablet viewport (md breakpoint, 768px)
89
+ * Horizontal layout with 56px padding
90
+ */
91
+ export const Tablet: Story = {
92
+ render: () => (
93
+ <Banner
94
+ heading="Tablet Banner"
95
+ description="Use banners to surface short, important updates or a single key action without disrupting the main page content."
96
+ action={<Button>Learn More</Button>}
97
+ />
98
+ ),
99
+ globals: {
100
+ viewport: { value: "md", isRotated: false },
101
+ },
102
+ };
103
+
104
+ /**
105
+ * Mobile viewport (sm breakpoint, 320px)
106
+ * Stacked layout with 20x32px padding
107
+ */
108
+ export const Mobile: Story = {
109
+ render: () => (
110
+ <Banner
111
+ heading="Mobile Banner"
112
+ description="Use banners to surface short, important updates or a single key action without disruption."
113
+ action={<Button size="sm">Learn More</Button>}
114
+ />
115
+ ),
116
+ globals: {
117
+ viewport: { value: "sm", isRotated: false },
118
+ },
119
+ };
120
+
121
+ // =============================================================================
122
+ // Examples
123
+ // =============================================================================
124
+
125
+ /**
126
+ * Banner with a longer description that wraps
127
+ */
128
+ export const LongDescription: Story = {
129
+ render: () => (
130
+ <Banner
131
+ heading="New Features Available"
132
+ description="We've made significant improvements to our platform including enhanced security features, faster load times, improved accessibility across all components, and a redesigned dashboard experience. Check out the documentation for more details on how to take advantage of these updates."
133
+ action={<Button>View Documentation</Button>}
134
+ />
135
+ ),
136
+ };
137
+
138
+ /**
139
+ * Banner with custom styling
140
+ */
141
+ export const CustomBackground: Story = {
142
+ render: () => (
143
+ <Banner
144
+ heading="Custom Styled Banner"
145
+ description="This banner has a custom background color applied via className."
146
+ action={<Button variant="ivory">Action</Button>}
147
+ className="bg-gray-1000 text-gray-50 [&_h2]:text-gray-50 [&_p]:text-gray-200"
148
+ />
149
+ ),
150
+ };
@@ -0,0 +1,185 @@
1
+ import { describe, expect, test } from "vitest";
2
+ import { page } from "vitest/browser";
3
+ import { render } from "vitest-browser-react";
4
+ import { Banner } from "./banner";
5
+
6
+ describe("Banner", () => {
7
+ describe("Accessibility", () => {
8
+ test("renders as section landmark", async () => {
9
+ render(
10
+ <Banner
11
+ heading="Test Banner"
12
+ description="Test description"
13
+ data-testid="banner"
14
+ />,
15
+ );
16
+
17
+ const banner = page.getByTestId("banner");
18
+ await expect.element(banner).toBeInTheDocument();
19
+ });
20
+
21
+ test("heading renders as h2", async () => {
22
+ render(
23
+ <Banner heading="Banner Heading" description="Test description" />,
24
+ );
25
+
26
+ await expect
27
+ .element(
28
+ page.getByRole("heading", { level: 2, name: "Banner Heading" }),
29
+ )
30
+ .toBeInTheDocument();
31
+ });
32
+
33
+ test("description is accessible to screen readers", async () => {
34
+ render(
35
+ <Banner heading="Test" description="Accessible description text" />,
36
+ );
37
+
38
+ await expect
39
+ .element(page.getByText("Accessible description text"))
40
+ .toBeInTheDocument();
41
+ });
42
+ });
43
+
44
+ describe("Props", () => {
45
+ test("renders with required heading and description props", async () => {
46
+ render(
47
+ <Banner
48
+ heading="Required Heading"
49
+ description="Required description"
50
+ />,
51
+ );
52
+
53
+ await expect
54
+ .element(page.getByText("Required Heading"))
55
+ .toBeInTheDocument();
56
+ await expect
57
+ .element(page.getByText("Required description"))
58
+ .toBeInTheDocument();
59
+ });
60
+
61
+ test("renders action when provided", async () => {
62
+ render(
63
+ <Banner
64
+ heading="With Action"
65
+ description="Description"
66
+ action={<button type="button">Click Me</button>}
67
+ />,
68
+ );
69
+
70
+ await expect
71
+ .element(page.getByRole("button", { name: "Click Me" }))
72
+ .toBeInTheDocument();
73
+ });
74
+
75
+ test("does not render action slot when not provided", async () => {
76
+ render(
77
+ <Banner
78
+ heading="Without Action"
79
+ description="Description"
80
+ data-testid="banner"
81
+ />,
82
+ );
83
+
84
+ const banner = page.getByTestId("banner");
85
+ await expect.element(banner).toBeInTheDocument();
86
+ // Only heading and description should be present
87
+ await expect.element(page.getByRole("button")).not.toBeInTheDocument();
88
+ });
89
+
90
+ test("supports custom className", async () => {
91
+ render(
92
+ <Banner
93
+ heading="Custom"
94
+ description="Description"
95
+ className="custom-class"
96
+ data-testid="banner"
97
+ />,
98
+ );
99
+
100
+ const banner = page.getByTestId("banner");
101
+ await expect.element(banner).toHaveClass(/custom-class/);
102
+ });
103
+
104
+ test("spreads additional props to section element", async () => {
105
+ render(
106
+ <Banner
107
+ heading="Props Test"
108
+ description="Description"
109
+ data-testid="banner"
110
+ aria-label="Banner section"
111
+ />,
112
+ );
113
+
114
+ const banner = page.getByTestId("banner");
115
+ await expect
116
+ .element(banner)
117
+ .toHaveAttribute("aria-label", "Banner section");
118
+ });
119
+ });
120
+
121
+ describe("Styling", () => {
122
+ test("applies default background color", async () => {
123
+ render(
124
+ <Banner
125
+ heading="Default"
126
+ description="Description"
127
+ data-testid="banner"
128
+ />,
129
+ );
130
+
131
+ const banner = page.getByTestId("banner");
132
+ await expect.element(banner).toHaveClass(/bg-gray-50/);
133
+ });
134
+
135
+ test("applies col-full for grid alignment", async () => {
136
+ render(
137
+ <Banner
138
+ heading="Grid Test"
139
+ description="Description"
140
+ data-testid="banner"
141
+ />,
142
+ );
143
+
144
+ const banner = page.getByTestId("banner");
145
+ await expect.element(banner).toHaveClass(/col-full/);
146
+ });
147
+
148
+ test("applies responsive padding classes", async () => {
149
+ render(
150
+ <Banner
151
+ heading="Responsive"
152
+ description="Description"
153
+ data-testid="banner"
154
+ />,
155
+ );
156
+
157
+ const banner = page.getByTestId("banner");
158
+ // Mobile padding
159
+ await expect.element(banner).toHaveClass(/px-spacing-20/);
160
+ await expect.element(banner).toHaveClass(/py-spacing-32/);
161
+ });
162
+ });
163
+
164
+ describe("Content", () => {
165
+ test("heading has correct text color", async () => {
166
+ render(<Banner heading="Styled Heading" description="Description" />);
167
+
168
+ const heading = page.getByRole("heading", { level: 2 });
169
+ await expect.element(heading).toHaveClass(/text-gray-900/);
170
+ });
171
+
172
+ test("description has correct text color", async () => {
173
+ render(
174
+ <Banner
175
+ heading="Test"
176
+ description="Styled description"
177
+ data-testid="banner"
178
+ />,
179
+ );
180
+
181
+ const description = page.getByText("Styled description");
182
+ await expect.element(description).toHaveClass(/text-gray-800/);
183
+ });
184
+ });
185
+ });
@@ -0,0 +1,132 @@
1
+ import { cva, type VariantProps } from "class-variance-authority";
2
+ import * as React from "react";
3
+ import { cn } from "@/lib/utils";
4
+
5
+ /**
6
+ * Banner component based on Figma BaseKit / Banners
7
+ *
8
+ * Used to surface short, important updates or a single key action
9
+ * without disrupting the main page content.
10
+ *
11
+ * Responsive behavior using 24-column grid:
12
+ * - Mobile (sm): Stacked layout, 20px horizontal / 32px vertical padding
13
+ * - Tablet (md): Horizontal layout, 56px padding
14
+ * - Desktop (lg): Horizontal layout, 72px padding
15
+ *
16
+ * Must be placed inside a `grid-container`. Uses `col-full` to span all columns.
17
+ */
18
+ const bannerVariants = cva(
19
+ [
20
+ // Grid alignment - full width
21
+ "col-full",
22
+ // Responsive padding: mobile -> tablet -> desktop
23
+ "px-spacing-20 py-spacing-32",
24
+ "md:p-spacing-56",
25
+ "lg:px-spacing-72 lg:pb-spacing-72 lg:pt-0",
26
+ ],
27
+ {
28
+ variants: {
29
+ theme: {
30
+ light: "bg-gray-50",
31
+ dark: "bg-gray-1200",
32
+ },
33
+ },
34
+ defaultVariants: {
35
+ theme: "light",
36
+ },
37
+ },
38
+ );
39
+
40
+ export interface BannerProps
41
+ extends React.HTMLAttributes<HTMLElement>,
42
+ VariantProps<typeof bannerVariants> {
43
+ /**
44
+ * The heading text displayed in the banner
45
+ */
46
+ heading: string;
47
+ /**
48
+ * The description text displayed below the heading
49
+ */
50
+ description: string;
51
+ /**
52
+ * Optional action element (typically a Button component)
53
+ */
54
+ action?: React.ReactNode;
55
+ }
56
+
57
+ /**
58
+ * Banner component for surfacing important updates or CTAs.
59
+ *
60
+ * Uses the 24-column grid system - must be placed inside a `grid-container`.
61
+ * Spans full width with `col-full`.
62
+ *
63
+ * Responsive across breakpoints:
64
+ * - Mobile: Stacked layout with smaller padding
65
+ * - Tablet: Horizontal layout with medium padding
66
+ * - Desktop: Horizontal layout with larger padding
67
+ *
68
+ * @example
69
+ * ```tsx
70
+ * <div className="grid-container">
71
+ * <Banner
72
+ * heading="Important Update"
73
+ * description="Check out our new features."
74
+ * action={<Button>Learn More</Button>}
75
+ * />
76
+ * </div>
77
+ *
78
+ * // Dark theme
79
+ * <Banner
80
+ * theme="dark"
81
+ * heading="Still Have Questions?"
82
+ * description="Contact us at support@example.com"
83
+ * action={<Button variant="secondary">Email Us</Button>}
84
+ * />
85
+ * ```
86
+ */
87
+ const Banner = React.forwardRef<HTMLElement, BannerProps>(
88
+ ({ className, theme, heading, description, action, ...props }, ref) => {
89
+ const isDark = theme === "dark";
90
+
91
+ return (
92
+ <section
93
+ ref={ref}
94
+ className={cn(bannerVariants({ theme }), className)}
95
+ {...props}
96
+ >
97
+ {/* Inner container with border-top for dark theme */}
98
+ <div
99
+ className={cn(
100
+ "flex flex-col md:flex-row gap-spacing-20 items-start md:items-center md:justify-between",
101
+ isDark && "border-t border-gray-700 py-spacing-36",
102
+ )}
103
+ >
104
+ {/* Copy section */}
105
+ <div className="flex flex-col gap-spacing-6 items-start">
106
+ <h2
107
+ className={cn(
108
+ "typography-subheading-small",
109
+ isDark ? "text-gray-100" : "text-gray-900",
110
+ )}
111
+ >
112
+ {heading}
113
+ </h2>
114
+ <p
115
+ className={cn(
116
+ "typography-body-small",
117
+ isDark ? "text-gray-500" : "text-gray-800",
118
+ )}
119
+ >
120
+ {description}
121
+ </p>
122
+ </div>
123
+ {/* Action slot */}
124
+ {action}
125
+ </div>
126
+ </section>
127
+ );
128
+ },
129
+ );
130
+ Banner.displayName = "Banner";
131
+
132
+ export { Banner, bannerVariants };
@@ -0,0 +1,2 @@
1
+ export type { BannerProps } from "./banner";
2
+ export { Banner, bannerVariants } from "./banner";