torch-glare 2.4.3 → 2.4.5

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.
@@ -224,12 +224,13 @@ export function SearchableTable<T extends Record<string, unknown>>({
224
224
  data-theme="dark"
225
225
  className="w-full rounded-[10px] overflow-hidden"
226
226
  >
227
- <TableHeader className="bg-black-alpha-20 shadow-[0px_4px_8px_0px_rgba(0,0,0,0.15)]">
228
- <TableRow className="h-[44px] [&_button]:bg-white-alpha-40 [&>th]:border-white-alpha-20">
227
+ <TableHeader className="backdrop-blur-none bg-black-alpha-20 shadow-[0px_4px_8px_0px_rgba(0,0,0,0.15)]">
228
+ <TableRow className="h-[44px] [&_button:not([data-slot=resize-handle])]:bg-white-alpha-40 [&>th]:border-white-alpha-20">
229
229
  {columns.map((col) => (
230
230
  <TableHead
231
231
  key={col.key}
232
- className="cursor-default typography-body-medium-medium text-white"
232
+ className="cursor-default"
233
+ contentClassName="typography-body-medium-medium text-white"
233
234
  >
234
235
  {col.header}
235
236
  </TableHead>
@@ -252,7 +253,7 @@ export function SearchableTable<T extends Record<string, unknown>>({
252
253
  {columns.map((col) => (
253
254
  <TableCell
254
255
  key={col.key}
255
- className="border-b border-white-alpha-20 text-white px-[8px] !whitespace-nowrap !break-normal"
256
+ className="h-[40px] border-b border-white-alpha-20 text-white px-[8px] !whitespace-nowrap !break-normal"
256
257
  >
257
258
  {col.render ? col.render(row) : String(row[col.key] ?? "")}
258
259
  </TableCell>
@@ -21,9 +21,61 @@ const titleBadge = cva(
21
21
  },
22
22
  );
23
23
 
24
+ const container = cva(
25
+ "flex w-full px-0 flex-col rounded-[16px] bg-background-presentation-form-base shadow-[0_0_32px_2px_rgba(0,0,0,0.05)]",
26
+ {
27
+ variants: {
28
+ variant: {
29
+ Default: "pt-[6px] pb-[24px]",
30
+ // No bottom padding — the table's end-action row is the last element and meets
31
+ // the card edge; `overflow-hidden` clips it to the 16px radius.
32
+ Table: "pt-[6px] pb-0 overflow-hidden",
33
+ },
34
+ },
35
+ defaultVariants: { variant: "Default" },
36
+ },
37
+ );
38
+
39
+ const header = cva("flex px-[6px] justify-between gap-3", {
40
+ variants: {
41
+ variant: {
42
+ Default: "items-center",
43
+ Table: "items-start",
44
+ },
45
+ },
46
+ defaultVariants: { variant: "Default" },
47
+ });
48
+
49
+ const body = cva("flex w-full flex-col", {
50
+ variants: {
51
+ variant: {
52
+ Default: "px-[42px] gap-[2px]",
53
+ // Full bleed, with the rule that separates the header from the table.
54
+ Table: "mt-[6px] border-t border-border-presentation-global-primary overflow-hidden",
55
+ },
56
+ },
57
+ defaultVariants: { variant: "Default" },
58
+ });
59
+
60
+ const rail = cva("flex w-full min-w-[300px] flex-col items-start", {
61
+ variants: {
62
+ variant: {
63
+ // Hairline between each direct child (form rows).
64
+ Default: "divide-y divide-gray-300",
65
+ // The table draws its own row borders — a divide rule would double up on the
66
+ // table / scroller / end-action siblings.
67
+ Table: "",
68
+ },
69
+ },
70
+ defaultVariants: { variant: "Default" },
71
+ });
72
+
24
73
  export type SectionColor = NonNullable<VariantProps<typeof titleBadge>["color"]>;
74
+ export type SectionVariant = NonNullable<VariantProps<typeof container>["variant"]>;
25
75
 
26
- export interface SectionBlockProps extends Omit<HTMLAttributes<HTMLDivElement>, "title"> {
76
+ export interface SectionBlockProps
77
+ extends Omit<HTMLAttributes<HTMLDivElement>, "title">,
78
+ VariantProps<typeof container> {
27
79
  color?: SectionColor;
28
80
  title?: ReactNode;
29
81
  icon?: ReactNode;
@@ -39,6 +91,7 @@ export const SectionBlock = forwardRef<HTMLDivElement, SectionBlockProps>(
39
91
  {
40
92
  children,
41
93
  color,
94
+ variant,
42
95
  title,
43
96
  action,
44
97
  className,
@@ -53,15 +106,11 @@ export const SectionBlock = forwardRef<HTMLDivElement, SectionBlockProps>(
53
106
  return (
54
107
  <div
55
108
  ref={ref}
56
- className={cn(
57
- "flex w-full pt-[6px] pb-[24px] px-0 flex-col rounded-[16px] bg-background-presentation-form-base shadow-[0_0_32px_2px_rgba(0,0,0,0.05)]",
58
- className,
59
- containerClassName,
60
- )}
109
+ className={cn(container({ variant }), className, containerClassName)}
61
110
  {...props}
62
111
  >
63
112
  {(title || action) && (
64
- <div className={cn("flex px-[6px] items-center justify-between gap-3", headerClassName)}>
113
+ <div className={cn(header({ variant }), headerClassName)}>
65
114
  {title ? (
66
115
  <div className={cn(titleBadge({ color }))}>
67
116
  <span className="flex items-center gap-1.5">
@@ -75,10 +124,8 @@ export const SectionBlock = forwardRef<HTMLDivElement, SectionBlockProps>(
75
124
  {action && <div className="flex shrink-0 items-center gap-2">{action}</div>}
76
125
  </div>
77
126
  )}
78
- <div className={cn("flex px-[42px] flex-col gap-[2px] px-[42px]", bodyClassName)}>
79
- <div className="flex w-full divide-y divide-gray-300 min-w-[300px] flex-col items-start ">
80
- {children}
81
- </div>
127
+ <div className={cn(body({ variant }), bodyClassName)}>
128
+ <div className={cn(rail({ variant }))}>{children}</div>
82
129
  </div>
83
130
  </div>
84
131
  );
@@ -293,7 +293,9 @@ const PopoverTriggerStyles = cva(
293
293
  "hover:caret-border-presentation-state-negative",
294
294
  ],
295
295
  },
296
- // Transparent border/background so the trigger blends into a table cell.
296
+ // Transparent border/background so the trigger blends into a table cell. The hover and
297
+ // focus shadows are deliberately left alone — a cell control lifts exactly like any
298
+ // other field.
297
299
  onTable: {
298
300
  true: ["border-transparent", "bg-transparent"],
299
301
  },
@@ -32,7 +32,14 @@ const TableHeader = React.forwardRef<
32
32
  >(({ className, ...props }, ref) => (
33
33
  <thead
34
34
  ref={ref}
35
- className={cn("shadow-[0px_4px_8px_0px_rgba(0,0,0,0.15)]", className)}
35
+ // The header band is one continuous bar, not a per-cell background — a background on each
36
+ // `<th>` would paint over the half of the previous column's resize handle that overhangs
37
+ // the boundary.
38
+ className={cn(
39
+ "bg-background-presentation-form-header backdrop-blur-[8px]",
40
+ "shadow-[0px_4px_8px_0px_rgba(0,0,0,0.15)]",
41
+ className,
42
+ )}
36
43
  {...props}
37
44
  />
38
45
  ));
@@ -95,6 +102,9 @@ const TableRow = React.forwardRef<
95
102
  ));
96
103
  TableRow.displayName = "TableRow";
97
104
 
105
+ /** Floor for a drag-resized column, so dragging past the column's own left edge can't invert it. */
106
+ const MIN_COLUMN_WIDTH = 40;
107
+
98
108
  const TableHead = React.forwardRef<
99
109
  HTMLTableCellElement,
100
110
  React.ThHTMLAttributes<HTMLTableCellElement> &
@@ -102,57 +112,133 @@ const TableHead = React.forwardRef<
102
112
  React.ButtonHTMLAttributes<HTMLButtonElement> & {
103
113
  sortType?: "asc" | "desc" | undefined;
104
114
  onSort?: () => void;
115
+ /** Column name for the sort button's accessible label. */
116
+ sortLabel?: string;
117
+ /**
118
+ * Called with the new width (px) while the column is being drag-resized. Pass it to take
119
+ * **control** of the width — the header then renders `style.width` and expects you to
120
+ * feed the new value back. Required whenever the table needs a definite width
121
+ * (`table-layout: fixed`), because only the owner of every column width can total them.
122
+ * Omit for uncontrolled resizing, where the header keeps the width itself.
123
+ */
124
+ onResize?: (width: number) => void;
125
+ /**
126
+ * Classes for the inner layout box (the flex row holding the label and sort toggle).
127
+ * `className` and every other prop go to the `<th>` — reach for this only when you need
128
+ * to restyle the content box itself, e.g. its typography or colour.
129
+ */
130
+ contentClassName?: string;
105
131
  isDummy?: boolean;
106
132
  }
107
- >(({ className, size = "M", disabled, sortType, onSort, isDummy, ...props }, forwardedRef) => {
108
- const headRef = useRef<HTMLTableCellElement>(null);
109
- const { width, handleStartResize } = useResize(headRef as React.RefObject<HTMLElement>);
133
+ >(
134
+ (
135
+ {
136
+ className,
137
+ contentClassName,
138
+ children,
139
+ style,
140
+ size = "M",
141
+ disabled,
142
+ sortType,
143
+ onSort,
144
+ sortLabel,
145
+ onResize,
146
+ isDummy,
147
+ ...props
148
+ },
149
+ forwardedRef,
150
+ ) => {
151
+ const headRef = useRef<HTMLTableCellElement>(null);
152
+ const { width, handleStartResize } = useResize(headRef as React.RefObject<HTMLElement>);
110
153
 
111
- // Combine refs using useEffect
112
- React.useEffect(() => {
113
- if (!forwardedRef) return;
114
- if (typeof forwardedRef === "function") forwardedRef(headRef.current);
115
- else forwardedRef.current = headRef.current;
116
- }, [forwardedRef]);
154
+ const clampedWidth = width === undefined ? undefined : Math.max(width, MIN_COLUMN_WIDTH);
117
155
 
118
- return (
119
- <th
120
- ref={headRef}
121
- className={cn(
122
- "relative py-[6px] px-[4px] border-b-[2px] border-border-presentation-table-header",
123
- )}
124
- >
125
- <div
156
+ React.useEffect(() => {
157
+ if (clampedWidth !== undefined) onResize?.(clampedWidth);
158
+ // `onResize` is intentionally not a dep — callers pass an inline closure, and re-firing
159
+ // on every render would loop.
160
+ // eslint-disable-next-line react-hooks/exhaustive-deps
161
+ }, [clampedWidth]);
162
+
163
+ // Combine refs using useEffect
164
+ React.useEffect(() => {
165
+ if (!forwardedRef) return;
166
+ if (typeof forwardedRef === "function") forwardedRef(headRef.current);
167
+ else forwardedRef.current = headRef.current;
168
+ }, [forwardedRef]);
169
+
170
+ // The width has to land on the `<th>` — that is the element the browser sizes the column
171
+ // from. Written to an inner div (as it once was) it did nothing at all. When `onResize` is
172
+ // given the caller owns the value and feeds it back via `style.width`; otherwise the drag
173
+ // result is applied here directly.
174
+ const resolvedWidth = onResize ? style?.width : (clampedWidth ?? style?.width);
175
+
176
+ return (
177
+ <th
178
+ ref={headRef}
179
+ // EVERY caller prop lands here: `className`, `style`, `aria-*` and event handlers
180
+ // alike. They have to share one element — dnd-kit's `useSortable` returns `attributes`
181
+ // (role/tabIndex/aria-*) and `listeners` (onKeyDown/onPointerDown) as a matched pair,
182
+ // and splitting them across the <th> and the inner div left the focusable node with no
183
+ // key handler. Use `contentClassName` to reach the inner box.
126
184
  {...props}
185
+ style={{ ...style, width: resolvedWidth }}
127
186
  className={cn(
128
- tableHeadVariants({ size, disabled, isDummy }),
129
- { "min-w-[100px]": !isDummy },
187
+ "relative h-[44px] py-[6px] px-[4px] border-b-[2px] border-border-presentation-table-header",
130
188
  className,
131
189
  )}
132
190
  >
133
191
  <div
134
- style={{ width: `${width}px` }}
135
- className={cn("flex items-center justify-between flex-1", {
136
- "justify-center": isDummy,
137
- })}
192
+ className={cn(
193
+ tableHeadVariants({ size, disabled, isDummy }),
194
+ {
195
+ // Only when nothing has sized the column — otherwise this floor would overflow a
196
+ // column narrower than 100px.
197
+ "min-w-[100px]": !isDummy && resolvedWidth === undefined,
198
+ },
199
+ contentClassName,
200
+ )}
138
201
  >
139
- {props.children}
140
- {isDummy || !onSort ? null : <SortButton onSort={onSort} sortType={sortType} />}
202
+ <div
203
+ className={cn("flex min-w-0 items-center justify-between flex-1", {
204
+ "justify-center": isDummy,
205
+ })}
206
+ >
207
+ {children}
208
+ {isDummy || !onSort ? null : (
209
+ <SortButton onSort={onSort} sortType={sortType} label={sortLabel} />
210
+ )}
211
+ </div>
141
212
  </div>
142
- </div>
143
- <button
144
- disabled={isDummy}
145
- className="absolute top-[50%] translate-y-[-50%] right-[-1px] rtl:left-[-1px] rtl:right-[unset] h-[20px] w-[2px] rounded-full bg-border-presentation-action-primary"
146
- >
147
- <ResizeIcon
148
- className={cn({ "!opacity-0 cursor-default": isDummy })}
213
+ {/* The grab target, straddling the column boundary. `z-10` keeps it above the next
214
+ column's content — it deliberately overhangs by half its width. The drag handlers
215
+ live here, not on the icon: the icon is `opacity-0` until hover, which made the
216
+ 8px-wide SVG a near-impossible thing to grab. */}
217
+ <button
218
+ type="button"
219
+ disabled={isDummy}
220
+ aria-label="Resize column"
221
+ // Lets consumers exclude the grip from blanket `[&_button]` rules on a header row.
222
+ data-slot="resize-handle"
149
223
  onMouseDown={handleStartResize}
150
224
  onTouchStart={handleStartResize}
151
- />
152
- </button>
153
- </th>
154
- );
155
- });
225
+ className={cn(
226
+ "group/resize absolute top-[50%] translate-y-[-50%] z-10",
227
+ "right-[-4px] rtl:left-[-4px] rtl:right-[unset]",
228
+ // `absolute` already makes this the containing block for the grip icon.
229
+ "flex h-[24px] w-[8px] items-center justify-center cursor-col-resize",
230
+ "disabled:cursor-default",
231
+ )}
232
+ >
233
+ <span className="h-[20px] w-[2px] rounded-full bg-border-presentation-action-primary" />
234
+ <ResizeIcon
235
+ className={cn("group-hover/resize:opacity-100", { "!opacity-0": isDummy })}
236
+ />
237
+ </button>
238
+ </th>
239
+ );
240
+ },
241
+ );
156
242
  TableHead.displayName = "TableHead";
157
243
 
158
244
  const TableCell = React.forwardRef<
@@ -161,15 +247,29 @@ const TableCell = React.forwardRef<
161
247
  isDummy?: boolean;
162
248
  childrenClassName?: string;
163
249
  className?: string;
250
+ /**
251
+ * Minimum width of the cell's content box, in px. Defaults to 200 — pass `0` when the
252
+ * column width is driven by the caller (e.g. `FormBuilder.Table`'s `column.width`),
253
+ * otherwise this floor silently overrides any narrower column.
254
+ */
255
+ minWidth?: number;
256
+ /**
257
+ * Fade the last 25% of the cell's content, to signal text clipped by the column width.
258
+ * Defaults to `true`. Set `false` for cells holding a **control** rather than text — the
259
+ * fade washes out whatever sits at the right edge (a Select's chevron, a date button).
260
+ * The built-in `:has(input)` escape hatch can't catch those, since a Radix trigger is a
261
+ * `<button>`, not an `<input>`.
262
+ */
263
+ fade?: boolean;
164
264
  }
165
- >(({ className, childrenClassName, isDummy, ...props }, ref) => (
265
+ >(({ className, childrenClassName, isDummy, minWidth = 200, fade = true, ...props }, ref) => (
166
266
  <td
167
267
  ref={ref}
168
268
  className={cn(
169
269
  [
170
- "h-[40px] text-content-presentation-action-light-primary",
270
+ "h-[50px] text-content-presentation-action-light-primary",
171
271
  "typography-body-small-regular relative",
172
- "border-r border-b border-border-presentation-table-header px-1 rtl:border-l rtl:border-r-0",
272
+ "border-r border-b border-border-presentation-table-header px-[8px] rtl:border-l rtl:border-r-0",
173
273
  "break-all",
174
274
  ],
175
275
  className,
@@ -177,11 +277,21 @@ const TableCell = React.forwardRef<
177
277
  {...props}
178
278
  >
179
279
  <div
280
+ style={isDummy ? undefined : { minWidth }}
180
281
  className={cn(
181
- "flex justify-start items-center gap-1 min-w-[200px] overflow-hidden has-input:bg-blue-200",
182
- "[mask-image:linear-gradient(to_right,black_0%,black_0%,black_75%,transparent_100%)]",
183
- "rtl:[mask-image:linear-gradient(to_left,black_0%,black_0%,black_75%,transparent_100%)]",
184
- "[&:has(input)]:[mask-image:none]",
282
+ "flex justify-start items-center gap-1",
283
+ // This div hugs the control exactly — same height, and the control is `w-full` — so
284
+ // `overflow-hidden` clips every side of a drop shadow drawn on it. Text cells still
285
+ // need the crop to feed the fade gradient; control cells need the shadow to escape.
286
+ fade ? "overflow-hidden" : "overflow-visible",
287
+ // Never fade a dummy cell — its content is a centred checkbox or drag handle, not
288
+ // clippable text.
289
+ fade &&
290
+ !isDummy && [
291
+ "[mask-image:linear-gradient(to_right,black_0%,black_0%,black_75%,transparent_100%)]",
292
+ "rtl:[mask-image:linear-gradient(to_left,black_0%,black_0%,black_75%,transparent_100%)]",
293
+ "[&:has(input)]:[mask-image:none]",
294
+ ],
185
295
  { "min-w-fit justify-center": isDummy },
186
296
  childrenClassName,
187
297
  )}
@@ -207,6 +317,75 @@ const TableCheckbox = React.forwardRef<
207
317
  });
208
318
  TableCheckbox.displayName = "TableCheckbox";
209
319
 
320
+ /**
321
+ * The full-width action bar that sits **below** a table — e.g. "+ Add New".
322
+ *
323
+ * Deliberately not a `<tr>`: it renders as a sibling of the table's horizontal scroll
324
+ * container, so it stays put while the columns scroll sideways under it. Use
325
+ * `TableFooterButton` instead when the action must scroll with the grid.
326
+ */
327
+ const TableEndAction = React.forwardRef<
328
+ HTMLButtonElement,
329
+ React.ButtonHTMLAttributes<HTMLButtonElement>
330
+ >(({ className, children, ...props }, ref) => (
331
+ <button
332
+ ref={ref}
333
+ type="button"
334
+ className={cn(
335
+ "flex h-[40px] w-full items-center justify-start gap-[8px] px-[8px]",
336
+ // No border. Figma's Table-End-Action-1.0 has none — it rounds its own bottom corners
337
+ // so it can sit flush on the card edge. A rule here is invisible against the card until
338
+ // the hover fill lands behind it, at which point it reads as a hard line cutting across
339
+ // the bar.
340
+ "rounded-bl-[16px] rounded-br-[16px]",
341
+ "typography-body-medium-semibold text-content-presentation-action-light-primary",
342
+ // NB: the "acton" spelling is the token that actually exists; the correctly-spelled
343
+ // `…table-action-hover` is defined nowhere and would be a dead class.
344
+ "transition-[background-color,box-shadow] hover:bg-background-presentation-table-acton-hover",
345
+ // Hover draws a ring rather than a border: it renders inset, so it costs no layout and
346
+ // can't push the bar's 40px box around the way a border would.
347
+ "hover:ring-2 hover:ring-inset hover:ring-border-presentation-table-action-hover",
348
+ "outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-border-presentation-state-focus",
349
+ "disabled:cursor-not-allowed disabled:opacity-50",
350
+ "[&_i]:text-[20px]",
351
+ className,
352
+ )}
353
+ {...props}
354
+ >
355
+ {children}
356
+ </button>
357
+ ));
358
+ TableEndAction.displayName = "TableEndAction";
359
+
360
+ /**
361
+ * Horizontal scroll container for a table — the 14px scroller row from the design,
362
+ * with the thin track that thickens and turns blue on hover.
363
+ */
364
+ const TableScroller = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
365
+ ({ className, children, ...props }, ref) => (
366
+ <div
367
+ ref={ref}
368
+ className={cn(
369
+ "w-full overflow-x-auto overflow-y-hidden",
370
+ "[&::-webkit-scrollbar]:h-[14px]",
371
+ "[&::-webkit-scrollbar-track]:bg-transparent",
372
+ "[&::-webkit-scrollbar-thumb]:rounded-[7px]",
373
+ "[&::-webkit-scrollbar-thumb]:border-[5px] [&::-webkit-scrollbar-thumb]:border-solid",
374
+ "[&::-webkit-scrollbar-thumb]:border-transparent",
375
+ "[&::-webkit-scrollbar-thumb]:bg-clip-content",
376
+ "[&::-webkit-scrollbar-thumb]:bg-background-presentation-body-scroller-default",
377
+ "[&::-webkit-scrollbar-thumb:hover]:border-[3px]",
378
+ "[&::-webkit-scrollbar-thumb:hover]:bg-background-presentation-body-scroller-hover",
379
+ className,
380
+ )}
381
+ {...props}
382
+ >
383
+ {children}
384
+ </div>
385
+ ),
386
+ );
387
+ TableScroller.displayName = "TableScroller";
388
+
210
389
  const TableCaption = React.forwardRef<
211
390
  HTMLTableCaptionElement,
212
391
  React.HTMLAttributes<HTMLTableCaptionElement>
@@ -225,7 +404,9 @@ const TableFooterButton = React.forwardRef<
225
404
  <TableRow className={cn("h-[40px] overflow-hidden", className)}>
226
405
  <TableCell
227
406
  className={
228
- "border-t-2 border-b-2 border-transparent hover:border-border-presentation-table-action-hover hover:bg-background-presentation-table-acton-hover"
407
+ // `h-[40px]` because the row asks for 40 and body cells default to 50 — the two sit
408
+ // on different elements, so CSS would otherwise take the larger and grow the footer.
409
+ "h-[40px] border-t-2 border-b-2 border-transparent hover:border-border-presentation-table-action-hover hover:bg-background-presentation-table-acton-hover"
229
410
  }
230
411
  colSpan={100}
231
412
  >
@@ -291,37 +472,41 @@ export {
291
472
  TableCheckbox,
292
473
  SubTableButton,
293
474
  TableFooterButton,
475
+ TableEndAction,
476
+ TableScroller,
294
477
  };
295
478
 
296
479
  interface ResizeIconProps {
297
480
  className?: string;
298
- onMouseDown?: React.MouseEventHandler<SVGElement>;
299
- onTouchStart?: React.TouchEventHandler<SVGElement>;
300
481
  }
301
482
 
302
- const ResizeIcon = (props: ResizeIconProps) => {
483
+ /**
484
+ * The grip that appears on hover. Sized to its parent button (8px wide), so it can't spill
485
+ * past the grab target; the button is what deliberately straddles the column boundary.
486
+ */
487
+ const ResizeIcon = ({ className }: ResizeIconProps) => {
303
488
  return (
304
489
  <svg
305
- {...props}
306
490
  className={cn(
307
- "z-50 cursor-col-resize absolute top-[50%] right-[50%] translate-x-[50%] translate-y-[-50%] opacity-0 hover:opacity-100 transition-opacity duration-200",
308
- props.className,
491
+ "pointer-events-none absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2",
492
+ "opacity-0 transition-opacity duration-200",
493
+ className,
309
494
  )}
310
495
  width="8"
311
- height="32"
312
- viewBox="0 0 8 40"
496
+ height="30"
497
+ viewBox="0 1 8 38"
313
498
  fill="none"
314
499
  xmlns="http://www.w3.org/2000/svg"
315
500
  >
316
- <rect {...props} y="5" width="8" height="30" rx="3" fill="#3391FF" />
317
- <circle {...props} cx="2.75" cy="15.5" r="0.75" fill="#F9F9F9" />
318
- <circle {...props} cx="5.25" cy="15.5" r="0.75" fill="#F9F9F9" />
319
- <circle {...props} cx="2.75" cy="18.5" r="0.75" fill="#F9F9F9" />
320
- <circle {...props} cx="5.25" cy="18.5" r="0.75" fill="#F9F9F9" />
321
- <circle {...props} cx="2.75" cy="21.5" r="0.75" fill="#F9F9F9" />
322
- <circle {...props} cx="5.25" cy="21.5" r="0.75" fill="#F9F9F9" />
323
- <circle {...props} cx="2.75" cy="24.5" r="0.75" fill="#F9F9F9" />
324
- <circle {...props} cx="5.25" cy="24.5" r="0.75" fill="#F9F9F9" />
501
+ <rect y="5" width="8" height="30" rx="3" fill="#3391FF" />
502
+ <circle cx="2.75" cy="15.5" r="0.75" fill="#F9F9F9" />
503
+ <circle cx="5.25" cy="15.5" r="0.75" fill="#F9F9F9" />
504
+ <circle cx="2.75" cy="18.5" r="0.75" fill="#F9F9F9" />
505
+ <circle cx="5.25" cy="18.5" r="0.75" fill="#F9F9F9" />
506
+ <circle cx="2.75" cy="21.5" r="0.75" fill="#F9F9F9" />
507
+ <circle cx="5.25" cy="21.5" r="0.75" fill="#F9F9F9" />
508
+ <circle cx="2.75" cy="24.5" r="0.75" fill="#F9F9F9" />
509
+ <circle cx="5.25" cy="24.5" r="0.75" fill="#F9F9F9" />
325
510
  </svg>
326
511
  );
327
512
  };
@@ -329,12 +514,25 @@ const ResizeIcon = (props: ResizeIconProps) => {
329
514
  const SortButton = ({
330
515
  onSort,
331
516
  sortType,
517
+ label,
332
518
  }: {
333
519
  onSort?: () => void;
334
520
  sortType?: "asc" | "desc" | undefined;
521
+ /** What this button sorts — without it the control is an unnamed icon. */
522
+ label?: string;
335
523
  }) => {
524
+ const next = sortType === "asc" ? "descending" : "ascending";
336
525
  return (
337
- <button className={cn("cursor-pointer text-[16px] z-10")} onPointerDown={onSort}>
526
+ // `type="button"` matters: this table renders inside a <form> (FormBuilder.Table),
527
+ // where an untyped button defaults to submit.
528
+ <button
529
+ type="button"
530
+ // `onClick`, not `onPointerDown`: a button activated from the keyboard fires
531
+ // `click` only, so pointer-down made this mouse-only.
532
+ onClick={onSort}
533
+ aria-label={label ? `Sort by ${label} ${next}` : `Sort ${next}`}
534
+ className={cn("cursor-pointer text-[16px] z-10")}
535
+ >
338
536
  {sortType === "asc" ? (
339
537
  <i className="ri-arrow-up-line text-border-presentation-state-focus" />
340
538
  ) : sortType === "desc" ? (
@@ -379,9 +577,9 @@ const tableHeadVariants = cva(
379
577
  isDummy: {
380
578
  true: ["hover:bg-transparent", "hover:text-content-presentation-global-primary"],
381
579
  },
382
- defaultVariants: {
383
- size: "M",
384
- },
580
+ },
581
+ defaultVariants: {
582
+ size: "M",
385
583
  },
386
584
  },
387
585
  );
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "2.4.3",
2
+ "version": "2.4.5",
3
3
  "generatedBy": "scripts/bin/generateRegistry",
4
4
  "items": [
5
5
  {