@recursica/mantine-adapter 0.36.1 → 0.38.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 (61) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/dist/mantine-adapter.cjs +2 -2
  3. package/dist/mantine-adapter.cjs.map +1 -1
  4. package/dist/mantine-adapter.css +1 -1
  5. package/dist/mantine-adapter.js +2352 -2099
  6. package/dist/mantine-adapter.js.map +1 -1
  7. package/dist/src/components/Dropdown/BareDropdown.d.ts +19 -0
  8. package/dist/src/components/TimePicker/TimePicker.d.ts +9 -3
  9. package/dist/src/components/Tree/Tree.d.ts +5 -0
  10. package/dist/src/index.d.ts +1 -1
  11. package/package.json +1 -1
  12. package/src/components/Accordion/USAGE.md +2 -41
  13. package/src/components/AutoComplete/USAGE.md +2 -18
  14. package/src/components/Avatar/USAGE.md +0 -27
  15. package/src/components/Badge/USAGE.md +3 -6
  16. package/src/components/Breadcrumb/USAGE.md +1 -5
  17. package/src/components/Button/Button.tsx +5 -0
  18. package/src/components/Button/IMPLEMENTATION_NOTES.md +8 -0
  19. package/src/components/Button/USAGE.md +5 -25
  20. package/src/components/Card/USAGE.md +4 -12
  21. package/src/components/Checkbox/USAGE.md +4 -20
  22. package/src/components/Chip/USAGE.md +3 -32
  23. package/src/components/DatePicker/USAGE.md +2 -12
  24. package/src/components/Dropdown/BareDropdown.tsx +85 -0
  25. package/src/components/Dropdown/Dropdown.tsx +12 -3
  26. package/src/components/Dropdown/USAGE.md +2 -6
  27. package/src/components/Flex/USAGE.md +1 -1
  28. package/src/components/FormControlWrapper/USAGE.md +3 -31
  29. package/src/components/Grid/USAGE.md +1 -1
  30. package/src/components/Group/USAGE.md +1 -1
  31. package/src/components/HoverCard/USAGE.md +3 -72
  32. package/src/components/Label/USAGE.md +8 -48
  33. package/src/components/Link/USAGE.md +4 -10
  34. package/src/components/Loader/USAGE.md +4 -23
  35. package/src/components/Menu/USAGE.md +3 -73
  36. package/src/components/Modal/USAGE.md +3 -3
  37. package/src/components/NumberInput/USAGE.md +5 -8
  38. package/src/components/Pagination/USAGE.md +0 -19
  39. package/src/components/Panel/USAGE.md +6 -95
  40. package/src/components/Popover/USAGE.md +6 -66
  41. package/src/components/ReadOnlyField/USAGE.md +2 -10
  42. package/src/components/SegmentedControl/USAGE.md +1 -15
  43. package/src/components/Slider/USAGE.md +1 -45
  44. package/src/components/Stack/USAGE.md +1 -1
  45. package/src/components/Switch/USAGE.md +1 -24
  46. package/src/components/TextArea/USAGE.md +2 -2
  47. package/src/components/TextField/USAGE.md +1 -17
  48. package/src/components/TimePicker/TIMEPICKER_IMPLEMENTATION_NOTES.md +72 -0
  49. package/src/components/TimePicker/TimePicker.module.css +225 -41
  50. package/src/components/TimePicker/TimePicker.stories.tsx +105 -4
  51. package/src/components/TimePicker/TimePicker.tsx +287 -7
  52. package/src/components/TimePicker/USAGE.md +23 -2
  53. package/src/components/Timeline/USAGE.md +2 -10
  54. package/src/components/Toast/USAGE.md +3 -33
  55. package/src/components/Tooltip/USAGE.md +7 -51
  56. package/src/components/Tree/IMPLEMENTATION_NOTES.md +31 -3
  57. package/src/components/Tree/Tree.module.css +84 -40
  58. package/src/components/Tree/Tree.stories.tsx +13 -0
  59. package/src/components/Tree/Tree.tsx +116 -27
  60. package/src/components/Tree/USAGE.md +18 -1
  61. package/src/index.ts +1 -0
@@ -1,11 +1,15 @@
1
1
  /* HARDCODED VALUES:
2
2
  * - list-style/margin/padding resets on .root/.subtree: layout resets, no corresponding
3
3
  * design tokens (structural, not visual design values).
4
- * - .expandIcon size (1em) and rotation transition: no dedicated icon-size/icon-color token
5
- * exists for Tree; the glyph is drawn with `currentColor` so it always tracks the row's
6
- * already-tokened text color, and sized relative to the row's own font-size.
4
+ * - .expandGlyph's rotation transition (150ms ease): no dedicated timing token exists for Tree;
5
+ * the glyph itself is sized/colored entirely by Button's own "text"/"small" tokens now, not a
6
+ * hand-drawn size/color like the previous plain-SVG implementation.
7
7
  * - Hover overlay uses the generic --recursica_brand_states_hover_* tokens (same technique as
8
8
  * Menu/Accordion/Button): Tree has no component-specific hover tokens of its own.
9
+ *
10
+ * NOTE: the row's box model (padding/border/font/color/background) lives entirely on `.label`,
11
+ * not `.row` — the highlighted "chip" (hover/selected/focus) covers only the label, never the
12
+ * expand button. `.row` itself is a plain flex layout container (indentation + button-node-gap).
9
13
  */
10
14
 
11
15
  .root {
@@ -25,6 +29,16 @@
25
29
  ) !important;
26
30
  }
27
31
 
32
+ /* Whole-tree `disabled` (no per-node concept — see RecursicaTreeProps.ts). `pointer-events: none`
33
+ blocks every mouse interaction tree-wide in one rule (row clicks, chevron clicks); keyboard is
34
+ separately blocked in Tree.tsx, since pointer-events has no effect on Tab/Enter/Space/arrows.
35
+ Uses the generic disabled opacity token, same convention as every other disabled component in
36
+ the design system (no tree-specific disabled token exists). */
37
+ .root[data-disabled] {
38
+ opacity: var(--recursica_brand_states_disabled);
39
+ pointer-events: none;
40
+ }
41
+
28
42
  .subtree {
29
43
  margin: 0;
30
44
  padding: 0;
@@ -36,11 +50,33 @@
36
50
 
37
51
  .node {
38
52
  /* Mantine's own base class already resets margin/padding/list-style and sets cursor:pointer
39
- on this <li>; it also holds the subtree as a second child, so all row-level visual styling
40
- (box model, colors, typography) lives on .row instead, not here. */
53
+ on this <li>; it also holds the subtree as a second child, so all node-level visual styling
54
+ (box model, colors, typography) lives on .label instead, not here. */
41
55
  position: relative;
42
56
  }
43
57
 
58
+ /* Mantine's roving-tabindex keyboard navigation (arrow up/down moves focus between rows, left/
59
+ right expand/collapse, matching the WAI-ARIA treeitem pattern) already works out of the box —
60
+ but with no visible indicator, it looked broken. DOM focus lands on this <li> (.node), not the
61
+ visible row box, so the ring is drawn on .label instead — the "node" content area, deliberately
62
+ excluding the expand Button, which never shows a focus state of its own (it's not independently
63
+ focusable at all: tabIndex={-1}). */
64
+ .node:focus-visible {
65
+ outline: none;
66
+ }
67
+ /* `> .row` (child combinator), not a plain descendant selector: a node's own `.row` is always a
68
+ direct child of its `.node` <li>, while every descendant node's `.row` (inside the nested
69
+ `.subtree` <ul>) is not — without the `>`, focusing a parent node drew the ring on every
70
+ expanded descendant's label too, since they're all still descendants of the focused `.node`. */
71
+ .node:focus-visible > .row .label {
72
+ box-shadow:
73
+ 0 0 0 var(--recursica_brand_states_focus_border-size)
74
+ var(--recursica_brand_states_focus_color),
75
+ 0 0 var(--recursica_brand_states_focus_blur)
76
+ var(--recursica_brand_states_focus_margin)
77
+ var(--recursica_brand_states_focus_color);
78
+ }
79
+
44
80
  .row {
45
81
  position: relative;
46
82
  box-sizing: border-box;
@@ -49,6 +85,39 @@
49
85
  cursor: pointer;
50
86
  margin-left: var(--label-offset, 0px);
51
87
  gap: var(--recursica_ui-kit_components_tree_properties_button-node-gap);
88
+ }
89
+
90
+ /* The expand Button is always rendered (even for leaf nodes, which have nothing to toggle) so
91
+ every row reserves identical layout space — hidden via visibility (not display) on leaf rows so
92
+ the label stays aligned across the whole tree without duplicating Button's own size tokens. */
93
+ .expandButton {
94
+ position: relative;
95
+ z-index: 1;
96
+ flex-shrink: 0;
97
+ }
98
+ .row:not([data-has-children]) .expandButton {
99
+ visibility: hidden;
100
+ }
101
+
102
+ .expandGlyph {
103
+ color: inherit;
104
+ }
105
+ .row[data-has-children] .expandGlyph {
106
+ transition: transform 150ms ease;
107
+ }
108
+ .row[data-expanded] .expandGlyph {
109
+ transform: rotate(90deg);
110
+ }
111
+
112
+ .label {
113
+ position: relative;
114
+ z-index: 1;
115
+ /* Sized to its own content, not stretched to fill the row's remaining width — the
116
+ hover/selected/focus "chip" should cover just the label, not the full row width. */
117
+ flex: 0 1 auto;
118
+ box-sizing: border-box;
119
+ display: flex;
120
+ align-items: center;
52
121
  padding: var(--recursica_ui-kit_components_tree_properties_vertical-padding)
53
122
  var(--recursica_ui-kit_components_tree_properties_horizontal-padding);
54
123
  border-style: solid; /* HARDCODE: structural border rule; thickness/color are tokened */
@@ -93,24 +162,28 @@
93
162
  );
94
163
  }
95
164
 
96
- /* Hover overlay via ::after pseudo-element (same technique as Menu/Accordion) */
97
- .row::after {
165
+ /* Hover overlay via ::after pseudo-element (same technique as Menu/Accordion), scoped to the
166
+ label's own box so hover never visually covers the expand button. `.label`'s `position:
167
+ relative` + `z-index: 1` above already forms a stacking context, so `z-index: -1` here paints
168
+ the overlay behind the label's real text content within that context, not behind `.label`
169
+ itself. */
170
+ .label::after {
98
171
  content: "";
99
172
  position: absolute;
100
173
  inset: 0;
101
174
  border-radius: inherit;
102
- z-index: 0;
175
+ z-index: -1;
103
176
  pointer-events: none;
104
177
  background-color: var(--recursica_brand_states_hover_color);
105
178
  opacity: 0;
106
179
  transition: opacity 150ms ease;
107
180
  }
108
- .row:hover::after,
109
- .row[data-hovered]::after {
181
+ .row:hover .label::after,
182
+ .row[data-hovered] .label::after {
110
183
  opacity: var(--recursica_brand_states_hover_opacity);
111
184
  }
112
185
 
113
- .row[data-selected] {
186
+ .label[data-selected] {
114
187
  font-family: var(
115
188
  --recursica_ui-kit_components_tree_variants_selection-states_selected_properties_text_font-family
116
189
  );
@@ -146,32 +219,3 @@
146
219
  --recursica_ui-kit_components_tree_variants_selection-states_selected_properties_colors_border-color
147
220
  );
148
221
  }
149
-
150
- .expandIcon {
151
- position: relative;
152
- z-index: 1;
153
- display: flex;
154
- align-items: center;
155
- justify-content: center;
156
- flex-shrink: 0;
157
- width: 1em;
158
- height: 1em;
159
- color: inherit;
160
- }
161
-
162
- .row[data-has-children] .expandIcon {
163
- transition: transform 150ms ease;
164
- }
165
- .row[data-expanded] .expandIcon {
166
- transform: rotate(90deg);
167
- }
168
-
169
- .label {
170
- position: relative;
171
- z-index: 1;
172
- color: inherit;
173
- font: inherit;
174
- letter-spacing: inherit;
175
- text-decoration: inherit;
176
- text-transform: inherit;
177
- }
@@ -80,6 +80,19 @@ export const MultipleSelection: StoryObj<typeof Tree> = {
80
80
  ),
81
81
  };
82
82
 
83
+ /** Whole tree disabled, with a node pre-selected so the selected chip's styling under the
84
+ * disabled dimming can be checked alongside unselected rows. */
85
+ export const Disabled: StoryObj<typeof Tree> = {
86
+ render: () => (
87
+ <Tree
88
+ data={sampleData}
89
+ initialExpandedValues={["documents"]}
90
+ initialSelectedValues={["documents/resume.pdf"]}
91
+ disabled
92
+ />
93
+ ),
94
+ };
95
+
83
96
  /** Demonstrates the component nested inside a non-default layer — the one case where an
84
97
  * explicit `<Layer>` wrap belongs in a story (see COMPONENT_STORYBOOK_GUIDE.md §9). */
85
98
  export const LayerOne: StoryObj<typeof Tree> = {
@@ -5,22 +5,27 @@ import {
5
5
  getTreeExpandedState,
6
6
  type RenderTreeNodePayload,
7
7
  } from "@mantine/core";
8
+ import { useMergedRef } from "@mantine/hooks";
8
9
  import {
9
10
  filterStylingProps,
10
11
  type RecursicaOverStyled,
11
12
  } from "../../utils/filterStylingProps";
12
13
  import { type RecursicaTreeProps } from "@recursica/adapter-common";
14
+ import { Button } from "../Button/Button";
13
15
  import styles from "./Tree.module.css";
14
16
 
15
17
  export type TreeProps = RecursicaOverStyled<
16
18
  RecursicaTreeProps & Omit<React.ComponentPropsWithoutRef<"ul">, "children">
17
19
  >;
18
20
 
19
- /** Simple chevron glyph; rotates via `[data-expanded]` in CSS. Uses `currentColor` so it
20
- * always matches the row's tokened text color — Tree has no dedicated icon-color token. */
21
+ /** Simple chevron glyph; rotates via the row's `[data-expanded]` in CSS (`.row[data-expanded]
22
+ * .expandGlyph`) — this reaches the glyph regardless of how deeply `Button` nests it internally,
23
+ * since it's a plain descendant-combinator selector, not dependent on Button's own DOM structure.
24
+ * Uses `currentColor` so it always matches Button's own tokened icon color for the "text" variant. */
21
25
  function ExpandGlyph() {
22
26
  return (
23
27
  <svg
28
+ className={styles.expandGlyph}
24
29
  viewBox="0 0 16 16"
25
30
  width="1em"
26
31
  height="1em"
@@ -36,25 +41,56 @@ function ExpandGlyph() {
36
41
  );
37
42
  }
38
43
 
39
- function renderTreeNode({
40
- node,
41
- hasChildren,
42
- expanded,
43
- elementProps,
44
- }: RenderTreeNodePayload) {
45
- return (
46
- <div
47
- {...elementProps}
48
- className={styles.row}
49
- data-has-children={hasChildren || undefined}
50
- data-expanded={hasChildren && expanded ? true : undefined}
51
- >
52
- <span className={styles.expandIcon} aria-hidden="true">
53
- {hasChildren && <ExpandGlyph />}
54
- </span>
55
- <span className={styles.label}>{node.label}</span>
56
- </div>
57
- );
44
+ /** Curried so the fixed-shape `renderNode` callback (Mantine controls its signature) can still
45
+ * close over `disabled`, which isn't part of `RenderTreeNodePayload`. */
46
+ function createRenderTreeNode(disabled: boolean) {
47
+ return function renderTreeNode({
48
+ node,
49
+ hasChildren,
50
+ expanded,
51
+ selected,
52
+ tree,
53
+ elementProps,
54
+ }: RenderTreeNodePayload) {
55
+ return (
56
+ <div
57
+ {...elementProps}
58
+ className={styles.row}
59
+ data-has-children={hasChildren || undefined}
60
+ data-expanded={hasChildren && expanded ? true : undefined}
61
+ >
62
+ {/* The only element that toggles expand/collapse on click — independent of selection,
63
+ which `elementProps.onClick` (spread onto `.row` above) handles for the rest of the
64
+ row. Stops propagation so the click never also reaches `.row`'s own handler and
65
+ selects the node. Never independently focusable/tab-stoppable (tabIndex={-1},
66
+ aria-hidden), so the row stays the only focusable element and this button never
67
+ shows its own focus state. Always rendered, even for leaf nodes, so every row
68
+ reserves the same layout space; CSS hides it (visibility, not display) when there
69
+ are no children to toggle. Guards `disabled` itself (not just via the CSS
70
+ `pointer-events: none` on `.root`) since this handler bypasses Mantine's own
71
+ `expandOnClick`/`selectOnClick` flags entirely by calling `tree.toggleExpanded`
72
+ directly. */}
73
+ <Button
74
+ overStyled
75
+ variant="text"
76
+ size="small"
77
+ icon={<ExpandGlyph />}
78
+ aria-label="Toggle subtree"
79
+ aria-hidden="true"
80
+ tabIndex={-1}
81
+ className={styles.expandButton}
82
+ onClick={(event) => {
83
+ event.stopPropagation();
84
+ if (disabled) return;
85
+ tree.toggleExpanded(node.value);
86
+ }}
87
+ />
88
+ <span className={styles.label} data-selected={selected || undefined}>
89
+ {node.label}
90
+ </span>
91
+ </div>
92
+ );
93
+ };
58
94
  }
59
95
 
60
96
  /**
@@ -64,6 +100,11 @@ function renderTreeNode({
64
100
  * Wraps Mantine's `Tree` with a fully custom node renderer so every visual aspect (row box
65
101
  * model, selected/unselected colors and typography, indent, item spacing) comes from
66
102
  * Recursica's `tree` design tokens rather than Mantine's defaults.
103
+ *
104
+ * **Interaction pattern (fixed, not prop-configurable):** expand/collapse and select are
105
+ * independent — the chevron button toggles a node's subtree only, clicking the rest of a row
106
+ * (or pressing `Enter`/`Space`) selects it only, and `ArrowLeft`/`ArrowRight` toggle expansion
107
+ * only. See `RecursicaTreeProps` for the full breakdown.
67
108
  */
68
109
  export const Tree = forwardRef<HTMLUListElement, TreeProps>(function Tree(
69
110
  {
@@ -72,8 +113,7 @@ export const Tree = forwardRef<HTMLUListElement, TreeProps>(function Tree(
72
113
  initialExpandedValues,
73
114
  initialSelectedValues,
74
115
  multiple = false,
75
- expandOnClick = true,
76
- selectOnClick = true,
116
+ disabled = false,
77
117
  onNodeExpand,
78
118
  onNodeCollapse,
79
119
  onSelectedChange,
@@ -99,6 +139,53 @@ export const Tree = forwardRef<HTMLUListElement, TreeProps>(function Tree(
99
139
  onSelectedChange?.(tree.selectedState);
100
140
  }, [tree.selectedState, onSelectedChange]);
101
141
 
142
+ // `Enter`/`Space` select the focused node. Mantine's own `TreeNode` only wires this for
143
+ // `Space`, and only as an expand toggle (`expandOnSpace`, disabled below) — it has no
144
+ // select-on-key behavior and no `Enter` handling at all, and its key handler lives on the
145
+ // `<li role="treeitem">` itself (not reachable through `renderNode`'s `elementProps`), so this
146
+ // is added as a native listener on the tree root instead. `tree.select` is referentially
147
+ // stable (Mantine wraps it in `useCallback` with no deps), so this effect only re-runs when
148
+ // `disabled` actually changes.
149
+ const rootRef = React.useRef<HTMLUListElement>(null);
150
+ useEffect(() => {
151
+ const root = rootRef.current;
152
+ if (!root) return;
153
+ const handleKeyDown = (event: KeyboardEvent) => {
154
+ if (disabled) return;
155
+ if (event.key !== "Enter" && event.key !== " ") return;
156
+ const treeItem = (event.target as HTMLElement).closest<HTMLElement>(
157
+ '[role="treeitem"]',
158
+ );
159
+ const value = treeItem?.dataset.value;
160
+ if (!value) return;
161
+ event.preventDefault();
162
+ tree.select(value);
163
+ };
164
+ root.addEventListener("keydown", handleKeyDown);
165
+ return () => root.removeEventListener("keydown", handleKeyDown);
166
+ // eslint-disable-next-line react-hooks/exhaustive-deps
167
+ }, [tree.select, disabled]);
168
+
169
+ // When disabled, block every keydown at the capture phase — before it can reach either the
170
+ // listener above or Mantine's own internal per-node keydown handling (`ArrowLeft`/`ArrowRight`
171
+ // expand/collapse, baked into `TreeNode` itself with no prop to disable it). A capture-phase
172
+ // `stopPropagation` here keeps the event from ever reaching its target, so neither listener
173
+ // fires — this is the only reachable way to block Mantine's internal handling for a component
174
+ // with no `disabled` concept of its own. Mouse clicks are separately blocked via `pointer-
175
+ // events: none` in CSS (`.root[data-disabled]`); this effect only needs to cover keyboard.
176
+ useEffect(() => {
177
+ const root = rootRef.current;
178
+ if (!root || !disabled) return;
179
+ const blockAllKeydown = (event: KeyboardEvent) => {
180
+ event.preventDefault();
181
+ event.stopPropagation();
182
+ };
183
+ root.addEventListener("keydown", blockAllKeydown, { capture: true });
184
+ return () =>
185
+ root.removeEventListener("keydown", blockAllKeydown, { capture: true });
186
+ }, [disabled]);
187
+ const mergedRef = useMergedRef(ref, rootRef);
188
+
102
189
  const classNameProp = (sanitizedProps as Record<string, unknown>)
103
190
  .className as string | undefined;
104
191
  const rootClass = classNameProp
@@ -107,12 +194,14 @@ export const Tree = forwardRef<HTMLUListElement, TreeProps>(function Tree(
107
194
 
108
195
  return (
109
196
  <MantineTree
110
- ref={ref}
197
+ ref={mergedRef}
111
198
  data={data}
112
199
  tree={tree}
113
- expandOnClick={expandOnClick}
114
- selectOnClick={selectOnClick}
115
- renderNode={renderTreeNode}
200
+ expandOnClick={false}
201
+ selectOnClick={!disabled}
202
+ expandOnSpace={false}
203
+ renderNode={createRenderTreeNode(disabled)}
204
+ data-disabled={disabled || undefined}
116
205
  classNames={{
117
206
  root: rootClass,
118
207
  node: styles.node,
@@ -57,7 +57,24 @@ Each node needs a unique `value` and a `label`. Any node with a `children` array
57
57
 
58
58
  - `initialExpandedValues` accepts an array of node values, or `"*"` to start with every node expanded.
59
59
  - `multiple` allows more than one node to be selected at once (default: single-select).
60
- - `expandOnClick` / `selectOnClick` (both default `true`) control whether clicking a row toggles its expansion and/or selection.
60
+
61
+ ### Interaction pattern
62
+
63
+ Expanding/collapsing and selecting are independent, fixed interactions (not prop-configurable):
64
+
65
+ - Clicking the expand/collapse chevron toggles that node's subtree only — it never selects.
66
+ - Clicking anywhere else on a row selects it — it never toggles expansion.
67
+ - `Enter`/`Space` on a focused node selects it, even if the node has children.
68
+ - `ArrowLeft`/`ArrowRight` expand/collapse the focused node only, without changing selection.
69
+ - `ArrowUp`/`ArrowDown` move focus between rows.
70
+
71
+ ### Disabled
72
+
73
+ ```tsx
74
+ <Tree data={data} initialSelectedValues={["1.1"]} disabled />
75
+ ```
76
+
77
+ `disabled` disables the whole tree — no expand/collapse or select via click or keyboard, dimmed to the standard disabled opacity. A pre-selected node stays visibly selected, just dimmed along with the rest. There's no per-node disabled state (see **Known Constraints** below).
61
78
 
62
79
  ---
63
80
 
package/src/index.ts CHANGED
@@ -175,4 +175,5 @@ export type {
175
175
  RecursicaSwitchGroupProps,
176
176
  RecursicaTextAreaProps,
177
177
  RecursicaTextFieldProps,
178
+ RecursicaTimePickerProps,
178
179
  } from "./components";