@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,245 @@
|
|
|
1
|
+
import { describe, expect, test } from "vitest";
|
|
2
|
+
import { page } from "vitest/browser";
|
|
3
|
+
import { render } from "vitest-browser-react";
|
|
4
|
+
import { Button } from "../../atoms/button";
|
|
5
|
+
import {
|
|
6
|
+
Card,
|
|
7
|
+
CardActions,
|
|
8
|
+
CardBody,
|
|
9
|
+
CardContent,
|
|
10
|
+
CardDescription,
|
|
11
|
+
CardEyebrow,
|
|
12
|
+
CardImage,
|
|
13
|
+
CardTitle,
|
|
14
|
+
} from "./card";
|
|
15
|
+
|
|
16
|
+
describe("Card", () => {
|
|
17
|
+
describe("Accessibility", () => {
|
|
18
|
+
test("CardTitle renders as h3 heading by default", async () => {
|
|
19
|
+
render(
|
|
20
|
+
<Card>
|
|
21
|
+
<CardContent>
|
|
22
|
+
<CardBody>
|
|
23
|
+
<CardTitle>Test Title</CardTitle>
|
|
24
|
+
</CardBody>
|
|
25
|
+
</CardContent>
|
|
26
|
+
</Card>,
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
await expect
|
|
30
|
+
.element(page.getByRole("heading", { level: 3, name: "Test Title" }))
|
|
31
|
+
.toBeInTheDocument();
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test("CardTitle supports as prop to change heading level", async () => {
|
|
35
|
+
render(
|
|
36
|
+
<Card>
|
|
37
|
+
<CardContent>
|
|
38
|
+
<CardBody>
|
|
39
|
+
<CardTitle as="h2">H2 Title</CardTitle>
|
|
40
|
+
</CardBody>
|
|
41
|
+
</CardContent>
|
|
42
|
+
</Card>,
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
await expect
|
|
46
|
+
.element(page.getByRole("heading", { level: 2, name: "H2 Title" }))
|
|
47
|
+
.toBeInTheDocument();
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test("CardImage renders img with alt text when src provided", async () => {
|
|
51
|
+
render(
|
|
52
|
+
<Card>
|
|
53
|
+
<CardImage
|
|
54
|
+
src="https://example.com/image.jpg"
|
|
55
|
+
alt="Test image description"
|
|
56
|
+
/>
|
|
57
|
+
<CardContent>
|
|
58
|
+
<CardBody>
|
|
59
|
+
<CardTitle>Card with Image</CardTitle>
|
|
60
|
+
</CardBody>
|
|
61
|
+
</CardContent>
|
|
62
|
+
</Card>,
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
await expect
|
|
66
|
+
.element(page.getByRole("img", { name: "Test image description" }))
|
|
67
|
+
.toBeInTheDocument();
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test("CardImage does not render img when no src provided", async () => {
|
|
71
|
+
render(
|
|
72
|
+
<Card>
|
|
73
|
+
<CardImage />
|
|
74
|
+
<CardContent>
|
|
75
|
+
<CardBody>
|
|
76
|
+
<CardTitle>Card without Image Source</CardTitle>
|
|
77
|
+
</CardBody>
|
|
78
|
+
</CardContent>
|
|
79
|
+
</Card>,
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
// Should not have an img element
|
|
83
|
+
const images = page.getByRole("img");
|
|
84
|
+
await expect.element(images).not.toBeInTheDocument();
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test("buttons in CardActions are accessible", async () => {
|
|
88
|
+
render(
|
|
89
|
+
<Card>
|
|
90
|
+
<CardContent>
|
|
91
|
+
<CardBody>
|
|
92
|
+
<CardTitle>Card with Actions</CardTitle>
|
|
93
|
+
</CardBody>
|
|
94
|
+
<CardActions>
|
|
95
|
+
<Button>Primary Action</Button>
|
|
96
|
+
<Button variant="charcoalOutline">Secondary Action</Button>
|
|
97
|
+
</CardActions>
|
|
98
|
+
</CardContent>
|
|
99
|
+
</Card>,
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
await expect
|
|
103
|
+
.element(page.getByRole("button", { name: "Primary Action" }))
|
|
104
|
+
.toBeInTheDocument();
|
|
105
|
+
await expect
|
|
106
|
+
.element(page.getByRole("button", { name: "Secondary Action" }))
|
|
107
|
+
.toBeInTheDocument();
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test("Card structure is semantically correct", async () => {
|
|
111
|
+
render(
|
|
112
|
+
<Card data-testid="card">
|
|
113
|
+
<CardContent>
|
|
114
|
+
<CardBody>
|
|
115
|
+
<CardEyebrow>Eyebrow Text</CardEyebrow>
|
|
116
|
+
<CardTitle>Card Title</CardTitle>
|
|
117
|
+
<CardDescription>Card description text</CardDescription>
|
|
118
|
+
</CardBody>
|
|
119
|
+
</CardContent>
|
|
120
|
+
</Card>,
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
// Verify all text content is present and readable
|
|
124
|
+
await expect.element(page.getByText("Eyebrow Text")).toBeInTheDocument();
|
|
125
|
+
await expect.element(page.getByText("Card Title")).toBeInTheDocument();
|
|
126
|
+
await expect
|
|
127
|
+
.element(page.getByText("Card description text"))
|
|
128
|
+
.toBeInTheDocument();
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
describe("Layout Variants", () => {
|
|
133
|
+
test("vertical layout applies correct classes", async () => {
|
|
134
|
+
render(
|
|
135
|
+
<Card layout="vertical" data-testid="card">
|
|
136
|
+
<CardContent>
|
|
137
|
+
<CardBody>
|
|
138
|
+
<CardTitle>Vertical Card</CardTitle>
|
|
139
|
+
</CardBody>
|
|
140
|
+
</CardContent>
|
|
141
|
+
</Card>,
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
const card = page.getByTestId("card");
|
|
145
|
+
await expect.element(card).toHaveClass(/flex-col/);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
test("horizontal layout applies correct classes", async () => {
|
|
149
|
+
render(
|
|
150
|
+
<Card layout="horizontal" data-testid="card">
|
|
151
|
+
<CardContent>
|
|
152
|
+
<CardBody>
|
|
153
|
+
<CardTitle>Horizontal Card</CardTitle>
|
|
154
|
+
</CardBody>
|
|
155
|
+
</CardContent>
|
|
156
|
+
</Card>,
|
|
157
|
+
);
|
|
158
|
+
|
|
159
|
+
const card = page.getByTestId("card");
|
|
160
|
+
await expect.element(card).toHaveClass(/flex-row/);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
test("default layout is vertical", async () => {
|
|
164
|
+
render(
|
|
165
|
+
<Card data-testid="card">
|
|
166
|
+
<CardContent>
|
|
167
|
+
<CardBody>
|
|
168
|
+
<CardTitle>Default Card</CardTitle>
|
|
169
|
+
</CardBody>
|
|
170
|
+
</CardContent>
|
|
171
|
+
</Card>,
|
|
172
|
+
);
|
|
173
|
+
|
|
174
|
+
const card = page.getByTestId("card");
|
|
175
|
+
await expect.element(card).toHaveClass(/flex-col/);
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
describe("Component Composition", () => {
|
|
180
|
+
test("renders complete card with all sub-components", async () => {
|
|
181
|
+
render(
|
|
182
|
+
<Card>
|
|
183
|
+
<CardImage src="https://example.com/test.jpg" alt="Test" />
|
|
184
|
+
<CardContent>
|
|
185
|
+
<CardBody>
|
|
186
|
+
<CardEyebrow>Category</CardEyebrow>
|
|
187
|
+
<CardTitle>Full Card Example</CardTitle>
|
|
188
|
+
<CardDescription>
|
|
189
|
+
This is a complete card with all components.
|
|
190
|
+
</CardDescription>
|
|
191
|
+
</CardBody>
|
|
192
|
+
<CardActions>
|
|
193
|
+
<Button>Action</Button>
|
|
194
|
+
</CardActions>
|
|
195
|
+
</CardContent>
|
|
196
|
+
</Card>,
|
|
197
|
+
);
|
|
198
|
+
|
|
199
|
+
await expect
|
|
200
|
+
.element(page.getByRole("img", { name: "Test" }))
|
|
201
|
+
.toBeInTheDocument();
|
|
202
|
+
await expect.element(page.getByText("Category")).toBeInTheDocument();
|
|
203
|
+
await expect
|
|
204
|
+
.element(page.getByRole("heading", { name: "Full Card Example" }))
|
|
205
|
+
.toBeInTheDocument();
|
|
206
|
+
await expect
|
|
207
|
+
.element(page.getByText("This is a complete card with all components."))
|
|
208
|
+
.toBeInTheDocument();
|
|
209
|
+
await expect
|
|
210
|
+
.element(page.getByRole("button", { name: "Action" }))
|
|
211
|
+
.toBeInTheDocument();
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
test("renders minimal card with only required components", async () => {
|
|
215
|
+
render(
|
|
216
|
+
<Card>
|
|
217
|
+
<CardContent>
|
|
218
|
+
<CardBody>
|
|
219
|
+
<CardTitle>Minimal</CardTitle>
|
|
220
|
+
</CardBody>
|
|
221
|
+
</CardContent>
|
|
222
|
+
</Card>,
|
|
223
|
+
);
|
|
224
|
+
|
|
225
|
+
await expect
|
|
226
|
+
.element(page.getByRole("heading", { name: "Minimal" }))
|
|
227
|
+
.toBeInTheDocument();
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
test("supports custom className on Card", async () => {
|
|
231
|
+
render(
|
|
232
|
+
<Card className="custom-class" data-testid="card">
|
|
233
|
+
<CardContent>
|
|
234
|
+
<CardBody>
|
|
235
|
+
<CardTitle>Custom Card</CardTitle>
|
|
236
|
+
</CardBody>
|
|
237
|
+
</CardContent>
|
|
238
|
+
</Card>,
|
|
239
|
+
);
|
|
240
|
+
|
|
241
|
+
const card = page.getByTestId("card");
|
|
242
|
+
await expect.element(card).toHaveClass(/custom-class/);
|
|
243
|
+
});
|
|
244
|
+
});
|
|
245
|
+
});
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { tv, type VariantProps } from "tailwind-variants";
|
|
3
|
+
import { cn } from "@/lib/utils";
|
|
4
|
+
|
|
5
|
+
const cardVariants = tv({
|
|
6
|
+
base: "flex overflow-hidden rounded-surface-card bg-card-background stroke-surface-card border-border-subtle border-solid",
|
|
7
|
+
variants: {
|
|
8
|
+
layout: {
|
|
9
|
+
vertical: "w-full flex-col",
|
|
10
|
+
horizontal: "w-full flex-row",
|
|
11
|
+
},
|
|
12
|
+
},
|
|
13
|
+
defaultVariants: {
|
|
14
|
+
layout: "vertical",
|
|
15
|
+
},
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
export interface CardProps
|
|
19
|
+
extends React.HTMLAttributes<HTMLDivElement>,
|
|
20
|
+
VariantProps<typeof cardVariants> {}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Card component for displaying content in a contained, scannable format.
|
|
24
|
+
*
|
|
25
|
+
* Layouts:
|
|
26
|
+
* - vertical: Image on top, content below (default)
|
|
27
|
+
* - horizontal: Image on left, content on right
|
|
28
|
+
*
|
|
29
|
+
* Use with CardImage, CardContent, CardEyebrow, CardTitle, CardDescription, and CardActions.
|
|
30
|
+
*/
|
|
31
|
+
const Card = React.forwardRef<HTMLDivElement, CardProps>(
|
|
32
|
+
({ className, layout, ...props }, ref) => {
|
|
33
|
+
return (
|
|
34
|
+
<div
|
|
35
|
+
ref={ref}
|
|
36
|
+
className={cardVariants({ layout, class: className })}
|
|
37
|
+
{...props}
|
|
38
|
+
/>
|
|
39
|
+
);
|
|
40
|
+
},
|
|
41
|
+
);
|
|
42
|
+
Card.displayName = "Card";
|
|
43
|
+
|
|
44
|
+
export interface CardImageProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
45
|
+
/**
|
|
46
|
+
* The image source URL
|
|
47
|
+
*/
|
|
48
|
+
src?: string;
|
|
49
|
+
/**
|
|
50
|
+
* Alt text for the image
|
|
51
|
+
*/
|
|
52
|
+
alt?: string;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Card image area. For vertical layout, displays with 16:9 aspect ratio.
|
|
57
|
+
* For horizontal layout, takes up ~40% width and stretches to content height.
|
|
58
|
+
*/
|
|
59
|
+
const CardImage = React.forwardRef<HTMLDivElement, CardImageProps>(
|
|
60
|
+
({ className, src, alt = "", ...props }, ref) => {
|
|
61
|
+
return (
|
|
62
|
+
<div
|
|
63
|
+
ref={ref}
|
|
64
|
+
className={cn(
|
|
65
|
+
"relative shrink-0 bg-gray-500",
|
|
66
|
+
// Vertical: full width with aspect ratio
|
|
67
|
+
"aspect-video w-full",
|
|
68
|
+
// When in horizontal card (parent has flex-row), override
|
|
69
|
+
"[.flex-row>&]:aspect-auto [.flex-row>&]:w-2/5 [.flex-row>&]:self-stretch",
|
|
70
|
+
className,
|
|
71
|
+
)}
|
|
72
|
+
{...props}
|
|
73
|
+
>
|
|
74
|
+
{src && (
|
|
75
|
+
<img
|
|
76
|
+
src={src}
|
|
77
|
+
alt={alt}
|
|
78
|
+
className="absolute inset-0 size-full object-cover"
|
|
79
|
+
/>
|
|
80
|
+
)}
|
|
81
|
+
</div>
|
|
82
|
+
);
|
|
83
|
+
},
|
|
84
|
+
);
|
|
85
|
+
CardImage.displayName = "CardImage";
|
|
86
|
+
|
|
87
|
+
export interface CardContentProps
|
|
88
|
+
extends React.HTMLAttributes<HTMLDivElement> {}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Card content container with proper padding and spacing.
|
|
92
|
+
* Uses spatial card tokens for consistent sizing.
|
|
93
|
+
*/
|
|
94
|
+
const CardContent = React.forwardRef<HTMLDivElement, CardContentProps>(
|
|
95
|
+
({ className, ...props }, ref) => {
|
|
96
|
+
return (
|
|
97
|
+
<div
|
|
98
|
+
ref={ref}
|
|
99
|
+
className={cn(
|
|
100
|
+
"flex w-full flex-1 flex-col gap-spatial-card-large-gap p-spatial-card-large-padding",
|
|
101
|
+
className,
|
|
102
|
+
)}
|
|
103
|
+
{...props}
|
|
104
|
+
/>
|
|
105
|
+
);
|
|
106
|
+
},
|
|
107
|
+
);
|
|
108
|
+
CardContent.displayName = "CardContent";
|
|
109
|
+
|
|
110
|
+
export interface CardEyebrowProps
|
|
111
|
+
extends React.HTMLAttributes<HTMLParagraphElement> {}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Optional eyebrow text above the card title.
|
|
115
|
+
*/
|
|
116
|
+
const CardEyebrow = React.forwardRef<HTMLParagraphElement, CardEyebrowProps>(
|
|
117
|
+
({ className, ...props }, ref) => {
|
|
118
|
+
return (
|
|
119
|
+
<p
|
|
120
|
+
ref={ref}
|
|
121
|
+
className={cn("typography-caption-large text-gray-500", className)}
|
|
122
|
+
{...props}
|
|
123
|
+
/>
|
|
124
|
+
);
|
|
125
|
+
},
|
|
126
|
+
);
|
|
127
|
+
CardEyebrow.displayName = "CardEyebrow";
|
|
128
|
+
|
|
129
|
+
export interface CardTitleProps
|
|
130
|
+
extends React.HTMLAttributes<HTMLHeadingElement> {
|
|
131
|
+
/**
|
|
132
|
+
* The heading level to render (h1-h6). Defaults to h3.
|
|
133
|
+
*/
|
|
134
|
+
as?: "h1" | "h2" | "h3" | "h4" | "h5" | "h6";
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Card title/heading. Use the `as` prop to change the heading level.
|
|
139
|
+
*/
|
|
140
|
+
const CardTitle = React.forwardRef<HTMLHeadingElement, CardTitleProps>(
|
|
141
|
+
({ className, as: Component = "h3", ...props }, ref) => {
|
|
142
|
+
return (
|
|
143
|
+
<Component
|
|
144
|
+
ref={ref}
|
|
145
|
+
className={cn("typography-subheading-small text-gray-1100", className)}
|
|
146
|
+
{...props}
|
|
147
|
+
/>
|
|
148
|
+
);
|
|
149
|
+
},
|
|
150
|
+
);
|
|
151
|
+
CardTitle.displayName = "CardTitle";
|
|
152
|
+
|
|
153
|
+
export interface CardDescriptionProps
|
|
154
|
+
extends React.HTMLAttributes<HTMLParagraphElement> {}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Card body/description text.
|
|
158
|
+
*/
|
|
159
|
+
const CardDescription = React.forwardRef<
|
|
160
|
+
HTMLParagraphElement,
|
|
161
|
+
CardDescriptionProps
|
|
162
|
+
>(({ className, ...props }, ref) => {
|
|
163
|
+
return (
|
|
164
|
+
<p
|
|
165
|
+
ref={ref}
|
|
166
|
+
className={cn("typography-body-small text-gray-800", className)}
|
|
167
|
+
{...props}
|
|
168
|
+
/>
|
|
169
|
+
);
|
|
170
|
+
});
|
|
171
|
+
CardDescription.displayName = "CardDescription";
|
|
172
|
+
|
|
173
|
+
export interface CardBodyProps extends React.HTMLAttributes<HTMLDivElement> {}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Container for card text content (eyebrow, title, description).
|
|
177
|
+
* Uses spatial card tokens for consistent sizing.
|
|
178
|
+
*/
|
|
179
|
+
const CardBody = React.forwardRef<HTMLDivElement, CardBodyProps>(
|
|
180
|
+
({ className, ...props }, ref) => {
|
|
181
|
+
return (
|
|
182
|
+
<div
|
|
183
|
+
ref={ref}
|
|
184
|
+
className={cn(
|
|
185
|
+
"flex w-full flex-col gap-spatial-card-small-gap",
|
|
186
|
+
className,
|
|
187
|
+
)}
|
|
188
|
+
{...props}
|
|
189
|
+
/>
|
|
190
|
+
);
|
|
191
|
+
},
|
|
192
|
+
);
|
|
193
|
+
CardBody.displayName = "CardBody";
|
|
194
|
+
|
|
195
|
+
export interface CardActionsProps
|
|
196
|
+
extends React.HTMLAttributes<HTMLDivElement> {}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Container for card action buttons.
|
|
200
|
+
* Uses primitive spacing tokens.
|
|
201
|
+
*/
|
|
202
|
+
const CardActions = React.forwardRef<HTMLDivElement, CardActionsProps>(
|
|
203
|
+
({ className, ...props }, ref) => {
|
|
204
|
+
return (
|
|
205
|
+
<div
|
|
206
|
+
ref={ref}
|
|
207
|
+
className={cn("flex gap-spacing-12", className)}
|
|
208
|
+
{...props}
|
|
209
|
+
/>
|
|
210
|
+
);
|
|
211
|
+
},
|
|
212
|
+
);
|
|
213
|
+
CardActions.displayName = "CardActions";
|
|
214
|
+
|
|
215
|
+
export {
|
|
216
|
+
Card,
|
|
217
|
+
cardVariants,
|
|
218
|
+
CardImage,
|
|
219
|
+
CardContent,
|
|
220
|
+
CardEyebrow,
|
|
221
|
+
CardTitle,
|
|
222
|
+
CardDescription,
|
|
223
|
+
CardBody,
|
|
224
|
+
CardActions,
|
|
225
|
+
};
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import { render } from "@testing-library/react";
|
|
2
|
+
import { describe, expect, test } from "vitest";
|
|
3
|
+
import { page } from "vitest/browser";
|
|
4
|
+
import { Button } from "../../atoms/button";
|
|
5
|
+
import {
|
|
6
|
+
Card,
|
|
7
|
+
CardActions,
|
|
8
|
+
CardBody,
|
|
9
|
+
CardContent,
|
|
10
|
+
CardDescription,
|
|
11
|
+
CardEyebrow,
|
|
12
|
+
CardImage,
|
|
13
|
+
CardTitle,
|
|
14
|
+
} from "./card";
|
|
15
|
+
|
|
16
|
+
describe("Card Visual Regression", () => {
|
|
17
|
+
test("default vertical layout renders correctly", async () => {
|
|
18
|
+
render(
|
|
19
|
+
<div
|
|
20
|
+
style={{ width: "418px", backgroundColor: "#1a1a1a", padding: "20px" }}
|
|
21
|
+
>
|
|
22
|
+
<Card data-testid="card">
|
|
23
|
+
<CardImage />
|
|
24
|
+
<CardContent>
|
|
25
|
+
<CardBody>
|
|
26
|
+
<CardEyebrow>Optional Eyebrow</CardEyebrow>
|
|
27
|
+
<div className="flex flex-col gap-[6px]">
|
|
28
|
+
<CardTitle>Card Title</CardTitle>
|
|
29
|
+
<CardDescription>
|
|
30
|
+
Use cards when citizens need to scan items at a glance.
|
|
31
|
+
</CardDescription>
|
|
32
|
+
</div>
|
|
33
|
+
</CardBody>
|
|
34
|
+
<CardActions>
|
|
35
|
+
<Button size="default" variant="charcoal">
|
|
36
|
+
Primary
|
|
37
|
+
</Button>
|
|
38
|
+
<Button size="default" variant="charcoalOutline">
|
|
39
|
+
Secondary
|
|
40
|
+
</Button>
|
|
41
|
+
</CardActions>
|
|
42
|
+
</CardContent>
|
|
43
|
+
</Card>
|
|
44
|
+
</div>,
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
await expect(page.getByTestId("card")).toMatchScreenshot(
|
|
48
|
+
"card-default-vertical",
|
|
49
|
+
);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test("horizontal layout renders correctly", async () => {
|
|
53
|
+
render(
|
|
54
|
+
<div
|
|
55
|
+
style={{ width: "700px", backgroundColor: "#1a1a1a", padding: "20px" }}
|
|
56
|
+
>
|
|
57
|
+
<Card layout="horizontal" data-testid="card">
|
|
58
|
+
<CardImage />
|
|
59
|
+
<CardContent>
|
|
60
|
+
<CardBody>
|
|
61
|
+
<CardEyebrow>Eyebrow</CardEyebrow>
|
|
62
|
+
<div className="flex flex-col gap-[9px]">
|
|
63
|
+
<CardTitle>
|
|
64
|
+
Cards can support multi line headings easily.
|
|
65
|
+
</CardTitle>
|
|
66
|
+
<CardDescription>
|
|
67
|
+
Use cards when citizens need to scan items at a glance.
|
|
68
|
+
</CardDescription>
|
|
69
|
+
</div>
|
|
70
|
+
</CardBody>
|
|
71
|
+
<CardActions>
|
|
72
|
+
<Button size="sm" variant="charcoal">
|
|
73
|
+
Primary
|
|
74
|
+
</Button>
|
|
75
|
+
<Button size="sm" variant="charcoalOutline">
|
|
76
|
+
Secondary
|
|
77
|
+
</Button>
|
|
78
|
+
</CardActions>
|
|
79
|
+
</CardContent>
|
|
80
|
+
</Card>
|
|
81
|
+
</div>,
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
await expect(page.getByTestId("card")).toMatchScreenshot("card-horizontal");
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test("without image renders correctly", async () => {
|
|
88
|
+
render(
|
|
89
|
+
<div
|
|
90
|
+
style={{ width: "418px", backgroundColor: "#1a1a1a", padding: "20px" }}
|
|
91
|
+
>
|
|
92
|
+
<Card data-testid="card">
|
|
93
|
+
<CardContent>
|
|
94
|
+
<CardBody>
|
|
95
|
+
<CardEyebrow>Category</CardEyebrow>
|
|
96
|
+
<div className="flex flex-col gap-[6px]">
|
|
97
|
+
<CardTitle>Card Without Image</CardTitle>
|
|
98
|
+
<CardDescription>
|
|
99
|
+
Cards can be used without images for text-focused content.
|
|
100
|
+
</CardDescription>
|
|
101
|
+
</div>
|
|
102
|
+
</CardBody>
|
|
103
|
+
<CardActions>
|
|
104
|
+
<Button size="default" variant="charcoal">
|
|
105
|
+
Learn More
|
|
106
|
+
</Button>
|
|
107
|
+
</CardActions>
|
|
108
|
+
</CardContent>
|
|
109
|
+
</Card>
|
|
110
|
+
</div>,
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
await expect(page.getByTestId("card")).toMatchScreenshot(
|
|
114
|
+
"card-without-image",
|
|
115
|
+
);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test("without eyebrow renders correctly", async () => {
|
|
119
|
+
render(
|
|
120
|
+
<div
|
|
121
|
+
style={{ width: "418px", backgroundColor: "#1a1a1a", padding: "20px" }}
|
|
122
|
+
>
|
|
123
|
+
<Card data-testid="card">
|
|
124
|
+
<CardImage />
|
|
125
|
+
<CardContent>
|
|
126
|
+
<CardBody>
|
|
127
|
+
<div className="flex flex-col gap-[6px]">
|
|
128
|
+
<CardTitle>Card Title</CardTitle>
|
|
129
|
+
<CardDescription>
|
|
130
|
+
The eyebrow is optional and can be omitted when not needed.
|
|
131
|
+
</CardDescription>
|
|
132
|
+
</div>
|
|
133
|
+
</CardBody>
|
|
134
|
+
<CardActions>
|
|
135
|
+
<Button size="default" variant="charcoal">
|
|
136
|
+
Primary
|
|
137
|
+
</Button>
|
|
138
|
+
</CardActions>
|
|
139
|
+
</CardContent>
|
|
140
|
+
</Card>
|
|
141
|
+
</div>,
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
await expect(page.getByTestId("card")).toMatchScreenshot(
|
|
145
|
+
"card-without-eyebrow",
|
|
146
|
+
);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
test("without actions renders correctly", async () => {
|
|
150
|
+
render(
|
|
151
|
+
<div
|
|
152
|
+
style={{ width: "418px", backgroundColor: "#1a1a1a", padding: "20px" }}
|
|
153
|
+
>
|
|
154
|
+
<Card data-testid="card">
|
|
155
|
+
<CardImage />
|
|
156
|
+
<CardContent>
|
|
157
|
+
<CardBody>
|
|
158
|
+
<CardEyebrow>Information</CardEyebrow>
|
|
159
|
+
<div className="flex flex-col gap-[6px]">
|
|
160
|
+
<CardTitle>Informational Card</CardTitle>
|
|
161
|
+
<CardDescription>
|
|
162
|
+
Cards without actions can be used for purely informational
|
|
163
|
+
content.
|
|
164
|
+
</CardDescription>
|
|
165
|
+
</div>
|
|
166
|
+
</CardBody>
|
|
167
|
+
</CardContent>
|
|
168
|
+
</Card>
|
|
169
|
+
</div>,
|
|
170
|
+
);
|
|
171
|
+
|
|
172
|
+
await expect(page.getByTestId("card")).toMatchScreenshot(
|
|
173
|
+
"card-without-actions",
|
|
174
|
+
);
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
test("minimal card renders correctly", async () => {
|
|
178
|
+
render(
|
|
179
|
+
<div
|
|
180
|
+
style={{ width: "418px", backgroundColor: "#1a1a1a", padding: "20px" }}
|
|
181
|
+
>
|
|
182
|
+
<Card data-testid="card">
|
|
183
|
+
<CardContent>
|
|
184
|
+
<CardBody>
|
|
185
|
+
<CardTitle>Minimal Card</CardTitle>
|
|
186
|
+
<CardDescription>
|
|
187
|
+
A minimal card with just title and description.
|
|
188
|
+
</CardDescription>
|
|
189
|
+
</CardBody>
|
|
190
|
+
</CardContent>
|
|
191
|
+
</Card>
|
|
192
|
+
</div>,
|
|
193
|
+
);
|
|
194
|
+
|
|
195
|
+
await expect(page.getByTestId("card")).toMatchScreenshot("card-minimal");
|
|
196
|
+
});
|
|
197
|
+
});
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export {
|
|
2
|
+
Card,
|
|
3
|
+
CardActions,
|
|
4
|
+
type CardActionsProps,
|
|
5
|
+
CardBody,
|
|
6
|
+
type CardBodyProps,
|
|
7
|
+
CardContent,
|
|
8
|
+
type CardContentProps,
|
|
9
|
+
CardDescription,
|
|
10
|
+
type CardDescriptionProps,
|
|
11
|
+
CardEyebrow,
|
|
12
|
+
type CardEyebrowProps,
|
|
13
|
+
CardImage,
|
|
14
|
+
type CardImageProps,
|
|
15
|
+
type CardProps,
|
|
16
|
+
CardTitle,
|
|
17
|
+
type CardTitleProps,
|
|
18
|
+
cardVariants,
|
|
19
|
+
} from "./card";
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|