torch-glare 2.5.2 → 2.5.4

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 (51) hide show
  1. package/apps/lib/components/ActionButton.tsx +39 -6
  2. package/apps/lib/components/Badge.tsx +5 -2
  3. package/apps/lib/components/BadgeField.tsx +1 -1
  4. package/apps/lib/components/Button.tsx +4 -4
  5. package/apps/lib/components/Calendar.tsx +7 -17
  6. package/apps/lib/components/ColorPicker.tsx +1 -1
  7. package/apps/lib/components/DataViews/data-views.tsx +1 -1
  8. package/apps/lib/components/DataViews/filters/filters.tsx +1 -1
  9. package/apps/lib/components/DataViews/header.tsx +5 -2
  10. package/apps/lib/components/DataViews/panel/section.tsx +3 -1
  11. package/apps/lib/components/DataViews/views/board-view.tsx +3 -1
  12. package/apps/lib/components/DataViews/views/inbox-view.tsx +4 -1
  13. package/apps/lib/components/DataViews/views/pane-views.tsx +5 -1
  14. package/apps/lib/components/DataViews/views/table-view.tsx +67 -8
  15. package/apps/lib/components/DataViews/views/tree-view.tsx +5 -3
  16. package/apps/lib/components/Drawer.tsx +18 -1
  17. package/apps/lib/components/FormBuilder/context.ts +9 -1
  18. package/apps/lib/components/FormBuilder/submit.tsx +11 -2
  19. package/apps/lib/components/FormBuilder/types.ts +5 -1
  20. package/apps/lib/components/FormRenderer/form-renderer.tsx +15 -3
  21. package/apps/lib/components/FormRenderer/stepper.tsx +35 -14
  22. package/apps/lib/components/FormRenderer/types.ts +1 -1
  23. package/apps/lib/components/Input.tsx +19 -4
  24. package/apps/lib/components/Popover.tsx +93 -55
  25. package/apps/lib/components/SearchableSelect.tsx +12 -11
  26. package/apps/lib/components/SearchableTree.tsx +10 -11
  27. package/apps/lib/components/SearchableTreeDialog.tsx +10 -11
  28. package/apps/lib/components/SectionBlock.tsx +14 -2
  29. package/apps/lib/components/Select.tsx +35 -11
  30. package/apps/lib/components/SimpleSelect.tsx +2 -2
  31. package/apps/lib/components/SlideDatePicker.tsx +17 -4
  32. package/apps/lib/components/Stepper.tsx +329 -180
  33. package/apps/lib/components/Switch.tsx +2 -2
  34. package/apps/lib/components/Table.tsx +55 -30
  35. package/apps/lib/components/Textarea.tsx +29 -4
  36. package/apps/lib/components/TreeDropDown.tsx +18 -6
  37. package/apps/lib/components/TreeFolder/TreeFolder.tsx +61 -61
  38. package/apps/lib/registry.json +7 -16
  39. package/apps/lib/tsconfig.tsbuildinfo +1 -1
  40. package/docs/components/action-button.md +3 -1
  41. package/docs/components/data-views/index.md +7 -0
  42. package/docs/components/form-builder.md +4 -2
  43. package/docs/components/form-renderer.md +1 -1
  44. package/docs/components/stepper.md +119 -24
  45. package/docs/components/table.md +33 -0
  46. package/docs/how-to/forms-with-form-builder.md +3 -1
  47. package/docs/reference/tailwind-plugins.md +42 -0
  48. package/docs/tutorials/getting-started.md +32 -11
  49. package/package.json +1 -1
  50. package/apps/lib/components/FormStepper.tsx +0 -272
  51. package/docs/components/form-stepper.md +0 -250
@@ -1,16 +1,37 @@
1
1
  "use client";
2
2
 
3
3
  import React, { forwardRef, HTMLAttributes, ReactNode, createContext, useContext } from "react";
4
- import { cva, type VariantProps } from "class-variance-authority";
4
+ import { cva } from "class-variance-authority";
5
5
  import { cn } from "../utils/cn";
6
6
  import { Themes } from "../utils/types";
7
7
 
8
+ /**
9
+ * Stepper — the pill-shaped multi-step indicator from Figma `FormStepper-1.0`.
10
+ *
11
+ * This is the single stepper in the library: it merges the Figma pill (three semantic types with
12
+ * resting/hover/selected states and a corner badge) with the orientation, sizes and connector that
13
+ * used to live in a separate generic component.
14
+ *
15
+ * `type` + `selected` express the four states the generic one had:
16
+ * pending → type="default" unselected · active → type="default" selected
17
+ * completed → type="success" · error → type="negative"
18
+ *
19
+ * Not to be confused with `FormRenderer.Stepper`, which is the wizard *behaviour* (step state,
20
+ * validation, Back/Next) and renders this component as its rail.
21
+ */
22
+
23
+ // ─── Types ───────────────────────────────────────────────────────────────────
24
+
25
+ type StepperType = "default" | "success" | "negative";
26
+ type StepperOrientation = "horizontal" | "vertical";
27
+ type StepperSize = "S" | "M" | "L";
28
+
8
29
  // ─── Context ─────────────────────────────────────────────────────────────────
9
30
 
10
31
  interface StepperContextValue {
11
32
  activeStep: number;
12
- orientation: "horizontal" | "vertical";
13
- size: "S" | "M" | "L";
33
+ orientation: StepperOrientation;
34
+ size: StepperSize;
14
35
  }
15
36
 
16
37
  const StepperContext = createContext<StepperContextValue>({
@@ -23,11 +44,20 @@ const useStepperContext = () => useContext(StepperContext);
23
44
 
24
45
  // ─── Stepper Root ────────────────────────────────────────────────────────────
25
46
 
26
- const stepperStyles = cva(["flex gap-0"], {
47
+ interface StepperProps extends HTMLAttributes<HTMLDivElement> {
48
+ theme?: Themes;
49
+ activeStep?: number;
50
+ /** Direction the step pills flow. Vertical is the FormRenderer rail. */
51
+ orientation?: StepperOrientation;
52
+ /** Scales the pill, indicator and label. Figma defines M; S and L scale from it. */
53
+ size?: StepperSize;
54
+ }
55
+
56
+ const stepperStyles = cva(["flex"], {
27
57
  variants: {
28
58
  orientation: {
29
- horizontal: "flex-row items-start",
30
- vertical: "flex-col",
59
+ horizontal: "flex-row items-center gap-3",
60
+ vertical: "flex-col items-start gap-[4px]",
31
61
  },
32
62
  },
33
63
  defaultVariants: {
@@ -35,21 +65,9 @@ const stepperStyles = cva(["flex gap-0"], {
35
65
  },
36
66
  });
37
67
 
38
- interface StepperProps extends HTMLAttributes<HTMLDivElement>, VariantProps<typeof stepperStyles> {
39
- theme?: Themes;
40
- activeStep?: number;
41
- size?: "S" | "M" | "L";
42
- }
43
-
44
68
  const Stepper = forwardRef<HTMLDivElement, StepperProps>(
45
- ({ className, orientation = "horizontal", theme, activeStep = 0, size = "M", ...props }, ref) => (
46
- <StepperContext.Provider
47
- value={{
48
- activeStep,
49
- orientation: orientation ?? "horizontal",
50
- size: size ?? "M",
51
- }}
52
- >
69
+ ({ className, theme, activeStep = 0, orientation = "horizontal", size = "M", ...props }, ref) => (
70
+ <StepperContext.Provider value={{ activeStep, orientation, size }}>
53
71
  <div
54
72
  ref={ref}
55
73
  data-theme={theme}
@@ -62,43 +80,74 @@ const Stepper = forwardRef<HTMLDivElement, StepperProps>(
62
80
  );
63
81
  Stepper.displayName = "Stepper";
64
82
 
65
- // ─── Step ────────────────────────────────────────────────────────────────────
83
+ // ─── Step (the pill wrapper) ─────────────────────────────────────────────────
84
+
85
+ const stepStyles = cva(
86
+ [
87
+ "group/step inline-flex items-center shrink-0",
88
+ "rounded-full p-[2px] max-w-[144px]",
89
+ "transition-all duration-200 ease-in-out",
90
+ "cursor-pointer select-none outline-none",
91
+ ],
92
+ {
93
+ variants: {
94
+ // M is the size Figma draws (28px pill / 24px indicator); S and L scale from it.
95
+ size: {
96
+ S: "h-[24px]",
97
+ M: "h-[28px]",
98
+ L: "h-[34px]",
99
+ },
100
+ // Selection drives the pill background; a raised surface appears on hover for unselected
101
+ // pills. `drop-shadow` rather than `shadow`: Figma draws a filter that follows the pill's
102
+ // rounded silhouette, where a box-shadow would ring the layout box.
103
+ selected: {
104
+ true: [
105
+ "bg-background-presentation-button-hover",
106
+ // Figma's onSelect and onSelect-Hoverd carry the same shadow — a selected pill does not
107
+ // deepen when you hover it.
108
+ "drop-shadow-[0_0_16px_rgba(0,0,0,0.2)]",
109
+ ],
110
+ false: [
111
+ "bg-transparent",
112
+ // Figma binds this to the raw palette colour `White/00`, which would stay solid white in
113
+ // dark theme. `form-base` is the themable equivalent — identical in light, and a raised
114
+ // surface rather than a white slab in dark.
115
+ "hover:bg-background-presentation-form-base",
116
+ "hover:drop-shadow-[0_0_16px_rgba(0,0,0,0.05)]",
117
+ ],
118
+ },
119
+ },
120
+ defaultVariants: {
121
+ selected: false,
122
+ size: "M",
123
+ },
124
+ },
125
+ );
66
126
 
67
- interface StepProps extends HTMLAttributes<HTMLDivElement> {
127
+ interface StepProps extends Omit<HTMLAttributes<HTMLDivElement>, "type"> {
68
128
  index?: number;
69
- isCompleted?: boolean;
70
- isActive?: boolean;
71
- isError?: boolean;
129
+ type?: StepperType;
130
+ selected?: boolean;
72
131
  }
73
132
 
74
133
  const Step = forwardRef<HTMLDivElement, StepProps>(
75
- ({ className, index = 0, isCompleted, isActive, isError, children, ...props }, ref) => {
76
- const { activeStep, orientation } = useStepperContext();
77
-
78
- const computedActive = isActive ?? index === activeStep;
79
- const computedCompleted = isCompleted ?? index < activeStep;
80
- const computedError = isError ?? false;
134
+ ({ className, index = 0, type = "default", selected, children, ...props }, ref) => {
135
+ const { activeStep, size } = useStepperContext();
136
+ const isSelected = selected ?? index === activeStep;
81
137
 
82
138
  return (
83
139
  <div
84
140
  ref={ref}
85
- data-active={computedActive || undefined}
86
- data-completed={computedCompleted || undefined}
87
- data-error={computedError || undefined}
88
- data-orientation={orientation}
89
- className={cn(
90
- "flex group/step",
91
- orientation === "horizontal" ? "shrink-0 flex-col items-center gap-2" : "flex-row gap-3",
92
- className,
93
- )}
141
+ data-selected={isSelected || undefined}
142
+ data-type={type}
143
+ className={cn(stepStyles({ selected: isSelected, size }), className)}
94
144
  {...props}
95
145
  >
96
146
  {React.Children.map(children, (child) => {
97
147
  if (!React.isValidElement(child)) return child;
98
148
  return React.cloneElement(child as React.ReactElement<Record<string, unknown>>, {
99
- _active: computedActive,
100
- _completed: computedCompleted,
101
- _error: computedError,
149
+ _selected: isSelected,
150
+ _type: type,
102
151
  _index: index,
103
152
  });
104
153
  })}
@@ -108,60 +157,117 @@ const Step = forwardRef<HTMLDivElement, StepProps>(
108
157
  );
109
158
  Step.displayName = "Step";
110
159
 
111
- // ─── Step Indicator ──────────────────────────────────────────────────────────
160
+ // ─── StepIndicator (circle badge) ────────────────────────────────────────────
112
161
 
113
162
  const stepIndicatorStyles = cva(
114
163
  [
115
- "flex items-center justify-center shrink-0 rounded-full",
116
- "border",
117
- "transition-all duration-200 ease-in-out",
118
- "typography-body-small-medium",
119
- "[&_i]:leading-none [&_i]:flex [&_i]:items-center [&_i]:justify-center",
164
+ "relative inline-flex items-center justify-center shrink-0",
165
+ "rounded-full",
166
+ "transition-colors duration-200 ease-in-out",
167
+ "typography-body-medium-medium",
120
168
  ],
121
169
  {
122
170
  variants: {
123
- state: {
124
- pending: [
125
- "bg-background-presentation-action-disabled",
126
- "border-border-presentation-action-disabled",
127
- "text-content-presentation-state-disabled",
171
+ // The pill is `p-[2px]`, so the indicator is always the pill height minus 4. It is a capsule,
172
+ // not a circle: `min-w` + `px` let a two-digit step number widen it, as Figma draws.
173
+ size: {
174
+ S: "h-[20px] min-w-[20px] px-[5px] text-[12px]",
175
+ M: "h-[24px] min-w-[24px] px-[6px] text-[14px]",
176
+ L: "h-[30px] min-w-[30px] px-[7px] text-[16px]",
177
+ },
178
+ type: {
179
+ // `ring` rather than `border`: a ring is a box-shadow and takes no layout space, so the
180
+ // `min-w`/`px` capsule above lands on Figma's exact width. A 3px border would eat the
181
+ // content box under `border-box` and push a single digit past it.
182
+ default: ["ring-[3px] ring-inset"],
183
+ success: [
184
+ "bg-background-presentation-state-success-primary text-content-presentation-global-primary-inverse",
128
185
  ],
129
- active: [
130
- "bg-background-presentation-state-information-primary",
131
- "border-border-presentation-state-focus",
132
- "text-content-presentation-state-information",
186
+ negative: [
187
+ "bg-background-presentation-state-negative-primary text-content-presentation-global-primary-inverse",
133
188
  ],
134
- completed: [
135
- "bg-background-presentation-state-success-primary",
136
- "border-border-presentation-state-success",
137
- "text-content-presentation-state-success",
189
+ },
190
+ // Only meaningful for type=default. Selected = filled blue, otherwise
191
+ // ring color comes from the resting/hover compound variants below.
192
+ selected: {
193
+ true: "",
194
+ false: "",
195
+ },
196
+ },
197
+ defaultVariants: {
198
+ type: "default",
199
+ selected: false,
200
+ size: "M",
201
+ },
202
+ compoundVariants: [
203
+ // Default type, not selected: gray ring + gray number at rest;
204
+ // hover (driven by parent .group/step) flips ring blue, number primary.
205
+ {
206
+ type: "default",
207
+ selected: false,
208
+ className: [
209
+ "bg-transparent ring-border-presentation-stepper-default text-content-presentation-global-secondary",
210
+ "group-hover/step:ring-content-presentation-state-information",
211
+ "group-hover/step:text-content-presentation-global-primary",
138
212
  ],
139
- error: [
140
- "bg-background-presentation-state-negative-primary",
141
- "border-border-presentation-state-negative",
142
- "text-content-presentation-state-negative",
213
+ },
214
+ // Default type, selected: solid blue fill, inverse number, no ring.
215
+ {
216
+ type: "default",
217
+ selected: true,
218
+ className: [
219
+ "bg-background-presentation-state-information-primary ring-transparent text-content-presentation-global-primary-inverse",
143
220
  ],
144
221
  },
222
+ ],
223
+ },
224
+ );
225
+
226
+ const stepIndicatorBadgeStyles = cva(
227
+ [
228
+ "absolute",
229
+ "rounded-full",
230
+ "inline-flex items-center justify-center",
231
+ "border border-background-presentation-body-primary",
232
+ // Figma sits the glyph in a fixed square frame. Without pinning the box, a Remix icon's
233
+ // advance width is wider than its font-size and pushes the badge past its designed 15px.
234
+ "[&_i]:leading-none [&_i]:flex [&_i]:items-center [&_i]:justify-center",
235
+ // LTR: badge top-right; RTL: badge top-left
236
+ "ltr:right-[-4.94px] rtl:left-[-4.94px]",
237
+ ],
238
+ {
239
+ variants: {
145
240
  size: {
146
- S: "w-[22px] h-[22px] text-[10px] [&_i]:text-[12px]",
147
- M: "w-[28px] h-[28px] text-[12px] [&_i]:text-[14px]",
148
- L: "w-[34px] h-[34px] text-[14px] [&_i]:text-[16px]",
241
+ // Padding absorbs the 1px ring: Figma draws its stroke inside the 15px frame, where
242
+ // `border-box` adds it on top. Its documented 2.667px would render a 16.9px badge.
243
+ S: "h-[13px] min-w-[13px] px-[1.35px] -top-[4px] [&_i]:text-[8.3px] [&_i]:size-[8.3px]",
244
+ M: "h-[15px] min-w-[15px] px-[1.7px] -top-[5px] [&_i]:text-[9.6px] [&_i]:size-[9.6px]",
245
+ L: "h-[18px] min-w-[18px] px-[2.25px] -top-[6px] [&_i]:text-[11.5px] [&_i]:size-[11.5px]",
246
+ },
247
+ type: {
248
+ success:
249
+ "bg-background-presentation-state-success-primary text-content-presentation-global-primary-inverse",
250
+ negative:
251
+ "bg-background-presentation-state-negative-primary text-content-presentation-global-primary-inverse",
149
252
  },
150
253
  },
151
254
  defaultVariants: {
152
- state: "pending",
153
255
  size: "M",
154
256
  },
155
257
  },
156
258
  );
157
259
 
158
260
  interface StepIndicatorProps extends HTMLAttributes<HTMLDivElement> {
261
+ /** Icon in the corner badge (success/negative types only). */
262
+ badgeIcon?: ReactNode;
263
+ /** Replaces the step number in the circle. */
159
264
  icon?: ReactNode;
265
+ /** Replaces the circle's content when the step is `success`. */
160
266
  completedIcon?: ReactNode;
267
+ /** Replaces the circle's content when the step is `negative`. */
161
268
  errorIcon?: ReactNode;
162
- _active?: boolean;
163
- _completed?: boolean;
164
- _error?: boolean;
269
+ _selected?: boolean;
270
+ _type?: StepperType;
165
271
  _index?: number;
166
272
  }
167
273
 
@@ -169,12 +275,12 @@ const StepIndicator = forwardRef<HTMLDivElement, StepIndicatorProps>(
169
275
  (
170
276
  {
171
277
  className,
278
+ badgeIcon,
172
279
  icon,
173
280
  completedIcon,
174
281
  errorIcon,
175
- _active,
176
- _completed,
177
- _error,
282
+ _selected,
283
+ _type = "default",
178
284
  _index = 0,
179
285
  children,
180
286
  ...props
@@ -182,139 +288,181 @@ const StepIndicator = forwardRef<HTMLDivElement, StepIndicatorProps>(
182
288
  ref,
183
289
  ) => {
184
290
  const { size } = useStepperContext();
185
-
186
- const state = _error ? "error" : _completed ? "completed" : _active ? "active" : "pending";
187
-
188
- const renderContent = () => {
189
- if (_error && errorIcon) return errorIcon;
190
- if (_error) return <i className="ri-close-line" />;
191
- if (_completed && completedIcon) return completedIcon;
192
- if (_completed) return <i className="ri-check-line" />;
193
- if (icon) return icon;
194
- if (children) return children;
195
- return _index + 1;
196
- };
291
+ const showBadge = _type === "success" || _type === "negative";
292
+ const defaultBadgeIcon =
293
+ _type === "success" ? <i className="ri-check-line" /> : <i className="ri-information-fill" />;
294
+
295
+ // Most specific first: a per-state icon, then a general one, then children, then the number.
296
+ const content =
297
+ (_type === "negative" && errorIcon) ||
298
+ (_type === "success" && completedIcon) ||
299
+ icon ||
300
+ children ||
301
+ _index + 1;
197
302
 
198
303
  return (
199
- <div ref={ref} className={cn(stepIndicatorStyles({ state, size }), className)} {...props}>
200
- {renderContent()}
304
+ <div
305
+ ref={ref}
306
+ className={cn(stepIndicatorStyles({ type: _type, selected: !!_selected, size }), className)}
307
+ {...props}
308
+ >
309
+ {content}
310
+ {showBadge && (
311
+ <span
312
+ className={cn(
313
+ stepIndicatorBadgeStyles({
314
+ type: _type as "success" | "negative",
315
+ size,
316
+ }),
317
+ )}
318
+ aria-hidden
319
+ >
320
+ {badgeIcon ?? defaultBadgeIcon}
321
+ </span>
322
+ )}
201
323
  </div>
202
324
  );
203
325
  },
204
326
  );
205
327
  StepIndicator.displayName = "StepIndicator";
206
328
 
207
- // ─── Step Connector (the line between steps) ────────────────────────────────
329
+ // ─── StepLabel ───────────────────────────────────────────────────────────────
208
330
 
209
- const connectorStyles = cva(["transition-all duration-200 ease-in-out"], {
210
- variants: {
211
- orientation: {
212
- horizontal: "h-[2px] flex-1 mx-2 mt-[13px]",
213
- vertical: "w-[2px] flex-1 min-h-[24px] my-2",
331
+ const stepLabelStyles = cva(
332
+ [
333
+ // Figma caps the label and ellipsizes it, so a long step title clips inside the pill rather
334
+ // than stretching it. Callers that need more room pass their own `max-w`.
335
+ "max-w-[106px] truncate",
336
+ "transition-[padding,color] duration-200 ease-in-out",
337
+ // At rest the label sits 6px from the circle. On hover (parent pill
338
+ // hovered) the gap grows to 9px — matches figma onHover/onSelect-Hoverd.
339
+ // The outer edge keeps a constant 12px.
340
+ "ltr:pl-[6px] ltr:pr-[12px] rtl:pr-[6px] rtl:pl-[12px]",
341
+ "group-hover/step:ltr:pl-[9px] group-hover/step:rtl:pr-[9px]",
342
+ ],
343
+ {
344
+ variants: {
345
+ size: {
346
+ S: "typography-body-small-medium",
347
+ M: "typography-body-small-medium",
348
+ L: "typography-body-medium-medium",
349
+ },
350
+ selected: {
351
+ true: "text-content-presentation-global-primary-light",
352
+ false: "",
353
+ },
354
+ // Resting colour depends on the type, which is why this cannot be driven by `selected` alone:
355
+ // a default step reads de-emphasized until you hover it, while a success/negative step is
356
+ // already resolved and reads at full strength.
357
+ type: {
358
+ default: "",
359
+ success: "",
360
+ negative: "",
361
+ },
214
362
  },
215
- state: {
216
- pending: "bg-border-presentation-action-disabled",
217
- completed: "bg-border-presentation-state-focus",
363
+ defaultVariants: {
364
+ selected: false,
365
+ type: "default",
366
+ size: "M",
218
367
  },
368
+ compoundVariants: [
369
+ {
370
+ selected: false,
371
+ type: "default",
372
+ className: [
373
+ "text-content-presentation-global-secondary",
374
+ "group-hover/step:text-content-presentation-global-primary",
375
+ ],
376
+ },
377
+ {
378
+ selected: false,
379
+ type: "success",
380
+ className: "text-content-presentation-global-primary",
381
+ },
382
+ {
383
+ selected: false,
384
+ type: "negative",
385
+ className: "text-content-presentation-global-primary",
386
+ },
387
+ ],
219
388
  },
220
- defaultVariants: {
221
- orientation: "horizontal",
222
- state: "pending",
223
- },
224
- compoundVariants: [
225
- {
226
- orientation: "horizontal",
227
- className: "self-start",
228
- },
229
- ],
230
- });
389
+ );
231
390
 
232
- interface StepConnectorProps extends HTMLAttributes<HTMLDivElement> {
233
- _completed?: boolean;
234
- _active?: boolean;
235
- _error?: boolean;
391
+ interface StepLabelProps extends HTMLAttributes<HTMLDivElement> {
392
+ _selected?: boolean;
393
+ _type?: StepperType;
236
394
  _index?: number;
237
395
  }
238
396
 
239
- const StepConnector = forwardRef<HTMLDivElement, StepConnectorProps>(
397
+ const StepLabel = forwardRef<HTMLDivElement, StepLabelProps>(
240
398
  // eslint-disable-next-line @typescript-eslint/no-unused-vars -- excluded from {...props} spread
241
- ({ className, _completed, _active: _a, _error: _e, _index: _i, ...props }, ref) => {
242
- const { orientation } = useStepperContext();
243
-
399
+ ({ className, _selected, _type = "default", _index: _i, ...props }, ref) => {
400
+ const { size } = useStepperContext();
244
401
  return (
245
402
  <div
246
403
  ref={ref}
247
- className={cn(
248
- connectorStyles({
249
- orientation,
250
- state: _completed ? "completed" : "pending",
251
- }),
252
- className,
253
- )}
404
+ className={cn(stepLabelStyles({ selected: !!_selected, type: _type, size }), className)}
254
405
  {...props}
255
406
  />
256
407
  );
257
408
  },
258
409
  );
259
- StepConnector.displayName = "StepConnector";
260
-
261
- // ─── Step Label ──────────────────────────────────────────────────────────────
410
+ StepLabel.displayName = "StepLabel";
262
411
 
263
- interface StepLabelProps extends HTMLAttributes<HTMLDivElement> {
264
- _active?: boolean;
265
- _completed?: boolean;
266
- _error?: boolean;
267
- _index?: number;
268
- }
412
+ // ─── StepConnector (the bar between steps) ───────────────────────────────────
269
413
 
270
- const StepLabel = forwardRef<HTMLDivElement, StepLabelProps>(
271
- // eslint-disable-next-line @typescript-eslint/no-unused-vars -- excluded from {...props} spread
272
- ({ className, _active, _completed, _error, _index: _i, ...props }, ref) => (
273
- <div
274
- ref={ref}
275
- className={cn(
276
- "typography-body-small-medium transition-colors duration-200 ease-in-out",
277
- _active
278
- ? "text-content-presentation-state-information"
279
- : _error
280
- ? "text-content-presentation-state-negative"
281
- : _completed
282
- ? "text-content-presentation-global-primary"
283
- : "text-content-presentation-state-disabled",
284
- className,
285
- )}
286
- {...props}
287
- />
288
- ),
414
+ const stepConnectorStyles = cva(
415
+ ["shrink-0 rounded-full transition-colors duration-200 ease-in-out"],
416
+ {
417
+ variants: {
418
+ orientation: {
419
+ horizontal: "h-[3px] w-[16px] self-center",
420
+ vertical: "w-[3px] h-[16px] mt-[2px]",
421
+ },
422
+ size: {
423
+ S: "",
424
+ M: "",
425
+ L: "",
426
+ },
427
+ completed: {
428
+ true: "bg-border-presentation-state-focus",
429
+ false: "bg-border-presentation-stepper-default",
430
+ },
431
+ },
432
+ defaultVariants: {
433
+ orientation: "horizontal",
434
+ size: "M",
435
+ completed: false,
436
+ },
437
+ compoundVariants: [
438
+ // Vertical: centre the bar under the indicator — the pill's 2px padding, plus half the
439
+ // indicator, minus half the bar. Which is why the inset tracks the size.
440
+ { orientation: "vertical", size: "S", className: "ms-[10.5px]" },
441
+ { orientation: "vertical", size: "M", className: "ms-[12.5px]" },
442
+ { orientation: "vertical", size: "L", className: "ms-[15.5px]" },
443
+ ],
444
+ },
289
445
  );
290
- StepLabel.displayName = "StepLabel";
291
-
292
- // ─── Step Description ────────────────────────────────────────────────────────
293
446
 
294
- interface StepDescriptionProps extends HTMLAttributes<HTMLDivElement> {
295
- _active?: boolean;
296
- _completed?: boolean;
297
- _error?: boolean;
298
- _index?: number;
447
+ interface StepConnectorProps extends HTMLAttributes<HTMLDivElement> {
448
+ /** Draws the bar in the focus colour, for the run of steps already passed. */
449
+ completed?: boolean;
299
450
  }
300
451
 
301
- const StepDescription = forwardRef<HTMLDivElement, StepDescriptionProps>(
302
- // eslint-disable-next-line @typescript-eslint/no-unused-vars -- excluded from {...props} spread
303
- ({ className, _active, _completed: _c, _error: _e, _index: _i, ...props }, ref) => (
304
- <div
305
- ref={ref}
306
- className={cn(
307
- "typography-body-small-regular",
308
- _active
309
- ? "text-content-presentation-global-secondary"
310
- : "text-content-presentation-state-disabled",
311
- className,
312
- )}
313
- {...props}
314
- />
315
- ),
452
+ const StepConnector = forwardRef<HTMLDivElement, StepConnectorProps>(
453
+ ({ className, completed = false, ...props }, ref) => {
454
+ const { orientation, size } = useStepperContext();
455
+ return (
456
+ <div
457
+ ref={ref}
458
+ aria-hidden
459
+ className={cn(stepConnectorStyles({ orientation, size, completed }), className)}
460
+ {...props}
461
+ />
462
+ );
463
+ },
316
464
  );
317
- StepDescription.displayName = "StepDescription";
465
+ StepConnector.displayName = "StepConnector";
318
466
 
319
467
  // ─── Exports ─────────────────────────────────────────────────────────────────
320
468
 
@@ -322,10 +470,11 @@ export {
322
470
  Stepper,
323
471
  Step,
324
472
  StepIndicator,
325
- StepConnector,
326
473
  StepLabel,
327
- StepDescription,
474
+ StepConnector,
328
475
  stepperStyles,
476
+ stepStyles,
329
477
  stepIndicatorStyles,
330
- connectorStyles,
478
+ stepLabelStyles,
479
+ stepConnectorStyles,
331
480
  };
@@ -56,9 +56,9 @@ function Switchers({
56
56
  >
57
57
  <div
58
58
  className={cn(
59
- "absolute left-[2px] w-[24px] h-[24px] rounded-full bg-background-presentation-switcher-knob shadow-[0px_0px_0px_1px_rgba(0,0,0,0.04),0px_3px_8px_0px_rgba(0,0,0,0.15),0px_3px_1px_0px_rgba(0,0,0,0.06)] transition-all duration-200",
59
+ "absolute start-[2px] w-[24px] h-[24px] rounded-full bg-background-presentation-switcher-knob shadow-[0px_0px_0px_1px_rgba(0,0,0,0.04),0px_3px_8px_0px_rgba(0,0,0,0.15),0px_3px_1px_0px_rgba(0,0,0,0.06)] transition-all duration-200",
60
60
  {
61
- "left-[22px]": active,
61
+ "start-[22px]": active,
62
62
  }
63
63
  )}
64
64
  />