basalt-ui 1.13.0 → 1.14.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 +7 -0
- package/dist/shell/app-sidebar.d.ts +19 -1
- package/dist/shell/app-sidebar.d.ts.map +1 -1
- package/dist/shell/app-sidebar.js +30 -26
- package/dist/shell/app-sidebar.module.css +11 -0
- package/dist/shell/app-sidebar.module.css.d.ts +1 -0
- package/dist/shell/app-sidebar.test.d.ts +2 -0
- package/dist/shell/app-sidebar.test.d.ts.map +1 -0
- package/dist/shell/index.d.ts +10 -1
- package/dist/shell/index.d.ts.map +1 -1
- package/dist/shell/index.js +2 -0
- package/dist/shell/index.test.d.ts +2 -0
- package/dist/shell/index.test.d.ts.map +1 -0
- package/llms.txt +2 -2
- package/package.json +1 -1
- package/src/shell/app-sidebar.module.css +11 -0
- package/src/shell/app-sidebar.module.css.d.ts +1 -0
- package/src/shell/app-sidebar.test.tsx +98 -0
- package/src/shell/app-sidebar.tsx +22 -0
- package/src/shell/index.test.tsx +35 -0
- package/src/shell/index.tsx +11 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
# [1.14.0](https://github.com/jkrumm/basalt-ui/compare/v1.13.0...v1.14.0) (2026-08-10)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Features
|
|
5
|
+
|
|
6
|
+
* let the sidebar host arbitrary content in its nav scroll region ([1b1a5f3](https://github.com/jkrumm/basalt-ui/commit/1b1a5f3f50cba0cf5f89e767d438dbd02adfe03f))
|
|
7
|
+
|
|
1
8
|
# [1.13.0](https://github.com/jkrumm/basalt-ui/compare/v1.12.0...v1.13.0) (2026-08-04)
|
|
2
9
|
|
|
3
10
|
|
|
@@ -51,6 +51,24 @@ export type AppSidebarProps = {
|
|
|
51
51
|
* fixed, non-scrolling row. Pair with basalt-ui/commands' openSpotlight.
|
|
52
52
|
*/
|
|
53
53
|
search?: SidebarSearchConfig;
|
|
54
|
+
/**
|
|
55
|
+
* Arbitrary content appended after `sections` inside the nav `ScrollArea` — a tree, a filter
|
|
56
|
+
* panel, a project list, anything a set of `SidebarItem`s can't express. Renders as the last
|
|
57
|
+
* child of the scrolling nav column, so a long list scrolls with the rest of the nav instead of
|
|
58
|
+
* fighting it for height. Pass `sections={[]}` to use this slot exclusively; the section-spacing
|
|
59
|
+
* rule only fires between adjacent children, so an empty `sections` produces no orphan divider or
|
|
60
|
+
* dead padding above it.
|
|
61
|
+
*
|
|
62
|
+
* Hidden on the collapsed desktop rail; still present in the mobile drawer, which opens at full
|
|
63
|
+
* width. The rail is ~56px of icon buttons with no sensible representation for arbitrary
|
|
64
|
+
* consumer content, so the CSS media query that drives the rail hides this slot the same way it
|
|
65
|
+
* hides `.childList` — never a JS check on `collapsed`, since that one value is shared by the
|
|
66
|
+
* rail AND the drawer (only the media query tells them apart). `SidebarSearch` gets to adapt
|
|
67
|
+
* itself to the rail (it takes `collapsed` and renders its own icon-only form) because the shell
|
|
68
|
+
* owns that control; it cannot adapt content it knows nothing about, so this slot hides instead
|
|
69
|
+
* of squashing it.
|
|
70
|
+
*/
|
|
71
|
+
navExtra?: ReactNode;
|
|
54
72
|
};
|
|
55
|
-
export declare function AppSidebar({ brand, sections, collapsed, onToggleCollapse, onClose, renderNavLink, settingsMenuItems, footerExtra, account, search, }: AppSidebarProps): import("react").JSX.Element;
|
|
73
|
+
export declare function AppSidebar({ brand, sections, collapsed, onToggleCollapse, onClose, renderNavLink, settingsMenuItems, footerExtra, account, search, navExtra, }: AppSidebarProps): import("react").JSX.Element;
|
|
56
74
|
//# sourceMappingURL=app-sidebar.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"app-sidebar.d.ts","sourceRoot":"","sources":["../../src/shell/app-sidebar.tsx"],"names":[],"mappings":"AA+BA,OAAO,KAAK,EAAc,SAAS,EAAE,MAAM,OAAO,CAAA;AAClD,OAAO,KAAK,EAAE,WAAW,EAAE,gBAAgB,EAAE,WAAW,EAAE,cAAc,EAAE,MAAM,SAAS,CAAA;AAEzF,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAA;AAEzD,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,kBAAkB,CAAA;AAK3D;;;;;;;;;;;;GAYG;AACH,MAAM,MAAM,eAAe,GAAG,CAC5B,IAAI,EAAE,WAAW,EACjB,IAAI,EAAE;IACJ,MAAM,EAAE,OAAO,CAAA;IACf,KAAK,CAAC,EAAE,CAAC,MAAM,IAAI,CAAC,GAAG,SAAS,CAAA;IAChC,gBAAgB,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;CACtC,KACE,SAAS,CAAA;AAEd,MAAM,MAAM,eAAe,GAAG;IAC5B,KAAK,EAAE,WAAW,CAAA;IAClB,QAAQ,EAAE,cAAc,EAAE,CAAA;IAC1B,SAAS,EAAE,OAAO,CAAA;IAClB,gBAAgB,EAAE,MAAM,IAAI,CAAA;IAC5B,OAAO,EAAE,MAAM,IAAI,CAAA;IACnB;;;OAGG;IACH,aAAa,CAAC,EAAE,eAAe,CAAA;IAC/B;;;;;OAKG;IACH,iBAAiB,CAAC,EAAE,gBAAgB,EAAE,CAAA;IACtC,mGAAmG;IACnG,WAAW,CAAC,EAAE,SAAS,CAAA;IACvB;;;;OAIG;IACH,OAAO,CAAC,EAAE,kBAAkB,CAAA;IAC5B;;;OAGG;IACH,MAAM,CAAC,EAAE,mBAAmB,CAAA;
|
|
1
|
+
{"version":3,"file":"app-sidebar.d.ts","sourceRoot":"","sources":["../../src/shell/app-sidebar.tsx"],"names":[],"mappings":"AA+BA,OAAO,KAAK,EAAc,SAAS,EAAE,MAAM,OAAO,CAAA;AAClD,OAAO,KAAK,EAAE,WAAW,EAAE,gBAAgB,EAAE,WAAW,EAAE,cAAc,EAAE,MAAM,SAAS,CAAA;AAEzF,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAA;AAEzD,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,kBAAkB,CAAA;AAK3D;;;;;;;;;;;;GAYG;AACH,MAAM,MAAM,eAAe,GAAG,CAC5B,IAAI,EAAE,WAAW,EACjB,IAAI,EAAE;IACJ,MAAM,EAAE,OAAO,CAAA;IACf,KAAK,CAAC,EAAE,CAAC,MAAM,IAAI,CAAC,GAAG,SAAS,CAAA;IAChC,gBAAgB,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;CACtC,KACE,SAAS,CAAA;AAEd,MAAM,MAAM,eAAe,GAAG;IAC5B,KAAK,EAAE,WAAW,CAAA;IAClB,QAAQ,EAAE,cAAc,EAAE,CAAA;IAC1B,SAAS,EAAE,OAAO,CAAA;IAClB,gBAAgB,EAAE,MAAM,IAAI,CAAA;IAC5B,OAAO,EAAE,MAAM,IAAI,CAAA;IACnB;;;OAGG;IACH,aAAa,CAAC,EAAE,eAAe,CAAA;IAC/B;;;;;OAKG;IACH,iBAAiB,CAAC,EAAE,gBAAgB,EAAE,CAAA;IACtC,mGAAmG;IACnG,WAAW,CAAC,EAAE,SAAS,CAAA;IACvB;;;;OAIG;IACH,OAAO,CAAC,EAAE,kBAAkB,CAAA;IAC5B;;;OAGG;IACH,MAAM,CAAC,EAAE,mBAAmB,CAAA;IAC5B;;;;;;;;;;;;;;;;OAgBG;IACH,QAAQ,CAAC,EAAE,SAAS,CAAA;CACrB,CAAA;AAkWD,wBAAgB,UAAU,CAAC,EACzB,KAAK,EACL,QAAQ,EACR,SAAS,EACT,gBAAgB,EAChB,OAAO,EACP,aAAa,EACb,iBAAiB,EACjB,WAAW,EACX,OAAO,EACP,MAAM,EACN,QAAQ,GACT,EAAE,eAAe,+BAyLjB"}
|
|
@@ -314,7 +314,8 @@ function AppSidebar({
|
|
|
314
314
|
settingsMenuItems,
|
|
315
315
|
footerExtra,
|
|
316
316
|
account,
|
|
317
|
-
search
|
|
317
|
+
search,
|
|
318
|
+
navExtra
|
|
318
319
|
}) {
|
|
319
320
|
const { step } = useBasaltSpacing();
|
|
320
321
|
const [collapsedSections, setCollapsedSections] = useState(
|
|
@@ -410,33 +411,36 @@ function AppSidebar({
|
|
|
410
411
|
scrollbarSize: 9,
|
|
411
412
|
className: classes.navScroll,
|
|
412
413
|
classNames: { viewport: classes.navViewport },
|
|
413
|
-
children: /* @__PURE__ */
|
|
414
|
-
|
|
414
|
+
children: /* @__PURE__ */ jsxs(Stack, { gap: 0, className: classes.nav, children: [
|
|
415
|
+
sections.map((section) => {
|
|
416
|
+
if (!section.collapsible) {
|
|
417
|
+
return /* @__PURE__ */ jsxs("div", { children: [
|
|
418
|
+
/* @__PURE__ */ jsx("div", { className: classes.sectionBand, children: /* @__PURE__ */ jsx(SectionLabel, { flush: true, children: section.label }) }),
|
|
419
|
+
/* @__PURE__ */ jsx(Stack, { gap: 1, children: renderSectionItems(section) })
|
|
420
|
+
] }, section.label);
|
|
421
|
+
}
|
|
422
|
+
const isOpen = !collapsedSections[section.label];
|
|
415
423
|
return /* @__PURE__ */ jsxs("div", { children: [
|
|
416
|
-
/* @__PURE__ */
|
|
417
|
-
|
|
424
|
+
/* @__PURE__ */ jsxs(
|
|
425
|
+
UnstyledButton,
|
|
426
|
+
{
|
|
427
|
+
className: `${classes.sectionBand} ${classes.sectionHeader}`,
|
|
428
|
+
onClick: () => setCollapsedSections((prev) => ({
|
|
429
|
+
...prev,
|
|
430
|
+
[section.label]: !prev[section.label]
|
|
431
|
+
})),
|
|
432
|
+
"aria-expanded": isOpen,
|
|
433
|
+
children: [
|
|
434
|
+
/* @__PURE__ */ jsx(SectionLabel, { flush: true, children: section.label }),
|
|
435
|
+
/* @__PURE__ */ jsx(IconChevron, { open: isOpen })
|
|
436
|
+
]
|
|
437
|
+
}
|
|
438
|
+
),
|
|
439
|
+
/* @__PURE__ */ jsx(Collapse, { expanded: isOpen, children: /* @__PURE__ */ jsx(Stack, { gap: 1, children: renderSectionItems(section) }) })
|
|
418
440
|
] }, section.label);
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
/* @__PURE__ */ jsxs(
|
|
423
|
-
UnstyledButton,
|
|
424
|
-
{
|
|
425
|
-
className: `${classes.sectionBand} ${classes.sectionHeader}`,
|
|
426
|
-
onClick: () => setCollapsedSections((prev) => ({
|
|
427
|
-
...prev,
|
|
428
|
-
[section.label]: !prev[section.label]
|
|
429
|
-
})),
|
|
430
|
-
"aria-expanded": isOpen,
|
|
431
|
-
children: [
|
|
432
|
-
/* @__PURE__ */ jsx(SectionLabel, { flush: true, children: section.label }),
|
|
433
|
-
/* @__PURE__ */ jsx(IconChevron, { open: isOpen })
|
|
434
|
-
]
|
|
435
|
-
}
|
|
436
|
-
),
|
|
437
|
-
/* @__PURE__ */ jsx(Collapse, { expanded: isOpen, children: /* @__PURE__ */ jsx(Stack, { gap: 1, children: renderSectionItems(section) }) })
|
|
438
|
-
] }, section.label);
|
|
439
|
-
}) })
|
|
441
|
+
}),
|
|
442
|
+
navExtra !== void 0 && navExtra !== null && /* @__PURE__ */ jsx("div", { className: classes.navExtra, children: navExtra })
|
|
443
|
+
] })
|
|
440
444
|
}
|
|
441
445
|
),
|
|
442
446
|
account ? /* @__PURE__ */ jsxs(Stack, { gap: 0, className: classes.footer, children: [
|
|
@@ -419,4 +419,15 @@
|
|
|
419
419
|
.root[data-collapsed] .childList {
|
|
420
420
|
display: none;
|
|
421
421
|
}
|
|
422
|
+
|
|
423
|
+
/* `navExtra` has no sensible icon-rail representation (arbitrary consumer content, not a nav
|
|
424
|
+
* row) — hide it in the rail, same treatment as `.childList`. It stays mounted (never a JS
|
|
425
|
+
* conditional on `collapsed`): `collapsed` is one value shared by the desktop rail AND the
|
|
426
|
+
* mobile drawer (`AppShell.Navbar`'s `collapsed: { mobile: !mobileOpened }` only ever un-collapses
|
|
427
|
+
* the mobile side), and the rail-vs-drawer split lives ENTIRELY in this media query, never in JS —
|
|
428
|
+
* a JS gate on `collapsed` would also drop `navExtra` from the mobile drawer, which opens at full
|
|
429
|
+
* width regardless of the persisted rail state. */
|
|
430
|
+
.root[data-collapsed] .navExtra {
|
|
431
|
+
display: none;
|
|
432
|
+
}
|
|
422
433
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"app-sidebar.test.d.ts","sourceRoot":"","sources":["../../src/shell/app-sidebar.test.tsx"],"names":[],"mappings":""}
|
package/dist/shell/index.d.ts
CHANGED
|
@@ -73,6 +73,15 @@ export type BasaltShellProps = {
|
|
|
73
73
|
globalActions?: ReactNode;
|
|
74
74
|
/** Extra content appended to the sidebar footer, beside the settings menu (mobile close, etc.). */
|
|
75
75
|
sidebarFooterExtra?: ReactNode;
|
|
76
|
+
/**
|
|
77
|
+
* Arbitrary content appended after `sections` inside the sidebar's nav scroll region (a tree, a
|
|
78
|
+
* filter panel, a project list, …). Hidden on the collapsed desktop rail; still present in the
|
|
79
|
+
* mobile drawer, which opens at full width — see `AppSidebarProps.navExtra`'s JSDoc for why.
|
|
80
|
+
* `AppSidebar` renders once and serves both the desktop rail and the mobile drawer (collapse
|
|
81
|
+
* styling is gated behind a `min-width: sm` media query, not a second render), so this slot
|
|
82
|
+
* reaches both.
|
|
83
|
+
*/
|
|
84
|
+
sidebarNavExtra?: ReactNode;
|
|
76
85
|
/** Entries appended to the sidebar settings menu. */
|
|
77
86
|
settingsMenuItems?: SettingsMenuItem[];
|
|
78
87
|
/**
|
|
@@ -119,5 +128,5 @@ export type BasaltShellProps = {
|
|
|
119
128
|
export declare function NavCountBadge({ count }: {
|
|
120
129
|
count: number;
|
|
121
130
|
}): import("react").JSX.Element | null;
|
|
122
|
-
export declare function BasaltShell({ brand, sections, renderNavLink, globalActions, sidebarFooterExtra, settingsMenuItems, storageKey, collapsed: collapsedProp, onCollapsedChange, renderBreadcrumbLink, account, search, children, }: BasaltShellProps): import("react").JSX.Element;
|
|
131
|
+
export declare function BasaltShell({ brand, sections, renderNavLink, globalActions, sidebarFooterExtra, sidebarNavExtra, settingsMenuItems, storageKey, collapsed: collapsedProp, onCollapsedChange, renderBreadcrumbLink, account, search, children, }: BasaltShellProps): import("react").JSX.Element;
|
|
123
132
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/shell/index.tsx"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/shell/index.tsx"],"names":[],"mappings":"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"}
|
package/dist/shell/index.js
CHANGED
|
@@ -95,6 +95,7 @@ function BasaltShell({
|
|
|
95
95
|
renderNavLink,
|
|
96
96
|
globalActions,
|
|
97
97
|
sidebarFooterExtra,
|
|
98
|
+
sidebarNavExtra,
|
|
98
99
|
settingsMenuItems,
|
|
99
100
|
storageKey = "basalt-sidebar-collapsed",
|
|
100
101
|
collapsed: collapsedProp,
|
|
@@ -153,6 +154,7 @@ function BasaltShell({
|
|
|
153
154
|
onToggleCollapse: toggleCollapse,
|
|
154
155
|
onClose: closeMobile,
|
|
155
156
|
footerExtra: sidebarFooterExtra,
|
|
157
|
+
navExtra: sidebarNavExtra,
|
|
156
158
|
...renderNavLink !== void 0 && { renderNavLink },
|
|
157
159
|
...settingsMenuItems !== void 0 && { settingsMenuItems },
|
|
158
160
|
...account !== void 0 && { account },
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.test.d.ts","sourceRoot":"","sources":["../../src/shell/index.test.tsx"],"names":[],"mappings":""}
|
package/llms.txt
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# basalt-ui v1.
|
|
1
|
+
# basalt-ui v1.14.0
|
|
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.
|
|
22
|
+
Version: 1.14.0
|
|
23
23
|
|
|
24
24
|
## basalt-ui
|
|
25
25
|
Import: import { ... } from 'basalt-ui'
|
package/package.json
CHANGED
|
@@ -419,4 +419,15 @@
|
|
|
419
419
|
.root[data-collapsed] .childList {
|
|
420
420
|
display: none;
|
|
421
421
|
}
|
|
422
|
+
|
|
423
|
+
/* `navExtra` has no sensible icon-rail representation (arbitrary consumer content, not a nav
|
|
424
|
+
* row) — hide it in the rail, same treatment as `.childList`. It stays mounted (never a JS
|
|
425
|
+
* conditional on `collapsed`): `collapsed` is one value shared by the desktop rail AND the
|
|
426
|
+
* mobile drawer (`AppShell.Navbar`'s `collapsed: { mobile: !mobileOpened }` only ever un-collapses
|
|
427
|
+
* the mobile side), and the rail-vs-drawer split lives ENTIRELY in this media query, never in JS —
|
|
428
|
+
* a JS gate on `collapsed` would also drop `navExtra` from the mobile drawer, which opens at full
|
|
429
|
+
* width regardless of the persisted rail state. */
|
|
430
|
+
.root[data-collapsed] .navExtra {
|
|
431
|
+
display: none;
|
|
432
|
+
}
|
|
422
433
|
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `navExtra` — the seam that lets a consumer put arbitrary content (a tree, a filter panel,
|
|
3
|
+
* a project list) inside the sidebar's nav scroll region instead of rendering a second column
|
|
4
|
+
* beside the shell. Four invariants:
|
|
5
|
+
*
|
|
6
|
+
* 1. It renders INSIDE the nav `ScrollArea`'s content (Mantine's own `.mantine-ScrollArea-content`
|
|
7
|
+
* class, stable across runs — CSS-module class hashes are not available under `bun test`, see
|
|
8
|
+
* `stat-card.test.tsx`/`use-basalt-spacing.test.tsx` for the same constraint elsewhere), as the
|
|
9
|
+
* LAST child, so a long tree scrolls with the nav instead of fighting it for height.
|
|
10
|
+
* 2. It STAYS MOUNTED when `collapsed` — `collapsed` is one value shared by the desktop rail AND
|
|
11
|
+
* the mobile drawer (`AppShell.Navbar`'s `collapsed: { mobile: !mobileOpened }` never touches
|
|
12
|
+
* the desktop side), and the rail-vs-drawer split lives entirely in a `min-width: sm` CSS media
|
|
13
|
+
* query, never in JS. A JS gate on `collapsed` would silently drop `navExtra` from the mobile
|
|
14
|
+
* drawer too, which opens at full width regardless of the persisted rail state. jsdom does not
|
|
15
|
+
* evaluate the media query, so this asserts presence in the DOM, not computed visibility — the
|
|
16
|
+
* CSS half (`.root[data-collapsed] .navExtra { display: none }`) is exercised by the guard rules
|
|
17
|
+
* it shares with `.childList`/`.sectionBand`, not by this test.
|
|
18
|
+
* 3. `sections={[]}` plus only `navExtra` renders cleanly: the nav Stack holds exactly the
|
|
19
|
+
* extra content, no orphan section wrapper.
|
|
20
|
+
* 4. Omitting the prop reproduces today's DOM exactly — the nav Stack's child count matches
|
|
21
|
+
* `sections.length`, with no stray wrapper appended for the (absent) slot. This is the
|
|
22
|
+
* regression guard for every existing consumer.
|
|
23
|
+
*/
|
|
24
|
+
import { MantineProvider } from '@mantine/core'
|
|
25
|
+
import { render, screen } from '@testing-library/react'
|
|
26
|
+
import { describe, expect, test } from 'bun:test'
|
|
27
|
+
import type { ReactNode } from 'react'
|
|
28
|
+
import { AppSidebar } from './app-sidebar'
|
|
29
|
+
import type { AppSidebarProps } from './app-sidebar'
|
|
30
|
+
|
|
31
|
+
const BRAND = { name: 'Argo' }
|
|
32
|
+
const ONE_SECTION: AppSidebarProps['sections'] = [
|
|
33
|
+
{ label: 'Main', items: [{ key: 'home', label: 'Home', icon: null }] },
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
function renderSidebar(props: Partial<AppSidebarProps> & { navExtra?: ReactNode }) {
|
|
37
|
+
return render(
|
|
38
|
+
<MantineProvider>
|
|
39
|
+
<AppSidebar
|
|
40
|
+
brand={BRAND}
|
|
41
|
+
sections={ONE_SECTION}
|
|
42
|
+
collapsed={false}
|
|
43
|
+
onToggleCollapse={() => {}}
|
|
44
|
+
onClose={() => {}}
|
|
45
|
+
{...props}
|
|
46
|
+
/>
|
|
47
|
+
</MantineProvider>,
|
|
48
|
+
)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** The nav `ScrollArea`'s content Stack — the direct parent of `sections` and `navExtra`. */
|
|
52
|
+
function navStack(container: HTMLElement): Element {
|
|
53
|
+
const stack = container.querySelector('.mantine-ScrollArea-content > .mantine-Stack-root')
|
|
54
|
+
if (!stack) throw new Error('expected the nav ScrollArea content Stack to render')
|
|
55
|
+
return stack
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
describe('navExtra', () => {
|
|
59
|
+
test('renders inside the nav scroll region, as the last child, when expanded', () => {
|
|
60
|
+
const { container } = renderSidebar({
|
|
61
|
+
navExtra: <div data-testid="nav-extra">Extra</div>,
|
|
62
|
+
})
|
|
63
|
+
const extra = screen.getByTestId('nav-extra')
|
|
64
|
+
const stack = navStack(container)
|
|
65
|
+
expect(stack.contains(extra)).toBe(true)
|
|
66
|
+
expect(stack.lastElementChild?.contains(extra)).toBe(true)
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
test('stays mounted in the DOM when collapsed — hiding it is CSS-only, not a JS gate', () => {
|
|
70
|
+
const { container } = renderSidebar({
|
|
71
|
+
collapsed: true,
|
|
72
|
+
navExtra: <div data-testid="nav-extra">Extra</div>,
|
|
73
|
+
})
|
|
74
|
+
const extra = screen.getByTestId('nav-extra')
|
|
75
|
+
const stack = navStack(container)
|
|
76
|
+
// Presence, not visibility — jsdom never evaluates the `min-width: sm` media query that hides
|
|
77
|
+
// this in the rail. A regression here (reintroducing `!collapsed &&` in app-sidebar.tsx) would
|
|
78
|
+
// fail this assertion instead of only breaking silently in a real mobile-drawer browser.
|
|
79
|
+
expect(stack.contains(extra)).toBe(true)
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
test('sections={[]} plus only navExtra renders cleanly — no orphan section wrapper', () => {
|
|
83
|
+
const { container } = renderSidebar({
|
|
84
|
+
sections: [],
|
|
85
|
+
navExtra: <div data-testid="nav-extra">Extra</div>,
|
|
86
|
+
})
|
|
87
|
+
const stack = navStack(container)
|
|
88
|
+
expect(screen.getByTestId('nav-extra')).toBeTruthy()
|
|
89
|
+
expect(stack.children.length).toBe(1)
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
test("omitting the prop reproduces today's DOM — no stray wrapper for the unused slot", () => {
|
|
93
|
+
const { container } = renderSidebar({})
|
|
94
|
+
expect(screen.queryByTestId('nav-extra')).toBeNull()
|
|
95
|
+
const stack = navStack(container)
|
|
96
|
+
expect(stack.children.length).toBe(ONE_SECTION.length)
|
|
97
|
+
})
|
|
98
|
+
})
|
|
@@ -92,6 +92,24 @@ export type AppSidebarProps = {
|
|
|
92
92
|
* fixed, non-scrolling row. Pair with basalt-ui/commands' openSpotlight.
|
|
93
93
|
*/
|
|
94
94
|
search?: SidebarSearchConfig
|
|
95
|
+
/**
|
|
96
|
+
* Arbitrary content appended after `sections` inside the nav `ScrollArea` — a tree, a filter
|
|
97
|
+
* panel, a project list, anything a set of `SidebarItem`s can't express. Renders as the last
|
|
98
|
+
* child of the scrolling nav column, so a long list scrolls with the rest of the nav instead of
|
|
99
|
+
* fighting it for height. Pass `sections={[]}` to use this slot exclusively; the section-spacing
|
|
100
|
+
* rule only fires between adjacent children, so an empty `sections` produces no orphan divider or
|
|
101
|
+
* dead padding above it.
|
|
102
|
+
*
|
|
103
|
+
* Hidden on the collapsed desktop rail; still present in the mobile drawer, which opens at full
|
|
104
|
+
* width. The rail is ~56px of icon buttons with no sensible representation for arbitrary
|
|
105
|
+
* consumer content, so the CSS media query that drives the rail hides this slot the same way it
|
|
106
|
+
* hides `.childList` — never a JS check on `collapsed`, since that one value is shared by the
|
|
107
|
+
* rail AND the drawer (only the media query tells them apart). `SidebarSearch` gets to adapt
|
|
108
|
+
* itself to the rail (it takes `collapsed` and renders its own icon-only form) because the shell
|
|
109
|
+
* owns that control; it cannot adapt content it knows nothing about, so this slot hides instead
|
|
110
|
+
* of squashing it.
|
|
111
|
+
*/
|
|
112
|
+
navExtra?: ReactNode
|
|
95
113
|
}
|
|
96
114
|
|
|
97
115
|
/** Inline collapse/expand chevrons — keeps the shell icon-dependency-free. */
|
|
@@ -457,6 +475,7 @@ export function AppSidebar({
|
|
|
457
475
|
footerExtra,
|
|
458
476
|
account,
|
|
459
477
|
search,
|
|
478
|
+
navExtra,
|
|
460
479
|
}: AppSidebarProps) {
|
|
461
480
|
// Density-tracking Menu dropdown width (`SPACE_STEP.sidebarSettingsMenuWidth`) — read the ACTIVE
|
|
462
481
|
// resolved level, not the frozen level-0 constant (see that constant's own doc in
|
|
@@ -622,6 +641,9 @@ export function AppSidebar({
|
|
|
622
641
|
</div>
|
|
623
642
|
)
|
|
624
643
|
})}
|
|
644
|
+
{navExtra !== undefined && navExtra !== null && (
|
|
645
|
+
<div className={classes.navExtra}>{navExtra}</div>
|
|
646
|
+
)}
|
|
625
647
|
</Stack>
|
|
626
648
|
</ScrollArea>
|
|
627
649
|
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `BasaltShell`'s `sidebarNavExtra` forwarding path — `AppSidebar` (exercised directly in
|
|
3
|
+
* `app-sidebar.test.tsx`) is not the only place this prop can break. `BasaltShell` destructures
|
|
4
|
+
* `sidebarNavExtra` and hands it to `AppSidebar` as `navExtra`; a typo'd destructure key, a dropped
|
|
5
|
+
* prop, or a `sidebarFooterExtra`/`sidebarNavExtra` mix-up in that hand-off would ship silently
|
|
6
|
+
* without a test that renders `BasaltShell` itself.
|
|
7
|
+
*/
|
|
8
|
+
import { MantineProvider } from '@mantine/core'
|
|
9
|
+
import { render, screen } from '@testing-library/react'
|
|
10
|
+
import { describe, expect, test } from 'bun:test'
|
|
11
|
+
import { BasaltShell } from './index'
|
|
12
|
+
import type { SidebarSection } from './index'
|
|
13
|
+
|
|
14
|
+
const BRAND = { name: 'Argo' }
|
|
15
|
+
const ONE_SECTION: SidebarSection[] = [
|
|
16
|
+
{ label: 'Main', items: [{ key: 'home', label: 'Home', icon: null }] },
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
describe('BasaltShell sidebarNavExtra', () => {
|
|
20
|
+
test("reaches AppSidebar's nav scroll region through the BasaltShell -> AppSidebar hand-off", () => {
|
|
21
|
+
const { container } = render(
|
|
22
|
+
<MantineProvider>
|
|
23
|
+
<BasaltShell
|
|
24
|
+
brand={BRAND}
|
|
25
|
+
sections={ONE_SECTION}
|
|
26
|
+
sidebarNavExtra={<div data-testid="nav-extra">Extra</div>}
|
|
27
|
+
/>
|
|
28
|
+
</MantineProvider>,
|
|
29
|
+
)
|
|
30
|
+
const extra = screen.getByTestId('nav-extra')
|
|
31
|
+
const stack = container.querySelector('.mantine-ScrollArea-content > .mantine-Stack-root')
|
|
32
|
+
expect(stack).not.toBeNull()
|
|
33
|
+
expect(stack?.contains(extra)).toBe(true)
|
|
34
|
+
})
|
|
35
|
+
})
|
package/src/shell/index.tsx
CHANGED
|
@@ -115,6 +115,15 @@ export type BasaltShellProps = {
|
|
|
115
115
|
globalActions?: ReactNode
|
|
116
116
|
/** Extra content appended to the sidebar footer, beside the settings menu (mobile close, etc.). */
|
|
117
117
|
sidebarFooterExtra?: ReactNode
|
|
118
|
+
/**
|
|
119
|
+
* Arbitrary content appended after `sections` inside the sidebar's nav scroll region (a tree, a
|
|
120
|
+
* filter panel, a project list, …). Hidden on the collapsed desktop rail; still present in the
|
|
121
|
+
* mobile drawer, which opens at full width — see `AppSidebarProps.navExtra`'s JSDoc for why.
|
|
122
|
+
* `AppSidebar` renders once and serves both the desktop rail and the mobile drawer (collapse
|
|
123
|
+
* styling is gated behind a `min-width: sm` media query, not a second render), so this slot
|
|
124
|
+
* reaches both.
|
|
125
|
+
*/
|
|
126
|
+
sidebarNavExtra?: ReactNode
|
|
118
127
|
/** Entries appended to the sidebar settings menu. */
|
|
119
128
|
settingsMenuItems?: SettingsMenuItem[]
|
|
120
129
|
/**
|
|
@@ -262,6 +271,7 @@ export function BasaltShell({
|
|
|
262
271
|
renderNavLink,
|
|
263
272
|
globalActions,
|
|
264
273
|
sidebarFooterExtra,
|
|
274
|
+
sidebarNavExtra,
|
|
265
275
|
settingsMenuItems,
|
|
266
276
|
storageKey = 'basalt-sidebar-collapsed',
|
|
267
277
|
collapsed: collapsedProp,
|
|
@@ -332,6 +342,7 @@ export function BasaltShell({
|
|
|
332
342
|
onToggleCollapse={toggleCollapse}
|
|
333
343
|
onClose={closeMobile}
|
|
334
344
|
footerExtra={sidebarFooterExtra}
|
|
345
|
+
navExtra={sidebarNavExtra}
|
|
335
346
|
{...(renderNavLink !== undefined && { renderNavLink })}
|
|
336
347
|
{...(settingsMenuItems !== undefined && { settingsMenuItems })}
|
|
337
348
|
{...(account !== undefined && { account })}
|