@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,52 @@
|
|
|
1
|
+
/// <reference types="@testing-library/jest-dom" />
|
|
2
|
+
import { render, screen, cleanup } from '@testing-library/svelte';
|
|
3
|
+
import { userEvent } from '@testing-library/user-event';
|
|
4
|
+
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
5
|
+
import ThemeToggle from './ThemeToggle.svelte';
|
|
6
|
+
describe('ThemeToggle', () => {
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
// Clear localStorage and reset document
|
|
9
|
+
localStorage.clear();
|
|
10
|
+
document.documentElement.classList.remove('dark');
|
|
11
|
+
});
|
|
12
|
+
afterEach(() => {
|
|
13
|
+
cleanup();
|
|
14
|
+
});
|
|
15
|
+
it('renders toggle button', () => {
|
|
16
|
+
render(ThemeToggle);
|
|
17
|
+
expect(screen.getByRole('button', { name: 'Toggle theme' })).toBeInTheDocument();
|
|
18
|
+
});
|
|
19
|
+
it('toggles theme on click', async () => {
|
|
20
|
+
const user = userEvent.setup();
|
|
21
|
+
render(ThemeToggle);
|
|
22
|
+
const button = screen.getByRole('button', { name: 'Toggle theme' });
|
|
23
|
+
const initialDark = document.documentElement.classList.contains('dark');
|
|
24
|
+
await user.click(button);
|
|
25
|
+
// Theme should be toggled from initial state
|
|
26
|
+
const afterClick = document.documentElement.classList.contains('dark');
|
|
27
|
+
expect(afterClick).not.toBe(initialDark);
|
|
28
|
+
});
|
|
29
|
+
it('toggles theme back on second click', async () => {
|
|
30
|
+
const user = userEvent.setup();
|
|
31
|
+
render(ThemeToggle);
|
|
32
|
+
const button = screen.getByRole('button', { name: 'Toggle theme' });
|
|
33
|
+
// Get state after first click
|
|
34
|
+
await user.click(button);
|
|
35
|
+
const afterFirstClick = document.documentElement.classList.contains('dark');
|
|
36
|
+
// Click again
|
|
37
|
+
await user.click(button);
|
|
38
|
+
const afterSecondClick = document.documentElement.classList.contains('dark');
|
|
39
|
+
// Should be opposite of after first click
|
|
40
|
+
expect(afterSecondClick).not.toBe(afterFirstClick);
|
|
41
|
+
});
|
|
42
|
+
it('persists theme to localStorage', async () => {
|
|
43
|
+
const user = userEvent.setup();
|
|
44
|
+
render(ThemeToggle);
|
|
45
|
+
const button = screen.getByRole('button', { name: 'Toggle theme' });
|
|
46
|
+
await user.click(button);
|
|
47
|
+
// Should have persisted something to localStorage
|
|
48
|
+
const stored = localStorage.getItem('theme');
|
|
49
|
+
expect(stored).toBeTruthy();
|
|
50
|
+
expect(['light', 'dark']).toContain(stored);
|
|
51
|
+
});
|
|
52
|
+
});
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { UseCase } from '@cwygoda/service-catalog-core/domain';
|
|
3
|
+
import Shield from './Shield.svelte';
|
|
4
|
+
|
|
5
|
+
let { useCase }: { useCase: UseCase } = $props();
|
|
6
|
+
|
|
7
|
+
function truncate(text: string, maxLength: number): string {
|
|
8
|
+
if (text.length <= maxLength) return text;
|
|
9
|
+
return text.slice(0, maxLength).trimEnd() + '...';
|
|
10
|
+
}
|
|
11
|
+
</script>
|
|
12
|
+
|
|
13
|
+
<a
|
|
14
|
+
href="/use-cases/{useCase.id}"
|
|
15
|
+
aria-label="View {useCase.name} use case"
|
|
16
|
+
class="block rounded-lg border border-gray-200 bg-white p-6 shadow-sm transition-shadow hover:shadow-md active:shadow-sm dark:border-gray-700 dark:bg-gray-800"
|
|
17
|
+
>
|
|
18
|
+
<div class="mb-3 flex items-start gap-4">
|
|
19
|
+
<Shield label={useCase.name} size={44} />
|
|
20
|
+
<div class="min-w-0 flex-1">
|
|
21
|
+
<h3 class="truncate text-lg font-semibold text-gray-900 dark:text-white">
|
|
22
|
+
{useCase.name}
|
|
23
|
+
</h3>
|
|
24
|
+
<p class="mt-1 text-sm text-gray-600 dark:text-gray-400">
|
|
25
|
+
{truncate(useCase.description, 120)}
|
|
26
|
+
</p>
|
|
27
|
+
</div>
|
|
28
|
+
</div>
|
|
29
|
+
|
|
30
|
+
<div class="flex items-center gap-4 text-xs text-gray-500 dark:text-gray-400">
|
|
31
|
+
<span class="flex items-center gap-1">
|
|
32
|
+
<svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
33
|
+
<path
|
|
34
|
+
stroke-linecap="round"
|
|
35
|
+
stroke-linejoin="round"
|
|
36
|
+
stroke-width="2"
|
|
37
|
+
d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z"
|
|
38
|
+
/>
|
|
39
|
+
</svg>
|
|
40
|
+
{useCase.participants.length} participant{useCase.participants.length !== 1 ? 's' : ''}
|
|
41
|
+
</span>
|
|
42
|
+
<span class="flex items-center gap-1">
|
|
43
|
+
<svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
44
|
+
<path
|
|
45
|
+
stroke-linecap="round"
|
|
46
|
+
stroke-linejoin="round"
|
|
47
|
+
stroke-width="2"
|
|
48
|
+
d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-3 7h3m-3 4h3m-6-4h.01M9 16h.01"
|
|
49
|
+
/>
|
|
50
|
+
</svg>
|
|
51
|
+
{useCase.steps.length} step{useCase.steps.length !== 1 ? 's' : ''}
|
|
52
|
+
</span>
|
|
53
|
+
{#if useCase.bpmn}
|
|
54
|
+
<span
|
|
55
|
+
class="rounded-full bg-primary-100 px-2 py-0.5 text-xs font-medium text-primary-800 dark:bg-primary-900 dark:text-primary-200"
|
|
56
|
+
>
|
|
57
|
+
BPMN
|
|
58
|
+
</span>
|
|
59
|
+
{/if}
|
|
60
|
+
</div>
|
|
61
|
+
</a>
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { UseCase } from '@cwygoda/service-catalog-core/domain';
|
|
2
|
+
type $$ComponentProps = {
|
|
3
|
+
useCase: UseCase;
|
|
4
|
+
};
|
|
5
|
+
declare const UseCaseCard: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
6
|
+
type UseCaseCard = ReturnType<typeof UseCaseCard>;
|
|
7
|
+
export default UseCaseCard;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/// <reference types="@testing-library/jest-dom" />
|
|
2
|
+
import { render, screen } from '@testing-library/svelte';
|
|
3
|
+
import { describe, expect, it } from 'vitest';
|
|
4
|
+
import UseCaseCard from './UseCaseCard.svelte';
|
|
5
|
+
describe('UseCaseCard', () => {
|
|
6
|
+
const baseUseCase = {
|
|
7
|
+
id: 'test-use-case',
|
|
8
|
+
name: 'Test Use Case',
|
|
9
|
+
description: 'A test use case description',
|
|
10
|
+
participants: [],
|
|
11
|
+
steps: [],
|
|
12
|
+
};
|
|
13
|
+
it('renders use case name', () => {
|
|
14
|
+
render(UseCaseCard, { props: { useCase: baseUseCase } });
|
|
15
|
+
expect(screen.getByText('Test Use Case')).toBeInTheDocument();
|
|
16
|
+
});
|
|
17
|
+
it('renders use case description', () => {
|
|
18
|
+
render(UseCaseCard, { props: { useCase: baseUseCase } });
|
|
19
|
+
expect(screen.getByText('A test use case description')).toBeInTheDocument();
|
|
20
|
+
});
|
|
21
|
+
it('links to use case detail page', () => {
|
|
22
|
+
render(UseCaseCard, { props: { useCase: baseUseCase } });
|
|
23
|
+
const link = screen.getByRole('link');
|
|
24
|
+
expect(link).toHaveAttribute('href', '/use-cases/test-use-case');
|
|
25
|
+
});
|
|
26
|
+
it('truncates long descriptions with ellipsis', () => {
|
|
27
|
+
const longDesc = 'A'.repeat(150);
|
|
28
|
+
const useCaseWithLongDesc = {
|
|
29
|
+
...baseUseCase,
|
|
30
|
+
description: longDesc,
|
|
31
|
+
};
|
|
32
|
+
render(UseCaseCard, { props: { useCase: useCaseWithLongDesc } });
|
|
33
|
+
// Should truncate to 120 chars + "..."
|
|
34
|
+
expect(screen.getByText(/^A{100,120}\.\.\.$/)).toBeInTheDocument();
|
|
35
|
+
});
|
|
36
|
+
it('shows BPMN badge when bpmn is defined', () => {
|
|
37
|
+
const useCaseWithBpmn = {
|
|
38
|
+
...baseUseCase,
|
|
39
|
+
bpmn: './diagram.bpmn',
|
|
40
|
+
};
|
|
41
|
+
render(UseCaseCard, { props: { useCase: useCaseWithBpmn } });
|
|
42
|
+
expect(screen.getByText('BPMN')).toBeInTheDocument();
|
|
43
|
+
});
|
|
44
|
+
it('does not show BPMN badge when no bpmn', () => {
|
|
45
|
+
render(UseCaseCard, { props: { useCase: baseUseCase } });
|
|
46
|
+
expect(screen.queryByText('BPMN')).not.toBeInTheDocument();
|
|
47
|
+
});
|
|
48
|
+
it('shows participant count', () => {
|
|
49
|
+
const useCaseWithParticipants = {
|
|
50
|
+
...baseUseCase,
|
|
51
|
+
participants: [
|
|
52
|
+
{ service: 'svc-1', role: 'Role 1' },
|
|
53
|
+
{ service: 'svc-2', role: 'Role 2' },
|
|
54
|
+
],
|
|
55
|
+
};
|
|
56
|
+
render(UseCaseCard, { props: { useCase: useCaseWithParticipants } });
|
|
57
|
+
expect(screen.getByText('2 participants')).toBeInTheDocument();
|
|
58
|
+
});
|
|
59
|
+
it('shows singular participant when count is 1', () => {
|
|
60
|
+
const useCaseWithOneParticipant = {
|
|
61
|
+
...baseUseCase,
|
|
62
|
+
participants: [{ service: 'svc-1', role: 'Role 1' }],
|
|
63
|
+
};
|
|
64
|
+
render(UseCaseCard, { props: { useCase: useCaseWithOneParticipant } });
|
|
65
|
+
expect(screen.getByText('1 participant')).toBeInTheDocument();
|
|
66
|
+
});
|
|
67
|
+
it('shows step count', () => {
|
|
68
|
+
const useCaseWithSteps = {
|
|
69
|
+
...baseUseCase,
|
|
70
|
+
steps: [
|
|
71
|
+
{ sequence: 1, action: 'Step 1' },
|
|
72
|
+
{ sequence: 2, action: 'Step 2' },
|
|
73
|
+
{ sequence: 3, action: 'Step 3' },
|
|
74
|
+
],
|
|
75
|
+
};
|
|
76
|
+
render(UseCaseCard, { props: { useCase: useCaseWithSteps } });
|
|
77
|
+
expect(screen.getByText('3 steps')).toBeInTheDocument();
|
|
78
|
+
});
|
|
79
|
+
it('shows singular step when count is 1', () => {
|
|
80
|
+
const useCaseWithOneStep = {
|
|
81
|
+
...baseUseCase,
|
|
82
|
+
steps: [{ sequence: 1, action: 'Step 1' }],
|
|
83
|
+
};
|
|
84
|
+
render(UseCaseCard, { props: { useCase: useCaseWithOneStep } });
|
|
85
|
+
expect(screen.getByText('1 step')).toBeInTheDocument();
|
|
86
|
+
});
|
|
87
|
+
});
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export { default as BpmnDiagram } from './BpmnDiagram.svelte';
|
|
2
|
+
export { default as Breadcrumbs } from './Breadcrumbs.svelte';
|
|
3
|
+
export { default as DataStoreCard } from './DataStoreCard.svelte';
|
|
4
|
+
export { default as DataStoreShield } from './DataStoreShield.svelte';
|
|
5
|
+
export { default as DomainCard } from './DomainCard.svelte';
|
|
6
|
+
export { default as Header } from './Header.svelte';
|
|
7
|
+
export { default as NavModeToggle } from './NavModeToggle.svelte';
|
|
8
|
+
export { default as NavTree } from './NavTree.svelte';
|
|
9
|
+
export { default as SearchModal } from './SearchModal.svelte';
|
|
10
|
+
export { default as ServiceCard } from './ServiceCard.svelte';
|
|
11
|
+
export { default as ServiceTypeShield } from './ServiceTypeShield.svelte';
|
|
12
|
+
export { default as Shield } from './Shield.svelte';
|
|
13
|
+
export { default as ServiceGraph } from './ServiceGraph.svelte';
|
|
14
|
+
export { default as ThemeToggle } from './ThemeToggle.svelte';
|
|
15
|
+
export { default as UseCaseCard } from './UseCaseCard.svelte';
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export { default as BpmnDiagram } from './BpmnDiagram.svelte';
|
|
2
|
+
export { default as Breadcrumbs } from './Breadcrumbs.svelte';
|
|
3
|
+
export { default as DataStoreCard } from './DataStoreCard.svelte';
|
|
4
|
+
export { default as DataStoreShield } from './DataStoreShield.svelte';
|
|
5
|
+
export { default as DomainCard } from './DomainCard.svelte';
|
|
6
|
+
export { default as Header } from './Header.svelte';
|
|
7
|
+
export { default as NavModeToggle } from './NavModeToggle.svelte';
|
|
8
|
+
export { default as NavTree } from './NavTree.svelte';
|
|
9
|
+
export { default as SearchModal } from './SearchModal.svelte';
|
|
10
|
+
export { default as ServiceCard } from './ServiceCard.svelte';
|
|
11
|
+
export { default as ServiceTypeShield } from './ServiceTypeShield.svelte';
|
|
12
|
+
export { default as Shield } from './Shield.svelte';
|
|
13
|
+
export { default as ServiceGraph } from './ServiceGraph.svelte';
|
|
14
|
+
export { default as ThemeToggle } from './ThemeToggle.svelte';
|
|
15
|
+
export { default as UseCaseCard } from './UseCaseCard.svelte';
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export type { CatalogPort } from './catalog.port.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/source.css
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
@source "./";
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { browser } from '$app/environment';
|
|
2
|
+
const STORAGE_KEY = 'nav-mode';
|
|
3
|
+
function getInitialMode() {
|
|
4
|
+
if (browser) {
|
|
5
|
+
const stored = localStorage.getItem(STORAGE_KEY);
|
|
6
|
+
if (stored === 'tree' || stored === 'flat') {
|
|
7
|
+
return stored;
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
return 'flat';
|
|
11
|
+
}
|
|
12
|
+
function createNavModeStore() {
|
|
13
|
+
let mode = $state(getInitialMode());
|
|
14
|
+
return {
|
|
15
|
+
get mode() {
|
|
16
|
+
return mode;
|
|
17
|
+
},
|
|
18
|
+
toggle() {
|
|
19
|
+
mode = mode === 'flat' ? 'tree' : 'flat';
|
|
20
|
+
if (browser) {
|
|
21
|
+
localStorage.setItem(STORAGE_KEY, mode);
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
setMode(newMode) {
|
|
25
|
+
mode = newMode;
|
|
26
|
+
if (browser) {
|
|
27
|
+
localStorage.setItem(STORAGE_KEY, mode);
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
export const navModeStore = createNavModeStore();
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/** Reactive store for search modal open state */
|
|
2
|
+
class SearchStore {
|
|
3
|
+
open = $state(false);
|
|
4
|
+
toggle() {
|
|
5
|
+
this.open = !this.open;
|
|
6
|
+
}
|
|
7
|
+
show() {
|
|
8
|
+
this.open = true;
|
|
9
|
+
}
|
|
10
|
+
hide() {
|
|
11
|
+
this.open = false;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
export const searchStore = new SearchStore();
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
const STORAGE_KEY = 'theme';
|
|
2
|
+
function getStoredTheme() {
|
|
3
|
+
if (typeof localStorage === 'undefined')
|
|
4
|
+
return 'system';
|
|
5
|
+
const stored = localStorage.getItem(STORAGE_KEY);
|
|
6
|
+
if (stored === 'light' || stored === 'dark' || stored === 'system') {
|
|
7
|
+
return stored;
|
|
8
|
+
}
|
|
9
|
+
return 'system';
|
|
10
|
+
}
|
|
11
|
+
function getSystemTheme() {
|
|
12
|
+
if (typeof window === 'undefined')
|
|
13
|
+
return 'light';
|
|
14
|
+
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
|
|
15
|
+
}
|
|
16
|
+
function applyTheme(theme) {
|
|
17
|
+
if (typeof document === 'undefined')
|
|
18
|
+
return;
|
|
19
|
+
const resolved = theme === 'system' ? getSystemTheme() : theme;
|
|
20
|
+
const root = document.documentElement;
|
|
21
|
+
if (resolved === 'dark') {
|
|
22
|
+
root.classList.add('dark');
|
|
23
|
+
}
|
|
24
|
+
else {
|
|
25
|
+
root.classList.remove('dark');
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
// Simple reactive state using $state rune at module level
|
|
29
|
+
let currentTheme = $state('system');
|
|
30
|
+
export const theme = {
|
|
31
|
+
get current() {
|
|
32
|
+
return currentTheme;
|
|
33
|
+
},
|
|
34
|
+
get resolved() {
|
|
35
|
+
return currentTheme === 'system' ? getSystemTheme() : currentTheme;
|
|
36
|
+
},
|
|
37
|
+
init() {
|
|
38
|
+
if (typeof window === 'undefined')
|
|
39
|
+
return;
|
|
40
|
+
// Load stored theme
|
|
41
|
+
currentTheme = getStoredTheme();
|
|
42
|
+
// Listen for system theme changes
|
|
43
|
+
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
|
|
44
|
+
mediaQuery.addEventListener('change', () => {
|
|
45
|
+
if (currentTheme === 'system') {
|
|
46
|
+
applyTheme('system');
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
},
|
|
50
|
+
set(newTheme) {
|
|
51
|
+
currentTheme = newTheme;
|
|
52
|
+
if (typeof localStorage !== 'undefined') {
|
|
53
|
+
localStorage.setItem(STORAGE_KEY, newTheme);
|
|
54
|
+
}
|
|
55
|
+
applyTheme(newTheme);
|
|
56
|
+
},
|
|
57
|
+
toggle() {
|
|
58
|
+
const next = this.resolved === 'dark' ? 'light' : 'dark';
|
|
59
|
+
this.set(next);
|
|
60
|
+
},
|
|
61
|
+
};
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { Catalog } from '@cwygoda/service-catalog-core/domain';
|
|
2
|
+
/**
|
|
3
|
+
* Fetches and validates catalog.json with proper error handling.
|
|
4
|
+
* Use in SvelteKit load functions.
|
|
5
|
+
*/
|
|
6
|
+
export declare function fetchCatalog(fetch: typeof globalThis.fetch, path?: string): Promise<Catalog>;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { error } from '@sveltejs/kit';
|
|
2
|
+
import { Value } from '@sinclair/typebox/value';
|
|
3
|
+
import { CatalogSchema } from '@cwygoda/service-catalog-core/schemas';
|
|
4
|
+
/**
|
|
5
|
+
* Fetches and validates catalog.json with proper error handling.
|
|
6
|
+
* Use in SvelteKit load functions.
|
|
7
|
+
*/
|
|
8
|
+
export async function fetchCatalog(fetch, path = '/catalog.json') {
|
|
9
|
+
const response = await fetch(path);
|
|
10
|
+
if (!response.ok) {
|
|
11
|
+
error(response.status, `Failed to load catalog: ${response.statusText}`);
|
|
12
|
+
}
|
|
13
|
+
let data;
|
|
14
|
+
try {
|
|
15
|
+
data = await response.json();
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
error(500, 'Invalid JSON in catalog');
|
|
19
|
+
}
|
|
20
|
+
if (!Value.Check(CatalogSchema, data)) {
|
|
21
|
+
const errors = [...Value.Errors(CatalogSchema, data)];
|
|
22
|
+
const message = errors.map((e) => `${e.path}: ${e.message}`).join('; ');
|
|
23
|
+
error(500, `Invalid catalog data: ${message}`);
|
|
24
|
+
}
|
|
25
|
+
return data;
|
|
26
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { fetchCatalog } from './fetch-catalog.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { fetchCatalog } from './fetch-catalog.js';
|
package/package.json
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@cwygoda/service-catalog-ui",
|
|
3
|
+
"version": "0.17.1",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"svelte": "./dist/index.js",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": {
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"style": "./dist/source.css",
|
|
10
|
+
"svelte": "./dist/index.js"
|
|
11
|
+
},
|
|
12
|
+
"./components": {
|
|
13
|
+
"types": "./dist/components/index.d.ts",
|
|
14
|
+
"style": "./dist/source.css",
|
|
15
|
+
"svelte": "./dist/components/index.js"
|
|
16
|
+
},
|
|
17
|
+
"./stores": {
|
|
18
|
+
"types": "./dist/stores/index.d.ts",
|
|
19
|
+
"style": "./dist/source.css",
|
|
20
|
+
"svelte": "./dist/stores/index.js"
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
"files": [
|
|
24
|
+
"dist"
|
|
25
|
+
],
|
|
26
|
+
"dependencies": {
|
|
27
|
+
"@sinclair/typebox": "^0.34.48",
|
|
28
|
+
"@cwygoda/service-catalog-core": "0.17.1"
|
|
29
|
+
},
|
|
30
|
+
"peerDependencies": {
|
|
31
|
+
"@sveltejs/kit": "^2.0.0",
|
|
32
|
+
"bpmn-js": "^18.0.0",
|
|
33
|
+
"d3-drag": "^3.0.0",
|
|
34
|
+
"d3-force": "^3.0.0",
|
|
35
|
+
"d3-selection": "^3.0.0",
|
|
36
|
+
"d3-zoom": "^3.0.0",
|
|
37
|
+
"svelte": "^5.0.0"
|
|
38
|
+
},
|
|
39
|
+
"devDependencies": {
|
|
40
|
+
"@sveltejs/kit": "^2.53.4",
|
|
41
|
+
"@sveltejs/package": "^2.5.7",
|
|
42
|
+
"@sveltejs/vite-plugin-svelte": "^6.2.4",
|
|
43
|
+
"@testing-library/jest-dom": "^6.9.1",
|
|
44
|
+
"@testing-library/svelte": "^5.3.1",
|
|
45
|
+
"@testing-library/user-event": "^14.6.1",
|
|
46
|
+
"@types/d3-drag": "^3.0.7",
|
|
47
|
+
"@types/d3-force": "^3.0.10",
|
|
48
|
+
"@types/d3-selection": "^3.0.11",
|
|
49
|
+
"@types/d3-zoom": "^3.0.8",
|
|
50
|
+
"jsdom": "^28.1.0",
|
|
51
|
+
"svelte": "^5.53.6",
|
|
52
|
+
"svelte-check": "^4.4.4",
|
|
53
|
+
"typescript": "^5.9.3",
|
|
54
|
+
"vite": "^7.3.1",
|
|
55
|
+
"vitest": "^4.0.18"
|
|
56
|
+
},
|
|
57
|
+
"scripts": {
|
|
58
|
+
"build": "svelte-kit sync && svelte-package",
|
|
59
|
+
"test": "vitest run",
|
|
60
|
+
"test:watch": "vitest",
|
|
61
|
+
"typecheck": "svelte-kit sync && svelte-check"
|
|
62
|
+
}
|
|
63
|
+
}
|