@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.
Files changed (164) hide show
  1. package/package.json +3 -2
  2. package/src/App.css +0 -0
  3. package/src/App.tsx +7 -0
  4. package/src/assets/fonts/PPNeueMontreal-Variable.woff2 +0 -0
  5. package/src/assets/react.svg +1 -0
  6. package/src/components/atoms/accordion/accordion.stories.tsx +228 -0
  7. package/src/components/atoms/accordion/accordion.tsx +219 -0
  8. package/src/components/atoms/accordion/index.ts +6 -0
  9. package/src/components/atoms/button/__screenshots__/button.visual.test.tsx/button-charcoal-chromium-darwin.png +0 -0
  10. package/src/components/atoms/button/__screenshots__/button.visual.test.tsx/button-charcoal-chromium-linux.png +0 -0
  11. package/src/components/atoms/button/__screenshots__/button.visual.test.tsx/button-charcoal-outline-chromium-darwin.png +0 -0
  12. package/src/components/atoms/button/__screenshots__/button.visual.test.tsx/button-charcoal-outline-chromium-linux.png +0 -0
  13. package/src/components/atoms/button/__screenshots__/button.visual.test.tsx/button-charcoal-outline-quiet-chromium-darwin.png +0 -0
  14. package/src/components/atoms/button/__screenshots__/button.visual.test.tsx/button-charcoal-outline-quiet-chromium-linux.png +0 -0
  15. package/src/components/atoms/button/__screenshots__/button.visual.test.tsx/button-disabled-chromium-darwin.png +0 -0
  16. package/src/components/atoms/button/__screenshots__/button.visual.test.tsx/button-disabled-chromium-linux.png +0 -0
  17. package/src/components/atoms/button/__screenshots__/button.visual.test.tsx/button-ivory-chromium-darwin.png +0 -0
  18. package/src/components/atoms/button/__screenshots__/button.visual.test.tsx/button-ivory-chromium-linux.png +0 -0
  19. package/src/components/atoms/button/__screenshots__/button.visual.test.tsx/button-ivory-outline-chromium-darwin.png +0 -0
  20. package/src/components/atoms/button/__screenshots__/button.visual.test.tsx/button-ivory-outline-chromium-linux.png +0 -0
  21. package/src/components/atoms/button/__screenshots__/button.visual.test.tsx/button-ivory-outline-quiet-chromium-darwin.png +0 -0
  22. package/src/components/atoms/button/__screenshots__/button.visual.test.tsx/button-ivory-outline-quiet-chromium-linux.png +0 -0
  23. package/src/components/atoms/button/__screenshots__/button.visual.test.tsx/button-size-large-chromium-darwin.png +0 -0
  24. package/src/components/atoms/button/__screenshots__/button.visual.test.tsx/button-size-large-chromium-linux.png +0 -0
  25. package/src/components/atoms/button/__screenshots__/button.visual.test.tsx/button-size-medium-chromium-darwin.png +0 -0
  26. package/src/components/atoms/button/__screenshots__/button.visual.test.tsx/button-size-medium-chromium-linux.png +0 -0
  27. package/src/components/atoms/button/__screenshots__/button.visual.test.tsx/button-size-small-chromium-darwin.png +0 -0
  28. package/src/components/atoms/button/__screenshots__/button.visual.test.tsx/button-size-small-chromium-linux.png +0 -0
  29. package/src/components/atoms/button/button.stories.tsx +102 -0
  30. package/src/components/atoms/button/button.test.tsx +135 -0
  31. package/src/components/atoms/button/button.tsx +139 -0
  32. package/src/components/atoms/button/button.visual.test.tsx +102 -0
  33. package/src/components/atoms/button/icon-button.stories.tsx +166 -0
  34. package/src/components/atoms/button/icon-button.tsx +120 -0
  35. package/src/components/atoms/button/index.ts +6 -0
  36. package/src/components/atoms/ndstudio-footer/index.ts +1 -0
  37. package/src/components/atoms/ndstudio-footer/ndstudio-footer.tsx +55 -0
  38. package/src/components/atoms/pager-control/index.ts +5 -0
  39. package/src/components/atoms/pager-control/pager-control.stories.tsx +209 -0
  40. package/src/components/atoms/pager-control/pager-control.test.tsx +130 -0
  41. package/src/components/atoms/pager-control/pager-control.tsx +329 -0
  42. package/src/components/dev-tools/dev-toolbar/dev-toolbar.stories.tsx +82 -0
  43. package/src/components/dev-tools/dev-toolbar/dev-toolbar.tsx +196 -0
  44. package/src/components/dev-tools/dev-toolbar/index.ts +1 -0
  45. package/src/components/dev-tools/grid-overlay/grid-overlay.tsx +41 -0
  46. package/src/components/dev-tools/grid-overlay/index.ts +1 -0
  47. package/src/components/dev-tools/index.ts +2 -0
  48. package/src/components/organisms/card/__screenshots__/card.visual.test.tsx/card-default-vertical-chromium-darwin.png +0 -0
  49. package/src/components/organisms/card/__screenshots__/card.visual.test.tsx/card-default-vertical-chromium-linux.png +0 -0
  50. package/src/components/organisms/card/__screenshots__/card.visual.test.tsx/card-horizontal-chromium-darwin.png +0 -0
  51. package/src/components/organisms/card/__screenshots__/card.visual.test.tsx/card-horizontal-chromium-linux.png +0 -0
  52. package/src/components/organisms/card/__screenshots__/card.visual.test.tsx/card-minimal-chromium-darwin.png +0 -0
  53. package/src/components/organisms/card/__screenshots__/card.visual.test.tsx/card-minimal-chromium-linux.png +0 -0
  54. package/src/components/organisms/card/__screenshots__/card.visual.test.tsx/card-without-actions-chromium-darwin.png +0 -0
  55. package/src/components/organisms/card/__screenshots__/card.visual.test.tsx/card-without-actions-chromium-linux.png +0 -0
  56. package/src/components/organisms/card/__screenshots__/card.visual.test.tsx/card-without-eyebrow-chromium-darwin.png +0 -0
  57. package/src/components/organisms/card/__screenshots__/card.visual.test.tsx/card-without-eyebrow-chromium-linux.png +0 -0
  58. package/src/components/organisms/card/__screenshots__/card.visual.test.tsx/card-without-image-chromium-darwin.png +0 -0
  59. package/src/components/organisms/card/__screenshots__/card.visual.test.tsx/card-without-image-chromium-linux.png +0 -0
  60. package/src/components/organisms/card/card.stories.tsx +293 -0
  61. package/src/components/organisms/card/card.test.tsx +245 -0
  62. package/src/components/organisms/card/card.tsx +225 -0
  63. package/src/components/organisms/card/card.visual.test.tsx +197 -0
  64. package/src/components/organisms/card/index.ts +19 -0
  65. package/src/components/organisms/navbar/__screenshots__/navbar.visual.test.tsx/navbar-active-link-chromium-darwin.png +0 -0
  66. package/src/components/organisms/navbar/__screenshots__/navbar.visual.test.tsx/navbar-active-link-chromium-linux.png +0 -0
  67. package/src/components/organisms/navbar/__screenshots__/navbar.visual.test.tsx/navbar-brand-only-chromium-darwin.png +0 -0
  68. package/src/components/organisms/navbar/__screenshots__/navbar.visual.test.tsx/navbar-brand-only-chromium-linux.png +0 -0
  69. package/src/components/organisms/navbar/__screenshots__/navbar.visual.test.tsx/navbar-default-chromium-darwin.png +0 -0
  70. package/src/components/organisms/navbar/__screenshots__/navbar.visual.test.tsx/navbar-default-chromium-linux.png +0 -0
  71. package/src/components/organisms/navbar/index.ts +18 -0
  72. package/src/components/organisms/navbar/navbar.stories.tsx +313 -0
  73. package/src/components/organisms/navbar/navbar.test.tsx +190 -0
  74. package/src/components/organisms/navbar/navbar.tsx +323 -0
  75. package/src/components/organisms/navbar/navbar.visual.test.tsx +85 -0
  76. package/src/components/organisms/us-gov-banner/__screenshots__/us-gov-banner.visual.test.tsx/us-gov-banner-custom-icon-chromium-darwin.png +0 -0
  77. package/src/components/organisms/us-gov-banner/__screenshots__/us-gov-banner.visual.test.tsx/us-gov-banner-custom-icon-chromium-linux.png +0 -0
  78. package/src/components/organisms/us-gov-banner/__screenshots__/us-gov-banner.visual.test.tsx/us-gov-banner-custom-text-chromium-darwin.png +0 -0
  79. package/src/components/organisms/us-gov-banner/__screenshots__/us-gov-banner.visual.test.tsx/us-gov-banner-custom-text-chromium-linux.png +0 -0
  80. package/src/components/organisms/us-gov-banner/__screenshots__/us-gov-banner.visual.test.tsx/us-gov-banner-default-chromium-darwin.png +0 -0
  81. package/src/components/organisms/us-gov-banner/__screenshots__/us-gov-banner.visual.test.tsx/us-gov-banner-default-chromium-linux.png +0 -0
  82. package/src/components/organisms/us-gov-banner/index.ts +1 -0
  83. package/src/components/organisms/us-gov-banner/us-gov-banner.stories.tsx +35 -0
  84. package/src/components/organisms/us-gov-banner/us-gov-banner.test.tsx +107 -0
  85. package/src/components/organisms/us-gov-banner/us-gov-banner.tsx +73 -0
  86. package/src/components/organisms/us-gov-banner/us-gov-banner.visual.test.tsx +46 -0
  87. package/src/components/sections/banner/banner.stories.tsx +150 -0
  88. package/src/components/sections/banner/banner.test.tsx +185 -0
  89. package/src/components/sections/banner/banner.tsx +130 -0
  90. package/src/components/sections/banner/index.ts +2 -0
  91. package/src/components/sections/card-grid/card-grid.stories.tsx +351 -0
  92. package/src/components/sections/card-grid/card-grid.tsx +116 -0
  93. package/src/components/sections/card-grid/index.ts +1 -0
  94. package/src/components/sections/faq-section/faq-section.stories.tsx +453 -0
  95. package/src/components/sections/faq-section/faq-section.tsx +84 -0
  96. package/src/components/sections/faq-section/index.ts +2 -0
  97. package/src/components/sections/hero/__screenshots__/hero.visual.test.tsx/hero-a1-desktop-chromium-darwin.png +0 -0
  98. package/src/components/sections/hero/__screenshots__/hero.visual.test.tsx/hero-a1-desktop-chromium-linux.png +0 -0
  99. package/src/components/sections/hero/__screenshots__/hero.visual.test.tsx/hero-a1-mobile-chromium-darwin.png +0 -0
  100. package/src/components/sections/hero/__screenshots__/hero.visual.test.tsx/hero-a1-mobile-chromium-linux.png +0 -0
  101. package/src/components/sections/hero/__screenshots__/hero.visual.test.tsx/hero-a1-tablet-chromium-darwin.png +0 -0
  102. package/src/components/sections/hero/__screenshots__/hero.visual.test.tsx/hero-a1-tablet-chromium-linux.png +0 -0
  103. package/src/components/sections/hero/__screenshots__/hero.visual.test.tsx/hero-a2-desktop-chromium-darwin.png +0 -0
  104. package/src/components/sections/hero/__screenshots__/hero.visual.test.tsx/hero-a2-desktop-chromium-linux.png +0 -0
  105. package/src/components/sections/hero/__screenshots__/hero.visual.test.tsx/hero-a2-mobile-chromium-darwin.png +0 -0
  106. package/src/components/sections/hero/__screenshots__/hero.visual.test.tsx/hero-a2-mobile-chromium-linux.png +0 -0
  107. package/src/components/sections/hero/__screenshots__/hero.visual.test.tsx/hero-a2-tablet-chromium-darwin.png +0 -0
  108. package/src/components/sections/hero/__screenshots__/hero.visual.test.tsx/hero-a2-tablet-chromium-linux.png +0 -0
  109. package/src/components/sections/hero/__screenshots__/hero.visual.test.tsx/hero-a3-desktop-chromium-darwin.png +0 -0
  110. package/src/components/sections/hero/__screenshots__/hero.visual.test.tsx/hero-a3-desktop-chromium-linux.png +0 -0
  111. package/src/components/sections/hero/__screenshots__/hero.visual.test.tsx/hero-a3-mobile-chromium-darwin.png +0 -0
  112. package/src/components/sections/hero/__screenshots__/hero.visual.test.tsx/hero-a3-mobile-chromium-linux.png +0 -0
  113. package/src/components/sections/hero/__screenshots__/hero.visual.test.tsx/hero-a3-tablet-chromium-darwin.png +0 -0
  114. package/src/components/sections/hero/__screenshots__/hero.visual.test.tsx/hero-a3-tablet-chromium-linux.png +0 -0
  115. package/src/components/sections/hero/__screenshots__/hero.visual.test.tsx/hero-custom-class-chromium-darwin.png +0 -0
  116. package/src/components/sections/hero/__screenshots__/hero.visual.test.tsx/hero-custom-class-chromium-linux.png +0 -0
  117. package/src/components/sections/hero/__screenshots__/hero.visual.test.tsx/hero-default-chromium-linux.png +0 -0
  118. package/src/components/sections/hero/__screenshots__/hero.visual.test.tsx/hero-long-title-chromium-darwin.png +0 -0
  119. package/src/components/sections/hero/__screenshots__/hero.visual.test.tsx/hero-long-title-chromium-linux.png +0 -0
  120. package/src/components/sections/hero/hero.stories.tsx +274 -0
  121. package/src/components/sections/hero/hero.test.tsx +135 -0
  122. package/src/components/sections/hero/hero.tsx +453 -0
  123. package/src/components/sections/hero/hero.visual.test.tsx +140 -0
  124. package/src/components/sections/hero/index.ts +10 -0
  125. package/src/components/sections/prose/__screenshots__/prose.visual.test.tsx/prose-h3-heading-chromium-darwin.png +0 -0
  126. package/src/components/sections/prose/__screenshots__/prose.visual.test.tsx/prose-h3-heading-chromium-linux.png +0 -0
  127. package/src/components/sections/prose/__screenshots__/prose.visual.test.tsx/prose-multiple-paragraphs-chromium-darwin.png +0 -0
  128. package/src/components/sections/prose/__screenshots__/prose.visual.test.tsx/prose-multiple-paragraphs-chromium-linux.png +0 -0
  129. package/src/components/sections/prose/__screenshots__/prose.visual.test.tsx/prose-multiple-sections-chromium-darwin.png +0 -0
  130. package/src/components/sections/prose/__screenshots__/prose.visual.test.tsx/prose-multiple-sections-chromium-linux.png +0 -0
  131. package/src/components/sections/prose/__screenshots__/prose.visual.test.tsx/prose-single-section-chromium-darwin.png +0 -0
  132. package/src/components/sections/prose/__screenshots__/prose.visual.test.tsx/prose-single-section-chromium-linux.png +0 -0
  133. package/src/components/sections/prose/index.ts +6 -0
  134. package/src/components/sections/prose/prose.stories.tsx +144 -0
  135. package/src/components/sections/prose/prose.test.tsx +178 -0
  136. package/src/components/sections/prose/prose.tsx +88 -0
  137. package/src/components/sections/prose/prose.visual.test.tsx +105 -0
  138. package/src/components/sections/river/index.ts +1 -0
  139. package/src/components/sections/river/river.stories.tsx +237 -0
  140. package/src/components/sections/river/river.test.tsx +268 -0
  141. package/src/components/sections/river/river.tsx +173 -0
  142. package/src/components/sections/tout/index.ts +1 -0
  143. package/src/components/sections/tout/tout.stories.tsx +171 -0
  144. package/src/components/sections/tout/tout.test.tsx +242 -0
  145. package/src/components/sections/tout/tout.tsx +270 -0
  146. package/src/components/sections/two-column-section/index.ts +5 -0
  147. package/src/components/sections/two-column-section/two-column-section.stories.tsx +285 -0
  148. package/src/components/sections/two-column-section/two-column-section.tsx +162 -0
  149. package/src/hooks/index.ts +1 -0
  150. package/src/hooks/use-event-listener.ts +73 -0
  151. package/src/index.ts +155 -0
  152. package/src/lib/theme.ts +1000 -0
  153. package/src/lib/utils.ts +6 -0
  154. package/src/main.tsx +13 -0
  155. package/src/stories/GridSystem.stories.tsx +84 -0
  156. package/src/stories/Introduction.mdx +114 -0
  157. package/src/stories/ThemeProvider.stories.tsx +357 -0
  158. package/src/stories/TokenShowcase.stories.tsx +92 -0
  159. package/src/stories/TokenShowcase.tsx +1429 -0
  160. package/src/styles.css +11 -0
  161. package/src/theme/ThemeProvider.tsx +297 -0
  162. package/src/theme/hooks.ts +40 -0
  163. package/src/theme/index.ts +43 -0
  164. 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";
@@ -0,0 +1,2 @@
1
+ export { DevToolbar, type DevToolbarProps } from "./dev-toolbar";
2
+ export { GridOverlay, type GridOverlayProps } from "./grid-overlay";