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

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