@boxcustodia/library 2.0.0-alpha.13 → 2.0.0-alpha.14

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 (173) hide show
  1. package/dist/index.cjs.js +1 -138
  2. package/dist/index.d.ts +1083 -715
  3. package/dist/index.es.js +7077 -56175
  4. package/dist/theme.css +1 -1
  5. package/package.json +34 -26
  6. package/src/__doc__/Examples.tsx +1 -1
  7. package/src/__doc__/Intro.mdx +3 -3
  8. package/src/__doc__/Tabs.mdx +112 -0
  9. package/src/__doc__/V2.mdx +1246 -0
  10. package/src/components/accordion/accordion.stories.tsx +143 -0
  11. package/src/components/accordion/accordion.tsx +135 -0
  12. package/src/components/accordion/index.ts +1 -0
  13. package/src/components/alert/alert.stories.tsx +24 -4
  14. package/src/components/alert/alert.tsx +17 -9
  15. package/src/components/alert-dialog/alert-dialog.stories.tsx +24 -0
  16. package/src/components/alert-dialog/alert-dialog.test.tsx +1 -1
  17. package/src/components/alert-dialog/alert-dialog.tsx +58 -10
  18. package/src/components/auto-complete/auto-complete.stories.tsx +616 -200
  19. package/src/components/auto-complete/auto-complete.tsx +420 -68
  20. package/src/components/auto-complete/index.ts +0 -1
  21. package/src/components/avatar/avatar.stories.tsx +162 -21
  22. package/src/components/avatar/avatar.tsx +79 -20
  23. package/src/components/button/button.stories.tsx +219 -294
  24. package/src/components/button/button.test.tsx +10 -17
  25. package/src/components/button/button.tsx +78 -19
  26. package/src/components/button/components/base-button.tsx +30 -53
  27. package/src/components/button/index.ts +0 -1
  28. package/src/components/calendar/calendar.stories.tsx +1 -1
  29. package/src/components/calendar/calendar.tsx +4 -4
  30. package/src/components/card/card.stories.tsx +141 -69
  31. package/src/components/card/card.tsx +155 -54
  32. package/src/components/center/center.stories.tsx +22 -39
  33. package/src/components/checkbox/checkbox.stories.tsx +25 -5
  34. package/src/components/checkbox/checkbox.tsx +76 -15
  35. package/src/components/checkbox-group/checkbox-group.stories.tsx +116 -28
  36. package/src/components/checkbox-group/checkbox-group.tsx +84 -3
  37. package/src/components/combobox/combobox.stories.tsx +33 -23
  38. package/src/components/combobox/combobox.tsx +119 -103
  39. package/src/components/date-picker/date-input.stories.tsx +14 -6
  40. package/src/components/date-picker/date-input.tsx +2 -2
  41. package/src/components/date-picker/date-picker.model.ts +13 -4
  42. package/src/components/date-picker/date-picker.stories.tsx +38 -12
  43. package/src/components/date-picker/date-picker.tsx +28 -14
  44. package/src/components/dialog/dialog.stories.tsx +18 -0
  45. package/src/components/dialog/dialog.test.tsx +1 -1
  46. package/src/components/dialog/dialog.tsx +51 -20
  47. package/src/components/divider/divider.stories.tsx +6 -0
  48. package/src/components/dropzone/dropzone.stories.tsx +71 -90
  49. package/src/components/dropzone/dropzone.tsx +383 -105
  50. package/src/components/dropzone/index.ts +0 -1
  51. package/src/components/empty/empty.stories.tsx +165 -0
  52. package/src/components/empty/empty.tsx +156 -0
  53. package/src/components/empty/index.ts +1 -0
  54. package/src/components/field/field.stories.tsx +226 -3
  55. package/src/components/field/field.tsx +77 -42
  56. package/src/components/form/form.stories.tsx +320 -197
  57. package/src/components/form/form.tsx +3 -23
  58. package/src/components/index.ts +2 -6
  59. package/src/components/input/input.stories.tsx +5 -5
  60. package/src/components/input/input.tsx +4 -4
  61. package/src/components/kbd/kbd.stories.tsx +1 -0
  62. package/src/components/label/label.stories.tsx +16 -0
  63. package/src/components/label/label.tsx +13 -2
  64. package/src/components/loader/loader.stories.tsx +7 -5
  65. package/src/components/loader/loader.tsx +8 -3
  66. package/src/components/menu/menu-primitives.tsx +207 -196
  67. package/src/components/menu/menu.stories.tsx +276 -146
  68. package/src/components/menu/menu.tsx +146 -54
  69. package/src/components/number-input/number-input.stories.tsx +27 -4
  70. package/src/components/number-input/number-input.test.tsx +2 -2
  71. package/src/components/number-input/number-input.tsx +25 -29
  72. package/src/components/otp/index.ts +1 -0
  73. package/src/components/otp/otp.stories.tsx +209 -0
  74. package/src/components/otp/otp.tsx +100 -0
  75. package/src/components/pagination/index.ts +1 -0
  76. package/src/components/pagination/pagination.model.ts +2 -0
  77. package/src/components/pagination/pagination.stories.tsx +154 -59
  78. package/src/components/pagination/pagination.test.tsx +122 -57
  79. package/src/components/pagination/pagination.tsx +575 -77
  80. package/src/components/password/password.stories.tsx +18 -3
  81. package/src/components/password/password.tsx +26 -10
  82. package/src/components/popover/popover.stories.tsx +26 -5
  83. package/src/components/popover/popover.tsx +15 -23
  84. package/src/components/progress/progress.stories.tsx +1 -0
  85. package/src/components/radio-group/index.ts +1 -0
  86. package/src/components/radio-group/radio-group.stories.tsx +251 -0
  87. package/src/components/radio-group/radio-group.tsx +212 -0
  88. package/src/components/scroll-area/scroll-area.stories.tsx +1 -0
  89. package/src/components/select/select.stories.tsx +118 -19
  90. package/src/components/select/select.tsx +67 -62
  91. package/src/components/skeleton/skeleton.stories.tsx +1 -0
  92. package/src/components/stack/stack.stories.tsx +179 -89
  93. package/src/components/stack/stack.tsx +2 -2
  94. package/src/components/stepper/index.ts +1 -1
  95. package/src/components/stepper/stepper.stories.tsx +767 -83
  96. package/src/components/stepper/stepper.test.tsx +18 -18
  97. package/src/components/stepper/stepper.tsx +554 -0
  98. package/src/components/switch/switch.stories.tsx +15 -1
  99. package/src/components/switch/switch.tsx +17 -4
  100. package/src/components/table/index.ts +0 -2
  101. package/src/components/table/table.stories.tsx +131 -18
  102. package/src/components/table/table.test.tsx +1 -1
  103. package/src/components/table/table.tsx +183 -77
  104. package/src/components/tabs/tabs.stories.tsx +373 -155
  105. package/src/components/tabs/tabs.test.tsx +12 -12
  106. package/src/components/tabs/tabs.tsx +72 -149
  107. package/src/components/tag/index.ts +0 -1
  108. package/src/components/tag/tag.stories.tsx +155 -120
  109. package/src/components/tag/tag.tsx +47 -95
  110. package/src/components/textarea/textarea.stories.tsx +8 -22
  111. package/src/components/textarea/textarea.tsx +17 -79
  112. package/src/components/timeline/timeline.stories.tsx +323 -42
  113. package/src/components/timeline/timeline.tsx +359 -132
  114. package/src/components/toast/toast.stories.tsx +1 -0
  115. package/src/components/tooltip/tooltip.tsx +11 -9
  116. package/src/components/tree/index.ts +0 -1
  117. package/src/components/tree/tree.stories.tsx +365 -408
  118. package/src/components/tree/tree.test.tsx +163 -0
  119. package/src/components/tree/tree.tsx +212 -36
  120. package/src/hooks/useAsync/__doc__/useAsync.stories.tsx +5 -5
  121. package/src/hooks/useClipboard/__doc__/useClipboard.stories.tsx +1 -3
  122. package/src/hooks/useDebounceCallback/__doc__/useDebouncedCallback.stories.tsx +6 -6
  123. package/src/hooks/useDocumentTitle/__doc__/useDocumentTitle.stories.tsx +1 -1
  124. package/src/hooks/useEventListener/__test__/useEventListener.test.tsx +1 -1
  125. package/src/hooks/useLocalStorage/__doc__/useLocalStorage.stories.tsx +1 -1
  126. package/src/hooks/usePagination/usePagination.tsx +36 -24
  127. package/src/styles/theme.css +1 -1
  128. package/src/utils/form.tsx +67 -37
  129. package/src/utils/index.ts +1 -1
  130. package/src/__doc__/Migration.mdx +0 -451
  131. package/src/components/auto-complete/auto-complete-primitives.tsx +0 -155
  132. package/src/components/background-image/background-image.stories.tsx +0 -21
  133. package/src/components/background-image/background-image.test.tsx +0 -29
  134. package/src/components/background-image/background-image.tsx +0 -23
  135. package/src/components/background-image/index.ts +0 -1
  136. package/src/components/button/button.variants.ts +0 -44
  137. package/src/components/button/components/loader-overlay.tsx +0 -21
  138. package/src/components/button/components/loading-icon.tsx +0 -47
  139. package/src/components/dropzone/upload-primitives.tsx +0 -310
  140. package/src/components/dropzone/use-dropzone.ts +0 -122
  141. package/src/components/empty-state/empty-state.stories.tsx +0 -56
  142. package/src/components/empty-state/empty-state.tsx +0 -39
  143. package/src/components/empty-state/index.ts +0 -1
  144. package/src/components/heading/heading.stories.tsx +0 -74
  145. package/src/components/heading/heading.tsx +0 -28
  146. package/src/components/heading/heading.variants.ts +0 -27
  147. package/src/components/heading/index.ts +0 -1
  148. package/src/components/kbd/kbd.variants.ts +0 -26
  149. package/src/components/menu/util/render-menu-item.tsx +0 -54
  150. package/src/components/multi-select/hooks/use-multi-select.ts +0 -66
  151. package/src/components/multi-select/index.ts +0 -1
  152. package/src/components/multi-select/multi-select.stories.tsx +0 -294
  153. package/src/components/multi-select/multi-select.tsx +0 -300
  154. package/src/components/multi-select/multi-select.variants.ts +0 -22
  155. package/src/components/pagination/components/pagination-option.tsx +0 -27
  156. package/src/components/show/index.ts +0 -1
  157. package/src/components/show/show.stories.tsx +0 -197
  158. package/src/components/show/show.test.tsx +0 -41
  159. package/src/components/show/show.tsx +0 -16
  160. package/src/components/stepper/Stepper.tsx +0 -190
  161. package/src/components/stepper/context/stepper-context.tsx +0 -11
  162. package/src/components/table/table-primitives.tsx +0 -122
  163. package/src/components/table/table.model.ts +0 -20
  164. package/src/components/table-pagination/index.ts +0 -2
  165. package/src/components/table-pagination/table-pagination.model.ts +0 -2
  166. package/src/components/table-pagination/table-pagination.stories.tsx +0 -23
  167. package/src/components/table-pagination/table-pagination.test.tsx +0 -32
  168. package/src/components/table-pagination/table-pagination.tsx +0 -108
  169. package/src/components/tabs/context/tabs-context.tsx +0 -14
  170. package/src/components/tag/tag.variants.ts +0 -31
  171. package/src/components/timeline/timeline-status.ts +0 -5
  172. package/src/components/tree/hooks/use-controllable-tree-state.ts +0 -80
  173. package/src/components/tree/tree-primitives.tsx +0 -126
@@ -1,102 +1,464 @@
1
1
  import type { Meta, StoryObj } from "@storybook/react-vite";
2
- import { RectangleEllipsis, Settings, User } from "lucide-react";
3
2
  import {
4
- Button,
5
- Input,
3
+ CheckIcon,
4
+ CreditCardIcon,
5
+ LoaderCircleIcon,
6
+ PackageCheckIcon,
7
+ TruckIcon,
8
+ UserIcon,
9
+ } from "lucide-react";
10
+ import { type ComponentType, useState } from "react";
11
+
12
+ import { useStep } from "../../hooks";
13
+ import { Button } from "../button/button";
14
+ import { Field } from "../field/field";
15
+ import { Input } from "../input/input";
16
+ import { toast } from "../toast/toast";
17
+ import {
6
18
  Stepper,
7
- StepperContainer,
8
19
  StepperContent,
9
- StepperList,
20
+ StepperDescription,
21
+ StepperIndicator,
22
+ StepperItem,
23
+ StepperNav,
24
+ StepperPanel,
25
+ StepperRoot,
26
+ StepperSeparator,
27
+ StepperTitle,
10
28
  StepperTrigger,
11
- } from "../../components";
12
- import { useStep } from "../../hooks";
29
+ } from "./stepper";
13
30
 
31
+ /**
32
+ * Accessible stepper that highlights progress across an ordered list of steps.
33
+ * Use the composite `Stepper` for items-driven setups, or compose the primitives
34
+ * (`StepperRoot`, `StepperNav`, `StepperItem`, `StepperTrigger`, `StepperIndicator`,
35
+ * `StepperSeparator`, `StepperTitle`, `StepperDescription`, `StepperPanel`,
36
+ * `StepperContent`) for full control over markup and layout.
37
+ *
38
+ * Key behaviors:
39
+ * - Steps are **1-indexed**. `defaultValue` / `value` is the active step number,
40
+ * matching the `step` prop of `StepperItem`. Items with `step < activeStep`
41
+ * render as `completed`; the equal one as `active`; the rest as `inactive`.
42
+ * - Pass `loading` on an item to render the active step in a `loading` state
43
+ * (sets `data-loading` on item, trigger, and indicator).
44
+ * - The composite renders `StepperPanel` automatically when any item carries
45
+ * `content`. For custom panel layouts, drop the composite and use the
46
+ * primitives directly (see the `Primitive` story).
47
+ * - `indicators` on the root acts as a global override map: any matching
48
+ * `state` (active / completed / inactive / loading) replaces the indicator's
49
+ * children. Useful to swap numbers for icons across the whole stepper.
50
+ * - `triggerMode` on the root controls which triggers respond to click and
51
+ * keyboard activation. `"both"` (default) is free navigation; `"backward-only"`
52
+ * blocks jumping ahead (typical wizard); `"forward-only"` locks out reviewing
53
+ * past steps; `"none"` makes the stepper display-only. Non-navigable triggers
54
+ * get `aria-disabled="true"`, lose pointer cursor, and are skipped by Arrow
55
+ * keys.
56
+ * - Keyboard nav (Arrow / Home / End) is wired on `StepperTrigger` and skips
57
+ * both `disabled` and `aria-disabled` triggers.
58
+ *
59
+ * Reference: [WAI-ARIA Tabs Pattern](https://www.w3.org/WAI/ARIA/apg/patterns/tabs/)
60
+ */
14
61
  const meta: Meta<typeof Stepper> = {
15
- title: "Data display/Stepper",
62
+ title: "Components/Stepper",
16
63
  component: Stepper,
17
- args: {},
64
+ parameters: {
65
+ layout: "centered",
66
+ },
67
+ subcomponents: {
68
+ StepperRoot: StepperRoot as ComponentType<unknown>,
69
+ StepperNav: StepperNav as ComponentType<unknown>,
70
+ StepperItem: StepperItem as ComponentType<unknown>,
71
+ StepperTrigger: StepperTrigger as ComponentType<unknown>,
72
+ StepperIndicator: StepperIndicator as ComponentType<unknown>,
73
+ StepperSeparator: StepperSeparator as ComponentType<unknown>,
74
+ StepperTitle: StepperTitle as ComponentType<unknown>,
75
+ StepperDescription: StepperDescription as ComponentType<unknown>,
76
+ StepperPanel: StepperPanel as ComponentType<unknown>,
77
+ StepperContent: StepperContent as ComponentType<unknown>,
78
+ },
79
+ args: {
80
+ defaultValue: 2,
81
+ items: [
82
+ { title: "Account", description: "Your details" },
83
+ { title: "Shipping", description: "Where to send it" },
84
+ { title: "Payment", description: "How to pay" },
85
+ { title: "Review", description: "Confirm order" },
86
+ ],
87
+ },
88
+ argTypes: {
89
+ items: { control: false },
90
+ onValueChange: { control: false },
91
+ classNames: { control: false },
92
+ },
93
+ tags: ["beta"],
18
94
  };
19
95
 
20
96
  export default meta;
21
97
  type Story = StoryObj<typeof Stepper>;
22
98
 
23
- const AccountStep = () => {
24
- return (
25
- <>
26
- <Input name="name" />
27
- <Input name="lastname" />
28
- </>
29
- );
30
- };
31
-
32
- const PasswordStep = () => {
33
- return (
34
- <>
35
- <Input name="password" type="password" />
36
- <Input name="confirmation" type="password" />
37
- </>
38
- );
39
- };
40
-
41
- const FileStep = () => {
42
- return (
43
- <>
44
- <Input name="file" type="file" />
45
- </>
46
- );
47
- };
48
-
49
- const steps = [
50
- { title: "Cuenta", content: <AccountStep />, indicator: <User /> },
51
- { title: "Configuracón", content: <FileStep />, indicator: <Settings /> },
52
- {
53
- title: "Contraseña",
54
- content: <PasswordStep />,
55
- indicator: <RectangleEllipsis />,
99
+ export const Default: Story = {
100
+ render: (args) => (
101
+ <div className="w-[840px]">
102
+ <Stepper {...args} />
103
+ </div>
104
+ ),
105
+ };
106
+
107
+ /**
108
+ * `className` estila el root del Stepper. `classNames` expone los slots
109
+ * `nav`, `item`, `trigger`, `indicator`, `title`, `description`, `separator`,
110
+ * `panel` y `content` para personalizar cada parte interna.
111
+ */
112
+ export const WithClassNames: Story = {
113
+ render: (args) => (
114
+ <div className="w-[840px]">
115
+ <Stepper
116
+ {...args}
117
+ classNames={{
118
+ indicator: "size-8",
119
+ title: "text-primary",
120
+ separator: "h-1",
121
+ }}
122
+ />
123
+ </div>
124
+ ),
125
+ };
126
+
127
+ /**
128
+ * Vertical orientation places the nav as a column with the panel rendered
129
+ * side-by-side. Pass `content` on each item so the active step's panel shows
130
+ * up next to the nav.
131
+ */
132
+ export const Vertical: Story = {
133
+ args: {
134
+ orientation: "vertical",
135
+ defaultValue: 2,
136
+ indicators: { completed: <CheckIcon className="size-3.5" /> },
137
+ items: [
138
+ {
139
+ title: "Account",
140
+ description: "Your details",
141
+ content: (
142
+ <div className="rounded-lg border p-4">
143
+ <p className="font-medium text-sm">Account information</p>
144
+ <p className="mt-1 text-muted-foreground text-sm">
145
+ Name, email, and password to access your workspace.
146
+ </p>
147
+ </div>
148
+ ),
149
+ },
150
+ {
151
+ title: "Shipping",
152
+ description: "Where to send it",
153
+ content: (
154
+ <div className="rounded-lg border p-4">
155
+ <p className="font-medium text-sm">Shipping address</p>
156
+ <p className="mt-1 text-muted-foreground text-sm">
157
+ Pick an existing address or add a new one for this order.
158
+ </p>
159
+ </div>
160
+ ),
161
+ },
162
+ {
163
+ title: "Payment",
164
+ description: "How to pay",
165
+ content: (
166
+ <div className="rounded-lg border p-4">
167
+ <p className="font-medium text-sm">Payment method</p>
168
+ <p className="mt-1 text-muted-foreground text-sm">
169
+ Use a saved card or add a new one before checkout.
170
+ </p>
171
+ </div>
172
+ ),
173
+ },
174
+ {
175
+ title: "Review",
176
+ description: "Confirm order",
177
+ content: (
178
+ <div className="rounded-lg border p-4">
179
+ <p className="font-medium text-sm">Review your order</p>
180
+ <p className="mt-1 text-muted-foreground text-sm">
181
+ Double-check the details and place the order.
182
+ </p>
183
+ </div>
184
+ ),
185
+ },
186
+ ],
56
187
  },
57
- ];
188
+ render: (args) => (
189
+ <div className="w-[720px]">
190
+ <Stepper {...args} />
191
+ </div>
192
+ ),
193
+ };
58
194
 
59
- export const Default: Story = {
60
- render: () => {
61
- return <Stepper items={steps} />;
195
+ /**
196
+ * Per-item `indicator` replaces the default step number. Pair with icons to
197
+ * communicate intent at a glance.
198
+ */
199
+ export const WithIcons: Story = {
200
+ args: {
201
+ defaultValue: 3,
202
+ items: [
203
+ { title: "Account", indicator: <UserIcon className="size-3.5" /> },
204
+ { title: "Shipping", indicator: <TruckIcon className="size-3.5" /> },
205
+ { title: "Payment", indicator: <CreditCardIcon className="size-3.5" /> },
206
+ { title: "Done", indicator: <PackageCheckIcon className="size-3.5" /> },
207
+ ],
62
208
  },
209
+ render: (args) => (
210
+ <div className="w-[840px]">
211
+ <Stepper {...args} />
212
+ </div>
213
+ ),
63
214
  };
64
215
 
216
+ /**
217
+ * `indicators` on the root replaces the children of every indicator whose
218
+ * `state` matches a key in the map. Use it to apply a consistent icon set
219
+ * (✓ for completed, spinner for loading) without touching each item.
220
+ */
221
+ export const GlobalIndicators: Story = {
222
+ args: {
223
+ defaultValue: 3,
224
+ indicators: {
225
+ completed: <CheckIcon className="size-3.5" />,
226
+ loading: <LoaderCircleIcon className="size-3.5 animate-spin" />,
227
+ },
228
+ items: [
229
+ { title: "Account" },
230
+ { title: "Shipping" },
231
+ { title: "Payment" },
232
+ { title: "Review" },
233
+ ],
234
+ },
235
+ render: (args) => (
236
+ <div className="w-[840px]">
237
+ <Stepper {...args} />
238
+ </div>
239
+ ),
240
+ };
241
+
242
+ /**
243
+ * `loading: true` on an item only takes effect when that item is the active
244
+ * step — it switches the item from `active` to `loading` and sets
245
+ * `data-loading` on the trigger and indicator. Combine with the `loading`
246
+ * entry in `indicators` to render a spinner.
247
+ */
248
+ export const Loading: Story = {
249
+ args: {
250
+ defaultValue: 2,
251
+ indicators: {
252
+ loading: <LoaderCircleIcon className="size-3.5 animate-spin" />,
253
+ completed: <CheckIcon className="size-3.5" />,
254
+ },
255
+ items: [
256
+ { title: "Account", description: "Saved" },
257
+ { title: "Shipping", description: "Validating address…", loading: true },
258
+ { title: "Payment", description: "Pending" },
259
+ { title: "Review", description: "Pending" },
260
+ ],
261
+ },
262
+ render: (args) => (
263
+ <div className="w-[840px]">
264
+ <Stepper {...args} />
265
+ </div>
266
+ ),
267
+ };
268
+
269
+ /**
270
+ * `disabled: true` on an item blocks click and keyboard activation. Arrow-key
271
+ * navigation skips disabled triggers and lands on the next enabled one.
272
+ */
65
273
  export const Disabled: Story = {
274
+ args: {
275
+ defaultValue: 1,
276
+ items: [
277
+ { title: "Account", description: "Available" },
278
+ { title: "Shipping", description: "Locked", disabled: true },
279
+ { title: "Payment", description: "Locked", disabled: true },
280
+ { title: "Review", description: "Locked", disabled: true },
281
+ ],
282
+ },
283
+ render: (args) => (
284
+ <div className="w-[840px]">
285
+ <Stepper {...args} />
286
+ </div>
287
+ ),
288
+ };
289
+
290
+ /**
291
+ * `triggerMode` is a shortcut for restricting which triggers respond to click
292
+ * and keyboard activation, without having to set `disabled` on individual
293
+ * items.
294
+ *
295
+ * - `"both"` (default) — every trigger is clickable.
296
+ * - `"backward-only"` — only past + current steps. The typical wizard guard:
297
+ * the user can revisit but cannot jump ahead.
298
+ * - `"forward-only"` — only current + upcoming steps. Past steps lock out,
299
+ * useful for irreversible flows.
300
+ * - `"none"` — nothing is clickable. The stepper becomes a display-only
301
+ * progress indicator (e.g. an order tracker).
302
+ *
303
+ * Composes with `disabled` per item: both layers apply independently.
304
+ */
305
+ export const TriggerMode: Story = {
66
306
  render: () => {
307
+ const variants: {
308
+ label: string;
309
+ value: "both" | "backward-only" | "forward-only" | "none";
310
+ }[] = [
311
+ { label: "both (default)", value: "both" },
312
+ { label: "backward-only", value: "backward-only" },
313
+ { label: "forward-only", value: "forward-only" },
314
+ { label: "none (read-only)", value: "none" },
315
+ ];
316
+ const items = [
317
+ { title: "Account" },
318
+ { title: "Shipping" },
319
+ { title: "Payment" },
320
+ { title: "Review" },
321
+ ];
322
+
67
323
  return (
68
- <Stepper
69
- items={[
70
- { title: "Cuenta", content: <AccountStep /> },
71
- { title: "Configuracón", content: <FileStep /> },
72
- { title: "Contraseña", content: <PasswordStep />, disabled: true },
73
- ]}
74
- />
324
+ <div className="w-[840px] space-y-8">
325
+ {variants.map(({ label, value }) => (
326
+ <div key={value} className="space-y-2">
327
+ <p className="font-medium text-muted-foreground text-xs uppercase tracking-wide">
328
+ {label}
329
+ </p>
330
+ <Stepper
331
+ defaultValue={2}
332
+ triggerMode={value}
333
+ indicators={{ completed: <CheckIcon className="size-3.5" /> }}
334
+ items={items}
335
+ />
336
+ </div>
337
+ ))}
338
+ </div>
75
339
  );
76
340
  },
77
341
  };
78
342
 
79
343
  /**
80
- * Se puede utilizar el hook `useStep` para facilitar el manejo de estado y validaciones
344
+ * Give every item a `content` and the composite wires the `StepperPanel`
345
+ * for you: one `StepperContent` per step, mount/unmount + ARIA on each
346
+ * change, no external state. Use this when the contents are static and
347
+ * you don't need controlled navigation.
348
+ *
349
+ * Rule of thumb: **all items have `content`, or none do**. Mixing leaves
350
+ * an empty panel on the steps that lack content.
351
+ *
352
+ * For custom panel layouts (animations, conditional rendering, async
353
+ * loading), skip `content` and use the primitives directly — see the
354
+ * `Primitive`, `Wizard`, and `Controlled` stories.
81
355
  */
82
- export const Steps: Story = {
356
+ export const WithPanels: Story = {
357
+ args: {
358
+ defaultValue: 1,
359
+ indicators: { completed: <CheckIcon className="size-3.5" /> },
360
+ items: [
361
+ {
362
+ title: "Account",
363
+ description: "Your details",
364
+ content: (
365
+ <div className="mt-6 rounded-lg border p-4">
366
+ <p className="font-medium text-sm">Account details</p>
367
+ <p className="mt-1 text-muted-foreground text-sm">
368
+ Add your name and email to continue.
369
+ </p>
370
+ </div>
371
+ ),
372
+ },
373
+ {
374
+ title: "Shipping",
375
+ description: "Where to send it",
376
+ content: (
377
+ <div className="mt-6 rounded-lg border p-4">
378
+ <p className="font-medium text-sm">Shipping address</p>
379
+ <p className="mt-1 text-muted-foreground text-sm">
380
+ Pick a saved address or add a new one.
381
+ </p>
382
+ </div>
383
+ ),
384
+ },
385
+ {
386
+ title: "Payment",
387
+ description: "How to pay",
388
+ content: (
389
+ <div className="mt-6 rounded-lg border p-4">
390
+ <p className="font-medium text-sm">Payment method</p>
391
+ <p className="mt-1 text-muted-foreground text-sm">
392
+ Add a credit card or use a saved one.
393
+ </p>
394
+ </div>
395
+ ),
396
+ },
397
+ ],
398
+ },
399
+ render: (args) => (
400
+ <div className="w-[840px]">
401
+ <Stepper {...args} />
402
+ </div>
403
+ ),
404
+ };
405
+
406
+ const wizardSteps = [
407
+ { value: "account", title: "Account", description: "Your details" },
408
+ { value: "shipping", title: "Shipping", description: "Where to send it" },
409
+ { value: "payment", title: "Payment", description: "How to pay" },
410
+ { value: "review", title: "Review", description: "Confirm order" },
411
+ ];
412
+
413
+ /**
414
+ * Linear wizard driven by `useStep`. Note the index translation: `useStep` is
415
+ * **0-indexed**, the stepper is **1-indexed**. Feed it `currentStepIndex + 1`
416
+ * and bridge `onValueChange` back with `goTo(value - 1)`.
417
+ *
418
+ * ```tsx
419
+ * const { currentStepIndex, goTo, next, back } = useStep(steps);
420
+ * <Stepper
421
+ * value={currentStepIndex + 1}
422
+ * onValueChange={(value) => goTo(value - 1)}
423
+ * items={steps}
424
+ * />
425
+ * ```
426
+ */
427
+ export const Wizard: Story = {
83
428
  render: () => {
84
- const { currentStepIndex, isFirstStep, isLastStep, next, back, goTo } =
85
- useStep(steps);
429
+ const {
430
+ currentStepIndex,
431
+ step,
432
+ isFirstStep,
433
+ isLastStep,
434
+ next,
435
+ back,
436
+ goTo,
437
+ } = useStep(wizardSteps);
86
438
 
87
439
  return (
88
- <div>
440
+ <div className="w-[840px] space-y-6">
89
441
  <Stepper
90
- items={steps}
91
- current={currentStepIndex}
92
- onChange={(step: number) => goTo(step)}
442
+ value={currentStepIndex + 1}
443
+ onValueChange={(value) => goTo(value - 1)}
444
+ indicators={{ completed: <CheckIcon className="size-3.5" /> }}
445
+ items={wizardSteps.map((s) => ({
446
+ title: s.title,
447
+ description: s.description,
448
+ }))}
93
449
  />
94
- <div className="flex justify-end gap-x-2 mt-2">
95
- <Button variant="ghost" disabled={isFirstStep} onClick={back}>
96
- Prev
450
+ <div className="rounded-lg border p-6">
451
+ <p className="font-medium text-sm">{step.title}</p>
452
+ <p className="mt-1 text-muted-foreground text-sm">
453
+ {step.description}
454
+ </p>
455
+ </div>
456
+ <div className="flex justify-between">
457
+ <Button variant="outline" disabled={isFirstStep} onClick={back}>
458
+ Back
97
459
  </Button>
98
460
  <Button disabled={isLastStep} onClick={next}>
99
- Next
461
+ {isLastStep ? "Finish" : "Next"}
100
462
  </Button>
101
463
  </div>
102
464
  </div>
@@ -104,27 +466,349 @@ export const Steps: Story = {
104
466
  },
105
467
  };
106
468
 
107
- export const StepperWithPrimitives: Story = {
469
+ /**
470
+ * Controlled mode — `value` + `onValueChange` drive the active step. Steps are
471
+ * 1-indexed.
472
+ *
473
+ * When the user reaches the last step the `Go to` buttons hide and a `Finish`
474
+ * action takes over. Clicking it sets `value` to `items.length + 1`, which
475
+ * pushes every item past the active threshold and flips the last one to the
476
+ * `completed` state.
477
+ */
478
+ export const Controlled: Story = {
108
479
  render: () => {
480
+ const items = [
481
+ { title: "Account" },
482
+ { title: "Shipping" },
483
+ { title: "Payment" },
484
+ { title: "Review" },
485
+ ];
486
+ const [value, setValue] = useState(1);
487
+ const isLastStep = value === items.length;
488
+ const isFinished = value > items.length;
489
+
109
490
  return (
110
- <StepperContainer>
111
- <StepperList>
112
- {steps?.map((item, i) => (
113
- <StepperTrigger
114
- key={i}
115
- value={i}
116
- title={item?.title}
117
- indicator={item?.indicator}
118
- />
119
- ))}
120
- </StepperList>
491
+ <div className="w-[840px] space-y-4">
492
+ <Stepper
493
+ value={value}
494
+ onValueChange={setValue}
495
+ indicators={{ completed: <CheckIcon className="size-3.5" /> }}
496
+ items={items}
497
+ />
498
+ {isFinished ? (
499
+ <div className="flex items-center justify-between rounded-md border bg-accent/40 px-4 py-2 text-sm">
500
+ <span className="inline-flex items-center gap-2 font-medium">
501
+ <CheckIcon className="size-4" />
502
+ All steps completed
503
+ </span>
504
+ <button
505
+ type="button"
506
+ onClick={() => setValue(1)}
507
+ className="cursor-pointer rounded-md border px-3 py-1 text-sm hover:bg-accent"
508
+ >
509
+ Reset
510
+ </button>
511
+ </div>
512
+ ) : isLastStep ? (
513
+ <div className="flex justify-end">
514
+ <Button onClick={() => setValue(items.length + 1)}>Finish</Button>
515
+ </div>
516
+ ) : (
517
+ <div className="flex gap-2">
518
+ {items.map((_, i) => (
519
+ <button
520
+ key={i}
521
+ type="button"
522
+ onClick={() => setValue(i + 1)}
523
+ className="cursor-pointer rounded-md border px-3 py-1 text-sm hover:bg-accent"
524
+ >
525
+ Go to {i + 1}
526
+ </button>
527
+ ))}
528
+ </div>
529
+ )}
530
+ </div>
531
+ );
532
+ },
533
+ };
121
534
 
122
- {steps?.map((item, i) => (
123
- <StepperContent key={i} value={i}>
124
- {item?.content}
125
- </StepperContent>
126
- ))}
127
- </StepperContainer>
535
+ type CheckoutForm = {
536
+ email: string;
537
+ name: string;
538
+ address: string;
539
+ city: string;
540
+ card: string;
541
+ };
542
+
543
+ const emptyForm: CheckoutForm = {
544
+ email: "",
545
+ name: "",
546
+ address: "",
547
+ city: "",
548
+ card: "",
549
+ };
550
+
551
+ const isEmail = (value: string) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value);
552
+
553
+ /**
554
+ * Multi-step form with **persistent state** across steps, **per-step validation
555
+ * gating**, and a simulated async submit that ends in `toast.success`.
556
+ *
557
+ * The recipe:
558
+ * - One `formData` state holds every field — inputs read and write to the
559
+ * same object regardless of which step is active, so navigating back keeps
560
+ * the previous values.
561
+ * - `maxReached` tracks how far the user has progressed. Items with
562
+ * `step > maxReached` get `disabled: true`, which blocks both click and
563
+ * keyboard nav to future steps. Visited steps stay clickable so the user
564
+ * can review.
565
+ * - `canAdvance` derives from the current step's required fields. The Next
566
+ * button is disabled until those fields are valid; the user cannot bypass
567
+ * it via the stepper triggers because of the rule above.
568
+ * - On Finish, a fake 1.2s promise simulates the backend call. The button
569
+ * shows `loading`, and on resolve a `toast.success` fires.
570
+ */
571
+ export const WithValidation: Story = {
572
+ render: () => {
573
+ const [activeStep, setActiveStep] = useState(1);
574
+ const [maxReached, setMaxReached] = useState(1);
575
+ const [formData, setFormData] = useState<CheckoutForm>(emptyForm);
576
+ const [isFinishing, setIsFinishing] = useState(false);
577
+
578
+ const update = (field: keyof CheckoutForm) => (value: string) =>
579
+ setFormData((prev) => ({ ...prev, [field]: value }));
580
+
581
+ const validators = {
582
+ 1: isEmail(formData.email) && formData.name.trim().length > 0,
583
+ 2: formData.address.trim().length > 0 && formData.city.trim().length > 0,
584
+ 3: formData.card.replace(/\s/g, "").length >= 12,
585
+ } as const;
586
+ const canAdvance = validators[activeStep as 1 | 2 | 3];
587
+
588
+ const handleNext = () => {
589
+ if (!canAdvance) return;
590
+ const next = activeStep + 1;
591
+ setActiveStep(next);
592
+ setMaxReached((m) => Math.max(m, next));
593
+ };
594
+
595
+ const handleBack = () => setActiveStep((s) => Math.max(1, s - 1));
596
+
597
+ const handleReset = () => {
598
+ setFormData(emptyForm);
599
+ setActiveStep(1);
600
+ setMaxReached(1);
601
+ };
602
+
603
+ const handleFinish = async () => {
604
+ if (!canAdvance) return;
605
+ setIsFinishing(true);
606
+ await new Promise((resolve) => setTimeout(resolve, 1200));
607
+ setIsFinishing(false);
608
+ setActiveStep(4);
609
+ setMaxReached(4);
610
+ toast.success("Order placed successfully");
611
+ };
612
+
613
+ const items = [
614
+ { title: "Account", description: "Your details" },
615
+ { title: "Shipping", description: "Where to send it" },
616
+ { title: "Payment", description: "How to pay" },
617
+ ];
618
+
619
+ const isLastStep = activeStep === items.length;
620
+ const isFinished = activeStep > items.length;
621
+
622
+ return (
623
+ <div className="w-[840px] space-y-6">
624
+ <Stepper
625
+ value={activeStep}
626
+ onValueChange={setActiveStep}
627
+ indicators={{ completed: <CheckIcon className="size-3.5" /> }}
628
+ items={items.map((item, i) => ({
629
+ ...item,
630
+ disabled: i + 1 > maxReached,
631
+ }))}
632
+ />
633
+
634
+ <div className="rounded-lg border p-6">
635
+ {activeStep === 1 && (
636
+ <div className="space-y-4">
637
+ <Field
638
+ label="Email"
639
+ required
640
+ error={
641
+ formData.email && !isEmail(formData.email)
642
+ ? "Enter a valid email address"
643
+ : undefined
644
+ }
645
+ >
646
+ <Input
647
+ value={formData.email}
648
+ onChange={update("email")}
649
+ placeholder="you@example.com"
650
+ />
651
+ </Field>
652
+ <Field label="Full name" required>
653
+ <Input
654
+ value={formData.name}
655
+ onChange={update("name")}
656
+ placeholder="Jane Doe"
657
+ />
658
+ </Field>
659
+ </div>
660
+ )}
661
+
662
+ {activeStep === 2 && (
663
+ <div className="space-y-4">
664
+ <Field label="Address" required>
665
+ <Input
666
+ value={formData.address}
667
+ onChange={update("address")}
668
+ placeholder="221B Baker Street"
669
+ />
670
+ </Field>
671
+ <Field label="City" required>
672
+ <Input
673
+ value={formData.city}
674
+ onChange={update("city")}
675
+ placeholder="London"
676
+ />
677
+ </Field>
678
+ </div>
679
+ )}
680
+
681
+ {activeStep === 3 && (
682
+ <Field
683
+ label="Card number"
684
+ required
685
+ description="At least 12 digits."
686
+ error={
687
+ formData.card && formData.card.replace(/\s/g, "").length < 12
688
+ ? "Card number is too short"
689
+ : undefined
690
+ }
691
+ >
692
+ <Input
693
+ value={formData.card}
694
+ onChange={update("card")}
695
+ placeholder="4242 4242 4242 4242"
696
+ inputMode="numeric"
697
+ />
698
+ </Field>
699
+ )}
700
+
701
+ {isFinished && (
702
+ <div className="flex flex-col items-center gap-2 py-4 text-center">
703
+ <CheckIcon className="size-8 text-success" />
704
+ <p className="font-medium">All set</p>
705
+ <p className="text-muted-foreground text-sm">
706
+ We've sent a receipt to{" "}
707
+ <span className="font-medium text-foreground">
708
+ {formData.email}
709
+ </span>
710
+ .
711
+ </p>
712
+ </div>
713
+ )}
714
+ </div>
715
+
716
+ {isFinished ? (
717
+ <div className="flex justify-center">
718
+ <Button variant="outline" onClick={handleReset}>
719
+ Start over
720
+ </Button>
721
+ </div>
722
+ ) : (
723
+ <div className="flex justify-between">
724
+ <Button
725
+ variant="outline"
726
+ onClick={handleBack}
727
+ disabled={activeStep === 1 || isFinishing}
728
+ >
729
+ Back
730
+ </Button>
731
+ {isLastStep ? (
732
+ <Button
733
+ onClick={handleFinish}
734
+ disabled={!canAdvance || isFinishing}
735
+ loading={isFinishing}
736
+ >
737
+ {isFinishing ? "Processing…" : "Finish"}
738
+ </Button>
739
+ ) : (
740
+ <Button onClick={handleNext} disabled={!canAdvance}>
741
+ Next
742
+ </Button>
743
+ )}
744
+ </div>
745
+ )}
746
+ </div>
128
747
  );
129
748
  },
130
749
  };
750
+
751
+ /**
752
+ * Compose the primitives directly when the items API isn't enough — custom
753
+ * markup between steps, dynamic indicator rendering, extra slots, etc.
754
+ *
755
+ * ```tsx
756
+ * <StepperRoot defaultValue={2}>
757
+ * <StepperNav>
758
+ * <StepperItem step={1}>
759
+ * <StepperTrigger>
760
+ * <StepperIndicator>1</StepperIndicator>
761
+ * <StepperTitle>Account</StepperTitle>
762
+ * </StepperTrigger>
763
+ * <StepperSeparator />
764
+ * </StepperItem>
765
+ * </StepperNav>
766
+ * <StepperPanel>
767
+ * <StepperContent value={1}>Account form…</StepperContent>
768
+ * </StepperPanel>
769
+ * </StepperRoot>
770
+ * ```
771
+ */
772
+ export const Primitive: Story = {
773
+ render: () => (
774
+ <div className="w-[840px]">
775
+ <StepperRoot defaultValue={2}>
776
+ <StepperNav>
777
+ {[
778
+ { step: 1, title: "Account" },
779
+ { step: 2, title: "Shipping" },
780
+ { step: 3, title: "Payment" },
781
+ { step: 4, title: "Review" },
782
+ ].map(({ step, title }, i, arr) => (
783
+ <StepperItem key={step} step={step}>
784
+ <StepperTrigger>
785
+ <StepperIndicator>
786
+ <CheckIcon className="hidden size-3.5 group-data-[state=completed]/step:block" />
787
+ <span className="group-data-[state=completed]/step:hidden">
788
+ {step}
789
+ </span>
790
+ </StepperIndicator>
791
+ <div className="flex flex-col gap-0.5 text-left">
792
+ <StepperTitle>{title}</StepperTitle>
793
+ </div>
794
+ </StepperTrigger>
795
+ {i < arr.length - 1 ? <StepperSeparator /> : null}
796
+ </StepperItem>
797
+ ))}
798
+ </StepperNav>
799
+ <StepperPanel className="mt-6">
800
+ {[1, 2, 3, 4].map((step) => (
801
+ <StepperContent key={step} value={step}>
802
+ <div className="rounded-lg border p-4">
803
+ <p className="font-medium text-sm">Step {step}</p>
804
+ <p className="mt-1 text-muted-foreground text-sm">
805
+ Custom panel content for step {step}.
806
+ </p>
807
+ </div>
808
+ </StepperContent>
809
+ ))}
810
+ </StepperPanel>
811
+ </StepperRoot>
812
+ </div>
813
+ ),
814
+ };