@recursica/mantine-adapter 0.44.0 → 0.46.0

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 (35) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/dist/index.d.ts +106 -54
  3. package/dist/mantine-adapter.cjs +2 -2
  4. package/dist/mantine-adapter.cjs.map +1 -1
  5. package/dist/mantine-adapter.css +1 -1
  6. package/dist/mantine-adapter.js +1901 -1848
  7. package/dist/mantine-adapter.js.map +1 -1
  8. package/package.json +3 -3
  9. package/src/components/Accordion/Accordion.tsx +1 -1
  10. package/src/components/Checkbox/CheckboxGroup.tsx +1 -1
  11. package/src/components/Chip/CHIP_IMPLEMENTATION_NOTES.md +8 -8
  12. package/src/components/Chip/Chip.module.css +5 -5
  13. package/src/components/Chip/Chip.stories.tsx +2 -2
  14. package/src/components/Chip/Chip.tsx +13 -13
  15. package/src/components/Chip/USAGE.md +2 -2
  16. package/src/components/Dropdown/Dropdown.module.css +1 -1
  17. package/src/components/FileInput/FILEINPUT_IMPLEMENTATION_NOTES.md +6 -4
  18. package/src/components/FileInput/FileInput.stories.tsx +3 -0
  19. package/src/components/FileInput/FileInput.tsx +11 -10
  20. package/src/components/FileInput/USAGE.md +1 -0
  21. package/src/components/FileUpload/FILEUPLOAD_IMPLEMENTATION_NOTES.md +12 -12
  22. package/src/components/FileUpload/FileUpload.tsx +9 -9
  23. package/src/components/Link/IMPLEMENTATION_NOTES.md +1 -1
  24. package/src/components/Link/Link.module.css +4 -0
  25. package/src/components/Radio/RadioGroup.tsx +1 -1
  26. package/src/components/SegmentedControl/IMPLEMENTATION_NOTES.md +8 -0
  27. package/src/components/SegmentedControl/SegmentedControl.stories.tsx +17 -28
  28. package/src/components/SegmentedControl/SegmentedControl.tsx +29 -17
  29. package/src/components/SegmentedControl/USAGE.md +13 -0
  30. package/src/components/Switch/SwitchGroup.tsx +1 -1
  31. package/src/components/Table/TABLE_IMPLEMENTATION_NOTES.md +67 -0
  32. package/src/components/Table/Table.icons.tsx +40 -0
  33. package/src/components/Table/Table.stories.tsx +93 -0
  34. package/src/components/Table/Table.tsx +46 -8
  35. package/src/components/Table/USAGE.md +35 -0
package/package.json CHANGED
@@ -13,7 +13,7 @@
13
13
  "url": "git+https://github.com/borderux/recursica.git",
14
14
  "directory": "packages/mantine-adapter"
15
15
  },
16
- "version": "0.44.0",
16
+ "version": "0.46.0",
17
17
  "type": "module",
18
18
  "main": "./dist/mantine-adapter.cjs",
19
19
  "module": "./dist/mantine-adapter.js",
@@ -94,8 +94,8 @@
94
94
  "vitest": "^3.2.4"
95
95
  },
96
96
  "dependencies": {
97
- "@recursica/adapter-common": "*",
98
- "@recursica/official-release": "*"
97
+ "@recursica/adapter-common": "^0.21.0",
98
+ "@recursica/official-release": "^2.8.0"
99
99
  },
100
100
  "peerDependencies": {
101
101
  "@mantine/core": "^8.0.0",
@@ -25,7 +25,7 @@ import {
25
25
  export type AccordionProps = RecursicaOverStyled<
26
26
  Omit<
27
27
  MantineAccordionProps,
28
- "value" | "defaultValue" | "onChange" | "multiple" | "variant"
28
+ "value" | "defaultValue" | "multiple" | "variant"
29
29
  > &
30
30
  RecursicaAccordionProps
31
31
  >;
@@ -19,7 +19,7 @@ import { type RecursicaCheckboxGroupProps as BaseRecursicaCheckboxGroupProps } f
19
19
  export interface RecursicaCheckboxGroupProps
20
20
  extends Omit<
21
21
  MantineCheckboxGroupProps,
22
- "size" | "labelProps" | "defaultValue" | "value" | "onChange"
22
+ "size" | "labelProps" | "defaultValue" | "value"
23
23
  >,
24
24
  Omit<
25
25
  RecursicaFormControlWrapperProps,
@@ -20,7 +20,7 @@ To achieve this without breaking Mantine's `Chip` input architecture, we wrapped
20
20
  <span className={styles.innerWrapper}>
21
21
  {icon}
22
22
  <span className={styles.children}>{children}</span>
23
- {onRemove}
23
+ {onDelete}
24
24
  </span>
25
25
  ```
26
26
 
@@ -42,10 +42,10 @@ During implementation, the parsed Figma design tokens natively exported specific
42
42
  `.children` already had `text-overflow: ellipsis; overflow: hidden; white-space: nowrap;`, but
43
43
  neither it nor its parent `.innerWrapper` had `min-width: 0` — a flex child without that refuses
44
44
  to shrink below its intrinsic (full, unwrapped) content width, so `text-overflow: ellipsis` never
45
- actually engaged. A long label overflowed the flex row instead, and since `.removeIcon` had no
45
+ actually engaged. A long label overflowed the flex row instead, and since `.deleteIcon` had no
46
46
  `flex-shrink: 0`, it got squeezed out of the clipped (`max-width`-bounded) chip entirely. Fixed by
47
47
  adding `min-width: 0` to `.innerWrapper`/`.children` and `flex-shrink: 0` to `.leadingIcon`/
48
- `.removeIcon`. Reproduces easily with `FileUpload`'s file list, since its chip `max-width`
48
+ `.deleteIcon`. Reproduces easily with `FileUpload`'s file list, since its chip `max-width`
49
49
  (`--recursica_ui-kit_components_chip_properties_max-width`, 200px) is small enough that most real
50
50
  filenames overflow it.
51
51
 
@@ -63,24 +63,24 @@ y-axis has no visual side effect.
63
63
 
64
64
  ### Roving tabindex support for chip groups (Matt Massey, 2026-08-17)
65
65
 
66
- Added two optional pass-through props — `removeTabIndex` and `removeIconRef` — purely so a parent
66
+ Added two optional pass-through props — `deleteTabIndex` and `deleteIconRef` — purely so a parent
67
67
  managing a _group_ of chips (e.g. `FileUpload`'s file list, see its own `IMPLEMENTATION_NOTES.md`)
68
- can implement roving-tabindex/arrow-key navigation across them: set `removeTabIndex={-1}` on every
69
- chip but the currently-active one, and use `removeIconRef` to move real DOM focus there
68
+ can implement roving-tabindex/arrow-key navigation across them: set `deleteTabIndex={-1}` on every
69
+ chip but the currently-active one, and use `deleteIconRef` to move real DOM focus there
70
70
  imperatively on arrow-key press. Both are no-ops for a standalone `Chip` (defaults: `tabIndex={0}`,
71
71
  no ref) — this doesn't change any existing single-chip behavior.
72
72
 
73
73
  ### `isInteractive` was measuring the wrong signal, and leaked a pointer cursor + phantom Tab stop (Matt Massey, 2026-08-18)
74
74
 
75
75
  A `Chip` rendered with `checked` but no real handler (e.g. `FileUpload`'s `readOnly` file list —
76
- `<Chip checked={false} tabIndex={-1}>`, no `onRemove`) still looked and behaved clickable, from two
76
+ `<Chip checked={false} tabIndex={-1}>`, no `onDelete`) still looked and behaved clickable, from two
77
77
  separate bugs:
78
78
 
79
79
  - `isInteractive` treated `checked !== undefined` (even `false`) as proof of interactivity. That's
80
80
  the wrong signal — a `checked`-controlled chip with no `onChange` can't actually be toggled by a
81
81
  click (Mantine's `useUncontrolled` discards the click when `value` is externally controlled), so
82
82
  clicking it does nothing observable regardless. `isInteractive` now only looks at whether
83
- something actually responds: `onRemove`, `onClick`, or `onChange`.
83
+ something actually responds: `onDelete`, `onClick`, or `onChange`.
84
84
  - `.label.label` never set its own `cursor`, so Mantine's base style (`cursor: pointer`, hardcoded
85
85
  on the underlying `mantine-Chip-label` class) always leaked through, independent of whether the
86
86
  chip was actually interactive. Reset it to `cursor: default` and added a `data-interactive`
@@ -55,7 +55,7 @@
55
55
  background: transparent; /* HARDCODE: Mantine forces background styling on label */
56
56
  cursor: default; /* HARDCODE: Mantine's own base styles hardcode cursor: pointer on this class —
57
57
  override it to plain default, then only re-enable it below for a chip with a real handler
58
- (onRemove/onClick/onChange — see Chip.tsx's `isInteractive`). A display-only chip (e.g. a
58
+ (onDelete/onClick/onChange — see Chip.tsx's `isInteractive`). A display-only chip (e.g. a
59
59
  read-only FileUpload file list) has nothing for a click to do, so it shouldn't look clickable. */
60
60
  border-style: solid;
61
61
  border-width: var(--recursica_ui-kit_components_chip_properties_border-size);
@@ -224,11 +224,11 @@
224
224
  height: 100%;
225
225
  }
226
226
 
227
- .removeIcon {
227
+ .deleteIcon {
228
228
  display: inline-flex;
229
229
  align-items: center;
230
230
  justify-content: center;
231
- flex-shrink: 0; /* HARDCODE: keep the remove icon visible when the label truncates instead */
231
+ flex-shrink: 0; /* HARDCODE: keep the delete icon visible when the label truncates instead */
232
232
  color: var(--chip-close);
233
233
  width: var(--recursica_ui-kit_components_chip_properties_close-icon-size);
234
234
  height: var(--recursica_ui-kit_components_chip_properties_close-icon-size);
@@ -237,7 +237,7 @@
237
237
  outline: none;
238
238
  }
239
239
 
240
- .removeIcon:focus-visible {
240
+ .deleteIcon:focus-visible {
241
241
  box-shadow:
242
242
  0 0 0 var(--recursica_brand_states_focus_border-size)
243
243
  var(--recursica_brand_states_focus_color),
@@ -246,7 +246,7 @@
246
246
  var(--recursica_brand_states_focus_color);
247
247
  }
248
248
 
249
- .removeIcon svg {
249
+ .deleteIcon svg {
250
250
  width: 100%;
251
251
  height: 100%;
252
252
  }
@@ -12,7 +12,7 @@ const meta: Meta<ChipStoryProps> = {
12
12
  docs: {
13
13
  description: {
14
14
  component:
15
- "The Chip component is used to represent interactive selections, descriptive tags, or dynamic filters natively bounded to Recursica variables. It can be used as a toggleable input, can render a custom leading `icon`, and handles close constraints automatically via the `onRemove` property.",
15
+ "The Chip component is used to represent interactive selections, descriptive tags, or dynamic filters natively bounded to Recursica variables. It can be used as a toggleable input, can render a custom leading `icon`, and handles close constraints automatically via the `onDelete` property.",
16
16
  },
17
17
  },
18
18
  },
@@ -86,7 +86,7 @@ export const Removable: Story = {
86
86
  args: {
87
87
  children: "Dismissible",
88
88
  checked: false,
89
- onRemove: () => console.log("Removal Action Triggered"),
89
+ onDelete: () => console.log("Removal Action Triggered"),
90
90
  },
91
91
  render: (args: ChipStoryProps) => <Chip {...args} />,
92
92
  };
@@ -41,10 +41,10 @@ export const Chip = forwardRef<HTMLInputElement, ChipProps>(function Chip(
41
41
  {
42
42
  error = false,
43
43
  icon,
44
- onRemove,
45
- removeLabel = "Remove",
46
- removeTabIndex,
47
- removeIconRef,
44
+ onDelete,
45
+ deleteLabel = "Delete",
46
+ deleteTabIndex,
47
+ deleteIconRef,
48
48
  children,
49
49
  overStyled = false,
50
50
  wrapperProps,
@@ -75,13 +75,13 @@ export const Chip = forwardRef<HTMLInputElement, ChipProps>(function Chip(
75
75
 
76
76
  // Determine state
77
77
  const dataError = error ? "" : undefined;
78
- const isIconOnly = !children && (!!icon || !!onRemove);
78
+ const isIconOnly = !children && (!!icon || !!onDelete);
79
79
  // A chip only counts as interactive when something actually responds to it — merely passing a
80
80
  // `checked` value (e.g. to pin a display-only chip to a fixed visual state, as FileUpload's
81
81
  // read-only file list does) isn't itself an interaction, since clicking it with no onChange/
82
82
  // onClick wired does nothing observable.
83
83
  const isInteractive =
84
- onRemove !== undefined ||
84
+ onDelete !== undefined ||
85
85
  restRecord.onClick !== undefined ||
86
86
  restRecord.onChange !== undefined;
87
87
 
@@ -119,27 +119,27 @@ export const Chip = forwardRef<HTMLInputElement, ChipProps>(function Chip(
119
119
 
120
120
  <span className={styles.children}>{children}</span>
121
121
 
122
- {onRemove && (
122
+ {onDelete && (
123
123
  <span
124
- ref={removeIconRef}
124
+ ref={deleteIconRef}
125
125
  role="button"
126
- className={styles.removeIcon}
126
+ className={styles.deleteIcon}
127
127
  onClick={(e) => {
128
128
  e.preventDefault();
129
129
  e.stopPropagation();
130
- onRemove(e);
130
+ onDelete(e);
131
131
  }}
132
- aria-label={removeLabel}
132
+ aria-label={deleteLabel}
133
133
  onKeyDown={(e) => {
134
134
  if (e.key === "Enter" || e.key === " ") {
135
135
  e.preventDefault();
136
136
  e.stopPropagation();
137
- onRemove(
137
+ onDelete(
138
138
  e as unknown as React.MouseEvent<HTMLSpanElement, MouseEvent>,
139
139
  );
140
140
  }
141
141
  }}
142
- tabIndex={removeTabIndex ?? 0}
142
+ tabIndex={deleteTabIndex ?? 0}
143
143
  >
144
144
  <CloseIcon />
145
145
  </span>
@@ -49,7 +49,7 @@ Chip does not support a `size` prop; chips render at a fixed size.
49
49
 
50
50
  ### Building a keyboard-navigable chip group
51
51
 
52
- `removeTabIndex` and `removeIconRef` are optional escape hatches for composing a _group_ of chips
52
+ `deleteTabIndex` and `deleteIconRef` are optional escape hatches for composing a _group_ of chips
53
53
  with roving-tabindex keyboard navigation (Tab reaches one chip at a time, arrow keys move between
54
54
  them) — see `FileUpload`'s file list for a working example. Ignore both for a standalone chip; they
55
- default to a normal `tabIndex={0}` remove icon with no ref.
55
+ default to a normal `tabIndex={0}` delete icon with no ref.
@@ -174,7 +174,7 @@
174
174
  hover background, neither of which is Recursica-token-driven. The double class selector (same
175
175
  specificity-boosting trick as Chip.module.css's `.root.root`) beats that default without
176
176
  needing `!important`. Same icon-size/trailing-icon tokens as the rest of this section, same
177
- focus-visible ring technique as Chip's own `.removeIcon` — the established inline icon-button
177
+ focus-visible ring technique as Chip's own `.deleteIcon` — the established inline icon-button
178
178
  pattern elsewhere in this adapter. */
179
179
  .clearButton.clearButton {
180
180
  width: var(--recursica_ui-kit_components_dropdown_properties_icon-size);
@@ -20,7 +20,7 @@ The control is one clickable, focusable `<div role="button">` containing:
20
20
  single- and multiple-file mode render identically here; only the selection/replace behavior
21
21
  differs (see "`multiple` defaults to `false`" below).
22
22
  3. **A trailing "clear" `Button`** — shown whenever a file is selected; clears the entire current
23
- selection (see "Trailing clear button" below).
23
+ selection, icon overridable via `clearIcon` (see "Trailing clear button" below).
24
24
  4. **A hidden `<input type="file">`**, triggered programmatically — same approach as
25
25
  `FileUpload`'s dropzone, not the "invisible overlay input" pattern some styled file inputs use
26
26
  (that pattern would need every interactive child — chip remove icons, the clear icon — to sit
@@ -107,7 +107,9 @@ color/disabled states, so `file-input`'s own `properties_colors_trailing-icon` t
107
107
  entirely. Clicking/activating it calls `onFileRemove` once per currently-selected file (reusing
108
108
  the same callback `FileUpload`'s individual chip removal uses, rather than introducing a separate
109
109
  `onClear` prop) — in single-file mode that's one call; in multiple-file mode it clears everything
110
- at once, distinct from a chip's own individual remove icon.
110
+ at once, distinct from a chip's own individual remove icon. The icon itself defaults to the
111
+ built-in X glyph and is overridable via `clearIcon` — the same pattern as the leading `icon`,
112
+ giving Forge's `leading-icon`/`trailing-icon` token pair a matching pair of override props.
111
113
 
112
114
  ## Why each chip is wrapped with its own `stopPropagation`
113
115
 
@@ -121,7 +123,7 @@ it needed no changes for this.
121
123
 
122
124
  ## Keyboard navigation for the chip row
123
125
 
124
- Same roving-tabindex model as `FileUpload`'s file list (`activeChipIndex`, `removeIconRefs`,
126
+ Same roving-tabindex model as `FileUpload`'s file list (`activeChipIndex`, `deleteIconRefs`,
125
127
  Left/Right/Up/Down roving, focus-survives-removal `useEffect`) — reused rather than reinvented,
126
128
  and applies in single-file mode too (a one-chip roving group is a no-op but needs no special
127
129
  casing). The only addition specific to `FileInput` is that `Tab` reaches the root control itself
@@ -140,7 +142,7 @@ already made for `.value`'s ellipsis truncation.
140
142
 
141
143
  `readOnly` renders the same content (placeholder/filename/chip row) but the root loses its
142
144
  `role="button"` interactivity (no `tabIndex`, no click/keyboard/drag handlers), chips render with
143
- no `removeLabel`/`onRemove` (so `Chip` itself renders no remove icon, same as `FileUpload`'s
145
+ no `deleteLabel`/`onDelete` (so `Chip` itself renders no delete icon, same as `FileUpload`'s
144
146
  read-only chips), and the trailing clear icon is omitted entirely. `disabled` keeps the control
145
147
  structurally the same but inert (`tabIndex={-1}`, `aria-disabled`, no handlers, native `<input>`
146
148
  disabled) — both are computed together as a single `interactive` flag used throughout, but remain
@@ -63,6 +63,9 @@ The \`FileInput\` component is a single-line, \`TextField\`-shaped control for c
63
63
  icon: {
64
64
  table: { disable: true },
65
65
  },
66
+ clearIcon: {
67
+ table: { disable: true },
68
+ },
66
69
  files: {
67
70
  table: { disable: true },
68
71
  },
@@ -125,6 +125,7 @@ export const FileInput = forwardRef<HTMLDivElement, FileInputProps>(
125
125
  ? `Maximum of ${maxFiles} files allowed`
126
126
  : "Only one file is allowed",
127
127
  icon,
128
+ clearIcon,
128
129
  placeholder = "Select a file...",
129
130
  browseLabel = "Choose file",
130
131
  removeFileLabel = "Remove",
@@ -235,10 +236,10 @@ export const FileInput = forwardRef<HTMLDivElement, FileInputProps>(
235
236
  };
236
237
 
237
238
  // Roving tabindex across the file chip list (single- or multiple-file mode): only the
238
- // "active" chip's remove icon is a tab stop, and Left/Right/Up/Down move it — same pattern
239
+ // "active" chip's delete icon is a tab stop, and Left/Right/Up/Down move it — same pattern
239
240
  // as FileUpload, see FILEINPUT_IMPLEMENTATION_NOTES.md.
240
241
  const [activeChipIndex, setActiveChipIndex] = useState(0);
241
- const removeIconRefs = useRef<Array<HTMLSpanElement | null>>([]);
242
+ const deleteIconRefs = useRef<Array<HTMLSpanElement | null>>([]);
242
243
  const prevFileCountRef = useRef(files?.length ?? 0);
243
244
 
244
245
  useEffect(() => {
@@ -246,7 +247,7 @@ export const FileInput = forwardRef<HTMLDivElement, FileInputProps>(
246
247
  if (count > 0 && count < prevFileCountRef.current) {
247
248
  const nextIndex = Math.min(activeChipIndex, count - 1);
248
249
  setActiveChipIndex(nextIndex);
249
- removeIconRefs.current[nextIndex]?.focus();
250
+ deleteIconRefs.current[nextIndex]?.focus();
250
251
  }
251
252
  prevFileCountRef.current = count;
252
253
  // Only react to the file list itself shrinking/growing, not to activeChipIndex changes.
@@ -268,7 +269,7 @@ export const FileInput = forwardRef<HTMLDivElement, FileInputProps>(
268
269
  event.preventDefault();
269
270
  event.stopPropagation();
270
271
  setActiveChipIndex(nextIndex);
271
- removeIconRefs.current[nextIndex]?.focus();
272
+ deleteIconRefs.current[nextIndex]?.focus();
272
273
  };
273
274
 
274
275
  // The built-in `accept`/cap-mismatch message is only shown when the integrator hasn't
@@ -353,14 +354,14 @@ export const FileInput = forwardRef<HTMLDivElement, FileInputProps>(
353
354
  <Chip
354
355
  checked={false}
355
356
  tabIndex={-1}
356
- removeLabel={readOnly ? undefined : removeFileLabel}
357
- removeTabIndex={
357
+ deleteLabel={readOnly ? undefined : removeFileLabel}
358
+ deleteTabIndex={
358
359
  !readOnly && index === activeChipIndex ? 0 : -1
359
360
  }
360
- removeIconRef={(el) => {
361
- removeIconRefs.current[index] = el;
361
+ deleteIconRef={(el) => {
362
+ deleteIconRefs.current[index] = el;
362
363
  }}
363
- onRemove={
364
+ onDelete={
364
365
  readOnly || disabled
365
366
  ? undefined
366
367
  : () => onFileRemove?.(itemId)
@@ -380,7 +381,7 @@ export const FileInput = forwardRef<HTMLDivElement, FileInputProps>(
380
381
  overStyled
381
382
  variant="text"
382
383
  size="small"
383
- icon={<ClearIcon />}
384
+ icon={clearIcon ?? <ClearIcon />}
384
385
  aria-label={clearLabel}
385
386
  className={styles.trailingIcon}
386
387
  disabled={disabled}
@@ -57,6 +57,7 @@ rather than appending — picking a new file in single-file mode always replaces
57
57
  | `invalidFileTypeMessage` | `React.ReactNode` | Error message shown when a file is rejected for not matching `accept`. Defaults to `"File type not accepted"`. An explicit `error` prop always takes priority over this. |
58
58
  | `maxFilesMessage` | `React.ReactNode` | Error message shown when a file is rejected for exceeding the cap. Defaults to `"Maximum of {maxFiles} files allowed"` when `multiple`, or `"Only one file is allowed"` otherwise. An explicit `error` prop always wins. |
59
59
  | `icon` | `React.ReactNode` | Leading icon shown inside the control. Defaults to the built-in upload icon. |
60
+ | `clearIcon` | `React.ReactNode` | Trailing clear-all icon shown once a file is selected. Defaults to the built-in X icon. |
60
61
  | `placeholder` | `React.ReactNode` | Text shown when no file is selected. Defaults to `"Select a file..."`. |
61
62
  | `browseLabel` | `string` | Screen-reader label for the control itself (it's the sole interactive/focusable surface, there being no separate "Browse" button). Defaults to `"Choose file"`. |
62
63
  | `removeFileLabel` | `string` | Screen-reader label for a file chip's remove button. Defaults to `"Remove"`. |
@@ -11,7 +11,7 @@ deliberately did not add — see "Why not `@mantine/dropzone`" below). The compo
11
11
  (`onDragOver`/`onDrop`), containing an upload icon, instructional text, and a hidden
12
12
  `<input type="file">` triggered by...
13
13
  2. **The browse button** — this adapter's own `<Button variant="outline" size="small">`.
14
- 3. **The file list** — this adapter's own `<Chip>` component (with `onRemove`), one per entry in
14
+ 3. **The file list** — this adapter's own `<Chip>` component (with `onDelete`), one per entry in
15
15
  the controlled `files` prop.
16
16
 
17
17
  This shape is **not configurable** — there is no prop to render a bare native file input without
@@ -34,7 +34,7 @@ Massey, 2026-08-11.)
34
34
  (spacing between/around file entries) but **no color/border tokens of its own for the file
35
35
  entries themselves** — the visual design intentionally delegates that to the existing `Chip`
36
36
  component's own token namespace (`--recursica_ui-kit_components_chip_...`). Reusing `<Chip
37
- onRemove={...}>` directly (rather than reimplementing a similar-looking element) keeps that
37
+ onDelete={...}>` directly (rather than reimplementing a similar-looking element) keeps that
38
38
  separation intact per the canonical guide's "Component Specificity" rule — `FileUpload.module.css`
39
39
  never reaches into `chip`'s namespace, and `Chip.module.css` never reaches into `file-upload`'s.
40
40
 
@@ -95,9 +95,9 @@ list's read-only form is the same chip list, just without the ability to remove
95
95
  `readOnly` is handled directly in `FileUpload.tsx` rather than reusing `WithReadOnlyWrapper`:
96
96
 
97
97
  - The dropzone (icon, instructional text, Browse button, hidden `<input>`) is omitted entirely.
98
- - Each `Chip` is rendered with no `onRemove` (and no `removeTabIndex`/`removeIconRef`/roving
98
+ - Each `Chip` is rendered with no `onDelete` (and no `deleteTabIndex`/`deleteIconRef`/roving
99
99
  keyboard handlers, which only exist to manage the remove icon) — `Chip` itself already renders no
100
- remove icon at all when `onRemove` is `undefined`, so this falls out for free rather than needing
100
+ remove icon at all when `onDelete` is `undefined`, so this falls out for free rather than needing
101
101
  a separate `readOnly` prop on `Chip`.
102
102
  - `disabled` is independent of `readOnly` and has no effect when `readOnly` is set (there's no
103
103
  dropzone/remove icon left for it to disable).
@@ -171,12 +171,12 @@ adapters).
171
171
  The file list previously had no group-level keyboard model at all — each chip's remove icon was
172
172
  simply the next `tabIndex={0}` element in natural DOM order (and, in the Mantine adapter, so was
173
173
  each chip's own hidden checkbox `<input>`, since `Chip`'s `isInteractive` check treats any chip
174
- with `onRemove` as tabbable-by-default — a second, redundant tab stop specific to this adapter).
174
+ with `onDelete` as tabbable-by-default — a second, redundant tab stop specific to this adapter).
175
175
  Implemented a standard roving-tabindex pattern instead, matching `Tree`'s existing keyboard model
176
176
  (see `../Tree/IMPLEMENTATION_NOTES.md`) rather than inventing a new one:
177
177
 
178
178
  - **Tab reaches exactly one stop per chip list, landing on the first chip.** `FileUpload` tracks
179
- `activeChipIndex` (initially `0`) and passes `removeTabIndex={index === activeChipIndex ? 0 : -1}`
179
+ `activeChipIndex` (initially `0`) and passes `deleteTabIndex={index === activeChipIndex ? 0 : -1}`
180
180
  to each `Chip` — a new pass-through prop added to `RecursicaChipProps`/both adapters' `Chip.tsx`
181
181
  (see `../Chip/CHIP_IMPLEMENTATION_NOTES.md`) that overrides the remove icon's own tabIndex. Every
182
182
  chip in the Mantine adapter is also given a plain `tabIndex={-1}` directly (flows through to
@@ -184,13 +184,13 @@ Implemented a standard roving-tabindex pattern instead, matching `Tree`'s existi
184
184
  kill that second, unwanted tab stop — the chip's label/checkbox was never meant to be
185
185
  interactive here (`checked={false}`, no `onChange`), only its remove icon is.
186
186
  - **Enter removes the focused chip, with focus already on its remove icon.** No new code needed —
187
- `Chip`'s remove icon already calls `onRemove` on `Enter`/`Space`, and it's already the focused
187
+ `Chip`'s remove icon already calls `onDelete` on `Enter`/`Space`, and it's already the focused
188
188
  element by construction (see above), so "focus ring on the remove icon" falls out for free from
189
- the existing `.removeIcon:focus-visible` style.
189
+ the existing `.deleteIcon:focus-visible` style.
190
190
  - **Left/Right or Up/Down move focus between chips.** A `onKeyDown` handler on the file list `<div>`
191
191
  (event delegation — it fires for keydowns on any focused chip inside it) computes the next index
192
- (wrapping at both ends) and moves real DOM focus there via `removeIconRefs`, an array of refs
193
- populated through the new `removeIconRef` prop on `Chip` (same PR as `removeTabIndex`).
192
+ (wrapping at both ends) and moves real DOM focus there via `deleteIconRefs`, an array of refs
193
+ populated through the new `deleteIconRef` prop on `Chip` (same PR as `deleteTabIndex`).
194
194
  - **Focus survives removal.** Since `files` is a controlled prop `FileUpload` doesn't mutate
195
195
  itself, removing a chip doesn't shrink `files` until the consumer's own state update flows back
196
196
  down as a new prop — a `useEffect` keyed on `files` detects the length decreasing, clamps
@@ -211,7 +211,7 @@ triggers both.
211
211
  ## Read-only chips were still interactive (Matt Massey, 2026-08-18)
212
212
 
213
213
  The `readOnly` file list (added 2026-08-18, see USAGE.md §7) rendered each filename as a
214
- `<Chip checked={false} tabIndex={-1}>` with no `onRemove`, expecting that to be fully inert. Two
214
+ `<Chip checked={false} tabIndex={-1}>` with no `onDelete`, expecting that to be fully inert. Two
215
215
  separate, previously-latent bugs made it look and behave otherwise — both fixed in
216
216
  `Chip`/`Chip.module.css`, not `FileUpload` itself, so every consumer of a non-interactive `Chip`
217
217
  benefits, not just this one:
@@ -221,7 +221,7 @@ benefits, not just this one:
221
221
  the tabbable/exposed branch — but a `checked`-controlled chip with no `onChange` can't actually be
222
222
  toggled by a click (Mantine's `useUncontrolled` just ignores the click when `value` is externally
223
223
  controlled), so that heuristic was measuring the wrong thing. `isInteractive` now only looks at
224
- whether something actually responds to interaction: `onRemove`, `onClick`, or `onChange`.
224
+ whether something actually responds to interaction: `onDelete`, `onClick`, or `onChange`.
225
225
  - **A read-only chip's cursor still showed `pointer` on hover.** Mantine's own base styles hardcode
226
226
  `cursor: pointer` on the chip label class; our CSS never overrode it, so it always leaked through
227
227
  regardless of the chip's actual behavior. `.label.label` now resets it to `cursor: default` and a
@@ -194,11 +194,11 @@ export const FileUpload = forwardRef<HTMLDivElement, FileUploadProps>(
194
194
  event.target.value = "";
195
195
  };
196
196
 
197
- // Roving tabindex across the file chip list: only the "active" chip's remove icon is a tab
197
+ // Roving tabindex across the file chip list: only the "active" chip's delete icon is a tab
198
198
  // stop (Tab lands on the first chip), and Left/Right/Up/Down move it — see
199
199
  // FILEUPLOAD_IMPLEMENTATION_NOTES.md.
200
200
  const [activeChipIndex, setActiveChipIndex] = useState(0);
201
- const removeIconRefs = useRef<Array<HTMLSpanElement | null>>([]);
201
+ const deleteIconRefs = useRef<Array<HTMLSpanElement | null>>([]);
202
202
  const prevFileCountRef = useRef(files?.length ?? 0);
203
203
 
204
204
  useEffect(() => {
@@ -208,7 +208,7 @@ export const FileUpload = forwardRef<HTMLDivElement, FileUploadProps>(
208
208
  if (count > 0 && count < prevFileCountRef.current) {
209
209
  const nextIndex = Math.min(activeChipIndex, count - 1);
210
210
  setActiveChipIndex(nextIndex);
211
- removeIconRefs.current[nextIndex]?.focus();
211
+ deleteIconRefs.current[nextIndex]?.focus();
212
212
  }
213
213
  prevFileCountRef.current = count;
214
214
  // Only react to the file list itself shrinking/growing, not to activeChipIndex changes.
@@ -229,7 +229,7 @@ export const FileUpload = forwardRef<HTMLDivElement, FileUploadProps>(
229
229
  if (nextIndex === undefined) return;
230
230
  event.preventDefault();
231
231
  setActiveChipIndex(nextIndex);
232
- removeIconRefs.current[nextIndex]?.focus();
232
+ deleteIconRefs.current[nextIndex]?.focus();
233
233
  };
234
234
 
235
235
  // The built-in `accept`-mismatch message is only shown when the integrator hasn't supplied
@@ -330,12 +330,12 @@ export const FileUpload = forwardRef<HTMLDivElement, FileUploadProps>(
330
330
  key={itemId}
331
331
  checked={false}
332
332
  tabIndex={-1}
333
- removeLabel={removeFileLabel}
334
- removeTabIndex={index === activeChipIndex ? 0 : -1}
335
- removeIconRef={(el) => {
336
- removeIconRefs.current[index] = el;
333
+ deleteLabel={removeFileLabel}
334
+ deleteTabIndex={index === activeChipIndex ? 0 : -1}
335
+ deleteIconRef={(el) => {
336
+ deleteIconRefs.current[index] = el;
337
337
  }}
338
- onRemove={
338
+ onDelete={
339
339
  disabled ? undefined : () => onFileRemove?.(itemId)
340
340
  }
341
341
  >
@@ -2,7 +2,7 @@
2
2
 
3
3
  ## States in Design Tokens
4
4
 
5
- The design tokens (`recursica_variables_scoped.css`) provide the base link styling directly on `--recursica_ui-kit_components_link_properties_*` (no distinct `default` state), plus a `visited` variant that overrides only `colors_text-color`/`colors_icon-color`. There is no per-state token for `hover` anymore (a prior schema version had one); the component currently applies no distinct hover treatment beyond the browser's native `cursor: pointer`. There are no tokens for `active` or `focus` either; the component relies on the browser's default focus outline for accessibility unless overridden by a global reset.
5
+ The design tokens (`recursica_variables_scoped.css`) provide the base link styling directly on `--recursica_ui-kit_components_link_properties_*` (no distinct `default` state), plus a `visited` variant that overrides only `colors_text-color`/`colors_icon-color`. Hover is not a per-component `link_variants_states_hover_*` token; it's driven by the brand-level `--recursica_brand_states_link_decoration` token (resolves to `underline`), applied on `.root:hover`. Earlier this token was exported as an invalid quoted string (`"underline"`), which silently failed and was mistaken for the token not existing at all now fixed upstream to a bare keyword. There are no tokens for `active` or `focus` either; the component relies on the browser's default focus outline for accessibility unless overridden by a global reset.
6
6
 
7
7
  ## Overriding Mantine's underline Prop
8
8
 
@@ -51,6 +51,10 @@ HARDCODED VALUES:
51
51
  );
52
52
  }
53
53
 
54
+ .root:hover {
55
+ text-decoration: var(--recursica_brand_states_link_decoration);
56
+ }
57
+
54
58
  /* Icon Support */
55
59
  .iconWrapper {
56
60
  /* HARDCODE: maintain intrinsic icon bounds */
@@ -19,7 +19,7 @@ import { type RecursicaRadioGroupProps as BaseRecursicaRadioGroupProps } from "@
19
19
  export interface RecursicaRadioGroupProps
20
20
  extends Omit<
21
21
  MantineRadioGroupProps,
22
- "size" | "labelProps" | "defaultValue" | "value" | "onChange"
22
+ "size" | "labelProps" | "defaultValue" | "value"
23
23
  >,
24
24
  Omit<
25
25
  RecursicaFormControlWrapperProps,
@@ -15,3 +15,11 @@ Mantine uses an `::before` pseudo-element on the `.control` block to draw standa
15
15
  ## 4. Indicator Mapping
16
16
 
17
17
  The moving active background element (`.indicator`) is decoupled from the actual text label. It is styled natively with its own background color, border size, and elevation shadow variables to match the exact visual parity of a "floating active chip" as defined in the Recursica properties map.
18
+
19
+ ## 5. Per-item `icon`
20
+
21
+ `RecursicaSegmentedControlProps.data` objects accept an optional `icon`. Mantine's own `data` item has no such slot, so this component `Omit`s Mantine's native `data` type and composes `icon`+`label` into a single `label` ReactNode before handing it to Mantine — Mantine's own `innerLabel` wrapper then lays it out using the icon-size/gap tokens already present in `SegmentedControl.module.css`.
22
+
23
+ ## 6. Whole-control `disabled`
24
+
25
+ `RecursicaSegmentedControlProps.disabled` was previously typed `never` and stripped at runtime. It's now a real `boolean?`, passed straight through to Mantine's own native `disabled` — Mantine's `SegmentedControl` already disables every item when set. Per-item disabling via `data[].disabled` continues to work independently of it.
@@ -21,7 +21,9 @@ const meta: Meta<typeof SegmentedControl> = {
21
21
  fullWidth: {
22
22
  control: "boolean",
23
23
  },
24
- disabled: { table: { disable: true } },
24
+ disabled: {
25
+ control: "boolean",
26
+ },
25
27
  data: { table: { disable: true } },
26
28
  defaultChecked: { table: { disable: true } },
27
29
  },
@@ -64,6 +66,17 @@ export const Vertical: Story = {
64
66
  },
65
67
  };
66
68
 
69
+ export const Disabled: Story = {
70
+ args: {
71
+ data: ["Preview", "Code", "Edit"],
72
+ disabled: true,
73
+ },
74
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unused-vars
75
+ render: ({ withLayer, layer, ...args }: any) => {
76
+ return <SegmentedControl {...args} />;
77
+ },
78
+ };
79
+
67
80
  const CheckIcon = () => (
68
81
  <svg
69
82
  viewBox="0 0 24 24"
@@ -81,33 +94,9 @@ const CheckIcon = () => (
81
94
  export const WithIcons: Story = {
82
95
  args: {
83
96
  data: [
84
- {
85
- value: "daily",
86
- label: (
87
- <>
88
- <CheckIcon />
89
- <span>Daily</span>
90
- </>
91
- ),
92
- },
93
- {
94
- value: "weekly",
95
- label: (
96
- <>
97
- <CheckIcon />
98
- <span>Weekly</span>
99
- </>
100
- ),
101
- },
102
- {
103
- value: "monthly",
104
- label: (
105
- <>
106
- <CheckIcon />
107
- <span>Monthly</span>
108
- </>
109
- ),
110
- },
97
+ { value: "daily", label: "Daily", icon: <CheckIcon /> },
98
+ { value: "weekly", label: "Weekly", icon: <CheckIcon /> },
99
+ { value: "monthly", label: "Monthly", icon: <CheckIcon /> },
111
100
  ],
112
101
  },
113
102
  // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unused-vars