@ponti-studios/ui 0.1.2 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (114) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +7 -7
  3. package/THIRD_PARTY_NOTICES.md +17 -0
  4. package/dist/index.js +1028 -638
  5. package/dist/index.js.map +1 -1
  6. package/package.json +30 -32
  7. package/src/animations.css +16 -434
  8. package/src/components/data-display/accordion.tsx +10 -7
  9. package/src/components/data-display/filter-chip.tsx +2 -2
  10. package/src/components/data-display/index.ts +5 -0
  11. package/src/components/data-display/metric-card.stories.tsx +48 -0
  12. package/src/components/data-display/metric-card.tsx +20 -0
  13. package/src/components/data-display/sort-controls.stories.tsx +45 -0
  14. package/src/components/data-display/sort-controls.tsx +99 -0
  15. package/src/components/data-display/sort-row.tsx +74 -0
  16. package/src/components/data-display/table.tsx +5 -5
  17. package/src/components/feedback/alert.tsx +4 -4
  18. package/src/components/feedback/empty-state.tsx +6 -6
  19. package/src/components/feedback/file-upload-status-badge.stories.tsx +28 -0
  20. package/src/components/feedback/file-upload-status-badge.tsx +33 -0
  21. package/src/components/feedback/file-upload-status.stories.tsx +57 -0
  22. package/src/components/feedback/file-upload-status.tsx +152 -0
  23. package/src/components/feedback/index.ts +9 -0
  24. package/src/components/feedback/progress-bar.stories.tsx +39 -0
  25. package/src/components/feedback/progress-bar.tsx +21 -0
  26. package/src/components/feedback/progress.stories.tsx +8 -6
  27. package/src/components/feedback/progress.tsx +1 -1
  28. package/src/components/feedback/skeleton.stories.tsx +36 -0
  29. package/src/components/feedback/skeleton.tsx +7 -0
  30. package/src/components/feedback/spinner.tsx +2 -2
  31. package/src/components/feedback/update-guard.stories.tsx +32 -0
  32. package/src/components/feedback/update-guard.tsx +280 -0
  33. package/src/components/forms/calendar.tsx +11 -11
  34. package/src/components/forms/date-month-select.stories.tsx +26 -0
  35. package/src/components/forms/date-month-select.tsx +70 -0
  36. package/src/components/forms/date-picker.tsx +3 -3
  37. package/src/components/forms/drop-zone.stories.tsx +56 -0
  38. package/src/components/forms/drop-zone.tsx +146 -0
  39. package/src/components/forms/entity-select.stories.tsx +65 -0
  40. package/src/components/forms/entity-select.tsx +79 -0
  41. package/src/components/forms/field.tsx +3 -3
  42. package/src/components/forms/group-by-select.stories.tsx +26 -0
  43. package/src/components/forms/group-by-select.tsx +47 -0
  44. package/src/components/forms/index.ts +10 -0
  45. package/src/components/forms/input.stories.tsx +4 -4
  46. package/src/components/forms/input.tsx +1 -1
  47. package/src/components/forms/passkey-management.stories.tsx +81 -0
  48. package/src/components/forms/passkey-management.tsx +138 -0
  49. package/src/components/forms/radio-group.tsx +14 -13
  50. package/src/components/forms/select.stories.tsx +4 -4
  51. package/src/components/forms/select.tsx +45 -53
  52. package/src/components/forms/slider.stories.tsx +5 -2
  53. package/src/components/forms/slider.tsx +27 -17
  54. package/src/components/forms/stepper.tsx +4 -4
  55. package/src/components/forms/switch.stories.tsx +1 -1
  56. package/src/components/forms/switch.tsx +2 -2
  57. package/src/components/forms/textarea.stories.tsx +2 -2
  58. package/src/components/forms/textarea.tsx +1 -1
  59. package/src/components/layout/index.ts +3 -0
  60. package/src/components/layout/scroll-area.stories.tsx +4 -4
  61. package/src/components/layout/scroll-area.tsx +23 -21
  62. package/src/components/layout/section-intro.stories.tsx +29 -0
  63. package/src/components/layout/section-intro.tsx +31 -0
  64. package/src/components/layout/surface-panel.stories.tsx +32 -0
  65. package/src/components/layout/surface-panel.tsx +15 -0
  66. package/src/components/navigation/app-navigation.stories.tsx +2 -2
  67. package/src/components/navigation/app-navigation.tsx +13 -13
  68. package/src/components/navigation/index.ts +4 -0
  69. package/src/components/navigation/pagination-controls.stories.tsx +41 -0
  70. package/src/components/navigation/pagination-controls.tsx +50 -0
  71. package/src/components/navigation/route-link.stories.tsx +30 -0
  72. package/src/components/navigation/route-link.tsx +42 -0
  73. package/src/components/navigation/tabs.stories.tsx +5 -5
  74. package/src/components/navigation/tabs.tsx +7 -7
  75. package/src/components/overlays/alert-dialog.tsx +21 -46
  76. package/src/components/overlays/command.tsx +51 -82
  77. package/src/components/overlays/dialog.tsx +5 -5
  78. package/src/components/overlays/dropdown-menu.stories.tsx +0 -5
  79. package/src/components/overlays/dropdown-menu.tsx +44 -142
  80. package/src/components/overlays/popover.stories.tsx +2 -2
  81. package/src/components/overlays/popover.tsx +34 -16
  82. package/src/components/overlays/sheet.stories.tsx +2 -2
  83. package/src/components/overlays/sheet.tsx +35 -17
  84. package/src/components/primitives/avatar.tsx +3 -3
  85. package/src/components/primitives/badge.tsx +7 -11
  86. package/src/components/primitives/button.tsx +41 -13
  87. package/src/components/primitives/card.stories.tsx +2 -2
  88. package/src/components/primitives/card.tsx +2 -2
  89. package/src/components/primitives/index.ts +2 -0
  90. package/src/components/primitives/label.stories.tsx +1 -1
  91. package/src/components/primitives/label.tsx +5 -6
  92. package/src/components/primitives/status-badge.stories.tsx +52 -0
  93. package/src/components/primitives/status-badge.tsx +33 -0
  94. package/src/constants/chart-colors.ts +5 -5
  95. package/src/hooks/index.ts +1 -0
  96. package/src/hooks/use-sort.ts +20 -0
  97. package/src/index.ts +30 -11
  98. package/src/styles.css +76 -1040
  99. package/src/tokens/color-systems.stories.tsx +9 -119
  100. package/src/tokens/foundations.stories.tsx +21 -113
  101. package/src/tokens/generated/dark.colors.generated.ts +56 -0
  102. package/src/tokens/generated/dark.css +54 -0
  103. package/src/tokens/generated/foundations.css +148 -0
  104. package/src/tokens/generated/foundations.generated.ts +443 -0
  105. package/src/tokens/generated/light.colors.generated.ts +56 -0
  106. package/src/tokens/generated/light.css +54 -0
  107. package/src/tokens/index.ts +74 -16
  108. package/src/tokens/layout.stories.tsx +3 -108
  109. package/src/tokens/colors.ts +0 -156
  110. package/src/tokens/motion.ts +0 -51
  111. package/src/tokens/radii.ts +0 -10
  112. package/src/tokens/shadows.ts +0 -34
  113. package/src/tokens/spacing.ts +0 -30
  114. package/src/tokens/typography.ts +0 -42
package/package.json CHANGED
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "name": "@ponti-studios/ui",
3
- "version": "0.1.2",
3
+ "version": "0.3.0",
4
4
  "private": false,
5
5
  "description": "Shared UI component library for ponti-studios labs",
6
- "license": "UNLICENSED",
6
+ "license": "MIT",
7
7
  "repository": {
8
8
  "type": "git",
9
9
  "url": "git+https://github.com/ponti-studios/labs.git",
@@ -15,7 +15,8 @@
15
15
  },
16
16
  "files": [
17
17
  "dist",
18
- "src"
18
+ "src",
19
+ "THIRD_PARTY_NOTICES.md"
19
20
  ],
20
21
  "type": "module",
21
22
  "sideEffects": [
@@ -72,48 +73,41 @@
72
73
  },
73
74
  "dependencies": {
74
75
  "@base-ui/react": "^1.6.0",
75
- "@radix-ui/react-accordion": "^1.2.14",
76
- "@radix-ui/react-alert-dialog": "^1.1.17",
77
- "@radix-ui/react-avatar": "^1.2.2",
78
- "@radix-ui/react-dialog": "^1.1.17",
79
- "@radix-ui/react-dropdown-menu": "^2.1.16",
80
- "@radix-ui/react-label": "^2.1.10",
81
- "@radix-ui/react-popover": "^1.1.19",
82
- "@radix-ui/react-progress": "^1.1.10",
83
- "@radix-ui/react-radio-group": "^1.4.1",
84
- "@radix-ui/react-select": "^2.3.1",
85
- "@radix-ui/react-slider": "^1.4.1",
86
- "@radix-ui/react-slot": "^1.3.0",
87
- "@radix-ui/react-switch": "^1.2.6",
88
- "@radix-ui/react-tabs": "^1.1.15",
89
76
  "class-variance-authority": "^0.7.1",
90
77
  "clsx": "^2.1.1",
91
- "cmdk": "^1.1.1",
92
78
  "date-fns": "^4.1.0",
93
- "framer-motion": "^12.42.0",
94
- "lucide-react": "^1.21.0",
95
- "react-day-picker": "^9.11.1",
79
+ "framer-motion": "^12.42.2",
80
+ "lucide-react": "^1.24.0",
81
+ "react-day-picker": "^10.0.1",
96
82
  "tailwind-merge": "^3.6.0"
97
83
  },
98
84
  "devDependencies": {
99
85
  "@chromatic-com/storybook": "^5.2.1",
100
- "@storybook/addon-docs": "^10.4.6",
101
- "@storybook/addon-links": "^10.4.6",
102
- "@storybook/addon-onboarding": "^10.4.6",
103
- "@storybook/react-vite": "^10.4.6",
104
- "@tailwindcss/vite": "^4.3.1",
105
- "@types/node": "^26.0.1",
86
+ "@storybook/addon-a11y": "10.5.3",
87
+ "@storybook/addon-docs": "^10.5.3",
88
+ "@storybook/addon-links": "^10.5.3",
89
+ "@storybook/addon-onboarding": "^10.5.3",
90
+ "@storybook/addon-themes": "10.5.3",
91
+ "@storybook/addon-vitest": "10.5.3",
92
+ "@storybook/react-vite": "^10.5.3",
93
+ "@tailwindcss/vite": "^4.3.3",
94
+ "@types/node": "^26.1.1",
106
95
  "@types/react": "^19.2.17",
107
96
  "@types/react-dom": "^19.2.3",
108
97
  "@vitejs/plugin-react": "^6.0.3",
109
- "oxfmt": "0.56.0",
110
- "oxlint": "^1.71.0",
98
+ "@vitest/browser": "4.1.10",
99
+ "@vitest/browser-playwright": "4.1.10",
100
+ "oxfmt": "0.59.0",
101
+ "oxlint": "^1.74.0",
102
+ "playwright": "^1.61.1",
111
103
  "react": "^19.2.7",
112
104
  "react-dom": "^19.2.7",
113
- "storybook": "^10.4.6",
114
- "tailwindcss": "^4.3.1",
105
+ "storybook": "^10.5.3",
106
+ "style-dictionary": "5.5.0",
107
+ "tailwindcss": "^4.3.3",
115
108
  "typescript": "6.0.3",
116
- "vite": "^8.1.0"
109
+ "vite": "^8.1.5",
110
+ "vitest": "^4.1.10"
117
111
  },
118
112
  "peerDependencies": {
119
113
  "react": "^18.2.0 || ^19.0.0",
@@ -123,6 +117,10 @@
123
117
  "build": "vite build",
124
118
  "dev": "vite",
125
119
  "storybook": "storybook dev -p 6006",
120
+ "check:source": "node tokens/check-source.mjs",
121
+ "test:a11y": "vitest --run --config vitest.config.ts",
122
+ "tokens:build": "style-dictionary build --config style-dictionary.foundations.json && style-dictionary build --config style-dictionary.light.json && style-dictionary build --config style-dictionary.dark.json",
123
+ "tokens:check": "pnpm run tokens:build && node tokens/check-foundations.mjs && git diff --exit-code -- src/tokens/generated",
126
124
  "typecheck": "tsc --noEmit",
127
125
  "lint": "oxlint src --fix",
128
126
  "format": "oxfmt src --write"
@@ -1,437 +1,19 @@
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
- }
1
+ @keyframes accordion-down { from { height: 0; } to { height: var(--accordion-content-height); } }
2
+ @keyframes accordion-up { from { height: var(--accordion-content-height); } to { height: 0; } }
3
+ @keyframes fade-in { from { opacity: 0; } to { opacity: 1; } }
4
+ @keyframes fade-out { from { opacity: 1; } to { opacity: 0; } }
5
+ @keyframes zoom-in { from { opacity: 0; transform: scale(0.95); } to { opacity: 1; transform: scale(1); } }
6
+ @keyframes zoom-out { from { opacity: 1; transform: scale(1); } to { opacity: 0; transform: scale(0.95); } }
7
+
8
+ .animate-in { animation: fade-in 150ms ease-out, zoom-in 150ms ease-out; }
9
+ .animate-out { animation: fade-out 120ms ease-in, zoom-out 120ms ease-in; }
10
+ .fade-in-0 { animation-name: fade-in; }
11
+ .fade-out-0 { animation-name: fade-out; }
12
+ .zoom-in-95 { animation-name: zoom-in; }
13
+ .zoom-out-95 { animation-name: zoom-out; }
14
+ .data-open\:animate-in[data-open], .data-open\:animate-in[data-state="open"] { animation: fade-in 150ms ease-out, zoom-in 150ms ease-out; }
15
+ .data-closed\:animate-out[data-closed], .data-closed\:animate-out[data-state="closed"] { animation: fade-out 120ms ease-in, zoom-out 120ms ease-in; }
430
16
 
431
17
  @media (prefers-reduced-motion: reduce) {
432
- ::view-transition-group(*),
433
- ::view-transition-old(*),
434
- ::view-transition-new(*) {
435
- animation: none !important;
436
- }
18
+ .animate-in, .animate-out, [data-open], [data-closed] { animation: none !important; }
437
19
  }
@@ -1,11 +1,14 @@
1
- import * as AccordionPrimitive from "@radix-ui/react-accordion";
1
+ import { Accordion as AccordionPrimitive } from "@base-ui/react/accordion";
2
2
  import { ChevronDown } from "lucide-react";
3
3
  import * as React from "react";
4
4
 
5
5
  import { cn } from "../../lib/utils";
6
6
 
7
- function Accordion({ ...props }: React.ComponentProps<typeof AccordionPrimitive.Root>) {
8
- return <AccordionPrimitive.Root data-slot="accordion" {...props} className="w-full" />;
7
+ function Accordion({ type, collapsible: _collapsible, ...props }: React.ComponentProps<typeof AccordionPrimitive.Root> & {
8
+ type?: "single" | "multiple";
9
+ collapsible?: boolean;
10
+ }) {
11
+ return <AccordionPrimitive.Root data-slot="accordion" multiple={type === "multiple"} {...props} className="w-full" />;
9
12
  }
10
13
 
11
14
  function AccordionItem({
@@ -16,7 +19,7 @@ function AccordionItem({
16
19
  <AccordionPrimitive.Item
17
20
  data-slot="accordion-item"
18
21
  className={cn(
19
- "border-default bg-canvas mb-2 overflow-hidden rounded-md border last:mb-0",
22
+ "mb-2 overflow-hidden rounded-md border last:mb-0",
20
23
  className,
21
24
  )}
22
25
  {...props}
@@ -50,15 +53,15 @@ function AccordionContent({
50
53
  className,
51
54
  children,
52
55
  ...props
53
- }: React.ComponentProps<typeof AccordionPrimitive.Content>) {
56
+ }: React.ComponentProps<typeof AccordionPrimitive.Panel>) {
54
57
  return (
55
- <AccordionPrimitive.Content
58
+ <AccordionPrimitive.Panel
56
59
  data-slot="accordion-content"
57
60
  className="animate-accordion-down overflow-hidden text-sm"
58
61
  {...props}
59
62
  >
60
63
  <div className={cn("px-4 pt-2 pb-3", className)}>{children}</div>
61
- </AccordionPrimitive.Content>
64
+ </AccordionPrimitive.Panel>
62
65
  );
63
66
  }
64
67
 
@@ -23,14 +23,14 @@ export function FilterChip({ label, onRemove, onClick, className }: FilterChipPr
23
23
  return (
24
24
  <span
25
25
  className={cn(
26
- "border-subtle bg-panel text-primary inline-flex items-center rounded-full border text-sm font-medium",
26
+ "border-border bg-secondary text-secondary-foreground inline-flex items-center rounded-full border text-sm font-medium",
27
27
  className,
28
28
  )}
29
29
  >
30
30
  {labelContent}
31
31
  <button
32
32
  type="button"
33
- className="text-secondary hover:text-primary mr-1 flex size-10 items-center justify-center rounded-full"
33
+ className="text-muted-foreground hover:text-foreground mr-1 flex size-10 items-center justify-center rounded-full"
34
34
  onClick={onRemove}
35
35
  aria-label={`Remove filter: ${typeof label === "string" ? label : "selected filter"}`}
36
36
  >
@@ -1,6 +1,11 @@
1
1
  export { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "./accordion";
2
2
  export { FilterChip } from "./filter-chip";
3
3
  export type { FilterChipProps } from "./filter-chip";
4
+ export { MetricCard } from "./metric-card";
5
+ export { SortControls } from "./sort-controls";
6
+ export type { SortControlsProps } from "./sort-controls";
7
+ export { SortRow } from "./sort-row";
8
+ export type { SortRowProps } from "./sort-row";
4
9
  export {
5
10
  Table,
6
11
  TableBody,
@@ -0,0 +1,48 @@
1
+ import type { Meta, StoryObj } from "@storybook/react-vite";
2
+
3
+ import { MetricCard } from "./metric-card";
4
+
5
+ const meta = {
6
+ title: "DataDisplay/MetricCard",
7
+ component: MetricCard,
8
+ tags: ["autodocs"],
9
+ } satisfies Meta<typeof MetricCard>;
10
+
11
+ export default meta;
12
+ type Story = StoryObj<typeof meta>;
13
+
14
+ export const Default: Story = {
15
+ args: {
16
+ label: "Net worth",
17
+ value: "$48,210",
18
+ },
19
+ };
20
+
21
+ export const WithChange: Story = {
22
+ args: {
23
+ label: "Monthly spend",
24
+ value: "$3,120",
25
+ change: "+4.2% vs last month",
26
+ },
27
+ };
28
+
29
+ export const Empty: Story = {
30
+ args: {
31
+ label: "Runway",
32
+ value: undefined,
33
+ },
34
+ };
35
+
36
+ export const Grid: Story = {
37
+ args: {
38
+ label: "Net worth",
39
+ value: "$48,210",
40
+ },
41
+ render: () => (
42
+ <div className="grid w-[520px] grid-cols-3 gap-3">
43
+ <MetricCard label="Net worth" value="$48,210" change="+2.1%" />
44
+ <MetricCard label="Income" value="$6,400" />
45
+ <MetricCard label="Expenses" value="$3,120" change="-1.4%" />
46
+ </div>
47
+ ),
48
+ };