@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.
- package/CHANGELOG.md +28 -0
- package/README.md +3 -3
- package/dist/mui-adapter.cjs +89 -58
- package/dist/mui-adapter.cjs.map +1 -1
- package/dist/mui-adapter.css +1 -1
- package/dist/mui-adapter.js +26163 -8727
- package/dist/mui-adapter.js.map +1 -1
- package/dist/src/components/Dropdown/BareDropdown.d.ts +41 -0
- package/dist/src/components/TimePicker/TimePicker.d.ts +16 -3
- package/dist/src/components/Tree/Tree.d.ts +5 -0
- package/dist/src/index.d.ts +1 -1
- package/docs/PHILOSOPHY.md +39 -0
- package/package.json +10 -3
- package/src/components/Box/USAGE.md +1 -1
- package/src/components/Button/BUTTON_IMPLEMENTATION_NOTES.md +22 -0
- package/src/components/Button/Button.module.css +10 -7
- package/src/components/Button/Button.tsx +4 -0
- package/src/components/Button/USAGE.md +1 -13
- package/src/components/Card/USAGE.md +1 -13
- package/src/components/Container/USAGE.md +1 -1
- package/src/components/Dropdown/BareDropdown.tsx +135 -0
- package/src/components/Dropdown/Dropdown.tsx +16 -0
- package/src/components/Grid/USAGE.md +6 -10
- package/src/components/Loader/USAGE.md +1 -23
- package/src/components/Menu/USAGE.md +0 -7
- package/src/components/Pagination/USAGE.md +0 -6
- package/src/components/Panel/USAGE.md +1 -58
- package/src/components/Stepper/USAGE.md +0 -7
- package/src/components/Tabs/USAGE.md +0 -7
- package/src/components/TimePicker/TIMEPICKER_IMPLEMENTATION_NOTES.md +68 -0
- package/src/components/TimePicker/TimePicker.module.css +213 -37
- package/src/components/TimePicker/TimePicker.stories.tsx +119 -4
- package/src/components/TimePicker/TimePicker.tsx +257 -8
- package/src/components/TimePicker/USAGE.md +27 -2
- package/src/components/Tree/IMPLEMENTATION_NOTES.md +28 -1
- package/src/components/Tree/Tree.module.css +114 -69
- package/src/components/Tree/Tree.stories.tsx +13 -0
- package/src/components/Tree/Tree.tsx +99 -32
- package/src/components/Tree/USAGE.md +18 -2
- 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
|
-
|
|
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
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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-
|
|
11
|
-
/* recursica-ignore: --recursica_ui-kit_components_time-
|
|
12
|
-
|
|
13
|
-
/*
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
/*
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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 {
|
|
3
|
+
import { formControlArgTypes } from "../../../.storybook/commonArgTypes";
|
|
4
4
|
|
|
5
5
|
const meta: Meta<typeof TimePicker> = {
|
|
6
|
-
title: "UI-Kit
|
|
6
|
+
title: "UI-Kit/TimePicker",
|
|
7
7
|
component: TimePicker,
|
|
8
8
|
tags: ["autodocs"],
|
|
9
|
-
|
|
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
|
-
|
|
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
|
};
|