@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.

Potentially problematic release.


This version of @cwygoda/service-catalog-ui might be problematic. Click here for more details.

Files changed (55) 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 +360 -0
  10. package/dist/components/BpmnDiagram.svelte.d.ts +7 -0
  11. package/dist/components/Breadcrumbs.svelte +32 -0
  12. package/dist/components/Breadcrumbs.svelte.d.ts +11 -0
  13. package/dist/components/DomainCard.svelte +29 -0
  14. package/dist/components/DomainCard.svelte.d.ts +9 -0
  15. package/dist/components/DomainCard.test.d.ts +1 -0
  16. package/dist/components/DomainCard.test.js +45 -0
  17. package/dist/components/Header.svelte +110 -0
  18. package/dist/components/Header.svelte.d.ts +3 -0
  19. package/dist/components/NavModeToggle.svelte +28 -0
  20. package/dist/components/NavModeToggle.svelte.d.ts +18 -0
  21. package/dist/components/NavTree.svelte +182 -0
  22. package/dist/components/NavTree.svelte.d.ts +9 -0
  23. package/dist/components/ServiceCard.svelte +26 -0
  24. package/dist/components/ServiceCard.svelte.d.ts +7 -0
  25. package/dist/components/ServiceCard.test.d.ts +1 -0
  26. package/dist/components/ServiceCard.test.js +36 -0
  27. package/dist/components/ServiceGraph.svelte +348 -0
  28. package/dist/components/ServiceGraph.svelte.d.ts +10 -0
  29. package/dist/components/ThemeToggle.svelte +29 -0
  30. package/dist/components/ThemeToggle.svelte.d.ts +18 -0
  31. package/dist/components/ThemeToggle.test.d.ts +1 -0
  32. package/dist/components/ThemeToggle.test.js +52 -0
  33. package/dist/components/UseCaseCard.svelte +57 -0
  34. package/dist/components/UseCaseCard.svelte.d.ts +7 -0
  35. package/dist/components/UseCaseCard.test.d.ts +1 -0
  36. package/dist/components/UseCaseCard.test.js +87 -0
  37. package/dist/components/index.d.ts +10 -0
  38. package/dist/components/index.js +10 -0
  39. package/dist/index.d.ts +5 -0
  40. package/dist/index.js +5 -0
  41. package/dist/ports/catalog.port.d.ts +5 -0
  42. package/dist/ports/catalog.port.js +1 -0
  43. package/dist/ports/index.d.ts +1 -0
  44. package/dist/ports/index.js +1 -0
  45. package/dist/stores/index.d.ts +3 -0
  46. package/dist/stores/index.js +2 -0
  47. package/dist/stores/nav-mode.svelte.d.ts +7 -0
  48. package/dist/stores/nav-mode.svelte.js +32 -0
  49. package/dist/stores/theme.svelte.d.ts +8 -0
  50. package/dist/stores/theme.svelte.js +61 -0
  51. package/dist/utils/fetch-catalog.d.ts +6 -0
  52. package/dist/utils/fetch-catalog.js +26 -0
  53. package/dist/utils/index.d.ts +1 -0
  54. package/dist/utils/index.js +1 -0
  55. package/package.json +54 -0
@@ -0,0 +1,110 @@
1
+ <script lang="ts">
2
+ import { page } from '$app/state';
3
+ import ThemeToggle from './ThemeToggle.svelte';
4
+ import NavModeToggle from './NavModeToggle.svelte';
5
+
6
+ const navLinks = [
7
+ { href: '/', label: 'Home' },
8
+ { href: '/domains', label: 'Domains' },
9
+ { href: '/use-cases', label: 'Use Cases' },
10
+ { href: '/services', label: 'Services' },
11
+ { href: '/graph', label: 'Graph' },
12
+ ];
13
+
14
+ let mobileMenuOpen = $state(false);
15
+
16
+ function isActive(href: string): boolean {
17
+ if (href === '/') {
18
+ return page.url.pathname === '/';
19
+ }
20
+ return page.url.pathname.startsWith(href);
21
+ }
22
+
23
+ function toggleMobileMenu() {
24
+ mobileMenuOpen = !mobileMenuOpen;
25
+ }
26
+
27
+ function closeMobileMenu() {
28
+ mobileMenuOpen = false;
29
+ }
30
+ </script>
31
+
32
+ <header class="border-b border-gray-200 bg-white dark:border-gray-700 dark:bg-gray-900">
33
+ <div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
34
+ <div class="flex h-16 items-center justify-between">
35
+ <div class="flex items-center">
36
+ <a href="/" class="text-xl font-bold text-gray-900 dark:text-white"> Service Catalog </a>
37
+ <!-- Desktop nav -->
38
+ <nav class="ml-10 hidden space-x-4 sm:flex">
39
+ {#each navLinks as link (link.href)}
40
+ <a
41
+ href={link.href}
42
+ class="rounded-md px-3 py-2 text-sm font-medium transition-colors {isActive(link.href)
43
+ ? 'bg-primary-100 text-primary-700 dark:bg-primary-900 dark:text-primary-200'
44
+ : '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
+ >
46
+ {link.label}
47
+ </a>
48
+ {/each}
49
+ </nav>
50
+ </div>
51
+
52
+ <div class="flex items-center gap-2">
53
+ <NavModeToggle />
54
+ <ThemeToggle />
55
+ <!-- Mobile menu button -->
56
+ <button
57
+ onclick={toggleMobileMenu}
58
+ class="rounded-md p-2 text-gray-600 transition-colors hover:bg-gray-100 hover:text-gray-900 sm:hidden dark:text-gray-400 dark:hover:bg-gray-800 dark:hover:text-white"
59
+ aria-label="Toggle menu"
60
+ aria-expanded={mobileMenuOpen}
61
+ >
62
+ {#if mobileMenuOpen}
63
+ <!-- Close icon -->
64
+ <svg
65
+ class="h-6 w-6"
66
+ fill="none"
67
+ viewBox="0 0 24 24"
68
+ stroke="currentColor"
69
+ stroke-width="2"
70
+ >
71
+ <path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
72
+ </svg>
73
+ {:else}
74
+ <!-- Hamburger icon -->
75
+ <svg
76
+ class="h-6 w-6"
77
+ fill="none"
78
+ viewBox="0 0 24 24"
79
+ stroke="currentColor"
80
+ stroke-width="2"
81
+ >
82
+ <path stroke-linecap="round" stroke-linejoin="round" d="M4 6h16M4 12h16M4 18h16" />
83
+ </svg>
84
+ {/if}
85
+ </button>
86
+ </div>
87
+ </div>
88
+ </div>
89
+
90
+ <!-- Mobile menu -->
91
+ {#if mobileMenuOpen}
92
+ <nav
93
+ class="border-t border-gray-200 bg-white px-4 py-3 sm:hidden dark:border-gray-700 dark:bg-gray-900"
94
+ >
95
+ <div class="flex flex-col space-y-1">
96
+ {#each navLinks as link (link.href)}
97
+ <a
98
+ href={link.href}
99
+ onclick={closeMobileMenu}
100
+ class="rounded-md px-3 py-2 text-base font-medium transition-colors {isActive(link.href)
101
+ ? 'bg-primary-100 text-primary-700 dark:bg-primary-900 dark:text-primary-200'
102
+ : 'text-gray-600 hover:bg-gray-100 hover:text-gray-900 dark:text-gray-300 dark:hover:bg-gray-800 dark:hover:text-white'}"
103
+ >
104
+ {link.label}
105
+ </a>
106
+ {/each}
107
+ </div>
108
+ </nav>
109
+ {/if}
110
+ </header>
@@ -0,0 +1,3 @@
1
+ declare const Header: import("svelte").Component<Record<string, never>, {}, "">;
2
+ type Header = ReturnType<typeof Header>;
3
+ export default Header;
@@ -0,0 +1,28 @@
1
+ <script lang="ts">
2
+ import { navModeStore } from '../stores/nav-mode.svelte.js';
3
+ </script>
4
+
5
+ <button
6
+ onclick={() => {
7
+ navModeStore.toggle();
8
+ }}
9
+ class="hidden rounded-md p-2 text-gray-600 transition-colors hover:bg-gray-100 hover:text-gray-900 lg:block dark:text-gray-400 dark:hover:bg-gray-800 dark:hover:text-white"
10
+ aria-label={navModeStore.mode === 'flat' ? 'Switch to tree view' : 'Switch to flat view'}
11
+ title={navModeStore.mode === 'flat' ? 'Switch to tree view' : 'Switch to flat view'}
12
+ >
13
+ {#if navModeStore.mode === 'flat'}
14
+ <!-- List icon -->
15
+ <svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
16
+ <path stroke-linecap="round" stroke-linejoin="round" d="M4 6h16M4 10h16M4 14h16M4 18h16" />
17
+ </svg>
18
+ {:else}
19
+ <!-- Tree icon -->
20
+ <svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
21
+ <path
22
+ stroke-linecap="round"
23
+ stroke-linejoin="round"
24
+ d="M3 7h2m4 0h12M3 12h2m4 0h12M3 17h2m4 0h12"
25
+ />
26
+ </svg>
27
+ {/if}
28
+ </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 NavModeToggle: $$__sveltets_2_IsomorphicComponent<Record<string, never>, {
15
+ [evt: string]: CustomEvent<any>;
16
+ }, {}, {}, string>;
17
+ type NavModeToggle = InstanceType<typeof NavModeToggle>;
18
+ export default NavModeToggle;
@@ -0,0 +1,182 @@
1
+ <script lang="ts">
2
+ import { SvelteSet } from 'svelte/reactivity';
3
+ import type { Domain, UseCase, Service } from '@cwygoda/service-catalog-core/domain';
4
+
5
+ interface Props {
6
+ domains: Domain[];
7
+ useCases: UseCase[];
8
+ services: Service[];
9
+ }
10
+
11
+ let { domains, useCases, services }: Props = $props();
12
+
13
+ // Track expanded state for domains and use cases
14
+ const expandedDomains = new SvelteSet<string>();
15
+ const expandedUseCases = new SvelteSet<string>();
16
+
17
+ function toggleDomain(id: string) {
18
+ if (expandedDomains.has(id)) {
19
+ expandedDomains.delete(id);
20
+ } else {
21
+ expandedDomains.add(id);
22
+ }
23
+ }
24
+
25
+ function toggleUseCase(id: string) {
26
+ if (expandedUseCases.has(id)) {
27
+ expandedUseCases.delete(id);
28
+ } else {
29
+ expandedUseCases.add(id);
30
+ }
31
+ }
32
+
33
+ // Get root domains (no parent)
34
+ const rootDomains = $derived(domains.filter((d) => !d.parent));
35
+
36
+ // Helper to get child domains
37
+ function getChildDomains(parentId: string): Domain[] {
38
+ return domains.filter((d) => d.parent === parentId);
39
+ }
40
+
41
+ // Helper to get use cases for a domain
42
+ function getDomainUseCases(domainId: string): UseCase[] {
43
+ return useCases.filter((uc) => uc.domain === domainId);
44
+ }
45
+
46
+ // Helper to get services for a domain
47
+ function getDomainServices(domainId: string): Service[] {
48
+ return services.filter((s) => s.domain === domainId);
49
+ }
50
+
51
+ // Helper to get services for a use case
52
+ function getUseCaseServices(useCase: UseCase): Service[] {
53
+ const serviceIds = useCase.participants.map((p) => p.service);
54
+ return services.filter((s) => serviceIds.includes(s.id));
55
+ }
56
+ </script>
57
+
58
+ <nav class="text-sm" aria-label="Catalog tree">
59
+ <ul class="space-y-1">
60
+ {#each rootDomains as domain (domain.id)}
61
+ <li>
62
+ <div class="flex items-center gap-1">
63
+ <button
64
+ type="button"
65
+ onclick={() => {
66
+ toggleDomain(domain.id);
67
+ }}
68
+ class="flex h-5 w-5 items-center justify-center rounded text-gray-500 hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-gray-700"
69
+ aria-expanded={expandedDomains.has(domain.id)}
70
+ aria-label={expandedDomains.has(domain.id) ? 'Collapse' : 'Expand'}
71
+ >
72
+ <svg
73
+ class="h-3 w-3 transition-transform {expandedDomains.has(domain.id)
74
+ ? 'rotate-90'
75
+ : ''}"
76
+ fill="currentColor"
77
+ viewBox="0 0 20 20"
78
+ >
79
+ <path
80
+ fill-rule="evenodd"
81
+ d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z"
82
+ clip-rule="evenodd"
83
+ />
84
+ </svg>
85
+ </button>
86
+ <a
87
+ href="/domains/{domain.id}"
88
+ class="flex-1 rounded px-2 py-1 font-medium text-gray-900 hover:bg-gray-100 dark:text-white dark:hover:bg-gray-700"
89
+ >
90
+ {domain.name}
91
+ </a>
92
+ </div>
93
+
94
+ {#if expandedDomains.has(domain.id)}
95
+ <ul class="ml-6 mt-1 space-y-1 border-l border-gray-200 pl-2 dark:border-gray-700">
96
+ <!-- Child Domains -->
97
+ {#each getChildDomains(domain.id) as child (child.id)}
98
+ <li>
99
+ <a
100
+ href="/domains/{child.id}"
101
+ class="block rounded px-2 py-1 text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-700"
102
+ >
103
+ {child.name}
104
+ </a>
105
+ </li>
106
+ {/each}
107
+
108
+ <!-- Use Cases -->
109
+ {#each getDomainUseCases(domain.id) as useCase (useCase.id)}
110
+ <li>
111
+ <div class="flex items-center gap-1">
112
+ {#if getUseCaseServices(useCase).length > 0}
113
+ <button
114
+ type="button"
115
+ onclick={() => {
116
+ toggleUseCase(useCase.id);
117
+ }}
118
+ class="flex h-5 w-5 items-center justify-center rounded text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700"
119
+ aria-expanded={expandedUseCases.has(useCase.id)}
120
+ aria-label={expandedUseCases.has(useCase.id) ? 'Collapse' : 'Expand'}
121
+ >
122
+ <svg
123
+ class="h-3 w-3 transition-transform {expandedUseCases.has(useCase.id)
124
+ ? 'rotate-90'
125
+ : ''}"
126
+ fill="currentColor"
127
+ viewBox="0 0 20 20"
128
+ >
129
+ <path
130
+ fill-rule="evenodd"
131
+ d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z"
132
+ clip-rule="evenodd"
133
+ />
134
+ </svg>
135
+ </button>
136
+ {:else}
137
+ <span class="w-5"></span>
138
+ {/if}
139
+ <a
140
+ href="/use-cases/{useCase.id}"
141
+ class="flex-1 rounded px-2 py-1 text-primary-600 hover:bg-gray-100 dark:text-primary-400 dark:hover:bg-gray-700"
142
+ >
143
+ {useCase.name}
144
+ </a>
145
+ </div>
146
+
147
+ {#if expandedUseCases.has(useCase.id)}
148
+ <ul
149
+ class="ml-6 mt-1 space-y-1 border-l border-gray-200 pl-2 dark:border-gray-700"
150
+ >
151
+ {#each getUseCaseServices(useCase) as service (service.id)}
152
+ <li>
153
+ <a
154
+ href="/services/{service.id}"
155
+ class="block rounded px-2 py-1 text-gray-600 hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-gray-700"
156
+ >
157
+ {service.name}
158
+ </a>
159
+ </li>
160
+ {/each}
161
+ </ul>
162
+ {/if}
163
+ </li>
164
+ {/each}
165
+
166
+ <!-- Services directly in domain (not via use case) -->
167
+ {#each getDomainServices(domain.id) as service (service.id)}
168
+ <li>
169
+ <a
170
+ href="/services/{service.id}"
171
+ class="ml-5 block rounded px-2 py-1 text-gray-600 hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-gray-700"
172
+ >
173
+ {service.name}
174
+ </a>
175
+ </li>
176
+ {/each}
177
+ </ul>
178
+ {/if}
179
+ </li>
180
+ {/each}
181
+ </ul>
182
+ </nav>
@@ -0,0 +1,9 @@
1
+ import type { Domain, UseCase, Service } from '@cwygoda/service-catalog-core/domain';
2
+ interface Props {
3
+ domains: Domain[];
4
+ useCases: UseCase[];
5
+ services: Service[];
6
+ }
7
+ declare const NavTree: import("svelte").Component<Props, {}, "">;
8
+ type NavTree = ReturnType<typeof NavTree>;
9
+ export default NavTree;
@@ -0,0 +1,26 @@
1
+ <script lang="ts">
2
+ import type { Service } from '@cwygoda/service-catalog-core/domain';
3
+
4
+ let { service }: { service: Service } = $props();
5
+ </script>
6
+
7
+ <a
8
+ href="/services/{service.id}"
9
+ 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
+ <div class="mb-2 flex items-center justify-between">
12
+ <h3 class="text-lg font-semibold text-gray-900 dark:text-white">
13
+ {service.name}
14
+ </h3>
15
+ {#if service.metadata?.version}
16
+ <span
17
+ 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"
18
+ >
19
+ v{service.metadata.version}
20
+ </span>
21
+ {/if}
22
+ </div>
23
+ <p class="text-sm text-gray-600 dark:text-gray-400">
24
+ {service.description}
25
+ </p>
26
+ </a>
@@ -0,0 +1,7 @@
1
+ import type { Service } from '@cwygoda/service-catalog-core/domain';
2
+ type $$ComponentProps = {
3
+ service: Service;
4
+ };
5
+ declare const ServiceCard: import("svelte").Component<$$ComponentProps, {}, "">;
6
+ type ServiceCard = ReturnType<typeof ServiceCard>;
7
+ export default ServiceCard;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,36 @@
1
+ /// <reference types="@testing-library/jest-dom" />
2
+ import { render, screen } from '@testing-library/svelte';
3
+ import { describe, expect, it } from 'vitest';
4
+ import ServiceCard from './ServiceCard.svelte';
5
+ describe('ServiceCard', () => {
6
+ const baseService = {
7
+ id: 'test-service',
8
+ name: 'Test Service',
9
+ description: 'A test service description',
10
+ };
11
+ it('renders service name', () => {
12
+ render(ServiceCard, { props: { service: baseService } });
13
+ expect(screen.getByText('Test Service')).toBeInTheDocument();
14
+ });
15
+ it('renders service description', () => {
16
+ render(ServiceCard, { props: { service: baseService } });
17
+ expect(screen.getByText('A test service description')).toBeInTheDocument();
18
+ });
19
+ it('links to service detail page', () => {
20
+ render(ServiceCard, { props: { service: baseService } });
21
+ const link = screen.getByRole('link');
22
+ expect(link).toHaveAttribute('href', '/services/test-service');
23
+ });
24
+ it('shows version badge when metadata has version', () => {
25
+ const serviceWithVersion = {
26
+ ...baseService,
27
+ metadata: { version: '1.2.3' },
28
+ };
29
+ render(ServiceCard, { props: { service: serviceWithVersion } });
30
+ expect(screen.getByText('v1.2.3')).toBeInTheDocument();
31
+ });
32
+ it('does not show version badge when no metadata', () => {
33
+ render(ServiceCard, { props: { service: baseService } });
34
+ expect(screen.queryByText(/^v\d/)).not.toBeInTheDocument();
35
+ });
36
+ });