@nationaldesignstudio/react 0.0.19 → 0.2.0

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 (34) hide show
  1. package/dist/component-registry.md +46 -19
  2. package/dist/components/atoms/accordion/accordion.d.ts +9 -9
  3. package/dist/components/atoms/background/background.d.ts +23 -0
  4. package/dist/components/atoms/button/icon-button.d.ts +44 -16
  5. package/dist/components/sections/banner/banner.d.ts +9 -9
  6. package/dist/components/sections/faq-section/faq-section.d.ts +1 -1
  7. package/dist/components/sections/hero/hero.d.ts +115 -18
  8. package/dist/components/sections/tout/tout.d.ts +8 -8
  9. package/dist/components/sections/two-column-section/two-column-section.d.ts +7 -21
  10. package/dist/index.js +2177 -2027
  11. package/dist/index.js.map +1 -1
  12. package/dist/tokens.css +16 -16
  13. package/package.json +1 -1
  14. package/src/components/atoms/accordion/accordion.test.tsx +231 -0
  15. package/src/components/atoms/accordion/accordion.tsx +21 -19
  16. package/src/components/atoms/background/background.test.tsx +213 -0
  17. package/src/components/atoms/background/background.tsx +104 -27
  18. package/src/components/atoms/button/button.tsx +11 -1
  19. package/src/components/atoms/button/icon-button.test.tsx +254 -0
  20. package/src/components/atoms/button/icon-button.tsx +108 -16
  21. package/src/components/atoms/pager-control/pager-control.tsx +32 -3
  22. package/src/components/dev-tools/dev-toolbar/dev-toolbar.tsx +2 -0
  23. package/src/components/organisms/card/card.test.tsx +4 -2
  24. package/src/components/organisms/navbar/navbar.tsx +2 -0
  25. package/src/components/sections/banner/banner.stories.tsx +5 -1
  26. package/src/components/sections/banner/banner.tsx +10 -10
  27. package/src/components/sections/faq-section/faq-section.stories.tsx +7 -7
  28. package/src/components/sections/faq-section/faq-section.tsx +3 -3
  29. package/src/components/sections/hero/hero.tsx +33 -51
  30. package/src/components/sections/tout/tout.stories.tsx +31 -7
  31. package/src/components/sections/tout/tout.tsx +6 -8
  32. package/src/components/sections/two-column-section/two-column-section.stories.tsx +11 -11
  33. package/src/components/sections/two-column-section/two-column-section.tsx +16 -10
  34. package/src/stories/ThemeProvider.stories.tsx +9 -3
package/dist/tokens.css CHANGED
@@ -1448,26 +1448,26 @@
1448
1448
 
1449
1449
  :root {
1450
1450
  /* Semantic Color Tokens */
1451
- --color-accent-brand-soft: color(srgb 0.7686 0.6588 0.4392);
1452
- --color-accent-brand: color(srgb 0.651 0.5451 0.3686);
1453
- --color-bg-muted: color(srgb 0.9961 0.9922 0.9765);
1454
- --color-bg-page: color(srgb 0.9961 0.9922 0.9765);
1455
- --color-bg-section: color(srgb 0.9608 0.9569 0.9373);
1456
- --color-border-divider: color(srgb 0.149 0.1569 0.2941 / 0.1);
1457
- --color-border-focus: color(srgb 0.651 0.5451 0.3686);
1458
- --color-border-strong: color(srgb 0.149 0.1569 0.2941 / 0.2);
1459
- --color-border-subtle: color(srgb 0.149 0.1569 0.2941 / 0.1);
1460
- --color-button-primary-bg-hover: color(srgb 0.149 0.1569 0.2941);
1461
- --color-button-primary-bg: color(srgb 0.0667 0.0745 0.149);
1451
+ --color-accent-brand-soft: color(srgb 0.9686 0.8078 0.749);
1452
+ --color-accent-brand: color(srgb 0.9216 0.3098 0.1529);
1453
+ --color-bg-muted: color(srgb 1 1 1);
1454
+ --color-bg-page: color(srgb 0.9804 0.9804 0.9804);
1455
+ --color-bg-section: color(srgb 0.9608 0.9608 0.9608);
1456
+ --color-border-divider: color(srgb 0 0 0 / 0.1);
1457
+ --color-border-focus: color(srgb 0.9216 0.3098 0.1529);
1458
+ --color-border-strong: color(srgb 0 0 0 / 0.2);
1459
+ --color-border-subtle: color(srgb 0 0 0 / 0.1);
1460
+ --color-button-primary-bg-hover: color(srgb 0.4588 0.4588 0.4588);
1461
+ --color-button-primary-bg: color(srgb 0.0784 0.0784 0.0784);
1462
1462
  --color-button-secondary-bg-hover: color(srgb 1 1 1);
1463
1463
  --color-button-secondary-bg: color(srgb 1 1 1);
1464
- --color-card-background: color(srgb 0.5412 0.4353 0.2588);
1465
- --color-text-inverted: color(srgb 0.9961 0.9922 0.9765);
1464
+ --color-card-background: color(srgb 1 1 1);
1465
+ --color-text-inverted: color(srgb 0.9608 0.9608 0.9608);
1466
1466
  --color-text-link-hover: color(srgb 0.3804 0.3804 0.3804);
1467
1467
  --color-text-link: color(srgb 0.0784 0.0784 0.0784);
1468
- --color-text-muted: color(srgb 0.651 0.5451 0.3686);
1469
- --color-text-primary: color(srgb 0.0667 0.0745 0.149);
1470
- --color-text-secondary: color(srgb 0.0667 0.0745 0.149);
1468
+ --color-text-muted: color(srgb 0.4588 0.4588 0.4588);
1469
+ --color-text-primary: color(srgb 0.0784 0.0784 0.0784);
1470
+ --color-text-secondary: color(srgb 0.2588 0.2588 0.2588);
1471
1471
  --radius-surface-button: var(--radius-6);
1472
1472
  --radius-surface-card: var(--radius-4);
1473
1473
  --surface-button-stroke: 1px;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nationaldesignstudio/react",
3
- "version": "0.0.19",
3
+ "version": "0.2.0",
4
4
  "type": "module",
5
5
  "sideEffects": [
6
6
  "*.css"
@@ -0,0 +1,231 @@
1
+ import { describe, expect, test } from "vitest";
2
+ import { page, userEvent } from "vitest/browser";
3
+ import { render } from "vitest-browser-react";
4
+ import { Accordion, AccordionItem } from "./accordion";
5
+
6
+ describe("Accordion", () => {
7
+ describe("Rendering", () => {
8
+ test("renders accordion with items", async () => {
9
+ render(
10
+ <Accordion>
11
+ <AccordionItem id="item-1" title="Question 1">
12
+ Answer 1
13
+ </AccordionItem>
14
+ <AccordionItem id="item-2" title="Question 2">
15
+ Answer 2
16
+ </AccordionItem>
17
+ </Accordion>,
18
+ );
19
+ await expect
20
+ .element(page.getByRole("button", { name: "Question 1" }))
21
+ .toBeInTheDocument();
22
+ await expect
23
+ .element(page.getByRole("button", { name: "Question 2" }))
24
+ .toBeInTheDocument();
25
+ });
26
+
27
+ test("renders with default expanded item", async () => {
28
+ render(
29
+ <Accordion defaultExpanded="item-1">
30
+ <AccordionItem id="item-1" title="Question 1">
31
+ Answer 1
32
+ </AccordionItem>
33
+ <AccordionItem id="item-2" title="Question 2">
34
+ Answer 2
35
+ </AccordionItem>
36
+ </Accordion>,
37
+ );
38
+ // The first item should be expanded (has data-open attribute)
39
+ await expect
40
+ .element(page.getByRole("button", { name: "Question 1" }))
41
+ .toHaveAttribute("aria-expanded", "true");
42
+ });
43
+
44
+ test("renders with multiple default expanded items", async () => {
45
+ render(
46
+ <Accordion allowMultiple defaultExpanded={["item-1", "item-2"]}>
47
+ <AccordionItem id="item-1" title="Question 1">
48
+ Answer 1
49
+ </AccordionItem>
50
+ <AccordionItem id="item-2" title="Question 2">
51
+ Answer 2
52
+ </AccordionItem>
53
+ </Accordion>,
54
+ );
55
+ await expect
56
+ .element(page.getByRole("button", { name: "Question 1" }))
57
+ .toHaveAttribute("aria-expanded", "true");
58
+ await expect
59
+ .element(page.getByRole("button", { name: "Question 2" }))
60
+ .toHaveAttribute("aria-expanded", "true");
61
+ });
62
+ });
63
+
64
+ describe("Keyboard Navigation", () => {
65
+ test("items are focusable via Tab", async () => {
66
+ render(
67
+ <Accordion>
68
+ <AccordionItem id="item-1" title="Question 1">
69
+ Answer 1
70
+ </AccordionItem>
71
+ <AccordionItem id="item-2" title="Question 2">
72
+ Answer 2
73
+ </AccordionItem>
74
+ </Accordion>,
75
+ );
76
+ await userEvent.keyboard("{Tab}");
77
+ await expect
78
+ .element(page.getByRole("button", { name: "Question 1" }))
79
+ .toHaveFocus();
80
+ });
81
+
82
+ test("Enter key expands/collapses item", async () => {
83
+ render(
84
+ <Accordion>
85
+ <AccordionItem id="item-1" title="Question 1">
86
+ Answer 1
87
+ </AccordionItem>
88
+ </Accordion>,
89
+ );
90
+ const trigger = page.getByRole("button", { name: "Question 1" });
91
+ trigger.element().focus();
92
+ await userEvent.keyboard("{Enter}");
93
+ await expect.element(trigger).toHaveAttribute("aria-expanded", "true");
94
+ });
95
+
96
+ test("Space key expands/collapses item", async () => {
97
+ render(
98
+ <Accordion>
99
+ <AccordionItem id="item-1" title="Question 1">
100
+ Answer 1
101
+ </AccordionItem>
102
+ </Accordion>,
103
+ );
104
+ const trigger = page.getByRole("button", { name: "Question 1" });
105
+ trigger.element().focus();
106
+ await userEvent.keyboard(" ");
107
+ await expect.element(trigger).toHaveAttribute("aria-expanded", "true");
108
+ });
109
+ });
110
+
111
+ describe("Click Interactions", () => {
112
+ test("clicking trigger expands item", async () => {
113
+ render(
114
+ <Accordion>
115
+ <AccordionItem id="item-1" title="Question 1">
116
+ Answer 1
117
+ </AccordionItem>
118
+ </Accordion>,
119
+ );
120
+ const trigger = page.getByRole("button", { name: "Question 1" });
121
+ await trigger.click();
122
+ await expect.element(trigger).toHaveAttribute("aria-expanded", "true");
123
+ });
124
+
125
+ test("clicking expanded trigger collapses item", async () => {
126
+ render(
127
+ <Accordion defaultExpanded="item-1">
128
+ <AccordionItem id="item-1" title="Question 1">
129
+ Answer 1
130
+ </AccordionItem>
131
+ </Accordion>,
132
+ );
133
+ const trigger = page.getByRole("button", { name: "Question 1" });
134
+ await expect.element(trigger).toHaveAttribute("aria-expanded", "true");
135
+ await trigger.click();
136
+ await expect.element(trigger).toHaveAttribute("aria-expanded", "false");
137
+ });
138
+
139
+ test("single mode collapses other items when opening new one", async () => {
140
+ render(
141
+ <Accordion defaultExpanded="item-1">
142
+ <AccordionItem id="item-1" title="Question 1">
143
+ Answer 1
144
+ </AccordionItem>
145
+ <AccordionItem id="item-2" title="Question 2">
146
+ Answer 2
147
+ </AccordionItem>
148
+ </Accordion>,
149
+ );
150
+ const trigger1 = page.getByRole("button", { name: "Question 1" });
151
+ const trigger2 = page.getByRole("button", { name: "Question 2" });
152
+
153
+ await expect.element(trigger1).toHaveAttribute("aria-expanded", "true");
154
+ await trigger2.click();
155
+ await expect.element(trigger2).toHaveAttribute("aria-expanded", "true");
156
+ await expect.element(trigger1).toHaveAttribute("aria-expanded", "false");
157
+ });
158
+
159
+ test("multiple mode allows multiple items open", async () => {
160
+ render(
161
+ <Accordion allowMultiple defaultExpanded="item-1">
162
+ <AccordionItem id="item-1" title="Question 1">
163
+ Answer 1
164
+ </AccordionItem>
165
+ <AccordionItem id="item-2" title="Question 2">
166
+ Answer 2
167
+ </AccordionItem>
168
+ </Accordion>,
169
+ );
170
+ const trigger1 = page.getByRole("button", { name: "Question 1" });
171
+ const trigger2 = page.getByRole("button", { name: "Question 2" });
172
+
173
+ await expect.element(trigger1).toHaveAttribute("aria-expanded", "true");
174
+ await trigger2.click();
175
+ await expect.element(trigger1).toHaveAttribute("aria-expanded", "true");
176
+ await expect.element(trigger2).toHaveAttribute("aria-expanded", "true");
177
+ });
178
+ });
179
+
180
+ describe("Color Schemes", () => {
181
+ test("applies light colorScheme by default", async () => {
182
+ render(
183
+ <Accordion>
184
+ <AccordionItem id="item-1" title="Question 1">
185
+ Answer 1
186
+ </AccordionItem>
187
+ </Accordion>,
188
+ );
189
+ const trigger = page.getByRole("button", { name: "Question 1" });
190
+ await expect.element(trigger).toHaveClass(/text-gray-800/);
191
+ });
192
+
193
+ test("applies dark colorScheme classes", async () => {
194
+ render(
195
+ <Accordion colorScheme="dark">
196
+ <AccordionItem id="item-1" title="Question 1">
197
+ Answer 1
198
+ </AccordionItem>
199
+ </Accordion>,
200
+ );
201
+ const trigger = page.getByRole("button", { name: "Question 1" });
202
+ await expect.element(trigger).toHaveClass(/text-gray-100/);
203
+ });
204
+ });
205
+
206
+ describe("Content", () => {
207
+ test("displays title text in trigger", async () => {
208
+ render(
209
+ <Accordion>
210
+ <AccordionItem id="item-1" title="Custom Title">
211
+ Content here
212
+ </AccordionItem>
213
+ </Accordion>,
214
+ );
215
+ await expect.element(page.getByText("Custom Title")).toBeInTheDocument();
216
+ });
217
+
218
+ test("displays content when expanded", async () => {
219
+ render(
220
+ <Accordion defaultExpanded="item-1">
221
+ <AccordionItem id="item-1" title="Question 1">
222
+ Answer content here
223
+ </AccordionItem>
224
+ </Accordion>,
225
+ );
226
+ await expect
227
+ .element(page.getByText("Answer content here"))
228
+ .toBeInTheDocument();
229
+ });
230
+ });
231
+ });
@@ -12,26 +12,26 @@ import { cn } from "@/lib/utils";
12
12
  const accordionVariants = tv({
13
13
  base: "flex flex-col",
14
14
  variants: {
15
- variant: {
15
+ colorScheme: {
16
16
  dark: "",
17
17
  light: "",
18
18
  },
19
19
  },
20
20
  defaultVariants: {
21
- variant: "dark",
21
+ colorScheme: "light",
22
22
  },
23
23
  });
24
24
 
25
25
  const accordionItemVariants = tv({
26
26
  base: "border-b overflow-hidden",
27
27
  variants: {
28
- variant: {
28
+ colorScheme: {
29
29
  dark: "border-gray-300",
30
30
  light: "border-gray-500",
31
31
  },
32
32
  },
33
33
  defaultVariants: {
34
- variant: "dark",
34
+ colorScheme: "light",
35
35
  },
36
36
  });
37
37
 
@@ -42,13 +42,13 @@ const accordionTriggerVariants = tv({
42
42
  "typography-body-large transition-colors cursor-pointer",
43
43
  ],
44
44
  variants: {
45
- variant: {
45
+ colorScheme: {
46
46
  dark: "text-gray-100 data-[open]:text-white",
47
47
  light: "text-gray-800 data-[open]:text-gray-900",
48
48
  },
49
49
  },
50
50
  defaultVariants: {
51
- variant: "dark",
51
+ colorScheme: "light",
52
52
  },
53
53
  });
54
54
 
@@ -56,13 +56,13 @@ const accordionPanelVariants = tv({
56
56
  // Uses primitive spacing tokens
57
57
  base: "typography-body-large pb-spacing-24",
58
58
  variants: {
59
- variant: {
59
+ colorScheme: {
60
60
  dark: "text-gray-100",
61
61
  light: "text-gray-800",
62
62
  },
63
63
  },
64
64
  defaultVariants: {
65
- variant: "dark",
65
+ colorScheme: "light",
66
66
  },
67
67
  });
68
68
 
@@ -71,9 +71,9 @@ const accordionPanelVariants = tv({
71
71
  // =============================================================================
72
72
 
73
73
  const AccordionContext = React.createContext<{
74
- variant: "dark" | "light";
74
+ colorScheme: "dark" | "light";
75
75
  }>({
76
- variant: "dark",
76
+ colorScheme: "light",
77
77
  });
78
78
 
79
79
  // =============================================================================
@@ -100,8 +100,8 @@ export interface AccordionProps
100
100
  * Built on Base UI's Accordion primitive.
101
101
  *
102
102
  * Variants:
103
- * - dark: Dark theme styling (default)
104
- * - light: Light theme styling
103
+ * - light: Light theme styling (default)
104
+ * - dark: Dark theme styling
105
105
  *
106
106
  * @example
107
107
  * ```tsx
@@ -121,7 +121,7 @@ const Accordion = React.forwardRef<HTMLDivElement, AccordionProps>(
121
121
  className,
122
122
  allowMultiple = false,
123
123
  defaultExpanded,
124
- variant = "light",
124
+ colorScheme = "light",
125
125
  children,
126
126
  },
127
127
  ref,
@@ -134,10 +134,10 @@ const Accordion = React.forwardRef<HTMLDivElement, AccordionProps>(
134
134
  }, [defaultExpanded]);
135
135
 
136
136
  return (
137
- <AccordionContext.Provider value={{ variant: variant ?? "dark" }}>
137
+ <AccordionContext.Provider value={{ colorScheme }}>
138
138
  <BaseAccordion.Root
139
139
  ref={ref}
140
- className={accordionVariants({ variant, class: className })}
140
+ className={accordionVariants({ colorScheme, class: className })}
141
141
  defaultValue={defaultValue}
142
142
  multiple={allowMultiple}
143
143
  >
@@ -175,18 +175,18 @@ export interface AccordionItemProps
175
175
  */
176
176
  const AccordionItem = React.forwardRef<HTMLDivElement, AccordionItemProps>(
177
177
  ({ className, id, title, children }, ref) => {
178
- const { variant } = React.useContext(AccordionContext);
178
+ const { colorScheme } = React.useContext(AccordionContext);
179
179
 
180
180
  return (
181
181
  <BaseAccordion.Item
182
182
  ref={ref}
183
183
  value={id}
184
- className={accordionItemVariants({ variant, class: className })}
184
+ className={accordionItemVariants({ colorScheme, class: className })}
185
185
  >
186
186
  {/* Header - always visible */}
187
187
  <BaseAccordion.Header>
188
188
  <BaseAccordion.Trigger
189
- className={accordionTriggerVariants({ variant })}
189
+ className={accordionTriggerVariants({ colorScheme })}
190
190
  >
191
191
  <span>{title}</span>
192
192
  <span
@@ -208,7 +208,9 @@ const AccordionItem = React.forwardRef<HTMLDivElement, AccordionItemProps>(
208
208
  "[&[data-starting-style]]:h-0 [&[data-ending-style]]:h-0",
209
209
  )}
210
210
  >
211
- <div className={accordionPanelVariants({ variant })}>{children}</div>
211
+ <div className={accordionPanelVariants({ colorScheme })}>
212
+ {children}
213
+ </div>
212
214
  </BaseAccordion.Panel>
213
215
  </BaseAccordion.Item>
214
216
  );
@@ -0,0 +1,213 @@
1
+ import { describe, expect, test } from "vitest";
2
+ import { page } from "vitest/browser";
3
+ import { render } from "vitest-browser-react";
4
+ import {
5
+ Background,
6
+ BackgroundGradient,
7
+ BackgroundImage,
8
+ BackgroundOverlay,
9
+ BackgroundVideo,
10
+ } from "./background";
11
+
12
+ describe("Background", () => {
13
+ describe("Background Container", () => {
14
+ test("renders as a div with absolute positioning", async () => {
15
+ render(<Background data-testid="bg-container">Content</Background>);
16
+ const container = page.getByTestId("bg-container");
17
+ await expect.element(container).toBeInTheDocument();
18
+ await expect.element(container).toHaveClass(/absolute/);
19
+ await expect.element(container).toHaveClass(/inset-0/);
20
+ });
21
+
22
+ test("has aria-hidden attribute", async () => {
23
+ render(<Background data-testid="bg-container" />);
24
+ await expect
25
+ .element(page.getByTestId("bg-container"))
26
+ .toHaveAttribute("aria-hidden", "true");
27
+ });
28
+
29
+ test("accepts custom className", async () => {
30
+ render(<Background data-testid="bg" className="custom-class" />);
31
+ await expect.element(page.getByTestId("bg")).toHaveClass(/custom-class/);
32
+ });
33
+ });
34
+
35
+ describe("BackgroundImage", () => {
36
+ test("renders an img element with src", async () => {
37
+ render(<BackgroundImage src="/test-image.jpg" data-testid="bg-img" />);
38
+ const img = page.getByTestId("bg-img");
39
+ await expect.element(img).toBeInTheDocument();
40
+ await expect.element(img).toHaveAttribute("src", "/test-image.jpg");
41
+ });
42
+
43
+ test("has object-cover class for proper fitting", async () => {
44
+ render(<BackgroundImage src="/test.jpg" data-testid="bg-img" />);
45
+ await expect
46
+ .element(page.getByTestId("bg-img"))
47
+ .toHaveClass(/object-cover/);
48
+ });
49
+
50
+ test("sets default alt to empty string for decorative images", async () => {
51
+ render(<BackgroundImage src="/test.jpg" data-testid="bg-img" />);
52
+ await expect
53
+ .element(page.getByTestId("bg-img"))
54
+ .toHaveAttribute("alt", "");
55
+ });
56
+
57
+ test("accepts custom alt text", async () => {
58
+ render(
59
+ <BackgroundImage
60
+ src="/test.jpg"
61
+ alt="Custom alt text"
62
+ data-testid="bg-img"
63
+ />,
64
+ );
65
+ await expect
66
+ .element(page.getByTestId("bg-img"))
67
+ .toHaveAttribute("alt", "Custom alt text");
68
+ });
69
+
70
+ test("applies custom position via style", async () => {
71
+ render(
72
+ <BackgroundImage
73
+ src="/test.jpg"
74
+ position="top left"
75
+ data-testid="bg-img"
76
+ />,
77
+ );
78
+ const img = page.getByTestId("bg-img");
79
+ // Check the style attribute contains the position
80
+ await expect.element(img).toHaveStyle({ objectPosition: "top left" });
81
+ });
82
+
83
+ test("supports render prop for custom element", async () => {
84
+ render(
85
+ <BackgroundImage
86
+ src="/test.jpg"
87
+ // biome-ignore lint/a11y/useAltText: Test case for custom element
88
+ render={<img className="custom-img-class" />}
89
+ data-testid="bg-img"
90
+ />,
91
+ );
92
+ await expect
93
+ .element(page.getByTestId("bg-img"))
94
+ .toHaveClass(/custom-img-class/);
95
+ });
96
+ });
97
+
98
+ describe("BackgroundVideo", () => {
99
+ test("renders a video element", async () => {
100
+ render(<BackgroundVideo src="/test-video.mp4" data-testid="bg-video" />);
101
+ const video = page.getByTestId("bg-video");
102
+ await expect.element(video).toBeInTheDocument();
103
+ });
104
+
105
+ test("has autoplay and loop by default", async () => {
106
+ render(<BackgroundVideo src="/test.mp4" data-testid="bg-video" />);
107
+ const video = page.getByTestId("bg-video");
108
+ await expect.element(video).toHaveAttribute("autoplay");
109
+ await expect.element(video).toHaveAttribute("loop");
110
+ // Note: muted is a boolean prop in React that may not render as an HTML attribute
111
+ });
112
+
113
+ test("has object-cover class", async () => {
114
+ render(<BackgroundVideo src="/test.mp4" data-testid="bg-video" />);
115
+ await expect
116
+ .element(page.getByTestId("bg-video"))
117
+ .toHaveClass(/object-cover/);
118
+ });
119
+
120
+ test("accepts poster prop", async () => {
121
+ render(
122
+ <BackgroundVideo
123
+ src="/test.mp4"
124
+ poster="/poster.jpg"
125
+ data-testid="bg-video"
126
+ />,
127
+ );
128
+ await expect
129
+ .element(page.getByTestId("bg-video"))
130
+ .toHaveAttribute("poster", "/poster.jpg");
131
+ });
132
+ });
133
+
134
+ describe("BackgroundOverlay", () => {
135
+ test("renders with default opacity", async () => {
136
+ render(<BackgroundOverlay data-testid="bg-overlay" />);
137
+ const overlay = page.getByTestId("bg-overlay");
138
+ await expect.element(overlay).toBeInTheDocument();
139
+ await expect.element(overlay).toHaveStyle({ opacity: "0.4" });
140
+ });
141
+
142
+ test("accepts custom opacity", async () => {
143
+ render(<BackgroundOverlay opacity={0.7} data-testid="bg-overlay" />);
144
+ await expect
145
+ .element(page.getByTestId("bg-overlay"))
146
+ .toHaveStyle({ opacity: "0.7" });
147
+ });
148
+
149
+ test("has aria-hidden attribute", async () => {
150
+ render(<BackgroundOverlay data-testid="bg-overlay" />);
151
+ await expect
152
+ .element(page.getByTestId("bg-overlay"))
153
+ .toHaveAttribute("aria-hidden", "true");
154
+ });
155
+ });
156
+
157
+ describe("BackgroundGradient", () => {
158
+ test("renders with default gradient direction", async () => {
159
+ render(<BackgroundGradient data-testid="bg-gradient" />);
160
+ const gradient = page.getByTestId("bg-gradient");
161
+ await expect.element(gradient).toBeInTheDocument();
162
+ });
163
+
164
+ test("has aria-hidden attribute", async () => {
165
+ render(<BackgroundGradient data-testid="bg-gradient" />);
166
+ await expect
167
+ .element(page.getByTestId("bg-gradient"))
168
+ .toHaveAttribute("aria-hidden", "true");
169
+ });
170
+
171
+ test("accepts custom gradient via gradient prop", async () => {
172
+ render(
173
+ <BackgroundGradient
174
+ gradient="linear-gradient(45deg, red, blue)"
175
+ data-testid="bg-gradient"
176
+ />,
177
+ );
178
+ await expect
179
+ .element(page.getByTestId("bg-gradient"))
180
+ .toHaveStyle({ backgroundImage: "linear-gradient(45deg, red, blue)" });
181
+ });
182
+ });
183
+
184
+ describe("Compound Component", () => {
185
+ test("Background.Image is accessible via dot notation", async () => {
186
+ render(
187
+ <Background data-testid="bg-container">
188
+ <Background.Image src="/test.jpg" data-testid="bg-img" />
189
+ </Background>,
190
+ );
191
+ await expect
192
+ .element(page.getByTestId("bg-container"))
193
+ .toBeInTheDocument();
194
+ await expect.element(page.getByTestId("bg-img")).toBeInTheDocument();
195
+ });
196
+
197
+ test("can compose multiple background layers", async () => {
198
+ render(
199
+ <Background data-testid="bg-container">
200
+ <Background.Image src="/test.jpg" data-testid="bg-img" />
201
+ <Background.Overlay data-testid="bg-overlay" />
202
+ <Background.Gradient data-testid="bg-gradient" />
203
+ </Background>,
204
+ );
205
+ await expect
206
+ .element(page.getByTestId("bg-container"))
207
+ .toBeInTheDocument();
208
+ await expect.element(page.getByTestId("bg-img")).toBeInTheDocument();
209
+ await expect.element(page.getByTestId("bg-overlay")).toBeInTheDocument();
210
+ await expect.element(page.getByTestId("bg-gradient")).toBeInTheDocument();
211
+ });
212
+ });
213
+ });