@countermeasure-platform/web-components 1.3.5 → 1.3.6-dev.37.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 (82) hide show
  1. package/dist/{component-D5sRm1fq.js → component-D_asjmrt.js} +5 -3
  2. package/dist/component-D_asjmrt.js.map +1 -0
  3. package/dist/components/brand/index.d.ts.map +1 -1
  4. package/dist/components/brand/index.js +11 -5
  5. package/dist/components/brand/index.js.map +1 -1
  6. package/dist/components/brand/types.d.ts +2 -0
  7. package/dist/components/brand/types.d.ts.map +1 -1
  8. package/dist/icons/index.d.ts +7 -2
  9. package/dist/icons/index.d.ts.map +1 -1
  10. package/dist/icons/index.js +7 -2
  11. package/dist/icons/index.js.map +1 -1
  12. package/dist/icons/lucide.d.ts +3 -0
  13. package/dist/icons/lucide.d.ts.map +1 -1
  14. package/dist/icons/lucide.js +3 -0
  15. package/dist/icons/lucide.js.map +1 -1
  16. package/dist/index.d.ts +1 -1
  17. package/dist/index.d.ts.map +1 -1
  18. package/dist/index.js +126 -125
  19. package/dist/layout/app-shell.d.ts +7 -0
  20. package/dist/layout/app-shell.d.ts.map +1 -1
  21. package/dist/layout/app-shell.js +24 -6
  22. package/dist/layout/app-shell.js.map +1 -1
  23. package/dist/layout/core-app-chrome.d.ts.map +1 -1
  24. package/dist/layout/core-app-chrome.js +8 -5
  25. package/dist/layout/core-app-chrome.js.map +1 -1
  26. package/dist/layout/core-app-library-dashboard.d.ts +72 -0
  27. package/dist/layout/core-app-library-dashboard.d.ts.map +1 -0
  28. package/dist/layout/core-app-library-dashboard.js +160 -0
  29. package/dist/layout/core-app-library-dashboard.js.map +1 -0
  30. package/dist/layout/index.d.ts +2 -0
  31. package/dist/layout/index.d.ts.map +1 -1
  32. package/dist/layout/index.js +38 -37
  33. package/dist/layout/index.js.map +1 -1
  34. package/dist/react/brand/index.d.ts +3 -1
  35. package/dist/react/brand/index.d.ts.map +1 -1
  36. package/dist/react/brand.js +19 -13
  37. package/dist/react/brand.js.map +1 -1
  38. package/dist/react/layout/core-app-library-dashboard.d.ts +13 -0
  39. package/dist/react/layout/core-app-library-dashboard.d.ts.map +1 -0
  40. package/dist/react/layout/core-app-library-dashboard.js +27 -0
  41. package/dist/react/layout/core-app-library-dashboard.js.map +1 -0
  42. package/dist/react/layout/index.d.ts +1 -0
  43. package/dist/react/layout/index.d.ts.map +1 -1
  44. package/dist/react/layout/index.js +3 -2
  45. package/dist/react/primitives/alert.d.ts +1 -1
  46. package/dist/react/primitives/toast.d.ts +1 -1
  47. package/dist/react/sidebar.js +1 -1
  48. package/dist/react.js +97 -96
  49. package/dist/sidebar/component.d.ts.map +1 -1
  50. package/dist/sidebar/index.js +1 -1
  51. package/dist/sidebar/types.d.ts +7 -0
  52. package/dist/sidebar/types.d.ts.map +1 -1
  53. package/dist/styles/components/brand.css +22 -0
  54. package/dist/styles/layout.css +577 -0
  55. package/dist/styles/sidebar.css +26 -0
  56. package/package.json +11 -1
  57. package/src/components/brand/index.ts +22 -4
  58. package/src/components/brand/types.ts +2 -0
  59. package/src/icons/icons.test.ts +1 -1
  60. package/src/icons/index.ts +7 -2
  61. package/src/icons/lucide.ts +4 -0
  62. package/src/index.ts +2 -0
  63. package/src/layout/app-shell.test.ts +76 -0
  64. package/src/layout/app-shell.ts +38 -7
  65. package/src/layout/core-app-chrome.test.ts +17 -5
  66. package/src/layout/core-app-chrome.ts +8 -3
  67. package/src/layout/core-app-library-dashboard.test.ts +397 -0
  68. package/src/layout/core-app-library-dashboard.ts +519 -0
  69. package/src/layout/index.ts +18 -0
  70. package/src/react/brand/index.test.tsx +10 -0
  71. package/src/react/brand/index.tsx +25 -4
  72. package/src/react/layout/core-app-chrome.test.tsx +2 -2
  73. package/src/react/layout/core-app-library-dashboard.test.tsx +42 -0
  74. package/src/react/layout/core-app-library-dashboard.tsx +56 -0
  75. package/src/react/layout/index.ts +6 -0
  76. package/src/sidebar/component.test.ts +21 -1
  77. package/src/sidebar/component.ts +14 -8
  78. package/src/sidebar/types.ts +7 -0
  79. package/src/styles/components/brand.css +22 -0
  80. package/src/styles/layout.css +577 -0
  81. package/src/styles/sidebar.css +26 -0
  82. package/dist/component-D5sRm1fq.js.map +0 -1
@@ -29,6 +29,8 @@ export interface BrandLockupConfig {
29
29
  container?: HTMLElement | string
30
30
  markSize?: BrandSize | number
31
31
  label?: string
32
+ subtitle?: string
33
+ wordmarkCase?: 'uppercase' | 'title'
32
34
  decorative?: boolean
33
35
  showWordmark?: boolean
34
36
  className?: string
@@ -106,7 +106,7 @@ describe('Icons Module', () => {
106
106
  expect(resolveLucideIconName('capacity')).toBe('Gauge')
107
107
  expect(resolveLucideIconName('api-access')).toBe('KeyRound')
108
108
  expect(resolveLucideIconName('intel-reports')).toBe('FileText')
109
- expect(resolveLucideIconName('notifications')).toBe('Siren')
109
+ expect(resolveLucideIconName('notifications')).toBe('Bell')
110
110
  expect(resolveLucideIconName('runners')).toBe('Server')
111
111
  expect(getIconSvgInner('lucide', 'capacity')).toBe(getIconSvgInner('lucide', 'Gauge'))
112
112
  })
@@ -52,7 +52,7 @@ export const SEMANTIC_ICON_ALIASES = {
52
52
  'api-access': 'KeyRound',
53
53
  authentication: 'Lock',
54
54
  actors: 'Users',
55
- bell: 'Siren',
55
+ bell: 'Bell',
56
56
  capacity: 'Gauge',
57
57
  'circle-help': 'CircleAlert',
58
58
  code: 'Code2',
@@ -63,7 +63,12 @@ export const SEMANTIC_ICON_ALIASES = {
63
63
  integrations: 'Plug',
64
64
  library: 'BookOpen',
65
65
  mitre: 'Grid3X3',
66
- notifications: 'Siren',
66
+ dark: 'Moon',
67
+ light: 'Sun',
68
+ midnight: 'Monitor',
69
+ monitor: 'Monitor',
70
+ moon: 'Moon',
71
+ notifications: 'Bell',
67
72
  passkeys: 'KeyRound',
68
73
  pipeline: 'Workflow',
69
74
  platform: 'Building2',
@@ -32,6 +32,7 @@ export const LUCIDE_ICON_PATHS = {
32
32
  BookOpen: 'M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2zM22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z',
33
33
  BarChart3: 'M3 3v18h18M7 16v-5M12 16v-9M17 16v-3',
34
34
  Activity: 'M22 12h-4l-3 9L9 3l-3 9H2',
35
+ Bell: 'M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9M13.73 21a2 2 0 0 1-3.46 0',
35
36
  Siren: 'M7 18a5 5 0 1 1 10 0M5 21h14M21 12h1M3 12H2M5.6 5.6l-.7-.7M18.4 5.6l.7-.7M11 4h2v3h-2z',
36
37
  Users:
37
38
  'M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2M9 11a4 4 0 1 0 0-8 4 4 0 0 0 0 8zM22 21v-2a4 4 0 0 0-3-3.87M16 3.13a4 4 0 0 1 0 7.75',
@@ -41,6 +42,9 @@ export const LUCIDE_ICON_PATHS = {
41
42
  'M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2zM12 15a3 3 0 1 0 0-6 3 3 0 0 0 0 6z',
42
43
  PanelLeftClose: 'M3 3h18v18H3zM9 3v18M16 15l-3-3 3-3',
43
44
  LogOut: 'M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4M16 17l5-5-5-5M21 12H9',
45
+ Monitor:
46
+ 'M20 3H4a2 2 0 0 0-2 2v11a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V5a2 2 0 0 0-2-2zM8 21h8M12 17v4',
47
+ Moon: 'M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9',
44
48
  Sun: 'M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M4.93 19.07l1.41-1.41M17.66 6.34l1.41-1.41M12 7a5 5 0 1 0 0 10 5 5 0 0 0 0-10z',
45
49
  ChevronDown: 'm6 9 6 6 6-6',
46
50
  ChevronRight: 'm9 6 6 6-6 6',
package/src/index.ts CHANGED
@@ -56,6 +56,8 @@ export {
56
56
  getCoreAppRouteLabel,
57
57
  mountCoreAppChrome,
58
58
  mountCoreAppChromeFromDom,
59
+ mountCoreAppLibraryDashboard,
60
+ mountCoreAppLibraryDashboardFromDom,
59
61
  PageHeader,
60
62
  createPageHeader,
61
63
  } from './layout/index'
@@ -21,6 +21,9 @@ describe('TopMenuBar', () => {
21
21
  expect(topbar.el.querySelector('.cmm-topbar__product')?.textContent).toBe('Threat Library')
22
22
  expect(topbar.el.querySelector('.cmm-topbar__current')?.textContent).toBe('Library Dashboard')
23
23
  expect(topbar.el.querySelector('[data-action-id="notifications"]')).toBeTruthy()
24
+ expect(
25
+ topbar.el.querySelector('[data-action-id="notifications"] svg path')?.getAttribute('d')
26
+ ).toContain('M18 8A6')
24
27
  expect(topbar.el.querySelector('.cmm-topbar__avatar')?.textContent).toBe('WR')
25
28
 
26
29
  const trigger = topbar.el.querySelector<HTMLButtonElement>('.cmm-topbar__user-trigger')
@@ -38,6 +41,79 @@ describe('TopMenuBar', () => {
38
41
  expect(document.body.contains(topbar.el)).toBe(false)
39
42
  })
40
43
 
44
+ it('renders lowercase theme mode icons for native topbar actions', () => {
45
+ const topbar = new TopMenuBar({
46
+ product: 'Threat Library',
47
+ current: 'Library Dashboard',
48
+ actions: [
49
+ { id: 'dark-mode', label: 'Dark mode', icon: 'moon' },
50
+ { id: 'system-mode', label: 'System mode', icon: 'monitor' },
51
+ { id: 'theme', label: 'Theme', icon: 'theme' },
52
+ ],
53
+ })
54
+
55
+ expect(
56
+ topbar.el.querySelector('[data-action-id="dark-mode"] svg path')?.getAttribute('d')
57
+ ).toContain('M12 3a6')
58
+ expect(
59
+ topbar.el.querySelector('[data-action-id="system-mode"] svg path')?.getAttribute('d')
60
+ ).toContain('M20 3H4')
61
+ expect(
62
+ topbar.el.querySelector('[data-action-id="theme"] svg path')?.getAttribute('d')
63
+ ).toContain('M12 2v2')
64
+
65
+ topbar.destroy()
66
+ })
67
+
68
+ it('updates the user menu without moving surrounding actions', () => {
69
+ const beforeAction = document.createElement('span')
70
+ const afterAction = document.createElement('span')
71
+ beforeAction.setAttribute('data-before-action', '')
72
+ afterAction.setAttribute('data-after-action', '')
73
+
74
+ const topbar = new TopMenuBar({
75
+ product: 'Threat Library',
76
+ beforeActions: beforeAction,
77
+ afterActions: afterAction,
78
+ actions: [{ id: 'notifications', label: 'Notifications', icon: 'bell' }],
79
+ user: {
80
+ name: 'CounterMeasure User',
81
+ initials: 'CU',
82
+ },
83
+ })
84
+
85
+ expect(topbar.el.querySelector('.cmm-topbar__avatar')?.textContent).toBe('CU')
86
+
87
+ topbar.updateUser({
88
+ name: 'Wyatt Roersma',
89
+ detail: 'wyattroersma@gmail.com',
90
+ initials: 'WR',
91
+ avatarUrl: '/api/v1/users/user-1/avatar?v=profile.png',
92
+ })
93
+
94
+ const actionNodes = Array.from(
95
+ topbar.el.querySelector('.cmm-topbar__actions')?.childNodes ?? []
96
+ )
97
+ const beforeIndex = actionNodes.indexOf(beforeAction)
98
+ const userIndex = actionNodes.indexOf(
99
+ topbar.el.querySelector('.cmm-topbar__user') as HTMLElement
100
+ )
101
+ const afterIndex = actionNodes.indexOf(afterAction)
102
+
103
+ expect(beforeIndex).toBeLessThan(userIndex)
104
+ expect(userIndex).toBeLessThan(afterIndex)
105
+ expect(topbar.el.querySelector('.cmm-topbar__avatar')?.textContent).toBe('')
106
+ expect(topbar.el.querySelector('.cmm-topbar__avatar img')?.getAttribute('src')).toBe(
107
+ '/api/v1/users/user-1/avatar?v=profile.png'
108
+ )
109
+ expect(topbar.el.querySelector('.cmm-topbar__user-name')?.textContent).toBe('Wyatt Roersma')
110
+ expect(topbar.el.querySelector('.cmm-topbar__user-detail')?.textContent).toBe(
111
+ 'wyattroersma@gmail.com'
112
+ )
113
+
114
+ topbar.destroy()
115
+ })
116
+
41
117
  it('supports native custom action nodes and data-hook attributes', () => {
42
118
  const notificationWrapper = document.createElement('div')
43
119
  notificationWrapper.setAttribute('data-notification-wrapper', '')
@@ -180,9 +180,15 @@ function createActionElement(action: TopMenuBarActionInput, iconSet: IconSet): H
180
180
  export class TopMenuBar {
181
181
  public readonly el: HTMLElement
182
182
  private readonly cleanupFns: (() => void)[] = []
183
+ private readonly iconSet: IconSet
184
+ private readonly actionsEl: HTMLElement
185
+ private readonly userAnchor: Comment
186
+ private userMenuEl: HTMLElement | null = null
187
+ private userCleanupFns: (() => void)[] = []
183
188
 
184
189
  constructor(config: TopMenuBarConfig = {}) {
185
190
  const iconSet = config.iconSet ?? 'lucide'
191
+ this.iconSet = iconSet
186
192
  this.el = document.createElement('header')
187
193
  this.el.className = ['cmm-topbar', config.className].filter(Boolean).join(' ')
188
194
 
@@ -231,6 +237,8 @@ export class TopMenuBar {
231
237
 
232
238
  const actions = document.createElement('div')
233
239
  actions.classList.add('cmm-topbar__actions')
240
+ this.actionsEl = actions
241
+ this.userAnchor = document.createComment('cmm-topbar-user')
234
242
 
235
243
  appendElements(actions, config.beforeActions)
236
244
 
@@ -238,8 +246,9 @@ export class TopMenuBar {
238
246
  actions.appendChild(createActionElement(action, iconSet))
239
247
  }
240
248
 
249
+ actions.appendChild(this.userAnchor)
241
250
  if (config.user !== undefined) {
242
- actions.appendChild(this.createUserMenu(config.user, iconSet))
251
+ this.updateUser(config.user)
243
252
  }
244
253
 
245
254
  appendElements(actions, config.afterActions)
@@ -253,13 +262,33 @@ export class TopMenuBar {
253
262
  }
254
263
 
255
264
  destroy(): void {
265
+ this.clearUserMenu()
256
266
  for (const cleanup of this.cleanupFns.splice(0)) {
257
267
  cleanup()
258
268
  }
259
269
  this.el.remove()
260
270
  }
261
271
 
262
- private createUserMenu(user: TopMenuBarUser, iconSet: IconSet): HTMLElement {
272
+ updateUser(user: TopMenuBarUser): void {
273
+ this.clearUserMenu()
274
+ const userMenu = this.createUserMenu(user, this.iconSet)
275
+ this.actionsEl.insertBefore(userMenu.element, this.userAnchor)
276
+ this.userMenuEl = userMenu.element
277
+ this.userCleanupFns = userMenu.cleanup
278
+ }
279
+
280
+ private clearUserMenu(): void {
281
+ for (const cleanup of this.userCleanupFns.splice(0)) {
282
+ cleanup()
283
+ }
284
+ this.userMenuEl?.remove()
285
+ this.userMenuEl = null
286
+ }
287
+
288
+ private createUserMenu(
289
+ user: TopMenuBarUser,
290
+ iconSet: IconSet
291
+ ): { element: HTMLElement; cleanup: (() => void)[] } {
263
292
  const root = document.createElement('div')
264
293
  root.classList.add('cmm-topbar__user')
265
294
 
@@ -338,14 +367,16 @@ export class TopMenuBar {
338
367
 
339
368
  document.addEventListener('pointerdown', closeOnOutsidePointer)
340
369
  document.addEventListener('keydown', closeOnEscape)
341
- this.cleanupFns.push(() => {
342
- document.removeEventListener('pointerdown', closeOnOutsidePointer)
343
- document.removeEventListener('keydown', closeOnEscape)
344
- })
370
+ const cleanup = [
371
+ (): void => {
372
+ document.removeEventListener('pointerdown', closeOnOutsidePointer)
373
+ document.removeEventListener('keydown', closeOnEscape)
374
+ },
375
+ ]
345
376
 
346
377
  root.appendChild(trigger)
347
378
  root.appendChild(menu)
348
- return root
379
+ return { element: root, cleanup }
349
380
  }
350
381
  }
351
382
 
@@ -43,7 +43,7 @@ describe('core app chrome preset', () => {
43
43
  })
44
44
 
45
45
  it('maps native app routes to topbar labels', () => {
46
- expect(getCoreAppRouteLabel('/app')).toBe('Platform Overview')
46
+ expect(getCoreAppRouteLabel('/app')).toBe('Library Dashboard')
47
47
  expect(getCoreAppRouteLabel('/app/dashboards/library')).toBe('Library Dashboard')
48
48
  expect(getCoreAppRouteLabel('/app/jobs')).toBe('Jobs')
49
49
  expect(getCoreAppRouteLabel('/app/api-access/tokens')).toBe('API Access')
@@ -60,8 +60,14 @@ describe('core app chrome preset', () => {
60
60
  },
61
61
  })
62
62
 
63
- expect(config.variant).toBe('cm')
64
- expect(config.brand).toMatchObject({ label: 'CounterMeasure', href: '/app' })
63
+ expect(config.variant).toBe('threat-library')
64
+ expect(config.brand).toMatchObject({
65
+ label: 'CounterMeasure',
66
+ subtitle: 'Platform',
67
+ wordmarkCase: 'title',
68
+ markSize: 24,
69
+ href: '/app',
70
+ })
65
71
  expect(config.scope?.options[0]?.label).toBe('All Tenants')
66
72
  expect(config.collapsible).toBe(true)
67
73
  expect(config.collapseButtonVisible).toBe(true)
@@ -75,7 +81,7 @@ describe('core app chrome preset', () => {
75
81
  const config = createCoreAppTopbarConfig({ pathname: '/app/jobs' })
76
82
 
77
83
  expect(actions).toHaveLength(3)
78
- expect(config.product).toBe('CounterMeasure Core')
84
+ expect(config.product).toBe('Threat Library')
79
85
  expect(config.current).toBe('Jobs')
80
86
  expect(config.actions?.[0]).toMatchObject({
81
87
  id: 'search',
@@ -204,7 +210,11 @@ describe('core app chrome preset', () => {
204
210
  expect(preset.topbar.current).toBe('Jobs')
205
211
  expect(preset.sidebar.pathname).toBe('/app/jobs')
206
212
  expect(aside?.classList.contains('sidebar--product')).toBe(true)
207
- expect(aside?.getAttribute('data-sidebar')).toBe('cm')
213
+ expect(aside?.getAttribute('data-sidebar')).toBe('threat-library')
214
+ expect(container.querySelector('.sidebar__header .cmm-brand-lockup--title')).toBeTruthy()
215
+ expect(
216
+ container.querySelector('.sidebar__header .cmm-brand-lockup__subtitle')?.textContent
217
+ ).toBe('Platform')
208
218
  expect(active?.classList.contains('sidebar__item--active')).toBe(true)
209
219
  expect(active?.getAttribute('aria-current')).toBe('page')
210
220
  expect(library?.classList.contains('sidebar__item--active')).toBe(false)
@@ -283,9 +293,11 @@ describe('core app chrome preset', () => {
283
293
  })
284
294
 
285
295
  const topbar = topbarContainer.querySelector<HTMLElement>('.cmm-topbar')
296
+ const actionGroup = topbarContainer.querySelector<HTMLElement>('.cmm-topbar__actions')
286
297
  expect(topbar?.querySelector('[data-app-search-trigger]')).toBeTruthy()
287
298
  expect(topbar?.querySelector('[data-before-actions]')).toBe(beforeActions)
288
299
  expect(topbar?.querySelector('[data-notification-toggle]')).toBe(action)
300
+ expect(Array.from(actionGroup?.children ?? []).slice(-2)).toEqual([beforeActions, action])
289
301
  expect(action.isConnected).toBe(true)
290
302
 
291
303
  mounted.destroy()
@@ -243,7 +243,7 @@ export function createCoreAppSidebarSections(basePath = '/app'): SidebarSection[
243
243
  export function getCoreAppRouteLabel(pathname: string, basePath = '/app'): string {
244
244
  const relativePathname = stripBasePath(pathname, basePath)
245
245
  if (relativePathname === '/' || relativePathname === '') {
246
- return 'Platform Overview'
246
+ return 'Library Dashboard'
247
247
  }
248
248
 
249
249
  return (
@@ -258,7 +258,7 @@ export function createCoreAppSidebarConfig(
258
258
  const {
259
259
  basePath = '/app',
260
260
  sections,
261
- variant = 'cm',
261
+ variant = 'threat-library',
262
262
  brand,
263
263
  scope,
264
264
  beforeNav,
@@ -284,6 +284,9 @@ export function createCoreAppSidebarConfig(
284
284
 
285
285
  const resolvedBrand: SidebarBrandConfig = brand ?? {
286
286
  label: 'CounterMeasure',
287
+ subtitle: 'Platform',
288
+ wordmarkCase: 'title',
289
+ markSize: 24,
287
290
  href: joinBasePath(basePath, '/'),
288
291
  }
289
292
 
@@ -369,7 +372,7 @@ export function createCoreAppTopbarConfig(
369
372
  const {
370
373
  basePath = '/app',
371
374
  pathname = typeof window === 'undefined' ? '/' : window.location.pathname,
372
- product = 'CounterMeasure Core',
375
+ product = 'Threat Library',
373
376
  current = getCoreAppRouteLabel(pathname, basePath),
374
377
  actions,
375
378
  beforeActions,
@@ -545,6 +548,8 @@ function buildTopbarMountOptions(
545
548
  if (options.pathname !== undefined) topbarOptions.pathname = options.pathname
546
549
  if (options.topbarUser !== undefined) topbarOptions.user = options.topbarUser
547
550
  if (options.topbarActions !== undefined) topbarOptions.actions = options.topbarActions
551
+ if (options.topbar?.beforeActions !== undefined)
552
+ topbarOptions.beforeActions = options.topbar.beforeActions
548
553
  if (afterActions !== undefined) topbarOptions.afterActions = afterActions
549
554
  dedupeDefaultActionsForPreservedNodes(topbarOptions, preservedActions)
550
555