@cwygoda/service-catalog-ui 1.0.1 → 1.0.3
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.
Potentially problematic release.
This version of @cwygoda/service-catalog-ui might be problematic. Click here for more details.
- package/dist/components/BpmnDiagram.svelte +24 -13
- package/dist/components/Breadcrumbs.svelte +3 -3
- package/dist/components/DomainCard.svelte +1 -0
- package/dist/components/Header.svelte +3 -1
- package/dist/components/NavModeToggle.svelte +1 -0
- package/dist/components/NavTree.svelte +52 -18
- package/dist/components/ServiceCard.svelte +1 -0
- package/dist/components/ServiceGraph.svelte +149 -75
- package/dist/components/ThemeToggle.svelte +1 -0
- package/dist/components/UseCaseCard.svelte +3 -2
- package/dist/source.css +1 -0
- package/package.json +13 -4
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
const FIT_PADDING = 40; // pixels of padding around diagram when fitting
|
|
25
|
+
const FULLSCREEN_TRANSITION_MS = 100;
|
|
25
26
|
|
|
26
27
|
interface BpmnViewer {
|
|
27
28
|
importXML: (xml: string) => Promise<unknown>;
|
|
@@ -33,6 +34,7 @@
|
|
|
33
34
|
|
|
34
35
|
let wrapper: HTMLDivElement;
|
|
35
36
|
let container: HTMLDivElement;
|
|
37
|
+
let resizeObserver: ResizeObserver | null = null;
|
|
36
38
|
let viewer: BpmnViewer | null = $state(null);
|
|
37
39
|
let error: string | null = $state(null);
|
|
38
40
|
let ready = $state(false);
|
|
@@ -53,7 +55,7 @@
|
|
|
53
55
|
isFullscreen = !!document.fullscreenElement;
|
|
54
56
|
// Re-fit diagram when entering/exiting fullscreen
|
|
55
57
|
if (ready) {
|
|
56
|
-
setTimeout(fitWithPadding,
|
|
58
|
+
setTimeout(fitWithPadding, FULLSCREEN_TRANSITION_MS);
|
|
57
59
|
}
|
|
58
60
|
}
|
|
59
61
|
|
|
@@ -134,6 +136,12 @@
|
|
|
134
136
|
// Fit diagram to container with padding
|
|
135
137
|
fitWithPadding();
|
|
136
138
|
ready = true;
|
|
139
|
+
|
|
140
|
+
// Re-fit on resize
|
|
141
|
+
resizeObserver = new ResizeObserver(() => {
|
|
142
|
+
if (ready) fitWithPadding();
|
|
143
|
+
});
|
|
144
|
+
resizeObserver.observe(container);
|
|
137
145
|
} catch (e) {
|
|
138
146
|
error = e instanceof Error ? e.message : 'Failed to render BPMN diagram';
|
|
139
147
|
console.error('BPMN render error:', e);
|
|
@@ -144,6 +152,7 @@
|
|
|
144
152
|
if (browser) {
|
|
145
153
|
document.removeEventListener('fullscreenchange', handleFullscreenChange);
|
|
146
154
|
}
|
|
155
|
+
resizeObserver?.disconnect();
|
|
147
156
|
if (viewer?.destroy) {
|
|
148
157
|
viewer.destroy();
|
|
149
158
|
}
|
|
@@ -178,10 +187,12 @@
|
|
|
178
187
|
>
|
|
179
188
|
<div
|
|
180
189
|
bind:this={container}
|
|
181
|
-
class="bpmn-container w-full rounded-lg border border-gray-200 bg-white
|
|
190
|
+
class="bpmn-container w-full rounded-lg border border-gray-200 bg-white {isFullscreen
|
|
191
|
+
? 'h-screen'
|
|
192
|
+
: 'h-64 sm:h-80 md:h-96'} dark:border-gray-700 dark:bg-gray-800"
|
|
182
193
|
class:cursor-grab={interactive}
|
|
183
|
-
|
|
184
|
-
|
|
194
|
+
role="img"
|
|
195
|
+
aria-label="BPMN process diagram"
|
|
185
196
|
></div>
|
|
186
197
|
|
|
187
198
|
<!-- Zoom controls -->
|
|
@@ -191,12 +202,12 @@
|
|
|
191
202
|
>
|
|
192
203
|
<button
|
|
193
204
|
onclick={zoomIn}
|
|
194
|
-
class="rounded p-1.5 text-gray-600 transition-colors hover:bg-gray-100 hover:text-gray-900 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-white"
|
|
205
|
+
class="flex min-h-11 min-w-11 items-center justify-center rounded p-1.5 text-gray-600 transition-colors hover:bg-gray-100 hover:text-gray-900 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-white"
|
|
195
206
|
aria-label="Zoom in"
|
|
196
207
|
title="Zoom in"
|
|
197
208
|
>
|
|
198
209
|
<svg
|
|
199
|
-
class="h-
|
|
210
|
+
class="h-5 w-5"
|
|
200
211
|
fill="none"
|
|
201
212
|
viewBox="0 0 24 24"
|
|
202
213
|
stroke="currentColor"
|
|
@@ -207,12 +218,12 @@
|
|
|
207
218
|
</button>
|
|
208
219
|
<button
|
|
209
220
|
onclick={zoomOut}
|
|
210
|
-
class="rounded p-1.5 text-gray-600 transition-colors hover:bg-gray-100 hover:text-gray-900 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-white"
|
|
221
|
+
class="flex min-h-11 min-w-11 items-center justify-center rounded p-1.5 text-gray-600 transition-colors hover:bg-gray-100 hover:text-gray-900 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-white"
|
|
211
222
|
aria-label="Zoom out"
|
|
212
223
|
title="Zoom out"
|
|
213
224
|
>
|
|
214
225
|
<svg
|
|
215
|
-
class="h-
|
|
226
|
+
class="h-5 w-5"
|
|
216
227
|
fill="none"
|
|
217
228
|
viewBox="0 0 24 24"
|
|
218
229
|
stroke="currentColor"
|
|
@@ -224,13 +235,13 @@
|
|
|
224
235
|
<div class="my-0.5 border-t border-gray-200 dark:border-gray-600"></div>
|
|
225
236
|
<button
|
|
226
237
|
onclick={resetZoom}
|
|
227
|
-
class="rounded p-1.5 text-gray-600 transition-colors hover:bg-gray-100 hover:text-gray-900 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-white"
|
|
238
|
+
class="flex min-h-11 min-w-11 items-center justify-center rounded p-1.5 text-gray-600 transition-colors hover:bg-gray-100 hover:text-gray-900 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-white"
|
|
228
239
|
aria-label="Fit to view"
|
|
229
240
|
title="Fit to view"
|
|
230
241
|
>
|
|
231
242
|
<!-- Viewfinder/target icon for fit-to-view -->
|
|
232
243
|
<svg
|
|
233
|
-
class="h-
|
|
244
|
+
class="h-5 w-5"
|
|
234
245
|
fill="none"
|
|
235
246
|
viewBox="0 0 24 24"
|
|
236
247
|
stroke="currentColor"
|
|
@@ -246,13 +257,13 @@
|
|
|
246
257
|
<div class="my-0.5 border-t border-gray-200 dark:border-gray-600"></div>
|
|
247
258
|
<button
|
|
248
259
|
onclick={toggleFullscreen}
|
|
249
|
-
class="rounded p-1.5 text-gray-600 transition-colors hover:bg-gray-100 hover:text-gray-900 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-white"
|
|
260
|
+
class="flex min-h-11 min-w-11 items-center justify-center rounded p-1.5 text-gray-600 transition-colors hover:bg-gray-100 hover:text-gray-900 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-white"
|
|
250
261
|
aria-label={isFullscreen ? 'Exit fullscreen' : 'Enter fullscreen'}
|
|
251
262
|
title={isFullscreen ? 'Exit fullscreen' : 'Fullscreen'}
|
|
252
263
|
>
|
|
253
264
|
{#if isFullscreen}
|
|
254
265
|
<svg
|
|
255
|
-
class="h-
|
|
266
|
+
class="h-5 w-5"
|
|
256
267
|
fill="none"
|
|
257
268
|
viewBox="0 0 24 24"
|
|
258
269
|
stroke="currentColor"
|
|
@@ -266,7 +277,7 @@
|
|
|
266
277
|
</svg>
|
|
267
278
|
{:else}
|
|
268
279
|
<svg
|
|
269
|
-
class="h-
|
|
280
|
+
class="h-5 w-5"
|
|
270
281
|
fill="none"
|
|
271
282
|
viewBox="0 0 24 24"
|
|
272
283
|
stroke="currentColor"
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
</script>
|
|
14
14
|
|
|
15
15
|
<nav aria-label="Breadcrumb" class="mb-6">
|
|
16
|
-
<ol class="flex flex-wrap items-center gap-1 text-sm">
|
|
16
|
+
<ol class="flex flex-wrap items-center gap-1 text-xs sm:text-sm">
|
|
17
17
|
{#each items as item (item.href)}
|
|
18
18
|
<li class="flex items-center">
|
|
19
19
|
<a
|
|
@@ -22,10 +22,10 @@
|
|
|
22
22
|
>
|
|
23
23
|
{item.label}
|
|
24
24
|
</a>
|
|
25
|
-
<span class="mx-2 text-gray-400" aria-hidden="true">/</span>
|
|
25
|
+
<span class="mx-1 sm:mx-2 text-gray-400" aria-hidden="true">/</span>
|
|
26
26
|
</li>
|
|
27
27
|
{/each}
|
|
28
|
-
<li class="text-gray-
|
|
28
|
+
<li class="text-gray-700 dark:text-gray-300" aria-current="page">
|
|
29
29
|
{current}
|
|
30
30
|
</li>
|
|
31
31
|
</ol>
|
|
@@ -39,7 +39,9 @@
|
|
|
39
39
|
{#each navLinks as link (link.href)}
|
|
40
40
|
<a
|
|
41
41
|
href={link.href}
|
|
42
|
-
class="rounded-md px-3 py-2 text-sm font-medium transition-colors {isActive(
|
|
42
|
+
class="rounded-md px-3 py-2 text-sm font-medium transition-colors focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-600 {isActive(
|
|
43
|
+
link.href
|
|
44
|
+
)
|
|
43
45
|
? 'bg-primary-100 text-primary-700 dark:bg-primary-900 dark:text-primary-200'
|
|
44
46
|
: 'text-gray-600 hover:bg-gray-100 hover:text-gray-900 dark:text-gray-300 dark:hover:bg-gray-800 dark:hover:text-white'}"
|
|
45
47
|
>
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
aria-label={navModeStore.mode === 'flat' ? 'Switch to tree view' : 'Switch to flat view'}
|
|
11
11
|
title={navModeStore.mode === 'flat' ? 'Switch to tree view' : 'Switch to flat view'}
|
|
12
12
|
>
|
|
13
|
+
<span class="sr-only" aria-live="polite">Navigation: {navModeStore.mode} view</span>
|
|
13
14
|
{#if navModeStore.mode === 'flat'}
|
|
14
15
|
<!-- List icon -->
|
|
15
16
|
<svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import { SvelteSet } from 'svelte/reactivity';
|
|
2
|
+
import { SvelteMap, SvelteSet } from 'svelte/reactivity';
|
|
3
3
|
import type { Domain, UseCase, Service } from '@cwygoda/service-catalog-core/domain';
|
|
4
4
|
|
|
5
5
|
interface Props {
|
|
@@ -30,28 +30,62 @@
|
|
|
30
30
|
}
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
-
//
|
|
33
|
+
// Pre-computed lookup maps — O(1) per query instead of O(n) filter scans
|
|
34
34
|
const rootDomains = $derived(domains.filter((d) => !d.parent));
|
|
35
35
|
|
|
36
|
-
|
|
36
|
+
const childDomainMap = $derived.by(() => {
|
|
37
|
+
const map = new SvelteMap<string, Domain[]>();
|
|
38
|
+
for (const d of domains) {
|
|
39
|
+
if (d.parent) {
|
|
40
|
+
const arr = map.get(d.parent);
|
|
41
|
+
if (arr) arr.push(d);
|
|
42
|
+
else map.set(d.parent, [d]);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return map;
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
const domainUseCaseMap = $derived.by(() => {
|
|
49
|
+
const map = new SvelteMap<string, UseCase[]>();
|
|
50
|
+
for (const uc of useCases) {
|
|
51
|
+
if (!uc.domain) continue;
|
|
52
|
+
const arr = map.get(uc.domain);
|
|
53
|
+
if (arr) arr.push(uc);
|
|
54
|
+
else map.set(uc.domain, [uc]);
|
|
55
|
+
}
|
|
56
|
+
return map;
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
const domainServiceMap = $derived.by(() => {
|
|
60
|
+
const map = new SvelteMap<string, Service[]>();
|
|
61
|
+
for (const s of services) {
|
|
62
|
+
if (!s.domain) continue;
|
|
63
|
+
const arr = map.get(s.domain);
|
|
64
|
+
if (arr) arr.push(s);
|
|
65
|
+
else map.set(s.domain, [s]);
|
|
66
|
+
}
|
|
67
|
+
return map;
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
const serviceById = $derived.by(() => new SvelteMap(services.map((s) => [s.id, s] as const)));
|
|
71
|
+
|
|
72
|
+
// Thin wrappers — keep template call sites unchanged
|
|
37
73
|
function getChildDomains(parentId: string): Domain[] {
|
|
38
|
-
return
|
|
74
|
+
return childDomainMap.get(parentId) ?? [];
|
|
39
75
|
}
|
|
40
76
|
|
|
41
|
-
// Helper to get use cases for a domain
|
|
42
77
|
function getDomainUseCases(domainId: string): UseCase[] {
|
|
43
|
-
return
|
|
78
|
+
return domainUseCaseMap.get(domainId) ?? [];
|
|
44
79
|
}
|
|
45
80
|
|
|
46
|
-
// Helper to get services for a domain
|
|
47
81
|
function getDomainServices(domainId: string): Service[] {
|
|
48
|
-
return
|
|
82
|
+
return domainServiceMap.get(domainId) ?? [];
|
|
49
83
|
}
|
|
50
84
|
|
|
51
|
-
// Helper to get services for a use case
|
|
52
85
|
function getUseCaseServices(useCase: UseCase): Service[] {
|
|
53
|
-
|
|
54
|
-
|
|
86
|
+
return useCase.participants
|
|
87
|
+
.map((p) => serviceById.get(p.service))
|
|
88
|
+
.filter((s): s is Service => s !== undefined);
|
|
55
89
|
}
|
|
56
90
|
</script>
|
|
57
91
|
|
|
@@ -65,12 +99,12 @@
|
|
|
65
99
|
onclick={() => {
|
|
66
100
|
toggleDomain(domain.id);
|
|
67
101
|
}}
|
|
68
|
-
class="flex h-
|
|
102
|
+
class="flex min-h-11 min-w-11 items-center justify-center rounded text-gray-500 hover:bg-gray-100 focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-primary-600 dark:text-gray-400 dark:hover:bg-gray-700"
|
|
69
103
|
aria-expanded={expandedDomains.has(domain.id)}
|
|
70
104
|
aria-label={expandedDomains.has(domain.id) ? 'Collapse' : 'Expand'}
|
|
71
105
|
>
|
|
72
106
|
<svg
|
|
73
|
-
class="h-
|
|
107
|
+
class="h-4 w-4 transition-transform {expandedDomains.has(domain.id)
|
|
74
108
|
? 'rotate-90'
|
|
75
109
|
: ''}"
|
|
76
110
|
fill="currentColor"
|
|
@@ -115,12 +149,12 @@
|
|
|
115
149
|
onclick={() => {
|
|
116
150
|
toggleUseCase(useCase.id);
|
|
117
151
|
}}
|
|
118
|
-
class="flex h-
|
|
152
|
+
class="flex min-h-11 min-w-11 items-center justify-center rounded text-gray-400 hover:bg-gray-100 focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-primary-600 dark:hover:bg-gray-700"
|
|
119
153
|
aria-expanded={expandedUseCases.has(useCase.id)}
|
|
120
154
|
aria-label={expandedUseCases.has(useCase.id) ? 'Collapse' : 'Expand'}
|
|
121
155
|
>
|
|
122
156
|
<svg
|
|
123
|
-
class="h-
|
|
157
|
+
class="h-4 w-4 transition-transform {expandedUseCases.has(useCase.id)
|
|
124
158
|
? 'rotate-90'
|
|
125
159
|
: ''}"
|
|
126
160
|
fill="currentColor"
|
|
@@ -134,7 +168,7 @@
|
|
|
134
168
|
</svg>
|
|
135
169
|
</button>
|
|
136
170
|
{:else}
|
|
137
|
-
<span class="w-
|
|
171
|
+
<span class="min-w-11"></span>
|
|
138
172
|
{/if}
|
|
139
173
|
<a
|
|
140
174
|
href="/use-cases/{useCase.id}"
|
|
@@ -152,7 +186,7 @@
|
|
|
152
186
|
<li>
|
|
153
187
|
<a
|
|
154
188
|
href="/services/{service.id}"
|
|
155
|
-
class="block rounded px-2 py-1 text-gray-
|
|
189
|
+
class="block rounded px-2 py-1 text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-700"
|
|
156
190
|
>
|
|
157
191
|
{service.name}
|
|
158
192
|
</a>
|
|
@@ -168,7 +202,7 @@
|
|
|
168
202
|
<li>
|
|
169
203
|
<a
|
|
170
204
|
href="/services/{service.id}"
|
|
171
|
-
class="ml-5 block rounded px-2 py-1 text-gray-
|
|
205
|
+
class="ml-5 block rounded px-2 py-1 text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-700"
|
|
172
206
|
>
|
|
173
207
|
{service.name}
|
|
174
208
|
</a>
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
<a
|
|
8
8
|
href="/services/{service.id}"
|
|
9
|
+
aria-label="View {service.name} service"
|
|
9
10
|
class="block rounded-lg border border-gray-200 bg-white p-6 shadow-sm transition-shadow hover:shadow-md dark:border-gray-700 dark:bg-gray-800"
|
|
10
11
|
>
|
|
11
12
|
<div class="mb-2 flex items-center justify-between">
|
|
@@ -3,7 +3,9 @@
|
|
|
3
3
|
import { browser } from '$app/environment';
|
|
4
4
|
import { goto } from '$app/navigation';
|
|
5
5
|
import type { GraphNode, GraphEdge } from '@cwygoda/service-catalog-core/domain';
|
|
6
|
-
import type
|
|
6
|
+
import type { Selection } from 'd3-selection';
|
|
7
|
+
import type { Simulation, SimulationNodeDatum } from 'd3-force';
|
|
8
|
+
import type { D3ZoomEvent } from 'd3-zoom';
|
|
7
9
|
|
|
8
10
|
interface Props {
|
|
9
11
|
nodes: GraphNode[];
|
|
@@ -14,22 +16,21 @@
|
|
|
14
16
|
|
|
15
17
|
let { nodes, edges, height = 500, highlightedNodes }: Props = $props();
|
|
16
18
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
return domainColors[key] ?? '#6b7280';
|
|
19
|
+
function getDomainFillClass(domain?: string): string {
|
|
20
|
+
switch (domain) {
|
|
21
|
+
case 'commerce':
|
|
22
|
+
return 'fill-blue-500 dark:fill-blue-400';
|
|
23
|
+
case 'platform':
|
|
24
|
+
return 'fill-green-500 dark:fill-green-400';
|
|
25
|
+
default:
|
|
26
|
+
return 'fill-gray-500 dark:fill-gray-400';
|
|
27
|
+
}
|
|
27
28
|
}
|
|
28
29
|
|
|
29
30
|
let wrapper: HTMLDivElement;
|
|
30
31
|
let container: HTMLDivElement;
|
|
31
32
|
let svg: SVGSVGElement | null = null;
|
|
32
|
-
let simulation:
|
|
33
|
+
let simulation: Simulation<SimulationNodeDatum, undefined> | null = null;
|
|
33
34
|
|
|
34
35
|
// Simulation node type with position
|
|
35
36
|
interface SimNode extends GraphNode {
|
|
@@ -47,12 +48,16 @@
|
|
|
47
48
|
}
|
|
48
49
|
|
|
49
50
|
// D3 selection refs for highlight updates
|
|
50
|
-
let nodeSelection:
|
|
51
|
-
let linkSelection:
|
|
51
|
+
let nodeSelection: Selection<SVGGElement, SimNode, SVGGElement, unknown> | null = null;
|
|
52
|
+
let linkSelection: Selection<SVGLineElement, SimLink, SVGGElement, unknown> | null = null;
|
|
52
53
|
|
|
53
54
|
// Tooltip state
|
|
54
55
|
let tooltip = $state({ visible: false, x: 0, y: 0, content: '' });
|
|
55
56
|
|
|
57
|
+
// Responsive height — scales with container width on narrow screens
|
|
58
|
+
let responsiveHeight = $state(height);
|
|
59
|
+
let resizeObserver: ResizeObserver | null = null;
|
|
60
|
+
|
|
56
61
|
// Fullscreen state
|
|
57
62
|
let isFullscreen = $state(false);
|
|
58
63
|
|
|
@@ -73,16 +78,28 @@
|
|
|
73
78
|
if (!browser) return;
|
|
74
79
|
|
|
75
80
|
document.addEventListener('fullscreenchange', handleFullscreenChange);
|
|
76
|
-
|
|
77
|
-
|
|
81
|
+
document.addEventListener('visibilitychange', handleVisibilityChange);
|
|
82
|
+
|
|
83
|
+
const [
|
|
84
|
+
{ select },
|
|
85
|
+
{ forceSimulation, forceLink, forceManyBody, forceCenter, forceCollide },
|
|
86
|
+
{ zoom },
|
|
87
|
+
{ drag },
|
|
88
|
+
] = await Promise.all([
|
|
89
|
+
import('d3-selection'),
|
|
90
|
+
import('d3-force'),
|
|
91
|
+
import('d3-zoom'),
|
|
92
|
+
import('d3-drag'),
|
|
93
|
+
]);
|
|
78
94
|
|
|
79
95
|
const width = container.clientWidth;
|
|
96
|
+
responsiveHeight = Math.max(300, Math.min(width * 0.6, height));
|
|
80
97
|
|
|
81
98
|
// Create simulation nodes with positions
|
|
82
99
|
const simNodes: SimNode[] = nodes.map((n) => ({
|
|
83
100
|
...n,
|
|
84
101
|
x: width / 2 + (Math.random() - 0.5) * 100,
|
|
85
|
-
y:
|
|
102
|
+
y: responsiveHeight / 2 + (Math.random() - 0.5) * 100,
|
|
86
103
|
}));
|
|
87
104
|
|
|
88
105
|
// Create links referencing node objects
|
|
@@ -97,12 +114,16 @@
|
|
|
97
114
|
}
|
|
98
115
|
|
|
99
116
|
// Create SVG
|
|
100
|
-
const svgEl =
|
|
101
|
-
.select(container)
|
|
117
|
+
const svgEl = select(container)
|
|
102
118
|
.append('svg')
|
|
103
119
|
.attr('width', '100%')
|
|
104
|
-
.attr('height',
|
|
105
|
-
.attr('viewBox', `0 0 ${String(width)} ${String(
|
|
120
|
+
.attr('height', responsiveHeight)
|
|
121
|
+
.attr('viewBox', `0 0 ${String(width)} ${String(responsiveHeight)}`)
|
|
122
|
+
.attr('role', 'img')
|
|
123
|
+
.attr(
|
|
124
|
+
'aria-label',
|
|
125
|
+
`Service dependency graph with ${String(nodes.length)} services and ${String(edges.length)} connections`
|
|
126
|
+
);
|
|
106
127
|
|
|
107
128
|
svg = svgEl.node();
|
|
108
129
|
|
|
@@ -125,14 +146,13 @@
|
|
|
125
146
|
const g = svgEl.append('g');
|
|
126
147
|
|
|
127
148
|
// Add zoom behavior
|
|
128
|
-
const
|
|
129
|
-
.zoom<SVGSVGElement, unknown>()
|
|
149
|
+
const zoomBehavior = zoom<SVGSVGElement, unknown>()
|
|
130
150
|
.scaleExtent([0.25, 4])
|
|
131
|
-
.on('zoom', (event:
|
|
151
|
+
.on('zoom', (event: D3ZoomEvent<SVGSVGElement, unknown>) => {
|
|
132
152
|
g.attr('transform', event.transform.toString());
|
|
133
153
|
});
|
|
134
154
|
|
|
135
|
-
svgEl.call(
|
|
155
|
+
svgEl.call(zoomBehavior);
|
|
136
156
|
|
|
137
157
|
// Draw edges
|
|
138
158
|
linkSelection = g
|
|
@@ -159,8 +179,7 @@
|
|
|
159
179
|
|
|
160
180
|
// Add drag behavior
|
|
161
181
|
nodeGroups.call(
|
|
162
|
-
|
|
163
|
-
.drag<SVGGElement, SimNode>()
|
|
182
|
+
drag<SVGGElement, SimNode>()
|
|
164
183
|
.on('start', (event, d) => {
|
|
165
184
|
if (!event.active) simulation?.alphaTarget(0.3).restart();
|
|
166
185
|
d.fx = d.x;
|
|
@@ -183,13 +202,16 @@
|
|
|
183
202
|
nodeSelection
|
|
184
203
|
.append('circle')
|
|
185
204
|
.attr('r', 20)
|
|
186
|
-
.attr(
|
|
187
|
-
|
|
205
|
+
.attr(
|
|
206
|
+
'class',
|
|
207
|
+
(d) =>
|
|
208
|
+
`${getDomainFillClass(d.domain)} stroke-2 stroke-white dark:stroke-gray-800 transition-opacity`
|
|
209
|
+
)
|
|
188
210
|
.on('click', (_, d) => {
|
|
189
211
|
void goto(`/services/${d.id}`);
|
|
190
212
|
})
|
|
191
213
|
.on('mouseenter', function (event: MouseEvent, d: SimNode) {
|
|
192
|
-
|
|
214
|
+
select(this).attr('r', 24);
|
|
193
215
|
tooltip = {
|
|
194
216
|
visible: true,
|
|
195
217
|
x: event.pageX,
|
|
@@ -202,7 +224,7 @@
|
|
|
202
224
|
tooltip.y = event.pageY - 10;
|
|
203
225
|
})
|
|
204
226
|
.on('mouseleave', function () {
|
|
205
|
-
|
|
227
|
+
select(this).attr('r', 20);
|
|
206
228
|
tooltip.visible = false;
|
|
207
229
|
});
|
|
208
230
|
|
|
@@ -218,31 +240,48 @@
|
|
|
218
240
|
);
|
|
219
241
|
|
|
220
242
|
// Force simulation - D3's forceSimulation has complex generics that don't match our SimNode
|
|
221
|
-
simulation =
|
|
243
|
+
simulation =
|
|
222
244
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
245
|
+
forceSimulation(simNodes as any)
|
|
246
|
+
.force(
|
|
247
|
+
'link',
|
|
248
|
+
forceLink(simLinks)
|
|
249
|
+
.id((d) => (d as SimNode).id)
|
|
250
|
+
.distance(120)
|
|
251
|
+
)
|
|
252
|
+
.force('charge', forceManyBody().strength(-400))
|
|
253
|
+
.force('center', forceCenter(width / 2, responsiveHeight / 2))
|
|
254
|
+
.force('collision', forceCollide().radius(40))
|
|
255
|
+
.on('tick', () => {
|
|
256
|
+
linkSelection
|
|
257
|
+
?.attr('x1', (d) => d.source.x)
|
|
258
|
+
.attr('y1', (d) => d.source.y)
|
|
259
|
+
.attr('x2', (d) => d.target.x)
|
|
260
|
+
.attr('y2', (d) => d.target.y);
|
|
261
|
+
|
|
262
|
+
nodeSelection?.attr('transform', (d) => `translate(${String(d.x)},${String(d.y)})`);
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
// Resize observer for responsive height
|
|
266
|
+
resizeObserver = new ResizeObserver((entries) => {
|
|
267
|
+
const entry = entries[0];
|
|
268
|
+
if (!entry) return;
|
|
269
|
+
const newWidth = entry.contentRect.width;
|
|
270
|
+
const newHeight = Math.max(300, Math.min(newWidth * 0.6, height));
|
|
271
|
+
if (Math.abs(newHeight - responsiveHeight) < 10) return;
|
|
272
|
+
responsiveHeight = newHeight;
|
|
273
|
+
svgEl
|
|
274
|
+
.attr('height', newHeight)
|
|
275
|
+
.attr('viewBox', `0 0 ${String(newWidth)} ${String(newHeight)}`);
|
|
276
|
+
simulation
|
|
277
|
+
?.force('center', forceCenter(newWidth / 2, newHeight / 2))
|
|
278
|
+
.alpha(0.3)
|
|
279
|
+
.restart();
|
|
280
|
+
});
|
|
281
|
+
resizeObserver.observe(container);
|
|
243
282
|
});
|
|
244
283
|
|
|
245
|
-
// Effect to update highlighting when highlightedNodes changes
|
|
284
|
+
// Effect to update highlighting when highlightedNodes changes (CSS class-based)
|
|
246
285
|
$effect(() => {
|
|
247
286
|
if (!nodeSelection || !linkSelection) return;
|
|
248
287
|
|
|
@@ -252,24 +291,41 @@
|
|
|
252
291
|
if (hasHighlight) {
|
|
253
292
|
const highlightSet = new Set(highlighted);
|
|
254
293
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
linkSelection
|
|
260
|
-
|
|
261
|
-
|
|
294
|
+
nodeSelection
|
|
295
|
+
.classed('graph-node-dimmed', (d) => !highlightSet.has(d.id))
|
|
296
|
+
.classed('graph-full-opacity', (d) => highlightSet.has(d.id));
|
|
297
|
+
|
|
298
|
+
linkSelection
|
|
299
|
+
.classed(
|
|
300
|
+
'graph-link-dimmed',
|
|
301
|
+
(d) => !(highlightSet.has(d.source.id) && highlightSet.has(d.target.id))
|
|
302
|
+
)
|
|
303
|
+
.classed(
|
|
304
|
+
'graph-full-opacity',
|
|
305
|
+
(d) => highlightSet.has(d.source.id) && highlightSet.has(d.target.id)
|
|
306
|
+
);
|
|
262
307
|
} else {
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
linkSelection.attr('opacity', 1);
|
|
308
|
+
nodeSelection.classed('graph-node-dimmed', false).classed('graph-full-opacity', false);
|
|
309
|
+
linkSelection.classed('graph-link-dimmed', false).classed('graph-full-opacity', false);
|
|
266
310
|
}
|
|
267
311
|
});
|
|
268
312
|
|
|
313
|
+
// Pause simulation when tab is hidden to save CPU
|
|
314
|
+
function handleVisibilityChange(): void {
|
|
315
|
+
if (!simulation) return;
|
|
316
|
+
if (document.hidden) {
|
|
317
|
+
simulation.stop();
|
|
318
|
+
} else if (simulation.alpha() > simulation.alphaMin()) {
|
|
319
|
+
simulation.restart();
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
269
323
|
onDestroy(() => {
|
|
270
324
|
if (browser) {
|
|
271
325
|
document.removeEventListener('fullscreenchange', handleFullscreenChange);
|
|
326
|
+
document.removeEventListener('visibilitychange', handleVisibilityChange);
|
|
272
327
|
}
|
|
328
|
+
resizeObserver?.disconnect();
|
|
273
329
|
simulation?.stop();
|
|
274
330
|
svg?.remove();
|
|
275
331
|
});
|
|
@@ -279,7 +335,7 @@
|
|
|
279
335
|
<div
|
|
280
336
|
bind:this={container}
|
|
281
337
|
class="w-full rounded-lg border border-gray-200 bg-white dark:border-gray-700 dark:bg-gray-800"
|
|
282
|
-
style="height: {isFullscreen ? '100vh' : `${String(
|
|
338
|
+
style="height: {isFullscreen ? '100vh' : `${String(responsiveHeight)}px`}"
|
|
283
339
|
></div>
|
|
284
340
|
|
|
285
341
|
<!-- Tooltip -->
|
|
@@ -298,13 +354,13 @@
|
|
|
298
354
|
>
|
|
299
355
|
<button
|
|
300
356
|
onclick={toggleFullscreen}
|
|
301
|
-
class="rounded p-1.5 text-gray-600 transition-colors hover:bg-gray-100 hover:text-gray-900 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-white"
|
|
357
|
+
class="flex min-h-11 min-w-11 items-center justify-center rounded p-1.5 text-gray-600 transition-colors hover:bg-gray-100 hover:text-gray-900 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-white"
|
|
302
358
|
aria-label={isFullscreen ? 'Exit fullscreen' : 'Enter fullscreen'}
|
|
303
359
|
title={isFullscreen ? 'Exit fullscreen' : 'Fullscreen'}
|
|
304
360
|
>
|
|
305
361
|
{#if isFullscreen}
|
|
306
362
|
<!-- Exit fullscreen icon -->
|
|
307
|
-
<svg class="h-
|
|
363
|
+
<svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
|
308
364
|
<path
|
|
309
365
|
stroke-linecap="round"
|
|
310
366
|
stroke-linejoin="round"
|
|
@@ -313,7 +369,7 @@
|
|
|
313
369
|
</svg>
|
|
314
370
|
{:else}
|
|
315
371
|
<!-- Enter fullscreen icon -->
|
|
316
|
-
<svg class="h-
|
|
372
|
+
<svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
|
317
373
|
<path
|
|
318
374
|
stroke-linecap="round"
|
|
319
375
|
stroke-linejoin="round"
|
|
@@ -329,20 +385,38 @@
|
|
|
329
385
|
class="absolute bottom-3 left-3 flex flex-wrap gap-3 rounded-lg border border-gray-200 bg-white/90 px-3 py-2 text-xs backdrop-blur-sm dark:border-gray-600 dark:bg-gray-800/90"
|
|
330
386
|
>
|
|
331
387
|
<div class="flex items-center gap-1.5">
|
|
332
|
-
<span class="h-3 w-3 rounded-full bg-blue-500"></span>
|
|
333
|
-
<span class="text-gray-
|
|
388
|
+
<span class="h-3 w-3 rounded-full bg-blue-500 dark:bg-blue-400"></span>
|
|
389
|
+
<span class="text-gray-700 dark:text-gray-300">Commerce</span>
|
|
334
390
|
</div>
|
|
335
391
|
<div class="flex items-center gap-1.5">
|
|
336
|
-
<span class="h-3 w-3 rounded-
|
|
337
|
-
<span class="text-gray-
|
|
392
|
+
<span class="h-3 w-3 rounded-sm bg-green-500 dark:bg-green-400"></span>
|
|
393
|
+
<span class="text-gray-700 dark:text-gray-300">Platform</span>
|
|
338
394
|
</div>
|
|
339
395
|
<div class="flex items-center gap-1.5">
|
|
340
|
-
<span class="mr-1 h-0.5 w-4 bg-gray-500"></span>
|
|
341
|
-
<span class="text-gray-
|
|
396
|
+
<span class="mr-1 h-0.5 w-4 bg-gray-500 dark:bg-gray-400"></span>
|
|
397
|
+
<span class="text-gray-700 dark:text-gray-300">HTTP</span>
|
|
342
398
|
</div>
|
|
343
399
|
<div class="flex items-center gap-1.5">
|
|
344
|
-
<span class="mr-1 h-0.5 w-4 border-t-2 border-dashed border-gray-400"
|
|
345
|
-
|
|
400
|
+
<span class="mr-1 h-0.5 w-4 border-t-2 border-dashed border-gray-400 dark:border-gray-500"
|
|
401
|
+
></span>
|
|
402
|
+
<span class="text-gray-700 dark:text-gray-300">Event</span>
|
|
346
403
|
</div>
|
|
347
404
|
</div>
|
|
348
405
|
</div>
|
|
406
|
+
|
|
407
|
+
<style>
|
|
408
|
+
:global(.graph-node-dimmed) {
|
|
409
|
+
opacity: 0.2;
|
|
410
|
+
transition: opacity 0.3s ease;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
:global(.graph-link-dimmed) {
|
|
414
|
+
opacity: 0.1;
|
|
415
|
+
transition: opacity 0.3s ease;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
:global(.graph-full-opacity) {
|
|
419
|
+
opacity: 1;
|
|
420
|
+
transition: opacity 0.3s ease;
|
|
421
|
+
}
|
|
422
|
+
</style>
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
class="rounded-md p-2 text-gray-600 transition-colors hover:bg-gray-100 hover:text-gray-900 dark:text-gray-400 dark:hover:bg-gray-800 dark:hover:text-white"
|
|
10
10
|
aria-label="Toggle theme"
|
|
11
11
|
>
|
|
12
|
+
<span class="sr-only" aria-live="polite">Theme: {theme.resolved}</span>
|
|
12
13
|
{#if theme.resolved === 'dark'}
|
|
13
14
|
<svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
|
14
15
|
<path
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
|
|
12
12
|
<a
|
|
13
13
|
href="/use-cases/{useCase.id}"
|
|
14
|
+
aria-label="View {useCase.name} use case"
|
|
14
15
|
class="block rounded-lg border border-gray-200 bg-white p-6 shadow-sm transition-shadow hover:shadow-md dark:border-gray-700 dark:bg-gray-800"
|
|
15
16
|
>
|
|
16
17
|
<div class="mb-2 flex items-center justify-between">
|
|
@@ -19,7 +20,7 @@
|
|
|
19
20
|
</h3>
|
|
20
21
|
{#if useCase.bpmn}
|
|
21
22
|
<span
|
|
22
|
-
class="rounded-full bg-
|
|
23
|
+
class="rounded-full bg-primary-100 px-2.5 py-0.5 text-xs font-medium text-primary-800 dark:bg-primary-900 dark:text-primary-200"
|
|
23
24
|
>
|
|
24
25
|
BPMN
|
|
25
26
|
</span>
|
|
@@ -30,7 +31,7 @@
|
|
|
30
31
|
{truncate(useCase.description, 120)}
|
|
31
32
|
</p>
|
|
32
33
|
|
|
33
|
-
<div class="flex items-center gap-4 text-xs text-gray-500 dark:text-gray-
|
|
34
|
+
<div class="flex items-center gap-4 text-xs text-gray-500 dark:text-gray-400">
|
|
34
35
|
<span class="flex items-center gap-1">
|
|
35
36
|
<svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
36
37
|
<path
|
package/dist/source.css
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
@source "./";
|
package/package.json
CHANGED
|
@@ -1,19 +1,22 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cwygoda/service-catalog-ui",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.3",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"svelte": "./dist/index.js",
|
|
6
6
|
"exports": {
|
|
7
7
|
".": {
|
|
8
8
|
"types": "./dist/index.d.ts",
|
|
9
|
+
"style": "./dist/source.css",
|
|
9
10
|
"svelte": "./dist/index.js"
|
|
10
11
|
},
|
|
11
12
|
"./components": {
|
|
12
13
|
"types": "./dist/components/index.d.ts",
|
|
14
|
+
"style": "./dist/source.css",
|
|
13
15
|
"svelte": "./dist/components/index.js"
|
|
14
16
|
},
|
|
15
17
|
"./stores": {
|
|
16
18
|
"types": "./dist/stores/index.d.ts",
|
|
19
|
+
"style": "./dist/source.css",
|
|
17
20
|
"svelte": "./dist/stores/index.js"
|
|
18
21
|
}
|
|
19
22
|
},
|
|
@@ -22,12 +25,15 @@
|
|
|
22
25
|
],
|
|
23
26
|
"dependencies": {
|
|
24
27
|
"@sinclair/typebox": "^0.34.0",
|
|
25
|
-
"@cwygoda/service-catalog-core": "1.0.
|
|
28
|
+
"@cwygoda/service-catalog-core": "1.0.3"
|
|
26
29
|
},
|
|
27
30
|
"peerDependencies": {
|
|
28
31
|
"@sveltejs/kit": "^2.0.0",
|
|
29
32
|
"bpmn-js": "^18.0.0",
|
|
30
|
-
"d3": "^
|
|
33
|
+
"d3-drag": "^3.0.0",
|
|
34
|
+
"d3-force": "^3.0.0",
|
|
35
|
+
"d3-selection": "^3.0.0",
|
|
36
|
+
"d3-zoom": "^3.0.0",
|
|
31
37
|
"svelte": "^5.0.0"
|
|
32
38
|
},
|
|
33
39
|
"devDependencies": {
|
|
@@ -37,7 +43,10 @@
|
|
|
37
43
|
"@testing-library/jest-dom": "^6.9.1",
|
|
38
44
|
"@testing-library/svelte": "^5.2.0",
|
|
39
45
|
"@testing-library/user-event": "^14.6.1",
|
|
40
|
-
"@types/d3": "^
|
|
46
|
+
"@types/d3-drag": "^3.0.0",
|
|
47
|
+
"@types/d3-force": "^3.0.0",
|
|
48
|
+
"@types/d3-selection": "^3.0.0",
|
|
49
|
+
"@types/d3-zoom": "^3.0.0",
|
|
41
50
|
"jsdom": "^26.0.0",
|
|
42
51
|
"svelte": "^5.49.2",
|
|
43
52
|
"svelte-check": "^4.3.6",
|