@countermeasure-platform/web-components 1.3.3-dev.33.1 → 1.3.4-dev.35.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.
- package/README.md +31 -0
- package/dist/component-D5sRm1fq.js +389 -0
- package/dist/component-D5sRm1fq.js.map +1 -0
- package/dist/components/index.js +27 -27
- package/dist/icons/index.d.ts +12 -0
- package/dist/icons/index.d.ts.map +1 -1
- package/dist/icons/index.js +12 -0
- package/dist/icons/index.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +127 -126
- package/dist/layout/app-shell.d.ts +50 -3
- package/dist/layout/app-shell.d.ts.map +1 -1
- package/dist/layout/app-shell.js +142 -13
- package/dist/layout/app-shell.js.map +1 -1
- package/dist/layout/core-app-chrome.d.ts +81 -0
- package/dist/layout/core-app-chrome.d.ts.map +1 -0
- package/dist/layout/core-app-chrome.js +349 -0
- package/dist/layout/core-app-chrome.js.map +1 -0
- package/dist/layout/index.d.ts +4 -2
- package/dist/layout/index.d.ts.map +1 -1
- package/dist/layout/index.js +88 -87
- package/dist/layout/index.js.map +1 -1
- package/dist/react/primitives/badge.d.ts +1 -1
- package/dist/react/primitives/button.d.ts +2 -2
- package/dist/react/primitives/copy-button.d.ts +1 -1
- package/dist/react/primitives/stat-card.d.ts +1 -1
- package/dist/react/primitives/toggle.d.ts +1 -1
- package/dist/react/sidebar.d.ts +4 -4
- package/dist/react/sidebar.d.ts.map +1 -1
- package/dist/react/sidebar.js +18 -16
- package/dist/react/sidebar.js.map +1 -1
- package/dist/sidebar/component.d.ts +24 -2
- package/dist/sidebar/component.d.ts.map +1 -1
- package/dist/sidebar/enhance.d.ts +8 -0
- package/dist/sidebar/enhance.d.ts.map +1 -1
- package/dist/sidebar/index.d.ts +3 -0
- package/dist/sidebar/index.d.ts.map +1 -1
- package/dist/sidebar/index.js +81 -28
- package/dist/sidebar/index.js.map +1 -1
- package/dist/sidebar/types.d.ts +126 -4
- package/dist/sidebar/types.d.ts.map +1 -1
- package/dist/styles/layout.css +252 -0
- package/dist/styles/sidebar.css +313 -5
- package/package.json +6 -1
- package/src/icons/icons.test.ts +9 -0
- package/src/icons/index.ts +12 -0
- package/src/index.ts +11 -0
- package/src/layout/app-shell.test.ts +204 -0
- package/src/layout/app-shell.ts +362 -3
- package/src/layout/core-app-chrome.test.ts +507 -0
- package/src/layout/core-app-chrome.ts +662 -0
- package/src/layout/index.ts +36 -2
- package/src/react/sidebar.test.tsx +104 -3
- package/src/react/sidebar.tsx +26 -4
- package/src/sidebar/component.test.ts +395 -1
- package/src/sidebar/component.ts +661 -86
- package/src/sidebar/enhance.ts +118 -0
- package/src/sidebar/index.ts +144 -0
- package/src/sidebar/types.ts +143 -4
- package/src/styles/layout.css +252 -0
- package/src/styles/sidebar.css +313 -5
- package/dist/component-Bxhxf21c.js +0 -167
- package/dist/component-Bxhxf21c.js.map +0 -1
package/src/sidebar/enhance.ts
CHANGED
|
@@ -24,6 +24,12 @@ export interface EnhanceOptions {
|
|
|
24
24
|
skeletonTimeoutMs?: number
|
|
25
25
|
/** CSS class for collapsed state (default: 'sidebar--collapsed') */
|
|
26
26
|
collapsedClass?: string
|
|
27
|
+
/** Variants that should opt into product sidebar visuals. */
|
|
28
|
+
productVariants?: string[]
|
|
29
|
+
/** Derive the active nav item from window.location on enhancement. */
|
|
30
|
+
activeFromLocation?: boolean
|
|
31
|
+
/** Close an open mobile sidebar after clicking a route link. */
|
|
32
|
+
closeOnNavigate?: boolean
|
|
27
33
|
}
|
|
28
34
|
|
|
29
35
|
const DEFAULT_OPTIONS: Required<EnhanceOptions> = {
|
|
@@ -31,10 +37,36 @@ const DEFAULT_OPTIONS: Required<EnhanceOptions> = {
|
|
|
31
37
|
mobileBreakpoint: 900,
|
|
32
38
|
skeletonTimeoutMs: 3000,
|
|
33
39
|
collapsedClass: 'sidebar--collapsed',
|
|
40
|
+
productVariants: ['app', 'cm', 'threat-library'],
|
|
41
|
+
activeFromLocation: true,
|
|
42
|
+
closeOnNavigate: true,
|
|
34
43
|
}
|
|
35
44
|
|
|
36
45
|
const ENHANCED_ATTR = 'data-sidebar-enhanced'
|
|
37
46
|
|
|
47
|
+
function normalizePathname(pathname: string): string {
|
|
48
|
+
if (pathname === '/') {
|
|
49
|
+
return pathname
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return pathname.replace(/\/+$/, '')
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function routeMatches(pathname: string, href: string): boolean {
|
|
56
|
+
try {
|
|
57
|
+
const hrefPathname = normalizePathname(new URL(href, window.location.href).pathname)
|
|
58
|
+
const currentPathname = normalizePathname(pathname)
|
|
59
|
+
|
|
60
|
+
if (hrefPathname === '/') {
|
|
61
|
+
return currentPathname === '/'
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return currentPathname === hrefPathname || currentPathname.startsWith(`${hrefPathname}/`)
|
|
65
|
+
} catch {
|
|
66
|
+
return false
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
38
70
|
export class Sidebar {
|
|
39
71
|
readonly element: HTMLElement
|
|
40
72
|
private readonly options: Required<EnhanceOptions>
|
|
@@ -50,11 +82,75 @@ export class Sidebar {
|
|
|
50
82
|
}
|
|
51
83
|
|
|
52
84
|
private init(): void {
|
|
85
|
+
this.setupProductChrome()
|
|
86
|
+
this.setupRouteState()
|
|
53
87
|
this.setupCollapse()
|
|
54
88
|
this.setupKeyboardNav()
|
|
55
89
|
this.setupSkeletonTimeout()
|
|
56
90
|
}
|
|
57
91
|
|
|
92
|
+
private setupProductChrome(): void {
|
|
93
|
+
const shouldUseProductStyle =
|
|
94
|
+
this.element.hasAttribute('data-sidebar-product') ||
|
|
95
|
+
this.element.classList.contains('sidebar--cm') ||
|
|
96
|
+
this.options.productVariants.includes(this.variant)
|
|
97
|
+
|
|
98
|
+
if (shouldUseProductStyle) {
|
|
99
|
+
this.element.classList.add('sidebar--product')
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
this.element
|
|
103
|
+
.querySelectorAll<HTMLElement>('.sidebar__item, .sidebar__nav-link')
|
|
104
|
+
.forEach(item => {
|
|
105
|
+
if (!item.hasAttribute('data-nav-item')) {
|
|
106
|
+
item.setAttribute('data-nav-item', '')
|
|
107
|
+
}
|
|
108
|
+
})
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
private setupRouteState(): void {
|
|
112
|
+
if (this.options.activeFromLocation !== true || typeof window === 'undefined') {
|
|
113
|
+
return
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const items = Array.from(
|
|
117
|
+
this.element.querySelectorAll<HTMLElement>(
|
|
118
|
+
'a.sidebar__item[href], a.sidebar__nav-link[href], a[data-nav-item][href]'
|
|
119
|
+
)
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
let active: HTMLElement | null = null
|
|
123
|
+
let activeLength = -1
|
|
124
|
+
|
|
125
|
+
for (const item of items) {
|
|
126
|
+
const href = item.getAttribute('href')
|
|
127
|
+
if (href === null || !routeMatches(window.location.pathname, href)) {
|
|
128
|
+
continue
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
try {
|
|
132
|
+
const pathname = normalizePathname(new URL(href, window.location.href).pathname)
|
|
133
|
+
if (pathname.length > activeLength) {
|
|
134
|
+
active = item
|
|
135
|
+
activeLength = pathname.length
|
|
136
|
+
}
|
|
137
|
+
} catch {
|
|
138
|
+
// Ignore malformed href values; they cannot represent the current route.
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (active === null) {
|
|
143
|
+
return
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
this.element.querySelectorAll<HTMLElement>('.sidebar__item--active').forEach(item => {
|
|
147
|
+
item.classList.remove('sidebar__item--active')
|
|
148
|
+
item.removeAttribute('aria-current')
|
|
149
|
+
})
|
|
150
|
+
active.classList.add('sidebar__item--active')
|
|
151
|
+
active.setAttribute('aria-current', 'page')
|
|
152
|
+
}
|
|
153
|
+
|
|
58
154
|
/**
|
|
59
155
|
* Collapse/expand toggle with localStorage persistence.
|
|
60
156
|
*/
|
|
@@ -99,6 +195,28 @@ export class Sidebar {
|
|
|
99
195
|
})
|
|
100
196
|
})
|
|
101
197
|
|
|
198
|
+
if (this.options.closeOnNavigate === true) {
|
|
199
|
+
const navigateHandler = (event: MouseEvent): void => {
|
|
200
|
+
const target = event.target
|
|
201
|
+
const navItem =
|
|
202
|
+
target instanceof Element
|
|
203
|
+
? target.closest<HTMLElement>(
|
|
204
|
+
'a.sidebar__item[href], a.sidebar__nav-link[href], a[data-nav-item][href]'
|
|
205
|
+
)
|
|
206
|
+
: null
|
|
207
|
+
if (navItem !== null && this.element.getAttribute('data-open') === 'true') {
|
|
208
|
+
this.element.setAttribute('data-open', 'false')
|
|
209
|
+
if (backdrop) {
|
|
210
|
+
backdrop.setAttribute('data-visible', 'false')
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
this.element.addEventListener('click', navigateHandler)
|
|
215
|
+
this.cleanupFns.push(() => {
|
|
216
|
+
this.element.removeEventListener('click', navigateHandler)
|
|
217
|
+
})
|
|
218
|
+
}
|
|
219
|
+
|
|
102
220
|
// Backdrop click closes sidebar
|
|
103
221
|
if (backdrop) {
|
|
104
222
|
const handler = (): void => {
|
package/src/sidebar/index.ts
CHANGED
|
@@ -26,6 +26,9 @@ export interface SidebarConfig {
|
|
|
26
26
|
storageKey?: string
|
|
27
27
|
mobileBreakpoint?: number
|
|
28
28
|
collapsedClass?: string
|
|
29
|
+
productVariants?: string[]
|
|
30
|
+
activeFromLocation?: boolean
|
|
31
|
+
closeOnNavigate?: boolean
|
|
29
32
|
}
|
|
30
33
|
|
|
31
34
|
export interface LegacySidebarConfig {
|
|
@@ -40,9 +43,13 @@ const DEFAULT_CONFIG: Required<SidebarConfig> = {
|
|
|
40
43
|
storageKey: 'sidebar-collapsed',
|
|
41
44
|
mobileBreakpoint: 900,
|
|
42
45
|
collapsedClass: 'sidebar--collapsed',
|
|
46
|
+
productVariants: ['app', 'cm', 'threat-library'],
|
|
47
|
+
activeFromLocation: true,
|
|
48
|
+
closeOnNavigate: true,
|
|
43
49
|
}
|
|
44
50
|
|
|
45
51
|
let currentConfig: Required<SidebarConfig> = { ...DEFAULT_CONFIG }
|
|
52
|
+
const INITIALIZED_ATTR = 'data-sidebar-initialized'
|
|
46
53
|
|
|
47
54
|
/**
|
|
48
55
|
* Configure sidebar settings.
|
|
@@ -103,6 +110,119 @@ interface SidebarHandlerParams {
|
|
|
103
110
|
key: string
|
|
104
111
|
}
|
|
105
112
|
|
|
113
|
+
function normalizePathname(pathname: string): string {
|
|
114
|
+
if (pathname === '/') {
|
|
115
|
+
return pathname
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return pathname.replace(/\/+$/, '')
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function routeMatches(pathname: string, href: string): boolean {
|
|
122
|
+
try {
|
|
123
|
+
const hrefPathname = normalizePathname(new URL(href, window.location.href).pathname)
|
|
124
|
+
const currentPathname = normalizePathname(pathname)
|
|
125
|
+
|
|
126
|
+
if (hrefPathname === '/') {
|
|
127
|
+
return currentPathname === '/'
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return currentPathname === hrefPathname || currentPathname.startsWith(`${hrefPathname}/`)
|
|
131
|
+
} catch {
|
|
132
|
+
return false
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function resolveActiveRouteItem(sidebar: HTMLElement): HTMLElement | null {
|
|
137
|
+
const items = Array.from(
|
|
138
|
+
sidebar.querySelectorAll<HTMLElement>(
|
|
139
|
+
'a.sidebar__item[href], a.sidebar__nav-link[href], a[data-nav-item][href]'
|
|
140
|
+
)
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
let active: HTMLElement | null = null
|
|
144
|
+
let activeLength = -1
|
|
145
|
+
|
|
146
|
+
for (const item of items) {
|
|
147
|
+
const href = item.getAttribute('href')
|
|
148
|
+
if (href === null || !routeMatches(window.location.pathname, href)) {
|
|
149
|
+
continue
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
try {
|
|
153
|
+
const pathname = normalizePathname(new URL(href, window.location.href).pathname)
|
|
154
|
+
if (pathname.length > activeLength) {
|
|
155
|
+
active = item
|
|
156
|
+
activeLength = pathname.length
|
|
157
|
+
}
|
|
158
|
+
} catch {
|
|
159
|
+
// Ignore malformed href values; they cannot represent the current route.
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return active
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function syncRouteActiveState(sidebar: HTMLElement): void {
|
|
167
|
+
if (currentConfig.activeFromLocation !== true || typeof window === 'undefined') {
|
|
168
|
+
return
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const active = resolveActiveRouteItem(sidebar)
|
|
172
|
+
if (active === null) {
|
|
173
|
+
return
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
sidebar.querySelectorAll<HTMLElement>('.sidebar__item--active').forEach(item => {
|
|
177
|
+
item.classList.remove('sidebar__item--active')
|
|
178
|
+
item.removeAttribute('aria-current')
|
|
179
|
+
})
|
|
180
|
+
active.classList.add('sidebar__item--active')
|
|
181
|
+
active.setAttribute('aria-current', 'page')
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function prepareUnifiedSidebar(sidebar: HTMLElement, variant: string): void {
|
|
185
|
+
const shouldUseProductStyle =
|
|
186
|
+
sidebar.hasAttribute('data-sidebar-product') ||
|
|
187
|
+
sidebar.classList.contains('sidebar--cm') ||
|
|
188
|
+
currentConfig.productVariants.includes(variant)
|
|
189
|
+
|
|
190
|
+
if (shouldUseProductStyle) {
|
|
191
|
+
sidebar.classList.add('sidebar--product')
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
sidebar.querySelectorAll<HTMLElement>('.sidebar__item, .sidebar__nav-link').forEach(item => {
|
|
195
|
+
if (!item.hasAttribute('data-nav-item')) {
|
|
196
|
+
item.setAttribute('data-nav-item', '')
|
|
197
|
+
}
|
|
198
|
+
})
|
|
199
|
+
|
|
200
|
+
syncRouteActiveState(sidebar)
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function setupKeyboardNavigation(sidebar: HTMLElement): void {
|
|
204
|
+
sidebar.addEventListener('keydown', event => {
|
|
205
|
+
if (event.key !== 'ArrowDown' && event.key !== 'ArrowUp') return
|
|
206
|
+
|
|
207
|
+
const items = Array.from(
|
|
208
|
+
sidebar.querySelectorAll<HTMLElement>('[data-nav-item]:not([aria-disabled="true"])')
|
|
209
|
+
).filter(item => !item.hidden && item.offsetParent !== null)
|
|
210
|
+
|
|
211
|
+
if (items.length === 0) return
|
|
212
|
+
|
|
213
|
+
const currentIndex = items.indexOf(document.activeElement as HTMLElement)
|
|
214
|
+
if (currentIndex === -1) return
|
|
215
|
+
|
|
216
|
+
event.preventDefault()
|
|
217
|
+
const nextIndex =
|
|
218
|
+
event.key === 'ArrowDown'
|
|
219
|
+
? (currentIndex + 1) % items.length
|
|
220
|
+
: (currentIndex - 1 + items.length) % items.length
|
|
221
|
+
|
|
222
|
+
items[nextIndex]?.focus()
|
|
223
|
+
})
|
|
224
|
+
}
|
|
225
|
+
|
|
106
226
|
/**
|
|
107
227
|
* Attach all sidebar event handlers (shared between unified and legacy init).
|
|
108
228
|
*/
|
|
@@ -141,6 +261,21 @@ function attachSidebarHandlers({
|
|
|
141
261
|
})
|
|
142
262
|
}
|
|
143
263
|
|
|
264
|
+
if (currentConfig.closeOnNavigate === true) {
|
|
265
|
+
sidebar.addEventListener('click', event => {
|
|
266
|
+
const target = event.target
|
|
267
|
+
const navItem =
|
|
268
|
+
target instanceof Element
|
|
269
|
+
? target.closest<HTMLElement>(
|
|
270
|
+
'a.sidebar__item[href], a.sidebar__nav-link[href], a[data-nav-item][href]'
|
|
271
|
+
)
|
|
272
|
+
: null
|
|
273
|
+
if (navItem !== null && sidebar.getAttribute('data-open') === 'true') {
|
|
274
|
+
closeSidebar(sidebar, backdrop)
|
|
275
|
+
}
|
|
276
|
+
})
|
|
277
|
+
}
|
|
278
|
+
|
|
144
279
|
// Handle window resize
|
|
145
280
|
window.addEventListener('resize', () => {
|
|
146
281
|
if (isMobile()) {
|
|
@@ -154,6 +289,9 @@ function attachSidebarHandlers({
|
|
|
154
289
|
closeSidebar(sidebar, backdrop)
|
|
155
290
|
}
|
|
156
291
|
})
|
|
292
|
+
|
|
293
|
+
setupKeyboardNavigation(sidebar)
|
|
294
|
+
sidebar.setAttribute(INITIALIZED_ATTR, 'true')
|
|
157
295
|
}
|
|
158
296
|
|
|
159
297
|
/**
|
|
@@ -165,6 +303,12 @@ function initUnifiedSidebar(sidebar: HTMLElement): void {
|
|
|
165
303
|
return
|
|
166
304
|
}
|
|
167
305
|
|
|
306
|
+
prepareUnifiedSidebar(sidebar, variant)
|
|
307
|
+
|
|
308
|
+
if (sidebar.getAttribute(INITIALIZED_ATTR) === 'true') {
|
|
309
|
+
return
|
|
310
|
+
}
|
|
311
|
+
|
|
168
312
|
attachSidebarHandlers({
|
|
169
313
|
sidebar,
|
|
170
314
|
toggle: sidebar.querySelector<HTMLButtonElement>('[data-sidebar-toggle]'),
|
package/src/sidebar/types.ts
CHANGED
|
@@ -6,22 +6,84 @@
|
|
|
6
6
|
import { type BrandSize } from '../components/brand'
|
|
7
7
|
import { type IconSet } from '../icons/index'
|
|
8
8
|
|
|
9
|
+
export type SidebarBadgeTone =
|
|
10
|
+
| 'default'
|
|
11
|
+
| 'primary'
|
|
12
|
+
| 'amber'
|
|
13
|
+
| 'cyan'
|
|
14
|
+
| 'rose'
|
|
15
|
+
| 'warning'
|
|
16
|
+
| 'error'
|
|
17
|
+
|
|
18
|
+
export type SidebarActiveMatcher =
|
|
19
|
+
| 'exact'
|
|
20
|
+
| 'prefix'
|
|
21
|
+
| string
|
|
22
|
+
| RegExp
|
|
23
|
+
| ((pathname: string, item: SidebarNavItem) => boolean)
|
|
24
|
+
|
|
25
|
+
export interface SidebarBadgeConfig {
|
|
26
|
+
/** Visible badge text. Use short labels such as "9", "99+", or "Soon". */
|
|
27
|
+
label: string
|
|
28
|
+
/** Optional accessible title used in collapsed tooltips and native titles. */
|
|
29
|
+
title?: string
|
|
30
|
+
/**
|
|
31
|
+
* Visual tone for the badge.
|
|
32
|
+
* @default 'primary'
|
|
33
|
+
*/
|
|
34
|
+
tone?: SidebarBadgeTone
|
|
35
|
+
/**
|
|
36
|
+
* Keep the collapsed rail dot visible even when label is empty.
|
|
37
|
+
* @default false
|
|
38
|
+
*/
|
|
39
|
+
dot?: boolean
|
|
40
|
+
}
|
|
41
|
+
|
|
9
42
|
export interface SidebarNavItem {
|
|
10
43
|
/** Stable identifier used for selection and badge updates. */
|
|
11
44
|
id: string
|
|
12
45
|
/** Visible label rendered alongside the icon. */
|
|
13
46
|
label: string
|
|
47
|
+
/** Optional navigation target. When omitted, the item renders as a button. */
|
|
48
|
+
href?: string
|
|
49
|
+
/** Optional target for link navigation. */
|
|
50
|
+
target?: string
|
|
51
|
+
/** Optional rel for external links. */
|
|
52
|
+
rel?: string
|
|
53
|
+
/** Accessible label override. */
|
|
54
|
+
ariaLabel?: string
|
|
14
55
|
/** Icon name resolved through the active icon set. */
|
|
15
56
|
icon?: string
|
|
16
|
-
/** Numeric badge displayed at the trailing edge of the row. */
|
|
17
|
-
badge?: number
|
|
57
|
+
/** Numeric or rich badge displayed at the trailing edge of the row. */
|
|
58
|
+
badge?: number | SidebarBadgeConfig
|
|
18
59
|
/**
|
|
19
60
|
* Visual tone for the badge.
|
|
20
61
|
* @default 'primary'
|
|
21
62
|
*/
|
|
22
|
-
badgeTone?:
|
|
63
|
+
badgeTone?: SidebarBadgeTone
|
|
64
|
+
/** Static pill label such as "Admin" or "Soon". */
|
|
65
|
+
tag?: string
|
|
66
|
+
/** Tooltip/title shown when the sidebar is collapsed. */
|
|
67
|
+
tooltip?: string
|
|
68
|
+
/**
|
|
69
|
+
* Whether this item is the current page. If omitted, active state can be
|
|
70
|
+
* derived from `activeItemId`, `href`, or `activeMatch`.
|
|
71
|
+
*/
|
|
72
|
+
active?: boolean
|
|
73
|
+
/** Route matching rule used against the current configured/browser pathname. */
|
|
74
|
+
activeMatch?: SidebarActiveMatcher
|
|
75
|
+
/**
|
|
76
|
+
* Treat href matching as exact instead of prefix matching.
|
|
77
|
+
* @default false
|
|
78
|
+
*/
|
|
79
|
+
end?: boolean
|
|
80
|
+
/**
|
|
81
|
+
* Render the item as unavailable while preserving its position.
|
|
82
|
+
* @default false
|
|
83
|
+
*/
|
|
84
|
+
disabled?: boolean
|
|
23
85
|
/** Called when the item is activated. */
|
|
24
|
-
onClick?: () => void
|
|
86
|
+
onClick?: (event: MouseEvent) => void
|
|
25
87
|
}
|
|
26
88
|
|
|
27
89
|
export interface SidebarSection {
|
|
@@ -50,6 +112,18 @@ export interface SidebarSection {
|
|
|
50
112
|
export interface SidebarUser {
|
|
51
113
|
/** Display name shown in the footer. */
|
|
52
114
|
name: string
|
|
115
|
+
/** Secondary user metadata such as email or role. */
|
|
116
|
+
detail?: string
|
|
117
|
+
/** Avatar image URL. */
|
|
118
|
+
avatarUrl?: string
|
|
119
|
+
/** Initials fallback when no avatar URL is provided. */
|
|
120
|
+
initials?: string
|
|
121
|
+
/** Presence dot tone shown on the avatar. */
|
|
122
|
+
presence?: 'online' | 'away' | 'busy' | 'dnd' | 'offline'
|
|
123
|
+
/** Accessible label for the user trigger. */
|
|
124
|
+
label?: string
|
|
125
|
+
/** Called when the user surface is activated. */
|
|
126
|
+
onClick?: () => void
|
|
53
127
|
}
|
|
54
128
|
|
|
55
129
|
export interface SidebarFooterAction {
|
|
@@ -89,6 +163,28 @@ export interface SidebarBrandConfig {
|
|
|
89
163
|
decorative?: boolean
|
|
90
164
|
}
|
|
91
165
|
|
|
166
|
+
export interface SidebarScopeOption {
|
|
167
|
+
id: string
|
|
168
|
+
label: string
|
|
169
|
+
status?: 'active' | 'suspended' | 'archived' | 'warning' | 'error' | 'neutral'
|
|
170
|
+
disabled?: boolean
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export interface SidebarScopeConfig {
|
|
174
|
+
/** Label shown above/inside the selector. */
|
|
175
|
+
label?: string
|
|
176
|
+
/** Icon name resolved through the active icon set. */
|
|
177
|
+
icon?: string
|
|
178
|
+
/** Current selected option id. */
|
|
179
|
+
value?: string
|
|
180
|
+
/** Fallback label when `value` does not match an option. */
|
|
181
|
+
placeholder?: string
|
|
182
|
+
/** Options rendered in the selector menu. */
|
|
183
|
+
options: SidebarScopeOption[]
|
|
184
|
+
/** Called when the selected option changes. */
|
|
185
|
+
onChange?: (option: SidebarScopeOption) => void
|
|
186
|
+
}
|
|
187
|
+
|
|
92
188
|
export interface SidebarComponentConfig {
|
|
93
189
|
/** Container element (or selector) the sidebar is mounted into. */
|
|
94
190
|
container: HTMLElement | string
|
|
@@ -98,6 +194,12 @@ export interface SidebarComponentConfig {
|
|
|
98
194
|
sections: SidebarSection[]
|
|
99
195
|
/** Optional brand header rendered above the navigation. */
|
|
100
196
|
brand?: SidebarBrandConfig
|
|
197
|
+
/** Optional scope/tenant selector rendered below the brand. */
|
|
198
|
+
scope?: SidebarScopeConfig
|
|
199
|
+
/** Optional element inserted between the header/scope area and navigation. */
|
|
200
|
+
beforeNav?: HTMLElement | HTMLElement[]
|
|
201
|
+
/** Optional element inserted after navigation and before footer. */
|
|
202
|
+
afterNav?: HTMLElement | HTMLElement[]
|
|
101
203
|
/**
|
|
102
204
|
* Render the shared edge-docked collapse control.
|
|
103
205
|
* @default false
|
|
@@ -124,6 +226,41 @@ export interface SidebarComponentConfig {
|
|
|
124
226
|
* @default false
|
|
125
227
|
*/
|
|
126
228
|
collapsed?: boolean
|
|
229
|
+
/**
|
|
230
|
+
* Current active nav item ID. Applied during initial render and when React
|
|
231
|
+
* wrappers call `setActive`.
|
|
232
|
+
*/
|
|
233
|
+
activeItemId?: string
|
|
234
|
+
/**
|
|
235
|
+
* Current route path used to derive active link state. When omitted, browser
|
|
236
|
+
* consumers fall back to `window.location.pathname`.
|
|
237
|
+
*/
|
|
238
|
+
pathname?: string
|
|
239
|
+
/**
|
|
240
|
+
* Persist collapsed state under this localStorage key.
|
|
241
|
+
* When omitted, collapsed state remains in memory only.
|
|
242
|
+
*/
|
|
243
|
+
storageKey?: string
|
|
244
|
+
/**
|
|
245
|
+
* Persist collapsed state when `storageKey` is supplied.
|
|
246
|
+
* @default true
|
|
247
|
+
*/
|
|
248
|
+
persistCollapsed?: boolean
|
|
249
|
+
/**
|
|
250
|
+
* Viewport width below which the sidebar behaves as a mobile drawer.
|
|
251
|
+
* @default 900
|
|
252
|
+
*/
|
|
253
|
+
mobileBreakpoint?: number
|
|
254
|
+
/**
|
|
255
|
+
* Render and control a mobile backdrop alongside the sidebar.
|
|
256
|
+
* @default false
|
|
257
|
+
*/
|
|
258
|
+
mobileBackdrop?: boolean
|
|
259
|
+
/**
|
|
260
|
+
* Close the mobile drawer after a nav item activates.
|
|
261
|
+
* @default true
|
|
262
|
+
*/
|
|
263
|
+
closeOnNavigate?: boolean
|
|
127
264
|
/**
|
|
128
265
|
* Controlled mobile open state. Reflected to `data-open` for the mobile
|
|
129
266
|
* sidebar visibility contract.
|
|
@@ -134,4 +271,6 @@ export interface SidebarComponentConfig {
|
|
|
134
271
|
onNavigate?: (item: SidebarNavItem) => void
|
|
135
272
|
/** Called when the sidebar's collapsed state changes. */
|
|
136
273
|
onCollapse?: (collapsed: boolean) => void
|
|
274
|
+
/** Called when the mobile open state changes. */
|
|
275
|
+
onOpenChange?: (open: boolean) => void
|
|
137
276
|
}
|