@recursica/mantine-adapter 0.50.0 → 0.50.2
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 +13 -0
- package/dist/mantine-adapter.css +1 -1
- package/package.json +1 -1
- package/src/components/DatePicker/DATEPICKER_IMPLEMENTATION_NOTES.md +1 -0
- package/src/components/DatePicker/DatePicker.module.css +13 -0
- package/src/components/DatePicker/DatePicker.stories.tsx +25 -0
- package/src/components/Modal/MODAL_IMPLEMENTATION_NOTES.md +12 -0
- package/src/components/Modal/Modal.module.css +119 -1
- package/src/components/Modal/Modal.stories.tsx +28 -2
package/package.json
CHANGED
|
@@ -27,3 +27,4 @@ The `DatePicker` component is a wrapper around the `@mantine/dates` `DatePickerI
|
|
|
27
27
|
- **Default leading icon**: `leftSection={<CalendarIcon />}` (new `DatePicker.icons.tsx`, following the existing local-inline-SVG convention used by `Pagination.icons.tsx` — no icon library is a dependency of either adapter) is set the same way, overridable via `leftSection`.
|
|
28
28
|
- **Default placeholder**: `placeholder="MM / DD / YY"`, set the same way (before `sanitizedProps`, so consumers can override).
|
|
29
29
|
- **Single-line enforcement (bug fix)**: `DatePickerInput` renders its value/placeholder as a `<button>`'s text content, not a native `<input>` — it wraps like any other block text by default. A long placeholder (e.g. the original story's `"Select a deadline..."`) wrapped to two lines, inflating the field's height well past its `min-height` token and making the field visually look narrower than its actual `width` token value even though the box itself was always the right size. Fixed with `white-space: nowrap`/`text-overflow: ellipsis`/`overflow-x: clip`/`overflow-y: visible` on `.input`, the same convention `FileInput.module.css`'s `.value` uses.
|
|
30
|
+
- **Day focus ring (bug fix)**: Mantine's `Day` ships no built-in `:focus-visible` style of its own (unlike `.calendarHeaderControl`/`.calendarHeaderLevel`, which are styled as `Button` and inherited a focus ring from that treatment), so a focused day — e.g. the auto-focused selected/today day when the calendar opens — fell through to the browser's native blue outline instead of a Recursica token. Fixed by adding `.day:focus-visible` with the same generic `--recursica_brand_states_focus_*` box-shadow ring already used by `.input` and `.calendarHeaderControl`/`.calendarHeaderLevel`. mui-adapter's `.MuiPickerDay-root.Mui-focusVisible` already had this — only the mantine-adapter side was missing it.
|
|
@@ -544,3 +544,16 @@
|
|
|
544
544
|
);
|
|
545
545
|
opacity: 1;
|
|
546
546
|
}
|
|
547
|
+
|
|
548
|
+
/* Focus ring, same convention as calendarHeaderControl/input's own :focus-visible treatment.
|
|
549
|
+
Mantine's Day ships no explicit focus style of its own, so without this it fell through to
|
|
550
|
+
the browser's native (blue) :focus-visible outline instead of a Recursica token. */
|
|
551
|
+
.day:focus-visible {
|
|
552
|
+
outline: none;
|
|
553
|
+
box-shadow:
|
|
554
|
+
0 0 0 var(--recursica_brand_states_focus_border-size)
|
|
555
|
+
var(--recursica_brand_states_focus_color),
|
|
556
|
+
0 0 var(--recursica_brand_states_focus_blur)
|
|
557
|
+
var(--recursica_brand_states_focus_margin)
|
|
558
|
+
var(--recursica_brand_states_focus_color);
|
|
559
|
+
}
|
|
@@ -115,6 +115,31 @@ export const ErrorState: Story = {
|
|
|
115
115
|
},
|
|
116
116
|
};
|
|
117
117
|
|
|
118
|
+
export const OpenedCalendar: Story = {
|
|
119
|
+
args: {
|
|
120
|
+
label: "Meeting Date",
|
|
121
|
+
assistiveText: "Calendar rendered open by default for styling review.",
|
|
122
|
+
// `popoverProps.opened` overrides Mantine's own internal disclosure state (see
|
|
123
|
+
// PickerInputBase's `opened: dropdownOpened, ...popoverProps` spread order), so the
|
|
124
|
+
// dropdown stays open with no click interaction needed — same convention as Menu's
|
|
125
|
+
// `opened: true` stories.
|
|
126
|
+
popoverProps: { opened: true },
|
|
127
|
+
// Fixed (not computed) so the selected-day fill is visible on load, alongside the
|
|
128
|
+
// today marker, for styling review. Local-component constructor, not an ISO date
|
|
129
|
+
// string — `new Date("2026-08-26")` parses as UTC midnight, which renders as the
|
|
130
|
+
// 25th in any timezone behind UTC.
|
|
131
|
+
defaultValue: new Date(2026, 7, 26),
|
|
132
|
+
},
|
|
133
|
+
parameters: {
|
|
134
|
+
docs: {
|
|
135
|
+
description: {
|
|
136
|
+
story:
|
|
137
|
+
"The calendar dropdown renders open by default so its styling can be reviewed without a click interaction.",
|
|
138
|
+
},
|
|
139
|
+
},
|
|
140
|
+
},
|
|
141
|
+
};
|
|
142
|
+
|
|
118
143
|
export const StaticReadOnly: Story = {
|
|
119
144
|
args: {
|
|
120
145
|
label: "Static ReadOnly Review",
|
|
@@ -13,3 +13,15 @@ Mantine natively exposes an abstract `size` prop (`"sm" | "md" | "lg" | "xl"`) t
|
|
|
13
13
|
### 2. Scroll Dividers behavior
|
|
14
14
|
|
|
15
15
|
Mantine internally handles scroll state natively, dynamically showing/hiding a divider line when content overflows in `.body`. This logic is tightly coupled to React DOM measurements internally. Our component inherits this dynamic behavior rather than statically rendering a permanent divider, matching Mantine's robust overflow UX. However, we aggressively override the generated `border-bottom` via CSS modules to ensure that when it _does_ appear, it correctly utilizes the `--recursica_ui-kit_components_modal_colors_scroll-divider` variable and `--recursica_ui-kit_components_modal_properties_scroll-divider-thickness` token.
|
|
16
|
+
|
|
17
|
+
### 3. Title truncation
|
|
18
|
+
|
|
19
|
+
`.title` truncates with an ellipsis (`overflow: hidden`, `white-space: nowrap`, `text-overflow: ellipsis`) rather than wrapping. It also needs `flex: 1 1 auto; min-width: 0;` since it's a flex child of `.header` alongside the close button — without `min-width: 0`, a flex item won't shrink below its content's intrinsic width, so ellipsis never engages. `.header`'s `display: flex` is likewise explicit rather than relied upon from Mantine's own header class, so the mui-adapter's plain-`<div>` header gets identical layout.
|
|
20
|
+
|
|
21
|
+
### 4. Width was pinned to Mantine's `md` size, not content-driven
|
|
22
|
+
|
|
23
|
+
Despite §1 above, `.content` never actually scaled fluidly: Mantine's own CSS sets `flex: 0 0 var(--modal-size)` (defaulting to 440px) on the Content element, and our module only added `min-width`/`max-width` without touching `flex`. A fixed flex-basis with `flex-shrink: 0` pins the box at exactly 440px regardless of those bounds, so they were unreachable dead code — e.g. "Authentication Required" (the `Default` story's title) didn't fit at 440px and silently wrapped to two lines. Overriding to `flex: 0 1 auto` makes the width shrink-to-fit the content within `min-width`/`max-width`, which is what makes title truncation (§3) only kick in once a title would exceed `max-width` rather than truncating titles that would otherwise fit. `.content[data-full-screen]` restores Mantine's own `flex: 0 0 100%` since the shrink-to-fit override would otherwise stop `fullScreen` from filling the viewport.
|
|
24
|
+
|
|
25
|
+
### 5. Close button restyled to match Button
|
|
26
|
+
|
|
27
|
+
`.close` reuses Button's text-variant/icon-only/small tokens (radius, padding, background/icon color, hover overlay, focus ring) instead of Mantine's native CloseButton look. Two overrides need `!important`/an explicit reset to win: Mantine's `CloseIcon` sizes itself via an inline `style` (`--cb-icon-size`, default `70%`), which beats any plain class rule — same category as Tree's `--level-offset` override; and Mantine's own subtle-variant `:hover` background (from `CloseButton.css`) is pinned back to the Button background token so only our `::after` overlay renders hover feedback.
|
|
@@ -1,3 +1,14 @@
|
|
|
1
|
+
/* Brand-layer exemptions (recursica-allow-brand) — see recursica-token-analyzer README.md.
|
|
2
|
+
* Close button is styled to match the Button component's text/icon-only variant, so it reuses
|
|
3
|
+
* the same global hover/focus state tokens Button.module.css exempts.
|
|
4
|
+
* recursica-allow-brand: --recursica_brand_states_focus_blur
|
|
5
|
+
* recursica-allow-brand: --recursica_brand_states_focus_border-size
|
|
6
|
+
* recursica-allow-brand: --recursica_brand_states_focus_color
|
|
7
|
+
* recursica-allow-brand: --recursica_brand_states_focus_margin
|
|
8
|
+
* recursica-allow-brand: --recursica_brand_states_hover_color
|
|
9
|
+
* recursica-allow-brand: --recursica_brand_states_hover_opacity
|
|
10
|
+
*/
|
|
11
|
+
|
|
1
12
|
.root {
|
|
2
13
|
}
|
|
3
14
|
|
|
@@ -6,6 +17,12 @@
|
|
|
6
17
|
}
|
|
7
18
|
|
|
8
19
|
.content {
|
|
20
|
+
/* HARDCODE: Mantine pins content to a fixed flex-basis from its own size scale
|
|
21
|
+
(`flex: 0 0 var(--modal-size)`, defaulting to 440px). Overriding to `0 1 auto` makes width
|
|
22
|
+
content-driven (shrink-to-fit) instead of fixed, so min-width/max-width below actually bound
|
|
23
|
+
it rather than being unreachable dead code. */
|
|
24
|
+
flex: 0 1 auto;
|
|
25
|
+
|
|
9
26
|
/* Geometric Bounds */
|
|
10
27
|
min-width: var(--recursica_ui-kit_components_modal_properties_min-width);
|
|
11
28
|
max-width: var(--recursica_ui-kit_components_modal_properties_max-width);
|
|
@@ -39,7 +56,16 @@
|
|
|
39
56
|
flex-direction: column;
|
|
40
57
|
}
|
|
41
58
|
|
|
59
|
+
.content[data-full-screen] {
|
|
60
|
+
/* HARDCODE: preserve Mantine's native full-screen sizing — the shrink-to-fit override above
|
|
61
|
+
would otherwise apply here too and stop `fullScreen` from filling the viewport. */
|
|
62
|
+
flex: 0 0 100%;
|
|
63
|
+
}
|
|
64
|
+
|
|
42
65
|
.header {
|
|
66
|
+
display: flex; /* HARDCODE: puts the title and close button side-by-side so the title has a bounded width to truncate against */
|
|
67
|
+
align-items: center;
|
|
68
|
+
justify-content: space-between;
|
|
43
69
|
padding: var(
|
|
44
70
|
--recursica_ui-kit_components_modal_properties_header-footer-vertical-padding
|
|
45
71
|
)
|
|
@@ -50,6 +76,12 @@
|
|
|
50
76
|
}
|
|
51
77
|
|
|
52
78
|
.title {
|
|
79
|
+
flex: 1 1 auto; /* HARDCODE: let the title claim the space between the header edge and the close button */
|
|
80
|
+
min-width: 0; /* HARDCODE: required for text-overflow ellipsis to take effect on a flex child */
|
|
81
|
+
overflow: hidden;
|
|
82
|
+
white-space: nowrap;
|
|
83
|
+
text-overflow: ellipsis;
|
|
84
|
+
|
|
53
85
|
color: var(--recursica_ui-kit_components_modal_properties_colors_title);
|
|
54
86
|
|
|
55
87
|
/* Direct Figma Typography Mapping */
|
|
@@ -173,5 +205,91 @@
|
|
|
173
205
|
}
|
|
174
206
|
|
|
175
207
|
.close {
|
|
176
|
-
/*
|
|
208
|
+
/* Matches the Button component's text-variant, icon-only, small-size visual treatment
|
|
209
|
+
(see Button.module.css) so the modal close control looks like a Recursica Button
|
|
210
|
+
rather than Mantine's native CloseButton. */
|
|
211
|
+
box-sizing: border-box;
|
|
212
|
+
display: flex;
|
|
213
|
+
align-items: center;
|
|
214
|
+
justify-content: center;
|
|
215
|
+
position: relative;
|
|
216
|
+
overflow: hidden;
|
|
217
|
+
transition: all 0.2s ease;
|
|
218
|
+
|
|
219
|
+
height: var(
|
|
220
|
+
--recursica_ui-kit_components_button_variants_sizes_small_properties_height
|
|
221
|
+
);
|
|
222
|
+
min-width: var(
|
|
223
|
+
--recursica_ui-kit_components_button_variants_content_icon-only_variants_sizes_small_properties_min-width
|
|
224
|
+
);
|
|
225
|
+
padding: 0
|
|
226
|
+
var(
|
|
227
|
+
--recursica_ui-kit_components_button_variants_content_icon-only_variants_sizes_small_properties_horizontal-padding
|
|
228
|
+
);
|
|
229
|
+
border-radius: var(
|
|
230
|
+
--recursica_ui-kit_components_button_variants_content_icon-only_variants_sizes_small_properties_border-radius
|
|
231
|
+
);
|
|
232
|
+
|
|
233
|
+
border-style: solid;
|
|
234
|
+
border-width: var(
|
|
235
|
+
--recursica_ui-kit_components_button_variants_styles_text_properties_border-size
|
|
236
|
+
);
|
|
237
|
+
border-color: var(
|
|
238
|
+
--recursica_ui-kit_components_button_variants_styles_text_properties_colors_border-color
|
|
239
|
+
);
|
|
240
|
+
background-color: var(
|
|
241
|
+
--recursica_ui-kit_components_button_variants_styles_text_properties_colors_background-color
|
|
242
|
+
);
|
|
243
|
+
color: var(
|
|
244
|
+
--recursica_ui-kit_components_button_variants_styles_text_properties_colors_icon-color
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/* Mantine's CloseIcon sets width/height via an inline `style` attribute
|
|
249
|
+
(`--cb-icon-size`, default 70%), which beats any plain class rule regardless of specificity —
|
|
250
|
+
same category of override as Tree's `--level-offset` (see Tree/IMPLEMENTATION_NOTES.md).
|
|
251
|
+
`!important` is required here to land the Button's small icon-size token instead. */
|
|
252
|
+
.close svg {
|
|
253
|
+
position: relative;
|
|
254
|
+
z-index: 1;
|
|
255
|
+
width: var(
|
|
256
|
+
--recursica_ui-kit_components_button_variants_sizes_small_properties_icon
|
|
257
|
+
) !important;
|
|
258
|
+
height: var(
|
|
259
|
+
--recursica_ui-kit_components_button_variants_sizes_small_properties_icon
|
|
260
|
+
) !important;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
.close::after {
|
|
264
|
+
content: "";
|
|
265
|
+
position: absolute;
|
|
266
|
+
inset: 0;
|
|
267
|
+
border-radius: inherit;
|
|
268
|
+
z-index: 0;
|
|
269
|
+
pointer-events: none;
|
|
270
|
+
transition: opacity 150ms ease;
|
|
271
|
+
opacity: 0;
|
|
272
|
+
background-color: var(--recursica_brand_states_hover_color);
|
|
273
|
+
}
|
|
274
|
+
.close:hover:not(:disabled)::after {
|
|
275
|
+
opacity: var(--recursica_brand_states_hover_opacity);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/* Mantine's own subtle-variant hover background (CloseButton.css) would otherwise show through
|
|
279
|
+
underneath the overlay above — pin it back to the Button's own background token so only the
|
|
280
|
+
`::after` overlay renders the hover feedback, matching Button's single-overlay treatment. */
|
|
281
|
+
.close:hover {
|
|
282
|
+
background-color: var(
|
|
283
|
+
--recursica_ui-kit_components_button_variants_styles_text_properties_colors_background-color
|
|
284
|
+
);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
.close:focus-visible {
|
|
288
|
+
outline: none;
|
|
289
|
+
box-shadow:
|
|
290
|
+
0 0 0 var(--recursica_brand_states_focus_border-size)
|
|
291
|
+
var(--recursica_brand_states_focus_color),
|
|
292
|
+
0 0 var(--recursica_brand_states_focus_blur)
|
|
293
|
+
var(--recursica_brand_states_focus_margin)
|
|
294
|
+
var(--recursica_brand_states_focus_color);
|
|
177
295
|
}
|
|
@@ -19,7 +19,8 @@ export default meta;
|
|
|
19
19
|
type Story = StoryObj<typeof Modal>;
|
|
20
20
|
|
|
21
21
|
const DefaultWrapper = (args: ModalProps) => {
|
|
22
|
-
|
|
22
|
+
// Starts opened so the modal is visible without pressing a button first.
|
|
23
|
+
const [opened, setOpened] = useState(true);
|
|
23
24
|
return (
|
|
24
25
|
<>
|
|
25
26
|
<Modal {...args} opened={opened} onClose={() => setOpened(false)}>
|
|
@@ -43,8 +44,33 @@ export const Default: Story = {
|
|
|
43
44
|
render: (args) => <DefaultWrapper {...args} />,
|
|
44
45
|
};
|
|
45
46
|
|
|
47
|
+
const LongTitleWrapper = (args: ModalProps) => {
|
|
48
|
+
const [opened, setOpened] = useState(true);
|
|
49
|
+
return (
|
|
50
|
+
<>
|
|
51
|
+
<Modal {...args} opened={opened} onClose={() => setOpened(false)}>
|
|
52
|
+
The title above is longer than the header can display, so it truncates
|
|
53
|
+
with an ellipsis instead of wrapping onto a second line.
|
|
54
|
+
<Modal.Footer>
|
|
55
|
+
<Button onClick={() => setOpened(false)}>Got it</Button>
|
|
56
|
+
</Modal.Footer>
|
|
57
|
+
</Modal>
|
|
58
|
+
<Button onClick={() => setOpened(true)}>Open Modal</Button>
|
|
59
|
+
</>
|
|
60
|
+
);
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
export const LongTitle: Story = {
|
|
64
|
+
args: {
|
|
65
|
+
title:
|
|
66
|
+
"This Modal Title Is Deliberately Long Enough To Exceed The Available Header Width",
|
|
67
|
+
},
|
|
68
|
+
render: (args) => <LongTitleWrapper {...args} />,
|
|
69
|
+
};
|
|
70
|
+
|
|
46
71
|
const ScrollingWrapper = (args: ModalProps) => {
|
|
47
|
-
|
|
72
|
+
// Starts opened so the modal is visible without pressing a button first.
|
|
73
|
+
const [opened, setOpened] = useState(true);
|
|
48
74
|
return (
|
|
49
75
|
<>
|
|
50
76
|
<Modal {...args} opened={opened} onClose={() => setOpened(false)}>
|