@bensdev/react-sidebar 0.2.0 → 0.3.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 CHANGED
@@ -4,14 +4,44 @@ All notable changes to this project are documented here. Format loosely follows
4
4
  [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). Pre-1.0: breaking changes bump the
5
5
  **minor** version, not the major.
6
6
 
7
- ## [0.1.0] — Unreleased
8
-
9
- Initial extraction from the B-Lines admin/user sidebars into a standalone package.
7
+ ## [0.2.0] — Unreleased
10
8
 
9
+ - **Fixed:** `collapsible: false` groups (documented as "children always visible") were silently
10
+ forced into the collapsed rail's flyout/ignore treatment, hiding their entire child list the
11
+ moment the sidebar collapsed — worst for a group with no icon of its own, which rendered as a
12
+ blank row. `collapsible: false` now overrides `collapsedGroupBehavior` and always stays fully
13
+ expanded; an icon-less trigger row is skipped on the rail instead of rendering blank.
14
+ - **Fixed:** nested rows that do render on the rail (the above, or `collapsedGroupBehavior:
15
+ "expand"`) had their icon pushed off-center by depth-based indentation, which only makes sense
16
+ once labels are visible. Indentation now resets to flush on the rail (excluding flyout content,
17
+ see below).
18
+ - **Fixed:** the collapsed-rail **flyout** — one of three `collapsedGroupBehavior` modes, and the
19
+ default — rendered every row inside it icon-only: the rail's label/badge/chevron-hiding rules
20
+ are all more specific than the flyout's "show them again" carve-out, so `display: none` silently
21
+ won regardless of source order. This affected any group with a badge or a nested sub-group (the
22
+ playground's own "Operations" example, badge included, was hit by it). The carve-out is
23
+ `!important` now.
24
+ - **Fixed:** the mobile drawer is portaled to `document.body`, which left an empty `.bsb-root`
25
+ column (sized to the desktop rail's width) sitting in the host layout at the sidebar's original
26
+ position, and left the portaled drawer itself with none of the sidebar's `--bsb-*` tokens,
27
+ resets, or dark-mode detection (all scoped to an `.bsb-root` ancestor the portal boundary broke).
28
+ The in-place wrapper is gone; the theme/reset scope now travels through the portal with the
29
+ drawer instead.
11
30
  - Collapse toggle now defaults to the sidebar's own background color (`--bsb-toggle-bg:
12
31
  var(--bsb-bg)`) with a stronger elevation shadow and a translucent ring border, so it reads as
13
32
  a floating control instead of an oddly-tinted, hard-to-see button. `greenMist`'s toggle tokens
14
- were updated to match.
33
+ were updated to match. (This was a specificity bug: a bare `.bsb-toggle` rule tied with the
34
+ `.bsb-root button` reset and lost at rest, leaving the button borderless and near-invisible
35
+ until `:hover`. Published `0.1.0` still has it; fixed here by scoping to `.bsb-root .bsb-toggle`.)
36
+ - Docs: clarified that `collapsible: false` beats `collapsedGroupBehavior` (see above), added a
37
+ "Layout requirements" section covering the `height: 100%` contract `.bsb-nav` scrolling depends
38
+ on and Next.js App Router's fixed CSS import order, and noted the mobile drawer's portal
39
+ behavior.
40
+
41
+ ## [0.1.0]
42
+
43
+ Initial extraction from the B-Lines admin/user sidebars into a standalone package.
44
+
15
45
  - Router-agnostic `<Sidebar>` component: works with plain `<a>`, React Router, Next.js `Link`,
16
46
  or any custom link component via `renderLink`.
17
47
  - Full CSS custom-property theming (`--bsb-*`), zero Tailwind requirement.
package/README.md CHANGED
@@ -30,6 +30,40 @@ import '@bensdev/react-sidebar/styles.css';
30
30
  Import the CSS once, near your app's own global stylesheet (before it, if you want your app's
31
31
  CSS to win any conflicts).
32
32
 
33
+ ## Layout requirements
34
+
35
+ `.bsb-root` is `height: 100%`, and its internal `.bsb-nav` is the only element that scrolls
36
+ (`overflow-y: auto`). Both only work if every ancestor between `<Sidebar>` and a definite-height
37
+ container also resolves to a real height — percentage heights fall back to `auto` otherwise. Miss
38
+ this and two things break together: the rail grows to fit its content instead of scrolling
39
+ internally, and the page picks up a scrollbar it shouldn't have — most visible as a height jump
40
+ whenever a group opens/closes.
41
+
42
+ Give your root layout a definite height and let `<Sidebar>` sit as a flex/grid item next to your
43
+ content:
44
+
45
+ ```css
46
+ html, body, #root { height: 100%; }
47
+ ```
48
+
49
+ ```tsx
50
+ <div style={{ display: 'flex', height: '100vh', overflow: 'hidden' }}>
51
+ <Sidebar items={items} />
52
+ <main style={{ flex: 1, overflow: 'auto' }}>{children}</main>
53
+ </div>
54
+ ```
55
+
56
+ A `min-height` (or no height at all) on that wrapper isn't enough — `min-height` doesn't give
57
+ descendants a *definite* height to resolve `height: 100%` against, so `.bsb-nav` still won't scroll.
58
+
59
+ **CSS import order.** "Near your app's own global stylesheet" above assumes your bundler honors
60
+ source order. Frameworks that bundle by module graph — Next.js App Router in particular — always
61
+ place a component's own imported CSS *after* the root layout's global CSS, regardless of which
62
+ file imports which. If your global CSS and `@bensdev/react-sidebar/styles.css` both set the same
63
+ property on the same selector at equal specificity, the sidebar's rule wins there no matter what
64
+ your source order says — override with a more specific selector (`.my-app .bsb-root`) or
65
+ `!important` instead of relying on import order.
66
+
33
67
  ## Quick start
34
68
 
35
69
  ```tsx
@@ -134,11 +168,17 @@ Router's `NavLink end`), and `isActive` to override the computed value entirely.
134
168
  ]}
135
169
  ```
136
170
 
137
- A group with `collapsible: false` renders as a static section (its `label` becomes a heading,
138
- children are always visible — a lighter-weight alternative to a top-level `heading` + flat items).
139
- When the sidebar is collapsed to its icon rail, a group renders as a hover/focus **flyout** by
140
- default (`collapsedGroupBehavior="flyout"`); set it to `"expand"` or `"ignore"` for different
141
- collapsed-rail behavior.
171
+ A group with `collapsible: false` renders as a static section: its `label` becomes a heading and
172
+ children are always visible — a lighter-weight alternative to a top-level `heading` + flat items.
173
+ That holds on the collapsed icon rail too: `collapsible: false` overrides `collapsedGroupBehavior`
174
+ entirely, so its children never fold into a flyout, and never disappear. Give the group its own
175
+ `icon` if you want a row for it on the rail as well; without one, only the children (each with
176
+ their own icon) render there, the same as a `heading` would.
177
+
178
+ *Collapsible* groups (the default) present differently once the sidebar collapses to its icon
179
+ rail: a hover/focus **flyout** by default (`collapsedGroupBehavior="flyout"`), or set it to
180
+ `"expand"` (rendered inline and indented, same as expanded) or `"ignore"` (trigger only, children
181
+ unreachable while collapsed) instead.
142
182
 
143
183
  ## Collapse / expand
144
184
 
@@ -172,6 +212,12 @@ The drawer includes a focus trap, Escape-to-close, backdrop-click-to-close, body
172
212
  respects `prefers-reduced-motion` — all on by default and individually toggleable
173
213
  (`trapFocus`, `closeOnEscape`, `closeOnBackdropClick`, `lockScroll`, `reduceMotion`).
174
214
 
215
+ It's portaled to `document.body` by default (pass `portalTarget` for somewhere else, or
216
+ `disablePortal` to render it in place), so it escapes any `overflow: hidden`/`z-index` ancestor
217
+ your layout has. The sidebar's theme tokens, resets, and dark-mode detection travel with it across
218
+ the portal boundary — no `.bsb-root` wrapper is left behind at the drawer's original position, and
219
+ none of its own CSS is scoped to depend on being a descendant of one.
220
+
175
221
  ## Theming
176
222
 
177
223
  Every visual value is a `--bsb-*` CSS custom property (see `src/styles.css` for the full list —
package/dist/index.cjs CHANGED
@@ -522,10 +522,17 @@ function SidebarGroupRow({ item, depth }) {
522
522
  children: /* @__PURE__ */ jsxRuntime.jsx(ChevronDownIcon, { size: 14 })
523
523
  }
524
524
  ),
525
- showTooltip && behavior !== "flyout" && /* @__PURE__ */ jsxRuntime.jsx(Tooltip, { id: tooltipId, className: ctx.classNames.tooltip, children: item.tooltip ?? item.label })
525
+ showTooltip && (!collapsible || behavior !== "flyout") && /* @__PURE__ */ jsxRuntime.jsx(Tooltip, { id: tooltipId, className: ctx.classNames.tooltip, children: item.tooltip ?? item.label })
526
526
  ]
527
527
  }
528
528
  );
529
+ if (!collapsible) {
530
+ const showTrigger = item.icon != null || !isRail;
531
+ return /* @__PURE__ */ jsxRuntime.jsxs("li", { className: cx("bsb-list-item", "bsb-group"), children: [
532
+ showTrigger && trigger,
533
+ /* @__PURE__ */ jsxRuntime.jsx("ul", { id: panelId, className: cx("bsb-group__panel", ctx.classNames.groupPanel), children: /* @__PURE__ */ jsxRuntime.jsx(SidebarNodeList, { items: item.items, depth: depth + 1 }) })
534
+ ] });
535
+ }
529
536
  if (isRail && behavior === "flyout") {
530
537
  return /* @__PURE__ */ jsxRuntime.jsxs("li", { className: cx("bsb-list-item", "bsb-group", "bsb-group--flyout"), children: [
531
538
  trigger,
@@ -548,7 +555,7 @@ function SidebarGroupRow({ item, depth }) {
548
555
  }
549
556
  return /* @__PURE__ */ jsxRuntime.jsxs("li", { className: cx("bsb-list-item", "bsb-group"), children: [
550
557
  trigger,
551
- (isOpen || !collapsible) && /* @__PURE__ */ jsxRuntime.jsx("ul", { id: panelId, className: cx("bsb-group__panel", ctx.classNames.groupPanel), children: /* @__PURE__ */ jsxRuntime.jsx(SidebarNodeList, { items: item.items, depth: depth + 1 }) })
558
+ isOpen && /* @__PURE__ */ jsxRuntime.jsx("ul", { id: panelId, className: cx("bsb-group__panel", ctx.classNames.groupPanel), children: /* @__PURE__ */ jsxRuntime.jsx(SidebarNodeList, { items: item.items, depth: depth + 1 }) })
552
559
  ] });
553
560
  }
554
561
  function SidebarNode({ item, depth }) {
@@ -852,7 +859,10 @@ function MobileDrawer({
852
859
  closeLabel,
853
860
  classNames,
854
861
  durationMs = 400,
855
- children
862
+ children,
863
+ rootClassName,
864
+ rootStyle,
865
+ rootDataAttrs
856
866
  }) {
857
867
  const { mounted, state, onPanelTransitionEnd } = useMountTransition(open, durationMs);
858
868
  const panelRef = React21__namespace.useRef(null);
@@ -864,7 +874,7 @@ function MobileDrawer({
864
874
  useBodyScrollLock(lockScroll && open);
865
875
  if (!mounted) return null;
866
876
  const widthValue = typeof width === "number" ? `${width}px` : width;
867
- return /* @__PURE__ */ jsxRuntime.jsxs(Portal, { target: portalTarget, disabled: disablePortal, children: [
877
+ return /* @__PURE__ */ jsxRuntime.jsx(Portal, { target: portalTarget, disabled: disablePortal, children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: cx(rootClassName, "bsb-root--drawer-host"), style: rootStyle, ...rootDataAttrs, children: [
868
878
  /* @__PURE__ */ jsxRuntime.jsx(
869
879
  "div",
870
880
  {
@@ -903,7 +913,7 @@ function MobileDrawer({
903
913
  ]
904
914
  }
905
915
  )
906
- ] });
916
+ ] }) });
907
917
  }
908
918
  function widthToCss(value) {
909
919
  if (value == null) return void 0;
@@ -1147,35 +1157,32 @@ var Sidebar = React21__namespace.forwardRef(function Sidebar2(props, ref) {
1147
1157
  )
1148
1158
  }
1149
1159
  ),
1150
- isMobile && /* @__PURE__ */ jsxRuntime.jsx(
1151
- "div",
1152
- {
1153
- ref: rootRef,
1154
- className: rootClassName,
1155
- style: mergedStyle,
1156
- ...rootDataAttrs,
1157
- children: /* @__PURE__ */ jsxRuntime.jsx(
1158
- MobileDrawer,
1159
- {
1160
- open: mobileOpen,
1161
- onClose: closeMobile,
1162
- ariaLabel,
1163
- width: drawerWidth,
1164
- position,
1165
- closeOnEscape,
1166
- closeOnBackdropClick,
1167
- trapFocus,
1168
- lockScroll,
1169
- restoreFocus,
1170
- portalTarget,
1171
- disablePortal,
1172
- closeLabel: "Close navigation",
1173
- classNames,
1174
- children: /* @__PURE__ */ jsxRuntime.jsx(SidebarShell, { ...shellProps, collapsed: false, showToggle: false, onToggle: toggleCollapsed, togglePosition: position, toggleStrategy, toggleLabels: { expand: "Expand sidebar", collapse: "Collapse sidebar" } })
1175
- }
1176
- )
1177
- }
1178
- )
1160
+ isMobile && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
1161
+ /* @__PURE__ */ jsxRuntime.jsx("span", { ref: rootRef, "aria-hidden": "true", style: { display: "none" } }),
1162
+ /* @__PURE__ */ jsxRuntime.jsx(
1163
+ MobileDrawer,
1164
+ {
1165
+ open: mobileOpen,
1166
+ onClose: closeMobile,
1167
+ ariaLabel,
1168
+ width: drawerWidth,
1169
+ position,
1170
+ closeOnEscape,
1171
+ closeOnBackdropClick,
1172
+ trapFocus,
1173
+ lockScroll,
1174
+ restoreFocus,
1175
+ portalTarget,
1176
+ disablePortal,
1177
+ closeLabel: "Close navigation",
1178
+ classNames,
1179
+ rootClassName,
1180
+ rootStyle: mergedStyle,
1181
+ rootDataAttrs,
1182
+ children: /* @__PURE__ */ jsxRuntime.jsx(SidebarShell, { ...shellProps, collapsed: false, showToggle: false, onToggle: toggleCollapsed, togglePosition: position, toggleStrategy, toggleLabels: { expand: "Expand sidebar", collapse: "Collapse sidebar" } })
1183
+ }
1184
+ )
1185
+ ] })
1179
1186
  ] });
1180
1187
  });
1181
1188
  Sidebar.displayName = "Sidebar";