sh3-core 0.13.1 → 0.13.3

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 (172) hide show
  1. package/dist/BrandSlot.svelte +62 -13
  2. package/dist/__test__/setup-dom.js +5 -0
  3. package/dist/actions/MenuButton.svelte +2 -1
  4. package/dist/actions/contextMenuModel.d.ts +1 -1
  5. package/dist/actions/contextMenuModel.js +2 -1
  6. package/dist/actions/dispatcher.svelte.d.ts +1 -1
  7. package/dist/actions/dispatcher.svelte.js +2 -1
  8. package/dist/actions/listActive.d.ts +1 -1
  9. package/dist/actions/listActive.js +2 -1
  10. package/dist/actions/listeners.d.ts +1 -1
  11. package/dist/actions/listeners.js +6 -5
  12. package/dist/actions/menuBarModel.js +3 -2
  13. package/dist/actions/paletteModel.js +2 -1
  14. package/dist/actions/resolveLabel.test.js +14 -0
  15. package/dist/actions/types.d.ts +12 -1
  16. package/dist/actions/types.js +7 -1
  17. package/dist/api.d.ts +3 -0
  18. package/dist/api.js +3 -0
  19. package/dist/app/store/AppUpdateAvailableModal.svelte +87 -0
  20. package/dist/app/store/AppUpdateAvailableModal.svelte.d.ts +11 -0
  21. package/dist/app/store/InstalledView.svelte +8 -54
  22. package/dist/app/store/UninstallAppDialog.svelte +86 -0
  23. package/dist/app/store/UninstallAppDialog.svelte.d.ts +10 -0
  24. package/dist/app/store/permissionConfirm.d.ts +4 -0
  25. package/dist/app/store/permissionConfirm.js +28 -0
  26. package/dist/app/store/storeShard.svelte.d.ts +8 -1
  27. package/dist/app/store/storeShard.svelte.js +42 -9
  28. package/dist/app/store/updatePackage.test.d.ts +1 -0
  29. package/dist/app/store/updatePackage.test.js +34 -0
  30. package/dist/app/store/verbs.d.ts +1 -0
  31. package/dist/app/store/verbs.js +79 -5
  32. package/dist/app/store/verbs.test.d.ts +1 -0
  33. package/dist/app/store/verbs.test.js +56 -0
  34. package/dist/app-appearance/AppAppearanceModal.svelte +174 -0
  35. package/dist/app-appearance/AppAppearanceModal.svelte.d.ts +8 -0
  36. package/dist/app-appearance/appearanceShard.svelte.d.ts +2 -0
  37. package/dist/app-appearance/appearanceShard.svelte.js +61 -0
  38. package/dist/app-appearance/appearanceState.svelte.d.ts +15 -0
  39. package/dist/app-appearance/appearanceState.svelte.js +59 -0
  40. package/dist/app-appearance/appearanceState.test.d.ts +1 -0
  41. package/dist/app-appearance/appearanceState.test.js +30 -0
  42. package/dist/app-appearance/index.d.ts +3 -0
  43. package/dist/app-appearance/index.js +2 -0
  44. package/dist/app-appearance/types.d.ts +11 -0
  45. package/dist/app-appearance/types.js +1 -0
  46. package/dist/apps/lifecycle.js +10 -2
  47. package/dist/apps/types.d.ts +18 -4
  48. package/dist/apps/workspace-rekey.d.ts +1 -0
  49. package/dist/apps/workspace-rekey.js +35 -0
  50. package/dist/apps/workspace-rekey.test.d.ts +1 -0
  51. package/dist/apps/workspace-rekey.test.js +23 -0
  52. package/dist/assets/iconIds.generated.d.ts +2 -0
  53. package/dist/assets/iconIds.generated.js +154 -0
  54. package/dist/auth/admin-users.svelte.d.ts +9 -0
  55. package/dist/auth/admin-users.svelte.js +42 -0
  56. package/dist/auth/admin-users.test.d.ts +1 -0
  57. package/dist/auth/admin-users.test.js +52 -0
  58. package/dist/createShell.js +5 -5
  59. package/dist/documents/config.d.ts +5 -1
  60. package/dist/documents/config.js +16 -8
  61. package/dist/documents/index.d.ts +1 -1
  62. package/dist/documents/index.js +1 -1
  63. package/dist/host-entry.d.ts +1 -1
  64. package/dist/host-entry.js +1 -1
  65. package/dist/host.d.ts +1 -1
  66. package/dist/host.js +9 -2
  67. package/dist/primitives/Button.svelte +50 -4
  68. package/dist/primitives/Button.svelte.d.ts +3 -1
  69. package/dist/primitives/Collapsible.svelte +110 -0
  70. package/dist/primitives/Collapsible.svelte.d.ts +14 -0
  71. package/dist/primitives/widgets/AppPicker.svelte +41 -0
  72. package/dist/primitives/widgets/AppPicker.svelte.d.ts +9 -0
  73. package/dist/primitives/widgets/AppPicker.svelte.test.d.ts +1 -0
  74. package/dist/primitives/widgets/AppPicker.svelte.test.js +26 -0
  75. package/dist/primitives/widgets/AppPicker.test.d.ts +1 -0
  76. package/dist/primitives/widgets/AppPicker.test.js +74 -0
  77. package/dist/primitives/widgets/ColorSwatch.svelte +7 -2
  78. package/dist/primitives/widgets/ColorSwatch.svelte.d.ts +2 -1
  79. package/dist/primitives/widgets/ColorSwatch.svelte.test.d.ts +1 -0
  80. package/dist/primitives/widgets/ColorSwatch.svelte.test.js +31 -0
  81. package/dist/primitives/widgets/Field.svelte +4 -2
  82. package/dist/primitives/widgets/Field.svelte.d.ts +2 -2
  83. package/dist/primitives/widgets/Field.svelte.test.d.ts +1 -0
  84. package/dist/primitives/widgets/Field.svelte.test.js +33 -0
  85. package/dist/primitives/widgets/FilePicker.svelte +2 -2
  86. package/dist/primitives/widgets/FilePicker.svelte.d.ts +2 -2
  87. package/dist/primitives/widgets/FilePicker.svelte.test.d.ts +1 -0
  88. package/dist/primitives/widgets/FilePicker.svelte.test.js +31 -0
  89. package/dist/primitives/widgets/IconPicker.svelte +115 -0
  90. package/dist/primitives/widgets/IconPicker.svelte.d.ts +9 -0
  91. package/dist/primitives/widgets/IconPicker.svelte.test.d.ts +1 -0
  92. package/dist/primitives/widgets/IconPicker.svelte.test.js +43 -0
  93. package/dist/primitives/widgets/IconToggleGroup.svelte +4 -4
  94. package/dist/primitives/widgets/IconToggleGroup.svelte.d.ts +3 -3
  95. package/dist/primitives/widgets/IconToggleGroup.svelte.test.d.ts +1 -0
  96. package/dist/primitives/widgets/IconToggleGroup.svelte.test.js +40 -0
  97. package/dist/primitives/widgets/NumberInput.svelte +19 -9
  98. package/dist/primitives/widgets/NumberInput.svelte.d.ts +2 -2
  99. package/dist/primitives/widgets/NumberInput.svelte.test.d.ts +1 -0
  100. package/dist/primitives/widgets/NumberInput.svelte.test.js +48 -0
  101. package/dist/primitives/widgets/PickerList.d.ts +24 -0
  102. package/dist/primitives/widgets/PickerList.js +21 -0
  103. package/dist/primitives/widgets/PickerList.svelte +150 -0
  104. package/dist/primitives/widgets/PickerList.svelte.d.ts +16 -0
  105. package/dist/primitives/widgets/PickerList.svelte.test.d.ts +1 -0
  106. package/dist/primitives/widgets/PickerList.svelte.test.js +31 -0
  107. package/dist/primitives/widgets/PickerList.test.d.ts +1 -0
  108. package/dist/primitives/widgets/PickerList.test.js +218 -0
  109. package/dist/primitives/widgets/RangeSlider.svelte +11 -4
  110. package/dist/primitives/widgets/RangeSlider.svelte.d.ts +2 -2
  111. package/dist/primitives/widgets/RangeSlider.svelte.test.d.ts +1 -0
  112. package/dist/primitives/widgets/RangeSlider.svelte.test.js +38 -0
  113. package/dist/primitives/widgets/Segmented.svelte +4 -4
  114. package/dist/primitives/widgets/Segmented.svelte.d.ts +3 -3
  115. package/dist/primitives/widgets/Segmented.svelte.test.d.ts +1 -0
  116. package/dist/primitives/widgets/Segmented.svelte.test.js +25 -0
  117. package/dist/primitives/widgets/Select.svelte +4 -4
  118. package/dist/primitives/widgets/Select.svelte.d.ts +3 -3
  119. package/dist/primitives/widgets/Select.svelte.test.d.ts +1 -0
  120. package/dist/primitives/widgets/Select.svelte.test.js +37 -0
  121. package/dist/primitives/widgets/Slider.svelte +4 -2
  122. package/dist/primitives/widgets/Slider.svelte.d.ts +2 -2
  123. package/dist/primitives/widgets/Slider.svelte.test.d.ts +1 -0
  124. package/dist/primitives/widgets/Slider.svelte.test.js +22 -0
  125. package/dist/primitives/widgets/SliderGroup.svelte +4 -2
  126. package/dist/primitives/widgets/SliderGroup.svelte.d.ts +2 -2
  127. package/dist/primitives/widgets/SliderGroup.svelte.test.d.ts +1 -0
  128. package/dist/primitives/widgets/SliderGroup.svelte.test.js +34 -0
  129. package/dist/primitives/widgets/Textarea.svelte +5 -2
  130. package/dist/primitives/widgets/Textarea.svelte.d.ts +2 -2
  131. package/dist/primitives/widgets/Textarea.svelte.test.d.ts +1 -0
  132. package/dist/primitives/widgets/Textarea.svelte.test.js +29 -0
  133. package/dist/primitives/widgets/UserPicker.svelte +53 -0
  134. package/dist/primitives/widgets/UserPicker.svelte.d.ts +9 -0
  135. package/dist/primitives/widgets/UserPicker.svelte.test.d.ts +1 -0
  136. package/dist/primitives/widgets/UserPicker.svelte.test.js +30 -0
  137. package/dist/primitives/widgets/UserPicker.test.d.ts +1 -0
  138. package/dist/primitives/widgets/UserPicker.test.js +115 -0
  139. package/dist/primitives/widgets/_contract.d.ts +27 -0
  140. package/dist/primitives/widgets/_contract.js +10 -0
  141. package/dist/projects/session-state.svelte.d.ts +17 -0
  142. package/dist/projects/session-state.svelte.js +39 -0
  143. package/dist/projects/session-state.test.d.ts +1 -0
  144. package/dist/projects/session-state.test.js +55 -0
  145. package/dist/projects-shard/DeleteProjectDialog.svelte +150 -0
  146. package/dist/projects-shard/DeleteProjectDialog.svelte.d.ts +12 -0
  147. package/dist/projects-shard/DeleteProjectDialog.test.d.ts +1 -0
  148. package/dist/projects-shard/DeleteProjectDialog.test.js +120 -0
  149. package/dist/projects-shard/ProjectManage.svelte +219 -0
  150. package/dist/projects-shard/ProjectManage.svelte.d.ts +8 -0
  151. package/dist/projects-shard/ProjectsSection.svelte +120 -0
  152. package/dist/projects-shard/ProjectsSection.svelte.d.ts +3 -0
  153. package/dist/projects-shard/index.d.ts +4 -0
  154. package/dist/projects-shard/index.js +4 -0
  155. package/dist/projects-shard/projectsApi.d.ts +20 -0
  156. package/dist/projects-shard/projectsApi.js +44 -0
  157. package/dist/projects-shard/projectsApi.test.d.ts +1 -0
  158. package/dist/projects-shard/projectsApi.test.js +71 -0
  159. package/dist/projects-shard/projectsShard.svelte.d.ts +10 -0
  160. package/dist/projects-shard/projectsShard.svelte.js +148 -0
  161. package/dist/sh3core-shard/ShellHome.svelte +83 -39
  162. package/dist/sh3core-shard/appActions.d.ts +13 -0
  163. package/dist/sh3core-shard/appActions.js +181 -0
  164. package/dist/sh3core-shard/appActions.test.d.ts +1 -0
  165. package/dist/sh3core-shard/appActions.test.js +25 -0
  166. package/dist/sh3core-shard/sh3coreShard.svelte.js +2 -0
  167. package/dist/shards/activate-scopeid.test.d.ts +1 -0
  168. package/dist/shards/{activate-tenantid.test.js → activate-scopeid.test.js} +6 -6
  169. package/dist/version.d.ts +1 -1
  170. package/dist/version.js +1 -1
  171. package/package.json +2 -2
  172. /package/dist/{shards/activate-tenantid.test.d.ts → actions/resolveLabel.test.d.ts} +0 -0
@@ -0,0 +1,150 @@
1
+ <script lang="ts">
2
+ import type { CommitOnlyEvents } from './_contract';
3
+ import { filterItems, toggle, type PickerItem } from './PickerList';
4
+
5
+ let {
6
+ items,
7
+ value = $bindable<string[]>([]),
8
+ loading = false,
9
+ error = null,
10
+ onRetry,
11
+ emptyText = 'No items.',
12
+ searchThreshold = 8,
13
+ disabled = false,
14
+ size = 'md',
15
+ onchange,
16
+ }: {
17
+ items: PickerItem[];
18
+ value?: string[];
19
+ loading?: boolean;
20
+ error?: string | null;
21
+ onRetry?: () => void;
22
+ emptyText?: string;
23
+ searchThreshold?: number;
24
+ disabled?: boolean;
25
+ size?: 'sm' | 'md';
26
+ } & CommitOnlyEvents<string[]> = $props();
27
+
28
+ let query = $state('');
29
+ const showSearch = $derived(items.length >= searchThreshold);
30
+ const filtered = $derived(filterItems(items, query));
31
+ const totalCount = $derived(items.length);
32
+ const selectedCount = $derived(value.length);
33
+
34
+ function toggleId(id: string) {
35
+ if (disabled) return;
36
+ const next = toggle(value, id);
37
+ value = next;
38
+ onchange?.(next);
39
+ }
40
+ </script>
41
+
42
+ <div class="sh3-picker" class:sh3-picker--sm={size === 'sm'} class:sh3-picker--disabled={disabled}>
43
+ {#if showSearch}
44
+ <input
45
+ type="search"
46
+ class="sh3-picker__search"
47
+ placeholder="Filter…"
48
+ bind:value={query}
49
+ {disabled}
50
+ />
51
+ {/if}
52
+
53
+ <div class="sh3-picker__list">
54
+ {#if loading}
55
+ <p class="sh3-picker__status">Loading…</p>
56
+ {:else if error}
57
+ <p class="sh3-picker__status sh3-picker__status--error">{error}</p>
58
+ {#if onRetry}
59
+ <button type="button" class="sh3-picker__retry" onclick={() => onRetry?.()}>Retry</button>
60
+ {/if}
61
+ {:else if items.length === 0}
62
+ <p class="sh3-picker__status">{emptyText}</p>
63
+ {:else}
64
+ {#each filtered as item (item.id)}
65
+ <label class="sh3-picker__row">
66
+ <input
67
+ type="checkbox"
68
+ checked={value.includes(item.id)}
69
+ {disabled}
70
+ onchange={() => toggleId(item.id)}
71
+ />
72
+ <span class="sh3-picker__row-text">
73
+ <span class="sh3-picker__row-label">{item.label}</span>
74
+ {#if item.sublabel}
75
+ <span class="sh3-picker__row-sub">{item.sublabel}</span>
76
+ {/if}
77
+ </span>
78
+ </label>
79
+ {/each}
80
+ {/if}
81
+ </div>
82
+
83
+ <p class="sh3-picker__footer">{selectedCount} of {totalCount} selected</p>
84
+ </div>
85
+
86
+ <style>
87
+ .sh3-picker {
88
+ display: flex;
89
+ flex-direction: column;
90
+ gap: 6px;
91
+ border: 1px solid var(--shell-border);
92
+ border-radius: var(--shell-radius-sm, 3px);
93
+ padding: 6px;
94
+ background: var(--shell-bg-elevated);
95
+ }
96
+ .sh3-picker__search {
97
+ width: 100%;
98
+ padding: 4px 6px;
99
+ border: 1px solid var(--shell-border);
100
+ border-radius: var(--shell-radius-sm, 3px);
101
+ background: var(--shell-bg);
102
+ color: var(--shell-fg);
103
+ font: inherit;
104
+ font-size: 12px;
105
+ }
106
+ .sh3-picker__list {
107
+ display: flex;
108
+ flex-direction: column;
109
+ max-height: 200px;
110
+ overflow-y: auto;
111
+ }
112
+ .sh3-picker__row {
113
+ display: flex;
114
+ align-items: center;
115
+ gap: 8px;
116
+ padding: 4px 6px;
117
+ cursor: pointer;
118
+ border-radius: var(--shell-radius-sm, 3px);
119
+ }
120
+ .sh3-picker__row:hover { background: var(--shell-bg); }
121
+ .sh3-picker__row-text { display: flex; flex-direction: column; gap: 0; }
122
+ .sh3-picker__row-label { font-size: 13px; }
123
+ .sh3-picker__row-sub { font-size: 11px; color: var(--shell-fg-muted); }
124
+ .sh3-picker__status {
125
+ margin: 0;
126
+ padding: 8px 6px;
127
+ color: var(--shell-fg-muted);
128
+ font-size: 12px;
129
+ }
130
+ .sh3-picker__status--error { color: var(--shell-error, #c33); }
131
+ .sh3-picker__retry {
132
+ align-self: flex-start;
133
+ margin: 0 6px 8px;
134
+ background: var(--shell-bg);
135
+ color: var(--shell-fg);
136
+ border: 1px solid var(--shell-border);
137
+ border-radius: var(--shell-radius-sm, 3px);
138
+ padding: 2px 8px;
139
+ font: inherit;
140
+ font-size: 12px;
141
+ cursor: pointer;
142
+ }
143
+ .sh3-picker__footer {
144
+ margin: 0;
145
+ font-size: 11px;
146
+ color: var(--shell-fg-muted);
147
+ text-align: right;
148
+ }
149
+ .sh3-picker--disabled .sh3-picker__row { cursor: not-allowed; opacity: 0.6; }
150
+ </style>
@@ -0,0 +1,16 @@
1
+ import type { CommitOnlyEvents } from './_contract';
2
+ import { type PickerItem } from './PickerList';
3
+ type $$ComponentProps = {
4
+ items: PickerItem[];
5
+ value?: string[];
6
+ loading?: boolean;
7
+ error?: string | null;
8
+ onRetry?: () => void;
9
+ emptyText?: string;
10
+ searchThreshold?: number;
11
+ disabled?: boolean;
12
+ size?: 'sm' | 'md';
13
+ } & CommitOnlyEvents<string[]>;
14
+ declare const PickerList: import("svelte").Component<$$ComponentProps, {}, "value">;
15
+ type PickerList = ReturnType<typeof PickerList>;
16
+ export default PickerList;
@@ -0,0 +1,31 @@
1
+ import { describe, it, expect, vi } from 'vitest';
2
+ import { render, fireEvent } from '@testing-library/svelte';
3
+ import PickerList from './PickerList.svelte';
4
+ const items = [
5
+ { id: '1', label: 'One' },
6
+ { id: '2', label: 'Two' },
7
+ { id: '3', label: 'Three' },
8
+ ];
9
+ describe('PickerList event contract', () => {
10
+ it('fires onchange with updated array when row toggled', async () => {
11
+ const onchange = vi.fn();
12
+ const { container } = render(PickerList, { props: { items, value: [], onchange } });
13
+ const checkboxes = container.querySelectorAll('input[type=checkbox]');
14
+ await fireEvent.click(checkboxes[1]);
15
+ expect(onchange).toHaveBeenCalledTimes(1);
16
+ expect(onchange).toHaveBeenCalledWith(['2']);
17
+ });
18
+ it('renders loading state', () => {
19
+ const { getByText } = render(PickerList, { props: { items: [], loading: true } });
20
+ expect(getByText('Loading…')).toBeInTheDocument();
21
+ });
22
+ it('renders error state with retry button when provided', async () => {
23
+ const onRetry = vi.fn();
24
+ const { getByText } = render(PickerList, {
25
+ props: { items: [], error: 'Boom', onRetry },
26
+ });
27
+ expect(getByText('Boom')).toBeInTheDocument();
28
+ await fireEvent.click(getByText('Retry'));
29
+ expect(onRetry).toHaveBeenCalled();
30
+ });
31
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,218 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
+ import { mount, unmount, tick } from 'svelte';
3
+ import PickerList from './PickerList.svelte';
4
+ import { filterItems, toggle } from './PickerList';
5
+ let host;
6
+ let cmp = null;
7
+ beforeEach(() => {
8
+ host = document.createElement('div');
9
+ document.body.appendChild(host);
10
+ });
11
+ afterEach(() => {
12
+ if (cmp) {
13
+ unmount(cmp);
14
+ cmp = null;
15
+ }
16
+ host.remove();
17
+ });
18
+ describe('filterItems', () => {
19
+ const items = [
20
+ { id: 'notes', label: 'Notes', sublabel: 'note-taking' },
21
+ { id: 'files', label: 'Files' },
22
+ { id: 'tasks', label: 'Task Manager' },
23
+ ];
24
+ it('returns everything when query is empty', () => {
25
+ expect(filterItems(items, '')).toEqual(items);
26
+ });
27
+ it('matches by label case-insensitively', () => {
28
+ expect(filterItems(items, 'note').map((i) => i.id)).toEqual(['notes']);
29
+ expect(filterItems(items, 'TASK').map((i) => i.id)).toEqual(['tasks']);
30
+ });
31
+ it('matches by id', () => {
32
+ expect(filterItems(items, 'fil').map((i) => i.id)).toEqual(['files']);
33
+ });
34
+ it('matches by sublabel', () => {
35
+ expect(filterItems(items, 'taking').map((i) => i.id)).toEqual(['notes']);
36
+ });
37
+ it('returns empty when nothing matches', () => {
38
+ expect(filterItems(items, 'zzz')).toEqual([]);
39
+ });
40
+ });
41
+ describe('toggle', () => {
42
+ it('adds an id when missing', () => {
43
+ expect(toggle(['a'], 'b')).toEqual(['a', 'b']);
44
+ });
45
+ it('removes an id when present', () => {
46
+ expect(toggle(['a', 'b'], 'a')).toEqual(['b']);
47
+ });
48
+ it('returns a new array (does not mutate)', () => {
49
+ const v = ['a'];
50
+ const next = toggle(v, 'b');
51
+ expect(v).toEqual(['a']);
52
+ expect(next).not.toBe(v);
53
+ });
54
+ });
55
+ describe('PickerList — rendering', () => {
56
+ function makeItems(n) {
57
+ return Array.from({ length: n }, (_, i) => ({ id: `id-${i}`, label: `Item ${i}` }));
58
+ }
59
+ it('renders one checkbox row per item', async () => {
60
+ cmp = mount(PickerList, {
61
+ target: host,
62
+ props: { items: makeItems(3), value: [] },
63
+ });
64
+ await tick();
65
+ const rows = host.querySelectorAll('.sh3-picker__row');
66
+ expect(rows.length).toBe(3);
67
+ const checks = host.querySelectorAll('input[type="checkbox"]');
68
+ expect(checks.length).toBe(3);
69
+ expect(Array.from(checks).every((c) => !c.checked)).toBe(true);
70
+ });
71
+ it('reflects value as checked rows', async () => {
72
+ cmp = mount(PickerList, {
73
+ target: host,
74
+ props: { items: makeItems(3), value: ['id-1'] },
75
+ });
76
+ await tick();
77
+ const checks = host.querySelectorAll('input[type="checkbox"]');
78
+ expect(checks[0].checked).toBe(false);
79
+ expect(checks[1].checked).toBe(true);
80
+ expect(checks[2].checked).toBe(false);
81
+ });
82
+ it('hides the search input when items.length < threshold (default 8)', async () => {
83
+ cmp = mount(PickerList, {
84
+ target: host,
85
+ props: { items: makeItems(7), value: [] },
86
+ });
87
+ await tick();
88
+ expect(host.querySelector('.sh3-picker__search')).toBeNull();
89
+ });
90
+ it('shows the search input when items.length >= threshold', async () => {
91
+ cmp = mount(PickerList, {
92
+ target: host,
93
+ props: { items: makeItems(8), value: [] },
94
+ });
95
+ await tick();
96
+ expect(host.querySelector('.sh3-picker__search')).not.toBeNull();
97
+ });
98
+ it('shows "Loading…" and hides the list when loading=true', async () => {
99
+ cmp = mount(PickerList, {
100
+ target: host,
101
+ props: { items: makeItems(2), value: [], loading: true },
102
+ });
103
+ await tick();
104
+ expect(host.textContent).toContain('Loading');
105
+ expect(host.querySelectorAll('.sh3-picker__row').length).toBe(0);
106
+ });
107
+ it('shows the error message and a Retry button when error is set', async () => {
108
+ let retried = false;
109
+ cmp = mount(PickerList, {
110
+ target: host,
111
+ props: {
112
+ items: [],
113
+ value: [],
114
+ error: 'boom',
115
+ onRetry: () => { retried = true; },
116
+ },
117
+ });
118
+ await tick();
119
+ expect(host.textContent).toContain('boom');
120
+ const retryBtn = host.querySelector('.sh3-picker__retry');
121
+ expect(retryBtn).not.toBeNull();
122
+ retryBtn.click();
123
+ expect(retried).toBe(true);
124
+ });
125
+ it('shows emptyText when items is empty and not loading/erroring', async () => {
126
+ cmp = mount(PickerList, {
127
+ target: host,
128
+ props: { items: [], value: [], emptyText: 'No widgets.' },
129
+ });
130
+ await tick();
131
+ expect(host.textContent).toContain('No widgets.');
132
+ });
133
+ it('shows the selected count in the footer', async () => {
134
+ cmp = mount(PickerList, {
135
+ target: host,
136
+ props: { items: makeItems(4), value: ['id-0', 'id-2'] },
137
+ });
138
+ await tick();
139
+ expect(host.textContent).toMatch(/2 of 4 selected/);
140
+ });
141
+ });
142
+ describe('PickerList — interaction', () => {
143
+ function makeItems(n) {
144
+ return Array.from({ length: n }, (_, i) => ({ id: `id-${i}`, label: `Item ${i}` }));
145
+ }
146
+ it('clicking a row fires onchange with the new array', async () => {
147
+ let received = null;
148
+ cmp = mount(PickerList, {
149
+ target: host,
150
+ props: {
151
+ items: makeItems(3),
152
+ value: [],
153
+ onchange: (next) => { received = next; },
154
+ },
155
+ });
156
+ await tick();
157
+ const checks = host.querySelectorAll('input[type="checkbox"]');
158
+ checks[1].click();
159
+ await tick();
160
+ expect(received).toEqual(['id-1']);
161
+ });
162
+ it('toggling an already-selected row removes it', async () => {
163
+ let received = null;
164
+ cmp = mount(PickerList, {
165
+ target: host,
166
+ props: {
167
+ items: makeItems(3),
168
+ value: ['id-1'],
169
+ onchange: (next) => { received = next; },
170
+ },
171
+ });
172
+ await tick();
173
+ const checks = host.querySelectorAll('input[type="checkbox"]');
174
+ checks[1].click();
175
+ await tick();
176
+ expect(received).toEqual([]);
177
+ });
178
+ it('disabled blocks input', async () => {
179
+ cmp = mount(PickerList, {
180
+ target: host,
181
+ props: {
182
+ items: makeItems(2),
183
+ value: [],
184
+ disabled: true,
185
+ },
186
+ });
187
+ await tick();
188
+ const checks = host.querySelectorAll('input[type="checkbox"]');
189
+ expect(checks[0].disabled).toBe(true);
190
+ });
191
+ it('typing in the search input filters the rendered rows but keeps value', async () => {
192
+ cmp = mount(PickerList, {
193
+ target: host,
194
+ props: {
195
+ items: [
196
+ { id: 'notes', label: 'Notes' },
197
+ { id: 'files', label: 'Files' },
198
+ { id: 'tasks', label: 'Tasks' },
199
+ { id: 'photos', label: 'Photos' },
200
+ { id: 'mail', label: 'Mail' },
201
+ { id: 'maps', label: 'Maps' },
202
+ { id: 'calc', label: 'Calculator' },
203
+ { id: 'paint', label: 'Paint' },
204
+ ],
205
+ value: ['mail'],
206
+ },
207
+ });
208
+ await tick();
209
+ const search = host.querySelector('.sh3-picker__search');
210
+ expect(search).not.toBeNull();
211
+ search.value = 'note';
212
+ search.dispatchEvent(new Event('input', { bubbles: true }));
213
+ await tick();
214
+ const rows = host.querySelectorAll('.sh3-picker__row');
215
+ expect(rows.length).toBe(1);
216
+ expect(rows[0].textContent).toContain('Notes');
217
+ });
218
+ });
@@ -1,4 +1,5 @@
1
1
  <script lang="ts">
2
+ import type { LiveInputEvents } from './_contract';
2
3
  import { valueToPercent } from './Slider';
3
4
  import { constrainPair, type Pair } from './RangeSlider';
4
5
 
@@ -10,6 +11,7 @@
10
11
  disabled = false,
11
12
  invalid = false,
12
13
  size = 'md',
14
+ oninput,
13
15
  onchange,
14
16
  }: {
15
17
  value?: Pair;
@@ -19,14 +21,19 @@
19
21
  disabled?: boolean;
20
22
  invalid?: boolean;
21
23
  size?: 'sm' | 'md';
22
- onchange?: (next: Pair) => void;
23
- } = $props();
24
+ } & LiveInputEvents<Pair> = $props();
24
25
 
25
26
  const lowPct = $derived(valueToPercent(value[0], min, max));
26
27
  const highPct = $derived(valueToPercent(value[1], min, max));
27
28
 
28
- function setLow(n: number) { value = constrainPair(value, 'low', n); }
29
- function setHigh(n: number) { value = constrainPair(value, 'high', n); }
29
+ function setLow(n: number) {
30
+ value = constrainPair(value, 'low', n);
31
+ oninput?.(value);
32
+ }
33
+ function setHigh(n: number) {
34
+ value = constrainPair(value, 'high', n);
35
+ oninput?.(value);
36
+ }
30
37
  </script>
31
38
 
32
39
  <div class="sh3-range" class:sh3-range--sm={size === 'sm'} class:sh3-range--invalid={invalid}>
@@ -1,3 +1,4 @@
1
+ import type { LiveInputEvents } from './_contract';
1
2
  import { type Pair } from './RangeSlider';
2
3
  type $$ComponentProps = {
3
4
  value?: Pair;
@@ -7,8 +8,7 @@ type $$ComponentProps = {
7
8
  disabled?: boolean;
8
9
  invalid?: boolean;
9
10
  size?: 'sm' | 'md';
10
- onchange?: (next: Pair) => void;
11
- };
11
+ } & LiveInputEvents<Pair>;
12
12
  declare const RangeSlider: import("svelte").Component<$$ComponentProps, {}, "value">;
13
13
  type RangeSlider = ReturnType<typeof RangeSlider>;
14
14
  export default RangeSlider;
@@ -0,0 +1,38 @@
1
+ import { describe, it, expect, vi } from 'vitest';
2
+ import { render, fireEvent } from '@testing-library/svelte';
3
+ import RangeSlider from './RangeSlider.svelte';
4
+ describe('RangeSlider event contract', () => {
5
+ it('fires oninput per thumb with constrained pair', async () => {
6
+ const oninput = vi.fn();
7
+ const { container } = render(RangeSlider, {
8
+ props: { value: [10, 90], min: 0, max: 100, oninput },
9
+ });
10
+ const lows = container.querySelectorAll('input[type=range]');
11
+ const low = lows[0];
12
+ await fireEvent.input(low, { target: { value: '20' } });
13
+ expect(oninput).toHaveBeenCalledTimes(1);
14
+ const lastCall = oninput.mock.calls[oninput.mock.calls.length - 1][0];
15
+ expect(lastCall).toEqual([20, 90]);
16
+ });
17
+ it('fires onchange per thumb on change event', async () => {
18
+ const onchange = vi.fn();
19
+ const { container } = render(RangeSlider, {
20
+ props: { value: [10, 90], min: 0, max: 100, onchange },
21
+ });
22
+ const high = container.querySelectorAll('input[type=range]')[1];
23
+ await fireEvent.input(high, { target: { value: '80' } });
24
+ await fireEvent.change(high, { target: { value: '80' } });
25
+ expect(onchange).toHaveBeenCalledTimes(1);
26
+ expect(onchange.mock.calls[0][0]).toEqual([10, 80]);
27
+ });
28
+ it('thumbs cannot cross', async () => {
29
+ const oninput = vi.fn();
30
+ const { container } = render(RangeSlider, {
31
+ props: { value: [10, 90], min: 0, max: 100, oninput },
32
+ });
33
+ const low = container.querySelectorAll('input[type=range]')[0];
34
+ await fireEvent.input(low, { target: { value: '99' } });
35
+ const lastCall = oninput.mock.calls[oninput.mock.calls.length - 1][0];
36
+ expect(lastCall[0]).toBeLessThanOrEqual(lastCall[1]);
37
+ });
38
+ });
@@ -1,19 +1,19 @@
1
1
  <script lang="ts">
2
+ import type { CommitOnlyEvents } from './_contract';
2
3
  import { type SegmentedOption, nextValue, prevValue, firstValue, lastValue } from './Segmented';
3
4
 
4
5
  let {
5
6
  options,
6
- value = $bindable(),
7
+ value = $bindable(options[0]?.value ?? ''),
7
8
  size = 'md',
8
9
  disabled = false,
9
10
  onchange,
10
11
  }: {
11
12
  options: SegmentedOption[];
12
- value: string;
13
+ value?: string;
13
14
  size?: 'sm' | 'md';
14
15
  disabled?: boolean;
15
- onchange?: (next: string) => void;
16
- } = $props();
16
+ } & CommitOnlyEvents<string> = $props();
17
17
 
18
18
  function commit(next: string) {
19
19
  value = next;
@@ -1,11 +1,11 @@
1
+ import type { CommitOnlyEvents } from './_contract';
1
2
  import { type SegmentedOption } from './Segmented';
2
3
  type $$ComponentProps = {
3
4
  options: SegmentedOption[];
4
- value: string;
5
+ value?: string;
5
6
  size?: 'sm' | 'md';
6
7
  disabled?: boolean;
7
- onchange?: (next: string) => void;
8
- };
8
+ } & CommitOnlyEvents<string>;
9
9
  declare const Segmented: import("svelte").Component<$$ComponentProps, {}, "value">;
10
10
  type Segmented = ReturnType<typeof Segmented>;
11
11
  export default Segmented;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,25 @@
1
+ import { describe, it, expect, vi } from 'vitest';
2
+ import { render, fireEvent } from '@testing-library/svelte';
3
+ import Segmented from './Segmented.svelte';
4
+ describe('Segmented event contract', () => {
5
+ const options = [
6
+ { value: 'a', label: 'A' },
7
+ { value: 'b', label: 'B' },
8
+ { value: 'c', label: 'C' },
9
+ ];
10
+ it('fires onchange once on click', async () => {
11
+ const onchange = vi.fn();
12
+ const { getByText } = render(Segmented, { props: { options, value: 'a', onchange } });
13
+ await fireEvent.click(getByText('B'));
14
+ expect(onchange).toHaveBeenCalledTimes(1);
15
+ expect(onchange).toHaveBeenCalledWith('b');
16
+ });
17
+ it('defaults value to first option when not passed', () => {
18
+ const { container } = render(Segmented, { props: { options } });
19
+ const active = container.querySelector('.sh3-seg__btn--active');
20
+ expect(active === null || active === void 0 ? void 0 : active.textContent).toBe('A');
21
+ });
22
+ it('does not throw props_invalid_value when bound to an undefined source', () => {
23
+ expect(() => render(Segmented, { props: { options, value: undefined } })).not.toThrow();
24
+ });
25
+ });
@@ -1,13 +1,14 @@
1
1
  <script lang="ts">
2
2
  import { tick } from 'svelte';
3
+ import type { CommitOnlyEvents } from './_contract';
3
4
  import { shell } from '../../shellRuntime.svelte';
4
5
  import Listbox from './_select-listbox.svelte';
5
6
  import { type SelectOption, resolveValueLabel, toggleMulti } from './Select';
6
7
 
7
8
  let {
8
9
  options,
9
- value = $bindable<string | string[]>(''),
10
10
  multiple = false,
11
+ value = $bindable<string | string[]>(multiple ? [] : ''),
11
12
  placeholder = 'Select…',
12
13
  label,
13
14
  disabled = false,
@@ -16,15 +17,14 @@
16
17
  onchange,
17
18
  }: {
18
19
  options: SelectOption[];
19
- value?: string | string[];
20
20
  multiple?: boolean;
21
+ value?: string | string[];
21
22
  placeholder?: string;
22
23
  label?: string;
23
24
  disabled?: boolean;
24
25
  invalid?: boolean;
25
26
  size?: 'sm' | 'md';
26
- onchange?: (next: string | string[]) => void;
27
- } = $props();
27
+ } & CommitOnlyEvents<string | string[]> = $props();
28
28
 
29
29
  let trigger = $state<HTMLButtonElement | undefined>(undefined);
30
30
  let nativeRef = $state<HTMLSelectElement | undefined>(undefined);
@@ -1,15 +1,15 @@
1
+ import type { CommitOnlyEvents } from './_contract';
1
2
  import { type SelectOption } from './Select';
2
3
  type $$ComponentProps = {
3
4
  options: SelectOption[];
4
- value?: string | string[];
5
5
  multiple?: boolean;
6
+ value?: string | string[];
6
7
  placeholder?: string;
7
8
  label?: string;
8
9
  disabled?: boolean;
9
10
  invalid?: boolean;
10
11
  size?: 'sm' | 'md';
11
- onchange?: (next: string | string[]) => void;
12
- };
12
+ } & CommitOnlyEvents<string | string[]>;
13
13
  declare const Select: import("svelte").Component<$$ComponentProps, {}, "value">;
14
14
  type Select = ReturnType<typeof Select>;
15
15
  export default Select;
@@ -0,0 +1 @@
1
+ export {};