@nationaldesignstudio/react 0.0.14 → 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 (166) hide show
  1. package/dist/tailwind.css +15 -1
  2. package/dist/tokens.css +45 -60
  3. package/package.json +5 -10
  4. package/src/App.css +0 -0
  5. package/src/App.tsx +7 -0
  6. package/src/assets/fonts/PPNeueMontreal-Variable.woff2 +0 -0
  7. package/src/assets/react.svg +1 -0
  8. package/src/components/atoms/accordion/accordion.stories.tsx +228 -0
  9. package/src/components/atoms/accordion/accordion.tsx +219 -0
  10. package/src/components/atoms/accordion/index.ts +6 -0
  11. package/src/components/atoms/button/__screenshots__/button.visual.test.tsx/button-charcoal-chromium-darwin.png +0 -0
  12. package/src/components/atoms/button/__screenshots__/button.visual.test.tsx/button-charcoal-chromium-linux.png +0 -0
  13. package/src/components/atoms/button/__screenshots__/button.visual.test.tsx/button-charcoal-outline-chromium-darwin.png +0 -0
  14. package/src/components/atoms/button/__screenshots__/button.visual.test.tsx/button-charcoal-outline-chromium-linux.png +0 -0
  15. package/src/components/atoms/button/__screenshots__/button.visual.test.tsx/button-charcoal-outline-quiet-chromium-darwin.png +0 -0
  16. package/src/components/atoms/button/__screenshots__/button.visual.test.tsx/button-charcoal-outline-quiet-chromium-linux.png +0 -0
  17. package/src/components/atoms/button/__screenshots__/button.visual.test.tsx/button-disabled-chromium-darwin.png +0 -0
  18. package/src/components/atoms/button/__screenshots__/button.visual.test.tsx/button-disabled-chromium-linux.png +0 -0
  19. package/src/components/atoms/button/__screenshots__/button.visual.test.tsx/button-ivory-chromium-darwin.png +0 -0
  20. package/src/components/atoms/button/__screenshots__/button.visual.test.tsx/button-ivory-chromium-linux.png +0 -0
  21. package/src/components/atoms/button/__screenshots__/button.visual.test.tsx/button-ivory-outline-chromium-darwin.png +0 -0
  22. package/src/components/atoms/button/__screenshots__/button.visual.test.tsx/button-ivory-outline-chromium-linux.png +0 -0
  23. package/src/components/atoms/button/__screenshots__/button.visual.test.tsx/button-ivory-outline-quiet-chromium-darwin.png +0 -0
  24. package/src/components/atoms/button/__screenshots__/button.visual.test.tsx/button-ivory-outline-quiet-chromium-linux.png +0 -0
  25. package/src/components/atoms/button/__screenshots__/button.visual.test.tsx/button-size-large-chromium-darwin.png +0 -0
  26. package/src/components/atoms/button/__screenshots__/button.visual.test.tsx/button-size-large-chromium-linux.png +0 -0
  27. package/src/components/atoms/button/__screenshots__/button.visual.test.tsx/button-size-medium-chromium-darwin.png +0 -0
  28. package/src/components/atoms/button/__screenshots__/button.visual.test.tsx/button-size-medium-chromium-linux.png +0 -0
  29. package/src/components/atoms/button/__screenshots__/button.visual.test.tsx/button-size-small-chromium-darwin.png +0 -0
  30. package/src/components/atoms/button/__screenshots__/button.visual.test.tsx/button-size-small-chromium-linux.png +0 -0
  31. package/src/components/atoms/button/button.stories.tsx +102 -0
  32. package/src/components/atoms/button/button.test.tsx +135 -0
  33. package/src/components/atoms/button/button.tsx +139 -0
  34. package/src/components/atoms/button/button.visual.test.tsx +102 -0
  35. package/src/components/atoms/button/icon-button.stories.tsx +166 -0
  36. package/src/components/atoms/button/icon-button.tsx +120 -0
  37. package/src/components/atoms/button/index.ts +6 -0
  38. package/src/components/atoms/ndstudio-footer/index.ts +1 -0
  39. package/src/components/atoms/ndstudio-footer/ndstudio-footer.tsx +55 -0
  40. package/src/components/atoms/pager-control/index.ts +5 -0
  41. package/src/components/atoms/pager-control/pager-control.stories.tsx +209 -0
  42. package/src/components/atoms/pager-control/pager-control.test.tsx +130 -0
  43. package/src/components/atoms/pager-control/pager-control.tsx +329 -0
  44. package/src/components/dev-tools/dev-toolbar/dev-toolbar.stories.tsx +82 -0
  45. package/src/components/dev-tools/dev-toolbar/dev-toolbar.tsx +196 -0
  46. package/src/components/dev-tools/dev-toolbar/index.ts +1 -0
  47. package/src/components/dev-tools/grid-overlay/grid-overlay.tsx +41 -0
  48. package/src/components/dev-tools/grid-overlay/index.ts +1 -0
  49. package/src/components/dev-tools/index.ts +2 -0
  50. package/src/components/organisms/card/__screenshots__/card.visual.test.tsx/card-default-vertical-chromium-darwin.png +0 -0
  51. package/src/components/organisms/card/__screenshots__/card.visual.test.tsx/card-default-vertical-chromium-linux.png +0 -0
  52. package/src/components/organisms/card/__screenshots__/card.visual.test.tsx/card-horizontal-chromium-darwin.png +0 -0
  53. package/src/components/organisms/card/__screenshots__/card.visual.test.tsx/card-horizontal-chromium-linux.png +0 -0
  54. package/src/components/organisms/card/__screenshots__/card.visual.test.tsx/card-minimal-chromium-darwin.png +0 -0
  55. package/src/components/organisms/card/__screenshots__/card.visual.test.tsx/card-minimal-chromium-linux.png +0 -0
  56. package/src/components/organisms/card/__screenshots__/card.visual.test.tsx/card-without-actions-chromium-darwin.png +0 -0
  57. package/src/components/organisms/card/__screenshots__/card.visual.test.tsx/card-without-actions-chromium-linux.png +0 -0
  58. package/src/components/organisms/card/__screenshots__/card.visual.test.tsx/card-without-eyebrow-chromium-darwin.png +0 -0
  59. package/src/components/organisms/card/__screenshots__/card.visual.test.tsx/card-without-eyebrow-chromium-linux.png +0 -0
  60. package/src/components/organisms/card/__screenshots__/card.visual.test.tsx/card-without-image-chromium-darwin.png +0 -0
  61. package/src/components/organisms/card/__screenshots__/card.visual.test.tsx/card-without-image-chromium-linux.png +0 -0
  62. package/src/components/organisms/card/card.stories.tsx +293 -0
  63. package/src/components/organisms/card/card.test.tsx +245 -0
  64. package/src/components/organisms/card/card.tsx +225 -0
  65. package/src/components/organisms/card/card.visual.test.tsx +197 -0
  66. package/src/components/organisms/card/index.ts +19 -0
  67. package/src/components/organisms/navbar/__screenshots__/navbar.visual.test.tsx/navbar-active-link-chromium-darwin.png +0 -0
  68. package/src/components/organisms/navbar/__screenshots__/navbar.visual.test.tsx/navbar-active-link-chromium-linux.png +0 -0
  69. package/src/components/organisms/navbar/__screenshots__/navbar.visual.test.tsx/navbar-brand-only-chromium-darwin.png +0 -0
  70. package/src/components/organisms/navbar/__screenshots__/navbar.visual.test.tsx/navbar-brand-only-chromium-linux.png +0 -0
  71. package/src/components/organisms/navbar/__screenshots__/navbar.visual.test.tsx/navbar-default-chromium-darwin.png +0 -0
  72. package/src/components/organisms/navbar/__screenshots__/navbar.visual.test.tsx/navbar-default-chromium-linux.png +0 -0
  73. package/src/components/organisms/navbar/index.ts +18 -0
  74. package/src/components/organisms/navbar/navbar.stories.tsx +313 -0
  75. package/src/components/organisms/navbar/navbar.test.tsx +190 -0
  76. package/src/components/organisms/navbar/navbar.tsx +323 -0
  77. package/src/components/organisms/navbar/navbar.visual.test.tsx +85 -0
  78. package/src/components/organisms/us-gov-banner/__screenshots__/us-gov-banner.visual.test.tsx/us-gov-banner-custom-icon-chromium-darwin.png +0 -0
  79. package/src/components/organisms/us-gov-banner/__screenshots__/us-gov-banner.visual.test.tsx/us-gov-banner-custom-icon-chromium-linux.png +0 -0
  80. package/src/components/organisms/us-gov-banner/__screenshots__/us-gov-banner.visual.test.tsx/us-gov-banner-custom-text-chromium-darwin.png +0 -0
  81. package/src/components/organisms/us-gov-banner/__screenshots__/us-gov-banner.visual.test.tsx/us-gov-banner-custom-text-chromium-linux.png +0 -0
  82. package/src/components/organisms/us-gov-banner/__screenshots__/us-gov-banner.visual.test.tsx/us-gov-banner-default-chromium-darwin.png +0 -0
  83. package/src/components/organisms/us-gov-banner/__screenshots__/us-gov-banner.visual.test.tsx/us-gov-banner-default-chromium-linux.png +0 -0
  84. package/src/components/organisms/us-gov-banner/index.ts +1 -0
  85. package/src/components/organisms/us-gov-banner/us-gov-banner.stories.tsx +35 -0
  86. package/src/components/organisms/us-gov-banner/us-gov-banner.test.tsx +107 -0
  87. package/src/components/organisms/us-gov-banner/us-gov-banner.tsx +73 -0
  88. package/src/components/organisms/us-gov-banner/us-gov-banner.visual.test.tsx +46 -0
  89. package/src/components/sections/banner/banner.stories.tsx +150 -0
  90. package/src/components/sections/banner/banner.test.tsx +185 -0
  91. package/src/components/sections/banner/banner.tsx +130 -0
  92. package/src/components/sections/banner/index.ts +2 -0
  93. package/src/components/sections/card-grid/card-grid.stories.tsx +351 -0
  94. package/src/components/sections/card-grid/card-grid.tsx +116 -0
  95. package/src/components/sections/card-grid/index.ts +1 -0
  96. package/src/components/sections/faq-section/faq-section.stories.tsx +453 -0
  97. package/src/components/sections/faq-section/faq-section.tsx +84 -0
  98. package/src/components/sections/faq-section/index.ts +2 -0
  99. package/src/components/sections/hero/__screenshots__/hero.visual.test.tsx/hero-a1-desktop-chromium-darwin.png +0 -0
  100. package/src/components/sections/hero/__screenshots__/hero.visual.test.tsx/hero-a1-desktop-chromium-linux.png +0 -0
  101. package/src/components/sections/hero/__screenshots__/hero.visual.test.tsx/hero-a1-mobile-chromium-darwin.png +0 -0
  102. package/src/components/sections/hero/__screenshots__/hero.visual.test.tsx/hero-a1-mobile-chromium-linux.png +0 -0
  103. package/src/components/sections/hero/__screenshots__/hero.visual.test.tsx/hero-a1-tablet-chromium-darwin.png +0 -0
  104. package/src/components/sections/hero/__screenshots__/hero.visual.test.tsx/hero-a1-tablet-chromium-linux.png +0 -0
  105. package/src/components/sections/hero/__screenshots__/hero.visual.test.tsx/hero-a2-desktop-chromium-darwin.png +0 -0
  106. package/src/components/sections/hero/__screenshots__/hero.visual.test.tsx/hero-a2-desktop-chromium-linux.png +0 -0
  107. package/src/components/sections/hero/__screenshots__/hero.visual.test.tsx/hero-a2-mobile-chromium-darwin.png +0 -0
  108. package/src/components/sections/hero/__screenshots__/hero.visual.test.tsx/hero-a2-mobile-chromium-linux.png +0 -0
  109. package/src/components/sections/hero/__screenshots__/hero.visual.test.tsx/hero-a2-tablet-chromium-darwin.png +0 -0
  110. package/src/components/sections/hero/__screenshots__/hero.visual.test.tsx/hero-a2-tablet-chromium-linux.png +0 -0
  111. package/src/components/sections/hero/__screenshots__/hero.visual.test.tsx/hero-a3-desktop-chromium-darwin.png +0 -0
  112. package/src/components/sections/hero/__screenshots__/hero.visual.test.tsx/hero-a3-desktop-chromium-linux.png +0 -0
  113. package/src/components/sections/hero/__screenshots__/hero.visual.test.tsx/hero-a3-mobile-chromium-darwin.png +0 -0
  114. package/src/components/sections/hero/__screenshots__/hero.visual.test.tsx/hero-a3-mobile-chromium-linux.png +0 -0
  115. package/src/components/sections/hero/__screenshots__/hero.visual.test.tsx/hero-a3-tablet-chromium-darwin.png +0 -0
  116. package/src/components/sections/hero/__screenshots__/hero.visual.test.tsx/hero-a3-tablet-chromium-linux.png +0 -0
  117. package/src/components/sections/hero/__screenshots__/hero.visual.test.tsx/hero-custom-class-chromium-darwin.png +0 -0
  118. package/src/components/sections/hero/__screenshots__/hero.visual.test.tsx/hero-custom-class-chromium-linux.png +0 -0
  119. package/src/components/sections/hero/__screenshots__/hero.visual.test.tsx/hero-default-chromium-linux.png +0 -0
  120. package/src/components/sections/hero/__screenshots__/hero.visual.test.tsx/hero-long-title-chromium-darwin.png +0 -0
  121. package/src/components/sections/hero/__screenshots__/hero.visual.test.tsx/hero-long-title-chromium-linux.png +0 -0
  122. package/src/components/sections/hero/hero.stories.tsx +274 -0
  123. package/src/components/sections/hero/hero.test.tsx +135 -0
  124. package/src/components/sections/hero/hero.tsx +453 -0
  125. package/src/components/sections/hero/hero.visual.test.tsx +140 -0
  126. package/src/components/sections/hero/index.ts +10 -0
  127. package/src/components/sections/prose/__screenshots__/prose.visual.test.tsx/prose-h3-heading-chromium-darwin.png +0 -0
  128. package/src/components/sections/prose/__screenshots__/prose.visual.test.tsx/prose-h3-heading-chromium-linux.png +0 -0
  129. package/src/components/sections/prose/__screenshots__/prose.visual.test.tsx/prose-multiple-paragraphs-chromium-darwin.png +0 -0
  130. package/src/components/sections/prose/__screenshots__/prose.visual.test.tsx/prose-multiple-paragraphs-chromium-linux.png +0 -0
  131. package/src/components/sections/prose/__screenshots__/prose.visual.test.tsx/prose-multiple-sections-chromium-darwin.png +0 -0
  132. package/src/components/sections/prose/__screenshots__/prose.visual.test.tsx/prose-multiple-sections-chromium-linux.png +0 -0
  133. package/src/components/sections/prose/__screenshots__/prose.visual.test.tsx/prose-single-section-chromium-darwin.png +0 -0
  134. package/src/components/sections/prose/__screenshots__/prose.visual.test.tsx/prose-single-section-chromium-linux.png +0 -0
  135. package/src/components/sections/prose/index.ts +6 -0
  136. package/src/components/sections/prose/prose.stories.tsx +144 -0
  137. package/src/components/sections/prose/prose.test.tsx +178 -0
  138. package/src/components/sections/prose/prose.tsx +88 -0
  139. package/src/components/sections/prose/prose.visual.test.tsx +105 -0
  140. package/src/components/sections/river/index.ts +1 -0
  141. package/src/components/sections/river/river.stories.tsx +237 -0
  142. package/src/components/sections/river/river.test.tsx +268 -0
  143. package/src/components/sections/river/river.tsx +173 -0
  144. package/src/components/sections/tout/index.ts +1 -0
  145. package/src/components/sections/tout/tout.stories.tsx +171 -0
  146. package/src/components/sections/tout/tout.test.tsx +242 -0
  147. package/src/components/sections/tout/tout.tsx +270 -0
  148. package/src/components/sections/two-column-section/index.ts +5 -0
  149. package/src/components/sections/two-column-section/two-column-section.stories.tsx +285 -0
  150. package/src/components/sections/two-column-section/two-column-section.tsx +162 -0
  151. package/src/hooks/index.ts +1 -0
  152. package/src/hooks/use-event-listener.ts +73 -0
  153. package/src/index.ts +155 -0
  154. package/src/lib/theme.ts +1000 -0
  155. package/src/lib/utils.ts +6 -0
  156. package/src/main.tsx +13 -0
  157. package/src/stories/GridSystem.stories.tsx +84 -0
  158. package/src/stories/Introduction.mdx +114 -0
  159. package/src/stories/ThemeProvider.stories.tsx +357 -0
  160. package/src/stories/TokenShowcase.stories.tsx +92 -0
  161. package/src/stories/TokenShowcase.tsx +1429 -0
  162. package/src/styles.css +11 -0
  163. package/src/theme/ThemeProvider.tsx +297 -0
  164. package/src/theme/hooks.ts +40 -0
  165. package/src/theme/index.ts +43 -0
  166. package/src/theme/utils.ts +104 -0
@@ -0,0 +1,102 @@
1
+ import { render } from "@testing-library/react";
2
+ import { describe, expect, test } from "vitest";
3
+ import { page } from "vitest/browser";
4
+ import { Button } from "./button";
5
+
6
+ describe("Button Visual Regression", () => {
7
+ test("charcoal variant renders correctly", async () => {
8
+ render(<Button variant="charcoal">Charcoal Button</Button>);
9
+
10
+ await expect(
11
+ page.getByRole("button", { name: "Charcoal Button" }),
12
+ ).toMatchScreenshot("button-charcoal");
13
+ });
14
+
15
+ test("charcoalOutline variant renders correctly", async () => {
16
+ render(<Button variant="charcoalOutline">Charcoal Outline Button</Button>);
17
+
18
+ await expect(
19
+ page.getByRole("button", { name: "Charcoal Outline Button" }),
20
+ ).toMatchScreenshot("button-charcoal-outline");
21
+ });
22
+
23
+ test("charcoalOutlineQuiet variant renders correctly", async () => {
24
+ render(
25
+ <Button variant="charcoalOutlineQuiet">Charcoal Outline Quiet</Button>,
26
+ );
27
+
28
+ await expect(
29
+ page.getByRole("button", { name: "Charcoal Outline Quiet" }),
30
+ ).toMatchScreenshot("button-charcoal-outline-quiet");
31
+ });
32
+
33
+ test("ivory variant renders correctly", async () => {
34
+ render(
35
+ <div style={{ background: "#1a1a1a", padding: "20px" }}>
36
+ <Button variant="ivory">Ivory Button</Button>
37
+ </div>,
38
+ );
39
+
40
+ await expect(
41
+ page.getByRole("button", { name: "Ivory Button" }),
42
+ ).toMatchScreenshot("button-ivory");
43
+ });
44
+
45
+ test("ivoryOutline variant renders correctly", async () => {
46
+ render(
47
+ <div style={{ background: "#1a1a1a", padding: "20px" }}>
48
+ <Button variant="ivoryOutline">Ivory Outline Button</Button>
49
+ </div>,
50
+ );
51
+
52
+ await expect(
53
+ page.getByRole("button", { name: "Ivory Outline Button" }),
54
+ ).toMatchScreenshot("button-ivory-outline");
55
+ });
56
+
57
+ test("ivoryOutlineQuiet variant renders correctly", async () => {
58
+ render(
59
+ <div style={{ background: "#1a1a1a", padding: "20px" }}>
60
+ <Button variant="ivoryOutlineQuiet">Ivory Outline Quiet</Button>
61
+ </div>,
62
+ );
63
+
64
+ await expect(
65
+ page.getByRole("button", { name: "Ivory Outline Quiet" }),
66
+ ).toMatchScreenshot("button-ivory-outline-quiet");
67
+ });
68
+
69
+ // Size variants
70
+ test("small size renders correctly", async () => {
71
+ render(<Button size="sm">Small Button</Button>);
72
+
73
+ await expect(
74
+ page.getByRole("button", { name: "Small Button" }),
75
+ ).toMatchScreenshot("button-size-small");
76
+ });
77
+
78
+ test("medium (default) size renders correctly", async () => {
79
+ render(<Button size="default">Medium Button</Button>);
80
+
81
+ await expect(
82
+ page.getByRole("button", { name: "Medium Button" }),
83
+ ).toMatchScreenshot("button-size-medium");
84
+ });
85
+
86
+ test("large size renders correctly", async () => {
87
+ render(<Button size="lg">Large Button</Button>);
88
+
89
+ await expect(
90
+ page.getByRole("button", { name: "Large Button" }),
91
+ ).toMatchScreenshot("button-size-large");
92
+ });
93
+
94
+ // Disabled state
95
+ test("disabled state renders correctly", async () => {
96
+ render(<Button disabled>Disabled Button</Button>);
97
+
98
+ await expect(
99
+ page.getByRole("button", { name: "Disabled Button" }),
100
+ ).toMatchScreenshot("button-disabled");
101
+ });
102
+ });
@@ -0,0 +1,166 @@
1
+ import type { Meta, StoryObj } from "@storybook/react-vite";
2
+ import { IconButton } from ".";
3
+
4
+ // Simple placeholder icons for stories
5
+ const SearchIcon = () => (
6
+ <svg
7
+ width="16"
8
+ height="16"
9
+ viewBox="0 0 16 16"
10
+ fill="none"
11
+ xmlns="http://www.w3.org/2000/svg"
12
+ aria-hidden="true"
13
+ >
14
+ <path
15
+ d="M7.33333 12.6667C10.2789 12.6667 12.6667 10.2789 12.6667 7.33333C12.6667 4.38781 10.2789 2 7.33333 2C4.38781 2 2 4.38781 2 7.33333C2 10.2789 4.38781 12.6667 7.33333 12.6667Z"
16
+ stroke="currentColor"
17
+ strokeWidth="1.5"
18
+ strokeLinecap="round"
19
+ strokeLinejoin="round"
20
+ />
21
+ <path
22
+ d="M14 14L11.1 11.1"
23
+ stroke="currentColor"
24
+ strokeWidth="1.5"
25
+ strokeLinecap="round"
26
+ strokeLinejoin="round"
27
+ />
28
+ </svg>
29
+ );
30
+
31
+ const ArrowRightIcon = () => (
32
+ <svg
33
+ width="12"
34
+ height="12"
35
+ viewBox="0 0 12 12"
36
+ fill="none"
37
+ xmlns="http://www.w3.org/2000/svg"
38
+ aria-hidden="true"
39
+ >
40
+ <path
41
+ d="M2.5 6H9.5M9.5 6L6 2.5M9.5 6L6 9.5"
42
+ stroke="currentColor"
43
+ strokeWidth="1.5"
44
+ strokeLinecap="round"
45
+ strokeLinejoin="round"
46
+ />
47
+ </svg>
48
+ );
49
+
50
+ const meta: Meta<typeof IconButton> = {
51
+ title: "Atoms/IconButton",
52
+ } as Meta<typeof IconButton>;
53
+
54
+ export default meta;
55
+ type Story = StoryObj<typeof IconButton>;
56
+
57
+ export const Playground: Story = {
58
+ render: (args) => (
59
+ <IconButton {...args}>
60
+ <SearchIcon />
61
+ </IconButton>
62
+ ),
63
+ };
64
+ Playground.argTypes = {
65
+ size: {
66
+ control: {
67
+ type: "radio",
68
+ },
69
+ options: ["sm", "default", "lg"],
70
+ },
71
+ disabled: {
72
+ control: {
73
+ type: "boolean",
74
+ },
75
+ },
76
+ variant: {
77
+ control: {
78
+ type: "radio",
79
+ },
80
+ options: [
81
+ "charcoal",
82
+ "charcoalOutline",
83
+ "charcoalOutlineQuiet",
84
+ "ivory",
85
+ "ivoryOutline",
86
+ "ivoryOutlineQuiet",
87
+ ],
88
+ },
89
+ };
90
+ Playground.args = {
91
+ size: "default",
92
+ disabled: false,
93
+ variant: "charcoal",
94
+ };
95
+
96
+ // =============================================================================
97
+ // Variants
98
+ // =============================================================================
99
+
100
+ export const Charcoal = () => (
101
+ <IconButton variant="charcoal">
102
+ <SearchIcon />
103
+ </IconButton>
104
+ );
105
+
106
+ export const CharcoalOutline = () => (
107
+ <IconButton variant="charcoalOutline">
108
+ <SearchIcon />
109
+ </IconButton>
110
+ );
111
+
112
+ export const CharcoalOutlineQuiet = () => (
113
+ <IconButton variant="charcoalOutlineQuiet">
114
+ <SearchIcon />
115
+ </IconButton>
116
+ );
117
+
118
+ export const Ivory = () => (
119
+ <IconButton variant="ivory">
120
+ <SearchIcon />
121
+ </IconButton>
122
+ );
123
+
124
+ export const IvoryOutline = () => (
125
+ <IconButton variant="ivoryOutline">
126
+ <ArrowRightIcon />
127
+ </IconButton>
128
+ );
129
+
130
+ export const IvoryOutlineQuiet = () => (
131
+ <IconButton variant="ivoryOutlineQuiet">
132
+ <ArrowRightIcon />
133
+ </IconButton>
134
+ );
135
+
136
+ // =============================================================================
137
+ // Sizes
138
+ // =============================================================================
139
+
140
+ export const Small = () => (
141
+ <IconButton size="sm">
142
+ <ArrowRightIcon />
143
+ </IconButton>
144
+ );
145
+
146
+ export const Medium = () => (
147
+ <IconButton size="default">
148
+ <SearchIcon />
149
+ </IconButton>
150
+ );
151
+
152
+ export const Large = () => (
153
+ <IconButton size="lg">
154
+ <SearchIcon />
155
+ </IconButton>
156
+ );
157
+
158
+ // =============================================================================
159
+ // States
160
+ // =============================================================================
161
+
162
+ export const Disabled = () => (
163
+ <IconButton disabled>
164
+ <SearchIcon />
165
+ </IconButton>
166
+ );
@@ -0,0 +1,120 @@
1
+ import { Slot } from "@radix-ui/react-slot";
2
+ import * as React from "react";
3
+ import { tv, type VariantProps } from "tailwind-variants";
4
+
5
+ /**
6
+ * IconButton component based on Figma BaseKit / Interface / Icon Button
7
+ *
8
+ * **IMPORTANT: Accessibility Requirement**
9
+ * Icon-only buttons MUST have an accessible label. Provide one of:
10
+ * - `aria-label`: A text description of the button's action (recommended)
11
+ * - `aria-labelledby`: Reference to an element containing the label
12
+ * - `title`: Tooltip text (less preferred, but provides a label)
13
+ *
14
+ * @example
15
+ * ```tsx
16
+ * // Correct usage with aria-label
17
+ * <IconButton aria-label="Close menu">
18
+ * <CloseIcon />
19
+ * </IconButton>
20
+ *
21
+ * // Correct usage with aria-labelledby
22
+ * <IconButton aria-labelledby="close-label">
23
+ * <CloseIcon />
24
+ * </IconButton>
25
+ * <span id="close-label" className="sr-only">Close menu</span>
26
+ * ```
27
+ *
28
+ * Variants:
29
+ * - charcoal: Dark filled button (for light backgrounds)
30
+ * - charcoalOutline: Dark outlined button (for light backgrounds)
31
+ * - charcoalOutlineQuiet: Subtle dark outlined button (for light backgrounds)
32
+ * - ghost: No background/border, just icon (for light backgrounds)
33
+ * - ghostDark: No background/border, just icon (for dark backgrounds)
34
+ * - ivory: Light filled button (for dark backgrounds)
35
+ * - ivoryOutline: Light outlined button (for dark backgrounds)
36
+ * - ivoryOutlineQuiet: Subtle light outlined button (for dark backgrounds)
37
+ *
38
+ * Sizes:
39
+ * - lg: Large (46x46)
40
+ * - default: Medium (36x36)
41
+ * - sm: Small (29x29)
42
+ */
43
+ const iconButtonVariants = tv({
44
+ base: "inline-flex items-center justify-center whitespace-nowrap transition-colors duration-150 cursor-pointer focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
45
+ variants: {
46
+ variant: {
47
+ // Charcoal (dark filled) - primary dark
48
+ charcoal:
49
+ "bg-gray-1200 text-gray-100 hover:bg-gray-1100 active:bg-gray-1000 focus-visible:ring-gray-1000",
50
+ // Charcoal Outline - outlined dark (for light backgrounds)
51
+ charcoalOutline:
52
+ "border border-alpha-black-30 text-gray-1000 hover:bg-alpha-black-5 active:bg-alpha-black-10 focus-visible:ring-gray-1000",
53
+ // Charcoal Outline Quiet - subtle outlined dark (for light backgrounds)
54
+ charcoalOutlineQuiet:
55
+ "border border-alpha-black-20 text-alpha-black-60 hover:border-alpha-black-30 hover:text-alpha-black-80 active:bg-alpha-black-5 focus-visible:ring-gray-1000",
56
+ // Ghost - no background/border (for light backgrounds)
57
+ ghost:
58
+ "text-gray-700 hover:text-gray-900 hover:bg-alpha-black-5 active:bg-alpha-black-10 focus-visible:ring-gray-1000",
59
+ // Ghost Dark - no background/border (for dark backgrounds)
60
+ ghostDark:
61
+ "text-gray-300 hover:text-gray-100 hover:bg-alpha-white-10 active:bg-alpha-white-20 focus-visible:ring-gray-50 focus-visible:ring-offset-gray-1000",
62
+ // Ivory (light filled) - primary light (for dark backgrounds)
63
+ ivory:
64
+ "bg-gray-50 text-gray-1000 hover:bg-gray-100 active:bg-gray-200 focus-visible:ring-gray-50 focus-visible:ring-offset-gray-1000",
65
+ // Ivory Outline - outlined light (for dark backgrounds)
66
+ ivoryOutline:
67
+ "border border-gray-50 text-gray-50 hover:bg-alpha-white-10 active:bg-alpha-white-20 focus-visible:ring-gray-50 focus-visible:ring-offset-gray-1000",
68
+ // Ivory Outline Quiet - subtle light outline (for dark backgrounds)
69
+ ivoryOutlineQuiet:
70
+ "border border-alpha-white-20 text-alpha-white-60 hover:border-alpha-white-30 hover:text-alpha-white-80 active:bg-alpha-white-5 focus-visible:ring-gray-50 focus-visible:ring-offset-gray-1000",
71
+ },
72
+ size: {
73
+ // Large (48x48) - uses primitive spacing tokens
74
+ lg: "rounded-radius-12 size-spacing-48",
75
+ // Medium (40x40) - default - uses primitive spacing tokens
76
+ default: "rounded-radius-12 size-spacing-40",
77
+ // Small (32x32) - uses primitive spacing tokens
78
+ sm: "rounded-radius-10 size-spacing-32",
79
+ },
80
+ },
81
+ defaultVariants: {
82
+ variant: "charcoal",
83
+ size: "default",
84
+ },
85
+ });
86
+
87
+ export interface IconButtonProps
88
+ extends React.ButtonHTMLAttributes<HTMLButtonElement>,
89
+ VariantProps<typeof iconButtonVariants> {
90
+ asChild?: boolean;
91
+ }
92
+
93
+ const IconButton = React.forwardRef<HTMLButtonElement, IconButtonProps>(
94
+ ({ className, variant, size, asChild = false, ...props }, ref) => {
95
+ // Development warning for missing accessible label
96
+ React.useEffect(() => {
97
+ if (import.meta.env?.DEV) {
98
+ const hasAccessibleLabel =
99
+ props["aria-label"] || props["aria-labelledby"] || props.title;
100
+ if (!hasAccessibleLabel) {
101
+ console.warn(
102
+ "IconButton: Missing accessible label. Icon-only buttons must have an aria-label, aria-labelledby, or title attribute for screen reader users.",
103
+ );
104
+ }
105
+ }
106
+ }, [props["aria-label"], props["aria-labelledby"], props.title]);
107
+
108
+ const Comp = asChild ? Slot : "button";
109
+ return (
110
+ <Comp
111
+ className={iconButtonVariants({ variant, size, class: className })}
112
+ ref={ref}
113
+ {...props}
114
+ />
115
+ );
116
+ },
117
+ );
118
+ IconButton.displayName = "IconButton";
119
+
120
+ export { IconButton, iconButtonVariants };
@@ -0,0 +1,6 @@
1
+ export { Button, type ButtonProps, buttonVariants } from "./button";
2
+ export {
3
+ IconButton,
4
+ type IconButtonProps,
5
+ iconButtonVariants,
6
+ } from "./icon-button";
@@ -0,0 +1 @@
1
+ export { NdstudioFooter, type NdstudioFooterProps } from "./ndstudio-footer";
@@ -0,0 +1,55 @@
1
+ import * as React from "react";
2
+ import { cn } from "@/lib/utils";
3
+
4
+ export interface NdstudioFooterProps
5
+ extends React.HTMLAttributes<HTMLDivElement> {
6
+ /**
7
+ * The URL to link to
8
+ * @default "https://ndstudio.gov"
9
+ */
10
+ href?: string;
11
+ }
12
+
13
+ /**
14
+ * NdstudioFooter component displays a "Designed and Engineered in DC by National Design Studio" footer link.
15
+ *
16
+ * This component is designed to be used as a footer within other components like Tout,
17
+ * but can also be used standalone.
18
+ *
19
+ * @example
20
+ * ```tsx
21
+ * // Used within a Tout component via footer slot
22
+ * <Tout
23
+ * headline="Feature Headline"
24
+ * primaryAction={<Button>Primary</Button>}
25
+ * backgroundMedia={<img src="/bg.jpg" alt="" />}
26
+ * footer={<NdstudioFooter />}
27
+ * />
28
+ *
29
+ * // Standalone usage
30
+ * <NdstudioFooter className="my-custom-class" />
31
+ * ```
32
+ */
33
+ const NdstudioFooter = React.forwardRef<HTMLDivElement, NdstudioFooterProps>(
34
+ ({ className, href = "https://ndstudio.gov", ...props }, ref) => {
35
+ return (
36
+ <div ref={ref} className={cn("text-center", className)} {...props}>
37
+ <p className="typography-label-large uppercase text-ivory-alpha-75">
38
+ Designed and Engineered in DC by{" "}
39
+ <a
40
+ href={href}
41
+ target="_blank"
42
+ rel="noopener noreferrer"
43
+ className="hover:underline"
44
+ >
45
+ National Design Studio
46
+ <span className="sr-only"> (opens in new tab)</span>
47
+ </a>
48
+ </p>
49
+ </div>
50
+ );
51
+ },
52
+ );
53
+ NdstudioFooter.displayName = "NdstudioFooter";
54
+
55
+ export { NdstudioFooter };
@@ -0,0 +1,5 @@
1
+ export {
2
+ PagerControl,
3
+ type PagerControlProps,
4
+ pagerControlVariants,
5
+ } from "./pager-control";
@@ -0,0 +1,209 @@
1
+ import type { Meta, StoryObj } from "@storybook/react-vite";
2
+ import * as React from "react";
3
+ import { PagerControl } from ".";
4
+
5
+ const meta: Meta<typeof PagerControl> = {
6
+ title: "Atoms/PagerControl",
7
+ } as Meta<typeof PagerControl>;
8
+
9
+ export default meta;
10
+ type Story = StoryObj<typeof PagerControl>;
11
+
12
+ export const Playground: Story = {
13
+ render: (args) => <PagerControl {...args} />,
14
+ };
15
+ Playground.argTypes = {
16
+ count: {
17
+ control: {
18
+ type: "number",
19
+ min: 1,
20
+ max: 10,
21
+ },
22
+ },
23
+ activeIndex: {
24
+ control: {
25
+ type: "number",
26
+ min: 0,
27
+ },
28
+ },
29
+ duration: {
30
+ control: {
31
+ type: "number",
32
+ min: 0,
33
+ max: 10000,
34
+ step: 500,
35
+ },
36
+ },
37
+ autoPlay: {
38
+ control: {
39
+ type: "boolean",
40
+ },
41
+ },
42
+ pauseOnHover: {
43
+ control: {
44
+ type: "boolean",
45
+ },
46
+ },
47
+ loop: {
48
+ control: {
49
+ type: "boolean",
50
+ },
51
+ },
52
+ size: {
53
+ control: {
54
+ type: "radio",
55
+ },
56
+ options: ["sm", "default", "lg"],
57
+ },
58
+ variant: {
59
+ control: {
60
+ type: "radio",
61
+ },
62
+ options: ["charcoal", "ivory"],
63
+ },
64
+ };
65
+ Playground.args = {
66
+ count: 4,
67
+ duration: 5000,
68
+ autoPlay: true,
69
+ pauseOnHover: true,
70
+ loop: true,
71
+ size: "default",
72
+ variant: "charcoal",
73
+ };
74
+
75
+ // =============================================================================
76
+ // Variants
77
+ // =============================================================================
78
+
79
+ export const Charcoal = () => <PagerControl count={4} variant="charcoal" />;
80
+
81
+ export const Ivory = () => <PagerControl count={4} variant="ivory" />;
82
+
83
+ // =============================================================================
84
+ // Sizes
85
+ // =============================================================================
86
+
87
+ export const Small = () => <PagerControl count={4} size="sm" />;
88
+
89
+ export const Default = () => <PagerControl count={4} size="default" />;
90
+
91
+ export const Large = () => <PagerControl count={4} size="lg" />;
92
+
93
+ // =============================================================================
94
+ // Counts
95
+ // =============================================================================
96
+
97
+ export const TwoPages = () => <PagerControl count={2} />;
98
+
99
+ export const FivePages = () => <PagerControl count={5} />;
100
+
101
+ export const EightPages = () => <PagerControl count={8} />;
102
+
103
+ // =============================================================================
104
+ // Durations
105
+ // =============================================================================
106
+
107
+ export const FastDuration = () => <PagerControl count={4} duration={2000} />;
108
+
109
+ export const SlowDuration = () => <PagerControl count={4} duration={8000} />;
110
+
111
+ // =============================================================================
112
+ // Controls
113
+ // =============================================================================
114
+
115
+ export const NoAutoPlay = () => <PagerControl count={4} autoPlay={false} />;
116
+
117
+ export const NoLoop = () => (
118
+ <PagerControl count={4} loop={false} duration={3000} />
119
+ );
120
+
121
+ export const NoPauseOnHover = () => (
122
+ <PagerControl count={4} pauseOnHover={false} />
123
+ );
124
+
125
+ // =============================================================================
126
+ // Controlled
127
+ // =============================================================================
128
+
129
+ export const Controlled = () => {
130
+ const [activeIndex, setActiveIndex] = React.useState(0);
131
+
132
+ return (
133
+ <div className="flex flex-col items-center gap-spacing-24">
134
+ <PagerControl
135
+ count={4}
136
+ activeIndex={activeIndex}
137
+ onChange={setActiveIndex}
138
+ autoPlay={false}
139
+ />
140
+ <div className="flex gap-spacing-10">
141
+ <button
142
+ type="button"
143
+ onClick={() => setActiveIndex((prev) => Math.max(0, prev - 1))}
144
+ className="rounded bg-gray-200 px-spacing-12 py-spacing-6"
145
+ >
146
+ Previous
147
+ </button>
148
+ <span className="px-spacing-12 py-spacing-6">
149
+ Page {activeIndex + 1} of 4
150
+ </span>
151
+ <button
152
+ type="button"
153
+ onClick={() => setActiveIndex((prev) => Math.min(3, prev + 1))}
154
+ className="rounded bg-gray-200 px-spacing-12 py-spacing-6"
155
+ >
156
+ Next
157
+ </button>
158
+ </div>
159
+ </div>
160
+ );
161
+ };
162
+
163
+ // =============================================================================
164
+ // Integration Example
165
+ // =============================================================================
166
+
167
+ export const WithCarousel = () => {
168
+ const [activeIndex, setActiveIndex] = React.useState(0);
169
+ const slides = [
170
+ { id: 1, color: "bg-blue-100", title: "Slide 1" },
171
+ { id: 2, color: "bg-green-100", title: "Slide 2" },
172
+ { id: 3, color: "bg-yellow-100", title: "Slide 3" },
173
+ { id: 4, color: "bg-purple-100", title: "Slide 4" },
174
+ ];
175
+
176
+ return (
177
+ <div className="flex w-[400px] flex-col gap-spacing-16">
178
+ <div className="relative h-[200px] overflow-hidden rounded-radius-12">
179
+ {slides.map((slide, index) => (
180
+ <div
181
+ key={slide.id}
182
+ className={cn(
183
+ "absolute inset-0 flex items-center justify-center transition-transform duration-500",
184
+ slide.color,
185
+ )}
186
+ style={{
187
+ transform: `translateX(${(index - activeIndex) * 100}%)`,
188
+ }}
189
+ >
190
+ <span className="text-xl font-semibold">{slide.title}</span>
191
+ </div>
192
+ ))}
193
+ </div>
194
+ <div className="flex justify-center">
195
+ <PagerControl
196
+ count={slides.length}
197
+ activeIndex={activeIndex}
198
+ onChange={setActiveIndex}
199
+ duration={5000}
200
+ />
201
+ </div>
202
+ </div>
203
+ );
204
+ };
205
+
206
+ // Helper for carousel story
207
+ function cn(...classes: (string | boolean | undefined)[]) {
208
+ return classes.filter(Boolean).join(" ");
209
+ }