@recursica/mui-adapter 0.19.0 → 0.21.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 (40) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/README.md +3 -3
  3. package/dist/mui-adapter.cjs +89 -58
  4. package/dist/mui-adapter.cjs.map +1 -1
  5. package/dist/mui-adapter.css +1 -1
  6. package/dist/mui-adapter.js +26163 -8727
  7. package/dist/mui-adapter.js.map +1 -1
  8. package/dist/src/components/Dropdown/BareDropdown.d.ts +41 -0
  9. package/dist/src/components/TimePicker/TimePicker.d.ts +16 -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 +10 -3
  14. package/src/components/Box/USAGE.md +1 -1
  15. package/src/components/Button/BUTTON_IMPLEMENTATION_NOTES.md +22 -0
  16. package/src/components/Button/Button.module.css +10 -7
  17. package/src/components/Button/Button.tsx +4 -0
  18. package/src/components/Button/USAGE.md +1 -13
  19. package/src/components/Card/USAGE.md +1 -13
  20. package/src/components/Container/USAGE.md +1 -1
  21. package/src/components/Dropdown/BareDropdown.tsx +135 -0
  22. package/src/components/Dropdown/Dropdown.tsx +16 -0
  23. package/src/components/Grid/USAGE.md +6 -10
  24. package/src/components/Loader/USAGE.md +1 -23
  25. package/src/components/Menu/USAGE.md +0 -7
  26. package/src/components/Pagination/USAGE.md +0 -6
  27. package/src/components/Panel/USAGE.md +1 -58
  28. package/src/components/Stepper/USAGE.md +0 -7
  29. package/src/components/Tabs/USAGE.md +0 -7
  30. package/src/components/TimePicker/TIMEPICKER_IMPLEMENTATION_NOTES.md +68 -0
  31. package/src/components/TimePicker/TimePicker.module.css +213 -37
  32. package/src/components/TimePicker/TimePicker.stories.tsx +119 -4
  33. package/src/components/TimePicker/TimePicker.tsx +257 -8
  34. package/src/components/TimePicker/USAGE.md +27 -2
  35. package/src/components/Tree/IMPLEMENTATION_NOTES.md +28 -1
  36. package/src/components/Tree/Tree.module.css +114 -69
  37. package/src/components/Tree/Tree.stories.tsx +13 -0
  38. package/src/components/Tree/Tree.tsx +99 -32
  39. package/src/components/Tree/USAGE.md +18 -2
  40. package/src/index.ts +1 -0
@@ -43,61 +43,4 @@ All Recursica components in the `@recursica/mui-adapter` package adhere strictly
43
43
 
44
44
  ## 4. Key Integration Features & Constraints
45
45
 
46
- ## 1. Mapping to MUI Drawer
47
-
48
- **Decision:** Panel maps to MUI's `Drawer` component, not `Paper` or `Card`.
49
-
50
- **Implementation:** Per the Recursica design system specification, "Panels slide in or expand from the edge of the screen to reveal additional content or functionality." This is the exact behavior of MUI's `Drawer` component, which provides:
51
-
52
- - Slide-in animation from any screen edge (using the `anchor` prop)
53
- - Backdrop/overlay support
54
- - Focus trap and overlay portal management
55
- - Internal scroll lock when open
56
-
57
- ---
58
-
59
- ## 2. Token Namespace: `panel`
60
-
61
- **Decision:** The CSS module exclusively uses variables from the `--recursica_ui-kit_components_panel_*` namespace.
62
-
63
- **Implementation:** The Recursica token system defines the `panel` namespace covering:
64
-
65
- - Geometry: border-radius, border-size, min-width (200px), max-width (960px)
66
- - Content padding: content-horizontal-padding (xl), content-vertical-padding (lg)
67
- - Header/Footer padding: header-footer-horizontal-padding (xl), header-footer-vertical-padding (md)
68
- - Spacing: header-close-gap (md), footer-button-gap (md)
69
- - Divider: divider-size (1px), divider-color
70
- - Elevation: elevation-3
71
- - Colors (layer-aware): background, border-color, content, divider-color, header-footer-background, title
72
- - Non-CSS: header-style ("h3")
73
-
74
- No tokens from other component namespaces are referenced.
75
-
76
- ---
77
-
78
- ## 3. Default Placement Override
79
-
80
- **Decision:** Use `placement` instead of `position` for configuring slide-out direction, and default it to `"right"`.
81
-
82
- **Implementation:** The prop was renamed from `position` to `placement` to prevent collision with the CSS `position` keyword, which is strictly blocked by the styling gatekeeper (`BLOCKED_STYLING_KEYS`). This allows configuring the drawer direction natively while maintaining strict design-system boundaries. The `placement="right"` default is mapped internally to MUI Drawer's `anchor` prop before any other sanitized props are applied. Right-side panels are the most common pattern for supplementary content, settings, and detail views.
83
-
84
- ---
85
-
86
- ## 4. Custom Panel.Footer
87
-
88
- **Decision:** A custom `Panel.Footer` sub-component is provided. MUI's Drawer does not have a native footer.
89
-
90
- **Implementation:** `Panel.Footer` is a `<div>` with styling referencing Recursica CSS variables for:
91
-
92
- - `header-footer-background` and `header-footer-padding` tokens
93
- - Top divider using `divider-size` and `divider-color`
94
- - `footer-button-gap` for action button spacing
95
- - `margin-top: auto` to push the footer to the bottom
96
-
97
- ---
98
-
99
- ## 5. Visibility Mapping (`opened` -> `open`)
100
-
101
- **Decision:** Accept `opened` prop to match the standard Recursica component API.
102
-
103
- **Implementation:** MUI Drawer natively expects the `open` boolean prop. The wrapper maps the incoming framework-agnostic `opened` prop to MUI's `open={Boolean(opened)}`, allowing consistent usage across both adapter implementations.
46
+ `Panel` accepts a `placement` prop (`"left"`, `"right"`, `"top"`, or `"bottom"`) that controls which edge of the screen the panel slides in from, and defaults to `"right"`. This prop was renamed from `position` to `placement`.
@@ -39,10 +39,3 @@ All Recursica components in the `@recursica/mui-adapter` package adhere strictly
39
39
  > - **Anti-override protection**: Rogues style injections (like inline `style` or arbitrary `className`) are automatically blocked by our prop layer unless `overStyled={true}` is explicitly provided.
40
40
  > - **No Direct Layers**: Do not pass a `layer` prop to this component. To place it on a specific visual layer, wrap it in a `<Layer layer={0|1|2|3}>` component natively.
41
41
  > - **Variables and Theming**: Styling is entirely determined by local CSS variables defined in `recursica_variables_scoped.css` and mapped in the component's CSS module.
42
-
43
- ---
44
-
45
- ## 4. Key Integration Features & Constraints
46
-
47
- - **Compositional API Dropped:** Mantine manages stepper state and content via `<Stepper.Step>` and `<Stepper.Completed>`. MUI delegates content rendering to the developer and focuses purely on the stepper visual layout using `<Step>`, `<StepLabel>`, etc.
48
- - **Monolithic API Adopted:** Following architectural review, we have abandoned the fabricated context wrappers for `mui-adapter`. We now natively export `Stepper`, `Step`, `StepLabel`, `StepButton`, and `StepConnector` wrapping their `@mui/material` counterparts. Developers are expected to manage the active step logic and content rendering outside the `Stepper` component, consistent with MUI patterns. Storybook tests have been updated to reflect this divergence while retaining core visual compatibility.
@@ -43,10 +43,3 @@ All Recursica components in the `@recursica/mui-adapter` package adhere strictly
43
43
  > - **Anti-override protection**: Rogues style injections (like inline `style` or arbitrary `className`) are automatically blocked by our prop layer unless `overStyled={true}` is explicitly provided.
44
44
  > - **No Direct Layers**: Do not pass a `layer` prop to this component. To place it on a specific visual layer, wrap it in a `<Layer layer={0|1|2|3}>` component natively.
45
45
  > - **Variables and Theming**: Styling is entirely determined by local CSS variables defined in `recursica_variables_scoped.css` and mapped in the component's CSS module.
46
-
47
- ---
48
-
49
- ## 4. Key Integration Features & Constraints
50
-
51
- - **Compositional API Dropped:** Mantine uses `<Tabs.List>`, `<Tabs.Tab>`, and `<Tabs.Panel>` natively with implicit context from `<Tabs>`. MUI relies on `@mui/lab/TabContext` and separates `Tabs` and `TabPanel`.
52
- - **Monolithic API Adopted:** Following architectural review, we have opted to drop the broken dot-notation wrappers for `mui-adapter`. We now natively export `Tabs` (MUI List), `Tab` (MUI Item), and `TabPanel` (from `@mui/lab`). Developers must use `TabContext` (from `@mui/lab`) to manage state, just like native MUI. Storybook and visual regression tests have been updated to reflect this divergence while retaining core property mapping compatibility.
@@ -0,0 +1,68 @@
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** — `@mui/x-date-pickers`'s `TimePicker`, always rendered with `format="hh:mm"` (or `"hh:mm:ss"` with `withSeconds`) — 12-hour digits, no native meridiem section.
8
+ 2. **The AM/PM selector** — `BareDropdown`, a headless variant of this adapter's own `Dropdown` component (see below), not a plain `Select`.
9
+
10
+ This shape is deliberate and **not configurable** — there is no prop to get a plain 24-hour field or to hide the AM/PM control. Every consumer gets the same 12-hour + Dropdown-styled-AM/PM composite. (Matt Massey, 2026-08-07 — an earlier revision of this component had a `hideAmPm` prop; it was removed once the design was confirmed fixed, and the shared `RecursicaTimePickerProps` contract in `adapter-common` no longer includes it.)
11
+
12
+ This was a deliberate choice over a native `<input type="time">`, in exchange for a real clock/list-based time selection UI, at the cost of a new dependency (`@mui/x-date-pickers` + `dayjs`, both added as optional peer deps + a real `dayjs` dependency).
13
+
14
+ ## `BareDropdown`
15
+
16
+ A new, **internal-only** component in `../Dropdown/BareDropdown.tsx` — not exported from this folder's `index.ts` (which only re-exports `./Dropdown`, not `./BareDropdown`). It's the same `@mui/material` `Select`/`MenuItem` primitives `Dropdown` wraps, styled via the **same** `Dropdown.module.css` classes (`root`/`input`/`icon`/`dropdown`/`option`), but with no `FormControlWrapper`/`WithReadOnlyWrapper` and no label/assistiveText/error/required props. Its `onChange` is normalized to `(value: string | null) => void` (just the selected value), unlike MUI's raw `Select` `onChange`, which hands back a `(event, child)` pair — this keeps its call signature identical to mantine-adapter's `BareDropdown` (whose underlying Mantine `Select` already has a plain-value `onChange`).
17
+
18
+ **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.)
19
+
20
+ ## Internal state, unlike every other component here
21
+
22
+ 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<Dayjs | null>` for the full 24-hour internal 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 a plain `"HH:mm"`/`"HH:mm:ss"` string; `dayjs` never leaks out.
23
+
24
+ ## `format="hh:mm"` has no meridiem section on purpose
25
+
26
+ Lowercase `hh` renders 12-hour digits (1–12) with no `a` (meridiem) token, so there's nothing for the user to toggle within the field itself — meridiem is only ever changed via the separate `BareDropdown`. Editing just the hour/minute digits preserves whichever meridiem the current internal value already has, since we always pass a fully-formed `Dayjs` value back into the field (never letting it manage its own uncontrolled state).
27
+
28
+ ## No `variant` prop to force "standard"
29
+
30
+ `@mui/x-date-pickers` v9's public `TimePicker`/`slotProps.field` API doesn't expose a `variant` prop at all (it's only reachable on an internal `textFieldProps` object this package doesn't surface). The CSS module defensively suppresses **both** the "standard" variant's underline (`::before`/`::after`) and the "outlined" variant's notched-outline, since which one actually renders isn't controllable from here — only one of the two suppressions will ever be doing anything at runtime.
31
+
32
+ ## Design tokens
33
+
34
+ - No dedicated `min-height` token exists for `time-picker` (unlike `text-field`/`date-picker`) — the field's height is derived from its own padding + line-height instead of a fixed token.
35
+ - `icon-size`/`icon-color`/`icon-text-gap`/`placeholder-opacity` are exempted (`recursica-ignore`) — the time field has no icon slot and MUI X's field renders empty sections via its own internal placeholder styling, not a native `::placeholder` pseudo-element.
36
+ - 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.
37
+
38
+ ## Known limitation — the popup clock/list view is unstyled
39
+
40
+ This pass covers the closed-state field and the AM/PM `BareDropdown` only. The open dropdown (digital clock list, depending on `views`) still renders with MUI's default theme, not Recursica tokens — there's no Figma-exported token set for it (mirrors the same category of gap `DatePicker`'s calendar popover has in `mantine-adapter`). Revisit once there's a design spec for it.
41
+
42
+ ## Read-Only Implementation
43
+
44
+ `readOnlyType="text"`, matching `DatePicker`'s convention.
45
+
46
+ ## Visual review fix (Matt Massey, 2026-08-07)
47
+
48
+ **`BareDropdown`'s border color didn't match Mantine's version, despite both reading the same `Dropdown` tokens**: a real bug in `BareDropdown.tsx` — `className={styles.root}` was set explicitly on `<MuiSelect>`, then `{...sanitizedProps}` was spread _after_ it. Any caller passing its own `className` (like `TimePicker.tsx`'s `styles.amPmSelect`) silently overwrote `styles.root` entirely via that later spread, so `Dropdown.module.css`'s border-color/background/`width: 100%` never actually applied — MUI's own default border rendered instead. Fixed by extracting `className` explicitly and merging it (`` `${styles.root} ${className}` ``) before it reaches `<MuiSelect>`, the same pattern mantine-adapter's `BareDropdown` already used. This also explains why the AM/PM box's width had looked accidentally "correct" before: the competing `width: 100%` rule from `.root` was never actually being applied either.
49
+
50
+ ## Visual review round 2 (Matt Massey, 2026-08-08)
51
+
52
+ - **Time field's default clock icon removed**: `time-picker`'s own token schema has no icon slot (see EXEMPTIONS above) and the popup it opens isn't styled to Recursica tokens anyway (see "Known limitation" below) — showing a button to open an unstyled popup was worse than not showing one. Removed via `slots={{ openPickerButton: () => null }}`; this also shrank the field's own width back down, since the icon's reserved layout space is gone.
53
+ - **AM/PM `BareDropdown` looked shorter than the time field and had a doubled border**: a latent, pre-existing bug in the _shared_ `Dropdown.module.css`, not something new in this composite — confirmed the standalone `Dropdown` component has the exact same issue (measured its own `.input` box at 42.8px instead of the 48px `min-height` token). Root cause: MUI generates a compound class for the select's inner box (e.g. `.css-xxx-...-MuiSelect-select`) that sets its own `min-height: 1.4375em` (~23px) — two classes' worth of specificity, which beats `Dropdown.module.css`'s single-class `.input` rule, so the 48px token silently loses. Separately, MUI's outlined variant renders its own native `fieldset`/`notchedOutline` border _underneath_ `.input`'s own Recursica border — the second border Matt saw. Both fixed **scoped to this composite only** (`.amPmSelect :global(.MuiSelect-select) { min-height: ... !important }` + suppressing `.MuiOutlinedInput-notchedOutline`), matching the same technique already used to suppress the time field's own native decoration — _not_ fixed in the shared `Dropdown.module.css`, since that would change the standalone `Dropdown` component everywhere in the app, outside this component's scope. Flagged to Matt as a separate, real bug worth a dedicated fix.
54
+
55
+ ## Visual review round 3 (Matt Massey, 2026-08-08)
56
+
57
+ - **AM/PM value wasn't vertically centered**: a side effect of the round-2 min-height fix above — forcing the box to 48px left real slack below the text (`display: block`, top-aligned line box), which reads as "not centered" once the box is taller than its own padding + line-height. Added `display: flex; align-items: center` to the same scoped override so the slack distributes evenly instead.
58
+ - **AM/PM dropdown menu showed MUI's default blue selected-state tint instead of Recursica's**: `Dropdown.module.css`'s `.option[data-selected="true"]` rule (Recursica's neutral hover/selected tint) was never actually wired up — neither `Dropdown.tsx` nor `BareDropdown.tsx` ever set `data-selected` on the currently-selected `MenuItem`, so MUI's own `Mui-selected` class (with its default primary-blue background) rendered uncontested instead. This is a **shared, pre-existing gap** affecting the real `Dropdown` component too, just not visible there by default (`Dropdown`'s own default story has no value pre-selected, so nothing shows the tint) — unlike `TimePicker`'s AM/PM, which always has a value. Fixed in **both** `Dropdown.tsx` and `BareDropdown.tsx` (compare each `MenuItem`'s value against the select's current `value`/`defaultValue`) since this is a low-risk, purely-additive fix completing an already-designed CSS contract, not a new design decision — unlike the min-height/border fix above, which changes visual geometry and was deliberately scoped to this composite only.
59
+
60
+ ## Visual review round 4 (Matt Massey, 2026-08-08) — time field width
61
+
62
+ The time field itself rendered wider than intended (220px vs. the 130px `time-picker.properties.width` token), despite `.field { width: fit-content }` already being set. Root cause: `@mui/x-date-pickers` computes and inline-sets a literal pixel width on `.MuiPickersInputBase-sectionsContainer` based on the format string (182px for "hh:mm") — sized generously for the widest possible rendered value, independent of `.field`'s own width. Reset via `.field :global(.MuiPickersInputBase-sectionsContainer) { width: auto !important; flex-grow: 0 !important; }` — safe here because these are real text `<span>`s (not native `<input>`s), so `auto` sizes to their actual content correctly, unlike the native-`<input>`-in-`BareDropdown` case elsewhere in this file, where `auto` alone didn't help. Now matches Mantine's `.timeWrapper`'s 130px exactly.
63
+
64
+ ## Visual review round 5 (Matt Massey, 2026-08-08)
65
+
66
+ - **AM/PM error-state border wasn't changing**: `Dropdown.tsx`'s error/disabled state was set via `inputProps` (`data-error`/`data-disabled`), which only reaches the nested accessibility `<input>` — a different element from `.root` (the actual Select root carrying the border), which is what `Dropdown.module.css`'s `.root[data-error]`/`[data-disabled]` selectors require. The error border never actually applied, in the real `Dropdown` component or `BareDropdown`. Fixed by also setting `data-error`/`data-disabled` directly as top-level props on `<MuiSelect>` (they land on its root element) in both `Dropdown.tsx` and `BareDropdown.tsx` — the latter also needed an `error?: boolean` prop added to its own interface, since it previously had none.
67
+ - **Static/Editable ReadOnly showed "Invalid Date" instead of a value**: a real, separate bug — `toDayjs`'s 2-argument `dayjs(value, format)` call silently does nothing without the `customParseFormat` plugin registered; without it, dayjs falls back to native `Date` parsing, which fails on a bare "HH:mm" string (no date component). This went unnoticed until now because every prior interactive test produced `Dayjs` objects directly from the picker's own `onChange`, never actually exercising `toDayjs` with a real initial `value`/`defaultValue` string. Fixed by adding `dayjs.extend(customParseFormat)`.
68
+ - **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` (uses `toDayjs` + dayjs's own `.format("h:mm A")`) before handing it to `WithReadOnlyWrapper`.
@@ -1,41 +1,217 @@
1
1
  /* EXEMPTIONS:
2
- This component is a placeholder stub. Once fully implemented, its specific
3
- tokens will be fully wired up to direct styles. */
4
-
5
- /* recursica-ignore: --recursica_ui-kit_components_time-picker_properties_border-radius */
6
- /* recursica-ignore: --recursica_ui-kit_components_time-picker_properties_horizontal-padding */
2
+ - state-specific border-size variables are ignored because a uniform border-size is applied globally
3
+ to prevent unexpected layout shift or flickering during focus, disabled, or error state 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 equivalent here: MUI X's field renders empty sections via its own
7
+ internal placeholder styling, not a native ::placeholder pseudo-element we can target. */
8
+ /* recursica-ignore: --recursica_ui-kit_components_time-picker_variants_states_error_properties_border-size */
9
+ /* recursica-ignore: --recursica_ui-kit_components_time-picker_variants_states_disabled_properties_border-size */
7
10
  /* recursica-ignore: --recursica_ui-kit_components_time-picker_properties_icon-size */
8
11
  /* recursica-ignore: --recursica_ui-kit_components_time-picker_properties_icon-text-gap */
12
+ /* recursica-ignore: --recursica_ui-kit_components_time-picker_properties_colors_icon-color */
9
13
  /* recursica-ignore: --recursica_ui-kit_components_time-picker_properties_placeholder-opacity */
10
- /* recursica-ignore: --recursica_ui-kit_components_time-picker_properties_text_font-family */
11
- /* recursica-ignore: --recursica_ui-kit_components_time-picker_properties_text_font-size */
12
- /* recursica-ignore: --recursica_ui-kit_components_time-picker_properties_text_font-style */
13
- /* recursica-ignore: --recursica_ui-kit_components_time-picker_properties_text_font-weight */
14
- /* recursica-ignore: --recursica_ui-kit_components_time-picker_properties_text_letter-spacing */
15
- /* recursica-ignore: --recursica_ui-kit_components_time-picker_properties_text_line-height */
16
- /* recursica-ignore: --recursica_ui-kit_components_time-picker_properties_text_text-decoration */
17
- /* recursica-ignore: --recursica_ui-kit_components_time-picker_properties_text_text-transform */
18
- /* recursica-ignore: --recursica_ui-kit_components_time-picker_properties_vertical-padding */
19
- /* recursica-ignore: --recursica_ui-kit_components_time-picker_properties_width */
20
- /* recursica-ignore: --recursica_ui-kit_components_time-picker_variants_layouts_side-by-side_properties_top-bottom-margin */
21
- /* recursica-ignore: --recursica_ui-kit_components_time-picker_variants_layouts_stacked_properties_top-bottom-margin */
22
- /* recursica-ignore: --recursica_ui-kit_components_time-picker_variants_states_default_properties_border-size */
23
- /* recursica-ignore: --recursica_ui-kit_components_time-picker_variants_states_default_properties_colors_background */
24
- /* recursica-ignore: --recursica_ui-kit_components_time-picker_variants_states_default_properties_colors_border-color */
25
- /* recursica-ignore: --recursica_ui-kit_components_time-picker_variants_states_default_properties_colors_icon */
26
- /* recursica-ignore: --recursica_ui-kit_components_time-picker_variants_states_default_properties_colors_text */
27
- /* recursica-ignore: --recursica_ui-kit_components_time-picker_variants_states_disabled_properties_border-size */
28
- /* recursica-ignore: --recursica_ui-kit_components_time-picker_variants_states_disabled_properties_colors_background */
29
- /* recursica-ignore: --recursica_ui-kit_components_time-picker_variants_states_disabled_properties_colors_border-color */
30
- /* recursica-ignore: --recursica_ui-kit_components_time-picker_variants_states_disabled_properties_colors_icon */
31
- /* recursica-ignore: --recursica_ui-kit_components_time-picker_variants_states_disabled_properties_colors_text */
32
- /* recursica-ignore: --recursica_ui-kit_components_time-picker_variants_states_error_properties_border-size */
33
- /* recursica-ignore: --recursica_ui-kit_components_time-picker_variants_states_error_properties_colors_background */
34
- /* recursica-ignore: --recursica_ui-kit_components_time-picker_variants_states_error_properties_colors_border-color */
35
- /* recursica-ignore: --recursica_ui-kit_components_time-picker_variants_states_error_properties_colors_icon */
36
- /* recursica-ignore: --recursica_ui-kit_components_time-picker_variants_states_error_properties_colors_text */
37
- /* recursica-ignore: --recursica_ui-kit_components_time-picker_variants_states_focus_properties_border-size */
38
- /* recursica-ignore: --recursica_ui-kit_components_time-picker_variants_states_focus_properties_colors_background */
39
- /* recursica-ignore: --recursica_ui-kit_components_time-picker_variants_states_focus_properties_colors_border-color */
40
- /* recursica-ignore: --recursica_ui-kit_components_time-picker_variants_states_focus_properties_colors_icon */
41
- /* recursica-ignore: --recursica_ui-kit_components_time-picker_variants_states_focus_properties_colors_text */
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 (BareDropdown draws its own
35
+ border/background from Dropdown.module.css). See TIMEPICKER_IMPLEMENTATION_NOTES.md.
36
+ - MUI X's field variant can't be forced from this component's public API (see
37
+ TIMEPICKER_IMPLEMENTATION_NOTES.md) — whichever native decoration it renders is suppressed
38
+ below, and our own border/background are applied to .field instead.
39
+ */
40
+
41
+ .root {
42
+ display: flex;
43
+ align-items: flex-start;
44
+ gap: var(
45
+ --recursica_ui-kit_components_time-picker_properties_horizontal-padding
46
+ );
47
+ width: fit-content;
48
+ }
49
+
50
+ .field {
51
+ /* min-width, not width: the `width` token was authored for a single plain input (confirmed against
52
+ text-field's identical token shape); this is now only the time-entry half of the composite. */
53
+ min-width: var(--recursica_ui-kit_components_time-picker_properties_width);
54
+ width: fit-content;
55
+ border-radius: var(
56
+ --recursica_ui-kit_components_time-picker_properties_border-radius
57
+ );
58
+ border-width: var(
59
+ --recursica_ui-kit_components_time-picker_properties_border-size
60
+ );
61
+ border-style: solid;
62
+ border-color: var(
63
+ --recursica_ui-kit_components_time-picker_properties_colors_border-color
64
+ );
65
+ background-color: var(
66
+ --recursica_ui-kit_components_time-picker_properties_colors_background-color
67
+ );
68
+ color: var(
69
+ --recursica_ui-kit_components_time-picker_properties_colors_text-color
70
+ );
71
+ }
72
+
73
+ /* Suppress whichever native field decoration MUI X renders by default (its public API doesn't
74
+ expose a variant prop to force "standard" in this version) — .field draws the border instead,
75
+ regardless of whether the field defaults to "outlined" or "standard". */
76
+ .field :global(.MuiPickersInputBase-root::before),
77
+ .field :global(.MuiPickersInputBase-root::after),
78
+ .field :global(.MuiInput-root::before),
79
+ .field :global(.MuiInput-root::after) {
80
+ border-bottom: none !important;
81
+ }
82
+
83
+ .field :global(.MuiPickersOutlinedInput-notchedOutline) {
84
+ border: none !important;
85
+ }
86
+
87
+ .field :global(.MuiPickersInputBase-sectionsContainer) {
88
+ /* MUI X computes and inline-sets a literal pixel width here based on the format string (e.g.
89
+ 182px for "hh:mm"), sized generously to fit the widest possible rendered value — wider than
90
+ .field's own `width: fit-content` intends. Reset it so the field visually matches Mantine's
91
+ equally-compact time input; the section <span>s are real text content, so width: auto sizes to
92
+ them correctly (unlike the native-<input>-in-BareDropdown case elsewhere in this file, where
93
+ `auto` didn't help — no UA-default-sizing quirk here since these aren't native inputs). */
94
+ width: auto !important;
95
+ flex-grow: 0 !important;
96
+ padding-top: var(
97
+ --recursica_ui-kit_components_time-picker_properties_vertical-padding
98
+ );
99
+ padding-bottom: var(
100
+ --recursica_ui-kit_components_time-picker_properties_vertical-padding
101
+ );
102
+ padding-left: var(
103
+ --recursica_ui-kit_components_time-picker_properties_horizontal-padding
104
+ );
105
+ padding-right: var(
106
+ --recursica_ui-kit_components_time-picker_properties_horizontal-padding
107
+ );
108
+
109
+ font-family: var(
110
+ --recursica_ui-kit_components_time-picker_properties_text_font-family
111
+ );
112
+ font-size: var(
113
+ --recursica_ui-kit_components_time-picker_properties_text_font-size
114
+ );
115
+ font-style: var(
116
+ --recursica_ui-kit_components_time-picker_properties_text_font-style
117
+ );
118
+ font-weight: var(
119
+ --recursica_ui-kit_components_time-picker_properties_text_font-weight
120
+ );
121
+ letter-spacing: var(
122
+ --recursica_ui-kit_components_time-picker_properties_text_letter-spacing
123
+ );
124
+ line-height: var(
125
+ --recursica_ui-kit_components_time-picker_properties_text_line-height
126
+ );
127
+ text-decoration: var(
128
+ --recursica_ui-kit_components_time-picker_properties_text_text-decoration
129
+ );
130
+ text-transform: var(
131
+ --recursica_ui-kit_components_time-picker_properties_text_text-transform
132
+ );
133
+ }
134
+
135
+ .field :global(.MuiPickersInputBase-input) {
136
+ color: inherit;
137
+ }
138
+
139
+ /* The AM/PM BareDropdown reuses Dropdown.module.css's own border/background/padding entirely —
140
+ this only controls its size/alignment within the flex row. */
141
+ .amPmSelect {
142
+ flex: 0 0 auto;
143
+ align-self: flex-start;
144
+ width: auto;
145
+ min-width: 5.5rem;
146
+ }
147
+
148
+ /* MUI's own generated class for the select's inner box (a compound selector, e.g.
149
+ `.css-xxx-...-MuiSelect-select`) has higher specificity than Dropdown.module.css's single-class
150
+ `.input` rule, so its own `min-height: 1.4375em` (~23px) silently wins over our 48px token —
151
+ this is a latent bug in the shared Dropdown.module.css itself (the standalone Dropdown
152
+ component has the exact same undersized box; flagged separately, not fixed here to avoid an
153
+ app-wide change outside TimePicker's scope). `!important` here only re-asserts the height
154
+ Dropdown was always supposed to have, scoped to this composite. MUI's outlined variant also
155
+ renders its own native `fieldset`/`notchedOutline` border *in addition to* `.input`'s own
156
+ Recursica border — suppressed the same way the time field already suppresses its own native
157
+ decoration above. See TIMEPICKER_IMPLEMENTATION_NOTES.md. */
158
+ .amPmSelect :global(.MuiSelect-select) {
159
+ min-height: var(
160
+ --recursica_ui-kit_globals_form_field_size_single-line-input-height
161
+ ) !important;
162
+ box-sizing: border-box !important;
163
+ /* Forcing min-height above leaves real slack below the (block-flow, top-aligned) text once the
164
+ box is taller than its own padding + line-height — the value read as "not centered". Flex +
165
+ center distributes that slack evenly instead. */
166
+ display: flex !important;
167
+ align-items: center !important;
168
+ }
169
+
170
+ .amPmSelect :global(.MuiOutlinedInput-notchedOutline) {
171
+ border: none !important;
172
+ }
173
+
174
+ /* -------------------------------------
175
+ STATE CASCADE ARCHITECTURE
176
+ -------------------------------------- */
177
+
178
+ /* Focus State Mapping — applied to the time field only (the BareDropdown has its own via
179
+ Dropdown's own CSS module). */
180
+ .field:has(:global(.Mui-focused)) {
181
+ box-shadow:
182
+ 0 0 0 var(--recursica_brand_states_focus_border-size)
183
+ var(--recursica_brand_states_focus_color),
184
+ 0 0 var(--recursica_brand_states_focus_blur)
185
+ var(--recursica_brand_states_focus_margin)
186
+ var(--recursica_brand_states_focus_color);
187
+ }
188
+
189
+ /* Error State Mapping (data-error is set explicitly by TimePicker.tsx). */
190
+ .root[data-error="true"] .field {
191
+ border-color: var(
192
+ --recursica_ui-kit_components_time-picker_variants_states_error_properties_colors_border-color
193
+ ) !important;
194
+ background-color: var(
195
+ --recursica_ui-kit_components_time-picker_variants_states_error_properties_colors_background-color
196
+ ) !important;
197
+ color: var(
198
+ --recursica_ui-kit_components_time-picker_variants_states_error_properties_colors_text-color
199
+ ) !important;
200
+ }
201
+
202
+ /* Disabled State Mapping */
203
+ .field:has(:global(.Mui-disabled)) {
204
+ border-color: var(
205
+ --recursica_ui-kit_components_time-picker_variants_states_disabled_properties_colors_border-color
206
+ ) !important;
207
+ background-color: var(
208
+ --recursica_ui-kit_components_time-picker_variants_states_disabled_properties_colors_background-color
209
+ ) !important;
210
+ color: var(
211
+ --recursica_ui-kit_components_time-picker_variants_states_disabled_properties_colors_text-color
212
+ ) !important;
213
+ opacity: var(
214
+ --recursica_ui-kit_components_time-picker_variants_states_disabled_properties_opacity
215
+ ) !important;
216
+ cursor: not-allowed;
217
+ }
@@ -1,12 +1,75 @@
1
1
  import type { Meta, StoryObj } from "@storybook/react";
2
2
  import { TimePicker } from "./TimePicker";
3
- import { ComingSoon } from "@recursica/storybook-template";
3
+ import { formControlArgTypes } from "../../../.storybook/commonArgTypes";
4
4
 
5
5
  const meta: Meta<typeof TimePicker> = {
6
- title: "UI-Kit/🚧 TimePicker",
6
+ title: "UI-Kit/TimePicker",
7
7
  component: TimePicker,
8
8
  tags: ["autodocs"],
9
- argTypes: {},
9
+ parameters: {
10
+ controls: {
11
+ include: [
12
+ "value",
13
+ "defaultValue",
14
+ "disabled",
15
+ "error",
16
+ "required",
17
+ "label",
18
+ "assistiveText",
19
+ "readOnly",
20
+ "withSeconds",
21
+ "formLayout",
22
+ ],
23
+ },
24
+ docs: {
25
+ description: {
26
+ component: `
27
+ The \`TimePicker\` primitive provides a 12-hour time field (via \`@mui/x-date-pickers\`) paired with a dedicated AM/PM \`Dropdown\`-style selector, integrated directly into the \`FormControlWrapper\` architecture. This composite is the only way this component operates — a Recursica-specific design, not a user-configurable option.
28
+
29
+ ### Examples
30
+ Always structure horizontal architectures via the generic \`formLayout\` parameter.
31
+ \`\`\`tsx
32
+ <TimePicker
33
+ label="Start Time"
34
+ assistiveText="Select the deployment kick-off time."
35
+ formLayout="stacked"
36
+ />
37
+ \`\`\`
38
+ `,
39
+ },
40
+ },
41
+ },
42
+ argTypes: {
43
+ ...formControlArgTypes,
44
+ disabled: {
45
+ control: "boolean",
46
+ description:
47
+ "Maps the formal disabled variable states structurally to the input core.",
48
+ },
49
+ error: {
50
+ control: "text",
51
+ description:
52
+ "Applies the strict error string boundary rendering invalid structures seamlessly.",
53
+ },
54
+ required: {
55
+ control: "boolean",
56
+ },
57
+ label: {
58
+ control: "text",
59
+ },
60
+ assistiveText: {
61
+ control: "text",
62
+ },
63
+ readOnly: {
64
+ control: "boolean",
65
+ description:
66
+ "Toggles structural read-only data presentation explicitly blocking standard component bindings.",
67
+ },
68
+ withSeconds: {
69
+ control: "boolean",
70
+ description: "Shows and allows editing the seconds segment.",
71
+ },
72
+ },
10
73
  };
11
74
 
12
75
  export default meta;
@@ -14,5 +77,57 @@ export default meta;
14
77
  type Story = StoryObj<typeof TimePicker>;
15
78
 
16
79
  export const Default: Story = {
17
- render: () => <ComingSoon componentName="TimePicker" />,
80
+ args: {
81
+ disabled: false,
82
+ label: "Meeting Time",
83
+ assistiveText: "Choose the start time in your local timezone.",
84
+ },
85
+ };
86
+
87
+ export const FormsSideBySide: Story = {
88
+ args: {
89
+ label: "Incident Start Time",
90
+ assistiveText: "When did the incident originally occur?",
91
+ formLayout: "side-by-side",
92
+ },
93
+ };
94
+
95
+ export const WithSeconds: Story = {
96
+ args: {
97
+ label: "Precise Execution Time",
98
+ assistiveText: "Includes a seconds segment for exact scheduling.",
99
+ withSeconds: true,
100
+ },
101
+ };
102
+
103
+ export const Disabled: Story = {
104
+ args: {
105
+ label: "Disabled Time Slot",
106
+ disabled: true,
107
+ },
108
+ };
109
+
110
+ export const ErrorState: Story = {
111
+ args: {
112
+ label: "Deployment Window",
113
+ error: "The chosen time falls outside the allowed deployment window.",
114
+ required: true,
115
+ },
116
+ };
117
+
118
+ export const StaticReadOnly: Story = {
119
+ args: {
120
+ label: "Static ReadOnly Review",
121
+ value: "14:30",
122
+ readOnly: true,
123
+ },
124
+ };
125
+
126
+ export const EditableReadOnly: Story = {
127
+ args: {
128
+ label: "Editable ReadOnly Review",
129
+ defaultValue: "09:00",
130
+ readOnly: true,
131
+ labelWithEditIcon: true,
132
+ },
18
133
  };