@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/package.json CHANGED
@@ -13,7 +13,7 @@
13
13
  "url": "git+https://github.com/borderux/recursica.git",
14
14
  "directory": "packages/mantine-adapter"
15
15
  },
16
- "version": "0.50.0",
16
+ "version": "0.50.2",
17
17
  "type": "module",
18
18
  "main": "./dist/mantine-adapter.cjs",
19
19
  "module": "./dist/mantine-adapter.js",
@@ -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
- /* Inherits standard icon button tokens natively, but we can override if required */
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
- const [opened, setOpened] = useState(false);
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
- const [opened, setOpened] = useState(false);
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)}>