@cwygoda/service-catalog-ui 0.17.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/__mocks__/app-environment.d.ts +4 -0
- package/dist/__mocks__/app-environment.js +4 -0
- package/dist/__mocks__/app-state.d.ts +12 -0
- package/dist/__mocks__/app-state.js +10 -0
- package/dist/adapters/index.d.ts +1 -0
- package/dist/adapters/index.js +1 -0
- package/dist/adapters/static-json.adapter.d.ts +2 -0
- package/dist/adapters/static-json.adapter.js +34 -0
- package/dist/components/BpmnDiagram.svelte +496 -0
- package/dist/components/BpmnDiagram.svelte.d.ts +18 -0
- package/dist/components/Breadcrumbs.svelte +32 -0
- package/dist/components/Breadcrumbs.svelte.d.ts +11 -0
- package/dist/components/DataStoreCard.svelte +26 -0
- package/dist/components/DataStoreCard.svelte.d.ts +7 -0
- package/dist/components/DataStoreShield.svelte +67 -0
- package/dist/components/DataStoreShield.svelte.d.ts +9 -0
- package/dist/components/DomainCard.svelte +34 -0
- package/dist/components/DomainCard.svelte.d.ts +9 -0
- package/dist/components/DomainCard.test.d.ts +1 -0
- package/dist/components/DomainCard.test.js +45 -0
- package/dist/components/Header.svelte +144 -0
- package/dist/components/Header.svelte.d.ts +3 -0
- package/dist/components/NavModeToggle.svelte +43 -0
- package/dist/components/NavModeToggle.svelte.d.ts +18 -0
- package/dist/components/NavTree.svelte +245 -0
- package/dist/components/NavTree.svelte.d.ts +10 -0
- package/dist/components/SearchModal.svelte +288 -0
- package/dist/components/SearchModal.svelte.d.ts +3 -0
- package/dist/components/ServiceCard.svelte +36 -0
- package/dist/components/ServiceCard.svelte.d.ts +7 -0
- package/dist/components/ServiceCard.test.d.ts +1 -0
- package/dist/components/ServiceCard.test.js +43 -0
- package/dist/components/ServiceGraph.svelte +437 -0
- package/dist/components/ServiceGraph.svelte.d.ts +10 -0
- package/dist/components/ServiceTypeShield.svelte +32 -0
- package/dist/components/ServiceTypeShield.svelte.d.ts +8 -0
- package/dist/components/Shield.svelte +118 -0
- package/dist/components/Shield.svelte.d.ts +7 -0
- package/dist/components/ThemeToggle.svelte +44 -0
- package/dist/components/ThemeToggle.svelte.d.ts +18 -0
- package/dist/components/ThemeToggle.test.d.ts +1 -0
- package/dist/components/ThemeToggle.test.js +52 -0
- package/dist/components/UseCaseCard.svelte +61 -0
- package/dist/components/UseCaseCard.svelte.d.ts +7 -0
- package/dist/components/UseCaseCard.test.d.ts +1 -0
- package/dist/components/UseCaseCard.test.js +87 -0
- package/dist/components/index.d.ts +15 -0
- package/dist/components/index.js +15 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +5 -0
- package/dist/ports/catalog.port.d.ts +5 -0
- package/dist/ports/catalog.port.js +1 -0
- package/dist/ports/index.d.ts +1 -0
- package/dist/ports/index.js +1 -0
- package/dist/source.css +1 -0
- package/dist/stores/index.d.ts +4 -0
- package/dist/stores/index.js +3 -0
- package/dist/stores/nav-mode.svelte.d.ts +7 -0
- package/dist/stores/nav-mode.svelte.js +32 -0
- package/dist/stores/search.svelte.d.ts +9 -0
- package/dist/stores/search.svelte.js +14 -0
- package/dist/stores/theme.svelte.d.ts +8 -0
- package/dist/stores/theme.svelte.js +61 -0
- package/dist/utils/fetch-catalog.d.ts +6 -0
- package/dist/utils/fetch-catalog.js +26 -0
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/index.js +1 -0
- package/package.json +63 -0
|
@@ -0,0 +1,437 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { onMount, onDestroy } from 'svelte';
|
|
3
|
+
import { browser } from '$app/environment';
|
|
4
|
+
import { goto } from '$app/navigation';
|
|
5
|
+
import type { GraphNode, GraphEdge } from '@cwygoda/service-catalog-core/domain';
|
|
6
|
+
import type { Selection } from 'd3-selection';
|
|
7
|
+
import type { Simulation, SimulationNodeDatum } from 'd3-force';
|
|
8
|
+
import type { D3ZoomEvent } from 'd3-zoom';
|
|
9
|
+
|
|
10
|
+
interface Props {
|
|
11
|
+
nodes: GraphNode[];
|
|
12
|
+
edges: GraphEdge[];
|
|
13
|
+
height?: number;
|
|
14
|
+
highlightedNodes?: string[] | undefined;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
let { nodes, edges, height = 500, highlightedNodes }: Props = $props();
|
|
18
|
+
|
|
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
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
let wrapper: HTMLDivElement;
|
|
31
|
+
let container: HTMLDivElement;
|
|
32
|
+
let svg: SVGSVGElement | null = null;
|
|
33
|
+
let simulation: Simulation<SimulationNodeDatum, undefined> | null = null;
|
|
34
|
+
|
|
35
|
+
// Simulation node type with position
|
|
36
|
+
interface SimNode extends GraphNode {
|
|
37
|
+
x: number;
|
|
38
|
+
y: number;
|
|
39
|
+
fx?: number | null;
|
|
40
|
+
fy?: number | null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Simulation link type with node references
|
|
44
|
+
interface SimLink {
|
|
45
|
+
source: SimNode;
|
|
46
|
+
target: SimNode;
|
|
47
|
+
type: string;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// D3 selection refs for highlight updates
|
|
51
|
+
let nodeSelection: Selection<SVGGElement, SimNode, SVGGElement, unknown> | null = null;
|
|
52
|
+
let linkSelection: Selection<SVGLineElement, SimLink, SVGGElement, unknown> | null = null;
|
|
53
|
+
|
|
54
|
+
// Tooltip state
|
|
55
|
+
let tooltip = $state({ visible: false, x: 0, y: 0, content: '' });
|
|
56
|
+
|
|
57
|
+
// Responsive height — scales with container width on narrow screens
|
|
58
|
+
// Initialized to default; computed from height prop in onMount/resize
|
|
59
|
+
let responsiveHeight = $state(500);
|
|
60
|
+
let resizeObserver: ResizeObserver | null = null;
|
|
61
|
+
|
|
62
|
+
// Fullscreen state
|
|
63
|
+
let isFullscreen = $state(false);
|
|
64
|
+
|
|
65
|
+
function toggleFullscreen(): void {
|
|
66
|
+
if (!browser) return;
|
|
67
|
+
if (!document.fullscreenElement) {
|
|
68
|
+
void wrapper.requestFullscreen();
|
|
69
|
+
} else {
|
|
70
|
+
void document.exitFullscreen();
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function handleFullscreenChange(): void {
|
|
75
|
+
isFullscreen = !!document.fullscreenElement;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
onMount(async () => {
|
|
79
|
+
if (!browser) return;
|
|
80
|
+
|
|
81
|
+
document.addEventListener('fullscreenchange', handleFullscreenChange);
|
|
82
|
+
document.addEventListener('visibilitychange', handleVisibilityChange);
|
|
83
|
+
|
|
84
|
+
const [
|
|
85
|
+
{ select },
|
|
86
|
+
{ forceSimulation, forceLink, forceManyBody, forceCenter, forceCollide },
|
|
87
|
+
{ zoom },
|
|
88
|
+
{ drag },
|
|
89
|
+
] = await Promise.all([
|
|
90
|
+
import('d3-selection'),
|
|
91
|
+
import('d3-force'),
|
|
92
|
+
import('d3-zoom'),
|
|
93
|
+
import('d3-drag'),
|
|
94
|
+
]);
|
|
95
|
+
|
|
96
|
+
const width = container.clientWidth;
|
|
97
|
+
responsiveHeight = Math.max(width < 640 ? 400 : 300, Math.min(width * 0.6, height));
|
|
98
|
+
|
|
99
|
+
// Create simulation nodes with positions
|
|
100
|
+
const simNodes: SimNode[] = nodes.map((n) => ({
|
|
101
|
+
...n,
|
|
102
|
+
x: width / 2 + (Math.random() - 0.5) * 100,
|
|
103
|
+
y: responsiveHeight / 2 + (Math.random() - 0.5) * 100,
|
|
104
|
+
}));
|
|
105
|
+
|
|
106
|
+
// Create links referencing node objects
|
|
107
|
+
const nodeById = new Map(simNodes.map((n) => [n.id, n]));
|
|
108
|
+
const simLinks: SimLink[] = [];
|
|
109
|
+
for (const e of edges) {
|
|
110
|
+
const source = nodeById.get(e.source);
|
|
111
|
+
const target = nodeById.get(e.target);
|
|
112
|
+
if (source && target) {
|
|
113
|
+
simLinks.push({ source, target, type: e.type });
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Create SVG
|
|
118
|
+
const svgEl = select(container)
|
|
119
|
+
.append('svg')
|
|
120
|
+
.attr('width', '100%')
|
|
121
|
+
.attr('height', responsiveHeight)
|
|
122
|
+
.attr('viewBox', `0 0 ${String(width)} ${String(responsiveHeight)}`)
|
|
123
|
+
.attr('role', 'img')
|
|
124
|
+
.attr(
|
|
125
|
+
'aria-label',
|
|
126
|
+
`Service dependency graph with ${String(nodes.length)} services and ${String(edges.length)} connections`
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
svg = svgEl.node();
|
|
130
|
+
|
|
131
|
+
// Arrow marker definitions
|
|
132
|
+
const defs = svgEl.append('defs');
|
|
133
|
+
defs
|
|
134
|
+
.append('marker')
|
|
135
|
+
.attr('id', 'arrowhead')
|
|
136
|
+
.attr('viewBox', '0 -5 10 10')
|
|
137
|
+
.attr('refX', 25)
|
|
138
|
+
.attr('refY', 0)
|
|
139
|
+
.attr('markerWidth', 6)
|
|
140
|
+
.attr('markerHeight', 6)
|
|
141
|
+
.attr('orient', 'auto')
|
|
142
|
+
.append('path')
|
|
143
|
+
.attr('d', 'M0,-5L10,0L0,5')
|
|
144
|
+
.attr('class', 'fill-gray-400 dark:fill-gray-500');
|
|
145
|
+
|
|
146
|
+
// Container group for zoom/pan
|
|
147
|
+
const g = svgEl.append('g');
|
|
148
|
+
|
|
149
|
+
// Add zoom behavior
|
|
150
|
+
const zoomBehavior = zoom<SVGSVGElement, unknown>()
|
|
151
|
+
.scaleExtent([0.25, 4])
|
|
152
|
+
.on('zoom', (event: D3ZoomEvent<SVGSVGElement, unknown>) => {
|
|
153
|
+
g.attr('transform', event.transform.toString());
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
svgEl.call(zoomBehavior);
|
|
157
|
+
|
|
158
|
+
// Draw edges
|
|
159
|
+
linkSelection = g
|
|
160
|
+
.append('g')
|
|
161
|
+
.selectAll<SVGLineElement, SimLink>('line')
|
|
162
|
+
.data(simLinks)
|
|
163
|
+
.join('line')
|
|
164
|
+
.attr('class', (d) =>
|
|
165
|
+
d.type === 'event'
|
|
166
|
+
? 'stroke-gray-400 dark:stroke-gray-500'
|
|
167
|
+
: 'stroke-gray-500 dark:stroke-gray-400'
|
|
168
|
+
)
|
|
169
|
+
.attr('stroke-width', 2)
|
|
170
|
+
.attr('stroke-dasharray', (d) => (d.type === 'event' ? '5,5' : 'none'))
|
|
171
|
+
.attr('marker-end', 'url(#arrowhead)');
|
|
172
|
+
|
|
173
|
+
// Draw nodes
|
|
174
|
+
const nodeGroups = g
|
|
175
|
+
.append('g')
|
|
176
|
+
.selectAll<SVGGElement, SimNode>('g')
|
|
177
|
+
.data(simNodes)
|
|
178
|
+
.join('g')
|
|
179
|
+
.attr('class', 'cursor-pointer');
|
|
180
|
+
|
|
181
|
+
// Add drag behavior
|
|
182
|
+
nodeGroups.call(
|
|
183
|
+
drag<SVGGElement, SimNode>()
|
|
184
|
+
.on('start', (event, d) => {
|
|
185
|
+
if (!event.active) simulation?.alphaTarget(0.3).restart();
|
|
186
|
+
d.fx = d.x;
|
|
187
|
+
d.fy = d.y;
|
|
188
|
+
})
|
|
189
|
+
.on('drag', (event, d) => {
|
|
190
|
+
d.fx = event.x as number;
|
|
191
|
+
d.fy = event.y as number;
|
|
192
|
+
})
|
|
193
|
+
.on('end', (event, d) => {
|
|
194
|
+
if (!event.active) simulation?.alphaTarget(0);
|
|
195
|
+
d.fx = null;
|
|
196
|
+
d.fy = null;
|
|
197
|
+
})
|
|
198
|
+
);
|
|
199
|
+
|
|
200
|
+
nodeSelection = nodeGroups;
|
|
201
|
+
|
|
202
|
+
// Node circles
|
|
203
|
+
nodeSelection
|
|
204
|
+
.append('circle')
|
|
205
|
+
.attr('r', 20)
|
|
206
|
+
.attr(
|
|
207
|
+
'class',
|
|
208
|
+
(d) =>
|
|
209
|
+
`${getDomainFillClass(d.domain)} stroke-2 stroke-white dark:stroke-gray-800 transition-opacity`
|
|
210
|
+
)
|
|
211
|
+
.on('click', (_, d) => {
|
|
212
|
+
void goto(`/services/${d.id}`);
|
|
213
|
+
})
|
|
214
|
+
.on('mouseenter', function (event: MouseEvent, d: SimNode) {
|
|
215
|
+
select(this).attr('r', 24);
|
|
216
|
+
tooltip = {
|
|
217
|
+
visible: true,
|
|
218
|
+
x: event.pageX,
|
|
219
|
+
y: event.pageY - 10,
|
|
220
|
+
content: `${d.name}${d.domain ? ` (${d.domain})` : ''}`,
|
|
221
|
+
};
|
|
222
|
+
})
|
|
223
|
+
.on('mousemove', (event: MouseEvent) => {
|
|
224
|
+
tooltip.x = event.pageX;
|
|
225
|
+
tooltip.y = event.pageY - 10;
|
|
226
|
+
})
|
|
227
|
+
.on('mouseleave', function () {
|
|
228
|
+
select(this).attr('r', 20);
|
|
229
|
+
tooltip.visible = false;
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
// Node labels
|
|
233
|
+
nodeSelection
|
|
234
|
+
.append('text')
|
|
235
|
+
.text((d) => d.name.split(' ')[0] ?? d.name)
|
|
236
|
+
.attr('text-anchor', 'middle')
|
|
237
|
+
.attr('dy', 35)
|
|
238
|
+
.attr(
|
|
239
|
+
'class',
|
|
240
|
+
'text-xs fill-gray-700 dark:fill-gray-300 pointer-events-none transition-opacity'
|
|
241
|
+
);
|
|
242
|
+
|
|
243
|
+
// Force simulation - D3's forceSimulation has complex generics that don't match our SimNode
|
|
244
|
+
simulation =
|
|
245
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
|
|
246
|
+
forceSimulation(simNodes as any)
|
|
247
|
+
.force(
|
|
248
|
+
'link',
|
|
249
|
+
forceLink(simLinks)
|
|
250
|
+
.id((d) => (d as SimNode).id)
|
|
251
|
+
.distance(120)
|
|
252
|
+
)
|
|
253
|
+
.force('charge', forceManyBody().strength(-400))
|
|
254
|
+
.force('center', forceCenter(width / 2, responsiveHeight / 2))
|
|
255
|
+
.force('collision', forceCollide().radius(40))
|
|
256
|
+
.on('tick', () => {
|
|
257
|
+
linkSelection
|
|
258
|
+
?.attr('x1', (d) => d.source.x)
|
|
259
|
+
.attr('y1', (d) => d.source.y)
|
|
260
|
+
.attr('x2', (d) => d.target.x)
|
|
261
|
+
.attr('y2', (d) => d.target.y);
|
|
262
|
+
|
|
263
|
+
nodeSelection?.attr('transform', (d) => `translate(${String(d.x)},${String(d.y)})`);
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
// Resize observer for responsive height
|
|
267
|
+
resizeObserver = new ResizeObserver((entries) => {
|
|
268
|
+
const entry = entries[0];
|
|
269
|
+
if (!entry) return;
|
|
270
|
+
const newWidth = entry.contentRect.width;
|
|
271
|
+
const newHeight = Math.max(300, Math.min(newWidth * 0.6, height));
|
|
272
|
+
if (Math.abs(newHeight - responsiveHeight) < 10) return;
|
|
273
|
+
responsiveHeight = newHeight;
|
|
274
|
+
svgEl
|
|
275
|
+
.attr('height', newHeight)
|
|
276
|
+
.attr('viewBox', `0 0 ${String(newWidth)} ${String(newHeight)}`);
|
|
277
|
+
simulation
|
|
278
|
+
?.force('center', forceCenter(newWidth / 2, newHeight / 2))
|
|
279
|
+
.alpha(0.3)
|
|
280
|
+
.restart();
|
|
281
|
+
});
|
|
282
|
+
resizeObserver.observe(container);
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
// Effect to update highlighting when highlightedNodes changes (CSS class-based)
|
|
286
|
+
$effect(() => {
|
|
287
|
+
if (!nodeSelection || !linkSelection) return;
|
|
288
|
+
|
|
289
|
+
const highlighted = highlightedNodes;
|
|
290
|
+
const hasHighlight = highlighted && highlighted.length > 0;
|
|
291
|
+
|
|
292
|
+
if (hasHighlight) {
|
|
293
|
+
const highlightSet = new Set(highlighted);
|
|
294
|
+
|
|
295
|
+
nodeSelection
|
|
296
|
+
.classed('graph-node-dimmed', (d) => !highlightSet.has(d.id))
|
|
297
|
+
.classed('graph-full-opacity', (d) => highlightSet.has(d.id));
|
|
298
|
+
|
|
299
|
+
linkSelection
|
|
300
|
+
.classed(
|
|
301
|
+
'graph-link-dimmed',
|
|
302
|
+
(d) => !(highlightSet.has(d.source.id) && highlightSet.has(d.target.id))
|
|
303
|
+
)
|
|
304
|
+
.classed(
|
|
305
|
+
'graph-full-opacity',
|
|
306
|
+
(d) => highlightSet.has(d.source.id) && highlightSet.has(d.target.id)
|
|
307
|
+
);
|
|
308
|
+
} else {
|
|
309
|
+
nodeSelection.classed('graph-node-dimmed', false).classed('graph-full-opacity', false);
|
|
310
|
+
linkSelection.classed('graph-link-dimmed', false).classed('graph-full-opacity', false);
|
|
311
|
+
}
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
// Pause simulation when tab is hidden to save CPU
|
|
315
|
+
function handleVisibilityChange(): void {
|
|
316
|
+
if (!simulation) return;
|
|
317
|
+
if (document.hidden) {
|
|
318
|
+
simulation.stop();
|
|
319
|
+
} else if (simulation.alpha() > simulation.alphaMin()) {
|
|
320
|
+
simulation.restart();
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
onDestroy(() => {
|
|
325
|
+
if (browser) {
|
|
326
|
+
document.removeEventListener('fullscreenchange', handleFullscreenChange);
|
|
327
|
+
document.removeEventListener('visibilitychange', handleVisibilityChange);
|
|
328
|
+
}
|
|
329
|
+
resizeObserver?.disconnect();
|
|
330
|
+
simulation?.stop();
|
|
331
|
+
svg?.remove();
|
|
332
|
+
});
|
|
333
|
+
</script>
|
|
334
|
+
|
|
335
|
+
<div bind:this={wrapper} class="relative {isFullscreen ? 'bg-white dark:bg-gray-900' : ''}">
|
|
336
|
+
<div
|
|
337
|
+
bind:this={container}
|
|
338
|
+
class="w-full rounded-lg border border-gray-200 bg-white dark:border-gray-700 dark:bg-gray-800"
|
|
339
|
+
style="height: {isFullscreen ? '100vh' : `${String(responsiveHeight)}px`}"
|
|
340
|
+
></div>
|
|
341
|
+
|
|
342
|
+
<!-- Tooltip -->
|
|
343
|
+
{#if tooltip.visible}
|
|
344
|
+
<div
|
|
345
|
+
class="pointer-events-none fixed z-50 rounded bg-gray-900 px-2 py-1 text-sm text-white shadow-lg dark:bg-gray-700"
|
|
346
|
+
style="left: {tooltip.x}px; top: {tooltip.y}px; transform: translate(-50%, -100%)"
|
|
347
|
+
>
|
|
348
|
+
{tooltip.content}
|
|
349
|
+
</div>
|
|
350
|
+
{/if}
|
|
351
|
+
|
|
352
|
+
<!-- Controls -->
|
|
353
|
+
<div
|
|
354
|
+
class="absolute right-3 top-3 flex flex-col gap-1 rounded-lg border border-gray-200 bg-white/90 p-1 shadow-sm backdrop-blur-sm dark:border-gray-600 dark:bg-gray-800/90"
|
|
355
|
+
>
|
|
356
|
+
<button
|
|
357
|
+
onclick={toggleFullscreen}
|
|
358
|
+
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 focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-primary-600 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-white"
|
|
359
|
+
aria-label={isFullscreen ? 'Exit fullscreen' : 'Enter fullscreen'}
|
|
360
|
+
title={isFullscreen ? 'Exit fullscreen' : 'Fullscreen'}
|
|
361
|
+
>
|
|
362
|
+
{#if isFullscreen}
|
|
363
|
+
<!-- Exit fullscreen icon -->
|
|
364
|
+
<svg
|
|
365
|
+
class="h-5 w-5"
|
|
366
|
+
fill="none"
|
|
367
|
+
viewBox="0 0 24 24"
|
|
368
|
+
stroke="currentColor"
|
|
369
|
+
stroke-width="2"
|
|
370
|
+
aria-hidden="true"
|
|
371
|
+
>
|
|
372
|
+
<path
|
|
373
|
+
stroke-linecap="round"
|
|
374
|
+
stroke-linejoin="round"
|
|
375
|
+
d="M9 9V4.5M9 9H4.5M9 9L3.75 3.75M9 15v4.5M9 15H4.5M9 15l-5.25 5.25M15 9h4.5M15 9V4.5M15 9l5.25-5.25M15 15h4.5M15 15v4.5m0-4.5l5.25 5.25"
|
|
376
|
+
/>
|
|
377
|
+
</svg>
|
|
378
|
+
{:else}
|
|
379
|
+
<!-- Enter fullscreen icon -->
|
|
380
|
+
<svg
|
|
381
|
+
class="h-5 w-5"
|
|
382
|
+
fill="none"
|
|
383
|
+
viewBox="0 0 24 24"
|
|
384
|
+
stroke="currentColor"
|
|
385
|
+
stroke-width="2"
|
|
386
|
+
aria-hidden="true"
|
|
387
|
+
>
|
|
388
|
+
<path
|
|
389
|
+
stroke-linecap="round"
|
|
390
|
+
stroke-linejoin="round"
|
|
391
|
+
d="M3.75 3.75v4.5m0-4.5h4.5m-4.5 0L9 9M3.75 20.25v-4.5m0 4.5h4.5m-4.5 0L9 15M20.25 3.75h-4.5m4.5 0v4.5m0-4.5L15 9m5.25 11.25h-4.5m4.5 0v-4.5m0 4.5L15 15"
|
|
392
|
+
/>
|
|
393
|
+
</svg>
|
|
394
|
+
{/if}
|
|
395
|
+
</button>
|
|
396
|
+
</div>
|
|
397
|
+
|
|
398
|
+
<!-- Legend -->
|
|
399
|
+
<div
|
|
400
|
+
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"
|
|
401
|
+
>
|
|
402
|
+
<div class="flex items-center gap-1.5">
|
|
403
|
+
<span class="h-3 w-3 rounded-full bg-blue-500 dark:bg-blue-400"></span>
|
|
404
|
+
<span class="text-gray-700 dark:text-gray-300">Commerce</span>
|
|
405
|
+
</div>
|
|
406
|
+
<div class="flex items-center gap-1.5">
|
|
407
|
+
<span class="h-3 w-3 rounded-sm bg-green-500 dark:bg-green-400"></span>
|
|
408
|
+
<span class="text-gray-700 dark:text-gray-300">Platform</span>
|
|
409
|
+
</div>
|
|
410
|
+
<div class="flex items-center gap-1.5">
|
|
411
|
+
<span class="mr-1 h-0.5 w-4 bg-gray-500 dark:bg-gray-400"></span>
|
|
412
|
+
<span class="text-gray-700 dark:text-gray-300">HTTP</span>
|
|
413
|
+
</div>
|
|
414
|
+
<div class="flex items-center gap-1.5">
|
|
415
|
+
<span class="mr-1 h-0.5 w-4 border-t-2 border-dashed border-gray-400 dark:border-gray-500"
|
|
416
|
+
></span>
|
|
417
|
+
<span class="text-gray-700 dark:text-gray-300">Event</span>
|
|
418
|
+
</div>
|
|
419
|
+
</div>
|
|
420
|
+
</div>
|
|
421
|
+
|
|
422
|
+
<style>
|
|
423
|
+
:global(.graph-node-dimmed) {
|
|
424
|
+
opacity: 0.2;
|
|
425
|
+
transition: opacity 0.3s ease;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
:global(.graph-link-dimmed) {
|
|
429
|
+
opacity: 0.1;
|
|
430
|
+
transition: opacity 0.3s ease;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
:global(.graph-full-opacity) {
|
|
434
|
+
opacity: 1;
|
|
435
|
+
transition: opacity 0.3s ease;
|
|
436
|
+
}
|
|
437
|
+
</style>
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { GraphNode, GraphEdge } from '@cwygoda/service-catalog-core/domain';
|
|
2
|
+
interface Props {
|
|
3
|
+
nodes: GraphNode[];
|
|
4
|
+
edges: GraphEdge[];
|
|
5
|
+
height?: number;
|
|
6
|
+
highlightedNodes?: string[] | undefined;
|
|
7
|
+
}
|
|
8
|
+
declare const ServiceGraph: import("svelte").Component<Props, {}, "">;
|
|
9
|
+
type ServiceGraph = ReturnType<typeof ServiceGraph>;
|
|
10
|
+
export default ServiceGraph;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { ServiceType } from '@cwygoda/service-catalog-core/domain';
|
|
3
|
+
import Shield from './Shield.svelte';
|
|
4
|
+
|
|
5
|
+
interface Props {
|
|
6
|
+
type: ServiceType;
|
|
7
|
+
size?: number;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
let { type, size = 40 }: Props = $props();
|
|
11
|
+
|
|
12
|
+
const labels: Record<ServiceType, { short: string; full: string }> = {
|
|
13
|
+
'web-service': { short: 'Ws', full: 'Web Service' },
|
|
14
|
+
'event-consumer': { short: 'Ec', full: 'Event Consumer' },
|
|
15
|
+
'event-producer': { short: 'Ep', full: 'Event Producer' },
|
|
16
|
+
'event-transformer': { short: 'Et', full: 'Event Transformer' },
|
|
17
|
+
'web-app': { short: 'Wa', full: 'Web App' },
|
|
18
|
+
library: { short: 'Li', full: 'Library' },
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
let entry = $derived(labels[type]);
|
|
22
|
+
</script>
|
|
23
|
+
|
|
24
|
+
<span class="group relative inline-flex" title={entry.full}>
|
|
25
|
+
<Shield label={entry.short} {size} />
|
|
26
|
+
<span
|
|
27
|
+
role="tooltip"
|
|
28
|
+
class="pointer-events-none absolute bottom-full left-1/2 z-10 mb-2 -translate-x-1/2 whitespace-nowrap rounded bg-gray-900 px-2 py-1 text-xs font-medium text-white opacity-0 shadow-lg transition-opacity group-hover:opacity-100 group-focus-within:opacity-100 dark:bg-gray-100 dark:text-gray-900"
|
|
29
|
+
>
|
|
30
|
+
{entry.full}
|
|
31
|
+
</span>
|
|
32
|
+
</span>
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { ServiceType } from '@cwygoda/service-catalog-core/domain';
|
|
2
|
+
interface Props {
|
|
3
|
+
type: ServiceType;
|
|
4
|
+
size?: number;
|
|
5
|
+
}
|
|
6
|
+
declare const ServiceTypeShield: import("svelte").Component<Props, {}, "">;
|
|
7
|
+
type ServiceTypeShield = ReturnType<typeof ServiceTypeShield>;
|
|
8
|
+
export default ServiceTypeShield;
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
interface Props {
|
|
3
|
+
label: string;
|
|
4
|
+
size?: number;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
let { label, size = 80 }: Props = $props();
|
|
8
|
+
|
|
9
|
+
// FNV-1a 32-bit hash with avalanche finalizer
|
|
10
|
+
function hash(str: string): number {
|
|
11
|
+
let h = 0x811c9dc5;
|
|
12
|
+
for (let i = 0; i < str.length; i++) {
|
|
13
|
+
h ^= str.charCodeAt(i);
|
|
14
|
+
h = Math.imul(h, 0x01000193) >>> 0;
|
|
15
|
+
}
|
|
16
|
+
// Murmur3-style finalizer — spreads clustered inputs
|
|
17
|
+
h ^= h >>> 16;
|
|
18
|
+
h = Math.imul(h, 0x85ebca6b) >>> 0;
|
|
19
|
+
h ^= h >>> 13;
|
|
20
|
+
h = Math.imul(h, 0xc2b2ae35) >>> 0;
|
|
21
|
+
h ^= h >>> 16;
|
|
22
|
+
return h >>> 0;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Golden-ratio distribution: maps hash to [0, range) with maximal spacing
|
|
26
|
+
function golden(h: number, range: number): number {
|
|
27
|
+
return Math.round(((h / 0x100000000) * 0x9e3779b9) % 1 * range);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const n = (v: number): string => String(v);
|
|
31
|
+
|
|
32
|
+
let display = $derived(label.slice(0, 2).toUpperCase());
|
|
33
|
+
|
|
34
|
+
let palette = $derived.by(() => {
|
|
35
|
+
const h = hash(label);
|
|
36
|
+
const h2 = hash(label + 'salt2');
|
|
37
|
+
const h3 = hash(label + 'salt3');
|
|
38
|
+
|
|
39
|
+
const hue1 = golden(h, 360);
|
|
40
|
+
const hue2 = (hue1 + 30 + golden(h2, 60)) % 360;
|
|
41
|
+
const sat1 = 65 + golden(h2, 30);
|
|
42
|
+
const sat2 = 70 + golden(h3, 25);
|
|
43
|
+
const lit1 = 38 + golden(h3, 18);
|
|
44
|
+
const lit2 = 28 + golden(h2, 14);
|
|
45
|
+
const angle = golden(h, 160) + 120;
|
|
46
|
+
|
|
47
|
+
const c1 = `hsl(${n(hue1)}, ${n(sat1)}%, ${n(lit1)}%)`;
|
|
48
|
+
const c2 = `hsl(${n(hue2)}, ${n(sat2)}%, ${n(lit2)}%)`;
|
|
49
|
+
const outline = `hsl(${n(hue1)}, ${n(sat1)}%, ${n(Math.min(lit1 + 28, 72))}%)`;
|
|
50
|
+
|
|
51
|
+
return { c1, c2, angle, outline };
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
let r = $derived(size * 0.22);
|
|
55
|
+
let fontSize = $derived(size * 0.36);
|
|
56
|
+
let border = $derived(size * 0.045);
|
|
57
|
+
|
|
58
|
+
let wrapStyle = $derived(
|
|
59
|
+
`width:${n(size)}px;height:${n(size)}px;border-radius:${n(r)}px;` +
|
|
60
|
+
`background:linear-gradient(${n(palette.angle)}deg,${palette.c1},${palette.c2});` +
|
|
61
|
+
`box-shadow:0 0 0 ${n(border)}px ${palette.outline}33,0 ${n(size * 0.06)}px ${n(size * 0.18)}px ${palette.c2}66;` +
|
|
62
|
+
`font-size:${n(fontSize)}px`
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
let sheenStyle = $derived(`border-radius:${n(r)}px ${n(r)}px 60% 60%`);
|
|
66
|
+
|
|
67
|
+
let innerStyle = $derived(
|
|
68
|
+
`inset:${n(border * 0.7)}px;border-radius:${n(r * 0.75)}px;` +
|
|
69
|
+
`border:${n(border * 0.6)}px solid rgba(255,255,255,0.13)`
|
|
70
|
+
);
|
|
71
|
+
</script>
|
|
72
|
+
|
|
73
|
+
<div class="shield" style={wrapStyle} aria-hidden="true">
|
|
74
|
+
<div class="sheen" style={sheenStyle}></div>
|
|
75
|
+
<div class="inner" style={innerStyle}></div>
|
|
76
|
+
<span class="label">{display}</span>
|
|
77
|
+
</div>
|
|
78
|
+
|
|
79
|
+
<style>
|
|
80
|
+
.shield {
|
|
81
|
+
display: flex;
|
|
82
|
+
align-items: center;
|
|
83
|
+
justify-content: center;
|
|
84
|
+
position: relative;
|
|
85
|
+
overflow: hidden;
|
|
86
|
+
flex-shrink: 0;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
.sheen {
|
|
90
|
+
position: absolute;
|
|
91
|
+
top: 0;
|
|
92
|
+
left: 0;
|
|
93
|
+
right: 0;
|
|
94
|
+
height: 52%;
|
|
95
|
+
background: linear-gradient(
|
|
96
|
+
180deg,
|
|
97
|
+
rgba(255, 255, 255, 0.18) 0%,
|
|
98
|
+
rgba(255, 255, 255, 0.04) 100%
|
|
99
|
+
);
|
|
100
|
+
pointer-events: none;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
.inner {
|
|
104
|
+
position: absolute;
|
|
105
|
+
pointer-events: none;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
.label {
|
|
109
|
+
font-family: 'Inter', 'Helvetica Neue', sans-serif;
|
|
110
|
+
font-weight: 800;
|
|
111
|
+
color: rgba(255, 255, 255, 0.95);
|
|
112
|
+
letter-spacing: -0.02em;
|
|
113
|
+
line-height: 1;
|
|
114
|
+
user-select: none;
|
|
115
|
+
text-shadow: 0 1px 4px rgba(0, 0, 0, 0.35);
|
|
116
|
+
position: relative;
|
|
117
|
+
}
|
|
118
|
+
</style>
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { theme } from '../stores/theme.svelte';
|
|
3
|
+
</script>
|
|
4
|
+
|
|
5
|
+
<button
|
|
6
|
+
onclick={() => {
|
|
7
|
+
theme.toggle();
|
|
8
|
+
}}
|
|
9
|
+
class="rounded-md p-2 text-gray-600 transition-colors hover:bg-gray-100 hover:text-gray-900 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-600 dark:text-gray-400 dark:hover:bg-gray-800 dark:hover:text-white"
|
|
10
|
+
aria-label="Toggle theme"
|
|
11
|
+
>
|
|
12
|
+
<span class="sr-only" aria-live="polite">Theme: {theme.resolved}</span>
|
|
13
|
+
{#if theme.resolved === 'dark'}
|
|
14
|
+
<svg
|
|
15
|
+
class="h-5 w-5"
|
|
16
|
+
fill="none"
|
|
17
|
+
viewBox="0 0 24 24"
|
|
18
|
+
stroke="currentColor"
|
|
19
|
+
stroke-width="2"
|
|
20
|
+
aria-hidden="true"
|
|
21
|
+
>
|
|
22
|
+
<path
|
|
23
|
+
stroke-linecap="round"
|
|
24
|
+
stroke-linejoin="round"
|
|
25
|
+
d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z"
|
|
26
|
+
/>
|
|
27
|
+
</svg>
|
|
28
|
+
{:else}
|
|
29
|
+
<svg
|
|
30
|
+
class="h-5 w-5"
|
|
31
|
+
fill="none"
|
|
32
|
+
viewBox="0 0 24 24"
|
|
33
|
+
stroke="currentColor"
|
|
34
|
+
stroke-width="2"
|
|
35
|
+
aria-hidden="true"
|
|
36
|
+
>
|
|
37
|
+
<path
|
|
38
|
+
stroke-linecap="round"
|
|
39
|
+
stroke-linejoin="round"
|
|
40
|
+
d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z"
|
|
41
|
+
/>
|
|
42
|
+
</svg>
|
|
43
|
+
{/if}
|
|
44
|
+
</button>
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
interface $$__sveltets_2_IsomorphicComponent<Props extends Record<string, any> = any, Events extends Record<string, any> = any, Slots extends Record<string, any> = any, Exports = {}, Bindings = string> {
|
|
2
|
+
new (options: import('svelte').ComponentConstructorOptions<Props>): import('svelte').SvelteComponent<Props, Events, Slots> & {
|
|
3
|
+
$$bindings?: Bindings;
|
|
4
|
+
} & Exports;
|
|
5
|
+
(internal: unknown, props: {
|
|
6
|
+
$$events?: Events;
|
|
7
|
+
$$slots?: Slots;
|
|
8
|
+
}): Exports & {
|
|
9
|
+
$set?: any;
|
|
10
|
+
$on?: any;
|
|
11
|
+
};
|
|
12
|
+
z_$$bindings?: Bindings;
|
|
13
|
+
}
|
|
14
|
+
declare const ThemeToggle: $$__sveltets_2_IsomorphicComponent<Record<string, never>, {
|
|
15
|
+
[evt: string]: CustomEvent<any>;
|
|
16
|
+
}, {}, {}, string>;
|
|
17
|
+
type ThemeToggle = InstanceType<typeof ThemeToggle>;
|
|
18
|
+
export default ThemeToggle;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|