@nationaldesignstudio/react 0.0.17 → 0.1.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.
- package/dist/component-registry.md +181 -29
- package/dist/components/atoms/accordion/accordion.d.ts +2 -2
- package/dist/components/atoms/background/background.d.ts +158 -0
- package/dist/components/atoms/button/button.d.ts +64 -82
- package/dist/components/atoms/button/icon-button.d.ts +128 -66
- package/dist/components/organisms/card/card.d.ts +130 -4
- package/dist/components/organisms/us-gov-banner/us-gov-banner.d.ts +120 -2
- package/dist/components/sections/hero/hero.d.ts +166 -150
- package/dist/components/sections/quote-block/quote-block.d.ts +152 -0
- package/dist/index.d.ts +6 -2
- package/dist/index.js +4068 -6052
- package/dist/index.js.map +1 -1
- package/dist/lib/utils.d.ts +1 -2
- package/dist/tokens.css +207 -16
- package/package.json +2 -4
- package/src/components/atoms/accordion/accordion.test.tsx +233 -0
- package/src/components/atoms/accordion/accordion.tsx +8 -8
- package/src/components/atoms/background/background.test.tsx +213 -0
- package/src/components/atoms/background/background.tsx +435 -0
- package/src/components/atoms/background/index.ts +22 -0
- package/src/components/atoms/button/button.stories.tsx +81 -32
- package/src/components/atoms/button/button.tsx +101 -49
- package/src/components/atoms/button/icon-button.stories.tsx +179 -28
- package/src/components/atoms/button/icon-button.test.tsx +254 -0
- package/src/components/atoms/button/icon-button.tsx +178 -59
- package/src/components/atoms/pager-control/pager-control.tsx +32 -3
- package/src/components/dev-tools/dev-toolbar/dev-toolbar.tsx +2 -0
- package/src/components/organisms/card/card.tsx +82 -24
- package/src/components/organisms/card/index.ts +7 -0
- package/src/components/organisms/navbar/navbar.tsx +2 -0
- package/src/components/organisms/us-gov-banner/index.ts +5 -1
- package/src/components/organisms/us-gov-banner/us-gov-banner.tsx +72 -16
- package/src/components/sections/hero/hero.stories.tsx +124 -1
- package/src/components/sections/hero/hero.test.tsx +21 -18
- package/src/components/sections/hero/hero.tsx +188 -301
- package/src/components/sections/hero/index.ts +13 -0
- package/src/components/sections/quote-block/index.ts +5 -0
- package/src/components/sections/quote-block/quote-block.tsx +216 -0
- package/src/index.ts +40 -0
- package/src/lib/utils.ts +1 -6
- package/src/stories/ThemeProvider.stories.tsx +11 -5
|
@@ -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, loop, and muted 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
|
+
await expect.element(video).toHaveAttribute("muted");
|
|
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
|
+
});
|
|
@@ -0,0 +1,435 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useRender } from "@base-ui-components/react/use-render";
|
|
4
|
+
import * as React from "react";
|
|
5
|
+
import { tv } from "tailwind-variants";
|
|
6
|
+
|
|
7
|
+
// =============================================================================
|
|
8
|
+
// Background Atomic Component
|
|
9
|
+
// =============================================================================
|
|
10
|
+
|
|
11
|
+
const backgroundVariants = tv({
|
|
12
|
+
base: "absolute inset-0",
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Base container for background composition.
|
|
17
|
+
* Use as a wrapper to compose multiple background layers (image, video, overlay, gradient).
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* ```tsx
|
|
21
|
+
* <Background>
|
|
22
|
+
* <Background.Image src="/hero.jpg" />
|
|
23
|
+
* <Background.Overlay opacity={0.4} />
|
|
24
|
+
* <Background.Gradient direction="to-t" from="black" to="transparent" />
|
|
25
|
+
* </Background>
|
|
26
|
+
* ```
|
|
27
|
+
*/
|
|
28
|
+
export interface BackgroundProps extends React.HTMLAttributes<HTMLDivElement> {}
|
|
29
|
+
|
|
30
|
+
const Background = React.forwardRef<HTMLDivElement, BackgroundProps>(
|
|
31
|
+
({ className, children, ...props }, ref) => (
|
|
32
|
+
<div
|
|
33
|
+
ref={ref}
|
|
34
|
+
aria-hidden="true"
|
|
35
|
+
className={backgroundVariants({ class: className })}
|
|
36
|
+
{...props}
|
|
37
|
+
>
|
|
38
|
+
{children}
|
|
39
|
+
</div>
|
|
40
|
+
),
|
|
41
|
+
);
|
|
42
|
+
Background.displayName = "Background";
|
|
43
|
+
|
|
44
|
+
// =============================================================================
|
|
45
|
+
// Background.Image
|
|
46
|
+
// =============================================================================
|
|
47
|
+
|
|
48
|
+
const backgroundImageVariants = tv({
|
|
49
|
+
base: "absolute inset-0 size-full object-cover",
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
export interface BackgroundImageProps
|
|
53
|
+
extends Omit<React.ImgHTMLAttributes<HTMLImageElement>, "src"> {
|
|
54
|
+
/**
|
|
55
|
+
* URL for the background image
|
|
56
|
+
*/
|
|
57
|
+
src: string;
|
|
58
|
+
/**
|
|
59
|
+
* Object position (default: "center")
|
|
60
|
+
*/
|
|
61
|
+
position?: string;
|
|
62
|
+
/**
|
|
63
|
+
* Custom render prop for element composition.
|
|
64
|
+
* Accepts a React element or render function.
|
|
65
|
+
* @example
|
|
66
|
+
* ```tsx
|
|
67
|
+
* // Element pattern
|
|
68
|
+
* <BackgroundImage render={<img className="custom" />} src="/bg.jpg" />
|
|
69
|
+
*
|
|
70
|
+
* // Callback pattern
|
|
71
|
+
* <BackgroundImage render={(props) => <img {...props} />} src="/bg.jpg" />
|
|
72
|
+
* ```
|
|
73
|
+
*/
|
|
74
|
+
render?:
|
|
75
|
+
| React.ReactElement
|
|
76
|
+
| ((
|
|
77
|
+
props: React.ImgHTMLAttributes<HTMLImageElement>,
|
|
78
|
+
) => React.ReactElement);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Background image layer using an actual img element with object-cover.
|
|
83
|
+
* Supports native lazy loading, srcset, and better accessibility.
|
|
84
|
+
* Supports render prop for element composition.
|
|
85
|
+
*/
|
|
86
|
+
const BackgroundImage = React.forwardRef<
|
|
87
|
+
HTMLImageElement,
|
|
88
|
+
BackgroundImageProps
|
|
89
|
+
>(
|
|
90
|
+
(
|
|
91
|
+
{ className, src, position = "center", alt = "", style, render, ...props },
|
|
92
|
+
ref,
|
|
93
|
+
) => {
|
|
94
|
+
const mergedProps = {
|
|
95
|
+
src,
|
|
96
|
+
alt,
|
|
97
|
+
className: backgroundImageVariants({ class: className }),
|
|
98
|
+
style: {
|
|
99
|
+
objectPosition: position,
|
|
100
|
+
...style,
|
|
101
|
+
},
|
|
102
|
+
...props,
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
const element = useRender({
|
|
106
|
+
// biome-ignore lint/a11y/useAltText: alt is provided via mergedProps
|
|
107
|
+
render: render ?? <img />,
|
|
108
|
+
ref,
|
|
109
|
+
props: mergedProps,
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
return element;
|
|
113
|
+
},
|
|
114
|
+
);
|
|
115
|
+
BackgroundImage.displayName = "Background.Image";
|
|
116
|
+
|
|
117
|
+
// =============================================================================
|
|
118
|
+
// Background.Video
|
|
119
|
+
// =============================================================================
|
|
120
|
+
|
|
121
|
+
const backgroundVideoVariants = tv({
|
|
122
|
+
base: "absolute inset-0 size-full object-cover",
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
export interface BackgroundVideoProps
|
|
126
|
+
extends Omit<React.VideoHTMLAttributes<HTMLVideoElement>, "children"> {
|
|
127
|
+
/**
|
|
128
|
+
* URL for the video source
|
|
129
|
+
*/
|
|
130
|
+
src: string;
|
|
131
|
+
/**
|
|
132
|
+
* Video MIME type (default: auto-detected from src)
|
|
133
|
+
*/
|
|
134
|
+
type?: string;
|
|
135
|
+
/**
|
|
136
|
+
* Poster image URL shown before video loads
|
|
137
|
+
*/
|
|
138
|
+
poster?: string;
|
|
139
|
+
/**
|
|
140
|
+
* Custom render prop for element composition.
|
|
141
|
+
* @example
|
|
142
|
+
* ```tsx
|
|
143
|
+
* <BackgroundVideo render={<video className="custom" />} src="/bg.mp4" />
|
|
144
|
+
* ```
|
|
145
|
+
*/
|
|
146
|
+
render?:
|
|
147
|
+
| React.ReactElement
|
|
148
|
+
| ((
|
|
149
|
+
props: React.VideoHTMLAttributes<HTMLVideoElement>,
|
|
150
|
+
) => React.ReactElement);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Background video layer using HTML5 video element.
|
|
155
|
+
* Supports render prop for element composition.
|
|
156
|
+
*/
|
|
157
|
+
const BackgroundVideo = React.forwardRef<
|
|
158
|
+
HTMLVideoElement,
|
|
159
|
+
BackgroundVideoProps
|
|
160
|
+
>(
|
|
161
|
+
(
|
|
162
|
+
{
|
|
163
|
+
className,
|
|
164
|
+
src,
|
|
165
|
+
type,
|
|
166
|
+
poster,
|
|
167
|
+
autoPlay = true,
|
|
168
|
+
loop = true,
|
|
169
|
+
muted = true,
|
|
170
|
+
playsInline = true,
|
|
171
|
+
render,
|
|
172
|
+
...props
|
|
173
|
+
},
|
|
174
|
+
ref,
|
|
175
|
+
) => {
|
|
176
|
+
const mergedProps = {
|
|
177
|
+
autoPlay,
|
|
178
|
+
loop,
|
|
179
|
+
muted,
|
|
180
|
+
playsInline,
|
|
181
|
+
poster,
|
|
182
|
+
className: backgroundVideoVariants({ class: className }),
|
|
183
|
+
...props,
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
const element = useRender({
|
|
187
|
+
render: render ?? (
|
|
188
|
+
// biome-ignore lint/a11y/useMediaCaption: Background videos are decorative and shouldn't have captions
|
|
189
|
+
<video>
|
|
190
|
+
<source src={src} type={type} />
|
|
191
|
+
</video>
|
|
192
|
+
),
|
|
193
|
+
ref,
|
|
194
|
+
props: mergedProps,
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
return element;
|
|
198
|
+
},
|
|
199
|
+
);
|
|
200
|
+
BackgroundVideo.displayName = "Background.Video";
|
|
201
|
+
|
|
202
|
+
// =============================================================================
|
|
203
|
+
// Background.Stream
|
|
204
|
+
// =============================================================================
|
|
205
|
+
|
|
206
|
+
const backgroundStreamVariants = tv({
|
|
207
|
+
base: "absolute inset-0 size-full border-0 scale-[1.5] object-cover",
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
export interface BackgroundStreamProps
|
|
211
|
+
extends React.IframeHTMLAttributes<HTMLIFrameElement> {
|
|
212
|
+
/**
|
|
213
|
+
* Cloudflare Stream video ID
|
|
214
|
+
*/
|
|
215
|
+
videoId: string;
|
|
216
|
+
/**
|
|
217
|
+
* Poster image URL (Cloudflare Stream thumbnail or custom)
|
|
218
|
+
*/
|
|
219
|
+
poster?: string;
|
|
220
|
+
/**
|
|
221
|
+
* Whether the video should autoplay (default: true)
|
|
222
|
+
*/
|
|
223
|
+
autoplay?: boolean;
|
|
224
|
+
/**
|
|
225
|
+
* Whether the video should loop (default: true)
|
|
226
|
+
*/
|
|
227
|
+
loop?: boolean;
|
|
228
|
+
/**
|
|
229
|
+
* Whether the video should be muted (default: true)
|
|
230
|
+
*/
|
|
231
|
+
muted?: boolean;
|
|
232
|
+
/**
|
|
233
|
+
* Whether to show playback controls (default: false)
|
|
234
|
+
*/
|
|
235
|
+
controls?: boolean;
|
|
236
|
+
/**
|
|
237
|
+
* Custom Cloudflare customer subdomain (if using custom domains)
|
|
238
|
+
*/
|
|
239
|
+
customerSubdomain?: string;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Background video layer using Cloudflare Stream.
|
|
244
|
+
*/
|
|
245
|
+
const BackgroundStream = React.forwardRef<
|
|
246
|
+
HTMLIFrameElement,
|
|
247
|
+
BackgroundStreamProps
|
|
248
|
+
>(
|
|
249
|
+
(
|
|
250
|
+
{
|
|
251
|
+
className,
|
|
252
|
+
videoId,
|
|
253
|
+
poster,
|
|
254
|
+
autoplay = true,
|
|
255
|
+
loop = true,
|
|
256
|
+
muted = true,
|
|
257
|
+
controls = false,
|
|
258
|
+
customerSubdomain,
|
|
259
|
+
title = "Background video",
|
|
260
|
+
...props
|
|
261
|
+
},
|
|
262
|
+
ref,
|
|
263
|
+
) => {
|
|
264
|
+
const baseUrl = customerSubdomain
|
|
265
|
+
? `https://${customerSubdomain}.cloudflarestream.com`
|
|
266
|
+
: "https://iframe.videodelivery.net";
|
|
267
|
+
|
|
268
|
+
const params = new URLSearchParams();
|
|
269
|
+
if (autoplay) params.set("autoplay", "true");
|
|
270
|
+
if (loop) params.set("loop", "true");
|
|
271
|
+
if (muted) params.set("muted", "true");
|
|
272
|
+
if (!controls) params.set("controls", "false");
|
|
273
|
+
if (poster) params.set("poster", poster);
|
|
274
|
+
params.set("preload", "auto");
|
|
275
|
+
|
|
276
|
+
const streamUrl = `${baseUrl}/${videoId}?${params.toString()}`;
|
|
277
|
+
|
|
278
|
+
return (
|
|
279
|
+
<iframe
|
|
280
|
+
ref={ref}
|
|
281
|
+
src={streamUrl}
|
|
282
|
+
title={title}
|
|
283
|
+
allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture"
|
|
284
|
+
allowFullScreen
|
|
285
|
+
className={backgroundStreamVariants({ class: className })}
|
|
286
|
+
{...props}
|
|
287
|
+
/>
|
|
288
|
+
);
|
|
289
|
+
},
|
|
290
|
+
);
|
|
291
|
+
BackgroundStream.displayName = "Background.Stream";
|
|
292
|
+
|
|
293
|
+
// =============================================================================
|
|
294
|
+
// Background.Overlay
|
|
295
|
+
// =============================================================================
|
|
296
|
+
|
|
297
|
+
const backgroundOverlayVariants = tv({
|
|
298
|
+
base: "absolute inset-0 bg-bg-overlay",
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
export interface BackgroundOverlayProps
|
|
302
|
+
extends React.HTMLAttributes<HTMLDivElement> {
|
|
303
|
+
/**
|
|
304
|
+
* Overlay opacity (0-1)
|
|
305
|
+
*/
|
|
306
|
+
opacity?: number;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Solid color overlay layer. Uses semantic bg-overlay token by default.
|
|
311
|
+
* Override with className for different colors.
|
|
312
|
+
*/
|
|
313
|
+
const BackgroundOverlay = React.forwardRef<
|
|
314
|
+
HTMLDivElement,
|
|
315
|
+
BackgroundOverlayProps
|
|
316
|
+
>(({ className, opacity = 0.4, style, ...props }, ref) => (
|
|
317
|
+
<div
|
|
318
|
+
ref={ref}
|
|
319
|
+
aria-hidden="true"
|
|
320
|
+
className={backgroundOverlayVariants({ class: className })}
|
|
321
|
+
style={{
|
|
322
|
+
opacity,
|
|
323
|
+
...style,
|
|
324
|
+
}}
|
|
325
|
+
{...props}
|
|
326
|
+
/>
|
|
327
|
+
));
|
|
328
|
+
BackgroundOverlay.displayName = "Background.Overlay";
|
|
329
|
+
|
|
330
|
+
// =============================================================================
|
|
331
|
+
// Background.Gradient
|
|
332
|
+
// =============================================================================
|
|
333
|
+
|
|
334
|
+
const backgroundGradientVariants = tv({
|
|
335
|
+
base: "absolute inset-0",
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
export interface BackgroundGradientProps
|
|
339
|
+
extends React.HTMLAttributes<HTMLDivElement> {
|
|
340
|
+
/**
|
|
341
|
+
* Gradient direction (Tailwind convention: to-t, to-b, to-l, to-r, etc.)
|
|
342
|
+
* Or CSS gradient direction (to top, to bottom, 45deg, etc.)
|
|
343
|
+
*/
|
|
344
|
+
direction?: string;
|
|
345
|
+
/**
|
|
346
|
+
* Starting color (from)
|
|
347
|
+
*/
|
|
348
|
+
from?: string;
|
|
349
|
+
/**
|
|
350
|
+
* Optional middle color (via)
|
|
351
|
+
*/
|
|
352
|
+
via?: string;
|
|
353
|
+
/**
|
|
354
|
+
* Ending color (to)
|
|
355
|
+
*/
|
|
356
|
+
to?: string;
|
|
357
|
+
/**
|
|
358
|
+
* Full custom gradient string (overrides from/via/to)
|
|
359
|
+
*/
|
|
360
|
+
gradient?: string;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* Gradient overlay layer. Use for fading backgrounds or creating depth.
|
|
365
|
+
*/
|
|
366
|
+
const BackgroundGradient = React.forwardRef<
|
|
367
|
+
HTMLDivElement,
|
|
368
|
+
BackgroundGradientProps
|
|
369
|
+
>(
|
|
370
|
+
(
|
|
371
|
+
{
|
|
372
|
+
className,
|
|
373
|
+
direction = "to-b",
|
|
374
|
+
from = "transparent",
|
|
375
|
+
via,
|
|
376
|
+
to = "black",
|
|
377
|
+
gradient,
|
|
378
|
+
style,
|
|
379
|
+
...props
|
|
380
|
+
},
|
|
381
|
+
ref,
|
|
382
|
+
) => {
|
|
383
|
+
// Convert Tailwind-style direction to CSS
|
|
384
|
+
const cssDirection = direction.startsWith("to-")
|
|
385
|
+
? direction.replace("to-", "to ")
|
|
386
|
+
: direction;
|
|
387
|
+
|
|
388
|
+
const gradientValue =
|
|
389
|
+
gradient ||
|
|
390
|
+
(via
|
|
391
|
+
? `linear-gradient(${cssDirection}, ${from}, ${via}, ${to})`
|
|
392
|
+
: `linear-gradient(${cssDirection}, ${from}, ${to})`);
|
|
393
|
+
|
|
394
|
+
return (
|
|
395
|
+
<div
|
|
396
|
+
ref={ref}
|
|
397
|
+
aria-hidden="true"
|
|
398
|
+
className={backgroundGradientVariants({ class: className })}
|
|
399
|
+
style={{
|
|
400
|
+
backgroundImage: gradientValue,
|
|
401
|
+
...style,
|
|
402
|
+
}}
|
|
403
|
+
{...props}
|
|
404
|
+
/>
|
|
405
|
+
);
|
|
406
|
+
},
|
|
407
|
+
);
|
|
408
|
+
BackgroundGradient.displayName = "Background.Gradient";
|
|
409
|
+
|
|
410
|
+
// =============================================================================
|
|
411
|
+
// Compound Export
|
|
412
|
+
// =============================================================================
|
|
413
|
+
|
|
414
|
+
const BackgroundCompound = Object.assign(Background, {
|
|
415
|
+
Image: BackgroundImage,
|
|
416
|
+
Video: BackgroundVideo,
|
|
417
|
+
Stream: BackgroundStream,
|
|
418
|
+
Overlay: BackgroundOverlay,
|
|
419
|
+
Gradient: BackgroundGradient,
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
export {
|
|
423
|
+
BackgroundCompound as Background,
|
|
424
|
+
BackgroundImage,
|
|
425
|
+
BackgroundVideo,
|
|
426
|
+
BackgroundStream,
|
|
427
|
+
BackgroundOverlay,
|
|
428
|
+
BackgroundGradient,
|
|
429
|
+
backgroundVariants,
|
|
430
|
+
backgroundImageVariants,
|
|
431
|
+
backgroundVideoVariants,
|
|
432
|
+
backgroundStreamVariants,
|
|
433
|
+
backgroundOverlayVariants,
|
|
434
|
+
backgroundGradientVariants,
|
|
435
|
+
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export type {
|
|
2
|
+
BackgroundGradientProps,
|
|
3
|
+
BackgroundImageProps,
|
|
4
|
+
BackgroundOverlayProps,
|
|
5
|
+
BackgroundProps,
|
|
6
|
+
BackgroundStreamProps,
|
|
7
|
+
BackgroundVideoProps,
|
|
8
|
+
} from "./background";
|
|
9
|
+
export {
|
|
10
|
+
Background,
|
|
11
|
+
BackgroundGradient,
|
|
12
|
+
BackgroundImage,
|
|
13
|
+
BackgroundOverlay,
|
|
14
|
+
BackgroundStream,
|
|
15
|
+
BackgroundVideo,
|
|
16
|
+
backgroundGradientVariants,
|
|
17
|
+
backgroundImageVariants,
|
|
18
|
+
backgroundOverlayVariants,
|
|
19
|
+
backgroundStreamVariants,
|
|
20
|
+
backgroundVariants,
|
|
21
|
+
backgroundVideoVariants,
|
|
22
|
+
} from "./background";
|