@recursica/mui-adapter 0.32.0 → 0.33.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 (39) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/dist/index.d.ts +42 -7
  3. package/dist/mui-adapter.cjs +66 -66
  4. package/dist/mui-adapter.cjs.map +1 -1
  5. package/dist/mui-adapter.css +1 -1
  6. package/dist/mui-adapter.js +6825 -6751
  7. package/dist/mui-adapter.js.map +1 -1
  8. package/package.json +2 -2
  9. package/src/components/Accordion/Accordion.module.css +7 -0
  10. package/src/components/Autocomplete/AUTOCOMPLETE_IMPLEMENTATION_NOTES.md +17 -0
  11. package/src/components/Autocomplete/Autocomplete.module.css +131 -12
  12. package/src/components/Autocomplete/Autocomplete.stories.tsx +149 -0
  13. package/src/components/Autocomplete/Autocomplete.tsx +47 -2
  14. package/src/components/Autocomplete/USAGE.md +26 -0
  15. package/src/components/Button/Button.module.css +10 -0
  16. package/src/components/Card/Card.stories.tsx +2 -4
  17. package/src/components/Chip/Chip.module.css +9 -0
  18. package/src/components/DatePicker/DatePicker.module.css +13 -0
  19. package/src/components/Dropdown/BareDropdown.tsx +34 -7
  20. package/src/components/Dropdown/DROPDOWN_IMPLEMENTATION_NOTES.md +15 -0
  21. package/src/components/Dropdown/Dropdown.module.css +123 -4
  22. package/src/components/Dropdown/Dropdown.stories.tsx +149 -0
  23. package/src/components/Dropdown/Dropdown.tsx +37 -4
  24. package/src/components/Dropdown/USAGE.md +20 -0
  25. package/src/components/FileInput/FILEINPUT_IMPLEMENTATION_NOTES.md +4 -1
  26. package/src/components/FileInput/FileInput.module.css +17 -0
  27. package/src/components/FileUpload/FileUpload.module.css +6 -0
  28. package/src/components/Link/Link.module.css +5 -0
  29. package/src/components/Menu/Menu.module.css +6 -0
  30. package/src/components/NumberInput/NumberInput.module.css +8 -0
  31. package/src/components/Pagination/Pagination.module.css +7 -0
  32. package/src/components/Slider/Slider.module.css +8 -0
  33. package/src/components/Switch/Switch.module.css +8 -0
  34. package/src/components/Tabs/Tabs.module.css +9 -0
  35. package/src/components/TextArea/TextArea.module.css +8 -0
  36. package/src/components/TextField/TextField.module.css +8 -0
  37. package/src/components/TimePicker/TimePicker.module.css +8 -0
  38. package/src/components/Tree/Tree.module.css +11 -0
  39. package/src/utils/renderRichOption.tsx +51 -0
@@ -0,0 +1,15 @@
1
+ # Dropdown Implementation Notes
2
+
3
+ ## Rich Option Content (`leadingIcon`/`supportingText`)
4
+
5
+ `data` items accept optional `leadingIcon`/`supportingText` fields, via the shared `RecursicaComboboxItem` type in `@recursica/adapter-common` (see `MANTINE_ADAPTER_RICH_OPTION_DATA.md` at the repo root — proposed against the mantine-adapter, but Forge needs both adapters to accept the same `data` shape, hence the type living in adapter-common rather than being redeclared per adapter). `Dropdown.tsx` and `BareDropdown.tsx` both build `MenuItem` children directly from `data` (there's no Mantine-style `renderOption` indirection here), so rendering the new fields is a straight change to that existing per-item mapping — `renderRichOptionContent` (`../../utils/renderRichOption.tsx`, shared with `Autocomplete`) renders `leadingIcon`+`label`+`supportingText` when either new field is present, or just `label` otherwise. The icon is only rendered as a child when `leadingIcon` is set (not a hidden reserved slot), so label/supportingText shift left when there's no icon; the row's `align-items: center` keeps the label vertically centered when there's no `supportingText` to stack under it.
6
+
7
+ `label` on the shared type is optional (falls back to `value`) — both components run `data` through adapter-common's `normalizeComboboxData` first (`const normalizedData = normalizeComboboxData(data)`), so every downstream read of `item.label` is a real string, with no per-call-site `?? value` fallback needed.
8
+
9
+ MUI's closed-field display has no separate slot analogous to Mantine's `option.label` — without a `renderValue`, MUI shows whichever `MenuItem`'s children matched the selected value, which would leak the rich icon/supporting-text row into the closed field. Both components now pass a `renderValue` that looks the selected item back up by value and returns its plain `label`, keeping the closed field a single line of text regardless of what the open dropdown renders.
10
+
11
+ New CSS classes (`.optionContent`/`.optionIcon`/`.optionText`/`.optionSupportingText`) in `Dropdown.module.css` reuse the menu-item component's icon/supporting-text tokens, matching the mantine-adapter's equivalent addition — no dedicated dropdown-option tokens exist for either.
12
+
13
+ ## `wrapItemText`
14
+
15
+ `label`/`supportingText` default to single-line truncation with an ellipsis (`.optionText > *` — `overflow: hidden; text-overflow: ellipsis; white-space: nowrap`). Passing `wrapItemText` adds `.optionTextWrap` alongside `.optionText`, re-enabling wrapping (`white-space: normal; overflow-wrap: anywhere`) for both children — later-cascade-wins, equal specificity. `renderRichOptionContent` takes `wrapItemText` as a third parameter and combines the two class names when it's true. `Dropdown.tsx` exposes this as a public prop; `BareDropdown.tsx` (internal-only, not part of the public `Dropdown`/`AutoComplete` API this was requested for) doesn't take the prop and always truncates.
@@ -1,3 +1,13 @@
1
+ /* Brand-layer exemptions (recursica-allow-brand) — see recursica-token-analyzer README.md.
2
+ * Global hover/focus/disabled state tokens (recursica_variables_scoped.css header, 'Hover & Focus states' / 'Disabled state' — implicit rule for every interactive element; components must not define their own per-component treatment).
3
+ * recursica-allow-brand: --recursica_brand_states_focus_blur
4
+ * recursica-allow-brand: --recursica_brand_states_focus_border-size
5
+ * recursica-allow-brand: --recursica_brand_states_focus_color
6
+ * recursica-allow-brand: --recursica_brand_states_focus_margin
7
+ * recursica-allow-brand: --recursica_brand_states_hover_color
8
+ * recursica-allow-brand: --recursica_brand_states_hover_opacity
9
+ */
10
+
1
11
  /* LAYOUT SPACING OVERRIDES:
2
12
  - Sets the --form-control-margin-bottom spacing hook to map component-specific layout tokens.
3
13
  - Also sets the --dropdown-control-{max,min}-width hooks consumed inline in Dropdown.tsx, since
@@ -356,12 +366,25 @@
356
366
  color: var(
357
367
  --recursica_ui-kit_components_dropdown_properties_colors_text-color
358
368
  );
359
- padding: calc(
360
- var(--recursica_ui-kit_components_dropdown_properties_vertical-padding) *
361
- 0.75
369
+ /* Padding/inter-item gap reuse the menu-item component's tokens (same reasoning as the icon/
370
+ supporting-text tokens below) instead of an arbitrary scaled dropdown padding — matches the
371
+ mantine-adapter's equivalent Menu.module.css `.item` padding + gap exactly. `!important` on
372
+ padding: MUI's `MenuItem` root sets its own paddingTop/Bottom (MenuItem.js) plus paddingLeft/
373
+ Right 16px via its `gutters` variant, both at equal selector specificity to this rule, so
374
+ without it the winner depends on emotion's injection order rather than this stylesheet. */
375
+ padding: var(
376
+ --recursica_ui-kit_components_menu-item_properties_vertical-padding
362
377
  )
363
- var(--recursica_ui-kit_components_dropdown_properties_horizontal-padding);
378
+ var(--recursica_ui-kit_components_menu-item_properties_horizontal-padding) !important;
379
+ margin-bottom: var(--recursica_ui-kit_components_menu_properties_item-gap);
364
380
  cursor: pointer;
381
+ /* MUI's `MenuItem` also forces `white-space: nowrap` by default; override it so long
382
+ labels/supportingText wrap instead of overflowing. Same equal-specificity reasoning. */
383
+ white-space: normal !important;
384
+ }
385
+
386
+ .option:last-of-type {
387
+ margin-bottom: 0;
365
388
  }
366
389
 
367
390
  /* No per-option hovered token exists in the schema; use the generic overlay tint (same technique
@@ -392,3 +415,99 @@
392
415
  --recursica_ui-kit_components_menu-item_variants_selection-states_selected_properties_colors_text-color
393
416
  ) !important;
394
417
  }
418
+
419
+ /* Rich option content (leadingIcon/supportingText — see MANTINE_ADAPTER_RICH_OPTION_DATA.md). No
420
+ dedicated dropdown-option icon or supporting-text token exists in the schema either; reuse the
421
+ menu-item component's tokens for the same reason the selected-state colors above do. Matches
422
+ the mantine-adapter's equivalent classes. */
423
+ .optionContent {
424
+ display: flex;
425
+ align-items: center;
426
+ gap: var(--recursica_ui-kit_components_menu-item_properties_icon-text-gap);
427
+ }
428
+
429
+ .optionIcon {
430
+ display: flex;
431
+ flex-shrink: 0;
432
+ width: var(
433
+ --recursica_ui-kit_components_menu-item_properties_icon-leading-size
434
+ );
435
+ height: var(
436
+ --recursica_ui-kit_components_menu-item_properties_icon-leading-size
437
+ );
438
+ color: var(
439
+ --recursica_ui-kit_components_menu-item_variants_selection-states_unselected_properties_colors_leading-icon-color
440
+ );
441
+ }
442
+
443
+ .optionIcon :global(svg) {
444
+ width: 100%;
445
+ height: 100%;
446
+ }
447
+
448
+ .optionText {
449
+ display: flex;
450
+ flex-direction: column;
451
+ min-width: 0;
452
+ gap: var(--recursica_ui-kit_components_menu-item_properties_text-gap);
453
+ }
454
+
455
+ /* Default: label/supportingText each truncate to a single line with an ellipsis. */
456
+ .optionText > * {
457
+ overflow: hidden;
458
+ text-overflow: ellipsis;
459
+ white-space: nowrap;
460
+ }
461
+
462
+ /* Applied alongside .optionText when `wrapItemText` is true — long values wrap onto additional
463
+ lines instead of overflowing the fixed-width dropdown. */
464
+ .optionTextWrap > * {
465
+ overflow: visible;
466
+ text-overflow: clip;
467
+ white-space: normal;
468
+ overflow-wrap: anywhere;
469
+ }
470
+
471
+ .optionSupportingText {
472
+ font-family: var(
473
+ --recursica_ui-kit_components_menu-item_properties_supporting-text_font-family
474
+ );
475
+ font-size: var(
476
+ --recursica_ui-kit_components_menu-item_properties_supporting-text_font-size
477
+ );
478
+ font-style: var(
479
+ --recursica_ui-kit_components_menu-item_properties_supporting-text_font-style
480
+ );
481
+ font-weight: var(
482
+ --recursica_ui-kit_components_menu-item_properties_supporting-text_font-weight
483
+ );
484
+ letter-spacing: var(
485
+ --recursica_ui-kit_components_menu-item_properties_supporting-text_letter-spacing
486
+ );
487
+ line-height: var(
488
+ --recursica_ui-kit_components_menu-item_properties_supporting-text_line-height
489
+ );
490
+ text-decoration: var(
491
+ --recursica_ui-kit_components_menu-item_properties_supporting-text_text-decoration
492
+ );
493
+ text-transform: var(
494
+ --recursica_ui-kit_components_menu-item_properties_supporting-text_text-transform
495
+ );
496
+ color: var(
497
+ --recursica_ui-kit_components_menu-item_variants_selection-states_unselected_properties_colors_supporting-text-color
498
+ );
499
+ }
500
+
501
+ .option[data-selected="true"] .optionIcon,
502
+ .option[data-combobox-selected="true"] .optionIcon {
503
+ color: var(
504
+ --recursica_ui-kit_components_menu-item_variants_selection-states_selected_properties_colors_leading-icon-color
505
+ ) !important;
506
+ }
507
+
508
+ .option[data-selected="true"] .optionSupportingText,
509
+ .option[data-combobox-selected="true"] .optionSupportingText {
510
+ color: var(
511
+ --recursica_ui-kit_components_menu-item_variants_selection-states_selected_properties_colors_supporting-text-color
512
+ ) !important;
513
+ }
@@ -2,6 +2,8 @@ import React from "react";
2
2
  import type { Meta, StoryObj } from "@storybook/react";
3
3
  import { Dropdown } from "./Dropdown";
4
4
  import { formControlArgTypes } from "../../../.storybook/commonArgTypes";
5
+ import { renderRichOptionContent } from "../../utils/renderRichOption";
6
+ import styles from "./Dropdown.module.css";
5
7
 
6
8
  type DropdownStoryProps = React.ComponentProps<typeof Dropdown>;
7
9
 
@@ -42,6 +44,11 @@ const meta: Meta<DropdownStoryProps> = {
42
44
  clearable: {
43
45
  control: "boolean",
44
46
  },
47
+ wrapItemText: {
48
+ control: "boolean",
49
+ description:
50
+ "Wraps option label/supportingText onto additional lines instead of truncating with an ellipsis.",
51
+ },
45
52
  containerWidth: {
46
53
  table: { disable: true },
47
54
  },
@@ -86,6 +93,148 @@ export const WithLeadingIcon: Story = {
86
93
  },
87
94
  };
88
95
 
96
+ const UserIcon = (
97
+ <svg
98
+ width="16"
99
+ height="16"
100
+ viewBox="0 0 24 24"
101
+ fill="none"
102
+ stroke="currentColor"
103
+ strokeWidth="2"
104
+ strokeLinecap="round"
105
+ strokeLinejoin="round"
106
+ >
107
+ <path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"></path>
108
+ <circle cx="12" cy="7" r="4"></circle>
109
+ </svg>
110
+ );
111
+
112
+ export const WithRichOptions: Story = {
113
+ args: {
114
+ label: "Assignee",
115
+ placeholder: "Pick a team member",
116
+ assistiveText:
117
+ "Each option can show a leading icon and supporting text — see MANTINE_ADAPTER_RICH_OPTION_DATA.md.",
118
+ data: [
119
+ {
120
+ value: "jdoe",
121
+ label: "Jane Doe",
122
+ leadingIcon: UserIcon,
123
+ supportingText: "jane.doe@example.com",
124
+ },
125
+ {
126
+ value: "asmith",
127
+ label: "Alex Smith",
128
+ leadingIcon: UserIcon,
129
+ supportingText: "alex.smith@example.com",
130
+ },
131
+ { value: "unassigned", label: "Unassigned" },
132
+ ],
133
+ },
134
+ };
135
+
136
+ export const WithRichOptionsWrapped: Story = {
137
+ args: {
138
+ label: "Assignee",
139
+ placeholder: "Pick a team member",
140
+ wrapItemText: true,
141
+ data: [
142
+ {
143
+ value: "jdoe",
144
+ label: "Jane Doe, Senior Staff Engineer, Platform Infrastructure",
145
+ leadingIcon: UserIcon,
146
+ supportingText:
147
+ "jane.doe@example.com — Platform Infrastructure team, on-call rotation lead",
148
+ },
149
+ { value: "unassigned", label: "Unassigned" },
150
+ ],
151
+ assistiveText:
152
+ "wrapItemText=true — long label/supportingText wrap instead of truncating.",
153
+ },
154
+ };
155
+
156
+ const optionRowPreviewClassNames = {
157
+ optionContent: styles.optionContent,
158
+ optionIcon: styles.optionIcon,
159
+ optionText: styles.optionText,
160
+ optionTextWrap: styles.optionTextWrap,
161
+ optionSupportingText: styles.optionSupportingText,
162
+ };
163
+
164
+ const OPTION_ROW_PREVIEW_ITEMS = [
165
+ {
166
+ value: "icon-and-supporting",
167
+ label: "Jane Doe",
168
+ leadingIcon: UserIcon,
169
+ supportingText: "jane.doe@example.com",
170
+ },
171
+ {
172
+ value: "no-icon",
173
+ label: "Alex Smith",
174
+ supportingText:
175
+ "No leadingIcon — label/supportingText shift left, no reserved icon space",
176
+ },
177
+ {
178
+ value: "no-supporting-text",
179
+ label: "Taylor Rivera",
180
+ leadingIcon: UserIcon,
181
+ },
182
+ {
183
+ value: "plain",
184
+ label: "Plain option — no leadingIcon, no supportingText",
185
+ },
186
+ {
187
+ value: "long-text",
188
+ label:
189
+ "A very long option label that, with wrapItemText, wraps onto a second line instead of overflowing the fixed-width dropdown — otherwise it truncates with an ellipsis",
190
+ leadingIcon: UserIcon,
191
+ supportingText:
192
+ "A similarly long supporting text string, to confirm the same wrap-or-truncate behavior applies to it too",
193
+ },
194
+ ];
195
+
196
+ // Renders the option row content directly — outside the MUI Menu portal — inside a container
197
+ // sized to Dropdown's own max-width token. Spacing between rows, icon/supportingText presence-
198
+ // or-absence alignment, and long-text wrapping/truncation are all much easier to inspect this way
199
+ // than by opening the real (portal-rendered) MUI Select menu. See
200
+ // MANTINE_ADAPTER_RICH_OPTION_DATA.md.
201
+ const renderOptionRowPreview = (wrapItemText: boolean) => (
202
+ <div
203
+ className={styles.dropdown}
204
+ style={{
205
+ width:
206
+ "var(--recursica_ui-kit_components_dropdown_variants_layouts_stacked_properties_max-width)",
207
+ }}
208
+ >
209
+ {OPTION_ROW_PREVIEW_ITEMS.map((item) => (
210
+ <div key={item.value} className={styles.option}>
211
+ {renderRichOptionContent(
212
+ item,
213
+ optionRowPreviewClassNames,
214
+ wrapItemText,
215
+ )}
216
+ </div>
217
+ ))}
218
+ </div>
219
+ );
220
+
221
+ // Default: `wrapItemText` is false — label/supportingText truncate to a single line with an
222
+ // ellipsis instead of wrapping.
223
+ export const RichOptionRowPreview: Story = {
224
+ parameters: {
225
+ controls: { disable: true },
226
+ },
227
+ render: () => renderOptionRowPreview(false),
228
+ };
229
+
230
+ // `wrapItemText: true` — label/supportingText wrap onto additional lines instead of truncating.
231
+ export const RichOptionRowPreviewWrapped: Story = {
232
+ parameters: {
233
+ controls: { disable: true },
234
+ },
235
+ render: () => renderOptionRowPreview(true),
236
+ };
237
+
89
238
  export const StaticError: Story = {
90
239
  args: {
91
240
  error: "You must choose a valid destination.",
@@ -5,7 +5,10 @@ import {
5
5
  SelectChangeEvent,
6
6
  MenuItem,
7
7
  } from "@mui/material";
8
- import { type ReadOnlyControlProps } from "@recursica/adapter-common";
8
+ import {
9
+ type ReadOnlyControlProps,
10
+ normalizeComboboxData,
11
+ } from "@recursica/adapter-common";
9
12
  import {
10
13
  filterStylingProps,
11
14
  omitUnsupportedProps,
@@ -13,6 +16,7 @@ import {
13
16
  } from "../../utils/filterStylingProps";
14
17
  import { type RecursicaFormControlWrapperProps } from "../FormControlWrapper/FormControlWrapper";
15
18
  import { WithReadOnlyWrapper } from "../ReadOnlyField/WithReadOnlyWrapper";
19
+ import { renderRichOptionContent } from "../../utils/renderRichOption";
16
20
  import { ChevronIcon, ClearIcon } from "./Dropdown.icons";
17
21
  import styles from "./Dropdown.module.css";
18
22
 
@@ -86,6 +90,7 @@ export const Dropdown = forwardRef<HTMLInputElement, DropdownProps>(
86
90
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
87
91
  searchable, // Not natively supported by basic MUI Select, stubbed
88
92
  clearable,
93
+ wrapItemText = false,
89
94
  ...rest
90
95
  } = props;
91
96
  // Props this component intentionally doesn't support — deleted at runtime so they can't leak
@@ -151,9 +156,22 @@ export const Dropdown = forwardRef<HTMLInputElement, DropdownProps>(
151
156
  ? `${styles.layoutOverride} ${className}`
152
157
  : styles.layoutOverride;
153
158
 
159
+ const optionClassNames = {
160
+ optionContent: styles.optionContent,
161
+ optionIcon: styles.optionIcon,
162
+ optionText: styles.optionText,
163
+ optionTextWrap: styles.optionTextWrap,
164
+ optionSupportingText: styles.optionSupportingText,
165
+ };
166
+
167
+ // See AutoComplete.tsx's identical use of `normalizeComboboxData` (adapter-common) — items
168
+ // always have a real `label` after this, so downstream code doesn't need its own `?? value`
169
+ // fallback at every read.
170
+ const normalizedData = normalizeComboboxData(data);
171
+
154
172
  const renderOptions = () => {
155
- if (!data) return null;
156
- return data.map((item, index) => {
173
+ if (!normalizedData) return null;
174
+ return normalizedData.map((item, index) => {
157
175
  if (typeof item === "string") {
158
176
  return (
159
177
  <MenuItem
@@ -179,12 +197,26 @@ export const Dropdown = forwardRef<HTMLInputElement, DropdownProps>(
179
197
  disableRipple
180
198
  data-selected={item.value === internalValue ? "true" : undefined}
181
199
  >
182
- {item.label}
200
+ {renderRichOptionContent(item, optionClassNames, wrapItemText)}
183
201
  </MenuItem>
184
202
  );
185
203
  });
186
204
  };
187
205
 
206
+ // MUI's closed-field display otherwise mirrors the selected `MenuItem`'s children directly —
207
+ // now that those can be a rich icon+label+supportingText row, `renderValue` keeps the closed
208
+ // field showing just the plain label, matching the field's own single-line text control.
209
+ const renderValue = (selected: unknown) => {
210
+ if (selected === "" || selected === undefined || selected === null) {
211
+ return "";
212
+ }
213
+ const match = normalizedData?.find((item) =>
214
+ typeof item === "string" ? item === selected : item.value === selected,
215
+ );
216
+ if (!match) return "";
217
+ return typeof match === "string" ? match : match.label;
218
+ };
219
+
188
220
  const wrappedStartAdornment = startAdornment ? (
189
221
  <span className={styles.section} data-position="left">
190
222
  {startAdornment}
@@ -246,6 +278,7 @@ export const Dropdown = forwardRef<HTMLInputElement, DropdownProps>(
246
278
  error={!!error}
247
279
  required={required}
248
280
  displayEmpty
281
+ renderValue={renderValue}
249
282
  className={styles.root}
250
283
  classes={{
251
284
  select: styles.input,
@@ -46,3 +46,23 @@ All Recursica components in the `@recursica/mui-adapter` package adhere strictly
46
46
 
47
47
  - Pass `startAdornment` for a leading icon, and `clearable` (with a value present) to show a clear button — both render using the dropdown's own icon-color tokens, matching the mantine-adapter's `leftSection`/`clearable` behavior.
48
48
  - `onChange` follows MUI's native `Select` signature: `(event: SelectChangeEvent, child: ReactNode) => void`.
49
+ - `data` items can carry a `leadingIcon` and `supportingText`, rendered inside each option row
50
+ (the closed field always shows the plain `label`, matching the mantine-adapter):
51
+
52
+ ```tsx
53
+ <Dropdown
54
+ label="Assignee"
55
+ data={[
56
+ {
57
+ value: "jdoe",
58
+ label: "Jane Doe",
59
+ leadingIcon: <UserIcon />,
60
+ supportingText: "jane.doe@example.com",
61
+ },
62
+ { value: "asmith", label: "Alex Smith" },
63
+ ]}
64
+ />
65
+ ```
66
+
67
+ By default `label`/`supportingText` truncate to a single line with an ellipsis. Set
68
+ `wrapItemText` to wrap them onto additional lines instead: `<Dropdown data={data} wrapItemText />`.
@@ -136,7 +136,10 @@ roving group, _then_ the trailing clear button as its own stop.
136
136
  wraps onto multiple lines below the dropzone — `FileInput` is a fixed single-line, `min-height`d
137
137
  control, so wrapping would grow it vertically. Enough chips to overflow the control's own width
138
138
  scroll horizontally within it instead (mouse wheel/trackpad or a native scrollbar), same tradeoff
139
- already made for `.value`'s ellipsis truncation.
139
+ already made for `.value`'s ellipsis truncation. The native scrollbar itself is hidden
140
+ (`scrollbar-width: none`, `-ms-overflow-style: none`, `::-webkit-scrollbar { display: none }`) —
141
+ the control's `min-height` isn't tall enough to host a visible horizontal scrollbar without
142
+ clipping the chips, and scrolling (wheel/trackpad/drag) still works with it hidden.
140
143
 
141
144
  ## Read-only vs disabled
142
145
 
@@ -1,3 +1,13 @@
1
+ /* Brand-layer exemptions (recursica-allow-brand) — see recursica-token-analyzer README.md.
2
+ * Global hover/focus/disabled state tokens (recursica_variables_scoped.css header, 'Hover & Focus states' / 'Disabled state' — implicit rule for every interactive element; components must not define their own per-component treatment).
3
+ * recursica-allow-brand: --recursica_brand_states_focus_blur
4
+ * recursica-allow-brand: --recursica_brand_states_focus_border-size
5
+ * recursica-allow-brand: --recursica_brand_states_focus_color
6
+ * recursica-allow-brand: --recursica_brand_states_focus_margin
7
+ * recursica-allow-brand: --recursica_brand_states_hover_color
8
+ * recursica-allow-brand: --recursica_brand_states_hover_opacity
9
+ */
10
+
1
11
  /* LAYOUT SPACING OVERRIDES:
2
12
  - Sets the --form-control-margin-bottom spacing hook to map component-specific layout tokens.
3
13
  - Also sets the --file-input-control-{max,min}-width hooks consumed inline in FileInput.tsx,
@@ -178,6 +188,13 @@
178
188
  overflow-x: auto;
179
189
  overflow-y: hidden;
180
190
  gap: var(--recursica_ui-kit_components_file-input_properties_icon-text-gap);
191
+ /* HARDCODE: scrollbar stays functional (still scrolls via wheel/trackpad/drag) but hidden —
192
+ the control isn't tall enough to host a visible scrollbar without clipping the chips. */
193
+ scrollbar-width: none; /* Firefox */
194
+ -ms-overflow-style: none; /* legacy Edge */
195
+ }
196
+ .chipRow::-webkit-scrollbar {
197
+ display: none; /* Chrome, Safari */
181
198
  }
182
199
 
183
200
  .chipWrapper {
@@ -1,3 +1,9 @@
1
+ /* Brand-layer exemptions (recursica-allow-brand) — see recursica-token-analyzer README.md.
2
+ * Global hover/focus/disabled state tokens (recursica_variables_scoped.css header, 'Hover & Focus states' / 'Disabled state' — implicit rule for every interactive element; components must not define their own per-component treatment).
3
+ * recursica-allow-brand: --recursica_brand_states_hover_color
4
+ * recursica-allow-brand: --recursica_brand_states_hover_opacity
5
+ */
6
+
1
7
  /* HARDCODED VALUES:
2
8
  - .root: display: flex; flex-direction: column; width: 100% — structural layout, not a design
3
9
  token concern (matches how other block-level components like Layer are laid out).
@@ -1,3 +1,8 @@
1
+ /* Brand-layer exemptions (recursica-allow-brand) — see recursica-token-analyzer README.md.
2
+ * Only source for link hover-underline; no per-component token exists (see IMPLEMENTATION_NOTES.md).
3
+ * recursica-allow-brand: --recursica_brand_states_link_decoration
4
+ */
5
+
1
6
  /*
2
7
  HARDCODED VALUES:
3
8
  - display: inline-flex; (Base layout to align icon and text correctly)
@@ -1,3 +1,9 @@
1
+ /* Brand-layer exemptions (recursica-allow-brand) — see recursica-token-analyzer README.md.
2
+ * Global hover/focus/disabled state tokens (recursica_variables_scoped.css header, 'Hover & Focus states' / 'Disabled state' — implicit rule for every interactive element; components must not define their own per-component treatment).
3
+ * recursica-allow-brand: --recursica_brand_states_hover_color
4
+ * recursica-allow-brand: --recursica_brand_states_hover_opacity
5
+ */
6
+
1
7
  /* HARDCODED VALUES:
2
8
  - border-style: solid. Structural rendering rule for the dropdown border (Mantine uses Paper which may not set border natively).
3
9
  - NOTE: overflow is intentionally NOT set on .dropdown. Mantine renders sub-menu dropdowns
@@ -1,3 +1,11 @@
1
+ /* Brand-layer exemptions (recursica-allow-brand) — see recursica-token-analyzer README.md.
2
+ * Global hover/focus/disabled state tokens (recursica_variables_scoped.css header, 'Hover & Focus states' / 'Disabled state' — implicit rule for every interactive element; components must not define their own per-component treatment).
3
+ * recursica-allow-brand: --recursica_brand_states_focus_blur
4
+ * recursica-allow-brand: --recursica_brand_states_focus_border-size
5
+ * recursica-allow-brand: --recursica_brand_states_focus_color
6
+ * recursica-allow-brand: --recursica_brand_states_focus_margin
7
+ */
8
+
1
9
  /* LAYOUT SPACING OVERRIDES:
2
10
  - Sets the --form-control-margin-bottom spacing hook to map component-specific layout tokens.
3
11
  - Also sets the --number-input-control-{max,min}-width hooks consumed inline in NumberInput.tsx,
@@ -1,3 +1,10 @@
1
+ /* Brand-layer exemptions (recursica-allow-brand) — see recursica-token-analyzer README.md.
2
+ * Global hover/focus/disabled state tokens (recursica_variables_scoped.css header, 'Hover & Focus states' / 'Disabled state' — implicit rule for every interactive element; components must not define their own per-component treatment).
3
+ * recursica-allow-brand: --recursica_brand_states_disabled
4
+ * recursica-allow-brand: --recursica_brand_states_hover_color
5
+ * recursica-allow-brand: --recursica_brand_states_hover_opacity
6
+ */
7
+
1
8
  .root {
2
9
  display: flex;
3
10
  align-items: center;
@@ -1,3 +1,11 @@
1
+ /* Brand-layer exemptions (recursica-allow-brand) — see recursica-token-analyzer README.md.
2
+ * Global hover/focus/disabled state tokens (recursica_variables_scoped.css header, 'Hover & Focus states' / 'Disabled state' — implicit rule for every interactive element; components must not define their own per-component treatment).
3
+ * recursica-allow-brand: --recursica_brand_states_focus_blur
4
+ * recursica-allow-brand: --recursica_brand_states_focus_border-size
5
+ * recursica-allow-brand: --recursica_brand_states_focus_color
6
+ * recursica-allow-brand: --recursica_brand_states_focus_margin
7
+ */
8
+
1
9
  /*
2
10
  * HARDCODED VALUES:
3
11
  * - display: flex; align-items: center; width: 100%; (Standard CSS flexbox layouts for bidirectional components)
@@ -1,3 +1,11 @@
1
+ /* Brand-layer exemptions (recursica-allow-brand) — see recursica-token-analyzer README.md.
2
+ * Global hover/focus/disabled state tokens (recursica_variables_scoped.css header, 'Hover & Focus states' / 'Disabled state' — implicit rule for every interactive element; components must not define their own per-component treatment).
3
+ * recursica-allow-brand: --recursica_brand_states_focus_blur
4
+ * recursica-allow-brand: --recursica_brand_states_focus_border-size
5
+ * recursica-allow-brand: --recursica_brand_states_focus_color
6
+ * recursica-allow-brand: --recursica_brand_states_focus_margin
7
+ */
8
+
1
9
  /* HARDCODED VALUES
2
10
  *
3
11
  * 1. track border: none; (we do not use border for the switch track)
@@ -1,3 +1,12 @@
1
+ /* Brand-layer exemptions (recursica-allow-brand) — see recursica-token-analyzer README.md.
2
+ * Outline variant's baseline-patch trick needs an opaque paint (active background is transparent by design); borrows Layer 0's surface as the default ambient layer absent a wrapping <Layer> (see IMPLEMENTATION_NOTES.md 'Outline selected-tab border'). Schema gap: no generic 'current ambient layer' token exists — flagged for the token-schema owner.
3
+ * recursica-allow-brand: --recursica_brand_layer_0_properties_surface
4
+ * Global hover/focus/disabled state tokens (recursica_variables_scoped.css header, 'Hover & Focus states' / 'Disabled state' — implicit rule for every interactive element; components must not define their own per-component treatment).
5
+ * recursica-allow-brand: --recursica_brand_states_disabled
6
+ * recursica-allow-brand: --recursica_brand_states_hover_color
7
+ * recursica-allow-brand: --recursica_brand_states_hover_opacity
8
+ */
9
+
1
10
  /*
2
11
  Recursica Tabs CSS module natively binding to Figma variables.
3
12
  */
@@ -1,3 +1,11 @@
1
+ /* Brand-layer exemptions (recursica-allow-brand) — see recursica-token-analyzer README.md.
2
+ * Global hover/focus/disabled state tokens (recursica_variables_scoped.css header, 'Hover & Focus states' / 'Disabled state' — implicit rule for every interactive element; components must not define their own per-component treatment).
3
+ * recursica-allow-brand: --recursica_brand_states_focus_blur
4
+ * recursica-allow-brand: --recursica_brand_states_focus_border-size
5
+ * recursica-allow-brand: --recursica_brand_states_focus_color
6
+ * recursica-allow-brand: --recursica_brand_states_focus_margin
7
+ */
8
+
1
9
  /* LAYOUT SPACING OVERRIDES:
2
10
  - Sets the --form-control-margin-bottom spacing hook to map component-specific layout tokens.
3
11
  - Also sets the --textarea-control-{max,min}-width hooks consumed inline in TextArea.tsx, since
@@ -1,3 +1,11 @@
1
+ /* Brand-layer exemptions (recursica-allow-brand) — see recursica-token-analyzer README.md.
2
+ * Global hover/focus/disabled state tokens (recursica_variables_scoped.css header, 'Hover & Focus states' / 'Disabled state' — implicit rule for every interactive element; components must not define their own per-component treatment).
3
+ * recursica-allow-brand: --recursica_brand_states_focus_blur
4
+ * recursica-allow-brand: --recursica_brand_states_focus_border-size
5
+ * recursica-allow-brand: --recursica_brand_states_focus_color
6
+ * recursica-allow-brand: --recursica_brand_states_focus_margin
7
+ */
8
+
1
9
  /* LAYOUT SPACING OVERRIDES:
2
10
  - Sets the --form-control-margin-bottom spacing hook to map component-specific layout tokens.
3
11
  - Also sets the --text-field-control-{max,min}-width hooks consumed inline in TextField.tsx,
@@ -1,3 +1,11 @@
1
+ /* Brand-layer exemptions (recursica-allow-brand) — see recursica-token-analyzer README.md.
2
+ * Global hover/focus/disabled state tokens (recursica_variables_scoped.css header, 'Hover & Focus states' / 'Disabled state' — implicit rule for every interactive element; components must not define their own per-component treatment).
3
+ * recursica-allow-brand: --recursica_brand_states_focus_blur
4
+ * recursica-allow-brand: --recursica_brand_states_focus_border-size
5
+ * recursica-allow-brand: --recursica_brand_states_focus_color
6
+ * recursica-allow-brand: --recursica_brand_states_focus_margin
7
+ */
8
+
1
9
  /* LAYOUT SPACING OVERRIDES:
2
10
  - Sets the --form-control-margin-bottom spacing hook to map component-specific layout tokens. */
3
11
  .layoutOverride {
@@ -1,3 +1,14 @@
1
+ /* Brand-layer exemptions (recursica-allow-brand) — see recursica-token-analyzer README.md.
2
+ * Global hover/focus/disabled state tokens (recursica_variables_scoped.css header, 'Hover & Focus states' / 'Disabled state' — implicit rule for every interactive element; components must not define their own per-component treatment).
3
+ * recursica-allow-brand: --recursica_brand_states_disabled
4
+ * recursica-allow-brand: --recursica_brand_states_focus_blur
5
+ * recursica-allow-brand: --recursica_brand_states_focus_border-size
6
+ * recursica-allow-brand: --recursica_brand_states_focus_color
7
+ * recursica-allow-brand: --recursica_brand_states_focus_margin
8
+ * recursica-allow-brand: --recursica_brand_states_hover_color
9
+ * recursica-allow-brand: --recursica_brand_states_hover_opacity
10
+ */
11
+
1
12
  /* HARDCODED VALUES:
2
13
  * - list-style/margin/padding resets on .root/.subtree/.node: layout resets, no corresponding
3
14
  * design tokens (structural, not visual design values).