@nationaldesignstudio/react 0.0.19 → 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/components/atoms/accordion/accordion.d.ts +2 -2
- package/dist/components/atoms/background/background.d.ts +23 -0
- package/dist/components/atoms/button/icon-button.d.ts +44 -16
- package/dist/index.js +2140 -2014
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- 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 +85 -27
- package/src/components/atoms/button/button.tsx +10 -0
- package/src/components/atoms/button/icon-button.test.tsx +254 -0
- package/src/components/atoms/button/icon-button.tsx +79 -22
- 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/navbar/navbar.tsx +2 -0
- package/src/stories/ThemeProvider.stories.tsx +9 -3
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
import { describe, expect, test, vi } from "vitest";
|
|
2
|
+
import { page, userEvent } from "vitest/browser";
|
|
3
|
+
import { render } from "vitest-browser-react";
|
|
4
|
+
import { IconButton } from "./icon-button";
|
|
5
|
+
|
|
6
|
+
// Simple icon for testing
|
|
7
|
+
const TestIcon = () => (
|
|
8
|
+
// biome-ignore lint/a11y/noSvgWithoutTitle: Test component doesn't need accessibility title
|
|
9
|
+
<svg data-testid="test-icon" width="24" height="24" viewBox="0 0 24 24">
|
|
10
|
+
<path d="M12 2L2 7l10 5 10-5-10-5z" />
|
|
11
|
+
</svg>
|
|
12
|
+
);
|
|
13
|
+
|
|
14
|
+
describe("IconButton", () => {
|
|
15
|
+
describe("Accessibility", () => {
|
|
16
|
+
test("has correct button role", async () => {
|
|
17
|
+
render(
|
|
18
|
+
<IconButton aria-label="Test action">
|
|
19
|
+
<TestIcon />
|
|
20
|
+
</IconButton>,
|
|
21
|
+
);
|
|
22
|
+
await expect
|
|
23
|
+
.element(page.getByRole("button", { name: "Test action" }))
|
|
24
|
+
.toBeInTheDocument();
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test("is focusable via keyboard", async () => {
|
|
28
|
+
render(
|
|
29
|
+
<IconButton aria-label="Focusable">
|
|
30
|
+
<TestIcon />
|
|
31
|
+
</IconButton>,
|
|
32
|
+
);
|
|
33
|
+
await userEvent.keyboard("{Tab}");
|
|
34
|
+
await expect
|
|
35
|
+
.element(page.getByRole("button", { name: "Focusable" }))
|
|
36
|
+
.toHaveFocus();
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test("disabled button has disabled attribute", async () => {
|
|
40
|
+
render(
|
|
41
|
+
<IconButton disabled aria-label="Disabled">
|
|
42
|
+
<TestIcon />
|
|
43
|
+
</IconButton>,
|
|
44
|
+
);
|
|
45
|
+
await expect
|
|
46
|
+
.element(page.getByRole("button", { name: "Disabled" }))
|
|
47
|
+
.toBeDisabled();
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test("disabled button is not focusable", async () => {
|
|
51
|
+
render(
|
|
52
|
+
<>
|
|
53
|
+
<IconButton disabled aria-label="Disabled">
|
|
54
|
+
<TestIcon />
|
|
55
|
+
</IconButton>
|
|
56
|
+
<IconButton aria-label="After">
|
|
57
|
+
<TestIcon />
|
|
58
|
+
</IconButton>
|
|
59
|
+
</>,
|
|
60
|
+
);
|
|
61
|
+
await userEvent.keyboard("{Tab}");
|
|
62
|
+
await expect
|
|
63
|
+
.element(page.getByRole("button", { name: "After" }))
|
|
64
|
+
.toHaveFocus();
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test("renders icon inside button", async () => {
|
|
68
|
+
render(
|
|
69
|
+
<IconButton aria-label="With icon">
|
|
70
|
+
<TestIcon />
|
|
71
|
+
</IconButton>,
|
|
72
|
+
);
|
|
73
|
+
const icon = page.getByTestId("test-icon");
|
|
74
|
+
await expect.element(icon).toBeInTheDocument();
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
describe("Interactions", () => {
|
|
79
|
+
test("calls onClick when clicked", async () => {
|
|
80
|
+
const handleClick = vi.fn();
|
|
81
|
+
render(
|
|
82
|
+
<IconButton onClick={handleClick} aria-label="Clickable">
|
|
83
|
+
<TestIcon />
|
|
84
|
+
</IconButton>,
|
|
85
|
+
);
|
|
86
|
+
await page.getByRole("button", { name: "Clickable" }).click();
|
|
87
|
+
expect(handleClick).toHaveBeenCalledOnce();
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test("responds to Enter key when focused", async () => {
|
|
91
|
+
const handleClick = vi.fn();
|
|
92
|
+
render(
|
|
93
|
+
<IconButton onClick={handleClick} aria-label="Enter key">
|
|
94
|
+
<TestIcon />
|
|
95
|
+
</IconButton>,
|
|
96
|
+
);
|
|
97
|
+
page.getByRole("button", { name: "Enter key" }).element().focus();
|
|
98
|
+
await userEvent.keyboard("{Enter}");
|
|
99
|
+
expect(handleClick).toHaveBeenCalledOnce();
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test("responds to Space key when focused", async () => {
|
|
103
|
+
const handleClick = vi.fn();
|
|
104
|
+
render(
|
|
105
|
+
<IconButton onClick={handleClick} aria-label="Space key">
|
|
106
|
+
<TestIcon />
|
|
107
|
+
</IconButton>,
|
|
108
|
+
);
|
|
109
|
+
page.getByRole("button", { name: "Space key" }).element().focus();
|
|
110
|
+
await userEvent.keyboard(" ");
|
|
111
|
+
expect(handleClick).toHaveBeenCalledOnce();
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test("does not fire onClick when disabled", async () => {
|
|
115
|
+
const handleClick = vi.fn();
|
|
116
|
+
render(
|
|
117
|
+
<IconButton disabled onClick={handleClick} aria-label="Disabled">
|
|
118
|
+
<TestIcon />
|
|
119
|
+
</IconButton>,
|
|
120
|
+
);
|
|
121
|
+
await page
|
|
122
|
+
.getByRole("button", { name: "Disabled" })
|
|
123
|
+
.click({ force: true });
|
|
124
|
+
expect(handleClick).not.toHaveBeenCalled();
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
describe("render prop", () => {
|
|
129
|
+
test("renders as anchor element when render prop is used", async () => {
|
|
130
|
+
render(
|
|
131
|
+
// biome-ignore lint/a11y/useAnchorContent: Content is provided via IconButton children
|
|
132
|
+
<IconButton render={<a href="/test" />} aria-label="Link button">
|
|
133
|
+
<TestIcon />
|
|
134
|
+
</IconButton>,
|
|
135
|
+
);
|
|
136
|
+
const element = page.getByRole("link", { name: "Link button" });
|
|
137
|
+
await expect.element(element).toBeInTheDocument();
|
|
138
|
+
await expect.element(element).toHaveAttribute("href", "/test");
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
describe("Data attributes", () => {
|
|
143
|
+
test("includes data-variant attribute", async () => {
|
|
144
|
+
render(
|
|
145
|
+
<IconButton variant="ghost" aria-label="Ghost variant">
|
|
146
|
+
<TestIcon />
|
|
147
|
+
</IconButton>,
|
|
148
|
+
);
|
|
149
|
+
await expect
|
|
150
|
+
.element(page.getByRole("button", { name: "Ghost variant" }))
|
|
151
|
+
.toHaveAttribute("data-variant", "ghost");
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
test("includes data-size attribute", async () => {
|
|
155
|
+
render(
|
|
156
|
+
<IconButton size="lg" aria-label="Large size">
|
|
157
|
+
<TestIcon />
|
|
158
|
+
</IconButton>,
|
|
159
|
+
);
|
|
160
|
+
await expect
|
|
161
|
+
.element(page.getByRole("button", { name: "Large size" }))
|
|
162
|
+
.toHaveAttribute("data-size", "lg");
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
test("includes data-color-scheme attribute", async () => {
|
|
166
|
+
render(
|
|
167
|
+
<IconButton colorScheme="light" aria-label="Light scheme">
|
|
168
|
+
<TestIcon />
|
|
169
|
+
</IconButton>,
|
|
170
|
+
);
|
|
171
|
+
await expect
|
|
172
|
+
.element(page.getByRole("button", { name: "Light scheme" }))
|
|
173
|
+
.toHaveAttribute("data-color-scheme", "light");
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
test("includes data-rounded attribute", async () => {
|
|
177
|
+
render(
|
|
178
|
+
<IconButton rounded="full" aria-label="Full rounded">
|
|
179
|
+
<TestIcon />
|
|
180
|
+
</IconButton>,
|
|
181
|
+
);
|
|
182
|
+
await expect
|
|
183
|
+
.element(page.getByRole("button", { name: "Full rounded" }))
|
|
184
|
+
.toHaveAttribute("data-rounded", "full");
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
test("has default data attributes when not specified", async () => {
|
|
188
|
+
render(
|
|
189
|
+
<IconButton aria-label="Defaults">
|
|
190
|
+
<TestIcon />
|
|
191
|
+
</IconButton>,
|
|
192
|
+
);
|
|
193
|
+
const button = page.getByRole("button", { name: "Defaults" });
|
|
194
|
+
await expect.element(button).toHaveAttribute("data-variant", "solid");
|
|
195
|
+
await expect.element(button).toHaveAttribute("data-size", "default");
|
|
196
|
+
await expect.element(button).toHaveAttribute("data-color-scheme", "dark");
|
|
197
|
+
await expect.element(button).toHaveAttribute("data-rounded", "default");
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
describe("Variants", () => {
|
|
202
|
+
test("applies solid variant classes by default", async () => {
|
|
203
|
+
render(
|
|
204
|
+
<IconButton aria-label="Default">
|
|
205
|
+
<TestIcon />
|
|
206
|
+
</IconButton>,
|
|
207
|
+
);
|
|
208
|
+
const button = page.getByRole("button", { name: "Default" });
|
|
209
|
+
await expect.element(button).toHaveClass(/bg-gray-1200/);
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
test("applies ghost variant classes", async () => {
|
|
213
|
+
render(
|
|
214
|
+
<IconButton variant="ghost" aria-label="Ghost">
|
|
215
|
+
<TestIcon />
|
|
216
|
+
</IconButton>,
|
|
217
|
+
);
|
|
218
|
+
const button = page.getByRole("button", { name: "Ghost" });
|
|
219
|
+
await expect.element(button).toHaveClass(/text-gray-700/);
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
describe("Sizes", () => {
|
|
224
|
+
test("applies small size classes", async () => {
|
|
225
|
+
render(
|
|
226
|
+
<IconButton size="sm" aria-label="Small">
|
|
227
|
+
<TestIcon />
|
|
228
|
+
</IconButton>,
|
|
229
|
+
);
|
|
230
|
+
const button = page.getByRole("button", { name: "Small" });
|
|
231
|
+
await expect.element(button).toHaveClass(/size-32/);
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
test("applies default size classes", async () => {
|
|
235
|
+
render(
|
|
236
|
+
<IconButton size="default" aria-label="Default">
|
|
237
|
+
<TestIcon />
|
|
238
|
+
</IconButton>,
|
|
239
|
+
);
|
|
240
|
+
const button = page.getByRole("button", { name: "Default" });
|
|
241
|
+
await expect.element(button).toHaveClass(/size-40/);
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
test("applies large size classes", async () => {
|
|
245
|
+
render(
|
|
246
|
+
<IconButton size="lg" aria-label="Large">
|
|
247
|
+
<TestIcon />
|
|
248
|
+
</IconButton>,
|
|
249
|
+
);
|
|
250
|
+
const button = page.getByRole("button", { name: "Large" });
|
|
251
|
+
await expect.element(button).toHaveClass(/size-48/);
|
|
252
|
+
});
|
|
253
|
+
});
|
|
254
|
+
});
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useRender } from "@base-ui-components/react/use-render";
|
|
2
4
|
import * as React from "react";
|
|
3
5
|
import { tv, type VariantProps } from "tailwind-variants";
|
|
4
6
|
|
|
@@ -30,9 +32,9 @@ import { tv, type VariantProps } from "tailwind-variants";
|
|
|
30
32
|
* - light: Light colors for use on dark backgrounds
|
|
31
33
|
*
|
|
32
34
|
* Sizes:
|
|
33
|
-
* - lg: Large (48x48)
|
|
34
|
-
* - default: Medium (40x40)
|
|
35
35
|
* - sm: Small (32x32)
|
|
36
|
+
* - default: Medium (40x40)
|
|
37
|
+
* - lg: Large (48x48)
|
|
36
38
|
*
|
|
37
39
|
* Rounded:
|
|
38
40
|
* - default: Standard border radius
|
|
@@ -53,9 +55,9 @@ const iconButtonVariants = tv({
|
|
|
53
55
|
light: "",
|
|
54
56
|
},
|
|
55
57
|
size: {
|
|
56
|
-
lg: "size-48",
|
|
57
|
-
default: "size-40",
|
|
58
58
|
sm: "size-32",
|
|
59
|
+
default: "size-40",
|
|
60
|
+
lg: "size-48",
|
|
59
61
|
},
|
|
60
62
|
rounded: {
|
|
61
63
|
default: "rounded-radius-12",
|
|
@@ -132,6 +134,38 @@ const iconButtonVariants = tv({
|
|
|
132
134
|
export interface IconButtonProps
|
|
133
135
|
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
|
134
136
|
VariantProps<typeof iconButtonVariants> {
|
|
137
|
+
/**
|
|
138
|
+
* Custom render prop for element composition.
|
|
139
|
+
* Accepts a React element or render function.
|
|
140
|
+
* @example
|
|
141
|
+
* ```tsx
|
|
142
|
+
* // Render as a link
|
|
143
|
+
* <IconButton render={<a href="/contact" />} aria-label="Contact">
|
|
144
|
+
* <LinkIcon />
|
|
145
|
+
* </IconButton>
|
|
146
|
+
*
|
|
147
|
+
* // Render with custom element
|
|
148
|
+
* <IconButton render={(props) => <Link {...props} to="/home" />} aria-label="Home">
|
|
149
|
+
* <HomeIcon />
|
|
150
|
+
* </IconButton>
|
|
151
|
+
* ```
|
|
152
|
+
*/
|
|
153
|
+
render?:
|
|
154
|
+
| React.ReactElement
|
|
155
|
+
| ((
|
|
156
|
+
props: React.ButtonHTMLAttributes<HTMLButtonElement>,
|
|
157
|
+
) => React.ReactElement);
|
|
158
|
+
/**
|
|
159
|
+
* @deprecated Use `render` prop instead for element composition.
|
|
160
|
+
* @example
|
|
161
|
+
* ```tsx
|
|
162
|
+
* // Old (deprecated)
|
|
163
|
+
* <IconButton asChild><a href="/link">...</a></IconButton>
|
|
164
|
+
*
|
|
165
|
+
* // New (recommended)
|
|
166
|
+
* <IconButton render={<a href="/link" />}>...</IconButton>
|
|
167
|
+
* ```
|
|
168
|
+
*/
|
|
135
169
|
asChild?: boolean;
|
|
136
170
|
}
|
|
137
171
|
|
|
@@ -143,14 +177,16 @@ const IconButton = React.forwardRef<HTMLButtonElement, IconButtonProps>(
|
|
|
143
177
|
colorScheme,
|
|
144
178
|
size,
|
|
145
179
|
rounded,
|
|
146
|
-
|
|
180
|
+
render,
|
|
181
|
+
asChild,
|
|
147
182
|
...props
|
|
148
183
|
},
|
|
149
184
|
ref,
|
|
150
185
|
) => {
|
|
151
|
-
// Development
|
|
186
|
+
// Development warnings
|
|
152
187
|
React.useEffect(() => {
|
|
153
188
|
if (import.meta.env?.DEV) {
|
|
189
|
+
// Warn about missing accessible label
|
|
154
190
|
const hasAccessibleLabel =
|
|
155
191
|
props["aria-label"] || props["aria-labelledby"] || props.title;
|
|
156
192
|
if (!hasAccessibleLabel) {
|
|
@@ -158,23 +194,44 @@ const IconButton = React.forwardRef<HTMLButtonElement, IconButtonProps>(
|
|
|
158
194
|
"IconButton: Missing accessible label. Icon-only buttons must have an aria-label, aria-labelledby, or title attribute for screen reader users.",
|
|
159
195
|
);
|
|
160
196
|
}
|
|
197
|
+
// Warn about deprecated asChild prop
|
|
198
|
+
if (asChild !== undefined) {
|
|
199
|
+
console.warn(
|
|
200
|
+
'IconButton: The "asChild" prop is deprecated. Use the "render" prop instead for element composition.\n' +
|
|
201
|
+
'Example: <IconButton render={<a href="/link" />}>...</IconButton>',
|
|
202
|
+
);
|
|
203
|
+
}
|
|
161
204
|
}
|
|
162
|
-
}, [props["aria-label"], props["aria-labelledby"], props.title]);
|
|
205
|
+
}, [props["aria-label"], props["aria-labelledby"], props.title, asChild]);
|
|
206
|
+
|
|
207
|
+
// Resolve actual values for data attributes
|
|
208
|
+
const resolvedVariant = variant ?? "solid";
|
|
209
|
+
const resolvedColorScheme = colorScheme ?? "dark";
|
|
210
|
+
const resolvedSize = size ?? "default";
|
|
211
|
+
const resolvedRounded = rounded ?? "default";
|
|
212
|
+
|
|
213
|
+
const mergedProps = {
|
|
214
|
+
className: iconButtonVariants({
|
|
215
|
+
variant,
|
|
216
|
+
colorScheme,
|
|
217
|
+
size,
|
|
218
|
+
rounded,
|
|
219
|
+
class: className,
|
|
220
|
+
}),
|
|
221
|
+
"data-variant": resolvedVariant,
|
|
222
|
+
"data-color-scheme": resolvedColorScheme,
|
|
223
|
+
"data-size": resolvedSize,
|
|
224
|
+
"data-rounded": resolvedRounded,
|
|
225
|
+
...props,
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
const element = useRender({
|
|
229
|
+
render: render ?? <button type="button" />,
|
|
230
|
+
ref,
|
|
231
|
+
props: mergedProps,
|
|
232
|
+
});
|
|
163
233
|
|
|
164
|
-
|
|
165
|
-
return (
|
|
166
|
-
<Comp
|
|
167
|
-
className={iconButtonVariants({
|
|
168
|
-
variant,
|
|
169
|
-
colorScheme,
|
|
170
|
-
size,
|
|
171
|
-
rounded,
|
|
172
|
-
class: className,
|
|
173
|
-
})}
|
|
174
|
-
ref={ref}
|
|
175
|
-
{...props}
|
|
176
|
-
/>
|
|
177
|
-
);
|
|
234
|
+
return element;
|
|
178
235
|
},
|
|
179
236
|
);
|
|
180
237
|
IconButton.displayName = "IconButton";
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
1
3
|
import * as React from "react";
|
|
2
4
|
import { tv, type VariantProps } from "tailwind-variants";
|
|
3
5
|
import { cn } from "@/lib/utils";
|
|
@@ -118,12 +120,39 @@ const PagerControl = React.forwardRef<HTMLDivElement, PagerControlProps>(
|
|
|
118
120
|
controlledIndex !== undefined ? controlledIndex : internalIndex;
|
|
119
121
|
const isControlled = controlledIndex !== undefined;
|
|
120
122
|
|
|
123
|
+
// Development warnings for common issues
|
|
124
|
+
React.useEffect(() => {
|
|
125
|
+
if (import.meta.env?.DEV) {
|
|
126
|
+
if (count < 1) {
|
|
127
|
+
console.warn("PagerControl: count must be at least 1");
|
|
128
|
+
}
|
|
129
|
+
if (controlledIndex !== undefined && controlledIndex >= count) {
|
|
130
|
+
console.warn(
|
|
131
|
+
`PagerControl: activeIndex (${controlledIndex}) is out of bounds. Must be less than count (${count}).`,
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
if (controlledIndex !== undefined && controlledIndex < 0) {
|
|
135
|
+
console.warn(
|
|
136
|
+
`PagerControl: activeIndex (${controlledIndex}) cannot be negative.`,
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
if (isControlled && onChange === undefined) {
|
|
140
|
+
console.warn(
|
|
141
|
+
"PagerControl: controlled mode (activeIndex provided) requires an onChange handler.",
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}, [count, controlledIndex, isControlled, onChange]);
|
|
146
|
+
|
|
147
|
+
// Clamp activeIndex to valid bounds
|
|
148
|
+
const safeActiveIndex = Math.max(0, Math.min(activeIndex, count - 1));
|
|
149
|
+
|
|
121
150
|
const animationFrameRef = React.useRef<number | null>(null);
|
|
122
151
|
const startTimeRef = React.useRef<number | null>(null);
|
|
123
152
|
const pausedProgressRef = React.useRef<number>(0);
|
|
124
153
|
|
|
125
154
|
const goToNext = React.useCallback(() => {
|
|
126
|
-
const nextIndex =
|
|
155
|
+
const nextIndex = safeActiveIndex + 1;
|
|
127
156
|
if (nextIndex >= count) {
|
|
128
157
|
if (loop) {
|
|
129
158
|
if (!isControlled) setInternalIndex(0);
|
|
@@ -133,7 +162,7 @@ const PagerControl = React.forwardRef<HTMLDivElement, PagerControlProps>(
|
|
|
133
162
|
if (!isControlled) setInternalIndex(nextIndex);
|
|
134
163
|
onChange?.(nextIndex);
|
|
135
164
|
}
|
|
136
|
-
}, [
|
|
165
|
+
}, [safeActiveIndex, count, loop, isControlled, onChange]);
|
|
137
166
|
|
|
138
167
|
const goToIndex = React.useCallback(
|
|
139
168
|
(index: number) => {
|
|
@@ -268,7 +297,7 @@ const PagerControl = React.forwardRef<HTMLDivElement, PagerControlProps>(
|
|
|
268
297
|
{...props}
|
|
269
298
|
>
|
|
270
299
|
{Array.from({ length: count }, (_, index) => {
|
|
271
|
-
const isActive = index ===
|
|
300
|
+
const isActive = index === safeActiveIndex;
|
|
272
301
|
|
|
273
302
|
if (isActive) {
|
|
274
303
|
// Active dot with progress fill
|
|
@@ -38,9 +38,15 @@ function ThemeDemo({ title }: { title?: string }) {
|
|
|
38
38
|
|
|
39
39
|
{/* Button variants */}
|
|
40
40
|
<div className="flex flex-wrap gap-3 mb-6">
|
|
41
|
-
<Button variant="solid" colorScheme="dark">
|
|
42
|
-
|
|
43
|
-
|
|
41
|
+
<Button variant="solid" colorScheme="dark">
|
|
42
|
+
Solid Dark
|
|
43
|
+
</Button>
|
|
44
|
+
<Button variant="solid" colorScheme="light">
|
|
45
|
+
Solid Light
|
|
46
|
+
</Button>
|
|
47
|
+
<Button variant="outline" colorScheme="dark">
|
|
48
|
+
Outline
|
|
49
|
+
</Button>
|
|
44
50
|
</div>
|
|
45
51
|
|
|
46
52
|
{/* Card component */}
|