@bensdev/react-sidebar 0.1.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 ADDED
@@ -0,0 +1,22 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented here. Format loosely follows
4
+ [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). Pre-1.0: breaking changes bump the
5
+ **minor** version, not the major.
6
+
7
+ ## [0.1.0] — Unreleased
8
+
9
+ Initial extraction from the B-Lines admin/user sidebars into a standalone package.
10
+
11
+ - Router-agnostic `<Sidebar>` component: works with plain `<a>`, React Router, Next.js `Link`,
12
+ or any custom link component via `renderLink`.
13
+ - Full CSS custom-property theming (`--bsb-*`), zero Tailwind requirement.
14
+ - `greenMist`, `slate`, and `midnight` built-in theme presets.
15
+ - Controlled/uncontrolled/persisted collapse state; controlled/uncontrolled mobile drawer.
16
+ - Nested groups with an accordion (expanded) and flyout (collapsed-rail) presentation.
17
+ - Links, actions, groups, headings, dividers, and fully custom items.
18
+ - Zero runtime dependencies; `react`/`react-dom` are the only peer dependencies.
19
+ - Accessible by default: keyboard navigation, focus trap + Escape in the drawer, `aria-current`,
20
+ `aria-expanded`, visible focus rings, reduced-motion support.
21
+ - SSR-safe (no `window`/`document` access during render); ships a `"use client"` banner for
22
+ Next.js App Router.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 bensdev
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,279 @@
1
+ # @bensdev/react-sidebar
2
+
3
+ A themeable, router-agnostic, zero-dependency React sidebar: collapsible rail, nested groups,
4
+ a mobile drawer, and full CSS-custom-property theming. No Tailwind required, no router required,
5
+ no icon library required — bring your own of each, or none at all.
6
+
7
+ - **Zero runtime dependencies.** `react` / `react-dom` are the only peer dependencies.
8
+ - **Router-agnostic.** Works with a plain `<a>` out of the box; plug in React Router's `Link`,
9
+ Next.js's `Link`, or anything else via `linkComponent` / `renderLink`.
10
+ - **Themeable via CSS variables.** Every color, size, radius, and transition is a `--bsb-*`
11
+ custom property. Override with the `theme` prop, plain CSS, or per-slot `classNames`.
12
+ - **Deeply configurable.** Links, actions, nested groups, headings, dividers, badges, disabled/
13
+ hidden items, and fully custom rows — all data-driven, no forking required.
14
+ - **Accessible.** Keyboard navigation, focus trap + Escape in the drawer, `aria-current`,
15
+ `aria-expanded`, visible focus rings, `prefers-reduced-motion` support.
16
+ - **SSR-safe.** No `window`/`document` access during render; ships a `"use client"` banner so it
17
+ drops straight into a Next.js App Router Server Component tree.
18
+
19
+ ## Install
20
+
21
+ ```bash
22
+ npm install @bensdev/react-sidebar
23
+ ```
24
+
25
+ ```tsx
26
+ import { Sidebar } from '@bensdev/react-sidebar';
27
+ import '@bensdev/react-sidebar/styles.css';
28
+ ```
29
+
30
+ Import the CSS once, near your app's own global stylesheet (before it, if you want your app's
31
+ CSS to win any conflicts).
32
+
33
+ ## Quick start
34
+
35
+ ```tsx
36
+ import { Sidebar, greenMist, type SidebarItem } from '@bensdev/react-sidebar';
37
+ import '@bensdev/react-sidebar/styles.css';
38
+ import { Home, Store, Settings, LogOut } from 'lucide-react';
39
+
40
+ const items: SidebarItem[] = [
41
+ { label: 'Home', href: '/home', icon: <Home size={18} /> },
42
+ { label: 'Suppliers', href: '/suppliers', icon: <Store size={18} />, badge: 12 },
43
+ { label: 'Settings', href: '/settings', icon: <Settings size={18} /> },
44
+ ];
45
+
46
+ export function AppSidebar() {
47
+ return (
48
+ <Sidebar
49
+ items={items}
50
+ theme={greenMist}
51
+ brand={{ logo: 'B', title: 'My App', subtitle: 'Dashboard' }}
52
+ user={{ name: 'Ada Lovelace', email: 'ada@example.com' }}
53
+ footerAction={{ icon: <LogOut size={15} />, label: 'Logout', onClick: () => {} }}
54
+ />
55
+ );
56
+ }
57
+ ```
58
+
59
+ With no other props, active-route detection falls back to `window.location.pathname` and links
60
+ render as plain `<a href>` — this works in any React app, router or not. For an SPA router, read
61
+ on.
62
+
63
+ ## Routing recipes
64
+
65
+ The package computes active state itself, so it behaves identically regardless of router. Pass
66
+ `currentPath` explicitly whenever you have a client-side router — the `window.location` fallback
67
+ only reacts to `popstate`/`hashchange`, not to `pushState`-based navigation.
68
+
69
+ **React Router (v6/v7)**
70
+
71
+ ```tsx
72
+ import { Link, useLocation } from 'react-router-dom';
73
+
74
+ <Sidebar items={items} linkComponent={Link} hrefProp="to" currentPath={useLocation().pathname} />
75
+ ```
76
+
77
+ **Next.js App Router**
78
+
79
+ ```tsx
80
+ 'use client';
81
+ import Link from 'next/link';
82
+ import { usePathname } from 'next/navigation';
83
+
84
+ <Sidebar items={items} linkComponent={Link} currentPath={usePathname()} />
85
+ ```
86
+
87
+ **TanStack Router**
88
+
89
+ ```tsx
90
+ import { Link, useRouterState } from '@tanstack/react-router';
91
+
92
+ <Sidebar
93
+ items={items}
94
+ linkComponent={Link}
95
+ currentPath={useRouterState({ select: (s) => s.location.pathname })}
96
+ />
97
+ ```
98
+
99
+ **Anything else** — analytics-wrapped links, a design-system `<Link>` with a different prop
100
+ shape, or navigation that isn't a real `<a>` at all:
101
+
102
+ ```tsx
103
+ <Sidebar
104
+ items={items}
105
+ renderLink={({ href, isActive, className, children, props }) => (
106
+ <MyLink to={href} data-active={isActive} className={className} {...props}>
107
+ {children}
108
+ </MyLink>
109
+ )}
110
+ />
111
+ ```
112
+
113
+ ## Items
114
+
115
+ ```ts
116
+ type SidebarItem =
117
+ | { href: string; label: ReactNode; icon?: ReactNode; badge?: ReactNode; end?: boolean, ... } // link (default)
118
+ | { type: 'action'; label: ReactNode; onSelect: (e) => void, ... } // button, no navigation
119
+ | { type: 'group'; label: ReactNode; items: SidebarItem[]; collapsible?: boolean, ... } // nested submenu
120
+ | { type: 'heading'; label: ReactNode } // section label
121
+ | { type: 'divider' } // horizontal rule
122
+ | { type: 'custom'; render: (ctx) => ReactNode } // anything at all
123
+ ```
124
+
125
+ Every item accepts `hidden` (the standard way to do role-based gating — filter is applied at
126
+ render time) and `disabled`. Links accept `end` for exact-match active state (mirrors React
127
+ Router's `NavLink end`), and `isActive` to override the computed value entirely.
128
+
129
+ ```tsx
130
+ { type: 'group', label: 'Admin', items: [
131
+ { label: 'Dashboard', href: '/admin', end: true, icon: <LayoutDashboard size={18} /> },
132
+ { label: 'Users', href: '/admin/users', icon: <Users size={18} />, badge: { content: 3, tone: 'danger' } },
133
+ { label: 'Billing', href: '/admin/billing', hidden: !user.isAdmin },
134
+ ]}
135
+ ```
136
+
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.
142
+
143
+ ## Collapse / expand
144
+
145
+ ```tsx
146
+ // Uncontrolled, persisted to localStorage under a key you choose:
147
+ <Sidebar items={items} persistCollapse="my-app-sidebar-collapsed" />
148
+
149
+ // Fully controlled:
150
+ <Sidebar items={items} collapsed={collapsed} onCollapsedChange={setCollapsed} />
151
+
152
+ // Non-collapsible:
153
+ <Sidebar items={items} collapsible={false} />
154
+ ```
155
+
156
+ `persistCollapse` also accepts a `StorageAdapter` (`{ getItem, setItem, subscribe? }`) if you
157
+ want to back it with something other than `localStorage`.
158
+
159
+ ## Mobile drawer
160
+
161
+ Below `breakpoint` (default `1024`px, or pass any media-query string), the desktop rail is
162
+ replaced by a slide-in drawer. Drive it from your own hamburger button:
163
+
164
+ ```tsx
165
+ const [open, setOpen] = useState(false);
166
+
167
+ <button onClick={() => setOpen(true)}>Menu</button>
168
+ <Sidebar items={items} mobileOpen={open} onMobileClose={() => setOpen(false)} />
169
+ ```
170
+
171
+ The drawer includes a focus trap, Escape-to-close, backdrop-click-to-close, body scroll lock, and
172
+ respects `prefers-reduced-motion` — all on by default and individually toggleable
173
+ (`trapFocus`, `closeOnEscape`, `closeOnBackdropClick`, `lockScroll`, `reduceMotion`).
174
+
175
+ ## Theming
176
+
177
+ Every visual value is a `--bsb-*` CSS custom property (see `src/styles.css` for the full list —
178
+ layout, radii, typography, motion, and color tokens). Three ways to change them, all without
179
+ touching package source:
180
+
181
+ **1. The `theme` prop** (inline styles, wins over everything):
182
+
183
+ ```tsx
184
+ <Sidebar items={items} theme={{ bg: '#0f172a', itemActiveBg: '#1d4ed8', radiusItem: '12px' }} />
185
+
186
+ // Split values per color scheme:
187
+ <Sidebar items={items} colorScheme="auto" theme={{ light: { bg: '#fff' }, dark: { bg: '#0f172a' } }} />
188
+ ```
189
+
190
+ **2. Plain CSS:**
191
+
192
+ ```css
193
+ .my-app .bsb-root {
194
+ --bsb-bg: #0f3a18;
195
+ --bsb-item-active-bg: rgba(31, 110, 46, 0.6);
196
+ }
197
+ ```
198
+
199
+ **3. Per-slot class names** (for Tailwind users who'd rather use utilities than tokens):
200
+
201
+ ```tsx
202
+ <Sidebar items={items} classNames={{ item: 'my-tailwind-classes', itemActive: 'bg-primary-600' }} />
203
+ ```
204
+
205
+ ### Color scheme
206
+
207
+ `colorScheme` is `"light" | "dark" | "auto"` (default `"auto"`). In `"auto"` mode the sidebar
208
+ looks for a `.dark`, `[data-theme="dark"]`, or `[data-color-scheme="dark"]` ancestor (or the
209
+ matching `light` variant, or `prefers-color-scheme` as a last resort) and re-resolves whenever
210
+ that ancestor's attributes change — no extra JS wiring needed if your app already toggles a class
211
+ on `<html>`.
212
+
213
+ ### Presets
214
+
215
+ ```tsx
216
+ import { greenMist, midnight, slate } from '@bensdev/react-sidebar';
217
+
218
+ <Sidebar items={items} theme={greenMist} />
219
+ ```
220
+
221
+ - `slate` — the stylesheet's own defaults (useful for explicitness).
222
+ - `greenMist` — the original B-Lines palette, including its `light`/`dark` split.
223
+ - `midnight` — a neutral dark theme.
224
+
225
+ Build your own with `defineTheme({...})` for autocomplete, or just pass a plain object.
226
+
227
+ ## Header / footer
228
+
229
+ ```tsx
230
+ <Sidebar
231
+ items={items}
232
+ brand={{ logo: <Logo />, title: 'Acme', subtitle: 'Admin', badge: 'Beta' }}
233
+ user={{ name: user.name, email: user.email, avatarUrl: user.avatarUrl }}
234
+ footerAction={{ icon: <LogOut size={15} />, label: 'Logout', onClick: logout }}
235
+ collapsedFooter="stack" // keep the logout button reachable even when collapsed
236
+ />
237
+ ```
238
+
239
+ For full control, `header` and `footer` accept any `ReactNode` or a `(ctx) => ReactNode`
240
+ render function, replacing the built-in brand/user blocks entirely.
241
+
242
+ ## Escape hatches
243
+
244
+ - `classNames` — override any of ~30 named slots (`root`, `item`, `itemActive`, `badge`,
245
+ `groupPanel`, `toggle`, `drawer`, …) without forking styles.
246
+ - `slots` — swap the collapse chevron, group chevron, close icon, or tooltip renderer.
247
+ - `renderItem(item, ctx, defaultNode)` — intercept any single row and return your own markup
248
+ (return `undefined` to fall through to the default).
249
+ - Item-level `render` on `{ type: 'custom' }` items for one-off rows (dividers with a label,
250
+ a search box, an upgrade banner, anything).
251
+
252
+ ## Accessibility
253
+
254
+ `<nav aria-label>` landmark, `aria-current="page"` on the active link, `aria-expanded`/
255
+ `aria-controls` on group triggers, visible `:focus-visible` rings, a labeled and focus-trapped
256
+ drawer dialog (`role="dialog" aria-modal`) with Escape support, and full `prefers-reduced-motion`
257
+ support (respected automatically, or forced with `reduceMotion`).
258
+
259
+ ## SSR / Next.js
260
+
261
+ The package never touches `window`/`document` during render — only inside effects and
262
+ `useSyncExternalStore`. The compiled bundle starts with `"use client"`, so it can be imported
263
+ directly from a Server Component file in the App Router. Persisted collapse state hydrates safely
264
+ (server and first client paint both render `defaultCollapsed`; the real value swaps in right
265
+ after).
266
+
267
+ ## API reference
268
+
269
+ The full prop surface is exported as `SidebarProps`, along with every item/theme/slot type
270
+ (`SidebarItem`, `SidebarLinkItem`, `SidebarGroupItem`, `SidebarTheme`, `SidebarClassNames`, …) —
271
+ import them for autocomplete:
272
+
273
+ ```ts
274
+ import type { SidebarProps, SidebarItem, SidebarTheme } from '@bensdev/react-sidebar';
275
+ ```
276
+
277
+ ## License
278
+
279
+ MIT