@xsolla/xui-b2b-sidebar 0.158.0 → 0.159.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/README.md CHANGED
@@ -34,7 +34,7 @@ import {
34
34
  type SidebarLinkProps,
35
35
  type SidebarLinkActiveCheck,
36
36
  type SidebarLinkClickHandler,
37
- } from '@xsolla/xui-b2b-sidebar';
37
+ } from "@xsolla/xui-b2b-sidebar";
38
38
  ```
39
39
 
40
40
  ## Quick start
@@ -42,8 +42,8 @@ import {
42
42
  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`.
43
43
 
44
44
  ```tsx
45
- import { useState } from 'react';
46
- import { Home, Wallet, Settings } from '@xsolla/xui-icons-base';
45
+ import { useState } from "react";
46
+ import { Home, Wallet, Settings } from "@xsolla/xui-icons-base";
47
47
  import {
48
48
  Sidebar,
49
49
  SidebarProvider,
@@ -55,7 +55,7 @@ import {
55
55
  SidebarMenuCollapsible,
56
56
  SidebarMenuSub,
57
57
  SidebarTrigger,
58
- } from '@xsolla/xui-b2b-sidebar';
58
+ } from "@xsolla/xui-b2b-sidebar";
59
59
 
60
60
  function AppShell({ pathname }: { pathname: string }) {
61
61
  const [collapsed, setCollapsed] = useState(false);
@@ -78,11 +78,19 @@ function AppShell({ pathname }: { pathname: string }) {
78
78
  <SidebarMenuCollapsible
79
79
  icon={<Wallet size={18} variant="line" aria-hidden />}
80
80
  label="Finance"
81
- matchPaths={['/finance']}
81
+ matchPaths={["/finance"]}
82
82
  >
83
83
  <SidebarMenuSub>
84
- <SidebarMenuItem to="/finance/payouts" label="Payouts" isNested />
85
- <SidebarMenuItem to="/finance/reports" label="Reports" isNested />
84
+ <SidebarMenuItem
85
+ to="/finance/payouts"
86
+ label="Payouts"
87
+ isNested
88
+ />
89
+ <SidebarMenuItem
90
+ to="/finance/reports"
91
+ label="Reports"
92
+ isNested
93
+ />
86
94
  </SidebarMenuSub>
87
95
  </SidebarMenuCollapsible>
88
96
  </SidebarMenu>
@@ -112,13 +120,14 @@ None of the sidebar components extend `ThemeOverrideProps` — they read theme v
112
120
 
113
121
  ### `<SidebarProvider>`
114
122
 
115
- | Prop | Type | Default | Description |
116
- | --- | --- | --- | --- |
117
- | `collapsed` | `boolean` | uncontrolled | Controlled collapsed state. Omit to use the internal state. |
118
- | `onCollapsedChange` | `(collapsed: boolean) => void` | | Called when the user toggles via `SidebarTrigger` or the collapsed-mode toggle. |
119
- | `pathname` | `string` | `""` | Current route, used for active-state matching and `matchPaths` auto-expand. |
120
- | `linkComponent` | `ComponentType<SidebarLinkProps>` | plain `<a>` | Router link component. |
121
- | `children` | `ReactNode` | | Sidebar tree. |
123
+ | Prop | Type | Default | Description |
124
+ | ------------------- | --------------------------------- | ------------ | ------------------------------------------------------------------------------------------------------------- |
125
+ | `testID` | `string` | | Test ID for testing frameworks. On web this renders as `data-testid`; on React Native it renders as `testID`. |
126
+ | `collapsed` | `boolean` | uncontrolled | Controlled collapsed state. Omit to use the internal state. |
127
+ | `onCollapsedChange` | `(collapsed: boolean) => void` | | Called when the user toggles via `SidebarTrigger` or the collapsed-mode toggle. |
128
+ | `pathname` | `string` | `""` | Current route, used for active-state matching and `matchPaths` auto-expand. |
129
+ | `linkComponent` | `ComponentType<SidebarLinkProps>` | plain `<a>` | Router link component. |
130
+ | `children` | `ReactNode` | — | Sidebar tree. |
122
131
 
123
132
  ### `useSidebar()`
124
133
 
@@ -139,61 +148,61 @@ function useSidebar(): {
139
148
 
140
149
  ### `<Sidebar>`
141
150
 
142
- | Prop | Type | Default | Description |
143
- | --- | --- | --- | --- |
144
- | `collapsedItems` | `SidebarItemType[]` | `[]` | Top icons in the collapsed strip. |
145
- | `collapsedToolItems` | `SidebarItemType[]` | `[]` | Tool icons rendered after a spacer. |
146
- | `collapsedBottomItems` | `SidebarItemType[]` | `[]` | Bottom icons (e.g. Settings, Billing). |
147
- | `showChat` | `boolean` | `true` | Render the chat button in the collapsed footer. |
148
- | `onChatClick` | `() => void` | — | Click handler for the chat button. |
149
- | `chatBadge` | `boolean` | `false` | Show an unread-indicator dot on the chat button. |
150
- | `children` | `ReactNode` | — | The expanded tree (`SidebarContent`, `SidebarFooter`). |
151
+ | Prop | Type | Default | Description |
152
+ | ---------------------- | ------------------- | ------- | ------------------------------------------------------ |
153
+ | `collapsedItems` | `SidebarItemType[]` | `[]` | Top icons in the collapsed strip. |
154
+ | `collapsedToolItems` | `SidebarItemType[]` | `[]` | Tool icons rendered after a spacer. |
155
+ | `collapsedBottomItems` | `SidebarItemType[]` | `[]` | Bottom icons (e.g. Settings, Billing). |
156
+ | `showChat` | `boolean` | `true` | Render the chat button in the collapsed footer. |
157
+ | `onChatClick` | `() => void` | — | Click handler for the chat button. |
158
+ | `chatBadge` | `boolean` | `false` | Show an unread-indicator dot on the chat button. |
159
+ | `children` | `ReactNode` | — | The expanded tree (`SidebarContent`, `SidebarFooter`). |
151
160
 
152
161
  ### `<SidebarMenuItem>`
153
162
 
154
- | Prop | Type | Default | Description |
155
- | --- | --- | --- | --- |
156
- | `to` | `string` | — | Route URL passed through to `linkComponent`. |
157
- | `label` | `ReactNode` | — | Text label. |
158
- | `icon` | `ReactNode` | — | Leading icon (hidden when `isNested`). |
159
- | `exact` | `boolean` | — | Exact-match flag forwarded to the link component. |
160
- | `external` | `boolean` | — | Marks the link as external (sets `target`/`rel`). |
161
- | `hasExternalIcon` | `boolean` | — | Shows the external-link icon at the trailing edge. |
162
- | `target` | `string \| null` | — | Anchor target. |
163
- | `onClick` | `SidebarLinkClickHandler` | — | Click handler. |
164
- | `dataId` | `string` | — | `data-id` attribute for tests/analytics. |
165
- | `isActive` | `SidebarLinkActiveCheck` | — | Custom active-check forwarded to the link. |
166
- | `isPinned` | `boolean` | — | Renders the icon at 12px to indicate a pinned item. |
167
- | `showBadge` | `boolean` | — | Shows a small alert dot next to the label. |
168
- | `hasTooltip` | `boolean` | — | Truncates labels longer than 20 chars and shows the full text on hover. |
169
- | `beta` | `boolean` | — | Renders a "Beta" tag at the trailing edge. |
170
- | `multiLine` | `boolean` | — | Allows the label to wrap to multiple lines. |
171
- | `extra` | `ReactNode` | — | Slot rendered between the icon and the label. |
172
- | `isNested` | `boolean` | `false` | Set when rendered inside `SidebarMenuSub` (drops the icon, indents under the rail). |
163
+ | Prop | Type | Default | Description |
164
+ | ----------------- | ------------------------- | ------- | ----------------------------------------------------------------------------------- |
165
+ | `to` | `string` | — | Route URL passed through to `linkComponent`. |
166
+ | `label` | `ReactNode` | — | Text label. |
167
+ | `icon` | `ReactNode` | — | Leading icon (hidden when `isNested`). |
168
+ | `exact` | `boolean` | — | Exact-match flag forwarded to the link component. |
169
+ | `external` | `boolean` | — | Marks the link as external (sets `target`/`rel`). |
170
+ | `hasExternalIcon` | `boolean` | — | Shows the external-link icon at the trailing edge. |
171
+ | `target` | `string \| null` | — | Anchor target. |
172
+ | `onClick` | `SidebarLinkClickHandler` | — | Click handler. |
173
+ | `dataId` | `string` | — | `data-id` attribute for tests/analytics. |
174
+ | `isActive` | `SidebarLinkActiveCheck` | — | Custom active-check forwarded to the link. |
175
+ | `isPinned` | `boolean` | — | Renders the icon at 12px to indicate a pinned item. |
176
+ | `showBadge` | `boolean` | — | Shows a small alert dot next to the label. |
177
+ | `hasTooltip` | `boolean` | — | Truncates labels longer than 20 chars and shows the full text on hover. |
178
+ | `beta` | `boolean` | — | Renders a "Beta" tag at the trailing edge. |
179
+ | `multiLine` | `boolean` | — | Allows the label to wrap to multiple lines. |
180
+ | `extra` | `ReactNode` | — | Slot rendered between the icon and the label. |
181
+ | `isNested` | `boolean` | `false` | Set when rendered inside `SidebarMenuSub` (drops the icon, indents under the rail). |
173
182
 
174
183
  ### `<SidebarMenuCollapsible>`
175
184
 
176
- | Prop | Type | Default | Description |
177
- | --- | --- | --- | --- |
178
- | `icon` | `ReactNode` | — | Leading icon. |
179
- | `label` | `ReactNode` | — | Section label. |
180
- | `dataId` | `string` | — | `data-id` attribute. Also seeds the stable id used by `aria-controls`. |
181
- | `matchPaths` | `string[]` | `[]` | Route prefixes that auto-expand this section. |
182
- | `children` | `ReactNode` | — | Nested items, typically a `SidebarMenuSub` of `SidebarMenuItem isNested`. |
185
+ | Prop | Type | Default | Description |
186
+ | ------------ | ----------- | ------- | ------------------------------------------------------------------------- |
187
+ | `icon` | `ReactNode` | — | Leading icon. |
188
+ | `label` | `ReactNode` | — | Section label. |
189
+ | `dataId` | `string` | — | `data-id` attribute. Also seeds the stable id used by `aria-controls`. |
190
+ | `matchPaths` | `string[]` | `[]` | Route prefixes that auto-expand this section. |
191
+ | `children` | `ReactNode` | — | Nested items, typically a `SidebarMenuSub` of `SidebarMenuItem isNested`. |
183
192
 
184
193
  ### `<SidebarCollapsed>`
185
194
 
186
195
  Lower-level collapsed-strip renderer. The `<Sidebar>` component composes this for you; reach for it when you need to render the collapsed strip outside the standard layout.
187
196
 
188
- | Prop | Type | Default | Description |
189
- | --- | --- | --- | --- |
190
- | `items` | `SidebarItemType[]` | — | Top icon items. |
191
- | `toolItems` | `SidebarItemType[]` | `[]` | Tool icons after a spacer. |
192
- | `bottomItems` | `SidebarItemType[]` | `[]` | Bottom icons. |
193
- | `onToggleCollapse` | `() => void` | — | Click handler for the toggle button in the footer. |
194
- | `onChatClick` | `() => void` | — | Click handler for the chat button. |
195
- | `showChat` | `boolean` | `true` | Render the chat button. |
196
- | `chatBadge` | `boolean` | `false` | Show the unread dot. |
197
+ | Prop | Type | Default | Description |
198
+ | ------------------ | ------------------- | ------- | -------------------------------------------------- |
199
+ | `items` | `SidebarItemType[]` | — | Top icon items. |
200
+ | `toolItems` | `SidebarItemType[]` | `[]` | Tool icons after a spacer. |
201
+ | `bottomItems` | `SidebarItemType[]` | `[]` | Bottom icons. |
202
+ | `onToggleCollapse` | `() => void` | — | Click handler for the toggle button in the footer. |
203
+ | `onChatClick` | `() => void` | — | Click handler for the chat button. |
204
+ | `showChat` | `boolean` | `true` | Render the chat button. |
205
+ | `chatBadge` | `boolean` | `false` | Show the unread dot. |
197
206
 
198
207
  ### `<SidebarContent>`
199
208
 
@@ -241,7 +250,7 @@ type SidebarItemType = {
241
250
  multiLine?: boolean;
242
251
  isPinned?: boolean;
243
252
  showBadge?: boolean;
244
- reverse?: boolean; // reserved for legacy reversed-layout items
253
+ reverse?: boolean; // reserved for legacy reversed-layout items
245
254
  isActive?: SidebarLinkActiveCheck;
246
255
  children?: SidebarItemType[];
247
256
  privileges?: string[];
@@ -260,6 +269,7 @@ interface SidebarLinkProps {
260
269
  isActive?: SidebarLinkActiveCheck;
261
270
  onClick?: SidebarLinkClickHandler;
262
271
  dataId?: string;
272
+ "aria-label"?: string;
263
273
  children: ReactNode;
264
274
  }
265
275
 
@@ -278,8 +288,8 @@ type SidebarLinkClickHandler = (event?: React.MouseEvent<HTMLElement>) => void;
278
288
  Supply your router's link via the `linkComponent` prop on `SidebarProvider`. The component receives `to`, `exact`, `external`, `target`, `className`, `activeClassName`, `isActive`, `onClick`, `dataId`, and renders children.
279
289
 
280
290
  ```tsx
281
- import { NavLink } from 'react-router-dom';
282
- import type { SidebarLinkProps } from '@xsolla/xui-b2b-sidebar';
291
+ import { NavLink } from "react-router-dom";
292
+ import type { SidebarLinkProps } from "@xsolla/xui-b2b-sidebar";
283
293
 
284
294
  const RouterLink: React.FC<SidebarLinkProps> = ({
285
295
  to,
@@ -291,9 +301,11 @@ const RouterLink: React.FC<SidebarLinkProps> = ({
291
301
  dataId,
292
302
  }) => (
293
303
  <NavLink
294
- to={to ?? '#'}
304
+ to={to ?? "#"}
295
305
  end={exact}
296
- className={({ isActive }) => [className, isActive && activeClassName].filter(Boolean).join(' ')}
306
+ className={({ isActive }) =>
307
+ [className, isActive && activeClassName].filter(Boolean).join(" ")
308
+ }
297
309
  data-id={dataId}
298
310
  onClick={onClick}
299
311
  >
@@ -311,13 +323,17 @@ const RouterLink: React.FC<SidebarLinkProps> = ({
311
323
  Pass `matchPaths` to `SidebarMenuCollapsible`. When the current `pathname` starts with any of the listed prefixes, the section auto-expands. Only one collapsible can be open at a time, so navigating between sections collapses the others.
312
324
 
313
325
  ```tsx
314
- import { Wallet } from '@xsolla/xui-icons-base';
315
- import { SidebarMenuCollapsible, SidebarMenuSub, SidebarMenuItem } from '@xsolla/xui-b2b-sidebar';
326
+ import { Wallet } from "@xsolla/xui-icons-base";
327
+ import {
328
+ SidebarMenuCollapsible,
329
+ SidebarMenuSub,
330
+ SidebarMenuItem,
331
+ } from "@xsolla/xui-b2b-sidebar";
316
332
 
317
333
  <SidebarMenuCollapsible
318
334
  icon={<Wallet size={18} variant="line" aria-hidden />}
319
335
  label="Finance"
320
- matchPaths={['/finance', '/finance/payouts', '/finance/reports']}
336
+ matchPaths={["/finance", "/finance/payouts", "/finance/reports"]}
321
337
  >
322
338
  <SidebarMenuSub>
323
339
  <SidebarMenuItem to="/finance/payouts" label="Payouts" isNested />
@@ -329,8 +345,8 @@ import { SidebarMenuCollapsible, SidebarMenuSub, SidebarMenuItem } from '@xsolla
329
345
  ### Item flags
330
346
 
331
347
  ```tsx
332
- import { Pin, Graph, Layer } from '@xsolla/xui-icons-base';
333
- import { SidebarMenuItem } from '@xsolla/xui-b2b-sidebar';
348
+ import { Pin, Graph, Layer } from "@xsolla/xui-icons-base";
349
+ import { SidebarMenuItem } from "@xsolla/xui-b2b-sidebar";
334
350
 
335
351
  <>
336
352
  <SidebarMenuItem
@@ -359,20 +375,24 @@ import { SidebarMenuItem } from '@xsolla/xui-b2b-sidebar';
359
375
 
360
376
  ### Collapsed icon strip
361
377
 
362
- Because the collapsed layout is fundamentally different (icons only, hover popovers), pass 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.
378
+ Because the collapsed layout is fundamentally different (icons only, hover popovers), pass 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 navigate directly on click (the icon is rendered as a link), and show a label tooltip on hover for discoverability.
363
379
 
364
380
  ```tsx
365
- import { Home, Wallet, Settings } from '@xsolla/xui-icons-base';
366
- import { Sidebar, type SidebarItemType } from '@xsolla/xui-b2b-sidebar';
381
+ import { Home, Wallet, Settings } from "@xsolla/xui-icons-base";
382
+ import { Sidebar, type SidebarItemType } from "@xsolla/xui-b2b-sidebar";
367
383
 
368
384
  const mainNavItems: SidebarItemType[] = [
369
- { to: '/dashboard', label: 'Dashboard', icon: <Home size={18} variant="line" aria-hidden /> },
370
385
  {
371
- label: 'Finance',
386
+ to: "/dashboard",
387
+ label: "Dashboard",
388
+ icon: <Home size={18} variant="line" aria-hidden />,
389
+ },
390
+ {
391
+ label: "Finance",
372
392
  icon: <Wallet size={18} variant="line" aria-hidden />,
373
393
  children: [
374
- { to: '/finance/payouts', label: 'Payouts' },
375
- { to: '/finance/reports', label: 'Reports' },
394
+ { to: "/finance/payouts", label: "Payouts" },
395
+ { to: "/finance/reports", label: "Reports" },
376
396
  ],
377
397
  },
378
398
  ];
@@ -380,7 +400,11 @@ const mainNavItems: SidebarItemType[] = [
380
400
  <Sidebar
381
401
  collapsedItems={mainNavItems}
382
402
  collapsedBottomItems={[
383
- { to: '/settings', label: 'Settings', icon: <Settings size={18} aria-hidden /> },
403
+ {
404
+ to: "/settings",
405
+ label: "Settings",
406
+ icon: <Settings size={18} aria-hidden />,
407
+ },
384
408
  ]}
385
409
  onChatClick={() => openSupportChat()}
386
410
  chatBadge={hasUnreadMessages}
@@ -394,7 +418,7 @@ const mainNavItems: SidebarItemType[] = [
394
418
  - The root sidebar is `role="navigation"` with `aria-label="Sidebar navigation"`. The expanded and collapsed panes are toggled via `aria-hidden` so only one is announced at a time.
395
419
  - `SidebarTrigger` and the collapsed-mode toggle have `aria-pressed` and a context-aware `aria-label` (`"Expand sidebar"` / `"Collapse sidebar"`).
396
420
  - Each `SidebarMenuCollapsible` header is a `<button>` with `aria-expanded` and `aria-controls` linking to its region.
397
- - Collapsed icon items expose `aria-haspopup="menu"` when they have children; popovers are keyboard-reachable (focus opens, blur closes after 150ms; Escape closes immediately).
421
+ - Collapsed icon items with children expose `aria-haspopup="menu"`; their popovers are keyboard-reachable (focus opens, blur closes after 150ms, Escape closes immediately). Leaf items (no children) render as native links and navigate on click or Enter.
398
422
  - Truncated labels (`hasTooltip`) fall back to a Tooltip with the full text.
399
423
 
400
424
  ## Behaviour
@@ -16,7 +16,10 @@ interface SidebarLinkProps {
16
16
  isActive?: SidebarLinkActiveCheck;
17
17
  onClick?: SidebarLinkClickHandler;
18
18
  dataId?: string;
19
+ "aria-label"?: string;
19
20
  children: ReactNode;
21
+ /** Test ID for testing frameworks */
22
+ testID?: string;
20
23
  }
21
24
  interface SidebarContextValue {
22
25
  collapsed: boolean;
@@ -32,6 +35,8 @@ interface SidebarProviderProps {
32
35
  pathname?: string;
33
36
  linkComponent?: React.ComponentType<SidebarLinkProps>;
34
37
  children: ReactNode;
38
+ /** Test ID for testing frameworks */
39
+ testID?: string;
35
40
  }
36
41
  declare const SidebarProvider: React.FC<SidebarProviderProps>;
37
42
  declare const useSidebar: () => SidebarContextValue;
@@ -104,6 +109,8 @@ interface SidebarProps {
104
109
  /** Whether the chat button shows a notification badge */
105
110
  chatBadge?: boolean;
106
111
  children: ReactNode;
112
+ /** Test ID for testing frameworks */
113
+ testID?: string;
107
114
  }
108
115
  declare const Sidebar: React.FC<SidebarProps>;
109
116
 
@@ -123,6 +130,8 @@ interface SidebarGroupProps {
123
130
  /** Docks this group to the bottom of the expanded sidebar (e.g. Settings, Billing). */
124
131
  pinnedToBottom?: boolean;
125
132
  children: ReactNode;
133
+ /** Test ID for testing frameworks */
134
+ testID?: string;
126
135
  }
127
136
  declare const SidebarGroup: React.FC<SidebarGroupProps>;
128
137
 
@@ -153,6 +162,8 @@ interface SidebarMenuItemProps {
153
162
  extra?: ReactNode;
154
163
  /** Whether this item is inside a SidebarMenuSub (parent collapsible sets nested styling) */
155
164
  isNested?: boolean;
165
+ /** Test ID for testing frameworks */
166
+ testID?: string;
156
167
  }
157
168
  declare const SidebarMenuItem: React.FC<SidebarMenuItemProps>;
158
169
 
@@ -163,6 +174,8 @@ interface SidebarMenuCollapsibleProps {
163
174
  /** Route prefixes that should auto-expand this collapsible */
164
175
  matchPaths?: string[];
165
176
  children: ReactNode;
177
+ /** Test ID for testing frameworks */
178
+ testID?: string;
166
179
  }
167
180
  declare const SidebarMenuCollapsible: React.FC<SidebarMenuCollapsibleProps>;
168
181
 
@@ -178,6 +191,8 @@ declare const SidebarMenuSub: React.FC<{
178
191
  interface SidebarChatButtonProps {
179
192
  onClick?: () => void;
180
193
  badge?: boolean;
194
+ /** Test ID for testing frameworks */
195
+ testID?: string;
181
196
  }
182
197
  declare const SidebarChatButton: React.FC<SidebarChatButtonProps>;
183
198
 
@@ -189,6 +204,8 @@ interface SidebarCollapsedProps {
189
204
  onChatClick?: () => void;
190
205
  showChat?: boolean;
191
206
  chatBadge?: boolean;
207
+ /** Test ID for testing frameworks */
208
+ testID?: string;
192
209
  }
193
210
  declare const SidebarCollapsed: React.FC<SidebarCollapsedProps>;
194
211
 
@@ -210,6 +227,8 @@ interface SidebarPinnedListProps {
210
227
  onReorder?: (fromKey: string, toKey: string) => void;
211
228
  /** Render 8px top/bottom spacers when items are present. Default true. */
212
229
  spacers?: boolean;
230
+ /** Test ID for testing frameworks */
231
+ testID?: string;
213
232
  }
214
233
  /**
215
234
  * Renders the pinned shortcuts list with pointer-event-based drag-and-drop
package/native/index.d.ts CHANGED
@@ -16,7 +16,10 @@ interface SidebarLinkProps {
16
16
  isActive?: SidebarLinkActiveCheck;
17
17
  onClick?: SidebarLinkClickHandler;
18
18
  dataId?: string;
19
+ "aria-label"?: string;
19
20
  children: ReactNode;
21
+ /** Test ID for testing frameworks */
22
+ testID?: string;
20
23
  }
21
24
  interface SidebarContextValue {
22
25
  collapsed: boolean;
@@ -32,6 +35,8 @@ interface SidebarProviderProps {
32
35
  pathname?: string;
33
36
  linkComponent?: React.ComponentType<SidebarLinkProps>;
34
37
  children: ReactNode;
38
+ /** Test ID for testing frameworks */
39
+ testID?: string;
35
40
  }
36
41
  declare const SidebarProvider: React.FC<SidebarProviderProps>;
37
42
  declare const useSidebar: () => SidebarContextValue;
@@ -104,6 +109,8 @@ interface SidebarProps {
104
109
  /** Whether the chat button shows a notification badge */
105
110
  chatBadge?: boolean;
106
111
  children: ReactNode;
112
+ /** Test ID for testing frameworks */
113
+ testID?: string;
107
114
  }
108
115
  declare const Sidebar: React.FC<SidebarProps>;
109
116
 
@@ -123,6 +130,8 @@ interface SidebarGroupProps {
123
130
  /** Docks this group to the bottom of the expanded sidebar (e.g. Settings, Billing). */
124
131
  pinnedToBottom?: boolean;
125
132
  children: ReactNode;
133
+ /** Test ID for testing frameworks */
134
+ testID?: string;
126
135
  }
127
136
  declare const SidebarGroup: React.FC<SidebarGroupProps>;
128
137
 
@@ -153,6 +162,8 @@ interface SidebarMenuItemProps {
153
162
  extra?: ReactNode;
154
163
  /** Whether this item is inside a SidebarMenuSub (parent collapsible sets nested styling) */
155
164
  isNested?: boolean;
165
+ /** Test ID for testing frameworks */
166
+ testID?: string;
156
167
  }
157
168
  declare const SidebarMenuItem: React.FC<SidebarMenuItemProps>;
158
169
 
@@ -163,6 +174,8 @@ interface SidebarMenuCollapsibleProps {
163
174
  /** Route prefixes that should auto-expand this collapsible */
164
175
  matchPaths?: string[];
165
176
  children: ReactNode;
177
+ /** Test ID for testing frameworks */
178
+ testID?: string;
166
179
  }
167
180
  declare const SidebarMenuCollapsible: React.FC<SidebarMenuCollapsibleProps>;
168
181
 
@@ -178,6 +191,8 @@ declare const SidebarMenuSub: React.FC<{
178
191
  interface SidebarChatButtonProps {
179
192
  onClick?: () => void;
180
193
  badge?: boolean;
194
+ /** Test ID for testing frameworks */
195
+ testID?: string;
181
196
  }
182
197
  declare const SidebarChatButton: React.FC<SidebarChatButtonProps>;
183
198
 
@@ -189,6 +204,8 @@ interface SidebarCollapsedProps {
189
204
  onChatClick?: () => void;
190
205
  showChat?: boolean;
191
206
  chatBadge?: boolean;
207
+ /** Test ID for testing frameworks */
208
+ testID?: string;
192
209
  }
193
210
  declare const SidebarCollapsed: React.FC<SidebarCollapsedProps>;
194
211
 
@@ -210,6 +227,8 @@ interface SidebarPinnedListProps {
210
227
  onReorder?: (fromKey: string, toKey: string) => void;
211
228
  /** Render 8px top/bottom spacers when items are present. Default true. */
212
229
  spacers?: boolean;
230
+ /** Test ID for testing frameworks */
231
+ testID?: string;
213
232
  }
214
233
  /**
215
234
  * Renders the pinned shortcuts list with pointer-event-based drag-and-drop
package/native/index.js CHANGED
@@ -62,7 +62,9 @@ var DefaultLink = ({
62
62
  className,
63
63
  children,
64
64
  dataId,
65
- onClick
65
+ testID,
66
+ onClick,
67
+ "aria-label": ariaLabel
66
68
  }) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
67
69
  "a",
68
70
  {
@@ -71,6 +73,8 @@ var DefaultLink = ({
71
73
  target: external ? target ?? "_blank" : target ?? void 0,
72
74
  rel: external ? "noopener noreferrer" : void 0,
73
75
  "data-id": dataId,
76
+ "data-testid": testID,
77
+ "aria-label": ariaLabel,
74
78
  onClick,
75
79
  children
76
80
  }
@@ -420,6 +424,62 @@ var SingleLinkWrap = import_styled_components.default.div`
420
424
  text-decoration: none;
421
425
  }
422
426
  `;
427
+ var CollapsedIconLinkWrap = import_styled_components.default.div`
428
+ width: ${(p) => p.$size}px;
429
+ height: ${(p) => p.$size}px;
430
+ flex-shrink: 0;
431
+
432
+ & > a,
433
+ & > * {
434
+ display: flex;
435
+ width: 100%;
436
+ height: 100%;
437
+ align-items: center;
438
+ justify-content: center;
439
+ border: 0;
440
+ border-radius: ${(p) => p.$radius}px;
441
+ background: ${(p) => p.$active ? p.$activeBg : "transparent"};
442
+ color: ${(p) => p.$active ? p.$hoverColor : p.$color};
443
+ cursor: pointer;
444
+ padding: 0;
445
+ font: inherit;
446
+ text-decoration: none;
447
+ transition:
448
+ background-color 0.15s ease-in-out,
449
+ color 0.15s ease-in-out;
450
+
451
+ ${(p) => p.$active && `
452
+ outline: 1px solid ${p.$activeOutline};
453
+ outline-offset: -1px;
454
+ `}
455
+ }
456
+
457
+ & > a:hover,
458
+ & > *:hover {
459
+ background-color: ${(p) => p.$hoverBg};
460
+ color: ${(p) => p.$hoverColor};
461
+ }
462
+
463
+ & > a:focus-visible,
464
+ & > *:focus-visible {
465
+ outline: ${(p) => p.$focusOutlineWidth}px solid ${(p) => p.$focusOutline};
466
+ outline-offset: -${(p) => p.$focusOutlineWidth}px;
467
+ }
468
+
469
+ & > a > span,
470
+ & > * > span {
471
+ display: flex;
472
+ width: ${(p) => p.$iconSize}px;
473
+ height: ${(p) => p.$iconSize}px;
474
+ align-items: center;
475
+ justify-content: center;
476
+
477
+ svg {
478
+ width: ${(p) => p.$iconSize}px;
479
+ height: ${(p) => p.$iconSize}px;
480
+ }
481
+ }
482
+ `;
423
483
  var isChildActive = (item, pathname) => {
424
484
  if (!item.children || !pathname) {
425
485
  return false;
@@ -434,6 +494,7 @@ var isChildActive = (item, pathname) => {
434
494
  };
435
495
  var CollapsedIconItem = ({ item, onPopoverOpen, onPopoverClose, pathname, sizing, colors }) => {
436
496
  const ref = (0, import_react2.useRef)(null);
497
+ const { LinkComponent } = useSidebar();
437
498
  const hasChildren = Boolean(item.children && item.children.length > 0);
438
499
  const isActive = hasChildren && isChildActive(item, pathname);
439
500
  const openPopover = (0, import_react2.useCallback)(() => {
@@ -459,6 +520,42 @@ var CollapsedIconItem = ({ item, onPopoverOpen, onPopoverClose, pathname, sizing
459
520
  if (!item.icon) {
460
521
  return null;
461
522
  }
523
+ if (!hasChildren) {
524
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
525
+ CollapsedIconLinkWrap,
526
+ {
527
+ ref,
528
+ onMouseEnter: openPopover,
529
+ onMouseLeave: onPopoverClose,
530
+ onFocus: openPopover,
531
+ onBlur: onPopoverClose,
532
+ "data-id": item.dataId,
533
+ $size: sizing.itemHeight,
534
+ $radius: sizing.radius,
535
+ $iconSize: sizing.iconSize,
536
+ $color: colors.content.tertiary,
537
+ $hoverBg: colors.overlay.mono,
538
+ $hoverColor: colors.content.primary,
539
+ $activeBg: colors.overlay.mono,
540
+ $activeOutline: colors.border.secondary,
541
+ $focusOutline: colors.border.brand,
542
+ $focusOutlineWidth: sizing.focusOutlineWidth,
543
+ $active: isSimpleActive,
544
+ children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
545
+ LinkComponent,
546
+ {
547
+ to: item.to,
548
+ exact: item.exact,
549
+ external: item.external,
550
+ target: item.target,
551
+ dataId: item.dataId,
552
+ "aria-label": typeof item.label === "string" ? item.label : void 0,
553
+ children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { children: item.icon })
554
+ }
555
+ )
556
+ }
557
+ );
558
+ }
462
559
  return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
463
560
  IconBtn,
464
561
  {
@@ -470,7 +567,7 @@ var CollapsedIconItem = ({ item, onPopoverOpen, onPopoverClose, pathname, sizing
470
567
  onBlur: onPopoverClose,
471
568
  onKeyDown: handleKeyDown,
472
569
  "data-id": item.dataId,
473
- "aria-haspopup": hasChildren ? "menu" : "false",
570
+ "aria-haspopup": "menu",
474
571
  $size: sizing.itemHeight,
475
572
  $radius: sizing.radius,
476
573
  $iconSize: sizing.iconSize,