@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.
- package/dist/tailwind.css +15 -1
- package/dist/tokens.css +45 -60
- package/package.json +5 -10
- package/src/App.css +0 -0
- package/src/App.tsx +7 -0
- package/src/assets/fonts/PPNeueMontreal-Variable.woff2 +0 -0
- package/src/assets/react.svg +1 -0
- package/src/components/atoms/accordion/accordion.stories.tsx +228 -0
- package/src/components/atoms/accordion/accordion.tsx +219 -0
- package/src/components/atoms/accordion/index.ts +6 -0
- package/src/components/atoms/button/__screenshots__/button.visual.test.tsx/button-charcoal-chromium-darwin.png +0 -0
- package/src/components/atoms/button/__screenshots__/button.visual.test.tsx/button-charcoal-chromium-linux.png +0 -0
- package/src/components/atoms/button/__screenshots__/button.visual.test.tsx/button-charcoal-outline-chromium-darwin.png +0 -0
- package/src/components/atoms/button/__screenshots__/button.visual.test.tsx/button-charcoal-outline-chromium-linux.png +0 -0
- package/src/components/atoms/button/__screenshots__/button.visual.test.tsx/button-charcoal-outline-quiet-chromium-darwin.png +0 -0
- package/src/components/atoms/button/__screenshots__/button.visual.test.tsx/button-charcoal-outline-quiet-chromium-linux.png +0 -0
- package/src/components/atoms/button/__screenshots__/button.visual.test.tsx/button-disabled-chromium-darwin.png +0 -0
- package/src/components/atoms/button/__screenshots__/button.visual.test.tsx/button-disabled-chromium-linux.png +0 -0
- package/src/components/atoms/button/__screenshots__/button.visual.test.tsx/button-ivory-chromium-darwin.png +0 -0
- package/src/components/atoms/button/__screenshots__/button.visual.test.tsx/button-ivory-chromium-linux.png +0 -0
- package/src/components/atoms/button/__screenshots__/button.visual.test.tsx/button-ivory-outline-chromium-darwin.png +0 -0
- package/src/components/atoms/button/__screenshots__/button.visual.test.tsx/button-ivory-outline-chromium-linux.png +0 -0
- package/src/components/atoms/button/__screenshots__/button.visual.test.tsx/button-ivory-outline-quiet-chromium-darwin.png +0 -0
- package/src/components/atoms/button/__screenshots__/button.visual.test.tsx/button-ivory-outline-quiet-chromium-linux.png +0 -0
- package/src/components/atoms/button/__screenshots__/button.visual.test.tsx/button-size-large-chromium-darwin.png +0 -0
- package/src/components/atoms/button/__screenshots__/button.visual.test.tsx/button-size-large-chromium-linux.png +0 -0
- package/src/components/atoms/button/__screenshots__/button.visual.test.tsx/button-size-medium-chromium-darwin.png +0 -0
- package/src/components/atoms/button/__screenshots__/button.visual.test.tsx/button-size-medium-chromium-linux.png +0 -0
- package/src/components/atoms/button/__screenshots__/button.visual.test.tsx/button-size-small-chromium-darwin.png +0 -0
- package/src/components/atoms/button/__screenshots__/button.visual.test.tsx/button-size-small-chromium-linux.png +0 -0
- package/src/components/atoms/button/button.stories.tsx +102 -0
- package/src/components/atoms/button/button.test.tsx +135 -0
- package/src/components/atoms/button/button.tsx +139 -0
- package/src/components/atoms/button/button.visual.test.tsx +102 -0
- package/src/components/atoms/button/icon-button.stories.tsx +166 -0
- package/src/components/atoms/button/icon-button.tsx +120 -0
- package/src/components/atoms/button/index.ts +6 -0
- package/src/components/atoms/ndstudio-footer/index.ts +1 -0
- package/src/components/atoms/ndstudio-footer/ndstudio-footer.tsx +55 -0
- package/src/components/atoms/pager-control/index.ts +5 -0
- package/src/components/atoms/pager-control/pager-control.stories.tsx +209 -0
- package/src/components/atoms/pager-control/pager-control.test.tsx +130 -0
- package/src/components/atoms/pager-control/pager-control.tsx +329 -0
- package/src/components/dev-tools/dev-toolbar/dev-toolbar.stories.tsx +82 -0
- package/src/components/dev-tools/dev-toolbar/dev-toolbar.tsx +196 -0
- package/src/components/dev-tools/dev-toolbar/index.ts +1 -0
- package/src/components/dev-tools/grid-overlay/grid-overlay.tsx +41 -0
- package/src/components/dev-tools/grid-overlay/index.ts +1 -0
- package/src/components/dev-tools/index.ts +2 -0
- package/src/components/organisms/card/__screenshots__/card.visual.test.tsx/card-default-vertical-chromium-darwin.png +0 -0
- package/src/components/organisms/card/__screenshots__/card.visual.test.tsx/card-default-vertical-chromium-linux.png +0 -0
- package/src/components/organisms/card/__screenshots__/card.visual.test.tsx/card-horizontal-chromium-darwin.png +0 -0
- package/src/components/organisms/card/__screenshots__/card.visual.test.tsx/card-horizontal-chromium-linux.png +0 -0
- package/src/components/organisms/card/__screenshots__/card.visual.test.tsx/card-minimal-chromium-darwin.png +0 -0
- package/src/components/organisms/card/__screenshots__/card.visual.test.tsx/card-minimal-chromium-linux.png +0 -0
- package/src/components/organisms/card/__screenshots__/card.visual.test.tsx/card-without-actions-chromium-darwin.png +0 -0
- package/src/components/organisms/card/__screenshots__/card.visual.test.tsx/card-without-actions-chromium-linux.png +0 -0
- package/src/components/organisms/card/__screenshots__/card.visual.test.tsx/card-without-eyebrow-chromium-darwin.png +0 -0
- package/src/components/organisms/card/__screenshots__/card.visual.test.tsx/card-without-eyebrow-chromium-linux.png +0 -0
- package/src/components/organisms/card/__screenshots__/card.visual.test.tsx/card-without-image-chromium-darwin.png +0 -0
- package/src/components/organisms/card/__screenshots__/card.visual.test.tsx/card-without-image-chromium-linux.png +0 -0
- package/src/components/organisms/card/card.stories.tsx +293 -0
- package/src/components/organisms/card/card.test.tsx +245 -0
- package/src/components/organisms/card/card.tsx +225 -0
- package/src/components/organisms/card/card.visual.test.tsx +197 -0
- package/src/components/organisms/card/index.ts +19 -0
- package/src/components/organisms/navbar/__screenshots__/navbar.visual.test.tsx/navbar-active-link-chromium-darwin.png +0 -0
- package/src/components/organisms/navbar/__screenshots__/navbar.visual.test.tsx/navbar-active-link-chromium-linux.png +0 -0
- package/src/components/organisms/navbar/__screenshots__/navbar.visual.test.tsx/navbar-brand-only-chromium-darwin.png +0 -0
- package/src/components/organisms/navbar/__screenshots__/navbar.visual.test.tsx/navbar-brand-only-chromium-linux.png +0 -0
- package/src/components/organisms/navbar/__screenshots__/navbar.visual.test.tsx/navbar-default-chromium-darwin.png +0 -0
- package/src/components/organisms/navbar/__screenshots__/navbar.visual.test.tsx/navbar-default-chromium-linux.png +0 -0
- package/src/components/organisms/navbar/index.ts +18 -0
- package/src/components/organisms/navbar/navbar.stories.tsx +313 -0
- package/src/components/organisms/navbar/navbar.test.tsx +190 -0
- package/src/components/organisms/navbar/navbar.tsx +323 -0
- package/src/components/organisms/navbar/navbar.visual.test.tsx +85 -0
- package/src/components/organisms/us-gov-banner/__screenshots__/us-gov-banner.visual.test.tsx/us-gov-banner-custom-icon-chromium-darwin.png +0 -0
- package/src/components/organisms/us-gov-banner/__screenshots__/us-gov-banner.visual.test.tsx/us-gov-banner-custom-icon-chromium-linux.png +0 -0
- package/src/components/organisms/us-gov-banner/__screenshots__/us-gov-banner.visual.test.tsx/us-gov-banner-custom-text-chromium-darwin.png +0 -0
- package/src/components/organisms/us-gov-banner/__screenshots__/us-gov-banner.visual.test.tsx/us-gov-banner-custom-text-chromium-linux.png +0 -0
- package/src/components/organisms/us-gov-banner/__screenshots__/us-gov-banner.visual.test.tsx/us-gov-banner-default-chromium-darwin.png +0 -0
- package/src/components/organisms/us-gov-banner/__screenshots__/us-gov-banner.visual.test.tsx/us-gov-banner-default-chromium-linux.png +0 -0
- package/src/components/organisms/us-gov-banner/index.ts +1 -0
- package/src/components/organisms/us-gov-banner/us-gov-banner.stories.tsx +35 -0
- package/src/components/organisms/us-gov-banner/us-gov-banner.test.tsx +107 -0
- package/src/components/organisms/us-gov-banner/us-gov-banner.tsx +73 -0
- package/src/components/organisms/us-gov-banner/us-gov-banner.visual.test.tsx +46 -0
- package/src/components/sections/banner/banner.stories.tsx +150 -0
- package/src/components/sections/banner/banner.test.tsx +185 -0
- package/src/components/sections/banner/banner.tsx +130 -0
- package/src/components/sections/banner/index.ts +2 -0
- package/src/components/sections/card-grid/card-grid.stories.tsx +351 -0
- package/src/components/sections/card-grid/card-grid.tsx +116 -0
- package/src/components/sections/card-grid/index.ts +1 -0
- package/src/components/sections/faq-section/faq-section.stories.tsx +453 -0
- package/src/components/sections/faq-section/faq-section.tsx +84 -0
- package/src/components/sections/faq-section/index.ts +2 -0
- package/src/components/sections/hero/__screenshots__/hero.visual.test.tsx/hero-a1-desktop-chromium-darwin.png +0 -0
- package/src/components/sections/hero/__screenshots__/hero.visual.test.tsx/hero-a1-desktop-chromium-linux.png +0 -0
- package/src/components/sections/hero/__screenshots__/hero.visual.test.tsx/hero-a1-mobile-chromium-darwin.png +0 -0
- package/src/components/sections/hero/__screenshots__/hero.visual.test.tsx/hero-a1-mobile-chromium-linux.png +0 -0
- package/src/components/sections/hero/__screenshots__/hero.visual.test.tsx/hero-a1-tablet-chromium-darwin.png +0 -0
- package/src/components/sections/hero/__screenshots__/hero.visual.test.tsx/hero-a1-tablet-chromium-linux.png +0 -0
- package/src/components/sections/hero/__screenshots__/hero.visual.test.tsx/hero-a2-desktop-chromium-darwin.png +0 -0
- package/src/components/sections/hero/__screenshots__/hero.visual.test.tsx/hero-a2-desktop-chromium-linux.png +0 -0
- package/src/components/sections/hero/__screenshots__/hero.visual.test.tsx/hero-a2-mobile-chromium-darwin.png +0 -0
- package/src/components/sections/hero/__screenshots__/hero.visual.test.tsx/hero-a2-mobile-chromium-linux.png +0 -0
- package/src/components/sections/hero/__screenshots__/hero.visual.test.tsx/hero-a2-tablet-chromium-darwin.png +0 -0
- package/src/components/sections/hero/__screenshots__/hero.visual.test.tsx/hero-a2-tablet-chromium-linux.png +0 -0
- package/src/components/sections/hero/__screenshots__/hero.visual.test.tsx/hero-a3-desktop-chromium-darwin.png +0 -0
- package/src/components/sections/hero/__screenshots__/hero.visual.test.tsx/hero-a3-desktop-chromium-linux.png +0 -0
- package/src/components/sections/hero/__screenshots__/hero.visual.test.tsx/hero-a3-mobile-chromium-darwin.png +0 -0
- package/src/components/sections/hero/__screenshots__/hero.visual.test.tsx/hero-a3-mobile-chromium-linux.png +0 -0
- package/src/components/sections/hero/__screenshots__/hero.visual.test.tsx/hero-a3-tablet-chromium-darwin.png +0 -0
- package/src/components/sections/hero/__screenshots__/hero.visual.test.tsx/hero-a3-tablet-chromium-linux.png +0 -0
- package/src/components/sections/hero/__screenshots__/hero.visual.test.tsx/hero-custom-class-chromium-darwin.png +0 -0
- package/src/components/sections/hero/__screenshots__/hero.visual.test.tsx/hero-custom-class-chromium-linux.png +0 -0
- package/src/components/sections/hero/__screenshots__/hero.visual.test.tsx/hero-default-chromium-linux.png +0 -0
- package/src/components/sections/hero/__screenshots__/hero.visual.test.tsx/hero-long-title-chromium-darwin.png +0 -0
- package/src/components/sections/hero/__screenshots__/hero.visual.test.tsx/hero-long-title-chromium-linux.png +0 -0
- package/src/components/sections/hero/hero.stories.tsx +274 -0
- package/src/components/sections/hero/hero.test.tsx +135 -0
- package/src/components/sections/hero/hero.tsx +453 -0
- package/src/components/sections/hero/hero.visual.test.tsx +140 -0
- package/src/components/sections/hero/index.ts +10 -0
- package/src/components/sections/prose/__screenshots__/prose.visual.test.tsx/prose-h3-heading-chromium-darwin.png +0 -0
- package/src/components/sections/prose/__screenshots__/prose.visual.test.tsx/prose-h3-heading-chromium-linux.png +0 -0
- package/src/components/sections/prose/__screenshots__/prose.visual.test.tsx/prose-multiple-paragraphs-chromium-darwin.png +0 -0
- package/src/components/sections/prose/__screenshots__/prose.visual.test.tsx/prose-multiple-paragraphs-chromium-linux.png +0 -0
- package/src/components/sections/prose/__screenshots__/prose.visual.test.tsx/prose-multiple-sections-chromium-darwin.png +0 -0
- package/src/components/sections/prose/__screenshots__/prose.visual.test.tsx/prose-multiple-sections-chromium-linux.png +0 -0
- package/src/components/sections/prose/__screenshots__/prose.visual.test.tsx/prose-single-section-chromium-darwin.png +0 -0
- package/src/components/sections/prose/__screenshots__/prose.visual.test.tsx/prose-single-section-chromium-linux.png +0 -0
- package/src/components/sections/prose/index.ts +6 -0
- package/src/components/sections/prose/prose.stories.tsx +144 -0
- package/src/components/sections/prose/prose.test.tsx +178 -0
- package/src/components/sections/prose/prose.tsx +88 -0
- package/src/components/sections/prose/prose.visual.test.tsx +105 -0
- package/src/components/sections/river/index.ts +1 -0
- package/src/components/sections/river/river.stories.tsx +237 -0
- package/src/components/sections/river/river.test.tsx +268 -0
- package/src/components/sections/river/river.tsx +173 -0
- package/src/components/sections/tout/index.ts +1 -0
- package/src/components/sections/tout/tout.stories.tsx +171 -0
- package/src/components/sections/tout/tout.test.tsx +242 -0
- package/src/components/sections/tout/tout.tsx +270 -0
- package/src/components/sections/two-column-section/index.ts +5 -0
- package/src/components/sections/two-column-section/two-column-section.stories.tsx +285 -0
- package/src/components/sections/two-column-section/two-column-section.tsx +162 -0
- package/src/hooks/index.ts +1 -0
- package/src/hooks/use-event-listener.ts +73 -0
- package/src/index.ts +155 -0
- package/src/lib/theme.ts +1000 -0
- package/src/lib/utils.ts +6 -0
- package/src/main.tsx +13 -0
- package/src/stories/GridSystem.stories.tsx +84 -0
- package/src/stories/Introduction.mdx +114 -0
- package/src/stories/ThemeProvider.stories.tsx +357 -0
- package/src/stories/TokenShowcase.stories.tsx +92 -0
- package/src/stories/TokenShowcase.tsx +1429 -0
- package/src/styles.css +11 -0
- package/src/theme/ThemeProvider.tsx +297 -0
- package/src/theme/hooks.ts +40 -0
- package/src/theme/index.ts +43 -0
- 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 @@
|
|
|
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,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
|
+
}
|