@nationaldesignstudio/react 0.0.15 → 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/package.json +3 -2
- 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,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
|
+
};
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
2
|
+
import { GridOverlay } from "../grid-overlay";
|
|
3
|
+
|
|
4
|
+
function GridIcon({ active }: { active?: boolean }) {
|
|
5
|
+
return (
|
|
6
|
+
<svg
|
|
7
|
+
width="20"
|
|
8
|
+
height="20"
|
|
9
|
+
viewBox="0 0 20 20"
|
|
10
|
+
fill="none"
|
|
11
|
+
stroke="currentColor"
|
|
12
|
+
strokeWidth={active ? "2" : "1.5"}
|
|
13
|
+
aria-hidden="true"
|
|
14
|
+
>
|
|
15
|
+
<rect x="2" y="2" width="16" height="16" rx="2" />
|
|
16
|
+
<line x1="7" y1="2" x2="7" y2="18" />
|
|
17
|
+
<line x1="13" y1="2" x2="13" y2="18" />
|
|
18
|
+
</svg>
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface DevToolbarProps {
|
|
23
|
+
defaultExpanded?: boolean;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const DRAG_THRESHOLD = 3;
|
|
27
|
+
|
|
28
|
+
export function DevToolbar({ defaultExpanded = false }: DevToolbarProps) {
|
|
29
|
+
const [isExpanded, setIsExpanded] = useState(defaultExpanded);
|
|
30
|
+
const [showGrid, setShowGrid] = useState(false);
|
|
31
|
+
const [position, setPosition] = useState({ x: 0, y: 0 });
|
|
32
|
+
const [isDragging, setIsDragging] = useState(false);
|
|
33
|
+
const hasDraggedRef = useRef(false);
|
|
34
|
+
const dragRef = useRef<{
|
|
35
|
+
startX: number;
|
|
36
|
+
startY: number;
|
|
37
|
+
startPosX: number;
|
|
38
|
+
startPosY: number;
|
|
39
|
+
} | null>(null);
|
|
40
|
+
const toolbarRef = useRef<HTMLDivElement>(null);
|
|
41
|
+
|
|
42
|
+
const toggleGrid = useCallback(() => setShowGrid((prev) => !prev), []);
|
|
43
|
+
const toggleExpanded = useCallback(() => setIsExpanded((prev) => !prev), []);
|
|
44
|
+
|
|
45
|
+
useEffect(() => {
|
|
46
|
+
const handleKeyDown = (e: KeyboardEvent) => {
|
|
47
|
+
if ((e.ctrlKey || e.metaKey) && e.key === "g") {
|
|
48
|
+
e.preventDefault();
|
|
49
|
+
toggleGrid();
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
window.addEventListener("keydown", handleKeyDown);
|
|
54
|
+
return () => window.removeEventListener("keydown", handleKeyDown);
|
|
55
|
+
}, [toggleGrid]);
|
|
56
|
+
|
|
57
|
+
const handleDragStart = useCallback(
|
|
58
|
+
(clientX: number, clientY: number) => {
|
|
59
|
+
setIsDragging(true);
|
|
60
|
+
hasDraggedRef.current = false;
|
|
61
|
+
dragRef.current = {
|
|
62
|
+
startX: clientX,
|
|
63
|
+
startY: clientY,
|
|
64
|
+
startPosX: position.x,
|
|
65
|
+
startPosY: position.y,
|
|
66
|
+
};
|
|
67
|
+
},
|
|
68
|
+
[position],
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
const handleDragMove = useCallback(
|
|
72
|
+
(clientX: number, clientY: number) => {
|
|
73
|
+
if (!isDragging || !dragRef.current) return;
|
|
74
|
+
|
|
75
|
+
const deltaX = clientX - dragRef.current.startX;
|
|
76
|
+
const deltaY = clientY - dragRef.current.startY;
|
|
77
|
+
|
|
78
|
+
if (
|
|
79
|
+
Math.abs(deltaX) > DRAG_THRESHOLD ||
|
|
80
|
+
Math.abs(deltaY) > DRAG_THRESHOLD
|
|
81
|
+
) {
|
|
82
|
+
hasDraggedRef.current = true;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
setPosition({
|
|
86
|
+
x: dragRef.current.startPosX + deltaX,
|
|
87
|
+
y: dragRef.current.startPosY - deltaY,
|
|
88
|
+
});
|
|
89
|
+
},
|
|
90
|
+
[isDragging],
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
const handleDragEnd = useCallback(() => {
|
|
94
|
+
setIsDragging(false);
|
|
95
|
+
dragRef.current = null;
|
|
96
|
+
}, []);
|
|
97
|
+
|
|
98
|
+
useEffect(() => {
|
|
99
|
+
if (!isDragging) return;
|
|
100
|
+
|
|
101
|
+
const handleMouseMove = (e: MouseEvent) =>
|
|
102
|
+
handleDragMove(e.clientX, e.clientY);
|
|
103
|
+
const handleTouchMove = (e: TouchEvent) => {
|
|
104
|
+
if (e.touches[0])
|
|
105
|
+
handleDragMove(e.touches[0].clientX, e.touches[0].clientY);
|
|
106
|
+
};
|
|
107
|
+
const handleEnd = () => handleDragEnd();
|
|
108
|
+
|
|
109
|
+
window.addEventListener("mousemove", handleMouseMove);
|
|
110
|
+
window.addEventListener("mouseup", handleEnd);
|
|
111
|
+
window.addEventListener("touchmove", handleTouchMove);
|
|
112
|
+
window.addEventListener("touchend", handleEnd);
|
|
113
|
+
|
|
114
|
+
return () => {
|
|
115
|
+
window.removeEventListener("mousemove", handleMouseMove);
|
|
116
|
+
window.removeEventListener("mouseup", handleEnd);
|
|
117
|
+
window.removeEventListener("touchmove", handleTouchMove);
|
|
118
|
+
window.removeEventListener("touchend", handleEnd);
|
|
119
|
+
};
|
|
120
|
+
}, [isDragging, handleDragMove, handleDragEnd]);
|
|
121
|
+
|
|
122
|
+
const handleBarMouseDown = (e: React.MouseEvent) => {
|
|
123
|
+
e.preventDefault();
|
|
124
|
+
handleDragStart(e.clientX, e.clientY);
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
const handleBarTouchStart = (e: React.TouchEvent) => {
|
|
128
|
+
if (e.touches[0]) {
|
|
129
|
+
handleDragStart(e.touches[0].clientX, e.touches[0].clientY);
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
const handleBarClick = () => {
|
|
134
|
+
if (!hasDraggedRef.current) {
|
|
135
|
+
toggleExpanded();
|
|
136
|
+
}
|
|
137
|
+
hasDraggedRef.current = false;
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
return (
|
|
141
|
+
<>
|
|
142
|
+
{showGrid && <GridOverlay />}
|
|
143
|
+
|
|
144
|
+
<div
|
|
145
|
+
ref={toolbarRef}
|
|
146
|
+
className="fixed bottom-4 left-1/2 z-[9999]"
|
|
147
|
+
style={{
|
|
148
|
+
transform: `translate(calc(-50% + ${position.x}px), ${-position.y}px)`,
|
|
149
|
+
}}
|
|
150
|
+
data-testid="dev-toolbar"
|
|
151
|
+
>
|
|
152
|
+
<div
|
|
153
|
+
className={`bg-gray-1100 rounded-radius-16 shadow-lg flex flex-col items-center overflow-hidden px-spacing-12 py-spacing-8 ${isExpanded ? "gap-spacing-4" : ""}`}
|
|
154
|
+
>
|
|
155
|
+
<div
|
|
156
|
+
className={`
|
|
157
|
+
grid transition-all duration-300 ease-out
|
|
158
|
+
${isExpanded ? "grid-rows-[1fr] opacity-100" : "grid-rows-[0fr] opacity-0"}
|
|
159
|
+
`}
|
|
160
|
+
>
|
|
161
|
+
<div className="overflow-hidden">
|
|
162
|
+
<button
|
|
163
|
+
type="button"
|
|
164
|
+
onClick={toggleGrid}
|
|
165
|
+
className={`
|
|
166
|
+
size-spacing-40 rounded-full flex items-center justify-center transition-colors
|
|
167
|
+
${
|
|
168
|
+
showGrid
|
|
169
|
+
? "text-gray-50"
|
|
170
|
+
: "text-gray-400 hover:text-gray-50 hover:bg-alpha-white-10"
|
|
171
|
+
}
|
|
172
|
+
`}
|
|
173
|
+
title="Toggle Grid (⌘G)"
|
|
174
|
+
aria-label="Toggle grid overlay"
|
|
175
|
+
>
|
|
176
|
+
<GridIcon active={showGrid} />
|
|
177
|
+
</button>
|
|
178
|
+
</div>
|
|
179
|
+
</div>
|
|
180
|
+
|
|
181
|
+
<button
|
|
182
|
+
type="button"
|
|
183
|
+
onMouseDown={handleBarMouseDown}
|
|
184
|
+
onTouchStart={handleBarTouchStart}
|
|
185
|
+
onClick={handleBarClick}
|
|
186
|
+
className={`
|
|
187
|
+
w-spacing-32 h-spacing-4 bg-gray-50 rounded-full transition-opacity
|
|
188
|
+
${isDragging ? "opacity-100 cursor-grabbing" : "opacity-60 hover:opacity-100 cursor-grab"}
|
|
189
|
+
`}
|
|
190
|
+
aria-label={isExpanded ? "Close dev tools" : "Open dev tools"}
|
|
191
|
+
/>
|
|
192
|
+
</div>
|
|
193
|
+
</div>
|
|
194
|
+
</>
|
|
195
|
+
);
|
|
196
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { DevToolbar, type DevToolbarProps } from "./dev-toolbar";
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
export interface GridOverlayProps {
|
|
2
|
+
columnOpacity?: number;
|
|
3
|
+
borderOpacity?: number;
|
|
4
|
+
visible?: boolean;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function GridOverlay({
|
|
8
|
+
columnOpacity = 0.15,
|
|
9
|
+
borderOpacity = 0.3,
|
|
10
|
+
visible = true,
|
|
11
|
+
}: GridOverlayProps) {
|
|
12
|
+
const columns = Array.from({ length: 24 }, (_, i) => i);
|
|
13
|
+
|
|
14
|
+
return (
|
|
15
|
+
<div
|
|
16
|
+
className={`
|
|
17
|
+
fixed inset-0 z-[9998] pointer-events-none overflow-hidden
|
|
18
|
+
transition-opacity duration-300 ease-out
|
|
19
|
+
${visible ? "opacity-100" : "opacity-0"}
|
|
20
|
+
`}
|
|
21
|
+
aria-hidden="true"
|
|
22
|
+
data-testid="grid-overlay"
|
|
23
|
+
>
|
|
24
|
+
<div className="h-full 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)]">
|
|
25
|
+
<div className="h-full 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)]">
|
|
26
|
+
{columns.map((index) => (
|
|
27
|
+
<div
|
|
28
|
+
key={index}
|
|
29
|
+
className="h-full border border-red-500"
|
|
30
|
+
style={{
|
|
31
|
+
backgroundColor: `rgb(239 68 68 / ${columnOpacity})`,
|
|
32
|
+
borderColor: `rgb(239 68 68 / ${borderOpacity})`,
|
|
33
|
+
}}
|
|
34
|
+
data-column={index + 1}
|
|
35
|
+
/>
|
|
36
|
+
))}
|
|
37
|
+
</div>
|
|
38
|
+
</div>
|
|
39
|
+
</div>
|
|
40
|
+
);
|
|
41
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { GridOverlay, type GridOverlayProps } from "./grid-overlay";
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|