@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,130 @@
|
|
|
1
|
+
import { describe, expect, test, vi } from "vitest";
|
|
2
|
+
import { page, userEvent } from "vitest/browser";
|
|
3
|
+
import { render } from "vitest-browser-react";
|
|
4
|
+
import { PagerControl } from "./pager-control";
|
|
5
|
+
|
|
6
|
+
describe("PagerControl", () => {
|
|
7
|
+
describe("Accessibility", () => {
|
|
8
|
+
test("has correct tablist role", async () => {
|
|
9
|
+
render(<PagerControl count={4} autoPlay={false} />);
|
|
10
|
+
await expect
|
|
11
|
+
.element(page.getByRole("tablist", { name: "Page indicators" }))
|
|
12
|
+
.toBeInTheDocument();
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
test("renders correct number of tab buttons", async () => {
|
|
16
|
+
render(<PagerControl count={4} autoPlay={false} />);
|
|
17
|
+
const tabs = page.getByRole("tab").all();
|
|
18
|
+
expect(await tabs).toHaveLength(4);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test("active tab has aria-selected true", async () => {
|
|
22
|
+
render(<PagerControl count={4} activeIndex={1} autoPlay={false} />);
|
|
23
|
+
const tabs = page.getByRole("tab").all();
|
|
24
|
+
const tabElements = await tabs;
|
|
25
|
+
await expect
|
|
26
|
+
.element(tabElements[0])
|
|
27
|
+
.toHaveAttribute("aria-selected", "false");
|
|
28
|
+
await expect
|
|
29
|
+
.element(tabElements[1])
|
|
30
|
+
.toHaveAttribute("aria-selected", "true");
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test("tabs have accessible labels", async () => {
|
|
34
|
+
render(<PagerControl count={3} activeIndex={0} autoPlay={false} />);
|
|
35
|
+
await expect
|
|
36
|
+
.element(page.getByRole("tab", { name: "Page 1 of 3, current" }))
|
|
37
|
+
.toBeInTheDocument();
|
|
38
|
+
await expect
|
|
39
|
+
.element(page.getByRole("tab", { name: "Go to page 2 of 3" }))
|
|
40
|
+
.toBeInTheDocument();
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test("is focusable via keyboard", async () => {
|
|
44
|
+
render(<PagerControl count={4} autoPlay={false} />);
|
|
45
|
+
await userEvent.keyboard("{Tab}");
|
|
46
|
+
const firstTab = page.getByRole("tab").all();
|
|
47
|
+
await expect.element((await firstTab)[0]).toHaveFocus();
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
describe("Interactions", () => {
|
|
52
|
+
test("calls onChange when dot is clicked", async () => {
|
|
53
|
+
const handleChange = vi.fn();
|
|
54
|
+
render(
|
|
55
|
+
<PagerControl
|
|
56
|
+
count={4}
|
|
57
|
+
activeIndex={0}
|
|
58
|
+
onChange={handleChange}
|
|
59
|
+
autoPlay={false}
|
|
60
|
+
/>,
|
|
61
|
+
);
|
|
62
|
+
await page.getByRole("tab", { name: "Go to page 3 of 4" }).click();
|
|
63
|
+
expect(handleChange).toHaveBeenCalledWith(2);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test("clicking active dot calls onChange with same index", async () => {
|
|
67
|
+
const handleChange = vi.fn();
|
|
68
|
+
render(
|
|
69
|
+
<PagerControl
|
|
70
|
+
count={4}
|
|
71
|
+
activeIndex={1}
|
|
72
|
+
onChange={handleChange}
|
|
73
|
+
autoPlay={false}
|
|
74
|
+
/>,
|
|
75
|
+
);
|
|
76
|
+
await page.getByRole("tab", { name: "Page 2 of 4, current" }).click();
|
|
77
|
+
expect(handleChange).toHaveBeenCalledWith(1);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test("updates internal state when uncontrolled", async () => {
|
|
81
|
+
render(<PagerControl count={4} autoPlay={false} />);
|
|
82
|
+
// Initially first is active
|
|
83
|
+
await expect
|
|
84
|
+
.element(page.getByRole("tab", { name: "Page 1 of 4, current" }))
|
|
85
|
+
.toBeInTheDocument();
|
|
86
|
+
|
|
87
|
+
// Click third dot
|
|
88
|
+
await page.getByRole("tab", { name: "Go to page 3 of 4" }).click();
|
|
89
|
+
|
|
90
|
+
// Now third should be active
|
|
91
|
+
await expect
|
|
92
|
+
.element(page.getByRole("tab", { name: "Page 3 of 4, current" }))
|
|
93
|
+
.toBeInTheDocument();
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
describe("Variants", () => {
|
|
98
|
+
test("applies charcoal variant classes by default", async () => {
|
|
99
|
+
render(<PagerControl count={4} autoPlay={false} />);
|
|
100
|
+
const tabs = await page.getByRole("tab").all();
|
|
101
|
+
// Active dot track should have bg-alpha-black-30
|
|
102
|
+
await expect.element(tabs[0]).toHaveClass(/bg-alpha-black-30/);
|
|
103
|
+
// Inactive dots should have bg-alpha-black-30
|
|
104
|
+
await expect.element(tabs[1]).toHaveClass(/bg-alpha-black-30/);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test("applies ivory variant classes", async () => {
|
|
108
|
+
render(<PagerControl count={4} variant="ivory" autoPlay={false} />);
|
|
109
|
+
const tabs = await page.getByRole("tab").all();
|
|
110
|
+
// Active dot track should have bg-alpha-white-30
|
|
111
|
+
await expect.element(tabs[0]).toHaveClass(/bg-alpha-white-30/);
|
|
112
|
+
// Inactive dots should have bg-alpha-white-30
|
|
113
|
+
await expect.element(tabs[1]).toHaveClass(/bg-alpha-white-30/);
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
describe("Count", () => {
|
|
118
|
+
test("renders correct number of dots for count=2", async () => {
|
|
119
|
+
render(<PagerControl count={2} autoPlay={false} />);
|
|
120
|
+
const tabs = await page.getByRole("tab").all();
|
|
121
|
+
expect(tabs).toHaveLength(2);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test("renders correct number of dots for count=8", async () => {
|
|
125
|
+
render(<PagerControl count={8} autoPlay={false} />);
|
|
126
|
+
const tabs = await page.getByRole("tab").all();
|
|
127
|
+
expect(tabs).toHaveLength(8);
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
});
|
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { tv, type VariantProps } from "tailwind-variants";
|
|
3
|
+
import { cn } from "@/lib/utils";
|
|
4
|
+
|
|
5
|
+
const pagerControlVariants = tv({
|
|
6
|
+
base: "flex items-center",
|
|
7
|
+
variants: {
|
|
8
|
+
size: {
|
|
9
|
+
// Uses primitive spacing tokens
|
|
10
|
+
sm: "gap-spacing-2",
|
|
11
|
+
default: "gap-spacing-2",
|
|
12
|
+
lg: "gap-spacing-4",
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
defaultVariants: {
|
|
16
|
+
size: "default",
|
|
17
|
+
},
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
const dotBaseVariants = tv({
|
|
21
|
+
base: "cursor-pointer rounded-full transition-all duration-500 ease-[cubic-bezier(0.23,1,0.32,1)]",
|
|
22
|
+
variants: {
|
|
23
|
+
size: {
|
|
24
|
+
// Uses primitive spacing tokens
|
|
25
|
+
sm: "h-spacing-6",
|
|
26
|
+
default: "h-spacing-10",
|
|
27
|
+
lg: "h-spacing-16",
|
|
28
|
+
},
|
|
29
|
+
variant: {
|
|
30
|
+
charcoal: "",
|
|
31
|
+
ivory: "",
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
defaultVariants: {
|
|
35
|
+
size: "default",
|
|
36
|
+
variant: "charcoal",
|
|
37
|
+
},
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
export interface PagerControlProps
|
|
41
|
+
extends Omit<React.HTMLAttributes<HTMLDivElement>, "onChange">,
|
|
42
|
+
VariantProps<typeof pagerControlVariants>,
|
|
43
|
+
VariantProps<typeof dotBaseVariants> {
|
|
44
|
+
/**
|
|
45
|
+
* Total number of pages/items
|
|
46
|
+
*/
|
|
47
|
+
count: number;
|
|
48
|
+
/**
|
|
49
|
+
* Current active page index (0-based)
|
|
50
|
+
*/
|
|
51
|
+
activeIndex?: number;
|
|
52
|
+
/**
|
|
53
|
+
* Duration in milliseconds for each page before auto-advancing
|
|
54
|
+
* Set to 0 to disable auto-advance
|
|
55
|
+
* @default 5000
|
|
56
|
+
*/
|
|
57
|
+
duration?: number;
|
|
58
|
+
/**
|
|
59
|
+
* Whether the pager should auto-advance
|
|
60
|
+
* @default true
|
|
61
|
+
*/
|
|
62
|
+
autoPlay?: boolean;
|
|
63
|
+
/**
|
|
64
|
+
* Callback when the active page changes
|
|
65
|
+
*/
|
|
66
|
+
onChange?: (index: number) => void;
|
|
67
|
+
/**
|
|
68
|
+
* Whether to pause auto-advance on hover
|
|
69
|
+
* @default true
|
|
70
|
+
*/
|
|
71
|
+
pauseOnHover?: boolean;
|
|
72
|
+
/**
|
|
73
|
+
* Whether to loop back to the first page after the last
|
|
74
|
+
* @default true
|
|
75
|
+
*/
|
|
76
|
+
loop?: boolean;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* PagerControl component for indicating progress through a series of pages/slides.
|
|
81
|
+
*
|
|
82
|
+
* Features smooth width transitions when switching between dots and an animated
|
|
83
|
+
* progress fill on the active dot that shows time remaining before auto-advancing
|
|
84
|
+
* (similar to Apple's carousel indicators).
|
|
85
|
+
*
|
|
86
|
+
* Variants:
|
|
87
|
+
* - charcoal: Dark dots (for light backgrounds)
|
|
88
|
+
* - ivory: Light dots (for dark backgrounds)
|
|
89
|
+
*
|
|
90
|
+
* Sizes:
|
|
91
|
+
* - sm: Small dots (6px height)
|
|
92
|
+
* - default: Medium dots (10px height)
|
|
93
|
+
* - lg: Large dots (16px height)
|
|
94
|
+
*/
|
|
95
|
+
const PagerControl = React.forwardRef<HTMLDivElement, PagerControlProps>(
|
|
96
|
+
(
|
|
97
|
+
{
|
|
98
|
+
className,
|
|
99
|
+
size,
|
|
100
|
+
variant,
|
|
101
|
+
count,
|
|
102
|
+
activeIndex: controlledIndex,
|
|
103
|
+
duration = 5000,
|
|
104
|
+
autoPlay = true,
|
|
105
|
+
onChange,
|
|
106
|
+
pauseOnHover = true,
|
|
107
|
+
loop = true,
|
|
108
|
+
...props
|
|
109
|
+
},
|
|
110
|
+
ref,
|
|
111
|
+
) => {
|
|
112
|
+
const [internalIndex, setInternalIndex] = React.useState(0);
|
|
113
|
+
const [isPaused, setIsPaused] = React.useState(false);
|
|
114
|
+
const [progress, setProgress] = React.useState(0);
|
|
115
|
+
|
|
116
|
+
// Use controlled index if provided, otherwise use internal state
|
|
117
|
+
const activeIndex =
|
|
118
|
+
controlledIndex !== undefined ? controlledIndex : internalIndex;
|
|
119
|
+
const isControlled = controlledIndex !== undefined;
|
|
120
|
+
|
|
121
|
+
const animationFrameRef = React.useRef<number | null>(null);
|
|
122
|
+
const startTimeRef = React.useRef<number | null>(null);
|
|
123
|
+
const pausedProgressRef = React.useRef<number>(0);
|
|
124
|
+
|
|
125
|
+
const goToNext = React.useCallback(() => {
|
|
126
|
+
const nextIndex = activeIndex + 1;
|
|
127
|
+
if (nextIndex >= count) {
|
|
128
|
+
if (loop) {
|
|
129
|
+
if (!isControlled) setInternalIndex(0);
|
|
130
|
+
onChange?.(0);
|
|
131
|
+
}
|
|
132
|
+
} else {
|
|
133
|
+
if (!isControlled) setInternalIndex(nextIndex);
|
|
134
|
+
onChange?.(nextIndex);
|
|
135
|
+
}
|
|
136
|
+
}, [activeIndex, count, loop, isControlled, onChange]);
|
|
137
|
+
|
|
138
|
+
const goToIndex = React.useCallback(
|
|
139
|
+
(index: number) => {
|
|
140
|
+
if (!isControlled) setInternalIndex(index);
|
|
141
|
+
onChange?.(index);
|
|
142
|
+
// Reset progress when manually changing
|
|
143
|
+
setProgress(0);
|
|
144
|
+
pausedProgressRef.current = 0;
|
|
145
|
+
startTimeRef.current = null;
|
|
146
|
+
},
|
|
147
|
+
[isControlled, onChange],
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
// Animation loop for smooth progress fill
|
|
151
|
+
React.useEffect(() => {
|
|
152
|
+
if (!autoPlay || duration <= 0 || isPaused) {
|
|
153
|
+
if (animationFrameRef.current) {
|
|
154
|
+
cancelAnimationFrame(animationFrameRef.current);
|
|
155
|
+
animationFrameRef.current = null;
|
|
156
|
+
}
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const animate = (timestamp: number) => {
|
|
161
|
+
if (startTimeRef.current === null) {
|
|
162
|
+
startTimeRef.current =
|
|
163
|
+
timestamp - (pausedProgressRef.current / 100) * duration;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const elapsed = timestamp - startTimeRef.current;
|
|
167
|
+
const newProgress = Math.min((elapsed / duration) * 100, 100);
|
|
168
|
+
setProgress(newProgress);
|
|
169
|
+
|
|
170
|
+
if (newProgress >= 100) {
|
|
171
|
+
goToNext();
|
|
172
|
+
// Reset for next cycle
|
|
173
|
+
setProgress(0);
|
|
174
|
+
pausedProgressRef.current = 0;
|
|
175
|
+
startTimeRef.current = null;
|
|
176
|
+
} else {
|
|
177
|
+
animationFrameRef.current = requestAnimationFrame(animate);
|
|
178
|
+
}
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
animationFrameRef.current = requestAnimationFrame(animate);
|
|
182
|
+
|
|
183
|
+
return () => {
|
|
184
|
+
if (animationFrameRef.current) {
|
|
185
|
+
cancelAnimationFrame(animationFrameRef.current);
|
|
186
|
+
}
|
|
187
|
+
};
|
|
188
|
+
}, [autoPlay, duration, isPaused, goToNext]);
|
|
189
|
+
|
|
190
|
+
// Handle pause/resume
|
|
191
|
+
const handleMouseEnter = React.useCallback(() => {
|
|
192
|
+
if (pauseOnHover) {
|
|
193
|
+
pausedProgressRef.current = progress;
|
|
194
|
+
startTimeRef.current = null;
|
|
195
|
+
setIsPaused(true);
|
|
196
|
+
}
|
|
197
|
+
}, [pauseOnHover, progress]);
|
|
198
|
+
|
|
199
|
+
const handleMouseLeave = React.useCallback(() => {
|
|
200
|
+
if (pauseOnHover) {
|
|
201
|
+
setIsPaused(false);
|
|
202
|
+
}
|
|
203
|
+
}, [pauseOnHover]);
|
|
204
|
+
|
|
205
|
+
// Reset progress when activeIndex changes externally (controlled mode)
|
|
206
|
+
React.useEffect(() => {
|
|
207
|
+
if (isControlled) {
|
|
208
|
+
setProgress(0);
|
|
209
|
+
pausedProgressRef.current = 0;
|
|
210
|
+
startTimeRef.current = null;
|
|
211
|
+
}
|
|
212
|
+
}, [isControlled]);
|
|
213
|
+
|
|
214
|
+
// Get dot dimensions based on size - uses primitive spacing tokens
|
|
215
|
+
const getDotWidth = (isActive: boolean) => {
|
|
216
|
+
if (isActive) {
|
|
217
|
+
switch (size) {
|
|
218
|
+
case "sm":
|
|
219
|
+
return "w-spacing-16";
|
|
220
|
+
case "lg":
|
|
221
|
+
return "w-spacing-36";
|
|
222
|
+
default:
|
|
223
|
+
return "w-spacing-28";
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
switch (size) {
|
|
227
|
+
case "sm":
|
|
228
|
+
return "w-spacing-6";
|
|
229
|
+
case "lg":
|
|
230
|
+
return "w-spacing-16";
|
|
231
|
+
default:
|
|
232
|
+
return "w-spacing-10";
|
|
233
|
+
}
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
// Get background classes for inactive dots
|
|
237
|
+
const getInactiveClasses = () => {
|
|
238
|
+
if (variant === "ivory") {
|
|
239
|
+
return "bg-alpha-white-30 hover:bg-alpha-white-60";
|
|
240
|
+
}
|
|
241
|
+
return "bg-alpha-black-30 hover:bg-alpha-black-60";
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
// Get background class for active dot (the track/background)
|
|
245
|
+
const getActiveTrackClass = () => {
|
|
246
|
+
if (variant === "ivory") {
|
|
247
|
+
return "bg-alpha-white-30";
|
|
248
|
+
}
|
|
249
|
+
return "bg-alpha-black-30";
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
// Get fill color for the progress indicator
|
|
253
|
+
const getProgressFillClass = () => {
|
|
254
|
+
if (variant === "ivory") {
|
|
255
|
+
return "bg-gray-50";
|
|
256
|
+
}
|
|
257
|
+
return "bg-gray-1200";
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
return (
|
|
261
|
+
<div
|
|
262
|
+
ref={ref}
|
|
263
|
+
role="tablist"
|
|
264
|
+
aria-label="Page indicators"
|
|
265
|
+
className={pagerControlVariants({ size, class: className })}
|
|
266
|
+
onMouseEnter={handleMouseEnter}
|
|
267
|
+
onMouseLeave={handleMouseLeave}
|
|
268
|
+
{...props}
|
|
269
|
+
>
|
|
270
|
+
{Array.from({ length: count }, (_, index) => {
|
|
271
|
+
const isActive = index === activeIndex;
|
|
272
|
+
|
|
273
|
+
if (isActive) {
|
|
274
|
+
// Active dot with progress fill
|
|
275
|
+
return (
|
|
276
|
+
<button
|
|
277
|
+
// biome-ignore lint/suspicious/noArrayIndexKey: Pagination dots have fixed order based on count
|
|
278
|
+
key={index}
|
|
279
|
+
type="button"
|
|
280
|
+
role="tab"
|
|
281
|
+
aria-selected={true}
|
|
282
|
+
aria-label={`Page ${index + 1} of ${count}, current`}
|
|
283
|
+
className={cn(
|
|
284
|
+
"relative cursor-pointer overflow-hidden rounded-full transition-all duration-500 ease-[cubic-bezier(0.23,1,0.32,1)]",
|
|
285
|
+
dotBaseVariants({ size, variant }),
|
|
286
|
+
getDotWidth(true),
|
|
287
|
+
getActiveTrackClass(),
|
|
288
|
+
)}
|
|
289
|
+
onClick={() => goToIndex(index)}
|
|
290
|
+
>
|
|
291
|
+
{/* Progress fill */}
|
|
292
|
+
<div
|
|
293
|
+
className={cn(
|
|
294
|
+
"absolute top-0 bottom-0 left-0 h-full rounded-full",
|
|
295
|
+
getProgressFillClass(),
|
|
296
|
+
)}
|
|
297
|
+
style={{
|
|
298
|
+
width: autoPlay && duration > 0 ? `${progress}%` : "100%",
|
|
299
|
+
}}
|
|
300
|
+
/>
|
|
301
|
+
</button>
|
|
302
|
+
);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// Inactive dot
|
|
306
|
+
return (
|
|
307
|
+
<button
|
|
308
|
+
// biome-ignore lint/suspicious/noArrayIndexKey: Pagination dots have fixed order based on count
|
|
309
|
+
key={index}
|
|
310
|
+
type="button"
|
|
311
|
+
role="tab"
|
|
312
|
+
aria-selected={false}
|
|
313
|
+
aria-label={`Go to page ${index + 1} of ${count}`}
|
|
314
|
+
className={cn(
|
|
315
|
+
dotBaseVariants({ size, variant }),
|
|
316
|
+
getDotWidth(false),
|
|
317
|
+
getInactiveClasses(),
|
|
318
|
+
)}
|
|
319
|
+
onClick={() => goToIndex(index)}
|
|
320
|
+
/>
|
|
321
|
+
);
|
|
322
|
+
})}
|
|
323
|
+
</div>
|
|
324
|
+
);
|
|
325
|
+
},
|
|
326
|
+
);
|
|
327
|
+
PagerControl.displayName = "PagerControl";
|
|
328
|
+
|
|
329
|
+
export { PagerControl, pagerControlVariants };
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
2
|
+
import { DevToolbar } from "./dev-toolbar";
|
|
3
|
+
|
|
4
|
+
const meta = {
|
|
5
|
+
title: "Dev Tools/DevToolbar",
|
|
6
|
+
component: DevToolbar,
|
|
7
|
+
parameters: {
|
|
8
|
+
layout: "fullscreen",
|
|
9
|
+
},
|
|
10
|
+
argTypes: {
|
|
11
|
+
defaultExpanded: {
|
|
12
|
+
control: "boolean",
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
} satisfies Meta<typeof DevToolbar>;
|
|
16
|
+
|
|
17
|
+
export default meta;
|
|
18
|
+
type Story = StoryObj<typeof meta>;
|
|
19
|
+
|
|
20
|
+
const DemoContent = () => (
|
|
21
|
+
<div className="min-h-screen bg-gray-100 py-spacing-64">
|
|
22
|
+
<div className="w-full max-w-[90rem] mx-auto px-[var(--spatial-grid-small-margin)] md:px-[var(--spatial-grid-medium-margin)] lg:px-[var(--spatial-grid-large-margin)]">
|
|
23
|
+
<h1 className="typography-headline-large mb-spacing-16">
|
|
24
|
+
Dev Toolbar Demo
|
|
25
|
+
</h1>
|
|
26
|
+
<p className="typography-body-medium text-gray-600 mb-spacing-8">
|
|
27
|
+
Click the bar at the bottom to expand, then toggle the Grid overlay.
|
|
28
|
+
</p>
|
|
29
|
+
<p className="typography-body-medium text-gray-600 mb-spacing-32">
|
|
30
|
+
Keyboard shortcut:{" "}
|
|
31
|
+
<kbd className="px-spacing-8 py-spacing-4 bg-gray-200 rounded-radius-8">
|
|
32
|
+
⌘G
|
|
33
|
+
</kbd>{" "}
|
|
34
|
+
or{" "}
|
|
35
|
+
<kbd className="px-spacing-8 py-spacing-4 bg-gray-200 rounded-radius-8">
|
|
36
|
+
Ctrl+G
|
|
37
|
+
</kbd>
|
|
38
|
+
</p>
|
|
39
|
+
|
|
40
|
+
<div className="grid grid-cols-4 md:grid-cols-12 lg:grid-cols-24 gap-[var(--spatial-grid-small-gutter)] md:gap-[var(--spatial-grid-medium-gutter)] lg:gap-[var(--spatial-grid-large-gutter)]">
|
|
41
|
+
{["alpha", "beta", "gamma", "delta", "epsilon", "zeta"].map((id) => (
|
|
42
|
+
<div
|
|
43
|
+
key={id}
|
|
44
|
+
className="col-span-4 md:col-span-4 lg:col-span-8 bg-white p-spacing-16 rounded-radius-12 shadow"
|
|
45
|
+
>
|
|
46
|
+
<h3 className="typography-headline-small mb-spacing-8">
|
|
47
|
+
Card {id}
|
|
48
|
+
</h3>
|
|
49
|
+
<p className="typography-body-small text-gray-500">
|
|
50
|
+
Sample content to visualize how the grid overlay aligns with your
|
|
51
|
+
layout.
|
|
52
|
+
</p>
|
|
53
|
+
</div>
|
|
54
|
+
))}
|
|
55
|
+
</div>
|
|
56
|
+
</div>
|
|
57
|
+
</div>
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
export const Default: Story = {
|
|
61
|
+
args: {
|
|
62
|
+
defaultExpanded: false,
|
|
63
|
+
},
|
|
64
|
+
render: (args) => (
|
|
65
|
+
<>
|
|
66
|
+
<DemoContent />
|
|
67
|
+
<DevToolbar {...args} />
|
|
68
|
+
</>
|
|
69
|
+
),
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
export const Expanded: Story = {
|
|
73
|
+
args: {
|
|
74
|
+
defaultExpanded: true,
|
|
75
|
+
},
|
|
76
|
+
render: (args) => (
|
|
77
|
+
<>
|
|
78
|
+
<DemoContent />
|
|
79
|
+
<DevToolbar {...args} />
|
|
80
|
+
</>
|
|
81
|
+
),
|
|
82
|
+
};
|