torch-glare 2.5.3 → 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 (32) 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/views/table-view.tsx +33 -4
  8. package/apps/lib/components/Drawer.tsx +18 -1
  9. package/apps/lib/components/FormRenderer/stepper.tsx +22 -16
  10. package/apps/lib/components/Input.tsx +19 -4
  11. package/apps/lib/components/Popover.tsx +93 -55
  12. package/apps/lib/components/SearchableSelect.tsx +12 -11
  13. package/apps/lib/components/SearchableTree.tsx +10 -11
  14. package/apps/lib/components/SearchableTreeDialog.tsx +10 -11
  15. package/apps/lib/components/SectionBlock.tsx +3 -1
  16. package/apps/lib/components/Select.tsx +35 -11
  17. package/apps/lib/components/SimpleSelect.tsx +2 -2
  18. package/apps/lib/components/SlideDatePicker.tsx +17 -4
  19. package/apps/lib/components/Stepper.tsx +329 -180
  20. package/apps/lib/components/Switch.tsx +2 -2
  21. package/apps/lib/components/Table.tsx +1 -1
  22. package/apps/lib/components/Textarea.tsx +29 -4
  23. package/apps/lib/components/TreeDropDown.tsx +18 -6
  24. package/apps/lib/registry.json +7 -16
  25. package/apps/lib/tsconfig.tsbuildinfo +1 -1
  26. package/docs/components/action-button.md +3 -1
  27. package/docs/components/stepper.md +119 -24
  28. package/docs/reference/tailwind-plugins.md +42 -0
  29. package/docs/tutorials/getting-started.md +32 -11
  30. package/package.json +1 -1
  31. package/apps/lib/components/FormStepper.tsx +0 -272
  32. package/docs/components/form-stepper.md +0 -250
@@ -4,12 +4,24 @@ import { Button } from "./Button";
4
4
  import { cn } from "../utils/cn";
5
5
  import { ButtonVariant, Themes } from "../utils/types";
6
6
 
7
- const buttonVariants = cva("!rounded-[4px]", {
7
+ /**
8
+ * The button that sits at the end of an input field — Figma `ActionButton` (Type=DropDown).
9
+ *
10
+ * Sizes are Figma's exactly: Xs 18, S 22, M 32, with `radius/sm` on the two small ones and
11
+ * `radius/lg` on M.
12
+ *
13
+ * The `!` on the radius is load-bearing. `twMerge` does not dedupe `rounded-radius-*` — they are
14
+ * custom scale keys, not values it recognises — so this radius and the one coming from `Button`'s
15
+ * own `size` variant both survive, and CSS source order picks the winner. Tailwind emits the scale
16
+ * in config order (sm before md before lg), so without `!` size S would render Button M's 6px
17
+ * instead of its own 4px.
18
+ */
19
+ const buttonVariants = cva("", {
8
20
  variants: {
9
21
  size: {
10
- XS: "h-[18px] w-[18px] text-[12px]",
11
- S: "h-[22px] w-[22px] text-[12px]",
12
- M: "h-[32px] w-[32px] text-[18px]",
22
+ XS: "h-[18px] w-[18px] text-[12px] !rounded-radius-sm",
23
+ S: "h-[22px] w-[22px] text-[12px] !rounded-radius-sm",
24
+ M: "h-[32px] w-[32px] text-[18px] !rounded-radius-lg",
13
25
  },
14
26
  },
15
27
  defaultVariants: {
@@ -17,6 +29,23 @@ const buttonVariants = cva("!rounded-[4px]", {
17
29
  },
18
30
  });
19
31
 
32
+ /**
33
+ * Default/hover/disabled colours as Figma binds them. Applied only when the caller passes no
34
+ * `variant`, so anything asking for a specific Button variant still gets it.
35
+ *
36
+ * `action-secondary` and `action-disabled` already match `Button`'s `PrimeStyle` equivalents; the
37
+ * real difference is hover, where `PrimeStyle` uses `button-hover` and the design uses
38
+ * `action-hover`.
39
+ */
40
+ const actionTokens = [
41
+ "bg-background-presentation-action-secondary",
42
+ "text-content-presentation-action-light-primary",
43
+ "hover:bg-background-presentation-action-hover",
44
+ "hover:text-content-presentation-global-hover",
45
+ "disabled:bg-background-presentation-action-disabled",
46
+ "disabled:text-content-presentation-state-disabled",
47
+ ];
48
+
20
49
  interface Props
21
50
  extends ButtonHTMLAttributes<HTMLButtonElement>, VariantProps<typeof buttonVariants> {
22
51
  is_loading?: boolean;
@@ -29,7 +58,6 @@ interface Props
29
58
  export const ActionButton = function ({
30
59
  size,
31
60
  asChild,
32
- // eslint-disable-next-line @typescript-eslint/no-unused-vars -- excluded from {...props} spread
33
61
  as: Tag = "button",
34
62
  className,
35
63
  variant,
@@ -42,14 +70,19 @@ export const ActionButton = function ({
42
70
  <Button
43
71
  theme={theme}
44
72
  asChild={asChild}
73
+ // Forwarded, not swallowed: the chevron inside a field renders `as="span"` because it is
74
+ // nested in a clickable trigger, and a <button> cannot contain a <button>.
75
+ as={Tag}
45
76
  buttonType="icon"
46
- type={type}
77
+ // `type` is only meaningful on a real button; on a span it would be an invalid attribute.
78
+ type={Tag === "button" ? type : undefined}
47
79
  size={size == "XS" ? "S" : size == "S" ? "M" : size == "M" ? "L" : "S"}
48
80
  variant={variant}
49
81
  className={cn(
50
82
  buttonVariants({
51
83
  size,
52
84
  }),
85
+ !variant && actionTokens,
53
86
  className,
54
87
  )}
55
88
  {...props}
@@ -118,7 +118,10 @@ const subtleCompoundVariants = [
118
118
  export const badgeStyles = cva(
119
119
  [
120
120
  "inline-flex items-center justify-center w-fit",
121
- "rounded-[6px]",
121
+ // `radius/md` (6px), read off the Badge master component in Figma. Note the badge instances
122
+ // nested inside InnerLabelField render at `radius/lg` — that is an instance override, not the
123
+ // component's own value, and reading it as the spec is how this briefly shipped as 8px.
124
+ "rounded-radius-md",
122
125
  "transition-all duration-200 ease-in-out",
123
126
  "whitespace-nowrap",
124
127
  "[&_i]:!leading-none",
@@ -215,7 +218,7 @@ export const Badge: React.FC<BadgeProps> = ({
215
218
  }
216
219
  }}
217
220
  className={cn(
218
- "rounded-[4px]",
221
+ "rounded-radius-sm",
219
222
  "flex items-center justify-center cursor-pointer",
220
223
  "hover:bg-background-presentation-action-secondary",
221
224
  "transition-colors duration-150",
@@ -222,7 +222,7 @@ export const BadgeField = forwardRef<HTMLInputElement, Props>(
222
222
  fill="black"
223
223
  />
224
224
  </svg>
225
- <p className="text-black-1000 text-right text-[12px] font-[510] leading-[148%]">
225
+ <p className="text-black-1000 text-end text-[12px] font-[510] leading-[148%]">
226
226
  {addLabel}
227
227
  </p>
228
228
  </div>
@@ -248,10 +248,10 @@ export const buttonVariants = cva(
248
248
  ],
249
249
  },
250
250
  size: {
251
- S: "h-[22px] py-[2px] px-[6px] typography-body-small-medium rounded-[4px] [&_i]:text-[12px]",
252
- M: "h-[28px] py-[2px] px-[14px] typography-body-large-medium rounded-[6px] [&_i]:text-[18px]",
253
- L: "h-[34px] py-[5px] px-[22px] typography-body-large-medium rounded-[8px] [&_i]:text-[20px]",
254
- XL: "h-[40px] py-[8px] px-[30px] typography-headers-medium-medium rounded-[8px] [&_i]:text-[22px]",
251
+ S: "h-[22px] py-[2px] px-[6px] typography-body-small-medium rounded-radius-sm [&_i]:text-[12px]",
252
+ M: "h-[28px] py-[2px] px-[14px] typography-body-large-medium rounded-radius-md [&_i]:text-[18px]",
253
+ L: "h-[34px] py-[5px] px-[22px] typography-body-large-medium rounded-radius-lg [&_i]:text-[20px]",
254
+ XL: "h-[40px] py-[8px] px-[30px] typography-headers-medium-medium rounded-radius-lg [&_i]:text-[22px]",
255
255
  },
256
256
  is_loading: {
257
257
  true: "[&_i]:hidden",
@@ -4,7 +4,7 @@ import * as React from "react";
4
4
  import { DayPicker } from "react-day-picker";
5
5
  import "react-day-picker/style.css";
6
6
  import { cn } from "../utils/cn";
7
- import { Button } from "./Button";
7
+ import { ActionButton } from "./ActionButton";
8
8
  import { SimpleSelectValue, SimpleSelectItem } from "./SimpleSelect";
9
9
 
10
10
  export type CalendarProps = React.ComponentProps<typeof DayPicker>;
@@ -30,24 +30,14 @@ const Calendar = ({
30
30
  Nav: (e) => {
31
31
  return (
32
32
  <div className="w-full flex items-center justify-between absolute top-0 left-0 p-[6px]">
33
- <Button
34
- onClick={e.onPreviousClick}
35
- buttonType="icon"
36
- variant="PrimeStyle"
37
- size="M"
38
- className="w-[26px] h-[26px]"
39
- >
33
+ {/* Popover navigation rather than an in-field button, so it keeps its own 26px box
34
+ and only adopts the shared action colours and radius. */}
35
+ <ActionButton onClick={e.onPreviousClick} size="S" className="w-[26px] h-[26px]">
40
36
  <i className="ri-arrow-left-s-line"></i>
41
- </Button>
42
- <Button
43
- onClick={e.onNextClick}
44
- buttonType="icon"
45
- variant="PrimeStyle"
46
- size="M"
47
- className="w-[26px] h-[26px]"
48
- >
37
+ </ActionButton>
38
+ <ActionButton onClick={e.onNextClick} size="S" className="w-[26px] h-[26px]">
49
39
  <i className="ri-arrow-right-s-line"></i>
50
- </Button>
40
+ </ActionButton>
51
41
  </div>
52
42
  );
53
43
  },
@@ -404,7 +404,7 @@ export const ColorPicker = forwardRef<HTMLElement, ColorPickerProps>(
404
404
  }}
405
405
  className={NUM_INPUT_CLS}
406
406
  />
407
- <span className="pr-1 typography-body-small-regular text-content-presentation-action-light-secondary">
407
+ <span className="pe-1 typography-body-small-regular text-content-presentation-action-light-secondary">
408
408
  %
409
409
  </span>
410
410
  </Group>
@@ -132,7 +132,18 @@ function TableViewImpl({
132
132
  // pushes the whole component wide instead of scrolling within it.
133
133
  className="min-w-0 flex-1 overflow-auto rounded-lg"
134
134
  >
135
- <Table ref={tableRef} className="w-full">
135
+ {/* One width for the table and the footer bar below it. Inside an `overflow-auto`
136
+ scroller a child's `w-full` resolves against the *visible* width, not the scrollable
137
+ width — so without this box the footer stopped at the fold whenever the columns
138
+ overflowed. `w-max` sizes to the table (the widest child); `min-w-full` keeps it
139
+ filling the scroller when the table is narrower. */}
140
+ <div className="w-max min-w-full">
141
+ {/* `overflow-visible` opts out of the primitive's `overflow-hidden` (tailwind-merge
142
+ lets ours win). It has to: a clipping table is its own scrollport, and the sticky
143
+ header would then resolve against a box that never scrolls. Safe here because the
144
+ scroller above is `min-w-0 overflow-auto`, so a wide table scrolls inside it rather
145
+ than pushing the layout. */}
146
+ <Table ref={tableRef} className="w-full overflow-visible">
136
147
  {/* The header sticks to the scroller above by default — that lives on `TableHeader`
137
148
  in the primitive. Two things this view adds:
138
149
 
@@ -228,7 +239,7 @@ function TableViewImpl({
228
239
  >
229
240
  {onRowMove && <GripCell />}
230
241
  {selectable && (
231
- <TableCell isDummy className="h-[40px] w-12" onClick={(e) => e.stopPropagation()}>
242
+ <TableCell isDummy className="min-h-[40px] w-12" onClick={(e) => e.stopPropagation()}>
232
243
  {/* What `TableCheckbox` renders, inlined: its props are typed as button
233
244
  attributes, so it cannot express a controlled checkbox. */}
234
245
  <div className="flex items-center justify-center">
@@ -245,8 +256,20 @@ function TableViewImpl({
245
256
  // `undefined` means "you paint it" — distinct from `null`, which is a
246
257
  // deliberate blank.
247
258
  const custom = renderCell?.({ field, row, id, index, fields: visibleFields });
259
+ // `fade={false}` and clip instead. The fade is a `mask-image` on every
260
+ // cell, close to free on its own but very expensive once a sticky header
261
+ // repaints the region each scroll frame: measured on this page,
262
+ // mask+sticky costs ~61ms/frame against ~29ms with the mask off and
263
+ // ~17ms with neither. `overflow-hidden` is passed back because
264
+ // `fade={false}` otherwise switches the cell to `overflow-visible`, and
265
+ // the text would spill into the next column.
248
266
  return (
249
- <TableCell key={`${field.path}-${i}`} className="h-[40px]">
267
+ <TableCell
268
+ key={`${field.path}-${i}`}
269
+ className="min-h-[40px]"
270
+ fade={false}
271
+ childrenClassName="overflow-hidden"
272
+ >
250
273
  {/* `isolate` confines the Badge's mix-blend-luminosity to a local
251
274
  stacking context and `transform-gpu` promotes it to its own layer, so
252
275
  the table's post-mount column reflow repaints cleanly instead of
@@ -287,6 +310,7 @@ function TableViewImpl({
287
310
  {addRowLabel}
288
311
  </TableEndAction>
289
312
  )}
313
+ </div>
290
314
  </div>
291
315
  </div>
292
316
  </div>
@@ -419,7 +443,12 @@ function SkeletonRows({
419
443
  {grip && <TableCell isDummy className="h-[40px] w-8" />}
420
444
  {selectable && <TableCell isDummy className="h-[40px] w-12" />}
421
445
  {fields.map((field, i) => (
422
- <TableCell key={`${field.path}-${i}`} className="h-[40px]">
446
+ <TableCell
447
+ key={`${field.path}-${i}`}
448
+ className="min-h-[40px]"
449
+ fade={false}
450
+ childrenClassName="overflow-hidden"
451
+ >
423
452
  {/* Widths cycle rather than repeat, so a column of bars reads as text that has not
424
453
  arrived instead of as a grid. */}
425
454
  <SkeletonBar className={["w-[70%]", "w-[45%]", "w-[85%]", "w-[60%]"][(row + i) % 4]} />
@@ -37,6 +37,7 @@ DrawerOverlay.displayName = DrawerPrimitive.Overlay.displayName;
37
37
  interface DrawerContentProps extends React.ComponentPropsWithoutRef<
38
38
  typeof DrawerPrimitive.Content
39
39
  > {
40
+ /** @deprecated No effect here — the drag handle is `DrawerPanel`'s `showHandle`. */
40
41
  showHandle?: boolean;
41
42
  notch?: React.ReactNode;
42
43
  notchSide?: "left" | "right";
@@ -87,7 +88,17 @@ const DrawerPanel = React.forwardRef<HTMLDivElement, DrawerPanelProps>(
87
88
  // would render white-on-light = invisible).
88
89
  data-theme="light"
89
90
  className={cn(
90
- "flex flex-1 gap-2 rounded-t-[16px] p-1 bg-[#F0F0F0] min-h-0",
91
+ // `flex-col` is load-bearing: this panel stacks header / body / footer, and its own
92
+ // `showHandle` centres the drag handle with `mx-auto`, which only centres in a column.
93
+ // It was dropped in 37fddd5 alongside the tray's — on the tray that was deliberate (FormDrawer
94
+ // puts the panel and its summary side by side), here it was not, and every consumer that puts
95
+ // a header/body/footer straight into a panel rendered them in a row.
96
+ //
97
+ // `min-w-0` for the same reason, one axis over: the tray is a row now, so width is its
98
+ // main axis, and a flex item's default `min-width: auto` pins it to its content — a wide
99
+ // form pushed the panel straight out past the tray. `min-h-0` alone covered the old
100
+ // column tray; the row needs both.
101
+ "flex flex-1 flex-col gap-2 rounded-t-[16px] p-1 bg-[#F0F0F0] min-h-0 min-w-0",
91
102
  framed && "border border-[#D4D4D4] shadow-[inset_0_-4px_16px_rgba(0,0,0,0.1)]",
92
103
  className,
93
104
  )}
@@ -113,6 +124,12 @@ const DrawerContent = React.forwardRef<
113
124
  framed: framedProp,
114
125
  wrapperClassName,
115
126
  trayClassName,
127
+ // Destructured only to keep it out of `...props`. The handle belongs to `DrawerPanel` now, so
128
+ // this does nothing here — but it was never pulled out either, so it was being spread onto the
129
+ // DOM node and React warned "does not recognize the `showHandle` prop on a DOM element" every
130
+ // time a drawer opened. Kept in the props type rather than removed: consumers still pass it.
131
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
132
+ showHandle,
116
133
  ...props
117
134
  },
118
135
  ref,
@@ -10,7 +10,16 @@ import { Button } from "../Button";
10
10
  // stepper — the rail, the state, Back/Next — is chrome and lives here. The dependency only ever
11
11
  // points this way: FormRenderer → FormBuilder, never back.
12
12
  import { StepContext, type StepRegistry } from "../FormBuilder/context";
13
- import { FormStepper, FormStep, FormStepIndicator, FormStepLabel } from "../FormStepper";
13
+ // Aliased: this module exports its own `Stepper` and `Step` (the wizard behaviour —
14
+ // `FormRenderer.Stepper` / `.Step`), which would otherwise collide with the presentational
15
+ // component of the same name.
16
+ import {
17
+ Stepper as StepperRail,
18
+ Step as StepPill,
19
+ StepIndicator,
20
+ StepLabel,
21
+ StepConnector,
22
+ } from "../Stepper";
14
23
 
15
24
  // ─── Stepper state context ───────────────────────────────────────────────────
16
25
 
@@ -116,7 +125,7 @@ function StepperNav({ control }: { control: Control<FieldValues> }) {
116
125
  // `min-w-0` so the rail can be squeezed: its grid track no longer grows to fit a long label
117
126
  // (see form-renderer.tsx), so the column has to be allowed to shrink and let the labels
118
127
  // truncate instead of spilling over the fields column.
119
- <FormStepper activeStep={currentStep} className="min-w-0 shrink-0 flex-col items-start gap-[4px]">
128
+ <StepperRail activeStep={currentStep} orientation="vertical" className="min-w-0 shrink-0">
120
129
  {titles.map((title, index) => {
121
130
  // The step buttons ARE the navigation: click to move. Backward is free;
122
131
  // clicking forward validates the steps in between (goToStep) and stops at
@@ -129,11 +138,11 @@ function StepperNav({ control }: { control: Control<FieldValues> }) {
129
138
  : "default";
130
139
  return (
131
140
  <React.Fragment key={title}>
132
- {/* The shrink/truncate pair is passed in rather than changed in `FormStepper`, so its
141
+ {/* The shrink/truncate pair is passed in rather than changed in `Stepper`, so its
133
142
  other consumers keep sizing to their labels. `shrink` beats the pill's own
134
143
  `shrink-0` and `truncate` beats the label's `whitespace-nowrap` — both via
135
- tailwind-merge, which `FormStepper` runs the caller's className through. */}
136
- <FormStep
144
+ tailwind-merge, which `Stepper` runs the caller's className through. */}
145
+ <StepPill
137
146
  index={index}
138
147
  type={type}
139
148
  onClick={() => void goToStep(index)}
@@ -142,20 +151,17 @@ function StepperNav({ control }: { control: Control<FieldValues> }) {
142
151
  // past the track instead of clipping inside it.
143
152
  className="min-w-0 max-w-full shrink"
144
153
  >
145
- <FormStepIndicator />
146
- <FormStepLabel className="truncate">{title}</FormStepLabel>
147
- </FormStep>
148
- {/* Connector between steps — a 3×16 rounded bar centred under the badge. */}
149
- {index < titles.length - 1 && (
150
- <div
151
- aria-hidden
152
- className="ms-[12.5px] mt-[2px] h-[16px] w-[3px] rounded-full bg-[#A0A0A0]"
153
- />
154
- )}
154
+ <StepIndicator />
155
+ {/* `max-w-none` opts out of the component's 106px cap: the rail sizes to its own
156
+ grid track, so the label should truncate there rather than 106px early. */}
157
+ <StepLabel className="max-w-none truncate">{title}</StepLabel>
158
+ </StepPill>
159
+ {/* Connector between steps — the component centres it under the indicator per size. */}
160
+ {index < titles.length - 1 && <StepConnector />}
155
161
  </React.Fragment>
156
162
  );
157
163
  })}
158
- </FormStepper>
164
+ </StepperRail>
159
165
  );
160
166
  }
161
167
 
@@ -62,7 +62,16 @@ interface TrillingProps extends HTMLAttributes<HTMLDivElement> {
62
62
  }
63
63
  export const Trilling = ({ children, className, ...props }: TrillingProps) => {
64
64
  return (
65
- <div {...props} className={cn("flex items-center justify-center h-full gap-1 py-1", className)}>
65
+ // `ps-1` mirrors the inner side of Figma's `EndField-Container` (`p-[4px]`), keeping the field's
66
+ // text clear of the end button instead of running straight into it.
67
+ //
68
+ // Logical, not physical: this slot flips to the left edge in RTL, so a `pl-1` would put the
69
+ // padding on the outside — measured as 0px of clearance and the button 8px from the edge
70
+ // instead of 4px. `padding-inline-start` follows the slot.
71
+ <div
72
+ {...props}
73
+ className={cn("flex items-center justify-center h-full gap-1 py-1 ps-1", className)}
74
+ >
66
75
  {children}
67
76
  </div>
68
77
  );
@@ -135,7 +144,10 @@ input:-webkit-autofill:active {
135
144
 
136
145
  export const GroupStyles = cva(
137
146
  [
138
- "flex w-full min-w-0 px-1 justify-center items-center",
147
+ // 3px, not 4: Figma's `EndField-Container` is `p-[4px]` and its stroke sits *inside* that
148
+ // padding, so the end button lands 4px from the field's outer edge. CSS `border-box` adds the
149
+ // 1px border on top of the padding, so 4px here would render 5px. 1 + 3 = 4.
150
+ "flex w-full min-w-0 px-[3px] justify-center items-center",
139
151
  "typography-body-small-regular",
140
152
  "border",
141
153
  "transition-all duration-200 ease-in-out",
@@ -180,14 +192,17 @@ export const GroupStyles = cva(
180
192
  "[&_input]:text-white",
181
193
  ],
182
194
  },
195
+ // Radius per Figma's `radius/*` collection: S is `radius/lg` (8px) and M is `radius/xl`
196
+ // (12px) — a deliberate jump, not consecutive steps. This `Group` is the shared field root,
197
+ // so it is also what gives `InputField` and `InnerLabelField` their corners.
183
198
  size: {
184
199
  S: [
185
200
  "h-[30px] min-h-[30px]",
186
- "rounded-[6px] [&_input]:h-[30px] [&_div[data-role='icon']]:text-[16px]",
201
+ "rounded-radius-lg [&_input]:h-[30px] [&_div[data-role='icon']]:text-[16px]",
187
202
  ],
188
203
  M: [
189
204
  "h-[40px] min-h-[40px]",
190
- "rounded-[8px] [&_input]:h-[40px] [&_div[data-role='icon']]:text-[18px] [&_div[data-role='icon']]:px-[2px]",
205
+ "rounded-radius-xl [&_input]:h-[40px] [&_div[data-role='icon']]:text-[18px] [&_div[data-role='icon']]:px-[2px]",
191
206
  ],
192
207
  },
193
208
  error: {