@cwygoda/service-catalog-ui 1.0.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 +360 -0
- package/dist/components/BpmnDiagram.svelte.d.ts +7 -0
- package/dist/components/Breadcrumbs.svelte +32 -0
- package/dist/components/Breadcrumbs.svelte.d.ts +11 -0
- package/dist/components/DomainCard.svelte +29 -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 +110 -0
- package/dist/components/Header.svelte.d.ts +3 -0
- package/dist/components/NavModeToggle.svelte +28 -0
- package/dist/components/NavModeToggle.svelte.d.ts +18 -0
- package/dist/components/NavTree.svelte +182 -0
- package/dist/components/NavTree.svelte.d.ts +9 -0
- package/dist/components/ServiceCard.svelte +26 -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 +36 -0
- package/dist/components/ServiceGraph.svelte +348 -0
- package/dist/components/ServiceGraph.svelte.d.ts +10 -0
- package/dist/components/ThemeToggle.svelte +29 -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 +57 -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 +10 -0
- package/dist/components/index.js +10 -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/stores/index.d.ts +3 -0
- package/dist/stores/index.js +2 -0
- package/dist/stores/nav-mode.svelte.d.ts +7 -0
- package/dist/stores/nav-mode.svelte.js +32 -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 +54 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { createStaticJsonAdapter } from './static-json.adapter.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { createStaticJsonAdapter } from './static-json.adapter.js';
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { Value } from '@sinclair/typebox/value';
|
|
2
|
+
import { findService } from '@cwygoda/service-catalog-core/domain';
|
|
3
|
+
import { CatalogSchema } from '@cwygoda/service-catalog-core/schemas';
|
|
4
|
+
const CACHE_TTL_MS = 5 * 60 * 1000; // 5 minutes
|
|
5
|
+
export function createStaticJsonAdapter(baseUrl = '') {
|
|
6
|
+
let cache = null;
|
|
7
|
+
async function fetchCatalog() {
|
|
8
|
+
const now = Date.now();
|
|
9
|
+
if (cache && now - cache.timestamp < CACHE_TTL_MS) {
|
|
10
|
+
return cache.data;
|
|
11
|
+
}
|
|
12
|
+
const response = await fetch(`${baseUrl}/catalog.json`);
|
|
13
|
+
if (!response.ok) {
|
|
14
|
+
throw new Error(`Failed to fetch catalog: ${String(response.status)}`);
|
|
15
|
+
}
|
|
16
|
+
const data = await response.json();
|
|
17
|
+
if (!Value.Check(CatalogSchema, data)) {
|
|
18
|
+
const errors = [...Value.Errors(CatalogSchema, data)];
|
|
19
|
+
const message = errors.map((e) => `${e.path}: ${e.message}`).join('; ');
|
|
20
|
+
throw new Error(`Invalid catalog data: ${message}`);
|
|
21
|
+
}
|
|
22
|
+
cache = { data: data, timestamp: now };
|
|
23
|
+
return cache.data;
|
|
24
|
+
}
|
|
25
|
+
return {
|
|
26
|
+
async getCatalog() {
|
|
27
|
+
return fetchCatalog();
|
|
28
|
+
},
|
|
29
|
+
async getService(id) {
|
|
30
|
+
const catalog = await fetchCatalog();
|
|
31
|
+
return findService(catalog, id);
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
}
|
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { onMount, onDestroy } from 'svelte';
|
|
3
|
+
import { browser } from '$app/environment';
|
|
4
|
+
|
|
5
|
+
interface Props {
|
|
6
|
+
xml: string;
|
|
7
|
+
interactive?: boolean;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
interface Bounds {
|
|
11
|
+
x: number;
|
|
12
|
+
y: number;
|
|
13
|
+
width: number;
|
|
14
|
+
height: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface BpmnCanvas {
|
|
18
|
+
zoom(level: 'fit-viewport' | number): void;
|
|
19
|
+
zoom(): number; // get current zoom
|
|
20
|
+
viewbox(): Bounds & { scale: number; inner: Bounds; outer: Bounds };
|
|
21
|
+
viewbox(bounds: Partial<Bounds>): void;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const FIT_PADDING = 40; // pixels of padding around diagram when fitting
|
|
25
|
+
|
|
26
|
+
interface BpmnViewer {
|
|
27
|
+
importXML: (xml: string) => Promise<unknown>;
|
|
28
|
+
get: (name: string) => unknown;
|
|
29
|
+
destroy?: () => void;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
let { xml, interactive = false }: Props = $props();
|
|
33
|
+
|
|
34
|
+
let wrapper: HTMLDivElement;
|
|
35
|
+
let container: HTMLDivElement;
|
|
36
|
+
let viewer: BpmnViewer | null = $state(null);
|
|
37
|
+
let error: string | null = $state(null);
|
|
38
|
+
let ready = $state(false);
|
|
39
|
+
|
|
40
|
+
// Fullscreen state
|
|
41
|
+
let isFullscreen = $state(false);
|
|
42
|
+
|
|
43
|
+
function toggleFullscreen() {
|
|
44
|
+
if (!browser) return;
|
|
45
|
+
if (!document.fullscreenElement) {
|
|
46
|
+
void wrapper.requestFullscreen();
|
|
47
|
+
} else {
|
|
48
|
+
void document.exitFullscreen();
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function handleFullscreenChange() {
|
|
53
|
+
isFullscreen = !!document.fullscreenElement;
|
|
54
|
+
// Re-fit diagram when entering/exiting fullscreen
|
|
55
|
+
if (ready) {
|
|
56
|
+
setTimeout(fitWithPadding, 100);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function getCanvas(): BpmnCanvas | null {
|
|
61
|
+
if (!viewer) return null;
|
|
62
|
+
return viewer.get('canvas') as BpmnCanvas;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function fitWithPadding() {
|
|
66
|
+
const canvas = getCanvas();
|
|
67
|
+
if (!canvas) return;
|
|
68
|
+
|
|
69
|
+
// First fit to viewport to get the content bounds
|
|
70
|
+
canvas.zoom('fit-viewport');
|
|
71
|
+
|
|
72
|
+
// Get current viewbox info - inner is the diagram bounds, outer is container
|
|
73
|
+
const vb = canvas.viewbox();
|
|
74
|
+
const inner = vb.inner;
|
|
75
|
+
const outer = vb.outer;
|
|
76
|
+
|
|
77
|
+
// Calculate scale to fit content with padding
|
|
78
|
+
const availableWidth = outer.width - FIT_PADDING * 2;
|
|
79
|
+
const availableHeight = outer.height - FIT_PADDING * 2;
|
|
80
|
+
const scaleX = availableWidth / inner.width;
|
|
81
|
+
const scaleY = availableHeight / inner.height;
|
|
82
|
+
const scale = Math.min(scaleX, scaleY);
|
|
83
|
+
|
|
84
|
+
// Calculate centered viewbox dimensions
|
|
85
|
+
const viewWidth = outer.width / scale;
|
|
86
|
+
const viewHeight = outer.height / scale;
|
|
87
|
+
|
|
88
|
+
// Center the content
|
|
89
|
+
const centerX = inner.x + inner.width / 2;
|
|
90
|
+
const centerY = inner.y + inner.height / 2;
|
|
91
|
+
|
|
92
|
+
canvas.viewbox({
|
|
93
|
+
x: centerX - viewWidth / 2,
|
|
94
|
+
y: centerY - viewHeight / 2,
|
|
95
|
+
width: viewWidth,
|
|
96
|
+
height: viewHeight,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function zoomIn() {
|
|
101
|
+
const canvas = getCanvas();
|
|
102
|
+
if (!canvas) return;
|
|
103
|
+
const current = canvas.zoom();
|
|
104
|
+
canvas.zoom(current * 1.25);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function zoomOut() {
|
|
108
|
+
const canvas = getCanvas();
|
|
109
|
+
if (!canvas) return;
|
|
110
|
+
const current = canvas.zoom();
|
|
111
|
+
canvas.zoom(current * 0.8);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function resetZoom() {
|
|
115
|
+
fitWithPadding();
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
onMount(async () => {
|
|
119
|
+
if (!browser || !xml) return;
|
|
120
|
+
|
|
121
|
+
document.addEventListener('fullscreenchange', handleFullscreenChange);
|
|
122
|
+
|
|
123
|
+
try {
|
|
124
|
+
// Dynamic import to avoid SSR issues
|
|
125
|
+
const { default: BpmnViewer } = await import('bpmn-js/lib/NavigatedViewer');
|
|
126
|
+
|
|
127
|
+
viewer = new BpmnViewer({
|
|
128
|
+
container,
|
|
129
|
+
keyboard: { bindTo: interactive ? document : undefined },
|
|
130
|
+
}) as BpmnViewer;
|
|
131
|
+
|
|
132
|
+
await viewer.importXML(xml);
|
|
133
|
+
|
|
134
|
+
// Fit diagram to container with padding
|
|
135
|
+
fitWithPadding();
|
|
136
|
+
ready = true;
|
|
137
|
+
} catch (e) {
|
|
138
|
+
error = e instanceof Error ? e.message : 'Failed to render BPMN diagram';
|
|
139
|
+
console.error('BPMN render error:', e);
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
onDestroy(() => {
|
|
144
|
+
if (browser) {
|
|
145
|
+
document.removeEventListener('fullscreenchange', handleFullscreenChange);
|
|
146
|
+
}
|
|
147
|
+
if (viewer?.destroy) {
|
|
148
|
+
viewer.destroy();
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
$effect(() => {
|
|
153
|
+
// Re-import when xml changes
|
|
154
|
+
if (browser && viewer && xml) {
|
|
155
|
+
viewer
|
|
156
|
+
.importXML(xml)
|
|
157
|
+
.then(() => {
|
|
158
|
+
fitWithPadding();
|
|
159
|
+
})
|
|
160
|
+
.catch((e: unknown) => {
|
|
161
|
+
error = e instanceof Error ? e.message : 'Failed to update diagram';
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
</script>
|
|
166
|
+
|
|
167
|
+
{#if error}
|
|
168
|
+
<div
|
|
169
|
+
class="rounded-lg border border-red-200 bg-red-50 p-4 text-red-700 dark:border-red-800 dark:bg-red-900/20 dark:text-red-400"
|
|
170
|
+
>
|
|
171
|
+
<p class="font-medium">Failed to render diagram</p>
|
|
172
|
+
<p class="mt-1 text-sm">{error}</p>
|
|
173
|
+
</div>
|
|
174
|
+
{:else}
|
|
175
|
+
<div
|
|
176
|
+
bind:this={wrapper}
|
|
177
|
+
class="bpmn-wrapper relative {isFullscreen ? 'bg-white dark:bg-gray-900' : ''}"
|
|
178
|
+
>
|
|
179
|
+
<div
|
|
180
|
+
bind:this={container}
|
|
181
|
+
class="bpmn-container w-full rounded-lg border border-gray-200 bg-white dark:border-gray-700 dark:bg-gray-800"
|
|
182
|
+
class:cursor-grab={interactive}
|
|
183
|
+
class:h-96={!isFullscreen}
|
|
184
|
+
class:h-screen={isFullscreen}
|
|
185
|
+
></div>
|
|
186
|
+
|
|
187
|
+
<!-- Zoom controls -->
|
|
188
|
+
{#if ready}
|
|
189
|
+
<div
|
|
190
|
+
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"
|
|
191
|
+
>
|
|
192
|
+
<button
|
|
193
|
+
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"
|
|
195
|
+
aria-label="Zoom in"
|
|
196
|
+
title="Zoom in"
|
|
197
|
+
>
|
|
198
|
+
<svg
|
|
199
|
+
class="h-4 w-4"
|
|
200
|
+
fill="none"
|
|
201
|
+
viewBox="0 0 24 24"
|
|
202
|
+
stroke="currentColor"
|
|
203
|
+
stroke-width="2"
|
|
204
|
+
>
|
|
205
|
+
<path stroke-linecap="round" stroke-linejoin="round" d="M12 4v16m8-8H4" />
|
|
206
|
+
</svg>
|
|
207
|
+
</button>
|
|
208
|
+
<button
|
|
209
|
+
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"
|
|
211
|
+
aria-label="Zoom out"
|
|
212
|
+
title="Zoom out"
|
|
213
|
+
>
|
|
214
|
+
<svg
|
|
215
|
+
class="h-4 w-4"
|
|
216
|
+
fill="none"
|
|
217
|
+
viewBox="0 0 24 24"
|
|
218
|
+
stroke="currentColor"
|
|
219
|
+
stroke-width="2"
|
|
220
|
+
>
|
|
221
|
+
<path stroke-linecap="round" stroke-linejoin="round" d="M20 12H4" />
|
|
222
|
+
</svg>
|
|
223
|
+
</button>
|
|
224
|
+
<div class="my-0.5 border-t border-gray-200 dark:border-gray-600"></div>
|
|
225
|
+
<button
|
|
226
|
+
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"
|
|
228
|
+
aria-label="Fit to view"
|
|
229
|
+
title="Fit to view"
|
|
230
|
+
>
|
|
231
|
+
<!-- Viewfinder/target icon for fit-to-view -->
|
|
232
|
+
<svg
|
|
233
|
+
class="h-4 w-4"
|
|
234
|
+
fill="none"
|
|
235
|
+
viewBox="0 0 24 24"
|
|
236
|
+
stroke="currentColor"
|
|
237
|
+
stroke-width="2"
|
|
238
|
+
>
|
|
239
|
+
<path
|
|
240
|
+
stroke-linecap="round"
|
|
241
|
+
stroke-linejoin="round"
|
|
242
|
+
d="M7.5 3.75H6A2.25 2.25 0 003.75 6v1.5M16.5 3.75H18A2.25 2.25 0 0120.25 6v1.5m0 9V18A2.25 2.25 0 0118 20.25h-1.5m-9 0H6A2.25 2.25 0 013.75 18v-1.5M15 12a3 3 0 11-6 0 3 3 0 016 0z"
|
|
243
|
+
/>
|
|
244
|
+
</svg>
|
|
245
|
+
</button>
|
|
246
|
+
<div class="my-0.5 border-t border-gray-200 dark:border-gray-600"></div>
|
|
247
|
+
<button
|
|
248
|
+
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"
|
|
250
|
+
aria-label={isFullscreen ? 'Exit fullscreen' : 'Enter fullscreen'}
|
|
251
|
+
title={isFullscreen ? 'Exit fullscreen' : 'Fullscreen'}
|
|
252
|
+
>
|
|
253
|
+
{#if isFullscreen}
|
|
254
|
+
<svg
|
|
255
|
+
class="h-4 w-4"
|
|
256
|
+
fill="none"
|
|
257
|
+
viewBox="0 0 24 24"
|
|
258
|
+
stroke="currentColor"
|
|
259
|
+
stroke-width="2"
|
|
260
|
+
>
|
|
261
|
+
<path
|
|
262
|
+
stroke-linecap="round"
|
|
263
|
+
stroke-linejoin="round"
|
|
264
|
+
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"
|
|
265
|
+
/>
|
|
266
|
+
</svg>
|
|
267
|
+
{:else}
|
|
268
|
+
<svg
|
|
269
|
+
class="h-4 w-4"
|
|
270
|
+
fill="none"
|
|
271
|
+
viewBox="0 0 24 24"
|
|
272
|
+
stroke="currentColor"
|
|
273
|
+
stroke-width="2"
|
|
274
|
+
>
|
|
275
|
+
<path
|
|
276
|
+
stroke-linecap="round"
|
|
277
|
+
stroke-linejoin="round"
|
|
278
|
+
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"
|
|
279
|
+
/>
|
|
280
|
+
</svg>
|
|
281
|
+
{/if}
|
|
282
|
+
</button>
|
|
283
|
+
</div>
|
|
284
|
+
{/if}
|
|
285
|
+
</div>
|
|
286
|
+
{/if}
|
|
287
|
+
|
|
288
|
+
<style>
|
|
289
|
+
.bpmn-container :global(.djs-container) {
|
|
290
|
+
height: 100% !important;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/* Dark mode styles for BPMN diagram */
|
|
294
|
+
:global(.dark) .bpmn-container :global(.djs-visual) {
|
|
295
|
+
/* Invert strokes: black -> light gray */
|
|
296
|
+
--bpmn-stroke: #d1d5db;
|
|
297
|
+
--bpmn-fill: #374151;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/* Participant/Pool borders and labels */
|
|
301
|
+
:global(.dark) .bpmn-container :global(.djs-group .djs-visual > rect) {
|
|
302
|
+
stroke: #9ca3af !important;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
:global(.dark) .bpmn-container :global(.djs-group .djs-visual > path) {
|
|
306
|
+
stroke: #9ca3af !important;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/* Task boxes */
|
|
310
|
+
:global(.dark) .bpmn-container :global(.djs-shape .djs-visual > rect) {
|
|
311
|
+
stroke: #9ca3af !important;
|
|
312
|
+
fill: #374151 !important;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/* Events (circles) */
|
|
316
|
+
:global(.dark) .bpmn-container :global(.djs-shape .djs-visual > circle) {
|
|
317
|
+
stroke: #9ca3af !important;
|
|
318
|
+
fill: #374151 !important;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/* Gateways (diamonds) */
|
|
322
|
+
:global(.dark) .bpmn-container :global(.djs-shape .djs-visual > polygon) {
|
|
323
|
+
stroke: #9ca3af !important;
|
|
324
|
+
fill: #374151 !important;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/* Sequence flows (solid lines) */
|
|
328
|
+
:global(.dark) .bpmn-container :global(.djs-connection .djs-visual > path) {
|
|
329
|
+
stroke: #9ca3af !important;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/* Message flows (dashed lines) */
|
|
333
|
+
:global(.dark) .bpmn-container :global(.djs-connection .djs-visual > polyline) {
|
|
334
|
+
stroke: #9ca3af !important;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/* Arrow markers */
|
|
338
|
+
:global(.dark) .bpmn-container :global(marker path) {
|
|
339
|
+
fill: #9ca3af !important;
|
|
340
|
+
stroke: #9ca3af !important;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/* Text labels */
|
|
344
|
+
:global(.dark) .bpmn-container :global(.djs-label text),
|
|
345
|
+
:global(.dark) .bpmn-container :global(.djs-visual text),
|
|
346
|
+
:global(.dark) .bpmn-container :global(text) {
|
|
347
|
+
fill: #e5e7eb !important;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/* Participant header background (the title band) */
|
|
351
|
+
:global(.dark) .bpmn-container :global(.djs-visual > rect:first-child) {
|
|
352
|
+
fill: #1f2937 !important;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/* bpmn.io logo/watermark in corner */
|
|
356
|
+
:global(.dark) .bpmn-container :global(.bjs-powered-by) {
|
|
357
|
+
filter: invert(1) hue-rotate(180deg);
|
|
358
|
+
opacity: 0.7;
|
|
359
|
+
}
|
|
360
|
+
</style>
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
interface BreadcrumbItem {
|
|
3
|
+
label: string;
|
|
4
|
+
href: string;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
interface Props {
|
|
8
|
+
items: BreadcrumbItem[];
|
|
9
|
+
current: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
let { items, current }: Props = $props();
|
|
13
|
+
</script>
|
|
14
|
+
|
|
15
|
+
<nav aria-label="Breadcrumb" class="mb-6">
|
|
16
|
+
<ol class="flex flex-wrap items-center gap-1 text-sm">
|
|
17
|
+
{#each items as item (item.href)}
|
|
18
|
+
<li class="flex items-center">
|
|
19
|
+
<a
|
|
20
|
+
href={item.href}
|
|
21
|
+
class="text-primary-600 hover:text-primary-800 hover:underline dark:text-primary-400 dark:hover:text-primary-200"
|
|
22
|
+
>
|
|
23
|
+
{item.label}
|
|
24
|
+
</a>
|
|
25
|
+
<span class="mx-2 text-gray-400" aria-hidden="true">/</span>
|
|
26
|
+
</li>
|
|
27
|
+
{/each}
|
|
28
|
+
<li class="text-gray-600 dark:text-gray-400" aria-current="page">
|
|
29
|
+
{current}
|
|
30
|
+
</li>
|
|
31
|
+
</ol>
|
|
32
|
+
</nav>
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
interface BreadcrumbItem {
|
|
2
|
+
label: string;
|
|
3
|
+
href: string;
|
|
4
|
+
}
|
|
5
|
+
interface Props {
|
|
6
|
+
items: BreadcrumbItem[];
|
|
7
|
+
current: string;
|
|
8
|
+
}
|
|
9
|
+
declare const Breadcrumbs: import("svelte").Component<Props, {}, "">;
|
|
10
|
+
type Breadcrumbs = ReturnType<typeof Breadcrumbs>;
|
|
11
|
+
export default Breadcrumbs;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Domain } from '@cwygoda/service-catalog-core/domain';
|
|
3
|
+
|
|
4
|
+
interface Props {
|
|
5
|
+
domain: Domain;
|
|
6
|
+
useCaseCount: number;
|
|
7
|
+
serviceCount: number;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
let { domain, useCaseCount, serviceCount }: Props = $props();
|
|
11
|
+
</script>
|
|
12
|
+
|
|
13
|
+
<a
|
|
14
|
+
href="/domains/{domain.id}"
|
|
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"
|
|
16
|
+
>
|
|
17
|
+
<div class="mb-2">
|
|
18
|
+
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">
|
|
19
|
+
{domain.name}
|
|
20
|
+
</h3>
|
|
21
|
+
</div>
|
|
22
|
+
<p class="mb-4 text-sm text-gray-600 dark:text-gray-400">
|
|
23
|
+
{domain.description}
|
|
24
|
+
</p>
|
|
25
|
+
<div class="flex gap-4 text-xs text-gray-500 dark:text-gray-400">
|
|
26
|
+
<span>{useCaseCount} use case{useCaseCount !== 1 ? 's' : ''}</span>
|
|
27
|
+
<span>{serviceCount} service{serviceCount !== 1 ? 's' : ''}</span>
|
|
28
|
+
</div>
|
|
29
|
+
</a>
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { Domain } from '@cwygoda/service-catalog-core/domain';
|
|
2
|
+
interface Props {
|
|
3
|
+
domain: Domain;
|
|
4
|
+
useCaseCount: number;
|
|
5
|
+
serviceCount: number;
|
|
6
|
+
}
|
|
7
|
+
declare const DomainCard: import("svelte").Component<Props, {}, "">;
|
|
8
|
+
type DomainCard = ReturnType<typeof DomainCard>;
|
|
9
|
+
export default DomainCard;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/// <reference types="@testing-library/jest-dom" />
|
|
2
|
+
import { render, screen } from '@testing-library/svelte';
|
|
3
|
+
import { describe, expect, it } from 'vitest';
|
|
4
|
+
import DomainCard from './DomainCard.svelte';
|
|
5
|
+
describe('DomainCard', () => {
|
|
6
|
+
const baseDomain = {
|
|
7
|
+
id: 'commerce',
|
|
8
|
+
name: 'Commerce',
|
|
9
|
+
description: 'E-commerce domain',
|
|
10
|
+
};
|
|
11
|
+
it('renders domain name', () => {
|
|
12
|
+
render(DomainCard, { props: { domain: baseDomain, useCaseCount: 2, serviceCount: 3 } });
|
|
13
|
+
expect(screen.getByText('Commerce')).toBeInTheDocument();
|
|
14
|
+
});
|
|
15
|
+
it('renders domain description', () => {
|
|
16
|
+
render(DomainCard, { props: { domain: baseDomain, useCaseCount: 2, serviceCount: 3 } });
|
|
17
|
+
expect(screen.getByText('E-commerce domain')).toBeInTheDocument();
|
|
18
|
+
});
|
|
19
|
+
it('links to domain detail page', () => {
|
|
20
|
+
render(DomainCard, { props: { domain: baseDomain, useCaseCount: 2, serviceCount: 3 } });
|
|
21
|
+
const link = screen.getByRole('link');
|
|
22
|
+
expect(link).toHaveAttribute('href', '/domains/commerce');
|
|
23
|
+
});
|
|
24
|
+
it('shows use case count', () => {
|
|
25
|
+
render(DomainCard, { props: { domain: baseDomain, useCaseCount: 2, serviceCount: 3 } });
|
|
26
|
+
expect(screen.getByText('2 use cases')).toBeInTheDocument();
|
|
27
|
+
});
|
|
28
|
+
it('shows singular use case for count of 1', () => {
|
|
29
|
+
render(DomainCard, { props: { domain: baseDomain, useCaseCount: 1, serviceCount: 3 } });
|
|
30
|
+
expect(screen.getByText('1 use case')).toBeInTheDocument();
|
|
31
|
+
});
|
|
32
|
+
it('shows service count', () => {
|
|
33
|
+
render(DomainCard, { props: { domain: baseDomain, useCaseCount: 2, serviceCount: 3 } });
|
|
34
|
+
expect(screen.getByText('3 services')).toBeInTheDocument();
|
|
35
|
+
});
|
|
36
|
+
it('shows singular service for count of 1', () => {
|
|
37
|
+
render(DomainCard, { props: { domain: baseDomain, useCaseCount: 2, serviceCount: 1 } });
|
|
38
|
+
expect(screen.getByText('1 service')).toBeInTheDocument();
|
|
39
|
+
});
|
|
40
|
+
it('shows zero counts correctly', () => {
|
|
41
|
+
render(DomainCard, { props: { domain: baseDomain, useCaseCount: 0, serviceCount: 0 } });
|
|
42
|
+
expect(screen.getByText('0 use cases')).toBeInTheDocument();
|
|
43
|
+
expect(screen.getByText('0 services')).toBeInTheDocument();
|
|
44
|
+
});
|
|
45
|
+
});
|