@ponti-studios/ui 0.1.2

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 (108) hide show
  1. package/README.md +27 -0
  2. package/dist/index.js +2118 -0
  3. package/dist/index.js.map +1 -0
  4. package/package.json +130 -0
  5. package/src/animations.css +437 -0
  6. package/src/components/data-display/accordion.stories.tsx +57 -0
  7. package/src/components/data-display/accordion.tsx +65 -0
  8. package/src/components/data-display/filter-chip.tsx +41 -0
  9. package/src/components/data-display/index.ts +13 -0
  10. package/src/components/data-display/table.stories.tsx +75 -0
  11. package/src/components/data-display/table.tsx +84 -0
  12. package/src/components/feedback/alert.stories.tsx +51 -0
  13. package/src/components/feedback/alert.tsx +60 -0
  14. package/src/components/feedback/empty-state.tsx +66 -0
  15. package/src/components/feedback/index.ts +6 -0
  16. package/src/components/feedback/progress.stories.tsx +37 -0
  17. package/src/components/feedback/progress.tsx +30 -0
  18. package/src/components/feedback/spinner.stories.tsx +52 -0
  19. package/src/components/feedback/spinner.tsx +48 -0
  20. package/src/components/forms/calendar.stories.tsx +30 -0
  21. package/src/components/forms/calendar.tsx +67 -0
  22. package/src/components/forms/date-picker.stories.tsx +48 -0
  23. package/src/components/forms/date-picker.tsx +60 -0
  24. package/src/components/forms/field.stories.tsx +62 -0
  25. package/src/components/forms/field.tsx +65 -0
  26. package/src/components/forms/field.types.ts +7 -0
  27. package/src/components/forms/index.ts +24 -0
  28. package/src/components/forms/input.stories.tsx +47 -0
  29. package/src/components/forms/input.tsx +20 -0
  30. package/src/components/forms/radio-group.stories.tsx +48 -0
  31. package/src/components/forms/radio-group.tsx +38 -0
  32. package/src/components/forms/select.stories.tsx +88 -0
  33. package/src/components/forms/select.tsx +147 -0
  34. package/src/components/forms/slider.stories.tsx +67 -0
  35. package/src/components/forms/slider.tsx +25 -0
  36. package/src/components/forms/stepper.stories.tsx +111 -0
  37. package/src/components/forms/stepper.tsx +66 -0
  38. package/src/components/forms/switch.stories.tsx +144 -0
  39. package/src/components/forms/switch.tsx +36 -0
  40. package/src/components/forms/text-field.stories.tsx +87 -0
  41. package/src/components/forms/text-field.tsx +40 -0
  42. package/src/components/forms/text-field.types.ts +9 -0
  43. package/src/components/forms/textarea.stories.tsx +150 -0
  44. package/src/components/forms/textarea.tsx +18 -0
  45. package/src/components/layout/index.ts +2 -0
  46. package/src/components/layout/scroll-area.stories.tsx +83 -0
  47. package/src/components/layout/scroll-area.tsx +40 -0
  48. package/src/components/navigation/app-navigation.stories.tsx +81 -0
  49. package/src/components/navigation/app-navigation.tsx +155 -0
  50. package/src/components/navigation/index.ts +3 -0
  51. package/src/components/navigation/tabs.stories.tsx +69 -0
  52. package/src/components/navigation/tabs.tsx +49 -0
  53. package/src/components/overlays/alert-dialog.stories.tsx +45 -0
  54. package/src/components/overlays/alert-dialog.tsx +119 -0
  55. package/src/components/overlays/command.stories.tsx +65 -0
  56. package/src/components/overlays/command.tsx +130 -0
  57. package/src/components/overlays/dialog.stories.tsx +111 -0
  58. package/src/components/overlays/dialog.tsx +162 -0
  59. package/src/components/overlays/dropdown-menu.stories.tsx +79 -0
  60. package/src/components/overlays/dropdown-menu.tsx +214 -0
  61. package/src/components/overlays/index.ts +65 -0
  62. package/src/components/overlays/popover.stories.tsx +39 -0
  63. package/src/components/overlays/popover.tsx +40 -0
  64. package/src/components/overlays/sheet.stories.tsx +68 -0
  65. package/src/components/overlays/sheet.tsx +98 -0
  66. package/src/components/primitives/avatar.stories.tsx +71 -0
  67. package/src/components/primitives/avatar.tsx +132 -0
  68. package/src/components/primitives/badge.stories.tsx +33 -0
  69. package/src/components/primitives/badge.tsx +44 -0
  70. package/src/components/primitives/button.stories.tsx +113 -0
  71. package/src/components/primitives/button.tsx +95 -0
  72. package/src/components/primitives/button.types.ts +10 -0
  73. package/src/components/primitives/card.stories.tsx +59 -0
  74. package/src/components/primitives/card.tsx +69 -0
  75. package/src/components/primitives/index.ts +13 -0
  76. package/src/components/primitives/label.stories.tsx +47 -0
  77. package/src/components/primitives/label.tsx +21 -0
  78. package/src/constants/chart-colors.ts +39 -0
  79. package/src/env.d.ts +9 -0
  80. package/src/hooks/index.ts +6 -0
  81. package/src/hooks/sort.types.ts +12 -0
  82. package/src/hooks/use-api-client.ts +122 -0
  83. package/src/hooks/use-debounced-value.ts +12 -0
  84. package/src/hooks/use-filter-state.ts +68 -0
  85. package/src/hooks/use-media-query.ts +65 -0
  86. package/src/hooks/use-mobile.tsx +19 -0
  87. package/src/index.ts +25 -0
  88. package/src/lib/auth.ts +5 -0
  89. package/src/lib/clipboard.ts +8 -0
  90. package/src/lib/create-memory-storage.ts +24 -0
  91. package/src/lib/index.ts +4 -0
  92. package/src/lib/utils.ts +6 -0
  93. package/src/storybook/controls.ts +117 -0
  94. package/src/storybook/options.ts +52 -0
  95. package/src/styles.css +1052 -0
  96. package/src/tokens/color-systems.stories.tsx +133 -0
  97. package/src/tokens/colors.ts +156 -0
  98. package/src/tokens/foundations.stories.tsx +151 -0
  99. package/src/tokens/index.ts +16 -0
  100. package/src/tokens/layout.stories.tsx +118 -0
  101. package/src/tokens/motion.ts +51 -0
  102. package/src/tokens/radii.ts +10 -0
  103. package/src/tokens/shadows.ts +34 -0
  104. package/src/tokens/spacing.ts +30 -0
  105. package/src/tokens/typography.ts +42 -0
  106. package/src/translations/en.ts +34 -0
  107. package/src/translations/index.ts +32 -0
  108. package/src/types/upload.ts +42 -0
@@ -0,0 +1,437 @@
1
+ @media (prefers-reduced-motion: reduce) {
2
+ .motion-anim-enter,
3
+ .motion-anim-exit,
4
+ .motion-anim-enter-top,
5
+ .motion-anim-enter-bottom,
6
+ .motion-anim-enter-left,
7
+ .motion-anim-enter-right,
8
+ .motion-anim-breezy,
9
+ .motion-anim-breezy-loop,
10
+ .motion-anim-breezy-progress,
11
+ .motion-anim-breezy-stagger > * {
12
+ animation: none !important;
13
+ opacity: 1 !important;
14
+ transform: none !important;
15
+ }
16
+ }
17
+
18
+ @keyframes motion-enter {
19
+ from {
20
+ opacity: 0;
21
+ transform: translateY(6px);
22
+ }
23
+ to {
24
+ opacity: 1;
25
+ transform: translateY(0);
26
+ }
27
+ }
28
+
29
+ @keyframes motion-exit {
30
+ from {
31
+ opacity: 1;
32
+ transform: translateY(0);
33
+ }
34
+ to {
35
+ opacity: 0;
36
+ transform: translateY(4px);
37
+ }
38
+ }
39
+
40
+ @keyframes motion-enter-top {
41
+ from {
42
+ opacity: 0;
43
+ transform: translateY(-6px);
44
+ }
45
+ to {
46
+ opacity: 1;
47
+ transform: translateY(0);
48
+ }
49
+ }
50
+
51
+ @keyframes motion-exit-top {
52
+ from {
53
+ opacity: 1;
54
+ transform: translateY(0);
55
+ }
56
+ to {
57
+ opacity: 0;
58
+ transform: translateY(-4px);
59
+ }
60
+ }
61
+
62
+ @keyframes motion-enter-bottom {
63
+ from {
64
+ opacity: 0;
65
+ transform: translateY(6px);
66
+ }
67
+ to {
68
+ opacity: 1;
69
+ transform: translateY(0);
70
+ }
71
+ }
72
+
73
+ @keyframes motion-exit-bottom {
74
+ from {
75
+ opacity: 1;
76
+ transform: translateY(0);
77
+ }
78
+ to {
79
+ opacity: 0;
80
+ transform: translateY(6px);
81
+ }
82
+ }
83
+
84
+ @keyframes motion-enter-left {
85
+ from {
86
+ opacity: 0;
87
+ transform: translateX(-6px);
88
+ }
89
+ to {
90
+ opacity: 1;
91
+ transform: translateX(0);
92
+ }
93
+ }
94
+
95
+ @keyframes motion-exit-left {
96
+ from {
97
+ opacity: 1;
98
+ transform: translateX(0);
99
+ }
100
+ to {
101
+ opacity: 0;
102
+ transform: translateX(-4px);
103
+ }
104
+ }
105
+
106
+ @keyframes motion-enter-right {
107
+ from {
108
+ opacity: 0;
109
+ transform: translateX(6px);
110
+ }
111
+ to {
112
+ opacity: 1;
113
+ transform: translateX(0);
114
+ }
115
+ }
116
+
117
+ @keyframes motion-exit-right {
118
+ from {
119
+ opacity: 1;
120
+ transform: translateX(0);
121
+ }
122
+ to {
123
+ opacity: 0;
124
+ transform: translateX(4px);
125
+ }
126
+ }
127
+
128
+ /*
129
+ * Sheet/drawer slide — full off-canvas travel (not the 4-6px "nudge" used by
130
+ * the motion-enter/exit-* keyframes above, which are meant for small popovers).
131
+ * Gated on the real [data-state] attribute so open/closed never race — see
132
+ * the note above the data-state fade/zoom rules for why that matters here.
133
+ */
134
+ @keyframes sheet-slide-in-from-right {
135
+ from {
136
+ transform: translateX(100%);
137
+ }
138
+ to {
139
+ transform: translateX(0);
140
+ }
141
+ }
142
+ @keyframes sheet-slide-out-to-right {
143
+ from {
144
+ transform: translateX(0);
145
+ }
146
+ to {
147
+ transform: translateX(100%);
148
+ }
149
+ }
150
+ @keyframes sheet-slide-in-from-bottom {
151
+ from {
152
+ transform: translateY(100%);
153
+ }
154
+ to {
155
+ transform: translateY(0);
156
+ }
157
+ }
158
+ @keyframes sheet-slide-out-to-bottom {
159
+ from {
160
+ transform: translateY(0);
161
+ }
162
+ to {
163
+ transform: translateY(100%);
164
+ }
165
+ }
166
+
167
+ .sheet-responsive[data-state="open"] {
168
+ animation: sheet-slide-in-from-bottom 480ms cubic-bezier(0.16, 1, 0.3, 1) both;
169
+ }
170
+ .sheet-responsive[data-state="closed"] {
171
+ animation: sheet-slide-out-to-bottom 280ms cubic-bezier(0.4, 0, 1, 1) both;
172
+ }
173
+
174
+ @media (min-width: 768px) {
175
+ .sheet-responsive[data-state="open"] {
176
+ animation-name: sheet-slide-in-from-right;
177
+ }
178
+
179
+ .sheet-responsive[data-state="closed"] {
180
+ animation-name: sheet-slide-out-to-right;
181
+ }
182
+ }
183
+
184
+ @media (prefers-reduced-motion: reduce) {
185
+ .sheet-responsive {
186
+ animation: none !important;
187
+ transform: none !important;
188
+ }
189
+ }
190
+
191
+ @keyframes motion-breezy-wave {
192
+ 0% {
193
+ transform: translateY(0);
194
+ opacity: 0.9;
195
+ }
196
+ 40% {
197
+ transform: translateY(-4px);
198
+ opacity: 1;
199
+ }
200
+ 70% {
201
+ transform: translateY(2px);
202
+ opacity: 0.92;
203
+ }
204
+ 100% {
205
+ transform: translateY(0);
206
+ opacity: 0.9;
207
+ }
208
+ }
209
+
210
+ @keyframes motion-breezy-stream {
211
+ 0% {
212
+ transform: translateX(-100%);
213
+ }
214
+ 100% {
215
+ transform: translateX(100%);
216
+ }
217
+ }
218
+
219
+ .motion-anim-enter {
220
+ animation: motion-enter 150ms cubic-bezier(0, 0, 0.2, 1) both;
221
+ }
222
+ .motion-anim-exit {
223
+ animation: motion-exit 120ms cubic-bezier(0.4, 0, 1, 1) both;
224
+ }
225
+ .motion-anim-enter-top {
226
+ animation: motion-enter-top 150ms cubic-bezier(0, 0, 0.2, 1) both;
227
+ }
228
+ .motion-anim-exit-top {
229
+ animation: motion-exit-top 120ms cubic-bezier(0.4, 0, 1, 1) both;
230
+ }
231
+ .motion-anim-enter-bottom {
232
+ animation: motion-enter-bottom 150ms cubic-bezier(0, 0, 0.2, 1) both;
233
+ }
234
+ .motion-anim-exit-bottom {
235
+ animation: motion-exit-bottom 120ms cubic-bezier(0.4, 0, 1, 1) both;
236
+ }
237
+ .motion-anim-enter-left {
238
+ animation: motion-enter-left 150ms cubic-bezier(0, 0, 0.2, 1) both;
239
+ }
240
+ .motion-anim-exit-left {
241
+ animation: motion-exit-left 120ms cubic-bezier(0.4, 0, 1, 1) both;
242
+ }
243
+ .motion-anim-enter-right {
244
+ animation: motion-enter-right 150ms cubic-bezier(0, 0, 0.2, 1) both;
245
+ }
246
+ .motion-anim-exit-right {
247
+ animation: motion-exit-right 120ms cubic-bezier(0.4, 0, 1, 1) both;
248
+ }
249
+
250
+ .motion-anim-breezy {
251
+ animation: motion-breezy-wave 320ms cubic-bezier(0.32, 0.72, 0.28, 1) forwards;
252
+ }
253
+ .motion-anim-breezy-loop {
254
+ animation: motion-breezy-wave 1.8s cubic-bezier(0.32, 0.72, 0.28, 1) infinite;
255
+ }
256
+ .motion-anim-breezy-progress {
257
+ animation: motion-breezy-stream 1.8s linear infinite;
258
+ }
259
+
260
+ .motion-anim-breezy-stagger > * {
261
+ animation: motion-breezy-wave 1.8s cubic-bezier(0.32, 0.72, 0.28, 1) infinite;
262
+ }
263
+ .motion-anim-breezy-stagger > *:nth-child(1) {
264
+ animation-delay: 0ms;
265
+ }
266
+ .motion-anim-breezy-stagger > *:nth-child(2) {
267
+ animation-delay: 120ms;
268
+ }
269
+ .motion-anim-breezy-stagger > *:nth-child(3) {
270
+ animation-delay: 240ms;
271
+ }
272
+ .motion-anim-breezy-stagger > *:nth-child(4) {
273
+ animation-delay: 360ms;
274
+ }
275
+ .motion-anim-breezy-stagger > *:nth-child(5) {
276
+ animation-delay: 480ms;
277
+ }
278
+
279
+ @keyframes accordion-down {
280
+ from {
281
+ opacity: 0;
282
+ transform: scaleY(0.8);
283
+ transform-origin: top;
284
+ }
285
+ to {
286
+ opacity: 1;
287
+ transform: scaleY(1);
288
+ }
289
+ }
290
+
291
+ @keyframes accordion-up {
292
+ from {
293
+ opacity: 1;
294
+ transform: scaleY(1);
295
+ transform-origin: top;
296
+ }
297
+ to {
298
+ opacity: 0;
299
+ transform: scaleY(0.8);
300
+ }
301
+ }
302
+
303
+ .animate-accordion-down,
304
+ [data-state="open"].animate-accordion-down {
305
+ animation: accordion-down 400ms cubic-bezier(0.32, 0.72, 0.28, 1) both;
306
+ }
307
+
308
+ .animate-accordion-up,
309
+ [data-state="closed"].animate-accordion-up {
310
+ animation: accordion-up 300ms cubic-bezier(0.32, 0.72, 0.28, 1) both;
311
+ }
312
+
313
+ .animate-in {
314
+ animation: motion-enter 150ms cubic-bezier(0, 0, 0.2, 1) both;
315
+ }
316
+ .animate-out {
317
+ animation: motion-exit 120ms cubic-bezier(0.4, 0, 1, 1) both;
318
+ }
319
+ .fade-in {
320
+ animation: motion-enter 150ms cubic-bezier(0, 0, 0.2, 1) both;
321
+ }
322
+ .fade-out {
323
+ animation: motion-exit 120ms cubic-bezier(0.4, 0, 1, 1) both;
324
+ }
325
+
326
+ /*
327
+ * These utility classes exist alongside their opposite (open + closed, or
328
+ * visible + hidden) on the same element at all times — only the `data-state`
329
+ * attribute value actually changes between renders. Each selector below must
330
+ * be qualified with the matching `[data-state="..."]` attribute so only the
331
+ * currently-active variant applies; without it, cascade order alone decides
332
+ * the winner (whichever rule is declared last), regardless of real state.
333
+ */
334
+ [data-state="open"].data-\[state\=open\]\:animate-in {
335
+ animation: motion-enter 150ms cubic-bezier(0, 0, 0.2, 1) both;
336
+ }
337
+ [data-state="closed"].data-\[state\=closed\]\:animate-out {
338
+ animation: motion-exit 120ms cubic-bezier(0.4, 0, 1, 1) both;
339
+ }
340
+
341
+ [data-state="open"].data-\[state\=open\]\:fade-in-0,
342
+ [data-state="open"].data-\[state\=open\]\:fade-in-90,
343
+ [data-state="visible"].data-\[state\=visible\]\:fade-in {
344
+ animation: motion-enter 150ms cubic-bezier(0, 0, 0.2, 1) both;
345
+ }
346
+
347
+ [data-state="closed"].data-\[state\=closed\]\:fade-out-0,
348
+ [data-state="hidden"].data-\[state\=hidden\]\:fade-out {
349
+ animation: motion-exit 120ms cubic-bezier(0.4, 0, 1, 1) both;
350
+ }
351
+
352
+ [data-state="open"].data-\[state\=open\]\:zoom-in-90,
353
+ [data-state="open"].data-\[state\=open\]\:zoom-in-95 {
354
+ animation: motion-enter 150ms cubic-bezier(0, 0, 0.2, 1) both;
355
+ }
356
+ [data-state="closed"].data-\[state\=closed\]\:zoom-out-95 {
357
+ animation: motion-exit 120ms cubic-bezier(0.4, 0, 1, 1) both;
358
+ }
359
+
360
+ /*
361
+ * Same issue as the data-state pairs above: Popover/Tooltip/DropdownMenu
362
+ * content applies all four data-[side=...] classes simultaneously (only the
363
+ * real `data-side` attribute changes at runtime), so each selector must be
364
+ * qualified with the matching [data-side="..."] attribute — otherwise
365
+ * cascade order alone picks a winner regardless of actual placement.
366
+ */
367
+ [data-side="bottom"].data-\[side\=bottom\]\:slide-in-from-top-2 {
368
+ animation: motion-enter-top 150ms cubic-bezier(0, 0, 0.2, 1) both;
369
+ }
370
+ [data-side="top"].data-\[side\=top\]\:slide-in-from-bottom-2 {
371
+ animation: motion-enter-bottom 150ms cubic-bezier(0, 0, 0.2, 1) both;
372
+ }
373
+ [data-side="left"].data-\[side\=left\]\:slide-in-from-right-2 {
374
+ animation: motion-enter-right 150ms cubic-bezier(0, 0, 0.2, 1) both;
375
+ }
376
+ [data-side="right"].data-\[side\=right\]\:slide-in-from-left-2 {
377
+ animation: motion-enter-left 150ms cubic-bezier(0, 0, 0.2, 1) both;
378
+ }
379
+
380
+ .slide-in-from-bottom-10,
381
+ .slide-in-from-bottom-0 {
382
+ animation: motion-enter-bottom 150ms cubic-bezier(0, 0, 0.2, 1) both;
383
+ }
384
+ .slide-in-from-left-52 {
385
+ animation: motion-enter-left 150ms cubic-bezier(0, 0, 0.2, 1) both;
386
+ }
387
+ .slide-in-from-right-52 {
388
+ animation: motion-enter-right 150ms cubic-bezier(0, 0, 0.2, 1) both;
389
+ }
390
+ .slide-out-to-left-52 {
391
+ animation: motion-exit-left 120ms cubic-bezier(0.4, 0, 1, 1) both;
392
+ }
393
+ .slide-out-to-right-52 {
394
+ animation: motion-exit-right 120ms cubic-bezier(0.4, 0, 1, 1) both;
395
+ }
396
+
397
+ .note-picker-panel {
398
+ transition:
399
+ opacity 150ms cubic-bezier(0, 0, 0.2, 1),
400
+ transform 150ms cubic-bezier(0, 0, 0.2, 1),
401
+ overlay 150ms allow-discrete,
402
+ display 150ms allow-discrete;
403
+ }
404
+
405
+ .note-picker-panel[open] {
406
+ opacity: 1;
407
+ transform: translateY(0) scale(1);
408
+ }
409
+
410
+ @starting-style {
411
+ .note-picker-panel[open] {
412
+ opacity: 0;
413
+ transform: translateY(8px) scale(0.98);
414
+ }
415
+ }
416
+
417
+ /*
418
+ * View Transitions: opt-in only.
419
+ * The browser's default root crossfade animates every diffed pixel of the
420
+ * whole page using mix-blend-mode: plus-lighter, which visibly flashes white
421
+ * on any element whose color changes during the transition (e.g. a nav item
422
+ * going from inactive to active). Disabling the root group's animation makes
423
+ * transitions opt-in — only elements with an explicit view-transition-name
424
+ * (set inline via style.viewTransitionName) animate; everything else swaps
425
+ * instantly with no crossfade artifact.
426
+ */
427
+ ::view-transition-group(root) {
428
+ animation-duration: 0s;
429
+ }
430
+
431
+ @media (prefers-reduced-motion: reduce) {
432
+ ::view-transition-group(*),
433
+ ::view-transition-old(*),
434
+ ::view-transition-new(*) {
435
+ animation: none !important;
436
+ }
437
+ }
@@ -0,0 +1,57 @@
1
+ import type { Meta, StoryObj } from "@storybook/react-vite";
2
+
3
+ import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "./accordion";
4
+
5
+ const meta = {
6
+ title: "DataDisplay/Accordion",
7
+ component: Accordion,
8
+ tags: ["autodocs"],
9
+ args: {
10
+ type: "single",
11
+ },
12
+ } satisfies Meta<typeof Accordion>;
13
+
14
+ export default meta;
15
+ type Story = StoryObj<typeof meta>;
16
+
17
+ export const Default: Story = {
18
+ render: () => (
19
+ <Accordion type="single" collapsible className="w-full">
20
+ <AccordionItem value="item-1">
21
+ <AccordionTrigger>Is it accessible?</AccordionTrigger>
22
+ <AccordionContent>Yes. It adheres to the WAI-ARIA design pattern.</AccordionContent>
23
+ </AccordionItem>
24
+ <AccordionItem value="item-2">
25
+ <AccordionTrigger>Is it styled?</AccordionTrigger>
26
+ <AccordionContent>
27
+ Yes. It comes with default styles that match the other components.
28
+ </AccordionContent>
29
+ </AccordionItem>
30
+ <AccordionItem value="item-3">
31
+ <AccordionTrigger>Is it animated?</AccordionTrigger>
32
+ <AccordionContent>
33
+ Yes. It's animated by default, but you can disable it if you prefer.
34
+ </AccordionContent>
35
+ </AccordionItem>
36
+ </Accordion>
37
+ ),
38
+ };
39
+
40
+ export const Multiple: Story = {
41
+ render: () => (
42
+ <Accordion type="multiple" className="w-full max-w-md">
43
+ <AccordionItem value="item-1">
44
+ <AccordionTrigger>First item</AccordionTrigger>
45
+ <AccordionContent>Content for the first item.</AccordionContent>
46
+ </AccordionItem>
47
+ <AccordionItem value="item-2">
48
+ <AccordionTrigger>Second item</AccordionTrigger>
49
+ <AccordionContent>Content for the second item.</AccordionContent>
50
+ </AccordionItem>
51
+ <AccordionItem value="item-3">
52
+ <AccordionTrigger>Third item</AccordionTrigger>
53
+ <AccordionContent>Content for the third item.</AccordionContent>
54
+ </AccordionItem>
55
+ </Accordion>
56
+ ),
57
+ };
@@ -0,0 +1,65 @@
1
+ import * as AccordionPrimitive from "@radix-ui/react-accordion";
2
+ import { ChevronDown } from "lucide-react";
3
+ import * as React from "react";
4
+
5
+ import { cn } from "../../lib/utils";
6
+
7
+ function Accordion({ ...props }: React.ComponentProps<typeof AccordionPrimitive.Root>) {
8
+ return <AccordionPrimitive.Root data-slot="accordion" {...props} className="w-full" />;
9
+ }
10
+
11
+ function AccordionItem({
12
+ className,
13
+ ...props
14
+ }: React.ComponentProps<typeof AccordionPrimitive.Item>) {
15
+ return (
16
+ <AccordionPrimitive.Item
17
+ data-slot="accordion-item"
18
+ className={cn(
19
+ "border-default bg-canvas mb-2 overflow-hidden rounded-md border last:mb-0",
20
+ className,
21
+ )}
22
+ {...props}
23
+ />
24
+ );
25
+ }
26
+
27
+ function AccordionTrigger({
28
+ className,
29
+ children,
30
+ ...props
31
+ }: React.ComponentProps<typeof AccordionPrimitive.Trigger>) {
32
+ return (
33
+ <AccordionPrimitive.Header className="flex">
34
+ <AccordionPrimitive.Trigger
35
+ data-slot="accordion-trigger"
36
+ className={cn(
37
+ "flex w-full flex-1 items-center justify-between gap-4 px-4 py-3 text-left text-sm font-semibold disabled:pointer-events-none disabled:opacity-50 [&[data-state=open]>svg]:rotate-180",
38
+ className,
39
+ )}
40
+ {...props}
41
+ >
42
+ {children}
43
+ <ChevronDown className="pointer-events-none size-4 shrink-0 transition-transform duration-200" />
44
+ </AccordionPrimitive.Trigger>
45
+ </AccordionPrimitive.Header>
46
+ );
47
+ }
48
+
49
+ function AccordionContent({
50
+ className,
51
+ children,
52
+ ...props
53
+ }: React.ComponentProps<typeof AccordionPrimitive.Content>) {
54
+ return (
55
+ <AccordionPrimitive.Content
56
+ data-slot="accordion-content"
57
+ className="animate-accordion-down overflow-hidden text-sm"
58
+ {...props}
59
+ >
60
+ <div className={cn("px-4 pt-2 pb-3", className)}>{children}</div>
61
+ </AccordionPrimitive.Content>
62
+ );
63
+ }
64
+
65
+ export { Accordion, AccordionContent, AccordionItem, AccordionTrigger };
@@ -0,0 +1,41 @@
1
+ import { X } from "lucide-react";
2
+ import type { ReactNode } from "react";
3
+
4
+ import { cn } from "../../lib/utils";
5
+
6
+ export interface FilterChipProps {
7
+ label: ReactNode;
8
+ onRemove: () => void;
9
+ onClick?: () => void;
10
+ className?: string;
11
+ }
12
+
13
+ /** An applied filter with an optional edit action and an explicit remove action. */
14
+ export function FilterChip({ label, onRemove, onClick, className }: FilterChipProps) {
15
+ const labelContent = onClick ? (
16
+ <button type="button" className="min-h-11 px-3 text-left" onClick={onClick}>
17
+ {label}
18
+ </button>
19
+ ) : (
20
+ <span className="flex min-h-11 items-center px-3">{label}</span>
21
+ );
22
+
23
+ return (
24
+ <span
25
+ className={cn(
26
+ "border-subtle bg-panel text-primary inline-flex items-center rounded-full border text-sm font-medium",
27
+ className,
28
+ )}
29
+ >
30
+ {labelContent}
31
+ <button
32
+ type="button"
33
+ className="text-secondary hover:text-primary mr-1 flex size-10 items-center justify-center rounded-full"
34
+ onClick={onRemove}
35
+ aria-label={`Remove filter: ${typeof label === "string" ? label : "selected filter"}`}
36
+ >
37
+ <X className="size-4" aria-hidden />
38
+ </button>
39
+ </span>
40
+ );
41
+ }
@@ -0,0 +1,13 @@
1
+ export { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "./accordion";
2
+ export { FilterChip } from "./filter-chip";
3
+ export type { FilterChipProps } from "./filter-chip";
4
+ export {
5
+ Table,
6
+ TableBody,
7
+ TableCaption,
8
+ TableCell,
9
+ TableFooter,
10
+ TableHead,
11
+ TableHeader,
12
+ TableRow,
13
+ } from "./table";
@@ -0,0 +1,75 @@
1
+ import type { Meta, StoryObj } from "@storybook/react-vite";
2
+
3
+ import {
4
+ Table,
5
+ TableBody,
6
+ TableCaption,
7
+ TableCell,
8
+ TableFooter,
9
+ TableHead,
10
+ TableHeader,
11
+ TableRow,
12
+ } from "./table";
13
+
14
+ const invoices = [
15
+ {
16
+ invoice: "INV001",
17
+ paymentStatus: "Paid",
18
+ totalAmount: "$250.00",
19
+ paymentMethod: "Credit Card",
20
+ },
21
+ { invoice: "INV002", paymentStatus: "Pending", totalAmount: "$150.00", paymentMethod: "PayPal" },
22
+ {
23
+ invoice: "INV003",
24
+ paymentStatus: "Unpaid",
25
+ totalAmount: "$350.00",
26
+ paymentMethod: "Bank Transfer",
27
+ },
28
+ {
29
+ invoice: "INV004",
30
+ paymentStatus: "Paid",
31
+ totalAmount: "$450.00",
32
+ paymentMethod: "Credit Card",
33
+ },
34
+ ];
35
+
36
+ const meta = {
37
+ title: "DataDisplay/Table",
38
+ component: Table,
39
+ tags: ["autodocs"],
40
+ } satisfies Meta<typeof Table>;
41
+
42
+ export default meta;
43
+ type Story = StoryObj<typeof meta>;
44
+
45
+ export const Default: Story = {
46
+ render: () => (
47
+ <Table>
48
+ <TableCaption>A list of your recent invoices.</TableCaption>
49
+ <TableHeader>
50
+ <TableRow>
51
+ <TableHead className="w-[100px]">Invoice</TableHead>
52
+ <TableHead>Status</TableHead>
53
+ <TableHead>Method</TableHead>
54
+ <TableHead className="text-right">Amount</TableHead>
55
+ </TableRow>
56
+ </TableHeader>
57
+ <TableBody>
58
+ {invoices.map((invoice) => (
59
+ <TableRow key={invoice.invoice}>
60
+ <TableCell className="font-medium">{invoice.invoice}</TableCell>
61
+ <TableCell>{invoice.paymentStatus}</TableCell>
62
+ <TableCell>{invoice.paymentMethod}</TableCell>
63
+ <TableCell className="text-right">{invoice.totalAmount}</TableCell>
64
+ </TableRow>
65
+ ))}
66
+ </TableBody>
67
+ <TableFooter>
68
+ <TableRow>
69
+ <TableCell colSpan={3}>Total</TableCell>
70
+ <TableCell className="text-right">$1,200.00</TableCell>
71
+ </TableRow>
72
+ </TableFooter>
73
+ </Table>
74
+ ),
75
+ };