@tribepad/themis 1.0.6 → 1.0.8

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 (98) hide show
  1. package/dist/elements/AlertDialog/index.js +1 -1
  2. package/dist/elements/AlertDialog/index.js.map +1 -1
  3. package/dist/elements/AlertDialog/index.mjs +1 -1
  4. package/dist/elements/AlertDialog/index.mjs.map +1 -1
  5. package/dist/elements/Chart/ChartContext.d.ts.map +1 -1
  6. package/dist/elements/Chart/ChartLineSeries.d.ts.map +1 -1
  7. package/dist/elements/Chart/index.js +1 -1
  8. package/dist/elements/Chart/index.js.map +1 -1
  9. package/dist/elements/Chart/index.mjs +1 -1
  10. package/dist/elements/Chart/index.mjs.map +1 -1
  11. package/dist/elements/DatePicker/index.js +1 -1
  12. package/dist/elements/DatePicker/index.js.map +1 -1
  13. package/dist/elements/DatePicker/index.mjs +1 -1
  14. package/dist/elements/DatePicker/index.mjs.map +1 -1
  15. package/dist/elements/FileField/index.js +1 -1
  16. package/dist/elements/FileField/index.js.map +1 -1
  17. package/dist/elements/FileField/index.mjs +1 -1
  18. package/dist/elements/FileField/index.mjs.map +1 -1
  19. package/dist/elements/Modal/Modal.styles.d.ts +2 -0
  20. package/dist/elements/Modal/Modal.styles.d.ts.map +1 -1
  21. package/dist/elements/Modal/index.js +1 -1
  22. package/dist/elements/Modal/index.js.map +1 -1
  23. package/dist/elements/Modal/index.mjs +1 -1
  24. package/dist/elements/Modal/index.mjs.map +1 -1
  25. package/dist/elements/NumberField/NumberField.d.ts.map +1 -1
  26. package/dist/elements/NumberField/NumberField.types.d.ts +12 -0
  27. package/dist/elements/NumberField/NumberField.types.d.ts.map +1 -1
  28. package/dist/elements/NumberField/index.js +1 -1
  29. package/dist/elements/NumberField/index.js.map +1 -1
  30. package/dist/elements/NumberField/index.mjs +1 -1
  31. package/dist/elements/NumberField/index.mjs.map +1 -1
  32. package/dist/elements/OTPInput/OTPInput.d.ts +1 -1
  33. package/dist/elements/Resizable/index.js +1 -1
  34. package/dist/elements/Resizable/index.js.map +1 -1
  35. package/dist/elements/Resizable/index.mjs +1 -1
  36. package/dist/elements/Resizable/index.mjs.map +1 -1
  37. package/dist/elements/Switch/Switch.d.ts +11 -4
  38. package/dist/elements/Switch/Switch.d.ts.map +1 -1
  39. package/dist/elements/Switch/Switch.types.d.ts +5 -0
  40. package/dist/elements/Switch/Switch.types.d.ts.map +1 -1
  41. package/dist/elements/Switch/index.js +1 -1
  42. package/dist/elements/Switch/index.js.map +1 -1
  43. package/dist/elements/Switch/index.mjs +1 -1
  44. package/dist/elements/Switch/index.mjs.map +1 -1
  45. package/dist/elements/Tabs/Tabs.d.ts +3 -3
  46. package/dist/elements/Tabs/Tabs.d.ts.map +1 -1
  47. package/dist/elements/Tabs/Tabs.types.d.ts +4 -0
  48. package/dist/elements/Tabs/Tabs.types.d.ts.map +1 -1
  49. package/dist/elements/Tabs/index.js +1 -1
  50. package/dist/elements/Tabs/index.js.map +1 -1
  51. package/dist/elements/Tabs/index.mjs +1 -1
  52. package/dist/elements/Tabs/index.mjs.map +1 -1
  53. package/dist/elements/index.js +1 -1
  54. package/dist/elements/index.js.map +1 -1
  55. package/dist/elements/index.mjs +1 -1
  56. package/dist/elements/index.mjs.map +1 -1
  57. package/dist/hooks/index.d.ts +2 -0
  58. package/dist/hooks/index.d.ts.map +1 -0
  59. package/dist/hooks/index.js +2 -0
  60. package/dist/hooks/index.js.map +1 -0
  61. package/dist/hooks/index.mjs +2 -0
  62. package/dist/hooks/index.mjs.map +1 -0
  63. package/dist/hooks/useReducedMotion.d.ts +9 -0
  64. package/dist/hooks/useReducedMotion.d.ts.map +1 -0
  65. package/dist/index.d.ts +2 -0
  66. package/dist/index.d.ts.map +1 -1
  67. package/dist/index.js +2 -2
  68. package/dist/index.js.map +1 -1
  69. package/dist/index.mjs +2 -2
  70. package/dist/index.mjs.map +1 -1
  71. package/dist/styles/animations.css +172 -0
  72. package/dist/styles/index.js +1 -1
  73. package/dist/styles/index.js.map +1 -1
  74. package/dist/styles/index.mjs +1 -1
  75. package/dist/styles/index.mjs.map +1 -1
  76. package/dist/styles/shared-variants.d.ts +23 -0
  77. package/dist/styles/shared-variants.d.ts.map +1 -1
  78. package/dist/tailwind-source.css +1 -0
  79. package/dist/types/animation.d.ts +24 -0
  80. package/dist/types/animation.d.ts.map +1 -0
  81. package/dist/types/index.d.ts +2 -0
  82. package/dist/types/index.d.ts.map +1 -0
  83. package/dist/types/index.js +2 -0
  84. package/dist/types/index.js.map +1 -0
  85. package/dist/types/index.mjs +2 -0
  86. package/dist/types/index.mjs.map +1 -0
  87. package/dist/utils/index.d.ts +1 -0
  88. package/dist/utils/index.d.ts.map +1 -1
  89. package/dist/utils/index.js +1 -1
  90. package/dist/utils/index.js.map +1 -1
  91. package/dist/utils/index.mjs +1 -1
  92. package/dist/utils/index.mjs.map +1 -1
  93. package/dist/utils/shouldAnimate.d.ts +12 -0
  94. package/dist/utils/shouldAnimate.d.ts.map +1 -0
  95. package/package.json +48 -16
  96. package/src/elements/NumberField/NumberField.stories.tsx +100 -0
  97. package/src/elements/Switch/Switch.stories.tsx +60 -0
  98. package/src/elements/Tabs/Tabs.stories.tsx +332 -2
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tribepad/themis",
3
- "version": "1.0.6",
3
+ "version": "1.0.8",
4
4
  "description": "Accessible React component library built on React Aria primitives",
5
5
  "author": "Tribepad <mbasford@tribepad.com>",
6
6
  "license": "MIT",
@@ -50,10 +50,31 @@
50
50
  }
51
51
  },
52
52
  "./styles/defaults.css": "./dist/styles/defaults.css",
53
+ "./styles/animations.css": "./dist/styles/animations.css",
53
54
  "./tailwind-source.css": {
54
55
  "style": "./dist/tailwind-source.css",
55
56
  "default": "./dist/tailwind-source.css"
56
57
  },
58
+ "./hooks": {
59
+ "import": {
60
+ "types": "./dist/hooks/index.d.ts",
61
+ "default": "./dist/hooks/index.mjs"
62
+ },
63
+ "require": {
64
+ "types": "./dist/hooks/index.d.ts",
65
+ "default": "./dist/hooks/index.js"
66
+ }
67
+ },
68
+ "./types": {
69
+ "import": {
70
+ "types": "./dist/types/index.d.ts",
71
+ "default": "./dist/types/index.mjs"
72
+ },
73
+ "require": {
74
+ "types": "./dist/types/index.d.ts",
75
+ "default": "./dist/types/index.js"
76
+ }
77
+ },
57
78
  "./utils": {
58
79
  "import": {
59
80
  "types": "./dist/utils/index.d.ts",
@@ -110,7 +131,8 @@
110
131
  "react": "^18.0.0 || ^19.0.0",
111
132
  "react-dom": "^18.0.0 || ^19.0.0",
112
133
  "tailwindcss": "^3.4.0 || ^4.0.0",
113
- "zod": "^3.0.0 || ^4.0.0"
134
+ "zod": "^3.0.0 || ^4.0.0",
135
+ "motion": ">=11.0.0"
114
136
  },
115
137
  "peerDependenciesMeta": {
116
138
  "tailwindcss": {
@@ -121,6 +143,9 @@
121
143
  },
122
144
  "zod": {
123
145
  "optional": true
146
+ },
147
+ "motion": {
148
+ "optional": true
124
149
  }
125
150
  },
126
151
  "dependencies": {
@@ -130,38 +155,39 @@
130
155
  "react-aria": "^3.35.0",
131
156
  "react-aria-components": "^1.5.0",
132
157
  "react-stately": "^3.33.0",
133
- "tailwind-merge": "^3.4.0"
158
+ "tailwind-merge": "^3.5.0"
134
159
  },
135
160
  "devDependencies": {
136
- "@eslint/js": "^9.0.0",
161
+ "@eslint/js": "^9.39.3",
137
162
  "@size-limit/preset-small-lib": "^11.0.0",
138
- "@storybook/addon-a11y": "^10.2.8",
139
- "@storybook/react-vite": "^10.2.8",
140
- "@tailwindcss/vite": "^4.1.18",
163
+ "@storybook/addon-a11y": "^10.2.10",
164
+ "@storybook/react-vite": "^10.2.10",
165
+ "@tailwindcss/vite": "^4.2.0",
141
166
  "@testing-library/jest-dom": "^6.0.0",
142
167
  "@testing-library/react": "^16.0.0",
143
168
  "@testing-library/user-event": "^14.0.0",
144
- "@types/node": "^25.2.2",
145
- "@types/react": "^19.0.0",
169
+ "@types/node": "^25.3.5",
170
+ "@types/react": "^19.2.14",
146
171
  "@types/react-dom": "^19.0.0",
147
- "@typescript-eslint/eslint-plugin": "^8.0.0",
148
- "@typescript-eslint/parser": "^8.0.0",
149
- "@vitejs/plugin-react": "^5.1.3",
172
+ "@typescript-eslint/eslint-plugin": "^8.56.1",
173
+ "@typescript-eslint/parser": "^8.56.1",
174
+ "@vitejs/plugin-react": "^5.1.4",
150
175
  "@vitest/coverage-v8": "^4.0.0",
151
176
  "eslint": "^9.0.0",
152
177
  "eslint-plugin-jsx-a11y": "^6.10.2",
153
178
  "globals": "^17.3.0",
154
- "happy-dom": "^20.5.3",
179
+ "happy-dom": "^20.7.0",
155
180
  "jest-axe": "^10.0.0",
156
- "jsdom": "^28.0.0",
181
+ "jsdom": "^28.1.0",
157
182
  "lucide-react": ">=0.400.0",
158
183
  "react": "^19.0.0",
159
184
  "react-dom": "^19.0.0",
160
185
  "size-limit": "^11.0.0",
161
- "storybook": "^10.2.8",
186
+ "storybook": "^10.2.10",
162
187
  "tsup": "^8.0.0",
163
188
  "tsx": "^4.0.0",
164
- "typescript": "^5.6.0",
189
+ "typescript": "^5.9.3",
190
+ "motion": "^12.35.0",
165
191
  "vitest": "^4.0.18",
166
192
  "zod": "^4.3.6"
167
193
  },
@@ -187,6 +213,12 @@
187
213
  "styles": [
188
214
  "dist/styles/index.d.ts"
189
215
  ],
216
+ "hooks": [
217
+ "dist/hooks/index.d.ts"
218
+ ],
219
+ "types": [
220
+ "dist/types/index.d.ts"
221
+ ],
190
222
  "utils": [
191
223
  "dist/utils/index.d.ts"
192
224
  ],
@@ -676,3 +676,103 @@ export const Playground: Story = {
676
676
  },
677
677
  },
678
678
  };
679
+
680
+ // =============================================================================
681
+ // Animation Stories (ADR 020)
682
+ // =============================================================================
683
+
684
+ /**
685
+ * Spring animation on stepper press (default behavior when motion.dev is installed).
686
+ * Click increment/decrement to see the value spring to its target.
687
+ */
688
+ export const AnimatedStepper: Story = {
689
+ args: {
690
+ label: 'Quantity',
691
+ defaultValue: 5,
692
+ step: 1,
693
+ minValue: 0,
694
+ maxValue: 99,
695
+ animated: true,
696
+ description: 'Click stepper buttons to see the spring animation',
697
+ },
698
+ parameters: {
699
+ docs: {
700
+ description: {
701
+ story:
702
+ 'Default animated NumberField with spring physics on stepper press. The displayed value springs from the old value to the new one. Direct keyboard input updates instantly.',
703
+ },
704
+ },
705
+ },
706
+ };
707
+
708
+ /**
709
+ * Animation disabled — value updates instantly on stepper press.
710
+ */
711
+ export const NoStepperAnimation: Story = {
712
+ args: {
713
+ label: 'Quantity',
714
+ defaultValue: 5,
715
+ step: 1,
716
+ minValue: 0,
717
+ maxValue: 99,
718
+ animated: false,
719
+ description: 'Stepper updates value instantly (no spring)',
720
+ },
721
+ parameters: {
722
+ docs: {
723
+ description: {
724
+ story:
725
+ 'NumberField with `animated={false}`. Stepper press updates the value instantly without spring animation.',
726
+ },
727
+ },
728
+ },
729
+ };
730
+
731
+ /**
732
+ * Currency formatting with spring animation.
733
+ * The animated overlay displays locale-formatted intermediate values.
734
+ */
735
+ export const AnimatedCurrency: Story = {
736
+ args: {
737
+ label: 'Price',
738
+ defaultValue: 9.99,
739
+ step: 0.5,
740
+ minValue: 0,
741
+ maxValue: 999.99,
742
+ animated: true,
743
+ formatOptions: { style: 'currency', currency: 'USD' },
744
+ description: 'Spring animation with currency formatting',
745
+ },
746
+ parameters: {
747
+ docs: {
748
+ description: {
749
+ story:
750
+ 'Currency-formatted NumberField with spring animation. The overlay formats intermediate values with the same Intl.NumberFormat options as the input.',
751
+ },
752
+ },
753
+ },
754
+ };
755
+
756
+ /**
757
+ * Custom spring configuration — softer, bouncier animation.
758
+ */
759
+ export const CustomSpringConfig: Story = {
760
+ args: {
761
+ label: 'Score',
762
+ defaultValue: 50,
763
+ step: 10,
764
+ minValue: 0,
765
+ maxValue: 100,
766
+ animated: true,
767
+ springConfig: { stiffness: 150, damping: 10 },
768
+ description: 'Low stiffness, low damping = more bounce',
769
+ },
770
+ parameters: {
771
+ docs: {
772
+ description: {
773
+ story:
774
+ 'Custom `springConfig` with lower stiffness and damping for a more pronounced bounce effect on stepper press.',
775
+ },
776
+ },
777
+ },
778
+ };
@@ -437,5 +437,65 @@ export const WithAriaLabel: Story = {
437
437
  },
438
438
  };
439
439
 
440
+ // =============================================================================
441
+ // Animation Stories (ADR 020)
442
+ // =============================================================================
443
+
444
+ /**
445
+ * Spring bounce animation on toggle (default behavior when motion.dev is installed).
446
+ * The thumb overshoots slightly before settling, giving a natural feel.
447
+ */
448
+ export const Animated: Story = {
449
+ args: {
450
+ label: 'Spring animation',
451
+ description: 'Toggle to see the bounce effect',
452
+ animated: true,
453
+ },
454
+ parameters: {
455
+ docs: {
456
+ description: {
457
+ story: 'Default animated switch with spring physics via motion.dev. The thumb bounces when toggled.',
458
+ },
459
+ },
460
+ },
461
+ };
462
+
463
+ /**
464
+ * Animation explicitly disabled — thumb slides instantly via CSS.
465
+ */
466
+ export const NoAnimation: Story = {
467
+ args: {
468
+ label: 'No animation',
469
+ description: 'Instant toggle with no spring',
470
+ animated: false,
471
+ },
472
+ parameters: {
473
+ docs: {
474
+ description: {
475
+ story: 'Switch with `animated={false}`. The thumb translates instantly using CSS, with no spring bounce.',
476
+ },
477
+ },
478
+ },
479
+ };
480
+
481
+ /**
482
+ * Custom spring configuration for a slower, bouncier feel.
483
+ */
484
+ export const CustomSpring: Story = {
485
+ args: {
486
+ label: 'Custom spring',
487
+ description: 'Low stiffness, low damping = more bounce',
488
+ animated: true,
489
+ springConfig: { stiffness: 200, damping: 12 },
490
+ },
491
+ parameters: {
492
+ docs: {
493
+ description: {
494
+ story: 'Custom `springConfig` with lower stiffness and damping for a softer, bouncier toggle.',
495
+ },
496
+ },
497
+ },
498
+ };
499
+
440
500
  // Import React for controlled example
441
501
  import { useState } from 'react';
@@ -26,8 +26,8 @@ const meta = {
26
26
  argTypes: {
27
27
  variant: {
28
28
  control: 'select',
29
- options: ['default', 'block'],
30
- description: 'Visual variant: underline (default) or block/pill',
29
+ options: ['default', 'block', 'stepper'],
30
+ description: 'Visual variant: underline (default), block/pill, or stepper (numbered steps)',
31
31
  },
32
32
  orientation: {
33
33
  control: 'select',
@@ -131,6 +131,235 @@ export const Block: Story = {
131
131
  ),
132
132
  };
133
133
 
134
+ // ============================================================================
135
+ // Stepper Variant Stories
136
+ // ============================================================================
137
+
138
+ /**
139
+ * Stepper: Numbered step circles with connecting lines.
140
+ * Completed steps show a checkmark, the active step is highlighted.
141
+ */
142
+ export const Stepper: Story = {
143
+ render: () => (
144
+ <Tabs variant="stepper" defaultSelectedKey="form" className="w-[600px]">
145
+ <TabList aria-label="Blueprint setup steps">
146
+ <Tab id="details">Job Details</Tab>
147
+ <Tab id="form">Application Form</Tab>
148
+ <Tab id="statuses">Statuses</Tab>
149
+ <Tab id="journeys">Journeys</Tab>
150
+ </TabList>
151
+ <TabPanel id="details">
152
+ <div className="p-4">
153
+ <h3 className="text-lg font-semibold mb-2">Job Details</h3>
154
+ <p className="text-sm text-[var(--muted-foreground)]">
155
+ Configure the basic job information and requirements.
156
+ </p>
157
+ </div>
158
+ </TabPanel>
159
+ <TabPanel id="form">
160
+ <div className="p-4">
161
+ <h3 className="text-lg font-semibold mb-2">Application Form</h3>
162
+ <p className="text-sm text-[var(--muted-foreground)]">
163
+ Build the candidate application form with fields and sections.
164
+ </p>
165
+ </div>
166
+ </TabPanel>
167
+ <TabPanel id="statuses">
168
+ <div className="p-4">
169
+ <h3 className="text-lg font-semibold mb-2">Statuses</h3>
170
+ <p className="text-sm text-[var(--muted-foreground)]">
171
+ Define the status workflow for applications.
172
+ </p>
173
+ </div>
174
+ </TabPanel>
175
+ <TabPanel id="journeys">
176
+ <div className="p-4">
177
+ <h3 className="text-lg font-semibold mb-2">Journeys</h3>
178
+ <p className="text-sm text-[var(--muted-foreground)]">
179
+ Configure candidate journeys for each status.
180
+ </p>
181
+ </div>
182
+ </TabPanel>
183
+ </Tabs>
184
+ ),
185
+ };
186
+
187
+ /**
188
+ * StepperFirstStep: Stepper with the first step active (no completed steps).
189
+ */
190
+ export const StepperFirstStep: Story = {
191
+ render: () => (
192
+ <Tabs variant="stepper" defaultSelectedKey="details" className="w-[600px]">
193
+ <TabList aria-label="Setup wizard">
194
+ <Tab id="details">Details</Tab>
195
+ <Tab id="form">Form</Tab>
196
+ <Tab id="statuses">Statuses</Tab>
197
+ <Tab id="journeys">Journeys</Tab>
198
+ </TabList>
199
+ <TabPanel id="details">
200
+ <div className="p-4">
201
+ <h3 className="text-lg font-semibold mb-2">Step 1: Details</h3>
202
+ <p className="text-sm text-[var(--muted-foreground)]">
203
+ First step is active. No completed steps yet.
204
+ </p>
205
+ </div>
206
+ </TabPanel>
207
+ <TabPanel id="form">
208
+ <div className="p-4">Step 2 content</div>
209
+ </TabPanel>
210
+ <TabPanel id="statuses">
211
+ <div className="p-4">Step 3 content</div>
212
+ </TabPanel>
213
+ <TabPanel id="journeys">
214
+ <div className="p-4">Step 4 content</div>
215
+ </TabPanel>
216
+ </Tabs>
217
+ ),
218
+ };
219
+
220
+ /**
221
+ * StepperLastStep: Stepper with the last step active (all previous completed).
222
+ */
223
+ export const StepperLastStep: Story = {
224
+ render: () => (
225
+ <Tabs variant="stepper" defaultSelectedKey="journeys" className="w-[600px]">
226
+ <TabList aria-label="Setup wizard">
227
+ <Tab id="details">Details</Tab>
228
+ <Tab id="form">Form</Tab>
229
+ <Tab id="statuses">Statuses</Tab>
230
+ <Tab id="journeys">Journeys</Tab>
231
+ </TabList>
232
+ <TabPanel id="details">
233
+ <div className="p-4">Step 1 content</div>
234
+ </TabPanel>
235
+ <TabPanel id="form">
236
+ <div className="p-4">Step 2 content</div>
237
+ </TabPanel>
238
+ <TabPanel id="statuses">
239
+ <div className="p-4">Step 3 content</div>
240
+ </TabPanel>
241
+ <TabPanel id="journeys">
242
+ <div className="p-4">
243
+ <h3 className="text-lg font-semibold mb-2">Step 4: Journeys</h3>
244
+ <p className="text-sm text-[var(--muted-foreground)]">
245
+ All previous steps show checkmarks as completed.
246
+ </p>
247
+ </div>
248
+ </TabPanel>
249
+ </Tabs>
250
+ ),
251
+ };
252
+
253
+ /**
254
+ * StepperControlled: Stepper with external step navigation controls.
255
+ */
256
+ export const StepperControlled: Story = {
257
+ render: function StepperControlledDemo() {
258
+ const steps = ['details', 'form', 'statuses', 'journeys'] as const;
259
+ const [selected, setSelected] = useState<Key>('details');
260
+ const currentIndex = steps.indexOf(selected as typeof steps[number]);
261
+
262
+ return (
263
+ <div className="w-[600px]">
264
+ <Tabs variant="stepper" selectedKey={selected} onSelectionChange={setSelected}>
265
+ <TabList aria-label="Controlled stepper">
266
+ <Tab id="details">Details</Tab>
267
+ <Tab id="form">Form</Tab>
268
+ <Tab id="statuses">Statuses</Tab>
269
+ <Tab id="journeys">Journeys</Tab>
270
+ </TabList>
271
+ <TabPanel id="details">
272
+ <div className="p-4">Step 1: Configure details</div>
273
+ </TabPanel>
274
+ <TabPanel id="form">
275
+ <div className="p-4">Step 2: Build the form</div>
276
+ </TabPanel>
277
+ <TabPanel id="statuses">
278
+ <div className="p-4">Step 3: Define statuses</div>
279
+ </TabPanel>
280
+ <TabPanel id="journeys">
281
+ <div className="p-4">Step 4: Set up journeys</div>
282
+ </TabPanel>
283
+ </Tabs>
284
+ <div className="flex justify-between mt-4 px-4">
285
+ <button
286
+ onClick={() => currentIndex > 0 && setSelected(steps[currentIndex - 1])}
287
+ disabled={currentIndex === 0}
288
+ className="px-4 py-2 text-sm rounded-md bg-[var(--secondary)] text-[var(--secondary-foreground)] disabled:opacity-50"
289
+ >
290
+ Previous
291
+ </button>
292
+ <button
293
+ onClick={() => currentIndex < steps.length - 1 && setSelected(steps[currentIndex + 1])}
294
+ disabled={currentIndex === steps.length - 1}
295
+ className="px-4 py-2 text-sm rounded-md bg-[var(--primary)] text-[var(--primary-foreground)] disabled:opacity-50"
296
+ >
297
+ Next
298
+ </button>
299
+ </div>
300
+ </div>
301
+ );
302
+ },
303
+ };
304
+
305
+ /**
306
+ * StepperWithDisabledTabs: Stepper with some tabs disabled.
307
+ */
308
+ export const StepperWithDisabledTabs: Story = {
309
+ render: () => (
310
+ <Tabs variant="stepper" defaultSelectedKey="form" disabledKeys={['journeys']} className="w-[600px]">
311
+ <TabList aria-label="Steps with disabled">
312
+ <Tab id="details">Details</Tab>
313
+ <Tab id="form">Form</Tab>
314
+ <Tab id="statuses">Statuses</Tab>
315
+ <Tab id="journeys">Journeys</Tab>
316
+ </TabList>
317
+ <TabPanel id="details">
318
+ <div className="p-4">Step 1 content</div>
319
+ </TabPanel>
320
+ <TabPanel id="form">
321
+ <div className="p-4">
322
+ <h3 className="text-lg font-semibold mb-2">Step 2: Form</h3>
323
+ <p className="text-sm text-[var(--muted-foreground)]">
324
+ The &quot;Journeys&quot; step is disabled and cannot be selected.
325
+ </p>
326
+ </div>
327
+ </TabPanel>
328
+ <TabPanel id="statuses">
329
+ <div className="p-4">Step 3 content</div>
330
+ </TabPanel>
331
+ <TabPanel id="journeys">
332
+ <div className="p-4">Step 4 content (disabled)</div>
333
+ </TabPanel>
334
+ </Tabs>
335
+ ),
336
+ };
337
+
338
+ /**
339
+ * StepperTwoSteps: Minimal stepper with only 2 steps.
340
+ */
341
+ export const StepperTwoSteps: Story = {
342
+ render: () => (
343
+ <Tabs variant="stepper" defaultSelectedKey="review" className="w-[400px]">
344
+ <TabList aria-label="Two-step process">
345
+ <Tab id="configure">Configure</Tab>
346
+ <Tab id="review">Review</Tab>
347
+ </TabList>
348
+ <TabPanel id="configure">
349
+ <div className="p-4">Configuration step content</div>
350
+ </TabPanel>
351
+ <TabPanel id="review">
352
+ <div className="p-4">
353
+ <h3 className="text-lg font-semibold mb-2">Review</h3>
354
+ <p className="text-sm text-[var(--muted-foreground)]">
355
+ The first step shows a checkmark. This is the active step.
356
+ </p>
357
+ </div>
358
+ </TabPanel>
359
+ </Tabs>
360
+ ),
361
+ };
362
+
134
363
  // ============================================================================
135
364
  // Orientation Stories
136
365
  // ============================================================================
@@ -703,3 +932,104 @@ export const Controlled: Story = {
703
932
  );
704
933
  },
705
934
  };
935
+
936
+ // =============================================================================
937
+ // Animation Stories (ADR 020)
938
+ // =============================================================================
939
+
940
+ /**
941
+ * Animated sliding indicator (default behavior when motion.dev is installed).
942
+ * Click between tabs to see the underline smoothly slide to the selected tab.
943
+ */
944
+ export const AnimatedIndicator: Story = {
945
+ render: () => (
946
+ <Tabs defaultSelectedKey="tab1" animated>
947
+ <TabList aria-label="Animated indicator demo">
948
+ <Tab id="tab1">Account</Tab>
949
+ <Tab id="tab2">Notifications</Tab>
950
+ <Tab id="tab3">Security</Tab>
951
+ </TabList>
952
+ <TabPanel id="tab1">
953
+ <div className="p-4">Click other tabs to see the indicator slide.</div>
954
+ </TabPanel>
955
+ <TabPanel id="tab2">
956
+ <div className="p-4">Notification preferences.</div>
957
+ </TabPanel>
958
+ <TabPanel id="tab3">
959
+ <div className="p-4">Security settings.</div>
960
+ </TabPanel>
961
+ </Tabs>
962
+ ),
963
+ parameters: {
964
+ docs: {
965
+ description: {
966
+ story:
967
+ 'Default animated tabs with a sliding underline indicator via motion.dev `layoutId`. The indicator smoothly transitions between tabs on selection.',
968
+ },
969
+ },
970
+ },
971
+ };
972
+
973
+ /**
974
+ * Animation disabled — indicator jumps instantly between tabs.
975
+ */
976
+ export const NoIndicatorAnimation: Story = {
977
+ render: () => (
978
+ <Tabs defaultSelectedKey="tab1" animated={false}>
979
+ <TabList aria-label="No animation demo">
980
+ <Tab id="tab1">Account</Tab>
981
+ <Tab id="tab2">Notifications</Tab>
982
+ <Tab id="tab3">Security</Tab>
983
+ </TabList>
984
+ <TabPanel id="tab1">
985
+ <div className="p-4">Indicator jumps instantly (no slide).</div>
986
+ </TabPanel>
987
+ <TabPanel id="tab2">
988
+ <div className="p-4">Notification preferences.</div>
989
+ </TabPanel>
990
+ <TabPanel id="tab3">
991
+ <div className="p-4">Security settings.</div>
992
+ </TabPanel>
993
+ </Tabs>
994
+ ),
995
+ parameters: {
996
+ docs: {
997
+ description: {
998
+ story:
999
+ 'Tabs with `animated={false}`. The indicator appears instantly on the selected tab without sliding.',
1000
+ },
1001
+ },
1002
+ },
1003
+ };
1004
+
1005
+ /**
1006
+ * Vertical orientation with animated indicator sliding up and down.
1007
+ */
1008
+ export const VerticalAnimated: Story = {
1009
+ render: () => (
1010
+ <Tabs defaultSelectedKey="tab1" orientation="vertical" animated>
1011
+ <TabList aria-label="Vertical animated demo">
1012
+ <Tab id="tab1">Profile</Tab>
1013
+ <Tab id="tab2">Settings</Tab>
1014
+ <Tab id="tab3">Billing</Tab>
1015
+ </TabList>
1016
+ <TabPanel id="tab1">
1017
+ <div className="p-4">Vertical tabs with sliding indicator.</div>
1018
+ </TabPanel>
1019
+ <TabPanel id="tab2">
1020
+ <div className="p-4">Settings panel.</div>
1021
+ </TabPanel>
1022
+ <TabPanel id="tab3">
1023
+ <div className="p-4">Billing information.</div>
1024
+ </TabPanel>
1025
+ </Tabs>
1026
+ ),
1027
+ parameters: {
1028
+ docs: {
1029
+ description: {
1030
+ story:
1031
+ 'Vertical orientation with animated indicator. The indicator slides vertically between tabs using the same `layoutId` approach.',
1032
+ },
1033
+ },
1034
+ },
1035
+ };