@xsolla/xui-b2b-sidebar 0.148.0 → 0.148.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.
Files changed (2) hide show
  1. package/README.md +280 -0
  2. package/package.json +6 -6
package/README.md ADDED
@@ -0,0 +1,280 @@
1
+ # B2B Sidebar
2
+
3
+ A composable navigation sidebar for B2B admin surfaces. Renders an expanded panel with grouped menu items and a parallel collapsed icon strip with hover-popovers, both driven by the same `SidebarProvider` so the active route, link component, and collapsed state stay in sync.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @xsolla/xui-b2b-sidebar
9
+ # or
10
+ yarn add @xsolla/xui-b2b-sidebar
11
+ ```
12
+
13
+ ## Demo
14
+
15
+ ### Basic usage
16
+
17
+ Wrap the sidebar in `SidebarProvider`, supplying the current `pathname`, the controlled `collapsed` state, and your app's link component (React Router, Next.js `Link`, etc.). The default link is a plain `<a>` if you omit `linkComponent`.
18
+
19
+ ```tsx
20
+ import { useState } from 'react';
21
+ import { Home, Wallet, Settings } from '@xsolla/xui-icons-base';
22
+ import {
23
+ Sidebar,
24
+ SidebarProvider,
25
+ SidebarContent,
26
+ SidebarFooter,
27
+ SidebarGroup,
28
+ SidebarMenu,
29
+ SidebarMenuItem,
30
+ SidebarMenuCollapsible,
31
+ SidebarMenuSub,
32
+ SidebarTrigger,
33
+ } from '@xsolla/xui-b2b-sidebar';
34
+
35
+ export default function AppShell({ pathname }: { pathname: string }) {
36
+ const [collapsed, setCollapsed] = useState(false);
37
+
38
+ return (
39
+ <SidebarProvider
40
+ collapsed={collapsed}
41
+ onCollapsedChange={setCollapsed}
42
+ pathname={pathname}
43
+ linkComponent={MyRouterLink}
44
+ >
45
+ <Sidebar
46
+ collapsedItems={mainNavItems}
47
+ collapsedBottomItems={workspaceNavItems}
48
+ >
49
+ <SidebarContent>
50
+ <SidebarGroup label="Main">
51
+ <SidebarMenu>
52
+ <SidebarMenuItem
53
+ to="/dashboard"
54
+ icon={<Home size={18} variant="line" />}
55
+ label="Dashboard"
56
+ />
57
+ <SidebarMenuCollapsible
58
+ icon={<Wallet size={18} variant="line" />}
59
+ label="Finance"
60
+ matchPaths={['/finance']}
61
+ >
62
+ <SidebarMenuSub>
63
+ <SidebarMenuItem to="/finance/payouts" label="Payouts" isNested />
64
+ <SidebarMenuItem to="/finance/reports" label="Reports" isNested />
65
+ </SidebarMenuSub>
66
+ </SidebarMenuCollapsible>
67
+ </SidebarMenu>
68
+ </SidebarGroup>
69
+ <SidebarGroup label="Workspace">
70
+ <SidebarMenu>
71
+ <SidebarMenuItem
72
+ to="/settings"
73
+ icon={<Settings size={18} variant="line" />}
74
+ label="Settings"
75
+ />
76
+ </SidebarMenu>
77
+ </SidebarGroup>
78
+ </SidebarContent>
79
+ <SidebarFooter>
80
+ <SidebarTrigger />
81
+ </SidebarFooter>
82
+ </Sidebar>
83
+ </SidebarProvider>
84
+ );
85
+ }
86
+ ```
87
+
88
+ ### Custom link component
89
+
90
+ Supply your router's link via the `linkComponent` prop on `SidebarProvider`. The component receives `to`, `className`, `activeClassName`, `onClick`, `external`, `target`, `dataId`, and renders children.
91
+
92
+ ```tsx
93
+ import { NavLink } from 'react-router-dom';
94
+ import type { SidebarLinkProps } from '@xsolla/xui-b2b-sidebar';
95
+
96
+ const RouterLink: React.FC<SidebarLinkProps> = ({
97
+ to,
98
+ exact,
99
+ className,
100
+ activeClassName,
101
+ children,
102
+ onClick,
103
+ dataId,
104
+ }) => (
105
+ <NavLink
106
+ to={to ?? '#'}
107
+ end={exact}
108
+ className={className}
109
+ activeClassName={activeClassName}
110
+ data-id={dataId}
111
+ onClick={onClick}
112
+ >
113
+ {children}
114
+ </NavLink>
115
+ );
116
+
117
+ <SidebarProvider linkComponent={RouterLink} pathname={location.pathname} ...>
118
+ ```
119
+
120
+ ### Auto-expanding sections
121
+
122
+ Pass `matchPaths` to `SidebarMenuCollapsible`. When the current `pathname` starts with any of the listed prefixes, the section auto-expands and stays expanded. Only one collapsible can be open at a time (Radix-style accordion), so navigating between sections collapses the others.
123
+
124
+ ```tsx
125
+ <SidebarMenuCollapsible
126
+ icon={<Wallet size={18} variant="line" />}
127
+ label="Finance"
128
+ matchPaths={['/finance', '/finance/payouts', '/finance/reports']}
129
+ >
130
+ <SidebarMenuSub>{/* ... */}</SidebarMenuSub>
131
+ </SidebarMenuCollapsible>
132
+ ```
133
+
134
+ ### Item flags (pin / badge / beta / external)
135
+
136
+ ```tsx
137
+ <SidebarMenuItem to="/pinned" label="Pinned report" icon={<Pin size={18} />} isPinned showBadge />
138
+ <SidebarMenuItem to="/labs" label="New analytics" icon={<Graph size={18} />} beta />
139
+ <SidebarMenuItem
140
+ to="https://docs.example.com"
141
+ label="Documentation"
142
+ icon={<Layer size={18} />}
143
+ external
144
+ hasExternalIcon
145
+ target="_blank"
146
+ />
147
+ ```
148
+
149
+ ### Collapsed icon strip
150
+
151
+ Because the collapsed layout is fundamentally different (icons only, hover popovers), pass the icon-strip items separately as `collapsedItems`, `collapsedToolItems` (rendered after a spacer in the main scroll area), and `collapsedBottomItems` (rendered above the chat/toggle footer). Items with `children` open a hover popover listing the children; items without children open a single-link popover.
152
+
153
+ ```tsx
154
+ const mainNavItems: SidebarItemType[] = [
155
+ { to: '/dashboard', label: 'Dashboard', icon: <Home size={18} variant="line" /> },
156
+ {
157
+ label: 'Finance',
158
+ icon: <Wallet size={18} variant="line" />,
159
+ children: [
160
+ { to: '/finance/payouts', label: 'Payouts' },
161
+ { to: '/finance/reports', label: 'Reports' },
162
+ ],
163
+ },
164
+ ];
165
+
166
+ <Sidebar
167
+ collapsedItems={mainNavItems}
168
+ collapsedBottomItems={[{ to: '/settings', label: 'Settings', icon: <Settings size={18} /> }]}
169
+ onChatClick={() => openSupportChat()}
170
+ chatBadge={hasUnreadMessages}
171
+ />
172
+ ```
173
+
174
+ ## API Reference
175
+
176
+ ### SidebarProvider
177
+
178
+ | Prop | Type | Default | Description |
179
+ | :--- | :--- | :------ | :---------- |
180
+ | `collapsed` | `boolean` | `false` | Controlled collapsed state. |
181
+ | `onCollapsedChange` | `(collapsed: boolean) => void` | - | Called when the user toggles collapsed via the trigger button. |
182
+ | `pathname` | `string` | `""` | Current route, used for active-state matching and auto-expand. |
183
+ | `linkComponent` | `ComponentType<SidebarLinkProps>` | plain `<a>` | Router link component. Receives `to`, `className`, `activeClassName`, `onClick`, `external`, `target`, `dataId`, `children`. |
184
+ | `children` | `ReactNode` | - | Sidebar tree. |
185
+
186
+ ### Sidebar
187
+
188
+ | Prop | Type | Default | Description |
189
+ | :--- | :--- | :------ | :---------- |
190
+ | `collapsedItems` | `SidebarItemType[]` | `[]` | Top icons in the collapsed strip. |
191
+ | `collapsedToolItems` | `SidebarItemType[]` | `[]` | Tool icons rendered after a spacer. |
192
+ | `collapsedBottomItems` | `SidebarItemType[]` | `[]` | Bottom icons (e.g. Settings, Billing). |
193
+ | `showChat` | `boolean` | `true` | Render the chat button in the collapsed footer. |
194
+ | `onChatClick` | `() => void` | - | Click handler for the chat button. |
195
+ | `chatBadge` | `boolean` | `false` | Show an unread-indicator dot on the chat button. |
196
+ | `children` | `ReactNode` | - | The expanded tree (`SidebarContent`, `SidebarFooter`). |
197
+
198
+ ### SidebarMenuItem
199
+
200
+ | Prop | Type | Default | Description |
201
+ | :--- | :--- | :------ | :---------- |
202
+ | `to` | `string` | - | Route URL passed through to `linkComponent`. |
203
+ | `label` | `ReactNode` | - | Text label. |
204
+ | `icon` | `ReactNode` | - | Leading icon (hidden when `isNested`). |
205
+ | `exact` | `boolean` | - | Exact-match flag forwarded to the link component. |
206
+ | `external` | `boolean` | - | Marks the link as external (sets `target`/`rel`). |
207
+ | `hasExternalIcon` | `boolean` | - | Shows the external-link icon at the trailing edge. |
208
+ | `target` | `string \| null` | - | Anchor target. |
209
+ | `onClick` | `(event?) => void` | - | Click handler. |
210
+ | `dataId` | `string` | - | `data-id` attribute for tests/analytics. |
211
+ | `isActive` | `(match, location) => boolean` | - | Custom active-check forwarded to the link. |
212
+ | `isPinned` | `boolean` | - | Renders the icon at 12px to indicate a pinned item. |
213
+ | `showBadge` | `boolean` | - | Shows a small alert dot next to the label. |
214
+ | `hasTooltip` | `boolean` | - | Truncates labels longer than 20 chars and shows the full text on hover. |
215
+ | `beta` | `boolean` | - | Renders a "Beta" tag at the trailing edge. |
216
+ | `multiLine` | `boolean` | - | Allows the label to wrap to multiple lines. |
217
+ | `extra` | `ReactNode` | - | Slot rendered between the icon and the label. |
218
+ | `isNested` | `boolean` | `false` | Set when rendered inside `SidebarMenuSub` (drops the icon, indents under the rail). |
219
+
220
+ ### SidebarMenuCollapsible
221
+
222
+ | Prop | Type | Default | Description |
223
+ | :--- | :--- | :------ | :---------- |
224
+ | `icon` | `ReactNode` | - | Leading icon. |
225
+ | `label` | `ReactNode` | - | Section label. |
226
+ | `dataId` | `string` | - | `data-id` attribute. |
227
+ | `matchPaths` | `string[]` | `[]` | Route prefixes that auto-expand this section. |
228
+ | `children` | `ReactNode` | - | Nested items, typically a `SidebarMenuSub` containing `SidebarMenuItem isNested`. |
229
+
230
+ ### SidebarTrigger / SidebarChatButton / SidebarFooter / SidebarGroup / SidebarMenu / SidebarMenuSub
231
+
232
+ Layout primitives:
233
+
234
+ - `<SidebarTrigger />` — collapse/expand toggle button (reads state from the provider).
235
+ - `<SidebarChatButton onClick badge />` — branded chat button for the footer.
236
+ - `<SidebarFooter>{...}</SidebarFooter>` — bordered footer row, typically holding the chat button and trigger.
237
+ - `<SidebarGroup label="...">` — section container with optional uppercase label.
238
+ - `<SidebarMenu>` — flex column wrapper around items.
239
+ - `<SidebarMenuSub>` — fragment used as a structural marker inside collapsibles.
240
+
241
+ ### Types
242
+
243
+ ```ts
244
+ type SidebarItemType = {
245
+ to?: string;
246
+ label: ReactNode;
247
+ icon?: ReactNode;
248
+ dataId?: string;
249
+ exact?: boolean;
250
+ external?: boolean;
251
+ hasExternalIcon?: boolean;
252
+ target?: string | null;
253
+ onClick?: (event?: MouseEvent) => void;
254
+ hasTooltip?: boolean;
255
+ beta?: boolean;
256
+ multiLine?: boolean;
257
+ isPinned?: boolean;
258
+ showBadge?: boolean;
259
+ isActive?: (match: unknown, location: { pathname: string }) => boolean;
260
+ children?: SidebarItemType[];
261
+ privileges?: string[];
262
+ visibility?: string;
263
+ disallowedIntegrationTypes?: string[];
264
+ };
265
+ ```
266
+
267
+ ## Behaviour
268
+
269
+ - Expanded width is 280px, collapsed width is 48px; the wrapper animates `width` over 250ms and cross-fades the two views via opacity.
270
+ - Only one `SidebarMenuCollapsible` is open at a time; the section auto-expands when `pathname` matches one of `matchPaths`.
271
+ - Hover popovers in collapsed mode are rendered into a portal on `document.body` and positioned with viewport-clamped `position: fixed` math.
272
+ - Custom scrollbar styling on the expanded scroll area; thumb fades in on sidebar hover.
273
+ - Active route detection lives in the consumer's `linkComponent` — apply `activeClassName` when the link matches the current route.
274
+
275
+ ## Accessibility
276
+
277
+ - Collapse/expand button has `aria-pressed` and a context-aware `aria-label` (`"Expand sidebar"` / `"Collapse sidebar"`).
278
+ - Each `SidebarMenuCollapsible` header is a `<button>` with `aria-expanded` and `aria-controls` linking to the region.
279
+ - Collapsed icon items expose `aria-haspopup="menu"` when they have children; popover is keyboard-reachable and closes on Escape.
280
+ - Truncated labels (`hasTooltip`) fall back to a Tooltip with the full text.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xsolla/xui-b2b-sidebar",
3
- "version": "0.148.0",
3
+ "version": "0.148.1",
4
4
  "main": "./web/index.js",
5
5
  "module": "./web/index.mjs",
6
6
  "types": "./web/index.d.ts",
@@ -13,11 +13,11 @@
13
13
  "test:coverage": "vitest run --coverage"
14
14
  },
15
15
  "dependencies": {
16
- "@xsolla/xui-core": "0.148.0",
17
- "@xsolla/xui-icons-base": "0.148.0",
18
- "@xsolla/xui-primitives-core": "0.148.0",
19
- "@xsolla/xui-tooltip": "0.148.0",
20
- "@xsolla/xui-typography": "0.148.0"
16
+ "@xsolla/xui-core": "0.148.1",
17
+ "@xsolla/xui-icons-base": "0.148.1",
18
+ "@xsolla/xui-primitives-core": "0.148.1",
19
+ "@xsolla/xui-tooltip": "0.148.1",
20
+ "@xsolla/xui-typography": "0.148.1"
21
21
  },
22
22
  "peerDependencies": {
23
23
  "react": ">=16.8.0",