@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.
Files changed (64) hide show
  1. package/README.md +31 -0
  2. package/dist/component-D5sRm1fq.js +389 -0
  3. package/dist/component-D5sRm1fq.js.map +1 -0
  4. package/dist/components/index.js +27 -27
  5. package/dist/icons/index.d.ts +12 -0
  6. package/dist/icons/index.d.ts.map +1 -1
  7. package/dist/icons/index.js +12 -0
  8. package/dist/icons/index.js.map +1 -1
  9. package/dist/index.d.ts +1 -1
  10. package/dist/index.d.ts.map +1 -1
  11. package/dist/index.js +127 -126
  12. package/dist/layout/app-shell.d.ts +50 -3
  13. package/dist/layout/app-shell.d.ts.map +1 -1
  14. package/dist/layout/app-shell.js +142 -13
  15. package/dist/layout/app-shell.js.map +1 -1
  16. package/dist/layout/core-app-chrome.d.ts +81 -0
  17. package/dist/layout/core-app-chrome.d.ts.map +1 -0
  18. package/dist/layout/core-app-chrome.js +349 -0
  19. package/dist/layout/core-app-chrome.js.map +1 -0
  20. package/dist/layout/index.d.ts +4 -2
  21. package/dist/layout/index.d.ts.map +1 -1
  22. package/dist/layout/index.js +88 -87
  23. package/dist/layout/index.js.map +1 -1
  24. package/dist/react/primitives/badge.d.ts +1 -1
  25. package/dist/react/primitives/button.d.ts +2 -2
  26. package/dist/react/primitives/copy-button.d.ts +1 -1
  27. package/dist/react/primitives/stat-card.d.ts +1 -1
  28. package/dist/react/primitives/toggle.d.ts +1 -1
  29. package/dist/react/sidebar.d.ts +4 -4
  30. package/dist/react/sidebar.d.ts.map +1 -1
  31. package/dist/react/sidebar.js +18 -16
  32. package/dist/react/sidebar.js.map +1 -1
  33. package/dist/sidebar/component.d.ts +24 -2
  34. package/dist/sidebar/component.d.ts.map +1 -1
  35. package/dist/sidebar/enhance.d.ts +8 -0
  36. package/dist/sidebar/enhance.d.ts.map +1 -1
  37. package/dist/sidebar/index.d.ts +3 -0
  38. package/dist/sidebar/index.d.ts.map +1 -1
  39. package/dist/sidebar/index.js +81 -28
  40. package/dist/sidebar/index.js.map +1 -1
  41. package/dist/sidebar/types.d.ts +126 -4
  42. package/dist/sidebar/types.d.ts.map +1 -1
  43. package/dist/styles/layout.css +252 -0
  44. package/dist/styles/sidebar.css +313 -5
  45. package/package.json +6 -1
  46. package/src/icons/icons.test.ts +9 -0
  47. package/src/icons/index.ts +12 -0
  48. package/src/index.ts +11 -0
  49. package/src/layout/app-shell.test.ts +204 -0
  50. package/src/layout/app-shell.ts +362 -3
  51. package/src/layout/core-app-chrome.test.ts +507 -0
  52. package/src/layout/core-app-chrome.ts +662 -0
  53. package/src/layout/index.ts +36 -2
  54. package/src/react/sidebar.test.tsx +104 -3
  55. package/src/react/sidebar.tsx +26 -4
  56. package/src/sidebar/component.test.ts +395 -1
  57. package/src/sidebar/component.ts +661 -86
  58. package/src/sidebar/enhance.ts +118 -0
  59. package/src/sidebar/index.ts +144 -0
  60. package/src/sidebar/types.ts +143 -4
  61. package/src/styles/layout.css +252 -0
  62. package/src/styles/sidebar.css +313 -5
  63. package/dist/component-Bxhxf21c.js +0 -167
  64. package/dist/component-Bxhxf21c.js.map +0 -1
@@ -0,0 +1,662 @@
1
+ import type {
2
+ SidebarBrandConfig,
3
+ SidebarComponentConfig,
4
+ SidebarFooterAction,
5
+ SidebarNavItem,
6
+ SidebarScopeConfig,
7
+ SidebarSection,
8
+ SidebarUser,
9
+ } from '../sidebar/types'
10
+ import { SidebarComponent } from '../sidebar/component'
11
+ import { resolveContainer } from '../primitives/utils'
12
+ import {
13
+ TopMenuBar,
14
+ type TopMenuBarActionInput,
15
+ type TopMenuBarConfig,
16
+ type TopMenuBarUser,
17
+ } from './app-shell'
18
+
19
+ export interface CoreAppRouteLabel {
20
+ pattern: RegExp
21
+ label: string
22
+ }
23
+
24
+ export interface CoreAppSidebarPresetOptions extends Omit<
25
+ SidebarComponentConfig,
26
+ 'container' | 'sections'
27
+ > {
28
+ basePath?: string
29
+ sections?: SidebarSection[]
30
+ }
31
+
32
+ export interface CoreAppTopbarPresetOptions extends Omit<
33
+ TopMenuBarConfig,
34
+ 'actions' | 'current' | 'product'
35
+ > {
36
+ basePath?: string
37
+ pathname?: string
38
+ product?: string
39
+ current?: string
40
+ actions?: TopMenuBarActionInput[]
41
+ includeSearchAction?: boolean
42
+ includeFeedbackAction?: boolean
43
+ includeHelpAction?: boolean
44
+ }
45
+
46
+ export interface CoreAppChromePreset {
47
+ sidebar: Omit<SidebarComponentConfig, 'container'>
48
+ topbar: TopMenuBarConfig
49
+ }
50
+
51
+ export interface CoreAppChromePresetOptions {
52
+ basePath?: string
53
+ pathname?: string
54
+ scope?: SidebarScopeConfig
55
+ sidebarUser?: SidebarUser
56
+ sidebarFooterActions?: SidebarFooterAction[]
57
+ topbarUser?: TopMenuBarUser
58
+ topbarActions?: TopMenuBarActionInput[]
59
+ }
60
+
61
+ export interface CoreAppChromeMountOptions extends CoreAppChromePresetOptions {
62
+ sidebarContainer: HTMLElement | string
63
+ topbarContainer?: HTMLElement | string
64
+ sidebar?: CoreAppSidebarPresetOptions
65
+ topbar?: CoreAppTopbarPresetOptions
66
+ /**
67
+ * Move explicitly marked server-rendered topbar action nodes into the new topbar.
68
+ * Useful for native hooks that are initialized outside the chrome mount.
69
+ * @default true
70
+ */
71
+ preserveTopbarActions?: boolean
72
+ /**
73
+ * Selector for preserved action nodes inside the topbar container.
74
+ * @default '[data-core-app-topbar-action]'
75
+ */
76
+ topbarActionSelector?: string
77
+ /**
78
+ * Remove existing placeholder/server-rendered children before mounting.
79
+ * @default true
80
+ */
81
+ clearContainers?: boolean
82
+ }
83
+
84
+ export interface CoreAppChromeDomMountOptions extends CoreAppChromePresetOptions {
85
+ root?: ParentNode
86
+ sidebarSelector?: string
87
+ topbarSelector?: string
88
+ sidebar?: CoreAppSidebarPresetOptions
89
+ topbar?: CoreAppTopbarPresetOptions
90
+ preserveTopbarActions?: boolean
91
+ topbarActionSelector?: string
92
+ clearContainers?: boolean
93
+ }
94
+
95
+ export interface CoreAppChromeMount {
96
+ sidebar: SidebarComponent
97
+ topbar?: TopMenuBar
98
+ destroy: () => void
99
+ }
100
+
101
+ interface CoreAppNavItemBlueprint {
102
+ id: string
103
+ label: string
104
+ href: string
105
+ icon: string
106
+ end?: boolean
107
+ }
108
+
109
+ interface CoreAppNavSectionBlueprint {
110
+ id: string
111
+ label: string
112
+ items: CoreAppNavItemBlueprint[]
113
+ }
114
+
115
+ const DEFAULT_DOM_SIDEBAR_SELECTOR = '[data-core-app-sidebar]'
116
+ const DEFAULT_DOM_TOPBAR_SELECTOR = '[data-core-app-topbar]'
117
+ const DEFAULT_TOPBAR_ACTION_SELECTOR = '[data-core-app-topbar-action]'
118
+
119
+ const CORE_APP_NAV_SECTIONS: CoreAppNavSectionBlueprint[] = [
120
+ {
121
+ id: 'dashboards',
122
+ label: 'Dashboards',
123
+ items: [
124
+ { id: 'library', label: 'Library', href: '/', icon: 'library', end: true },
125
+ { id: 'capacity', label: 'Capacity', href: '/capacity', icon: 'capacity' },
126
+ { id: 'operational-alerts', label: 'Operational Alerts', href: '/alerts', icon: 'alerts' },
127
+ ],
128
+ },
129
+ {
130
+ id: 'threat-library',
131
+ label: 'Threat Library',
132
+ items: [
133
+ { id: 'detections', label: 'Detections', href: '/detections', icon: 'detections' },
134
+ { id: 'actors', label: 'Actors', href: '/actors', icon: 'actors' },
135
+ { id: 'library-reports', label: 'Library Reports', href: '/intel', icon: 'intel-reports' },
136
+ { id: 'mitre', label: 'MITRE ATT&CK', href: '/mitre', icon: 'mitre' },
137
+ ],
138
+ },
139
+ {
140
+ id: 'library-operations',
141
+ label: 'Library Operations',
142
+ items: [{ id: 'pipeline', label: 'Pipeline', href: '/pipeline', icon: 'pipeline' }],
143
+ },
144
+ {
145
+ id: 'integrations',
146
+ label: 'Integrations',
147
+ items: [
148
+ { id: 'connectors', label: 'Connectors', href: '/connectors', icon: 'connectors' },
149
+ { id: 'jobs', label: 'Jobs', href: '/jobs', icon: 'jobs' },
150
+ ],
151
+ },
152
+ {
153
+ id: 'platform',
154
+ label: 'Platform',
155
+ items: [
156
+ { id: 'tenants', label: 'Tenants', href: '/tenants', icon: 'tenants' },
157
+ { id: 'runners', label: 'Runners', href: '/runners', icon: 'runners' },
158
+ { id: 'topology', label: 'Topology', href: '/topology', icon: 'topology' },
159
+ { id: 'api-access', label: 'API Access', href: '/api-access', icon: 'api-access' },
160
+ ],
161
+ },
162
+ ]
163
+
164
+ export const CORE_APP_ROUTE_LABELS: CoreAppRouteLabel[] = [
165
+ { pattern: /^\/(?:dashboards\/)?capacity(?:\/|$)/, label: 'Capacity' },
166
+ { pattern: /^\/(?:dashboards\/)?alerts(?:\/|$)/, label: 'Operational Alerts' },
167
+ { pattern: /^\/dashboards\/library(?:\/|$)/, label: 'Library Dashboard' },
168
+ { pattern: /^\/detections(?:\/|$)/, label: 'Detections' },
169
+ { pattern: /^\/actors(?:\/|$)/, label: 'Actors' },
170
+ { pattern: /^\/intel(?:\/|$)/, label: 'Library Reports' },
171
+ { pattern: /^\/mitre(?:\/|$)/, label: 'MITRE ATT&CK' },
172
+ { pattern: /^\/pipeline(?:\/|$)/, label: 'Pipeline' },
173
+ { pattern: /^\/connectors(?:\/|$)/, label: 'Connectors' },
174
+ { pattern: /^\/jobs(?:\/|$)/, label: 'Jobs' },
175
+ { pattern: /^\/topology(?:\/|$)/, label: 'Topology' },
176
+ { pattern: /^\/tenants(?:\/|$)/, label: 'Tenants' },
177
+ { pattern: /^\/runners(?:\/|$)/, label: 'Runners' },
178
+ { pattern: /^\/api-access(?:\/|$)/, label: 'API Access' },
179
+ { pattern: /^\/settings(?:\/|$)/, label: 'Settings' },
180
+ ]
181
+
182
+ function normalizeBasePath(basePath: string): string {
183
+ const trimmed = basePath.trim()
184
+ if (trimmed === '' || trimmed === '/') {
185
+ return ''
186
+ }
187
+ return `/${trimmed.replace(/^\/+|\/+$/g, '')}`
188
+ }
189
+
190
+ function joinBasePath(basePath: string, href: string): string {
191
+ const normalizedBase = normalizeBasePath(basePath)
192
+ if (href === '/' || href === '') {
193
+ return normalizedBase || '/'
194
+ }
195
+ return `${normalizedBase}/${href.replace(/^\/+/, '')}`
196
+ }
197
+
198
+ function stripBasePath(pathname: string, basePath: string): string {
199
+ const normalizedBase = normalizeBasePath(basePath)
200
+ if (normalizedBase === '') {
201
+ return pathname || '/'
202
+ }
203
+
204
+ if (pathname === normalizedBase) {
205
+ return '/'
206
+ }
207
+
208
+ if (pathname.startsWith(`${normalizedBase}/`)) {
209
+ return pathname.slice(normalizedBase.length) || '/'
210
+ }
211
+
212
+ return pathname || '/'
213
+ }
214
+
215
+ function cloneSections(sections: SidebarSection[]): SidebarSection[] {
216
+ return sections.map(section => ({
217
+ ...section,
218
+ items: section.items.map(item => ({ ...item })),
219
+ }))
220
+ }
221
+
222
+ export function createCoreAppSidebarSections(basePath = '/app'): SidebarSection[] {
223
+ return CORE_APP_NAV_SECTIONS.map(section => ({
224
+ id: section.id,
225
+ label: section.label,
226
+ items: section.items.map(item => {
227
+ const navItem: SidebarNavItem = {
228
+ id: item.id,
229
+ label: item.label,
230
+ href: joinBasePath(basePath, item.href),
231
+ icon: item.icon,
232
+ tooltip: item.label,
233
+ activeMatch: item.end === true ? 'exact' : 'prefix',
234
+ }
235
+ if (item.end !== undefined) {
236
+ navItem.end = item.end
237
+ }
238
+ return navItem
239
+ }),
240
+ }))
241
+ }
242
+
243
+ export function getCoreAppRouteLabel(pathname: string, basePath = '/app'): string {
244
+ const relativePathname = stripBasePath(pathname, basePath)
245
+ if (relativePathname === '/' || relativePathname === '') {
246
+ return 'Platform Overview'
247
+ }
248
+
249
+ return (
250
+ CORE_APP_ROUTE_LABELS.find(route => route.pattern.test(relativePathname))?.label ??
251
+ 'Threat Library'
252
+ )
253
+ }
254
+
255
+ export function createCoreAppSidebarConfig(
256
+ options: CoreAppSidebarPresetOptions = {}
257
+ ): Omit<SidebarComponentConfig, 'container'> {
258
+ const {
259
+ basePath = '/app',
260
+ sections,
261
+ variant = 'cm',
262
+ brand,
263
+ scope,
264
+ beforeNav,
265
+ afterNav,
266
+ collapsible = true,
267
+ collapseButtonVisible = true,
268
+ user,
269
+ footerActions,
270
+ iconSet,
271
+ collapsed,
272
+ activeItemId,
273
+ pathname,
274
+ storageKey = 'cm-sidebar-collapsed',
275
+ persistCollapsed,
276
+ mobileBreakpoint,
277
+ mobileBackdrop = true,
278
+ closeOnNavigate = true,
279
+ open,
280
+ onNavigate,
281
+ onCollapse,
282
+ onOpenChange,
283
+ } = options
284
+
285
+ const resolvedBrand: SidebarBrandConfig = brand ?? {
286
+ label: 'CounterMeasure',
287
+ href: joinBasePath(basePath, '/'),
288
+ }
289
+
290
+ const config: Omit<SidebarComponentConfig, 'container'> = {
291
+ variant,
292
+ sections:
293
+ sections === undefined ? createCoreAppSidebarSections(basePath) : cloneSections(sections),
294
+ brand: resolvedBrand,
295
+ collapsible,
296
+ collapseButtonVisible,
297
+ storageKey,
298
+ mobileBackdrop,
299
+ closeOnNavigate,
300
+ }
301
+
302
+ if (scope !== undefined) config.scope = scope
303
+ if (beforeNav !== undefined) config.beforeNav = beforeNav
304
+ if (afterNav !== undefined) config.afterNav = afterNav
305
+ if (user !== undefined) config.user = user
306
+ if (footerActions !== undefined) config.footerActions = footerActions
307
+ if (iconSet !== undefined) config.iconSet = iconSet
308
+ if (collapsed !== undefined) config.collapsed = collapsed
309
+ if (activeItemId !== undefined) config.activeItemId = activeItemId
310
+ if (pathname !== undefined) config.pathname = pathname
311
+ if (persistCollapsed !== undefined) config.persistCollapsed = persistCollapsed
312
+ if (mobileBreakpoint !== undefined) config.mobileBreakpoint = mobileBreakpoint
313
+ if (open !== undefined) config.open = open
314
+ if (onNavigate !== undefined) config.onNavigate = onNavigate
315
+ if (onCollapse !== undefined) config.onCollapse = onCollapse
316
+ if (onOpenChange !== undefined) config.onOpenChange = onOpenChange
317
+
318
+ return config
319
+ }
320
+
321
+ export function createCoreAppTopbarActions(
322
+ options: Pick<
323
+ CoreAppTopbarPresetOptions,
324
+ 'includeSearchAction' | 'includeFeedbackAction' | 'includeHelpAction'
325
+ > = {}
326
+ ): TopMenuBarActionInput[] {
327
+ const {
328
+ includeSearchAction = true,
329
+ includeFeedbackAction = true,
330
+ includeHelpAction = true,
331
+ } = options
332
+ const actions: TopMenuBarActionInput[] = []
333
+
334
+ if (includeSearchAction) {
335
+ actions.push({
336
+ id: 'search',
337
+ label: 'Find...',
338
+ icon: 'search',
339
+ showLabel: true,
340
+ className: 'cmm-topbar__action--search',
341
+ attributes: { 'data-app-search-trigger': true },
342
+ })
343
+ }
344
+
345
+ if (includeFeedbackAction) {
346
+ actions.push({
347
+ id: 'feedback',
348
+ label: 'Feedback',
349
+ showLabel: true,
350
+ attributes: { 'data-feedback-toggle': true },
351
+ })
352
+ }
353
+
354
+ if (includeHelpAction) {
355
+ actions.push({
356
+ id: 'help',
357
+ label: 'Help',
358
+ icon: 'help',
359
+ attributes: { 'data-help-toggle': true },
360
+ })
361
+ }
362
+
363
+ return actions
364
+ }
365
+
366
+ export function createCoreAppTopbarConfig(
367
+ options: CoreAppTopbarPresetOptions = {}
368
+ ): TopMenuBarConfig {
369
+ const {
370
+ basePath = '/app',
371
+ pathname = typeof window === 'undefined' ? '/' : window.location.pathname,
372
+ product = 'CounterMeasure Core',
373
+ current = getCoreAppRouteLabel(pathname, basePath),
374
+ actions,
375
+ beforeActions,
376
+ afterActions,
377
+ user,
378
+ iconSet,
379
+ className,
380
+ breadcrumbs,
381
+ container,
382
+ includeSearchAction,
383
+ includeFeedbackAction,
384
+ includeHelpAction,
385
+ } = options
386
+
387
+ const actionOptions: Pick<
388
+ CoreAppTopbarPresetOptions,
389
+ 'includeSearchAction' | 'includeFeedbackAction' | 'includeHelpAction'
390
+ > = {}
391
+ if (includeSearchAction !== undefined) actionOptions.includeSearchAction = includeSearchAction
392
+ if (includeFeedbackAction !== undefined)
393
+ actionOptions.includeFeedbackAction = includeFeedbackAction
394
+ if (includeHelpAction !== undefined) actionOptions.includeHelpAction = includeHelpAction
395
+
396
+ const config: TopMenuBarConfig = {
397
+ product,
398
+ current,
399
+ actions: actions ?? createCoreAppTopbarActions(actionOptions),
400
+ }
401
+
402
+ if (container !== undefined) config.container = container
403
+ if (breadcrumbs !== undefined) config.breadcrumbs = breadcrumbs
404
+ if (beforeActions !== undefined) config.beforeActions = beforeActions
405
+ if (afterActions !== undefined) config.afterActions = afterActions
406
+ if (user !== undefined) config.user = user
407
+ if (iconSet !== undefined) config.iconSet = iconSet
408
+ if (className !== undefined) config.className = className
409
+
410
+ return config
411
+ }
412
+
413
+ export function createCoreAppChromePreset(
414
+ options: CoreAppChromePresetOptions = {}
415
+ ): CoreAppChromePreset {
416
+ const {
417
+ basePath = '/app',
418
+ pathname,
419
+ scope,
420
+ sidebarUser,
421
+ sidebarFooterActions,
422
+ topbarUser,
423
+ topbarActions,
424
+ } = options
425
+
426
+ const sidebarOptions: CoreAppSidebarPresetOptions = { basePath }
427
+ if (scope !== undefined) sidebarOptions.scope = scope
428
+ if (sidebarUser !== undefined) sidebarOptions.user = sidebarUser
429
+ if (sidebarFooterActions !== undefined) sidebarOptions.footerActions = sidebarFooterActions
430
+ if (pathname !== undefined) sidebarOptions.pathname = pathname
431
+
432
+ const topbarOptions: CoreAppTopbarPresetOptions = { basePath }
433
+ if (pathname !== undefined) topbarOptions.pathname = pathname
434
+ if (topbarUser !== undefined) topbarOptions.user = topbarUser
435
+ if (topbarActions !== undefined) topbarOptions.actions = topbarActions
436
+
437
+ return {
438
+ sidebar: createCoreAppSidebarConfig(sidebarOptions),
439
+ topbar: createCoreAppTopbarConfig(topbarOptions),
440
+ }
441
+ }
442
+
443
+ function clearContainer(container: HTMLElement, clearContainers: boolean | undefined): void {
444
+ if (clearContainers !== false) {
445
+ container.replaceChildren()
446
+ }
447
+ }
448
+
449
+ function collectTopbarActionNodes(options: {
450
+ container: HTMLElement
451
+ preserveTopbarActions: boolean | undefined
452
+ selector: string | undefined
453
+ }): HTMLElement[] {
454
+ if (options.preserveTopbarActions === false) {
455
+ return []
456
+ }
457
+
458
+ return Array.from(
459
+ options.container.querySelectorAll<HTMLElement>(
460
+ options.selector ?? DEFAULT_TOPBAR_ACTION_SELECTOR
461
+ )
462
+ )
463
+ }
464
+
465
+ function mergeElements(
466
+ existing: HTMLElement | HTMLElement[] | undefined,
467
+ additions: HTMLElement[]
468
+ ): HTMLElement | HTMLElement[] | undefined {
469
+ if (additions.length === 0) {
470
+ return existing
471
+ }
472
+
473
+ if (existing === undefined) {
474
+ return additions
475
+ }
476
+
477
+ return Array.isArray(existing) ? [...existing, ...additions] : [existing, ...additions]
478
+ }
479
+
480
+ function hasPreservedActionAttribute(actions: HTMLElement[], attribute: string): boolean {
481
+ return actions.some(action => action.hasAttribute(attribute))
482
+ }
483
+
484
+ function dedupeDefaultActionsForPreservedNodes(
485
+ options: CoreAppTopbarPresetOptions,
486
+ preservedActions: HTMLElement[]
487
+ ): void {
488
+ if (options.actions !== undefined || preservedActions.length === 0) {
489
+ return
490
+ }
491
+
492
+ if (
493
+ options.includeSearchAction === undefined &&
494
+ hasPreservedActionAttribute(preservedActions, 'data-app-search-trigger')
495
+ ) {
496
+ options.includeSearchAction = false
497
+ }
498
+
499
+ if (
500
+ options.includeFeedbackAction === undefined &&
501
+ hasPreservedActionAttribute(preservedActions, 'data-feedback-toggle')
502
+ ) {
503
+ options.includeFeedbackAction = false
504
+ }
505
+
506
+ if (
507
+ options.includeHelpAction === undefined &&
508
+ hasPreservedActionAttribute(preservedActions, 'data-help-toggle')
509
+ ) {
510
+ options.includeHelpAction = false
511
+ }
512
+ }
513
+
514
+ function buildSidebarMountOptions(options: CoreAppChromeMountOptions): CoreAppSidebarPresetOptions {
515
+ const basePath = options.sidebar?.basePath ?? options.basePath ?? '/app'
516
+ const sidebarOptions: CoreAppSidebarPresetOptions = {
517
+ ...options.sidebar,
518
+ basePath,
519
+ }
520
+
521
+ if (options.scope !== undefined) sidebarOptions.scope = options.scope
522
+ if (options.sidebarUser !== undefined) sidebarOptions.user = options.sidebarUser
523
+ if (options.sidebarFooterActions !== undefined)
524
+ sidebarOptions.footerActions = options.sidebarFooterActions
525
+ if (options.pathname !== undefined && sidebarOptions.pathname === undefined) {
526
+ sidebarOptions.pathname = options.pathname
527
+ }
528
+
529
+ return sidebarOptions
530
+ }
531
+
532
+ function buildTopbarMountOptions(
533
+ options: CoreAppChromeMountOptions,
534
+ topbarContainer: HTMLElement,
535
+ preservedActions: HTMLElement[]
536
+ ): CoreAppTopbarPresetOptions {
537
+ const basePath = options.topbar?.basePath ?? options.basePath ?? '/app'
538
+ const topbarOptions: CoreAppTopbarPresetOptions = {
539
+ ...options.topbar,
540
+ basePath,
541
+ container: topbarContainer,
542
+ }
543
+ const afterActions = mergeElements(options.topbar?.afterActions, preservedActions)
544
+
545
+ if (options.pathname !== undefined) topbarOptions.pathname = options.pathname
546
+ if (options.topbarUser !== undefined) topbarOptions.user = options.topbarUser
547
+ if (options.topbarActions !== undefined) topbarOptions.actions = options.topbarActions
548
+ if (afterActions !== undefined) topbarOptions.afterActions = afterActions
549
+ dedupeDefaultActionsForPreservedNodes(topbarOptions, preservedActions)
550
+
551
+ return topbarOptions
552
+ }
553
+
554
+ export function mountCoreAppChrome(options: CoreAppChromeMountOptions): CoreAppChromeMount {
555
+ const sidebarContainer = resolveContainer(options.sidebarContainer)
556
+ clearContainer(sidebarContainer, options.clearContainers)
557
+
558
+ const sidebar = new SidebarComponent({
559
+ container: sidebarContainer,
560
+ ...createCoreAppSidebarConfig(buildSidebarMountOptions(options)),
561
+ })
562
+
563
+ const topbarContainerOption = options.topbarContainer ?? options.topbar?.container
564
+ const topbarContainer =
565
+ topbarContainerOption === undefined ? undefined : resolveContainer(topbarContainerOption)
566
+ let topbar: TopMenuBar | undefined
567
+
568
+ if (topbarContainer !== undefined) {
569
+ const preservedActions = collectTopbarActionNodes({
570
+ container: topbarContainer,
571
+ preserveTopbarActions: options.preserveTopbarActions,
572
+ selector: options.topbarActionSelector,
573
+ })
574
+ clearContainer(topbarContainer, options.clearContainers)
575
+ topbar = new TopMenuBar(
576
+ createCoreAppTopbarConfig(buildTopbarMountOptions(options, topbarContainer, preservedActions))
577
+ )
578
+ }
579
+
580
+ const mounted: CoreAppChromeMount = {
581
+ sidebar,
582
+ destroy: () => {
583
+ topbar?.destroy()
584
+ sidebar.destroy()
585
+ },
586
+ }
587
+
588
+ if (topbar !== undefined) {
589
+ mounted.topbar = topbar
590
+ }
591
+
592
+ return mounted
593
+ }
594
+
595
+ function readDataValue(element: HTMLElement | undefined, key: string): string | undefined {
596
+ const value = element?.dataset[key]
597
+ return value === undefined || value.length === 0 ? undefined : value
598
+ }
599
+
600
+ function buildDeclarativeTopbarOptions(
601
+ options: CoreAppChromeDomMountOptions,
602
+ sidebarContainer: HTMLElement,
603
+ topbarContainer: HTMLElement | null
604
+ ): CoreAppTopbarPresetOptions | undefined {
605
+ const product =
606
+ options.topbar?.product ??
607
+ readDataValue(topbarContainer ?? undefined, 'product') ??
608
+ readDataValue(sidebarContainer, 'product')
609
+ const current =
610
+ options.topbar?.current ??
611
+ readDataValue(topbarContainer ?? undefined, 'current') ??
612
+ readDataValue(sidebarContainer, 'current')
613
+
614
+ if (options.topbar === undefined && product === undefined && current === undefined) {
615
+ return undefined
616
+ }
617
+
618
+ const topbar: CoreAppTopbarPresetOptions = { ...(options.topbar ?? {}) }
619
+ if (product !== undefined) topbar.product = product
620
+ if (current !== undefined) topbar.current = current
621
+ return topbar
622
+ }
623
+
624
+ export function mountCoreAppChromeFromDom(
625
+ options: CoreAppChromeDomMountOptions = {}
626
+ ): CoreAppChromeMount | null {
627
+ const root = options.root ?? document
628
+ const sidebarContainer = root.querySelector<HTMLElement>(
629
+ options.sidebarSelector ?? DEFAULT_DOM_SIDEBAR_SELECTOR
630
+ )
631
+
632
+ if (sidebarContainer === null) {
633
+ return null
634
+ }
635
+
636
+ const topbarContainer = root.querySelector<HTMLElement>(
637
+ options.topbarSelector ?? DEFAULT_DOM_TOPBAR_SELECTOR
638
+ )
639
+ const basePath =
640
+ options.basePath ??
641
+ options.sidebar?.basePath ??
642
+ options.topbar?.basePath ??
643
+ readDataValue(sidebarContainer, 'basePath') ??
644
+ readDataValue(topbarContainer ?? undefined, 'basePath') ??
645
+ '/app'
646
+ const pathname =
647
+ options.pathname ??
648
+ readDataValue(sidebarContainer, 'pathname') ??
649
+ readDataValue(topbarContainer ?? undefined, 'pathname')
650
+
651
+ const mountOptions: CoreAppChromeMountOptions = {
652
+ ...options,
653
+ sidebarContainer,
654
+ basePath,
655
+ }
656
+ const topbar = buildDeclarativeTopbarOptions(options, sidebarContainer, topbarContainer)
657
+ if (topbar !== undefined) mountOptions.topbar = topbar
658
+ if (topbarContainer !== null) mountOptions.topbarContainer = topbarContainer
659
+ if (pathname !== undefined) mountOptions.pathname = pathname
660
+
661
+ return mountCoreAppChrome(mountOptions)
662
+ }
@@ -7,8 +7,42 @@
7
7
 
8
8
  import { createSafeMarkupFragment } from '../utils/sanitize'
9
9
  import { navigateDocumentTo } from '../utils/navigation'
10
- export { AppShell, createAppShell } from './app-shell'
11
- export type { AppShellConfig } from './app-shell'
10
+ export {
11
+ AppShell,
12
+ TopMenuBar,
13
+ createAppShell,
14
+ createProductShell,
15
+ createTopMenuBar,
16
+ } from './app-shell'
17
+ export type {
18
+ AppShellConfig,
19
+ TopMenuBarAction,
20
+ TopMenuBarActionInput,
21
+ TopMenuBarBreadcrumb,
22
+ TopMenuBarConfig,
23
+ TopMenuBarUser,
24
+ } from './app-shell'
25
+ export {
26
+ CORE_APP_ROUTE_LABELS,
27
+ createCoreAppChromePreset,
28
+ createCoreAppSidebarConfig,
29
+ createCoreAppSidebarSections,
30
+ createCoreAppTopbarActions,
31
+ createCoreAppTopbarConfig,
32
+ getCoreAppRouteLabel,
33
+ mountCoreAppChrome,
34
+ mountCoreAppChromeFromDom,
35
+ } from './core-app-chrome'
36
+ export type {
37
+ CoreAppChromePreset,
38
+ CoreAppChromeDomMountOptions,
39
+ CoreAppChromeMount,
40
+ CoreAppChromeMountOptions,
41
+ CoreAppChromePresetOptions,
42
+ CoreAppRouteLabel,
43
+ CoreAppSidebarPresetOptions,
44
+ CoreAppTopbarPresetOptions,
45
+ } from './core-app-chrome'
12
46
  export { PageHeader, createPageHeader } from './page-header'
13
47
  export type { PageHeaderConfig, PageHeaderMetadata } from './page-header'
14
48