basalt-ui 1.19.0 → 1.19.1

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 CHANGED
@@ -1,3 +1,10 @@
1
+ ## [1.19.1](https://github.com/jkrumm/basalt-ui/compare/v1.19.0...v1.19.1) (2026-08-20)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * cap the mobile bottom-sheet height to its content ([04093ac](https://github.com/jkrumm/basalt-ui/commit/04093ac902d894249ebea3e6381755497670e305))
7
+
1
8
  # [1.19.0](https://github.com/jkrumm/basalt-ui/compare/v1.18.0...v1.19.0) (2026-08-20)
2
9
 
3
10
 
@@ -1 +1 @@
1
- {"version":3,"file":"app-mobile-nav.d.ts","sourceRoot":"","sources":["../../src/shell/app-mobile-nav.tsx"],"names":[],"mappings":"AAqBA,OAAO,KAAK,EAAc,YAAY,EAAa,MAAM,OAAO,CAAA;AAChE,OAAO,KAAK,EACV,eAAe,EAEf,cAAc,EAGf,MAAM,cAAc,CAAA;AACrB,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAA;AACzD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAA;AAiB/C,MAAM,MAAM,cAAc,GAAG;IAC3B,kFAAkF;IAClF,KAAK,EAAE,cAAc,CAAA;IACrB,MAAM,CAAC,EAAE,eAAe,GAAG,SAAS,CAAA;IACpC;;wEAEoE;IACpE,OAAO,CAAC,EAAE,kBAAkB,GAAG,SAAS,CAAA;IACxC,iBAAiB,CAAC,EAAE,gBAAgB,EAAE,GAAG,SAAS,CAAA;CACnD,CAAA;AAkFD;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,kBAAkB,GAAG,SAAS,GAAG,MAAM,CAE/E;AAsFD,wBAAgB,SAAS,CAAC,EACxB,KAAK,EACL,MAAM,EACN,OAAO,EACP,iBAAiB,GAClB,EAAE,cAAc,GAAG,YAAY,CAsO/B"}
1
+ {"version":3,"file":"app-mobile-nav.d.ts","sourceRoot":"","sources":["../../src/shell/app-mobile-nav.tsx"],"names":[],"mappings":"AAqBA,OAAO,KAAK,EAAc,YAAY,EAAa,MAAM,OAAO,CAAA;AAChE,OAAO,KAAK,EACV,eAAe,EAEf,cAAc,EAGf,MAAM,cAAc,CAAA;AACrB,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAA;AACzD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAA;AAiB/C,MAAM,MAAM,cAAc,GAAG;IAC3B,kFAAkF;IAClF,KAAK,EAAE,cAAc,CAAA;IACrB,MAAM,CAAC,EAAE,eAAe,GAAG,SAAS,CAAA;IACpC;;wEAEoE;IACpE,OAAO,CAAC,EAAE,kBAAkB,GAAG,SAAS,CAAA;IACxC,iBAAiB,CAAC,EAAE,gBAAgB,EAAE,GAAG,SAAS,CAAA;CACnD,CAAA;AAkFD;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,kBAAkB,GAAG,SAAS,GAAG,MAAM,CAE/E;AAsFD,wBAAgB,SAAS,CAAC,EACxB,KAAK,EACL,MAAM,EACN,OAAO,EACP,iBAAiB,GAClB,EAAE,cAAc,GAAG,YAAY,CAiP/B"}
@@ -281,32 +281,32 @@ function MobileNav({
281
281
  );
282
282
  const sheetSlot = (slot) => /* @__PURE__ */ jsxs(Fragment2, { children: [
283
283
  surfaceTab(slot, () => setOpenKey(slot.key)),
284
- /* @__PURE__ */ jsxs(
284
+ /* @__PURE__ */ jsx(
285
285
  Drawer,
286
286
  {
287
287
  opened: openKey === slot.key,
288
288
  onClose: close,
289
289
  position: "bottom",
290
- size: "auto",
291
290
  padding: "md",
292
291
  title: slot.label,
293
- classNames: { content: classes.sheet, title: classes.sheetTitle },
292
+ classNames: {
293
+ content: classes.sheet,
294
+ title: classes.sheetTitle,
295
+ header: classes.sheetHeader
296
+ },
294
297
  transitionProps: {
295
298
  transition: "slide-up",
296
299
  duration: reduceMotion ? 0 : 220,
297
300
  timingFunction: "cubic-bezier(0.32, 0.72, 0, 1)"
298
301
  },
299
- children: [
300
- /* @__PURE__ */ jsx("div", { className: classes.grabber, "aria-hidden": true }),
301
- /* @__PURE__ */ jsx(ScrollArea.Autosize, { mah: "62dvh", type: "scroll", children: /* @__PURE__ */ jsxs(Stack, { gap: "xs", children: [
302
- slot.groups.map((group) => /* @__PURE__ */ jsxs(Stack, { gap: 2, children: [
303
- sheetGroupLabel(group),
304
- rows(group.items, 0, sheetRow)
305
- ] }, group.key)),
306
- slot.isMore && extraRows.length > 0 ? /* @__PURE__ */ jsx(Stack, { gap: 2, children: extraRows.map(sheetActionRow) }) : null,
307
- slot.isMore ? config?.moreExtra : null
308
- ] }) })
309
- ]
302
+ children: /* @__PURE__ */ jsx(ScrollArea.Autosize, { mah: "62dvh", type: "scroll", children: /* @__PURE__ */ jsxs(Stack, { gap: "xs", children: [
303
+ slot.groups.map((group) => /* @__PURE__ */ jsxs(Stack, { gap: 2, children: [
304
+ sheetGroupLabel(group),
305
+ rows(group.items, 0, sheetRow)
306
+ ] }, group.key)),
307
+ slot.isMore && extraRows.length > 0 ? /* @__PURE__ */ jsx(Stack, { gap: 2, children: extraRows.map(sheetActionRow) }) : null,
308
+ slot.isMore ? config?.moreExtra : null
309
+ ] }) })
310
310
  }
311
311
  )
312
312
  ] }, slot.key);
@@ -170,6 +170,27 @@
170
170
  border-top-right-radius: var(--mantine-radius-lg);
171
171
  /* A Drawer is NOT an AppShell section, so Mantine adds no inset of its own here. */
172
172
  padding-bottom: env(safe-area-inset-bottom, 0px);
173
+
174
+ /*
175
+ * THE `--drawer-size-auto` TRAP — do not "fix" this by restoring `size="auto"` on the Drawer.
176
+ * `getSize('auto', 'drawer-size')` (Mantine's `get-size.mjs`) resolves ANY non-numeric size to
177
+ * `var(--drawer-size-auto)`, and `@mantine/core` defines that custom property NOWHERE — grepped
178
+ * the full shipped `styles.css`. On a bottom Drawer that dead var flows straight into
179
+ * `--drawer-height`, so `size="auto"` was never wired to anything; the sheet rendered at
180
+ * `calc(100% - offset*2)`, i.e. the full viewport, no matter how little content it held. That is
181
+ * why this component no longer passes a `size` prop at all — it would resolve to *some* value,
182
+ * but this rule below overrides Mantine's `height`/`max-height` regardless of what that value is.
183
+ *
184
+ * This works with NO `!important`: `packages/basalt-ui/styles.css` wraps every Mantine rule in
185
+ * `@layer mantine` (verified against the installed `styles.layer.css`), and this CSS module is
186
+ * unlayered — an unlayered rule always wins over a layered one, independent of specificity or
187
+ * source order. `height: auto` lets the sheet hug its content; `max-height` is a hard ceiling on
188
+ * the whole sheet (header + body), sized with headroom ABOVE the `ScrollArea.Autosize`'s
189
+ * `mah="62dvh"` in the JSX — that autosize cap is what actually decides when the sheet starts
190
+ * scrolling internally, so the two never fight over which one owns "the maximum".
191
+ */
192
+ height: auto;
193
+ max-height: min(70dvh, 100%);
173
194
  }
174
195
 
175
196
  /* Sheet title = a micro-label (docs/DESIGN-SPEC.md §3), matching the sidebar's `.sectionLabel`. */
@@ -182,14 +203,18 @@
182
203
  color: var(--vx-faint);
183
204
  }
184
205
 
185
- /* The dismiss affordance. There is no swipe-to-dismiss: Mantine ships no drag primitive and
186
- * writing `transform` to Drawer.Content fights its own Transition on the same node. */
187
- .grabber {
188
- width: 36px;
189
- height: 4px;
190
- margin: 0 auto 8px;
191
- border-radius: 999px;
192
- background-color: color-mix(in srgb, var(--vx-ink) 18%, transparent);
206
+ /*
207
+ * Mantine's `ModalBase` header forces a `min-height` of `calc(3.75rem * var(--mantine-scale))`
208
+ * (60px) — sized for a real dialog title, not a 10px micro-label, and a big share of "unnecessarily
209
+ * big". `--vx-space-mobile-nav-row-height` pins it to the same 44px touch-target floor as every
210
+ * other row in this bar instead. The header's close button IS the dismiss affordance now — no
211
+ * separate grabber: there was never swipe-to-dismiss behind it (Mantine ships no drag primitive),
212
+ * so a plain decorative bar bought nothing an explicit, focusable close control doesn't already
213
+ * give for free, including to keyboard/screen-reader users the grabber never reached.
214
+ */
215
+ .sheetHeader {
216
+ min-height: var(--vx-space-mobile-nav-row-height);
217
+ padding: 0 var(--mantine-spacing-md);
193
218
  }
194
219
 
195
220
  /* Large tap rows inside the sheet. NavLink rows reuse the sidebar's neutral active fill.
@@ -16,7 +16,7 @@ declare const classes: {
16
16
  readonly menuLabel: string
17
17
  readonly sheet: string
18
18
  readonly sheetTitle: string
19
- readonly grabber: string
19
+ readonly sheetHeader: string
20
20
  readonly row: string
21
21
  readonly rowNested: string
22
22
  }
package/llms.txt CHANGED
@@ -1,4 +1,4 @@
1
- # basalt-ui v1.19.0
1
+ # basalt-ui v1.19.1
2
2
  # Opinionated framework for Mantine-based React apps — theme, app shell, visx charts, toolchain presets
3
3
  #
4
4
  # basalt-ui is an opinionated framework for Mantine-based React apps.
@@ -19,7 +19,7 @@
19
19
  # ./tokens.css subpath. Guide: docs/FRAMEWORK-FREE.md in the repo.
20
20
 
21
21
  Package: basalt-ui
22
- Version: 1.19.0
22
+ Version: 1.19.1
23
23
 
24
24
  ## basalt-ui
25
25
  Import: import { ... } from 'basalt-ui'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "basalt-ui",
3
- "version": "1.19.0",
3
+ "version": "1.19.1",
4
4
  "description": "Opinionated framework for Mantine-based React apps — theme, app shell, visx charts, toolchain presets",
5
5
  "keywords": [
6
6
  "charts",
@@ -170,6 +170,27 @@
170
170
  border-top-right-radius: var(--mantine-radius-lg);
171
171
  /* A Drawer is NOT an AppShell section, so Mantine adds no inset of its own here. */
172
172
  padding-bottom: env(safe-area-inset-bottom, 0px);
173
+
174
+ /*
175
+ * THE `--drawer-size-auto` TRAP — do not "fix" this by restoring `size="auto"` on the Drawer.
176
+ * `getSize('auto', 'drawer-size')` (Mantine's `get-size.mjs`) resolves ANY non-numeric size to
177
+ * `var(--drawer-size-auto)`, and `@mantine/core` defines that custom property NOWHERE — grepped
178
+ * the full shipped `styles.css`. On a bottom Drawer that dead var flows straight into
179
+ * `--drawer-height`, so `size="auto"` was never wired to anything; the sheet rendered at
180
+ * `calc(100% - offset*2)`, i.e. the full viewport, no matter how little content it held. That is
181
+ * why this component no longer passes a `size` prop at all — it would resolve to *some* value,
182
+ * but this rule below overrides Mantine's `height`/`max-height` regardless of what that value is.
183
+ *
184
+ * This works with NO `!important`: `packages/basalt-ui/styles.css` wraps every Mantine rule in
185
+ * `@layer mantine` (verified against the installed `styles.layer.css`), and this CSS module is
186
+ * unlayered — an unlayered rule always wins over a layered one, independent of specificity or
187
+ * source order. `height: auto` lets the sheet hug its content; `max-height` is a hard ceiling on
188
+ * the whole sheet (header + body), sized with headroom ABOVE the `ScrollArea.Autosize`'s
189
+ * `mah="62dvh"` in the JSX — that autosize cap is what actually decides when the sheet starts
190
+ * scrolling internally, so the two never fight over which one owns "the maximum".
191
+ */
192
+ height: auto;
193
+ max-height: min(70dvh, 100%);
173
194
  }
174
195
 
175
196
  /* Sheet title = a micro-label (docs/DESIGN-SPEC.md §3), matching the sidebar's `.sectionLabel`. */
@@ -182,14 +203,18 @@
182
203
  color: var(--vx-faint);
183
204
  }
184
205
 
185
- /* The dismiss affordance. There is no swipe-to-dismiss: Mantine ships no drag primitive and
186
- * writing `transform` to Drawer.Content fights its own Transition on the same node. */
187
- .grabber {
188
- width: 36px;
189
- height: 4px;
190
- margin: 0 auto 8px;
191
- border-radius: 999px;
192
- background-color: color-mix(in srgb, var(--vx-ink) 18%, transparent);
206
+ /*
207
+ * Mantine's `ModalBase` header forces a `min-height` of `calc(3.75rem * var(--mantine-scale))`
208
+ * (60px) — sized for a real dialog title, not a 10px micro-label, and a big share of "unnecessarily
209
+ * big". `--vx-space-mobile-nav-row-height` pins it to the same 44px touch-target floor as every
210
+ * other row in this bar instead. The header's close button IS the dismiss affordance now — no
211
+ * separate grabber: there was never swipe-to-dismiss behind it (Mantine ships no drag primitive),
212
+ * so a plain decorative bar bought nothing an explicit, focusable close control doesn't already
213
+ * give for free, including to keyboard/screen-reader users the grabber never reached.
214
+ */
215
+ .sheetHeader {
216
+ min-height: var(--vx-space-mobile-nav-row-height);
217
+ padding: 0 var(--mantine-spacing-md);
193
218
  }
194
219
 
195
220
  /* Large tap rows inside the sheet. NavLink rows reuse the sidebar's neutral active fill.
@@ -16,7 +16,7 @@ declare const classes: {
16
16
  readonly menuLabel: string
17
17
  readonly sheet: string
18
18
  readonly sheetTitle: string
19
- readonly grabber: string
19
+ readonly sheetHeader: string
20
20
  readonly row: string
21
21
  readonly rowNested: string
22
22
  }
@@ -366,4 +366,38 @@ describe('MobileNav', () => {
366
366
  expect(dropdown.querySelectorAll('.mantine-Menu-divider')).toHaveLength(0)
367
367
  expect(dropdown.querySelectorAll('.mantine-Menu-item')).toHaveLength(1)
368
368
  })
369
+
370
+ /**
371
+ * Regression guard for the mobile-sheet-content-height fix. Neither half is pixel-assertable
372
+ * under happy-dom (no layout, and CSS-module class hashes are unavailable under `bun test` per
373
+ * the file header) — so this pins the two DOM-observable facts instead: the sheet is no longer
374
+ * wired through the dead `size="auto"` → `--drawer-size-auto` custom property (see `.sheet` in
375
+ * the CSS module for why that was never wired to anything), and the redundant grabber is gone
376
+ * now that the header's own close button is the sheet's sole dismiss affordance.
377
+ */
378
+ test('11. the sheet drops the no-op size prop and the redundant grabber', async () => {
379
+ renderBar([
380
+ {
381
+ label: 'Reports',
382
+ mobile: { tab: true },
383
+ items: Array.from({ length: 9 }, (_, i) => item(`row${i + 1}`)),
384
+ },
385
+ { label: 'Main', items: [item('home')] },
386
+ ])
387
+
388
+ fireEvent.click(screen.getByLabelText('Reports'))
389
+ await waitFor(() => expect(drawer()).not.toBeNull())
390
+
391
+ // `size="auto"` resolves to `var(--drawer-size-auto)`, a custom property @mantine/core defines
392
+ // nowhere — its presence anywhere in the tree would be the old bug reappearing.
393
+ expect(document.querySelector('[style*="drawer-size-auto"]')).toBeNull()
394
+
395
+ // The header (title + Mantine's own close button) is the ONLY dismiss affordance now — the
396
+ // grabber div that used to precede the scroll area is gone, so the body wraps exactly the
397
+ // `ScrollArea.Autosize` root and nothing else.
398
+ const header = document.querySelector('.mantine-Drawer-header')
399
+ expect(header).not.toBeNull()
400
+ expect(header?.querySelector('.mantine-Drawer-close')).not.toBeNull()
401
+ expect(document.querySelector('.mantine-Drawer-body')?.children).toHaveLength(1)
402
+ })
369
403
  })
@@ -428,18 +428,29 @@ export function MobileNav({
428
428
  opened={openKey === slot.key}
429
429
  onClose={close}
430
430
  position="bottom"
431
- size="auto"
431
+ // NO `size` prop — `size="auto"` used to be here, but it is a no-op on a bottom Drawer in
432
+ // Mantine 9.3.0 (see the `.sheet` rule in the CSS module for the full trap). The sheet's
433
+ // actual height/max-height comes entirely from that unlayered CSS rule, which overrides
434
+ // Mantine's own regardless of what `size` resolves to.
432
435
  padding="md"
436
+ // `title` is both the sheet's accessible name (Mantine wires `aria-labelledby` to it
437
+ // automatically) and what makes the header render at all — dropping it would need an
438
+ // explicit `aria-label` on the Drawer instead. `classNames.header` slims Mantine's 60px
439
+ // title bar down to this bar's touch-target row height; its close button is the sheet's
440
+ // ONLY dismiss affordance now (see `.sheetHeader` in the CSS module for why the grabber
441
+ // that used to sit here is gone rather than the header).
433
442
  title={slot.label}
434
- classNames={{ content: classes.sheet, title: classes.sheetTitle }}
443
+ classNames={{
444
+ content: classes.sheet,
445
+ title: classes.sheetTitle,
446
+ header: classes.sheetHeader,
447
+ }}
435
448
  transitionProps={{
436
449
  transition: 'slide-up',
437
450
  duration: reduceMotion ? 0 : 220,
438
451
  timingFunction: 'cubic-bezier(0.32, 0.72, 0, 1)',
439
452
  }}
440
453
  >
441
- {/* The dismiss affordance. No swipe-to-dismiss — see `.grabber` in the CSS module. */}
442
- <div className={classes.grabber} aria-hidden />
443
454
  <ScrollArea.Autosize mah="62dvh" type="scroll">
444
455
  <Stack gap="xs">
445
456
  {slot.groups.map((group) => (