@industream/flowmaker-flowbox-ui-components 1.0.6-poc.0 → 1.1.0

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.
@@ -1,4 +1,5 @@
1
1
  <script>
2
+ import { onDestroy } from 'svelte';
2
3
  import { DEFAULT_COLUMNS, resolveColumn, resolveValue, resolveSearchText } from './picker-column-helpers';
3
4
 
4
5
  let {
@@ -6,18 +7,23 @@
6
7
  selectedIds = $bindable([]),
7
8
  columns = DEFAULT_COLUMNS,
8
9
  loading = false,
10
+ loadingMore = false,
9
11
  error = '',
12
+ loadMoreError = '',
10
13
  emptyMessage = 'No entries found for this source connection.',
14
+ totalCount = 0,
15
+ remaining = 0,
16
+ serverSearch = false,
17
+ searching = false,
11
18
  onSelectionChange = null,
12
19
  onSearch = null,
13
- searching = false,
14
- tooMany = false
20
+ onLoadMore = null
15
21
  } = $props();
16
22
 
17
23
  let searchQuery = $state('');
18
24
  let debounceTimer = null;
19
25
 
20
- /** Resolved column descriptors for rendering — maps each column key to its label, resolver and render type */
26
+ /** Resolved column descriptors for rendering. */
21
27
  const columnDescriptors = $derived(
22
28
  columns
23
29
  .map(k => { const col = resolveColumn(k); return col ? { key: k, ...col } : null; })
@@ -25,31 +31,40 @@
25
31
  );
26
32
 
27
33
  /**
28
- * When onSearch is set (server mode), entries are already filtered server-side.
29
- * When onSearch is NOT set (client mode), filter locally.
34
+ * In server-search mode entries are already filtered server-side.
35
+ * Otherwise (whole catalog loaded) filter the in-memory entries locally.
30
36
  */
31
37
  const filtered = $derived(() => {
32
- if (onSearch) return entries;
38
+ if (serverSearch) return entries;
33
39
  if (!searchQuery.trim()) return entries;
34
40
  const q = searchQuery.toLowerCase();
35
41
  return entries.filter(e => resolveSearchText(e, columns).includes(q));
36
42
  });
37
43
 
38
44
  const selectedCount = $derived(selectedIds.length);
39
- const totalCount = $derived(entries.length);
40
45
  const allVisibleSelected = $derived(filtered().length > 0 && filtered().every(e => selectedIds.includes(e.id)));
41
46
 
47
+ /** Whether the load-more affordance should show (more pages for the current query). */
48
+ const showLoadMore = $derived(remaining > 0 && !loading && !error);
49
+
42
50
  function handleSearchInput(e) {
43
51
  searchQuery = e.target.value;
44
- if (!onSearch) return;
45
52
 
46
- // Debounce 1s for server search
53
+ // Always cancel a pending server search: if the mode is (or just became)
54
+ // client-side, no request should fire; if server-side, we re-arm below.
47
55
  if (debounceTimer) clearTimeout(debounceTimer);
56
+ if (!serverSearch) return; // client-side filtering is reactive via filtered()
57
+
58
+ // Debounce 1s for server search
48
59
  debounceTimer = setTimeout(() => {
49
- onSearch(searchQuery.trim());
60
+ onSearch?.(searchQuery.trim());
50
61
  }, 1000);
51
62
  }
52
63
 
64
+ onDestroy(() => {
65
+ if (debounceTimer) clearTimeout(debounceTimer);
66
+ });
67
+
53
68
  function toggleEntry(id) {
54
69
  selectedIds = selectedIds.indexOf(id) >= 0
55
70
  ? selectedIds.filter(s => s !== id)
@@ -73,6 +88,21 @@
73
88
  function toggleAll() {
74
89
  allVisibleSelected ? deselectAll() : selectAll();
75
90
  }
91
+
92
+ /**
93
+ * Svelte action: fires `onLoadMore` when the sentinel scrolls into the
94
+ * viewport of the scrollable table container. The "Load more" button is
95
+ * the accessible/explicit fallback for the same callback.
96
+ */
97
+ function autoLoad(node) {
98
+ const observer = new IntersectionObserver((entriesObserved) => {
99
+ if (entriesObserved.some(en => en.isIntersecting) && !loadingMore) {
100
+ onLoadMore?.();
101
+ }
102
+ }, { root: node.closest('.table-container'), rootMargin: '120px' });
103
+ observer.observe(node);
104
+ return { destroy() { observer.disconnect(); } };
105
+ }
76
106
  </script>
77
107
 
78
108
  <div class="tag-browser">
@@ -84,7 +114,7 @@
84
114
  <input
85
115
  type="text"
86
116
  class="search-input"
87
- placeholder={tooMany ? "Search to find entries..." : "Search..."}
117
+ placeholder={serverSearch ? "Search to find entries..." : "Search..."}
88
118
  value={searchQuery}
89
119
  oninput={handleSearchInput}
90
120
  />
@@ -107,14 +137,8 @@
107
137
  <span>{error}</span>
108
138
  </div>
109
139
  {:else if entries.length === 0 && !searching}
110
- <div class="state-message empty-state"><span>{tooMany ? 'No matching entries — refine your search.' : emptyMessage}</span></div>
140
+ <div class="state-message empty-state"><span>{emptyMessage}</span></div>
111
141
  {:else}
112
- {#if tooMany}
113
- <div class="too-many-banner">
114
- <svg viewBox="0 0 16 16" width="16" height="16"><path d="M8 1a7 7 0 1 0 0 14A7 7 0 0 0 8 1zm-.75 3.5h1.5v5h-1.5zm.75 8a1 1 0 1 1 0-2 1 1 0 0 1 0 2z" fill="currentColor"></path></svg>
115
- <span>Showing {entries.length} entries — not all are displayed. Use the search box to find others.</span>
116
- </div>
117
- {/if}
118
142
  <div class="table-container">
119
143
  <table class="entry-table">
120
144
  <!-- Table headers: one checkbox column + one column per columnDescriptor -->
@@ -156,8 +180,22 @@
156
180
  {/each}
157
181
  </tbody>
158
182
  </table>
183
+ {#if showLoadMore}
184
+ <div class="load-more" use:autoLoad>
185
+ {#if loadingMore}
186
+ <div class="spinner"></div> <span>Loading more…</span>
187
+ {:else}
188
+ <button class="action-button" onclick={() => onLoadMore?.()}>
189
+ Load more ({remaining} remaining)
190
+ </button>
191
+ {/if}
192
+ </div>
193
+ {#if loadMoreError}
194
+ <div class="load-more-error">{loadMoreError} — <button class="link-button" onclick={() => onLoadMore?.()}>retry</button></div>
195
+ {/if}
196
+ {/if}
159
197
  </div>
160
- {#if !onSearch && filtered().length !== entries.length}
198
+ {#if !serverSearch && filtered().length !== entries.length}
161
199
  <div class="filter-info">Showing {filtered().length} of {entries.length} entries</div>
162
200
  {/if}
163
201
  {/if}
@@ -201,14 +239,6 @@
201
239
  }
202
240
  .error-state { color: var(--cds-support-error, #fa4d56); }
203
241
  .empty-state { color: var(--cds-text-helper, #8d8d8d); }
204
- .too-many-banner {
205
- display: flex; align-items: center; gap: 0.5rem;
206
- padding: 0.5rem 0.75rem;
207
- background: var(--cds-notification-background-warning, #3d3522);
208
- border-left: 3px solid var(--cds-support-warning, #f1c21b);
209
- color: var(--cds-text-primary, #f4f4f4); font-size: 0.75rem;
210
- }
211
- .too-many-banner svg { color: var(--cds-support-warning, #f1c21b); flex: 0 0 auto; }
212
242
  .spinner {
213
243
  width: 1rem; height: 1rem;
214
244
  border: 2px solid var(--cds-border-subtle-01, #e0e0e0);
@@ -260,4 +290,18 @@
260
290
  font-size: 0.75rem; color: var(--cds-text-helper, #8d8d8d);
261
291
  text-align: center; padding: 0.25rem;
262
292
  }
293
+ .load-more {
294
+ display: flex; align-items: center; justify-content: center;
295
+ gap: 0.5rem; padding: 0.75rem; font-size: 0.8125rem;
296
+ color: var(--cds-text-secondary, #c6c6c6);
297
+ }
298
+ .load-more-error {
299
+ font-size: 0.75rem; color: var(--cds-support-error, #fa4d56);
300
+ text-align: center; padding: 0.25rem;
301
+ }
302
+ .link-button {
303
+ background: none; border: none; padding: 0; cursor: pointer;
304
+ color: var(--cds-link-primary, #78a9ff); text-decoration: underline;
305
+ font-size: inherit;
306
+ }
263
307
  </style>
@@ -8,23 +8,33 @@ declare const TagBrowser: import("svelte").Component<{
8
8
  selectedIds?: any[];
9
9
  columns?: typeof DEFAULT_COLUMNS;
10
10
  loading?: boolean;
11
+ loadingMore?: boolean;
11
12
  error?: string;
13
+ loadMoreError?: string;
12
14
  emptyMessage?: string;
15
+ totalCount?: number;
16
+ remaining?: number;
17
+ serverSearch?: boolean;
18
+ searching?: boolean;
13
19
  onSelectionChange?: any;
14
20
  onSearch?: any;
15
- searching?: boolean;
16
- tooMany?: boolean;
21
+ onLoadMore?: any;
17
22
  }, {}, "selectedIds">;
18
23
  type $$ComponentProps = {
19
24
  entries?: any[];
20
25
  selectedIds?: any[];
21
26
  columns?: typeof DEFAULT_COLUMNS;
22
27
  loading?: boolean;
28
+ loadingMore?: boolean;
23
29
  error?: string;
30
+ loadMoreError?: string;
24
31
  emptyMessage?: string;
32
+ totalCount?: number;
33
+ remaining?: number;
34
+ serverSearch?: boolean;
35
+ searching?: boolean;
25
36
  onSelectionChange?: any;
26
37
  onSearch?: any;
27
- searching?: boolean;
28
- tooMany?: boolean;
38
+ onLoadMore?: any;
29
39
  };
30
40
  import { DEFAULT_COLUMNS } from './picker-column-helpers';
@@ -0,0 +1,24 @@
1
+ import type { CatalogEntry } from '@industream/datacatalog-client/dto';
2
+ /** Number of entries fetched per page. */
3
+ export declare const PAGE_SIZE = 100;
4
+ /** Query token that matches all entries (server-side wildcard). */
5
+ export declare const MATCH_ALL = "%";
6
+ /** Deduplicate entries by id, keeping the first occurrence and preserving order. */
7
+ export declare function deduplicateEntries(items: CatalogEntry[]): CatalogEntry[];
8
+ /** Append a freshly fetched page to the accumulated list, dropping duplicates. */
9
+ export declare function mergePage(existing: CatalogEntry[], page: CatalogEntry[]): CatalogEntry[];
10
+ /** Trim a raw search input; an empty query becomes the match-all token. */
11
+ export declare function normalizeQuery(raw: string): string;
12
+ /** True when the full match-all set is in memory (small catalog → client-side search). */
13
+ export declare function isAllLoaded(query: string, loadedCount: number, totalCount: number): boolean;
14
+ /** Whether more pages remain for the current query. */
15
+ export declare function hasMore(loadedCount: number, totalCount: number): boolean;
16
+ /** Number of not-yet-loaded entries for the current query (never negative). */
17
+ export declare function remainingCount(loadedCount: number, totalCount: number): number;
18
+ /** Upsert every fetched entry into the selected-entry metadata cache (mutates and returns it). */
19
+ export declare function upsertSelectedCache(cache: Map<string, CatalogEntry>, items: CatalogEntry[]): Map<string, CatalogEntry>;
20
+ /**
21
+ * Resolve the selected entries for display: prefer the currently loaded window,
22
+ * fall back to the cache, in the order of `selectedIds`. Ids absent from both are skipped.
23
+ */
24
+ export declare function resolveSelected(entries: CatalogEntry[], cache: Map<string, CatalogEntry>, selectedIds: string[]): CatalogEntry[];
@@ -0,0 +1,58 @@
1
+ /** Number of entries fetched per page. */
2
+ export const PAGE_SIZE = 100;
3
+ /** Query token that matches all entries (server-side wildcard). */
4
+ export const MATCH_ALL = '%';
5
+ /** Deduplicate entries by id, keeping the first occurrence and preserving order. */
6
+ export function deduplicateEntries(items) {
7
+ const seen = new Set();
8
+ const result = [];
9
+ for (const item of items) {
10
+ if (!seen.has(item.id)) {
11
+ seen.add(item.id);
12
+ result.push(item);
13
+ }
14
+ }
15
+ return result;
16
+ }
17
+ /** Append a freshly fetched page to the accumulated list, dropping duplicates. */
18
+ export function mergePage(existing, page) {
19
+ return deduplicateEntries([...existing, ...page]);
20
+ }
21
+ /** Trim a raw search input; an empty query becomes the match-all token. */
22
+ export function normalizeQuery(raw) {
23
+ const trimmed = raw.trim();
24
+ return trimmed === '' ? MATCH_ALL : trimmed;
25
+ }
26
+ /** True when the full match-all set is in memory (small catalog → client-side search). */
27
+ export function isAllLoaded(query, loadedCount, totalCount) {
28
+ return query === MATCH_ALL && loadedCount >= totalCount;
29
+ }
30
+ /** Whether more pages remain for the current query. */
31
+ export function hasMore(loadedCount, totalCount) {
32
+ return loadedCount < totalCount;
33
+ }
34
+ /** Number of not-yet-loaded entries for the current query (never negative). */
35
+ export function remainingCount(loadedCount, totalCount) {
36
+ return Math.max(0, totalCount - loadedCount);
37
+ }
38
+ /** Upsert every fetched entry into the selected-entry metadata cache (mutates and returns it). */
39
+ export function upsertSelectedCache(cache, items) {
40
+ for (const item of items) {
41
+ cache.set(item.id, item);
42
+ }
43
+ return cache;
44
+ }
45
+ /**
46
+ * Resolve the selected entries for display: prefer the currently loaded window,
47
+ * fall back to the cache, in the order of `selectedIds`. Ids absent from both are skipped.
48
+ */
49
+ export function resolveSelected(entries, cache, selectedIds) {
50
+ const windowById = new Map(entries.map(e => [e.id, e]));
51
+ const result = [];
52
+ for (const id of selectedIds) {
53
+ const found = windowById.get(id) ?? cache.get(id);
54
+ if (found)
55
+ result.push(found);
56
+ }
57
+ return result;
58
+ }
@@ -0,0 +1,61 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { PAGE_SIZE, MATCH_ALL, deduplicateEntries, mergePage, normalizeQuery, isAllLoaded, hasMore, remainingCount, upsertSelectedCache, resolveSelected } from './picker-pagination';
3
+ const entry = (id, name = id) => ({ id, name });
4
+ describe('constants', () => {
5
+ it('exposes the page size and match-all token', () => {
6
+ expect(PAGE_SIZE).toBe(100);
7
+ expect(MATCH_ALL).toBe('%');
8
+ });
9
+ });
10
+ describe('deduplicateEntries', () => {
11
+ it('keeps first occurrence by id and preserves order', () => {
12
+ const result = deduplicateEntries([entry('a'), entry('b'), entry('a')]);
13
+ expect(result.map(e => e.id)).toEqual(['a', 'b']);
14
+ });
15
+ });
16
+ describe('mergePage', () => {
17
+ it('appends a new page and drops duplicates against existing', () => {
18
+ const result = mergePage([entry('a'), entry('b')], [entry('b'), entry('c')]);
19
+ expect(result.map(e => e.id)).toEqual(['a', 'b', 'c']);
20
+ });
21
+ });
22
+ describe('normalizeQuery', () => {
23
+ it('trims and maps empty input to the match-all token', () => {
24
+ expect(normalizeQuery(' ')).toBe(MATCH_ALL);
25
+ expect(normalizeQuery('')).toBe(MATCH_ALL);
26
+ });
27
+ it('trims a real query', () => {
28
+ expect(normalizeQuery(' temp ')).toBe('temp');
29
+ });
30
+ });
31
+ describe('isAllLoaded', () => {
32
+ it('is true only for match-all with every item loaded', () => {
33
+ expect(isAllLoaded(MATCH_ALL, 50, 50)).toBe(true);
34
+ expect(isAllLoaded(MATCH_ALL, 50, 120)).toBe(false);
35
+ expect(isAllLoaded('temp', 50, 50)).toBe(false);
36
+ });
37
+ });
38
+ describe('hasMore / remainingCount', () => {
39
+ it('reports remaining pages relative to total', () => {
40
+ expect(hasMore(100, 350)).toBe(true);
41
+ expect(hasMore(350, 350)).toBe(false);
42
+ expect(remainingCount(100, 350)).toBe(250);
43
+ expect(remainingCount(350, 350)).toBe(0);
44
+ expect(remainingCount(400, 350)).toBe(0);
45
+ });
46
+ });
47
+ describe('selected cache', () => {
48
+ it('upserts fetched items by id', () => {
49
+ const cache = upsertSelectedCache(new Map(), [entry('a'), entry('b')]);
50
+ upsertSelectedCache(cache, [entry('b', 'renamed')]);
51
+ expect(cache.get('b')?.name).toBe('renamed');
52
+ expect(cache.size).toBe(2);
53
+ });
54
+ it('resolves selected entries from window first, cache as fallback, in selection order', () => {
55
+ const cache = upsertSelectedCache(new Map(), [entry('x'), entry('y')]);
56
+ const window = [entry('y', 'in-window')];
57
+ const result = resolveSelected(window, cache, ['x', 'y']);
58
+ expect(result.map(e => e.id)).toEqual(['x', 'y']);
59
+ expect(result.find(e => e.id === 'y')?.name).toBe('in-window');
60
+ });
61
+ });
@@ -0,0 +1,20 @@
1
+ import type { AssetNode, AssetDictionary } from '@industream/datacatalog-client/dto';
2
+ import type { CatalogEntry } from '@industream/datacatalog-client/dto';
3
+ /** Depth-first flatten of a node forest — self emitted before its children. */
4
+ export declare function flattenNodes(nodes: AssetNode[]): AssetNode[];
5
+ /** Find a node by id anywhere in the forest; null when absent. */
6
+ export declare function findNodeById(nodes: AssetNode[], id: string): AssetNode | null;
7
+ /**
8
+ * Collect a node's assigned parent entryIds. With `includeDescendants`, unions
9
+ * every descendant's entryIds too. De-duplicated, node-first, DFS order.
10
+ */
11
+ export declare function collectEntryIds(node: AssetNode, includeDescendants?: boolean): string[];
12
+ /**
13
+ * Keep only binding rows on the given source connection. An empty
14
+ * `sourceConnectionId` disables filtering (returns the input unchanged).
15
+ * The typed `query` filter cannot scope by connection, so the container
16
+ * filters client-side (node entry counts are small).
17
+ */
18
+ export declare function filterEntriesByConnection(entries: CatalogEntry[], sourceConnectionId: string): CatalogEntry[];
19
+ /** Id of the first dictionary (default selection), or undefined for an empty list. */
20
+ export declare function firstDictionaryId(dictionaries: AssetDictionary[]): string | undefined;
@@ -0,0 +1,52 @@
1
+ /** Depth-first flatten of a node forest — self emitted before its children. */
2
+ export function flattenNodes(nodes) {
3
+ const result = [];
4
+ for (const n of nodes) {
5
+ result.push(n);
6
+ if (n.children && n.children.length > 0) {
7
+ result.push(...flattenNodes(n.children));
8
+ }
9
+ }
10
+ return result;
11
+ }
12
+ /** Find a node by id anywhere in the forest; null when absent. */
13
+ export function findNodeById(nodes, id) {
14
+ for (const n of flattenNodes(nodes)) {
15
+ if (n.id === id)
16
+ return n;
17
+ }
18
+ return null;
19
+ }
20
+ /**
21
+ * Collect a node's assigned parent entryIds. With `includeDescendants`, unions
22
+ * every descendant's entryIds too. De-duplicated, node-first, DFS order.
23
+ */
24
+ export function collectEntryIds(node, includeDescendants = false) {
25
+ const source = includeDescendants ? flattenNodes([node]) : [node];
26
+ const seen = new Set();
27
+ const result = [];
28
+ for (const n of source) {
29
+ for (const id of n.entryIds ?? []) {
30
+ if (!seen.has(id)) {
31
+ seen.add(id);
32
+ result.push(id);
33
+ }
34
+ }
35
+ }
36
+ return result;
37
+ }
38
+ /**
39
+ * Keep only binding rows on the given source connection. An empty
40
+ * `sourceConnectionId` disables filtering (returns the input unchanged).
41
+ * The typed `query` filter cannot scope by connection, so the container
42
+ * filters client-side (node entry counts are small).
43
+ */
44
+ export function filterEntriesByConnection(entries, sourceConnectionId) {
45
+ if (!sourceConnectionId)
46
+ return entries;
47
+ return entries.filter(e => e.sourceConnection?.id === sourceConnectionId);
48
+ }
49
+ /** Id of the first dictionary (default selection), or undefined for an empty list. */
50
+ export function firstDictionaryId(dictionaries) {
51
+ return dictionaries.length > 0 ? dictionaries[0].id : undefined;
52
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,54 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { flattenNodes, findNodeById, collectEntryIds, filterEntriesByConnection, firstDictionaryId } from './picker-tree';
3
+ const node = (id, entryIds = [], children = []) => ({ id, dictionaryId: 'd1', name: id, order: 0, entryIds, children });
4
+ const tree = [
5
+ node('root', ['e1'], [
6
+ node('zone-a', ['e2', 'e3'], [
7
+ node('leaf', ['e3', 'e4'])
8
+ ]),
9
+ node('zone-b', ['e5'])
10
+ ])
11
+ ];
12
+ describe('flattenNodes', () => {
13
+ it('returns every node depth-first, self before children', () => {
14
+ expect(flattenNodes(tree).map(n => n.id)).toEqual(['root', 'zone-a', 'leaf', 'zone-b']);
15
+ });
16
+ it('handles an empty forest', () => {
17
+ expect(flattenNodes([])).toEqual([]);
18
+ });
19
+ });
20
+ describe('findNodeById', () => {
21
+ it('finds a nested node', () => {
22
+ expect(findNodeById(tree, 'leaf')?.id).toBe('leaf');
23
+ });
24
+ it('returns null when absent', () => {
25
+ expect(findNodeById(tree, 'nope')).toBeNull();
26
+ });
27
+ });
28
+ describe('collectEntryIds', () => {
29
+ it('returns only the node own entryIds by default', () => {
30
+ expect(collectEntryIds(findNodeById(tree, 'zone-a'))).toEqual(['e2', 'e3']);
31
+ });
32
+ it('unions descendants and de-duplicates, node first, DFS order', () => {
33
+ expect(collectEntryIds(findNodeById(tree, 'zone-a'), true)).toEqual(['e2', 'e3', 'e4']);
34
+ });
35
+ it('collects the whole tree from the root with descendants', () => {
36
+ expect(collectEntryIds(tree[0], true)).toEqual(['e1', 'e2', 'e3', 'e4', 'e5']);
37
+ });
38
+ });
39
+ describe('filterEntriesByConnection', () => {
40
+ const entry = (id, connId) => ({ id, sourceConnection: { id: connId } });
41
+ const rows = [entry('a', 'c1'), entry('b', 'c2'), entry('c', 'c1')];
42
+ it('keeps only rows on the given connection', () => {
43
+ expect(filterEntriesByConnection(rows, 'c1').map(e => e.id)).toEqual(['a', 'c']);
44
+ });
45
+ it('returns all rows when connection id is empty', () => {
46
+ expect(filterEntriesByConnection(rows, '')).toHaveLength(3);
47
+ });
48
+ });
49
+ describe('firstDictionaryId', () => {
50
+ it('returns the first id or undefined', () => {
51
+ expect(firstDictionaryId([{ id: 'x' }])).toBe('x');
52
+ expect(firstDictionaryId([])).toBeUndefined();
53
+ });
54
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@industream/flowmaker-flowbox-ui-components",
3
- "version": "1.0.6-poc.0",
3
+ "version": "1.1.0",
4
4
  "description": "Reusable Svelte components for FlowMaker FlowBox UI",
5
5
  "type": "module",
6
6
  "svelte": "./dist/index.js",
@@ -40,19 +40,21 @@
40
40
  ],
41
41
  "scripts": {
42
42
  "build": "svelte-package --input src",
43
- "dev": "vite --config vite.dev.config.ts"
43
+ "test": "vitest",
44
+ "test:run": "vitest run"
44
45
  },
45
46
  "peerDependencies": {
46
47
  "svelte": "^5.0.0",
47
- "@industream/datacatalog-client": "1.9.2"
48
+ "@industream/datacatalog-client": "1.9.1"
48
49
  },
49
50
  "devDependencies": {
50
- "@industream/datacatalog-client": "1.9.2",
51
+ "@industream/datacatalog-client": "1.9.1",
51
52
  "svelte": "^5.0.0",
52
53
  "vite": "^6.0.0",
53
54
  "@sveltejs/vite-plugin-svelte": "^5.0.0",
54
55
  "@sveltejs/package": "^2.0.0",
55
- "typescript": "^5.0.0"
56
+ "typescript": "^5.0.0",
57
+ "vitest": "^2.1.0"
56
58
  },
57
59
  "publishConfig": {
58
60
  "access": "public"