@recursica/mantine-adapter 0.36.0 → 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 (63) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/README.md +3 -3
  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 +2352 -2099
  7. package/dist/mantine-adapter.js.map +1 -1
  8. package/dist/src/components/Dropdown/BareDropdown.d.ts +19 -0
  9. package/dist/src/components/TimePicker/TimePicker.d.ts +9 -3
  10. package/dist/src/components/Tree/Tree.d.ts +5 -0
  11. package/dist/src/index.d.ts +1 -1
  12. package/docs/PHILOSOPHY.md +39 -0
  13. package/package.json +3 -2
  14. package/src/components/Accordion/USAGE.md +2 -41
  15. package/src/components/AutoComplete/USAGE.md +2 -18
  16. package/src/components/Avatar/USAGE.md +0 -27
  17. package/src/components/Badge/USAGE.md +3 -6
  18. package/src/components/Breadcrumb/USAGE.md +1 -5
  19. package/src/components/Button/Button.tsx +5 -0
  20. package/src/components/Button/IMPLEMENTATION_NOTES.md +8 -0
  21. package/src/components/Button/USAGE.md +5 -25
  22. package/src/components/Card/USAGE.md +4 -12
  23. package/src/components/Checkbox/USAGE.md +4 -20
  24. package/src/components/Chip/USAGE.md +3 -32
  25. package/src/components/DatePicker/USAGE.md +2 -12
  26. package/src/components/Dropdown/BareDropdown.tsx +85 -0
  27. package/src/components/Dropdown/Dropdown.tsx +12 -3
  28. package/src/components/Dropdown/USAGE.md +2 -6
  29. package/src/components/Flex/USAGE.md +1 -1
  30. package/src/components/FormControlWrapper/USAGE.md +3 -31
  31. package/src/components/Grid/USAGE.md +1 -1
  32. package/src/components/Group/USAGE.md +1 -1
  33. package/src/components/HoverCard/USAGE.md +3 -72
  34. package/src/components/Label/USAGE.md +8 -48
  35. package/src/components/Link/USAGE.md +4 -10
  36. package/src/components/Loader/USAGE.md +4 -23
  37. package/src/components/Menu/USAGE.md +3 -73
  38. package/src/components/Modal/USAGE.md +3 -3
  39. package/src/components/NumberInput/USAGE.md +5 -8
  40. package/src/components/Pagination/USAGE.md +0 -19
  41. package/src/components/Panel/USAGE.md +6 -95
  42. package/src/components/Popover/USAGE.md +6 -66
  43. package/src/components/ReadOnlyField/USAGE.md +2 -10
  44. package/src/components/SegmentedControl/USAGE.md +1 -15
  45. package/src/components/Slider/USAGE.md +1 -45
  46. package/src/components/Stack/USAGE.md +1 -1
  47. package/src/components/Switch/USAGE.md +1 -24
  48. package/src/components/TextArea/USAGE.md +2 -2
  49. package/src/components/TextField/USAGE.md +1 -17
  50. package/src/components/TimePicker/TIMEPICKER_IMPLEMENTATION_NOTES.md +72 -0
  51. package/src/components/TimePicker/TimePicker.module.css +225 -41
  52. package/src/components/TimePicker/TimePicker.stories.tsx +105 -4
  53. package/src/components/TimePicker/TimePicker.tsx +287 -7
  54. package/src/components/TimePicker/USAGE.md +23 -2
  55. package/src/components/Timeline/USAGE.md +2 -10
  56. package/src/components/Toast/USAGE.md +3 -33
  57. package/src/components/Tooltip/USAGE.md +7 -51
  58. package/src/components/Tree/IMPLEMENTATION_NOTES.md +31 -3
  59. package/src/components/Tree/Tree.module.css +84 -40
  60. package/src/components/Tree/Tree.stories.tsx +13 -0
  61. package/src/components/Tree/Tree.tsx +116 -27
  62. package/src/components/Tree/USAGE.md +18 -1
  63. package/src/index.ts +1 -0
@@ -39,14 +39,6 @@ All Recursica components in the `@recursica/mantine-adapter` package adhere stri
39
39
 
40
40
  ## 4. Key Integration Features & Constraints
41
41
 
42
- ## 1. Stripping Mantine Assumptions
42
+ ### Editable Mode
43
43
 
44
- Unlike standard input variables, standard HTML output `<p>` tags inherently carry margin and spacing assumptions from core browser stylesheets. To correctly map `ReadOnlyTextField` elements gracefully inside the generic `FormControlWrapper` bounded context, we hardcode resets:
45
-
46
- - `margin: 0` explicitly strips block flow gap so `FormControlWrapper` handles vertical rhythm.
47
- - `min-height`: Native `Input` boxes typically have baseline padding borders. We map directly to `var(--recursica_ui-kit_components_read-only-field_properties_min-height)` to ensure a side-by-side editable `TextField` and `ReadOnlyField` perfectly share roughly identical visual heights.
48
-
49
- ## 2. Unidirectional Editable Mode
50
-
51
- The main wrapper intercepts `readOnly` boolean blocks, maintaining its own `isReadOnly` state. Natively, if a user clicks an exposed 'Edit' action (like our legacy SVG or custom `labelActionArea`), the context permanently switches to active.
52
- There is intentionally no built-in reverse toggle inside typical field bindings (like input "Blur") to revert state. Parents must pass external controls to `readOnly` forcing the internal hooks to reset via standard `useEffect` propagation.
44
+ If an edit action (e.g. via `labelActionArea`) is used to let a user switch a field from read-only to editable, that switch is one-directional in the UI — there is no built-in control (such as blurring the input) that switches it back to read-only. To revert to read-only, the parent must pass an updated `readOnly` value.
@@ -39,18 +39,4 @@ All Recursica components in the `@recursica/mantine-adapter` package adhere stri
39
39
 
40
40
  ## 4. Key Integration Features & Constraints
41
41
 
42
- ## 1. Stripping Mantine's Native Variants and Sizes
43
-
44
- The Figma design tokens for the SegmentedControl component do not define nested layers of variants (such as `solid`, `outline`) or specific sizing steps (`xs`, `sm`, etc.). They are defined globally. Therefore, we explicitly `Omit` the standard `variant`, `size`, `radius`, and `color` props from the generic Mantine `SegmentedControlProps` interface.
45
-
46
- ## 2. Hardcoded Overrides for Figma Strictness
47
-
48
- Mantine injects inline hover styles on `.label` (specifically, adding a subtle gray background when hovering). Since Recursica defines explicit transparent or specifically-driven hover states, we use `!important` tags within `SegmentedControl.module.css` for background and typography overriding.
49
-
50
- ## 3. Divider Separators
51
-
52
- Mantine uses an `::before` pseudo-element on the `.control` block to draw standard visual separators between adjacent elements. Instead of stripping this functionality out, we hook directly into the pseudo-element and override its `background-color` with `--recursica_ui-kit_components_segmented-control_properties_colors_divider-color`.
53
-
54
- ## 4. Indicator Mapping
55
-
56
- 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.
42
+ The `variant`, `size`, `radius`, and `color` props are not available on this component, since appearance is fully controlled by the design system tokens. The active segment is shown as a floating indicator that moves behind the selected label, with a divider rendered between adjacent segments.
@@ -39,48 +39,4 @@ All Recursica components in the `@recursica/mantine-adapter` package adhere stri
39
39
 
40
40
  ## 4. Key Integration Features & Constraints
41
41
 
42
- ## 1. Bidirectional State Synchronization
43
-
44
- **Decision:** Maintain a highly responsive, bidirectional connection between the sliding track value and the adjacent numeric text input.
45
- **Implementation:**
46
-
47
- - The slider track requires a clean `number` state, whereas the text box requires a `string` state (`inputValue`) to allow typing intermediate characters like decimals (`2.`), negative signs (`-`), or empty text without breaking standard React input binding.
48
- - A `useEffect` hook continuously feeds the outer numeric state changes back into the text input value as a string representation.
49
- - Input changes instantly parsed as float clamp bounds securely. On blur (`onBlur`), the input state is automatically sanitized and reset to the clean, clamped string representation of the final track value.
50
-
51
- ## 2. Outer Form Control Wrapper Integration
52
-
53
- **Decision:** Bypass Mantine's native `Input.Wrapper` and `label` properties.
54
- **Implementation:**
55
-
56
- - Universal form wrappers like `<FormControlWrapper>` and `<WithReadOnlyWrapper>` handle the outer layout architecture, including labels, assistive text, error states, and optional edit-triggering fields.
57
- - Therefore, we map the outer form label directly to the `label` property of the `Slider` (delegated to the wrapper), and rename Mantine's internal dragging tooltip label property to `tooltipLabel`.
58
-
59
- ## 3. Custom Min-Max Labels and Step Indicators
60
-
61
- **Decision:** Enforce rigid typography tokens on lower bounds and custom mark indicators.
62
- **Implementation:**
63
-
64
- - Mantine's native mark and step structures are fully styles-mapped back to our scoped variables in `Slider.module.css`.
65
- - Min and Max numeric guides are rendered directly to the left and right of the slider track, centered vertically and spaced automatically using standard input gaps, while dynamically fetching custom typography tokens for min-max labels to avoid hardcoded formatting constraints.
66
-
67
- ## 4. Visual Overrides for Stacked and Side-by-Side Spacing
68
-
69
- **Decision:** Enforce layout margins dynamically based on container orientation parameters.
70
- **Implementation:**
71
-
72
- - Using custom layouts (e.g. `stacked` and `side-by-side`), we override the margins by assigning the component-specific Figma spacing variables to the unified `--form-control-margin-bottom` property.
73
-
74
- ## 5. Right-Aligned Floating Current Value
75
-
76
- **Decision:** Position the current active value of the slider directly above the max guide (or right-side element) on the right side of the track.
77
- **Implementation:**
78
-
79
- - Wrap the max guide element in a relative layout container (`.rightGuideContainer`) to provide a positioning anchor.
80
- - Place the active value element (`.currentValue`) inside `.rightGuideContainer` and position it absolutely (`bottom: calc(100% + var(--recursica_ui-kit_globals_form_properties_label-field-gap-vertical, 8px))`, `right: 0`).
81
- - This absolute positioning strategy guarantees that the active value floats cleanly above the track's right side, while aligning it vertically on the Y-axis to sit in perfect baseline alignment with the component's left-aligned form label.
82
- - Set typography using the Figma-aligned component-specific read-only value variables (`--recursica_ui-kit_components_slider_properties_read-only-value_...`).
83
- - Allow the text color of both the floating current value (`.currentValue`) and the component's read-only value (`.readOnlyValue`) to naturally inherit from their parent states/form globals, automatically supporting default (`--form-field-text-valued`), disabled (`--form-field-disabled-text`), and error colors without explicit color overrides, matching the min/max guides.
84
- - If `showInput` is enabled, the floating `.currentValue` is hidden since the active value is already displayed and editable within the adjacent numeric text input, avoiding visual redundancy.
85
- - In `side-by-side` form layouts, the `.currentValue` is positioned inline (static positioning) to the right of the max label rather than floating above it, centered vertically with the max label and aligned right to the container. This uses CSS flexbox ordering (`order: 2` for `.currentValue` and `order: 1` for `.minMaxGuide`) to visually swap their positions while preserving clean, semantic DOM ordering.
86
- - To ensure perfect, pixel-perfect vertical track alignment between sliders that show numeric text inputs (`showInput={true}`) and sliders that display the active inline value (`showInput={false}`), the `.currentValue` element is globally given a width equal to the input width (`var(--recursica_ui-kit_components_slider_properties_input-width)`) and right-aligned (`text-align: right`). In `side-by-side` layouts, `.rightGuideContainer`'s flex gap is also matched to the horizontal input-to-track gap (`var(--recursica_ui-kit_components_slider_properties_input-gap)`), making the horizontal space occupied by the rightmost elements exactly identical in both component modes.
42
+ The `label` prop is passed through to the surrounding form label rather than Mantine's dragging tooltip; use `tooltipLabel` to set the label shown while dragging. When `showInput` is enabled, a numeric text input is rendered alongside the track and stays in sync with the slider's value. Set `showMinMaxLabels` to `false` to hide the min/max guides shown at either end of the track. Otherwise, the current value is displayed near the track instead.
@@ -45,4 +45,4 @@ All Recursica components in the `@recursica/mantine-adapter` package adhere stri
45
45
  ## 4. Key Integration Features & Constraints
46
46
 
47
47
  The `Stack` component is a generic flex layout wrapper mapped directly to Mantine's `Stack`.
48
- It currently does not require any custom logical layouts or CSS workarounds since it serves only to organize layout structure, and doesn't enforce any strict design-system token styling itself. All gap, align, and justify properties pass safely through via the `filterStylingProps` layout-property allowance.
48
+ `gap`, `align`, and `justify` all pass through as normal.
@@ -39,27 +39,4 @@ All Recursica components in the `@recursica/mantine-adapter` package adhere stri
39
39
 
40
40
  ## 4. Key Integration Features & Constraints
41
41
 
42
- ## 1. Stripping Mantine's Size Engine
43
-
44
- Mantine uses properties like `size`, `color`, and `radius` to dynamically map CSS layout values across its `.track` and `.thumb` nodes. We proactively strip and delete these properties using `filterStylingProps` to entirely neutralize this native behavior.
45
-
46
- ## 2. Hardcoded Values & Transitions
47
-
48
- Mantine injects dynamic width/height attributes into its switch through inline CSS variables (e.g. `--switch-height`). To strictly enforce the UI Kit tokens without breaking Mantine's internal math, our `Switch.module.css` structurally remaps Mantine's internal variables explicitly:
49
-
50
- ```css
51
- --switch-width: var(--switch-track-width);
52
- --switch-height: calc(
53
- var(--switch-thumb-height) + (var(--switch-track-padding) * 2)
54
- );
55
- ```
56
-
57
- We also hardcode `border: none` since the UI kit designs rely purely on box-shadow elevations and background color tracking. Mantine’s default border logic is entirely disabled.
58
-
59
- ## 3. ReadOnly Behavior
60
-
61
- Similar to `Checkbox`, the `Switch` component handles `readOnly` presentation by dropping the entire underlying node tree and falling back structurally onto `<FormControlWrapper>` when `readOnly: true`. This strictly preserves exact baseline alignment across all primitives without trying to hack disabled CSS to look like read-only text.
62
-
63
- ## 4. Hover State Reset
64
-
65
- Mantine forcefully triggers track hover color states globally. Since Recursica currently does not map specific hover states to switch backgrounds across themes (falling back to standard unselected tokens or simply providing a cursor), we structurally wipe out Mantine's `.track:hover` class block inside `Switch.module.css`.
42
+ The `size`, `color`, and `radius` props are not available on this component, since appearance is fully controlled by the design system tokens. When `readOnly` is set, the switch renders as a read-only label instead of an interactive control. There is currently no distinct hover color for the track; it falls back to the standard unselected background.
@@ -43,6 +43,6 @@ All Recursica components in the `@recursica/mantine-adapter` package adhere stri
43
43
 
44
44
  The `TextArea` component is mapped explicitly to Mantine's `<Textarea>` following the same strict encapsulation rules as `TextField`.
45
45
 
46
- 1. **Naked Primitive Mapping:** Mantine's `Textarea` natively executes macro-label generation. To decouple it, we explicitly disable internal labels (`label={undefined}`) and inject it purely inside our generic `FormControlWrapper`.
47
- 2. **Text Field Token Re-Use:** Because text areas fundamentally share the same box-geometry, text, and state definitions as single-line inputs, it strictly implements the `--recursica_ui-kit_components_text-field_...` variables natively.
46
+ 1. **Label Rendering:** The label is rendered via the surrounding form label, not Mantine's built-in label.
47
+ 2. **Visual Styling:** `TextArea` shares the same visual styling (geometry, typography, and state colors) as `TextField`.
48
48
  3. **Autosize Handling:** The component supports Mantine's raw `autosize`, `minRows`, and `maxRows` parameters out of the box dynamically via property passthrough.
@@ -41,20 +41,4 @@ All Recursica components in the `@recursica/mantine-adapter` package adhere stri
41
41
 
42
42
  ## 4. Key Integration Features & Constraints
43
43
 
44
- ## Architectural philosophy
45
-
46
- The `TextField` primitive intentionally ignores Mantine's built-in `<TextInput>` structure in favor of directly binding against `<Input>`.
47
-
48
- ### 1. Bypassing `Input.Wrapper`
49
-
50
- Because `<TextInput>` inherently renders Mantine's `Input.Wrapper` underneath the hood, utilizing it natively double-wraps our layouts causing massive DOM bloat and margin collapsing errors. By leveraging the completely naked `<Input>` primitive natively natively alongside `FormControlWrapper`, we retain full logical control of where the `label` maps, avoiding dual label conflicts or mis-aligned asterisks securely.
51
-
52
- ### 2. State Hooks (`wrapperProps`)
53
-
54
- Because we target `<Input>`, all dynamic UI modifiers (`disabled`, `error`, etc) must strictly be applied to the `.root` CSS module to correctly style the internal `<input>` boxes AND the nested `.[data-position]` icon sections simultaneously.
55
-
56
- To accomplish this safely without spilling random pseudo-variables onto the input parameters, we exclusively use Mantine's `<Input wrapperProps={{...}}>` block natively locking `.root[data-error]` to correctly map Recursica UI variable hooks without breaking the DOM hierarchy natively.
57
-
58
- ### 3. Component Definition Intersections
59
-
60
- To strictly acquire native HTML typings, `TextField.tsx` explicitly extracts `React.ComponentPropsWithoutRef<"input">`. However, to prevent Typescript strict union collisions with overlapping Mantine definitions over functional `style` components and custom mappings natively, we meticulously omit (`Pick<InputWrapperProps, ...>`) explicit base elements to guarantee that strictly Recursica styles traverse the array flawlessly.
44
+ The label, assistive text, and error state are rendered via the surrounding form label rather than Mantine's built-in label, so there is a single, consistently positioned label and asterisk for the field.
@@ -0,0 +1,72 @@
1
+ # TimePicker Implementation Notes
2
+
3
+ ## Architecture Overview
4
+
5
+ `TimePicker` is a composite of two independently-styled controls sitting side by side in a flex row:
6
+
7
+ 1. **The time field** — Mantine's own `TimePicker` component (not `TimeInput` — a separate, more advanced component in the same package with real segmented hour/minute/second masking), always rendered with `format="12h"`.
8
+ 2. **The AM/PM selector** — `BareDropdown`, a headless variant of this adapter's own `Dropdown` component (see below), not a native `<select>`.
9
+
10
+ This shape is deliberate and **not configurable** — there is no prop to get a plain 24-hour input or to hide the AM/PM control. Every consumer gets the same 12-hour + Dropdown-styled-AM/PM composite.
11
+
12
+ ## Why not just Mantine's `TimePicker` alone
13
+
14
+ Mantine's `TimePicker` with `format="12h"` already renders hour/minute masking **and** an AM/PM control together — but that control is a native HTML `<select>` (`AmPmInput`, rendered via `format === "12h" && <AmPmInput />` in Mantine's own source, unconditionally bundled with 12h format — there's no prop to get one without the other). A native `<select>`'s styling is limited, especially its open option list (browser/OS-rendered), so it doesn't visually match Recursica's `Dropdown` component. (Matt Massey, 2026-08-07, after reviewing the initial native-select version in Storybook.)
15
+
16
+ Resolution: keep Mantine's `TimePicker` for what it does well (real, accessible 1-12 hour + minute + second masking with proper keyboard navigation — not something worth rebuilding from scratch), **CSS-hide its bundled native AM/PM `<select>`** (`.timeWrapper :global([data-am-pm]) { display: none; }`), and drive AM/PM entirely through our own `BareDropdown` next to it.
17
+
18
+ ## `BareDropdown`
19
+
20
+ A new, **internal-only** component in `../Dropdown/BareDropdown.tsx` — not exported from this folder's `index.ts` (there isn't one; the barrel export is at the package level and only references `Dropdown`, not `BareDropdown`). It's the same `MantineSelect` primitive `Dropdown` wraps, styled via the **same** `Dropdown.module.css` classes (`wrapper`/`input`/`section`/`dropdown`/`option`), but with no `FormControlWrapper`/`WithReadOnlyWrapper` and no label/assistiveText/error/required props.
21
+
22
+ **Why not the public `Dropdown` component directly**: `Dropdown` wraps its own `FormControlWrapper`/`FormControlLayout` internally (it's meant to be used standalone). Nesting the full `Dropdown` inside `TimePicker` — which is already `FormControlWrapper`-wrapped — would double up `FormControl`/`FormControlLayout` structure. `BareDropdown` is the headless escape hatch for exactly this situation: reuse `Dropdown`'s visual styling without its structural wrapping. (Matt Massey, 2026-08-07.)
23
+
24
+ ## Internal state, unlike every other component here
25
+
26
+ Every other input in this adapter is a thin pass-through — the wrapped library component owns all interaction state. `TimePicker` is the exception: the time field and the AM/PM `BareDropdown` both mutate the _same_ conceptual value, so something has to reconcile them. `TimePicker.tsx` keeps a `useState<string | undefined>` for the full 24-hour `"HH:mm[:ss]"` value, seeded from `value`/`defaultValue`, synced when `value` changes (controlled usage), and updated by both controls (the field's own `onChange`, and the `BareDropdown`'s `onChange` — which just adds or subtracts 12 from the current hour). The public `value`/`onChange` API is unaffected — still the same plain string convention as always.
27
+
28
+ ## Hidden/omitted native props
29
+
30
+ Because the AM/PM behavior is fixed, several of Mantine's `TimePicker` props are **omitted from the public API** rather than exposed and silently ignored — a consumer shouldn't be able to pass a prop that looks like it should do something (change format, relabel the native AM/PM control) when it can't:
31
+
32
+ - `format`, `min`, `max` — hardcoded/remapped internally (`format` always `"12h"`; `min`/`max` come from `minTime`/`maxTime` instead, matching the shared `RecursicaTimePickerProps` contract).
33
+ - `amPmInputLabel`, `amPmLabels`, `amPmSelectProps`, `amPmRef` — all control the native AM/PM `<select>`, which is now CSS-hidden and non-interactive. Exposing these would be misleading.
34
+ - `withDropdown`, `presets`, `maxDropdownContentHeight`, `scrollAreaProps`, `reverseTimeControlsList`, `popoverProps` — the optional time-presets popover feature isn't wired up; kept out of the public API to keep it minimal.
35
+
36
+ ## Design tokens
37
+
38
+ - No dedicated `min-height` token exists for `time-picker` (unlike `text-field`/`date-picker`) — `.fieldsGroup`'s explicit `height` is derived from `text_line-height` instead, which also fixes a real bug: Mantine's own field CSS sets `height: 100%` on every field to fill `.fieldsGroup`, and a percentage height only resolves against a _concrete_ parent height. Before this, `.fieldsGroup` was auto-height (padding only), so `100%` resolved to `0` — this is why the AM/PM control was invisible in early builds (verified via real headless-browser inspection, not just markup checks — a raw server-render test had missed this entirely since it doesn't compute layout).
39
+ - `icon-size`/`icon-color`/`icon-text-gap`/`placeholder-opacity` are exempted (`recursica-ignore`) — the time field has no icon slot and no native `::placeholder` pseudo-element to target (each `SpinInput`'s `"--"` placeholder is styled internally by Mantine).
40
+ - The AM/PM `BareDropdown` draws its own border/background/padding from `Dropdown`'s own tokens via `Dropdown.module.css` — it does not reuse any `time-picker` tokens.
41
+
42
+ ## Read-Only Implementation
43
+
44
+ `readOnlyType="text"`, matching `DatePicker`'s convention.
45
+
46
+ ## Visual review fixes (Matt Massey, 2026-08-07)
47
+
48
+ Follow-up fixes after the first Storybook review of the `BareDropdown`-based rebuild:
49
+
50
+ - **Outer box was ~62px tall instead of matching `TextField`'s 48px**: Mantine's `TimePicker` renders its `classNames.input` slot as a real, separately-styled box (its own border, background, and `min-height: var(--input-height)` = 36px by default) nested _inside_ `classNames.wrapper` — not a plain content container. Left alone, that gave every field a second, competing Mantine-default border/background stacked underneath our own `.timeWrapper` chrome. Fixed by resetting `.timeInput` (mapped to `classNames.input`) to a bare, unstyled flex shell (`border: none; background-color: transparent; padding: 0; height: auto; min-height: 0;`) — every visual property now lives on `.timeWrapper` alone. Confirmed by reading Mantine's `Input.mjs`/`use-input-props.mjs` source directly, not just trial-and-error CSS.
51
+ - **No dedicated `min-height` token for `time-picker`** (unlike `text-field`): `.timeWrapper` now sets `min-height: var(--recursica_ui-kit_globals_form_field_size_single-line-input-height)` — the same global `text-field`'s own `min-height` token itself resolves to (confirmed via `recursica_variables_scoped.css`), so both land on exactly 48px without binding to another component's own namespace.
52
+ - **No dedicated gap token between the time field and the AM/PM dropdown**: checked `ui-kit.globals.form.properties` — the closest candidates (`label-field-gap-horizontal`, `vertical-item-gap`) are for label-to-field and vertical stacking, not a horizontal gap between two sibling controls. `.root`'s flex `gap` continues to reuse `time-picker`'s own `horizontal-padding` token as a stand-in until a real token exists.
53
+ - **AM/PM `BareDropdown` was as wide as the time field**: two stacked causes, both now fixed:
54
+ 1. A plain `style={{ width: "fit-content" }}` prop doesn't reach the bordered box at all — Mantine's `useInputProps` routes a top-level `style` prop to the _label_ `InputWrapper`, not the input box. Fixed by using the styles-api `styles={{ wrapper: { width: "fit-content" } }}` prop instead, which targets that box directly.
55
+ 2. Even with that fixed, the box stayed wide: `Dropdown.module.css`'s `.input` sets `width: 100%`, and a native `<input>`'s own intrinsic/`auto` width is a fixed browser default (~20 characters), not based on its actual "AM"/"PM" value text, so resetting to `auto` alone didn't shrink it either. Fixed with an explicit small width (`.amPmSelect :global(.mantine-Select-input) { width: 2.5rem; }`, targeting Mantine's stable global class since this module can't reference `Dropdown.module.css`'s own hashed class name) — the wrapper's rendered width just follows this input, since `Dropdown.module.css`'s right-section icon is positioned absolutely and doesn't add to flex flow.
56
+
57
+ ## Visual review round 2 (Matt Massey, 2026-08-08)
58
+
59
+ - **AM/PM box was too tight — 2.5rem left no room for the "AM"/"PM" text or breathing room around the chevron**: `2.5rem` (40px) is a `box-sizing: border-box` width, so it has to fit Dropdown's own left padding (16px) _and_ its right padding reserved for the chevron section (48px, `horizontal-padding + icon-size + icon-text-gap`) inside it — that's 64px of padding alone, more than the 40px box, leaving a _negative_ content area (hence the invisible value and the chevron crowding the border). Widened to `6rem` (96px), leaving a real ~32px for the text.
60
+
61
+ ## Visual review round 3 (Matt Massey, 2026-08-08) — AM/PM value now defaults and persists
62
+
63
+ Previously flagged as a known limitation, now fixed: Mantine's own `TimePicker` only reports a valid `onChange` value once its internal `amPm` state is non-`null` (see `getTimeString` in `@mantine/dates`) — that state is normally set by interacting with the native AM/PM `<select>`, which this component CSS-hides entirely. In practice, typing hour/minute digits alone never fired a valid `onChange`, so the AM/PM `BareDropdown` stayed blank forever and selecting a value there did nothing (its own `handleMeridiemChange` logic bailed out early, since `hour` was always `undefined`).
64
+
65
+ **Fix**: `convertTimeTo12HourFormat` in `@mantine/dates` derives `amPm` from whatever `hours` value it's given — `null` only when `hours` itself is `null`. And `onAmPmChange` (Mantine's internal handler for the — hidden — native select) calls `setAmPm(value)` _unconditionally_, before any validity check. So simulating one real interaction with that native `<select>` — defaulting it to "AM" — permanently seeds Mantine's internal `amPm` state, after which typing hour/minute correctly resolves and reports a real value, and our own `BareDropdown` (which drives the same native select the same way) correctly displays and changes it.
66
+
67
+ Implementation: a `useEffect` on mount, gated to only run when there's no real initial `value`/`defaultValue` already (Mantine already derives the correct AM/PM from a real value on its own — this only fixes the genuinely-empty-start case). It sets the native select's `.value` via `Object.getOwnPropertyDescriptor(HTMLSelectElement.prototype, "value").set` (required to make a React-controlled element pick up a value set outside of React) and dispatches a `change` event — access to the native element comes via `amPmRef`, a prop omitted from this component's own public API but still usable internally when calling Mantine's `<TimePicker>` directly.
68
+
69
+ ## Visual review round 5 (Matt Massey, 2026-08-08)
70
+
71
+ - **AM/PM error-state border wasn't changing**: `BareDropdown` set `data-error`/`data-disabled` via Mantine's `wrapperProps` — which targets the _outer_ `Input.Wrapper` (the label/description/error stacking element), a different, ancestor element from the "wrapper" styles-api slot that actually carries `styles.root`'s border. `Dropdown.module.css`'s `.root[data-error]`/`[data-disabled]` rules never matched as a result. This is the exact same "two different things both called 'wrapper'" trap as the earlier `style` vs `styles.wrapper` bug. Fixed by using `attributes={{ wrapper: {...} }}` instead — the styles-api hook that actually targets the same slot as `classNames.wrapper`. **This is a shared, pre-existing bug** — the real `Dropdown.tsx` had the identical mistake, so its error/disabled states never applied a border color either; fixed there too (low-risk, purely-additive, same reasoning as the `data-selected` fix above).
72
+ - **Static/Editable ReadOnly showed the raw 24-hour value with no AM/PM** (e.g. "14:30" instead of "2:30 PM"): `readOnlyValue` was passed the raw internal string as-is. Added `formatReadOnlyTime`, converting to a 12-hour + AM/PM display string before handing it to `WithReadOnlyWrapper`.
@@ -1,44 +1,228 @@
1
- /*
2
- * EXEMPTIONS:
3
- * - TimePicker is a "Coming Soon" stub component.
4
- * - It renders a placeholder and has no active implementation or CSS stylesheet yet.
5
- * - The following tokens generated by Figma are ignored until the component is fully built.
6
- */
7
-
8
- /* recursica-ignore: --recursica_ui-kit_components_time-picker_properties_border-radius */
9
- /* recursica-ignore: --recursica_ui-kit_components_time-picker_properties_horizontal-padding */
1
+ /* EXEMPTIONS:
2
+ - state-specific border-size variables are ignored because a uniform border-size is applied
3
+ globally to prevent unexpected layout shift or flickering during disabled/error transitions.
4
+ - icon-size/icon-color/icon-text-gap have no equivalent here: the time field has no icon slot,
5
+ unlike TextField/DatePicker.
6
+ - placeholder-opacity has no direct equivalent: each SpinInput's native "--" placeholder is styled
7
+ by Mantine's own SpinInput internals, not a ::placeholder pseudo-element we can target directly. */
8
+ /* recursica-ignore: --recursica_ui-kit_components_time-picker_variants_states_disabled_properties_border-size */
9
+ /* recursica-ignore: --recursica_ui-kit_components_time-picker_variants_states_error_properties_border-size */
10
10
  /* recursica-ignore: --recursica_ui-kit_components_time-picker_properties_icon-size */
11
11
  /* recursica-ignore: --recursica_ui-kit_components_time-picker_properties_icon-text-gap */
12
12
  /* recursica-ignore: --recursica_ui-kit_components_time-picker_properties_placeholder-opacity */
13
- /* recursica-ignore: --recursica_ui-kit_components_time-picker_properties_text_font-family */
14
- /* recursica-ignore: --recursica_ui-kit_components_time-picker_properties_text_font-size */
15
- /* recursica-ignore: --recursica_ui-kit_components_time-picker_properties_text_font-style */
16
- /* recursica-ignore: --recursica_ui-kit_components_time-picker_properties_text_font-weight */
17
- /* recursica-ignore: --recursica_ui-kit_components_time-picker_properties_text_letter-spacing */
18
- /* recursica-ignore: --recursica_ui-kit_components_time-picker_properties_text_line-height */
19
- /* recursica-ignore: --recursica_ui-kit_components_time-picker_properties_text_text-decoration */
20
- /* recursica-ignore: --recursica_ui-kit_components_time-picker_properties_text_text-transform */
21
- /* recursica-ignore: --recursica_ui-kit_components_time-picker_properties_vertical-padding */
22
- /* recursica-ignore: --recursica_ui-kit_components_time-picker_properties_width */
23
- /* recursica-ignore: --recursica_ui-kit_components_time-picker_variants_layouts_side-by-side_properties_top-bottom-margin */
24
- /* recursica-ignore: --recursica_ui-kit_components_time-picker_variants_layouts_stacked_properties_top-bottom-margin */
25
- /* recursica-ignore: --recursica_ui-kit_components_time-picker_variants_states_default_properties_border-size */
26
- /* recursica-ignore: --recursica_ui-kit_components_time-picker_variants_states_disabled_properties_border-size */
27
- /* recursica-ignore: --recursica_ui-kit_components_time-picker_variants_states_error_properties_border-size */
28
- /* recursica-ignore: --recursica_ui-kit_components_time-picker_variants_states_focus_properties_border-size */
29
- /* recursica-ignore: --recursica_ui-kit_components_time-picker_variants_states_default_properties_colors_background */
30
- /* recursica-ignore: --recursica_ui-kit_components_time-picker_variants_states_default_properties_colors_border-color */
31
- /* recursica-ignore: --recursica_ui-kit_components_time-picker_variants_states_default_properties_colors_icon */
32
- /* recursica-ignore: --recursica_ui-kit_components_time-picker_variants_states_default_properties_colors_text */
33
- /* recursica-ignore: --recursica_ui-kit_components_time-picker_variants_states_disabled_properties_colors_background */
34
- /* recursica-ignore: --recursica_ui-kit_components_time-picker_variants_states_disabled_properties_colors_border-color */
35
- /* recursica-ignore: --recursica_ui-kit_components_time-picker_variants_states_disabled_properties_colors_icon */
36
- /* recursica-ignore: --recursica_ui-kit_components_time-picker_variants_states_disabled_properties_colors_text */
37
- /* recursica-ignore: --recursica_ui-kit_components_time-picker_variants_states_error_properties_colors_background */
38
- /* recursica-ignore: --recursica_ui-kit_components_time-picker_variants_states_error_properties_colors_border-color */
39
- /* recursica-ignore: --recursica_ui-kit_components_time-picker_variants_states_error_properties_colors_icon */
40
- /* recursica-ignore: --recursica_ui-kit_components_time-picker_variants_states_error_properties_colors_text */
41
- /* recursica-ignore: --recursica_ui-kit_components_time-picker_variants_states_focus_properties_colors_background */
42
- /* recursica-ignore: --recursica_ui-kit_components_time-picker_variants_states_focus_properties_colors_border-color */
43
- /* recursica-ignore: --recursica_ui-kit_components_time-picker_variants_states_focus_properties_colors_icon */
44
- /* recursica-ignore: --recursica_ui-kit_components_time-picker_variants_states_focus_properties_colors_text */
13
+ /* recursica-ignore: --recursica_ui-kit_components_time-picker_properties_colors_icon-color */
14
+ /* recursica-ignore: --recursica_ui-kit_components_time-picker_variants_states_disabled_properties_colors_icon-color */
15
+ /* recursica-ignore: --recursica_ui-kit_components_time-picker_variants_states_error_properties_colors_icon-color */
16
+
17
+ /* LAYOUT SPACING OVERRIDES:
18
+ - Sets the --form-control-margin-bottom spacing hook to map component-specific layout tokens. */
19
+ .layoutOverride {
20
+ --form-control-margin-bottom: var(
21
+ --recursica_ui-kit_components_time-picker_variants_layouts_stacked_properties_top-bottom-margin
22
+ );
23
+ }
24
+
25
+ .layoutOverride[data-form-layout="side-by-side"] {
26
+ --form-control-margin-bottom: var(
27
+ --recursica_ui-kit_components_time-picker_variants_layouts_side-by-side_properties_top-bottom-margin
28
+ );
29
+ }
30
+
31
+ /* HARDCODED VALUES:
32
+ - border-style: solid. Native structural rendering rule.
33
+ - display: flex on .root — the time field and the AM/PM BareDropdown are two visually separate
34
+ controls sitting side by side, not merged into one shared box. See
35
+ TIMEPICKER_IMPLEMENTATION_NOTES.md for why this composite exists.
36
+ */
37
+
38
+ .root {
39
+ display: flex;
40
+ align-items: flex-start;
41
+ gap: var(
42
+ --recursica_ui-kit_components_time-picker_properties_horizontal-padding
43
+ );
44
+ width: fit-content;
45
+ }
46
+
47
+ /* Mantine's own native AM/PM <select> is unconditionally bundled with format="12h" (no prop to
48
+ omit it) — hide it; our own BareDropdown next to the field is the only AM/PM control. */
49
+ .timeWrapper :global([data-am-pm]) {
50
+ display: none;
51
+ }
52
+
53
+ .timeWrapper {
54
+ display: flex;
55
+ align-items: center;
56
+ position: relative;
57
+ box-sizing: border-box;
58
+ /* min-width, not width: the `width` token was authored for a single plain input (confirmed against
59
+ text-field's identical token shape); this is now only the time-entry half of the composite. */
60
+ min-width: var(--recursica_ui-kit_components_time-picker_properties_width);
61
+ width: fit-content;
62
+ /* time-picker has no dedicated min-height token (unlike text-field/date-picker); this global is
63
+ the one text-field's own min-height token resolves to, so both controls land on the same 48px
64
+ height without binding to another component's own namespace. See
65
+ TIMEPICKER_IMPLEMENTATION_NOTES.md. */
66
+ min-height: var(
67
+ --recursica_ui-kit_globals_form_field_size_single-line-input-height
68
+ );
69
+ border-radius: var(
70
+ --recursica_ui-kit_components_time-picker_properties_border-radius
71
+ );
72
+ border-width: var(
73
+ --recursica_ui-kit_components_time-picker_properties_border-size
74
+ );
75
+ border-style: solid;
76
+ border-color: var(
77
+ --recursica_ui-kit_components_time-picker_properties_colors_border-color
78
+ );
79
+ background-color: var(
80
+ --recursica_ui-kit_components_time-picker_properties_colors_background-color
81
+ );
82
+ color: var(
83
+ --recursica_ui-kit_components_time-picker_properties_colors_text-color
84
+ );
85
+ padding-top: var(
86
+ --recursica_ui-kit_components_time-picker_properties_vertical-padding
87
+ );
88
+ padding-bottom: var(
89
+ --recursica_ui-kit_components_time-picker_properties_vertical-padding
90
+ );
91
+ padding-left: var(
92
+ --recursica_ui-kit_components_time-picker_properties_horizontal-padding
93
+ );
94
+ padding-right: var(
95
+ --recursica_ui-kit_components_time-picker_properties_horizontal-padding
96
+ );
97
+ }
98
+
99
+ /* Mantine's TimePicker renders its own "input" slot as a real, separately-styled box (border,
100
+ background, height/min-height, padding-inline) nested *inside* .timeWrapper — not just a plain
101
+ content container. Left alone, that gave every field its own competing Mantine-default border/
102
+ background/36px-min-height stacked underneath our own .timeWrapper chrome, which is what made
103
+ the outer box look far taller than intended. Reset it to a plain, unstyled flex shell; every
104
+ visual property (border, background, padding, height) is owned by .timeWrapper alone. */
105
+ .timeInput {
106
+ height: auto;
107
+ min-height: 0;
108
+ border: none;
109
+ background-color: transparent;
110
+ padding: 0;
111
+ display: block;
112
+ }
113
+
114
+ /* Padding lives on .timeWrapper, not .fieldsGroup: Mantine's own field styles set `height: 100%` on
115
+ every field (hour/minute/second SpinInputs) to fill their direct parent (.fieldsGroup). A
116
+ percentage height only resolves against a parent with a *concrete* height — giving .fieldsGroup
117
+ an explicit height (derived from time-picker's own text_line-height token, since there's no
118
+ dedicated min-height token for this component, unlike text-field/date-picker) avoids that
119
+ collapsing to zero, and doubles as the "min-height" this component doesn't have a token for. */
120
+ .fieldsGroup {
121
+ display: flex;
122
+ align-items: center;
123
+ height: var(
124
+ --recursica_ui-kit_components_time-picker_properties_text_line-height
125
+ );
126
+ }
127
+
128
+ .timeField {
129
+ font-family: var(
130
+ --recursica_ui-kit_components_time-picker_properties_text_font-family
131
+ );
132
+ font-size: var(
133
+ --recursica_ui-kit_components_time-picker_properties_text_font-size
134
+ );
135
+ font-style: var(
136
+ --recursica_ui-kit_components_time-picker_properties_text_font-style
137
+ );
138
+ font-weight: var(
139
+ --recursica_ui-kit_components_time-picker_properties_text_font-weight
140
+ );
141
+ letter-spacing: var(
142
+ --recursica_ui-kit_components_time-picker_properties_text_letter-spacing
143
+ );
144
+ line-height: var(
145
+ --recursica_ui-kit_components_time-picker_properties_text_line-height
146
+ );
147
+ text-decoration: var(
148
+ --recursica_ui-kit_components_time-picker_properties_text_text-decoration
149
+ );
150
+ text-transform: var(
151
+ --recursica_ui-kit_components_time-picker_properties_text_text-transform
152
+ );
153
+ color: inherit;
154
+ outline: none;
155
+ }
156
+
157
+ /* The AM/PM BareDropdown reuses Dropdown.module.css's own border/background/padding entirely —
158
+ this only controls its size/alignment within the flex row. */
159
+ .amPmSelect {
160
+ flex: 0 0 auto;
161
+ width: auto;
162
+ min-width: 5.5rem;
163
+ }
164
+
165
+ /* Dropdown.module.css's own .input sets width: 100% (correct for a standalone Dropdown filling
166
+ its own column) — resetting that to `auto` still isn't enough here: a native <input>'s intrinsic
167
+ ("auto"/"fit-content") width is a fixed UA default (~20 characters wide), not based on its actual
168
+ value text, so it doesn't naturally shrink to "AM"/"PM" either way. Give it an explicit small
169
+ width sized for a couple of characters instead — the wrapper (.amPmSelect, whose own rendered
170
+ width just follows this input, since Dropdown.module.css's .section icon is positioned
171
+ absolutely and doesn't add to flex flow) ends up hugging the content as a result. Target
172
+ Mantine's own stable global class (not the Dropdown.module.css hash, which this module can't
173
+ reference directly). See TIMEPICKER_IMPLEMENTATION_NOTES.md.
174
+ Width math (box-sizing: border-box, so padding eats into this number): Dropdown's own
175
+ horizontal-padding (16px) on the left + the chevron's reserved right-section padding
176
+ (horizontal-padding + icon-size + icon-text-gap = 16+24+8 = 48px) on the right leaves very
177
+ little room for the "AM"/"PM" text itself — 2.5rem (40px) put the padding in a negative content
178
+ box, which is why the value was invisible and the chevron looked jammed against the border. 6rem
179
+ leaves a real ~32px content area. */
180
+ .amPmSelect :global(.mantine-Select-input) {
181
+ width: 6rem;
182
+ }
183
+
184
+ /* -------------------------------------
185
+ STATE CASCADE ARCHITECTURE
186
+ -------------------------------------- */
187
+
188
+ /* Focus State Mapping — applied to the time field only (the BareDropdown has its own via Dropdown's
189
+ own CSS module). */
190
+ .timeField:focus-within,
191
+ .timeWrapper:focus-within {
192
+ box-shadow:
193
+ 0 0 0 var(--recursica_brand_states_focus_border-size)
194
+ var(--recursica_brand_states_focus_color),
195
+ 0 0 var(--recursica_brand_states_focus_blur)
196
+ var(--recursica_brand_states_focus_margin)
197
+ var(--recursica_brand_states_focus_color);
198
+ }
199
+
200
+ /* Error State Mapping (Propagated strictly down from the wrapper DOM context) */
201
+ .root[data-error="true"] .timeWrapper {
202
+ border-color: var(
203
+ --recursica_ui-kit_components_time-picker_variants_states_error_properties_colors_border-color
204
+ ) !important;
205
+ background-color: var(
206
+ --recursica_ui-kit_components_time-picker_variants_states_error_properties_colors_background-color
207
+ ) !important;
208
+ color: var(
209
+ --recursica_ui-kit_components_time-picker_variants_states_error_properties_colors_text-color
210
+ ) !important;
211
+ }
212
+
213
+ /* Disabled State Mapping (Propagated strictly down from the wrapper DOM context) */
214
+ .root[data-disabled="true"] .timeWrapper {
215
+ border-color: var(
216
+ --recursica_ui-kit_components_time-picker_variants_states_disabled_properties_colors_border-color
217
+ ) !important;
218
+ background-color: var(
219
+ --recursica_ui-kit_components_time-picker_variants_states_disabled_properties_colors_background-color
220
+ ) !important;
221
+ color: var(
222
+ --recursica_ui-kit_components_time-picker_variants_states_disabled_properties_colors_text-color
223
+ ) !important;
224
+ opacity: var(
225
+ --recursica_ui-kit_components_time-picker_variants_states_disabled_properties_opacity
226
+ ) !important;
227
+ cursor: not-allowed;
228
+ }