@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 +22 -0
- package/LICENSE +21 -0
- package/README.md +279 -0
- package/dist/index.cjs +1323 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +262 -0
- package/dist/index.d.ts +262 -0
- package/dist/index.js +1287 -0
- package/dist/index.js.map +1 -0
- package/dist/styles.css +723 -0
- package/package.json +65 -0
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
|