@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.
Files changed (68) hide show
  1. package/dist/__mocks__/app-environment.d.ts +4 -0
  2. package/dist/__mocks__/app-environment.js +4 -0
  3. package/dist/__mocks__/app-state.d.ts +12 -0
  4. package/dist/__mocks__/app-state.js +10 -0
  5. package/dist/adapters/index.d.ts +1 -0
  6. package/dist/adapters/index.js +1 -0
  7. package/dist/adapters/static-json.adapter.d.ts +2 -0
  8. package/dist/adapters/static-json.adapter.js +34 -0
  9. package/dist/components/BpmnDiagram.svelte +496 -0
  10. package/dist/components/BpmnDiagram.svelte.d.ts +18 -0
  11. package/dist/components/Breadcrumbs.svelte +32 -0
  12. package/dist/components/Breadcrumbs.svelte.d.ts +11 -0
  13. package/dist/components/DataStoreCard.svelte +26 -0
  14. package/dist/components/DataStoreCard.svelte.d.ts +7 -0
  15. package/dist/components/DataStoreShield.svelte +67 -0
  16. package/dist/components/DataStoreShield.svelte.d.ts +9 -0
  17. package/dist/components/DomainCard.svelte +34 -0
  18. package/dist/components/DomainCard.svelte.d.ts +9 -0
  19. package/dist/components/DomainCard.test.d.ts +1 -0
  20. package/dist/components/DomainCard.test.js +45 -0
  21. package/dist/components/Header.svelte +144 -0
  22. package/dist/components/Header.svelte.d.ts +3 -0
  23. package/dist/components/NavModeToggle.svelte +43 -0
  24. package/dist/components/NavModeToggle.svelte.d.ts +18 -0
  25. package/dist/components/NavTree.svelte +245 -0
  26. package/dist/components/NavTree.svelte.d.ts +10 -0
  27. package/dist/components/SearchModal.svelte +288 -0
  28. package/dist/components/SearchModal.svelte.d.ts +3 -0
  29. package/dist/components/ServiceCard.svelte +36 -0
  30. package/dist/components/ServiceCard.svelte.d.ts +7 -0
  31. package/dist/components/ServiceCard.test.d.ts +1 -0
  32. package/dist/components/ServiceCard.test.js +43 -0
  33. package/dist/components/ServiceGraph.svelte +437 -0
  34. package/dist/components/ServiceGraph.svelte.d.ts +10 -0
  35. package/dist/components/ServiceTypeShield.svelte +32 -0
  36. package/dist/components/ServiceTypeShield.svelte.d.ts +8 -0
  37. package/dist/components/Shield.svelte +118 -0
  38. package/dist/components/Shield.svelte.d.ts +7 -0
  39. package/dist/components/ThemeToggle.svelte +44 -0
  40. package/dist/components/ThemeToggle.svelte.d.ts +18 -0
  41. package/dist/components/ThemeToggle.test.d.ts +1 -0
  42. package/dist/components/ThemeToggle.test.js +52 -0
  43. package/dist/components/UseCaseCard.svelte +61 -0
  44. package/dist/components/UseCaseCard.svelte.d.ts +7 -0
  45. package/dist/components/UseCaseCard.test.d.ts +1 -0
  46. package/dist/components/UseCaseCard.test.js +87 -0
  47. package/dist/components/index.d.ts +15 -0
  48. package/dist/components/index.js +15 -0
  49. package/dist/index.d.ts +5 -0
  50. package/dist/index.js +5 -0
  51. package/dist/ports/catalog.port.d.ts +5 -0
  52. package/dist/ports/catalog.port.js +1 -0
  53. package/dist/ports/index.d.ts +1 -0
  54. package/dist/ports/index.js +1 -0
  55. package/dist/source.css +1 -0
  56. package/dist/stores/index.d.ts +4 -0
  57. package/dist/stores/index.js +3 -0
  58. package/dist/stores/nav-mode.svelte.d.ts +7 -0
  59. package/dist/stores/nav-mode.svelte.js +32 -0
  60. package/dist/stores/search.svelte.d.ts +9 -0
  61. package/dist/stores/search.svelte.js +14 -0
  62. package/dist/stores/theme.svelte.d.ts +8 -0
  63. package/dist/stores/theme.svelte.js +61 -0
  64. package/dist/utils/fetch-catalog.d.ts +6 -0
  65. package/dist/utils/fetch-catalog.js +26 -0
  66. package/dist/utils/index.d.ts +1 -0
  67. package/dist/utils/index.js +1 -0
  68. 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,7 @@
1
+ interface Props {
2
+ label: string;
3
+ size?: number;
4
+ }
5
+ declare const Shield: import("svelte").Component<Props, {}, "">;
6
+ type Shield = ReturnType<typeof Shield>;
7
+ export default Shield;
@@ -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 {};