@countermeasure-platform/web-components 1.3.5-dev.36.1 → 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
@@ -0,0 +1,397 @@
1
+ import { beforeEach, describe, expect, it } from 'vitest'
2
+
3
+ import {
4
+ mountCoreAppLibraryDashboard,
5
+ mountCoreAppLibraryDashboardFromDom,
6
+ } from './core-app-library-dashboard'
7
+
8
+ describe('core app library dashboard', () => {
9
+ beforeEach(() => {
10
+ document.body.innerHTML = ''
11
+ })
12
+
13
+ it('mounts an empty real-data Threat Library dashboard surface by default', () => {
14
+ const container = document.createElement('div')
15
+ document.body.appendChild(container)
16
+
17
+ const mounted = mountCoreAppLibraryDashboard({ container })
18
+
19
+ expect(container.querySelector('.cmm-library-dashboard')).toBe(mounted.element)
20
+ expect(container.querySelector('.cmm-library-dashboard__title')?.textContent).toBe(
21
+ 'Library Dashboard'
22
+ )
23
+ expect(container.querySelector('.cmm-library-chart--depth')?.textContent).toContain(
24
+ 'Detection Depth by Tactic'
25
+ )
26
+ expect(container.querySelector('.cmm-library-chart--freshness')?.textContent).toContain(
27
+ 'Content Freshness (30d)'
28
+ )
29
+ expect(container.querySelectorAll('.cmm-library-metric')).toHaveLength(0)
30
+ expect(container.querySelectorAll('.cmm-library-pipeline-card')).toHaveLength(0)
31
+ expect(container.querySelector('.cmm-library-dashboard__empty')?.textContent).toContain(
32
+ 'No metrics available'
33
+ )
34
+
35
+ mounted.destroy()
36
+ expect(container.querySelector('.cmm-library-dashboard')).toBeNull()
37
+ })
38
+
39
+ it('updates with caller-provided native data', () => {
40
+ const container = document.createElement('div')
41
+ document.body.appendChild(container)
42
+
43
+ const mounted = mountCoreAppLibraryDashboard({
44
+ container,
45
+ data: {
46
+ title: 'Native Library',
47
+ metrics: [
48
+ {
49
+ id: 'detections',
50
+ label: 'Detections',
51
+ value: 42,
52
+ helper: 'Native source',
53
+ tone: 'cyan',
54
+ },
55
+ ],
56
+ connectors: [],
57
+ pipelineSummary: { connectors: 0, active: 0, error: 0 },
58
+ },
59
+ })
60
+
61
+ expect(container.querySelector('.cmm-library-dashboard__title')?.textContent).toBe(
62
+ 'Native Library'
63
+ )
64
+ expect(container.querySelector('.cmm-library-metric__value')?.textContent).toBe('42')
65
+ expect(container.querySelectorAll('.cmm-library-pipeline-card')).toHaveLength(0)
66
+
67
+ mounted.update({
68
+ title: 'Updated Library',
69
+ metrics: [{ id: 'actors', label: 'Actors', value: '9', tone: 'orange' }],
70
+ })
71
+
72
+ expect(container.querySelector('.cmm-library-dashboard__title')?.textContent).toBe(
73
+ 'Updated Library'
74
+ )
75
+ expect(container.querySelector('.cmm-library-metric__label')?.textContent).toBe('Actors')
76
+ })
77
+
78
+ it('renders real API charts, connector statuses, and partial dashboard metadata', () => {
79
+ const container = document.createElement('div')
80
+ container.innerHTML = '<p class="existing-node">Keep me</p>'
81
+ document.body.appendChild(container)
82
+
83
+ mountCoreAppLibraryDashboard({
84
+ container,
85
+ className: 'extra-shell release-candidate',
86
+ clearContainer: false,
87
+ data: {
88
+ title: 'Real Library',
89
+ subtitle: 'Live dashboard data',
90
+ generatedAt: '2026-06-20T15:00:00Z',
91
+ errors: [
92
+ { source: 'cti', detail: 'Threat actor feed unavailable' },
93
+ { detail: 'Rules freshness partially unavailable' },
94
+ ],
95
+ metrics: [
96
+ {
97
+ id: 'detections',
98
+ label: 'Total Detections',
99
+ value: 12109,
100
+ helper: 'Across all sources',
101
+ tone: 'cyan',
102
+ },
103
+ { id: 'actors', label: 'Threat Actors', value: '990', tone: 'orange' },
104
+ ],
105
+ tacticDepth: [
106
+ { label: 'Execution', value: 450 },
107
+ { label: 'Impact', value: 0 },
108
+ ],
109
+ freshness: [
110
+ { label: 'May 21', detections: 5, rules: 2, intel: 1 },
111
+ { label: '', detections: 0 },
112
+ ],
113
+ pipelineSummary: { connectors: 4, active: 1, error: 1 },
114
+ connectors: [
115
+ {
116
+ id: 'active',
117
+ name: 'Chronicle YARA-L',
118
+ status: 'active',
119
+ schedule: 'Daily 2:30',
120
+ tags: ['YARA-L Rules'],
121
+ lastRun: 'Last run 2026-06-20 15:00',
122
+ },
123
+ {
124
+ id: 'error',
125
+ name: 'Sigma Community',
126
+ status: 'error',
127
+ schedule: 'Daily 2:30',
128
+ tags: ['Sigma Rules'],
129
+ lastRun: 'Last run failed',
130
+ },
131
+ {
132
+ id: 'paused',
133
+ name: 'MISP Galaxy',
134
+ status: 'paused',
135
+ schedule: 'Daily 0:00',
136
+ tags: [],
137
+ lastRun: 'Last run 2026-06-19 10:00',
138
+ },
139
+ {
140
+ id: 'disabled',
141
+ name: 'MITRE ATT&CK',
142
+ status: 'disabled',
143
+ schedule: 'Manual',
144
+ tags: ['ATT&CK'],
145
+ lastRun: 'Never run',
146
+ enabled: false,
147
+ },
148
+ ],
149
+ },
150
+ })
151
+
152
+ expect(container.querySelector('.existing-node')).not.toBeNull()
153
+ expect(
154
+ container.querySelector('.cmm-library-dashboard')?.classList.contains('extra-shell')
155
+ ).toBe(true)
156
+ expect(
157
+ container.querySelector('.cmm-library-dashboard')?.classList.contains('release-candidate')
158
+ ).toBe(true)
159
+ expect(container.querySelector('.cmm-library-dashboard__subtitle')?.textContent).toBe(
160
+ 'Live dashboard data'
161
+ )
162
+ expect(container.querySelector('.cmm-library-dashboard__timestamp')?.textContent).toBe(
163
+ 'Updated 2026-06-20T15:00:00Z'
164
+ )
165
+ expect(
166
+ Array.from(container.querySelectorAll('.cmm-library-dashboard__warning')).map(
167
+ node => node.textContent
168
+ )
169
+ ).toEqual(['cti: Threat actor feed unavailable', 'Rules freshness partially unavailable'])
170
+ expect(container.querySelector('[data-metric-id="detections"]')?.textContent).toContain(
171
+ '12,109'
172
+ )
173
+ expect(container.querySelector('.cmm-library-depth__bar')?.getAttribute('aria-label')).toBe(
174
+ 'Execution: 450'
175
+ )
176
+ expect(container.querySelectorAll('.cmm-library-freshness__bucket')).toHaveLength(2)
177
+ expect(container.querySelectorAll('.cmm-library-freshness__label')).toHaveLength(1)
178
+ expect(
179
+ Array.from(container.querySelectorAll('.cmm-library-pipeline-card')).map(card => [
180
+ (card as HTMLElement).dataset.status,
181
+ card.querySelector('.cmm-library-pipeline-card__badge')?.getAttribute('data-tone'),
182
+ ])
183
+ ).toEqual([
184
+ ['active', 'success'],
185
+ ['error', 'error'],
186
+ ['paused', 'warn'],
187
+ ['disabled', 'neutral'],
188
+ ])
189
+ expect(container.querySelector('.cmm-library-dashboard__pipeline-summary')?.textContent).toBe(
190
+ '4 connectors1 active1 error'
191
+ )
192
+ expect(
193
+ container.querySelector('.cmm-library-pipeline-card[data-status="disabled"]')?.textContent
194
+ ).toContain('Disabled')
195
+ })
196
+
197
+ it('renders chart scales with unique integer ticks for small real-data ranges', () => {
198
+ const container = document.createElement('div')
199
+ document.body.appendChild(container)
200
+
201
+ mountCoreAppLibraryDashboard({
202
+ container,
203
+ data: {
204
+ tacticDepth: [
205
+ { label: 'Execution', value: 2 },
206
+ { label: 'Impact', value: 1 },
207
+ { label: 'Resource', value: 0 },
208
+ ],
209
+ freshness: [
210
+ { label: 'Day 1', detections: 2, rules: 1, intel: 1 },
211
+ { label: 'Day 2', detections: 1, rules: 1 },
212
+ { label: 'Day 3', detections: 0 },
213
+ ],
214
+ },
215
+ })
216
+
217
+ const depthAxis = Array.from(container.querySelectorAll('.cmm-library-depth__axis span')).map(
218
+ node => node.textContent
219
+ )
220
+ expect(depthAxis).toEqual(['0', '1', '2'])
221
+ expect(new Set(depthAxis).size).toBe(depthAxis.length)
222
+ expect(
223
+ Array.from(container.querySelectorAll<HTMLElement>('.cmm-library-depth__bar')).map(
224
+ bar => bar.style.width
225
+ )
226
+ ).toEqual(['100%', '50%', '4%'])
227
+
228
+ const freshnessAxis = Array.from(
229
+ container.querySelectorAll('.cmm-library-freshness__axis span')
230
+ ).map(node => node.textContent)
231
+ expect(freshnessAxis).toEqual(['4', '3', '2', '1', '0'])
232
+ expect(new Set(freshnessAxis).size).toBe(freshnessAxis.length)
233
+ expect(
234
+ Array.from(container.querySelectorAll<HTMLElement>('.cmm-library-freshness__stack')).map(
235
+ stack => stack.style.height
236
+ )
237
+ ).toEqual(['100%', '50%', '8%'])
238
+ expect(
239
+ container
240
+ .querySelector<HTMLElement>('.cmm-library-freshness__stack')
241
+ ?.getAttribute('aria-label')
242
+ ).toBe('Day 1: 4')
243
+ })
244
+
245
+ it('uses unique stable title ids for multiple dashboard instances', () => {
246
+ const firstContainer = document.createElement('div')
247
+ const secondContainer = document.createElement('div')
248
+ document.body.append(firstContainer, secondContainer)
249
+
250
+ const first = mountCoreAppLibraryDashboard({
251
+ container: firstContainer,
252
+ data: { title: 'First Library' },
253
+ })
254
+ const second = mountCoreAppLibraryDashboard({
255
+ container: secondContainer,
256
+ data: { title: 'Second Library' },
257
+ })
258
+
259
+ const firstTitle = first.element.querySelector<HTMLElement>('.cmm-library-dashboard__title')
260
+ const secondTitle = second.element.querySelector<HTMLElement>('.cmm-library-dashboard__title')
261
+
262
+ expect(first.element.getAttribute('aria-labelledby')).toBe(firstTitle?.id)
263
+ expect(second.element.getAttribute('aria-labelledby')).toBe(secondTitle?.id)
264
+ expect(firstTitle?.id).not.toBe(secondTitle?.id)
265
+
266
+ const originalFirstTitleId = firstTitle?.id
267
+ first.update({ title: 'Updated First Library' })
268
+
269
+ expect(first.element.getAttribute('aria-labelledby')).toBe(originalFirstTitleId)
270
+ expect(first.element.querySelector<HTMLElement>('.cmm-library-dashboard__title')?.id).toBe(
271
+ originalFirstTitleId
272
+ )
273
+ })
274
+
275
+ it('renders collection pipeline cards with status, schedule, tags, and enabled state', () => {
276
+ const container = document.createElement('div')
277
+ document.body.appendChild(container)
278
+
279
+ mountCoreAppLibraryDashboard({
280
+ container,
281
+ data: {
282
+ pipelineSummary: { connectors: 1, active: 1, error: 0 },
283
+ connectors: [
284
+ {
285
+ id: 'yara',
286
+ name: 'YARA Community',
287
+ status: 'active',
288
+ schedule: 'Daily 2:30',
289
+ tags: ['YARA Rules', 'Community'],
290
+ lastRun: 'Last run 2026-06-20 12:00',
291
+ },
292
+ ],
293
+ },
294
+ })
295
+
296
+ const card = container.querySelector<HTMLElement>('.cmm-library-pipeline-card')
297
+ expect(container.querySelector('.cmm-library-dashboard__pipeline-summary')?.textContent).toBe(
298
+ '1 connectors1 active0 error'
299
+ )
300
+ expect(card?.dataset.status).toBe('active')
301
+ expect(card?.querySelector('.cmm-library-pipeline-card__badge')?.textContent).toBe('Active')
302
+ expect(card?.textContent).toContain('Daily 2:30')
303
+ expect(card?.textContent).toContain('YARA Rules')
304
+ expect(card?.textContent).toContain('Community')
305
+ expect(card?.textContent).toContain('Enabled')
306
+ })
307
+
308
+ it('renders loading and error states without demo data', () => {
309
+ const container = document.createElement('div')
310
+ document.body.appendChild(container)
311
+
312
+ const mounted = mountCoreAppLibraryDashboard({
313
+ container,
314
+ data: { loading: true },
315
+ })
316
+
317
+ expect(container.querySelector('.cmm-library-dashboard__state')?.textContent).toContain(
318
+ 'Loading library dashboard'
319
+ )
320
+ expect(container.querySelectorAll('.cmm-library-metric')).toHaveLength(0)
321
+
322
+ mounted.update({ error: 'Dashboard API returned 500' })
323
+
324
+ expect(container.querySelector('.cmm-library-dashboard__state')?.textContent).toContain(
325
+ 'Unable to load dashboard'
326
+ )
327
+ expect(container.querySelector('.cmm-library-dashboard__state')?.textContent).toContain(
328
+ 'Dashboard API returned 500'
329
+ )
330
+ })
331
+
332
+ it('mounts declaratively from DOM placeholders and JSON data islands', () => {
333
+ const root = document.createElement('section')
334
+ root.innerHTML = `
335
+ <script type="application/json" data-core-app-library-dashboard-data>
336
+ {"title":"Declarative Library","metrics":[{"id":"total","label":"Total","value":"11"}]}
337
+ </script>
338
+ <div data-core-app-library-dashboard></div>
339
+ `
340
+ document.body.appendChild(root)
341
+
342
+ const mounted = mountCoreAppLibraryDashboardFromDom({ root })
343
+
344
+ expect(mounted).not.toBeNull()
345
+ expect(root.querySelector('.cmm-library-dashboard__title')?.textContent).toBe(
346
+ 'Declarative Library'
347
+ )
348
+ expect(root.querySelector('.cmm-library-metric__value')?.textContent).toBe('11')
349
+
350
+ mounted?.destroy()
351
+ })
352
+
353
+ it('falls back to empty real-data rendering when declarative JSON is invalid', () => {
354
+ const root = document.createElement('section')
355
+ root.innerHTML = `
356
+ <script type="application/json" data-core-app-library-dashboard-data>
357
+ {
358
+ </script>
359
+ <div data-core-app-library-dashboard></div>
360
+ `
361
+ document.body.appendChild(root)
362
+
363
+ const mounted = mountCoreAppLibraryDashboardFromDom({ root })
364
+
365
+ expect(mounted).not.toBeNull()
366
+ expect(root.querySelector('.cmm-library-dashboard__title')?.textContent).toBe(
367
+ 'Library Dashboard'
368
+ )
369
+ expect(root.querySelectorAll('.cmm-library-metric')).toHaveLength(0)
370
+
371
+ mounted?.destroy()
372
+ })
373
+
374
+ it('ignores non-object declarative JSON dashboard data', () => {
375
+ const root = document.createElement('section')
376
+ root.innerHTML = `
377
+ <script type="application/json" data-core-app-library-dashboard-data>
378
+ null
379
+ </script>
380
+ <div data-core-app-library-dashboard></div>
381
+ `
382
+ document.body.appendChild(root)
383
+
384
+ const mounted = mountCoreAppLibraryDashboardFromDom({ root, clearContainer: false })
385
+
386
+ expect(mounted).not.toBeNull()
387
+ expect(root.querySelector('.cmm-library-dashboard__title')?.textContent).toBe(
388
+ 'Library Dashboard'
389
+ )
390
+
391
+ mounted?.destroy()
392
+ })
393
+
394
+ it('returns null when no declarative dashboard placeholder exists', () => {
395
+ expect(mountCoreAppLibraryDashboardFromDom({ root: document.createElement('div') })).toBeNull()
396
+ })
397
+ })