sh3-core 0.11.8 → 0.13.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.
- package/dist/__test__/reset.js +2 -0
- package/dist/actions/MenuButton.svelte +2 -1
- package/dist/actions/contextMenuModel.js +8 -0
- package/dist/actions/contextMenuModel.test.js +22 -2
- package/dist/actions/listeners.js +28 -2
- package/dist/actions/listeners.test.js +87 -1
- package/dist/actions/scope-helpers.d.ts +17 -0
- package/dist/actions/scope-helpers.js +37 -0
- package/dist/actions/scope-helpers.test.js +33 -1
- package/dist/api.d.ts +18 -1
- package/dist/api.js +15 -1
- package/dist/app/store/InstalledView.svelte +2 -1
- package/dist/app/store/StoreView.svelte +2 -1
- package/dist/apps/lifecycle.d.ts +7 -0
- package/dist/apps/lifecycle.js +25 -5
- package/dist/apps/lifecycle.test.js +95 -0
- package/dist/host.js +30 -4
- package/dist/layout/LayoutRenderer.svelte +5 -1
- package/dist/layout/LayoutRenderer.test.js +42 -0
- package/dist/layout/SlotContainer.svelte +11 -2
- package/dist/layout/SlotContainer.svelte.d.ts +1 -0
- package/dist/layout/slotHostPool.svelte.js +10 -3
- package/dist/layout/slotHostPool.test.js +15 -0
- package/dist/navigation/back-stack.d.ts +29 -0
- package/dist/navigation/back-stack.js +87 -0
- package/dist/navigation/back-stack.test.d.ts +1 -0
- package/dist/navigation/back-stack.test.js +145 -0
- package/dist/navigation/index.d.ts +2 -0
- package/dist/navigation/index.js +6 -0
- package/dist/navigation/platform-web.d.ts +3 -0
- package/dist/navigation/platform-web.js +54 -0
- package/dist/navigation/platform-web.test.d.ts +1 -0
- package/dist/navigation/platform-web.test.js +96 -0
- package/dist/overlays/modal.js +7 -0
- package/dist/overlays/modal.test.js +35 -0
- package/dist/overlays/popup.js +7 -0
- package/dist/overlays/popup.test.js +33 -0
- package/dist/platform/index.d.ts +15 -0
- package/dist/platform/index.js +47 -0
- package/dist/primitives/base.css +17 -6
- package/dist/primitives/widgets/ColorSwatch.svelte +66 -0
- package/dist/primitives/widgets/ColorSwatch.svelte.d.ts +9 -0
- package/dist/primitives/widgets/Field.svelte +124 -0
- package/dist/primitives/widgets/Field.svelte.d.ts +19 -0
- package/dist/primitives/widgets/FilePicker.d.ts +3 -0
- package/dist/primitives/widgets/FilePicker.js +19 -0
- package/dist/primitives/widgets/FilePicker.svelte +79 -0
- package/dist/primitives/widgets/FilePicker.svelte.d.ts +13 -0
- package/dist/primitives/widgets/FilePicker.test.d.ts +1 -0
- package/dist/primitives/widgets/FilePicker.test.js +44 -0
- package/dist/primitives/widgets/IconToggleGroup.d.ts +2 -0
- package/dist/primitives/widgets/IconToggleGroup.js +8 -0
- package/dist/primitives/widgets/IconToggleGroup.svelte +86 -0
- package/dist/primitives/widgets/IconToggleGroup.svelte.d.ts +16 -0
- package/dist/primitives/widgets/IconToggleGroup.test.d.ts +1 -0
- package/dist/primitives/widgets/IconToggleGroup.test.js +19 -0
- package/dist/primitives/widgets/NumberInput.d.ts +6 -0
- package/dist/primitives/widgets/NumberInput.js +19 -0
- package/dist/primitives/widgets/NumberInput.svelte +167 -0
- package/dist/primitives/widgets/NumberInput.svelte.d.ts +17 -0
- package/dist/primitives/widgets/NumberInput.test.d.ts +1 -0
- package/dist/primitives/widgets/NumberInput.test.js +28 -0
- package/dist/primitives/widgets/RangeSlider.d.ts +2 -0
- package/dist/primitives/widgets/RangeSlider.js +7 -0
- package/dist/primitives/widgets/RangeSlider.svelte +124 -0
- package/dist/primitives/widgets/RangeSlider.svelte.d.ts +13 -0
- package/dist/primitives/widgets/RangeSlider.test.d.ts +1 -0
- package/dist/primitives/widgets/RangeSlider.test.js +14 -0
- package/dist/primitives/widgets/Segmented.d.ts +9 -0
- package/dist/primitives/widgets/Segmented.js +28 -0
- package/dist/primitives/widgets/Segmented.svelte +82 -0
- package/dist/primitives/widgets/Segmented.svelte.d.ts +10 -0
- package/dist/primitives/widgets/Segmented.test.d.ts +1 -0
- package/dist/primitives/widgets/Segmented.test.js +24 -0
- package/dist/primitives/widgets/Select.d.ts +11 -0
- package/dist/primitives/widgets/Select.js +42 -0
- package/dist/primitives/widgets/Select.svelte +163 -0
- package/dist/primitives/widgets/Select.svelte.d.ts +14 -0
- package/dist/primitives/widgets/Select.test.d.ts +1 -0
- package/dist/primitives/widgets/Select.test.js +68 -0
- package/dist/primitives/widgets/Slider.d.ts +6 -0
- package/dist/primitives/widgets/Slider.js +19 -0
- package/dist/primitives/widgets/Slider.svelte +205 -0
- package/dist/primitives/widgets/Slider.svelte.d.ts +15 -0
- package/dist/primitives/widgets/Slider.test.d.ts +1 -0
- package/dist/primitives/widgets/Slider.test.js +31 -0
- package/dist/primitives/widgets/SliderGroup.svelte +58 -0
- package/dist/primitives/widgets/SliderGroup.svelte.d.ts +18 -0
- package/dist/primitives/widgets/Textarea.svelte +81 -0
- package/dist/primitives/widgets/Textarea.svelte.d.ts +16 -0
- package/dist/primitives/widgets/_select-listbox.svelte +228 -0
- package/dist/primitives/widgets/_select-listbox.svelte.d.ts +18 -0
- package/dist/shards/activate-error-isolation.test.d.ts +1 -0
- package/dist/shards/activate-error-isolation.test.js +98 -0
- package/dist/shards/activate.svelte.d.ts +30 -2
- package/dist/shards/activate.svelte.js +62 -17
- package/dist/shell-shard/Terminal.svelte +1 -4
- package/dist/shell-shard/verbs/index.js +2 -0
- package/dist/shell-shard/verbs/reset.d.ts +2 -0
- package/dist/shell-shard/verbs/reset.js +26 -0
- package/dist/tokens.css +32 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
let {
|
|
3
|
+
value = $bindable(''),
|
|
4
|
+
label,
|
|
5
|
+
placeholder,
|
|
6
|
+
helper,
|
|
7
|
+
error,
|
|
8
|
+
disabled = false,
|
|
9
|
+
invalid = false,
|
|
10
|
+
size = 'md',
|
|
11
|
+
required = false,
|
|
12
|
+
rows = 3,
|
|
13
|
+
resize = 'vertical',
|
|
14
|
+
}: {
|
|
15
|
+
value?: string;
|
|
16
|
+
label?: string;
|
|
17
|
+
placeholder?: string;
|
|
18
|
+
helper?: string;
|
|
19
|
+
error?: string;
|
|
20
|
+
disabled?: boolean;
|
|
21
|
+
invalid?: boolean;
|
|
22
|
+
size?: 'sm' | 'md';
|
|
23
|
+
required?: boolean;
|
|
24
|
+
rows?: number;
|
|
25
|
+
resize?: 'none' | 'vertical' | 'both';
|
|
26
|
+
} = $props();
|
|
27
|
+
|
|
28
|
+
const showError = $derived(invalid && !!error);
|
|
29
|
+
const helperText = $derived(showError ? error : helper);
|
|
30
|
+
</script>
|
|
31
|
+
|
|
32
|
+
<label class="sh3-textarea" class:sh3-textarea--invalid={invalid} class:sh3-textarea--sm={size === 'sm'}>
|
|
33
|
+
{#if label}<span class="sh3-textarea__label">{label}{#if required}<span aria-hidden="true"> *</span>{/if}</span>{/if}
|
|
34
|
+
<textarea
|
|
35
|
+
class="sh3-textarea__input"
|
|
36
|
+
style:resize
|
|
37
|
+
{placeholder}
|
|
38
|
+
{disabled}
|
|
39
|
+
{required}
|
|
40
|
+
{rows}
|
|
41
|
+
aria-invalid={invalid || undefined}
|
|
42
|
+
bind:value
|
|
43
|
+
></textarea>
|
|
44
|
+
{#if helperText}<span class="sh3-textarea__helper" class:sh3-textarea__helper--error={showError}>{helperText}</span>{/if}
|
|
45
|
+
</label>
|
|
46
|
+
|
|
47
|
+
<style>
|
|
48
|
+
.sh3-textarea {
|
|
49
|
+
display: inline-flex;
|
|
50
|
+
flex-direction: column;
|
|
51
|
+
gap: 4px;
|
|
52
|
+
font-family: var(--shell-font-ui);
|
|
53
|
+
font-size: 0.8125rem;
|
|
54
|
+
}
|
|
55
|
+
.sh3-textarea__label {
|
|
56
|
+
color: var(--shell-fg-muted);
|
|
57
|
+
font-size: 0.75rem;
|
|
58
|
+
}
|
|
59
|
+
.sh3-textarea__input {
|
|
60
|
+
background: var(--shell-input-bg);
|
|
61
|
+
color: var(--shell-fg);
|
|
62
|
+
border: 1px solid var(--shell-border);
|
|
63
|
+
border-radius: var(--shell-widget-radius);
|
|
64
|
+
padding: var(--shell-pad-sm) var(--shell-field-pad-x);
|
|
65
|
+
font: inherit;
|
|
66
|
+
outline: none;
|
|
67
|
+
transition: border-color var(--shell-motion-fast) var(--shell-ease-standard);
|
|
68
|
+
}
|
|
69
|
+
.sh3-textarea__input:focus {
|
|
70
|
+
border-color: var(--shell-input-border-focus);
|
|
71
|
+
box-shadow: var(--shell-focus-ring);
|
|
72
|
+
}
|
|
73
|
+
.sh3-textarea--invalid .sh3-textarea__input {
|
|
74
|
+
border-color: var(--shell-error);
|
|
75
|
+
}
|
|
76
|
+
.sh3-textarea__helper {
|
|
77
|
+
color: var(--shell-fg-muted);
|
|
78
|
+
font-size: 0.75rem;
|
|
79
|
+
}
|
|
80
|
+
.sh3-textarea__helper--error { color: var(--shell-error); }
|
|
81
|
+
</style>
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
type $$ComponentProps = {
|
|
2
|
+
value?: string;
|
|
3
|
+
label?: string;
|
|
4
|
+
placeholder?: string;
|
|
5
|
+
helper?: string;
|
|
6
|
+
error?: string;
|
|
7
|
+
disabled?: boolean;
|
|
8
|
+
invalid?: boolean;
|
|
9
|
+
size?: 'sm' | 'md';
|
|
10
|
+
required?: boolean;
|
|
11
|
+
rows?: number;
|
|
12
|
+
resize?: 'none' | 'vertical' | 'both';
|
|
13
|
+
};
|
|
14
|
+
declare const Textarea: import("svelte").Component<$$ComponentProps, {}, "value">;
|
|
15
|
+
type Textarea = ReturnType<typeof Textarea>;
|
|
16
|
+
export default Textarea;
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { tick } from 'svelte';
|
|
3
|
+
import {
|
|
4
|
+
type SelectOption,
|
|
5
|
+
shouldShowSearch,
|
|
6
|
+
filterOptions,
|
|
7
|
+
matchTypeAhead,
|
|
8
|
+
} from './Select';
|
|
9
|
+
|
|
10
|
+
let {
|
|
11
|
+
options,
|
|
12
|
+
getValue,
|
|
13
|
+
multiple,
|
|
14
|
+
onSelect,
|
|
15
|
+
onClose,
|
|
16
|
+
close,
|
|
17
|
+
}: {
|
|
18
|
+
options: SelectOption[];
|
|
19
|
+
/** Live read of the selected value(s). Returning a closure (rather
|
|
20
|
+
* than passing value as a snapshot) is what lets the listbox
|
|
21
|
+
* re-render checkmarks across multiple selections without unmounting. */
|
|
22
|
+
getValue: () => string | string[];
|
|
23
|
+
multiple: boolean;
|
|
24
|
+
onSelect: (v: string) => void;
|
|
25
|
+
/** Called by listbox-initiated close paths so the trigger can
|
|
26
|
+
* restore its open-flag and focus. Outside-click and Escape
|
|
27
|
+
* paths are handled by the popup manager via Select's wrap. */
|
|
28
|
+
onClose?: () => void;
|
|
29
|
+
close: () => void;
|
|
30
|
+
} = $props();
|
|
31
|
+
|
|
32
|
+
const value = $derived(getValue());
|
|
33
|
+
|
|
34
|
+
function dismiss() {
|
|
35
|
+
onClose?.();
|
|
36
|
+
close();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const showSearch = $derived(shouldShowSearch(options));
|
|
40
|
+
|
|
41
|
+
function initialActiveIdx(): number {
|
|
42
|
+
if (multiple) return 0;
|
|
43
|
+
const v = typeof value === 'string' ? value : '';
|
|
44
|
+
const idx = options.findIndex((o) => o.value === v && !o.disabled);
|
|
45
|
+
return idx >= 0 ? idx : 0;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
let query = $state('');
|
|
49
|
+
let activeIdx = $state(initialActiveIdx());
|
|
50
|
+
let listEl = $state<HTMLDivElement | undefined>(undefined);
|
|
51
|
+
let searchEl = $state<HTMLInputElement | undefined>(undefined);
|
|
52
|
+
let typeAheadBuffer = '';
|
|
53
|
+
let typeAheadTimer: ReturnType<typeof setTimeout> | null = null;
|
|
54
|
+
|
|
55
|
+
const filtered = $derived(filterOptions(options, query));
|
|
56
|
+
|
|
57
|
+
$effect(() => {
|
|
58
|
+
if (activeIdx >= filtered.length) activeIdx = 0;
|
|
59
|
+
void scrollActiveIntoView();
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
async function scrollActiveIntoView() {
|
|
63
|
+
await tick();
|
|
64
|
+
if (!listEl) return;
|
|
65
|
+
const el = listEl.querySelector<HTMLElement>(`[data-idx="${activeIdx}"]`);
|
|
66
|
+
el?.scrollIntoView({ block: 'nearest' });
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function isSelected(o: SelectOption): boolean {
|
|
70
|
+
if (multiple && Array.isArray(value)) return value.includes(o.value);
|
|
71
|
+
return value === o.value;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function commit(idx: number) {
|
|
75
|
+
const o = filtered[idx];
|
|
76
|
+
if (!o || o.disabled) return;
|
|
77
|
+
onSelect(o.value);
|
|
78
|
+
if (!multiple) dismiss();
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function onListKey(e: KeyboardEvent) {
|
|
82
|
+
if (filtered.length === 0) return;
|
|
83
|
+
switch (e.key) {
|
|
84
|
+
case 'ArrowDown':
|
|
85
|
+
e.preventDefault();
|
|
86
|
+
activeIdx = (activeIdx + 1) % filtered.length;
|
|
87
|
+
break;
|
|
88
|
+
case 'ArrowUp':
|
|
89
|
+
e.preventDefault();
|
|
90
|
+
activeIdx = (activeIdx - 1 + filtered.length) % filtered.length;
|
|
91
|
+
break;
|
|
92
|
+
case 'Home':
|
|
93
|
+
e.preventDefault();
|
|
94
|
+
activeIdx = 0;
|
|
95
|
+
break;
|
|
96
|
+
case 'End':
|
|
97
|
+
e.preventDefault();
|
|
98
|
+
activeIdx = filtered.length - 1;
|
|
99
|
+
break;
|
|
100
|
+
case 'Enter':
|
|
101
|
+
e.preventDefault();
|
|
102
|
+
commit(activeIdx);
|
|
103
|
+
break;
|
|
104
|
+
case ' ':
|
|
105
|
+
case 'Space':
|
|
106
|
+
e.preventDefault();
|
|
107
|
+
commit(activeIdx);
|
|
108
|
+
break;
|
|
109
|
+
case 'Escape':
|
|
110
|
+
e.preventDefault();
|
|
111
|
+
dismiss();
|
|
112
|
+
break;
|
|
113
|
+
default:
|
|
114
|
+
if (showSearch && document.activeElement === searchEl) return;
|
|
115
|
+
if (e.key.length !== 1 || e.metaKey || e.ctrlKey || e.altKey) return;
|
|
116
|
+
typeAheadBuffer += e.key;
|
|
117
|
+
if (typeAheadTimer) clearTimeout(typeAheadTimer);
|
|
118
|
+
typeAheadTimer = setTimeout(() => { typeAheadBuffer = ''; }, 500);
|
|
119
|
+
const idx = matchTypeAhead(filtered, typeAheadBuffer, activeIdx);
|
|
120
|
+
if (idx >= 0) activeIdx = idx;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
</script>
|
|
124
|
+
|
|
125
|
+
<div
|
|
126
|
+
class="sh3-listbox"
|
|
127
|
+
role="listbox"
|
|
128
|
+
aria-multiselectable={multiple}
|
|
129
|
+
tabindex="-1"
|
|
130
|
+
onkeydown={onListKey}
|
|
131
|
+
>
|
|
132
|
+
{#if showSearch}
|
|
133
|
+
<input
|
|
134
|
+
bind:this={searchEl}
|
|
135
|
+
class="sh3-listbox__search"
|
|
136
|
+
type="search"
|
|
137
|
+
placeholder="Filter…"
|
|
138
|
+
bind:value={query}
|
|
139
|
+
/>
|
|
140
|
+
{/if}
|
|
141
|
+
<div class="sh3-listbox__list" bind:this={listEl}>
|
|
142
|
+
{#each filtered as opt, i}
|
|
143
|
+
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
|
144
|
+
<div
|
|
145
|
+
role="option"
|
|
146
|
+
tabindex="-1"
|
|
147
|
+
aria-selected={isSelected(opt)}
|
|
148
|
+
aria-disabled={opt.disabled}
|
|
149
|
+
data-idx={i}
|
|
150
|
+
class:sh3-listbox__opt--active={i === activeIdx}
|
|
151
|
+
class:sh3-listbox__opt--selected={isSelected(opt)}
|
|
152
|
+
onclick={() => commit(i)}
|
|
153
|
+
onmouseenter={() => activeIdx = i}
|
|
154
|
+
>
|
|
155
|
+
{#if multiple}
|
|
156
|
+
<span class="sh3-listbox__check" class:sh3-listbox__check--on={isSelected(opt)} aria-hidden="true"></span>
|
|
157
|
+
{/if}
|
|
158
|
+
<span class="sh3-listbox__label">{opt.label}</span>
|
|
159
|
+
</div>
|
|
160
|
+
{/each}
|
|
161
|
+
{#if filtered.length === 0}
|
|
162
|
+
<div class="sh3-listbox__empty">No matches</div>
|
|
163
|
+
{/if}
|
|
164
|
+
</div>
|
|
165
|
+
</div>
|
|
166
|
+
|
|
167
|
+
<style>
|
|
168
|
+
.sh3-listbox {
|
|
169
|
+
background: var(--shell-bg-elevated);
|
|
170
|
+
border: 1px solid var(--shell-border);
|
|
171
|
+
border-radius: var(--shell-widget-radius);
|
|
172
|
+
box-shadow: var(--shell-shadow-sm);
|
|
173
|
+
min-width: 180px;
|
|
174
|
+
max-width: 320px;
|
|
175
|
+
color: var(--shell-fg);
|
|
176
|
+
font-size: 0.8125rem;
|
|
177
|
+
overflow: hidden;
|
|
178
|
+
}
|
|
179
|
+
.sh3-listbox__search {
|
|
180
|
+
width: 100%;
|
|
181
|
+
height: 26px;
|
|
182
|
+
padding: 0 8px;
|
|
183
|
+
background: var(--shell-input-bg);
|
|
184
|
+
border: none;
|
|
185
|
+
border-bottom: 1px solid var(--shell-border);
|
|
186
|
+
color: var(--shell-fg);
|
|
187
|
+
font: inherit;
|
|
188
|
+
outline: none;
|
|
189
|
+
}
|
|
190
|
+
.sh3-listbox__list {
|
|
191
|
+
max-height: 280px;
|
|
192
|
+
overflow-y: auto;
|
|
193
|
+
padding: 4px 0;
|
|
194
|
+
}
|
|
195
|
+
.sh3-listbox__list > div {
|
|
196
|
+
display: flex; align-items: center; gap: 6px;
|
|
197
|
+
padding: 4px 10px;
|
|
198
|
+
cursor: pointer;
|
|
199
|
+
}
|
|
200
|
+
.sh3-listbox__opt--active { background: var(--shell-bg); }
|
|
201
|
+
.sh3-listbox__opt--selected { color: var(--shell-accent); }
|
|
202
|
+
.sh3-listbox__opt--active.sh3-listbox__opt--selected { background: var(--shell-accent); color: var(--shell-fg-on-accent); }
|
|
203
|
+
.sh3-listbox__list > div[aria-disabled="true"] {
|
|
204
|
+
color: var(--shell-fg-subtle);
|
|
205
|
+
cursor: not-allowed;
|
|
206
|
+
}
|
|
207
|
+
.sh3-listbox__check {
|
|
208
|
+
flex-shrink: 0;
|
|
209
|
+
width: 12px;
|
|
210
|
+
height: 12px;
|
|
211
|
+
}
|
|
212
|
+
.sh3-listbox__check--on::before {
|
|
213
|
+
content: "";
|
|
214
|
+
display: block;
|
|
215
|
+
width: 100%;
|
|
216
|
+
height: 100%;
|
|
217
|
+
background: var(--shell-accent);
|
|
218
|
+
clip-path: polygon(14% 44%, 0 60%, 40% 100%, 100% 20%, 85% 8%, 38% 70%);
|
|
219
|
+
}
|
|
220
|
+
.sh3-listbox__opt--active.sh3-listbox__opt--selected .sh3-listbox__check--on::before {
|
|
221
|
+
background: var(--shell-fg-on-accent);
|
|
222
|
+
}
|
|
223
|
+
.sh3-listbox__empty {
|
|
224
|
+
padding: 6px 10px;
|
|
225
|
+
color: var(--shell-fg-muted);
|
|
226
|
+
font-style: italic;
|
|
227
|
+
}
|
|
228
|
+
</style>
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { type SelectOption } from './Select';
|
|
2
|
+
type $$ComponentProps = {
|
|
3
|
+
options: SelectOption[];
|
|
4
|
+
/** Live read of the selected value(s). Returning a closure (rather
|
|
5
|
+
* than passing value as a snapshot) is what lets the listbox
|
|
6
|
+
* re-render checkmarks across multiple selections without unmounting. */
|
|
7
|
+
getValue: () => string | string[];
|
|
8
|
+
multiple: boolean;
|
|
9
|
+
onSelect: (v: string) => void;
|
|
10
|
+
/** Called by listbox-initiated close paths so the trigger can
|
|
11
|
+
* restore its open-flag and focus. Outside-click and Escape
|
|
12
|
+
* paths are handled by the popup manager via Select's wrap. */
|
|
13
|
+
onClose?: () => void;
|
|
14
|
+
close: () => void;
|
|
15
|
+
};
|
|
16
|
+
declare const SelectListbox: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
17
|
+
type SelectListbox = ReturnType<typeof SelectListbox>;
|
|
18
|
+
export default SelectListbox;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from 'vitest';
|
|
2
|
+
import { MemoryDocumentBackend } from '../documents/backends';
|
|
3
|
+
import { __setDocumentBackend, __setTenantId } from '../documents/config';
|
|
4
|
+
import { registerShard, activateShard, registeredShards, activeShards, __resetShardRegistryForTest, erroredShards, } from './activate.svelte';
|
|
5
|
+
describe('erroredShards map', () => {
|
|
6
|
+
beforeEach(() => {
|
|
7
|
+
__resetShardRegistryForTest();
|
|
8
|
+
__setDocumentBackend(new MemoryDocumentBackend());
|
|
9
|
+
__setTenantId('tenant-a');
|
|
10
|
+
});
|
|
11
|
+
it('is empty after reset', () => {
|
|
12
|
+
expect(erroredShards.size).toBe(0);
|
|
13
|
+
});
|
|
14
|
+
it('supports the Map read API used by callers', () => {
|
|
15
|
+
expect(typeof erroredShards.has).toBe('function');
|
|
16
|
+
expect(typeof erroredShards.get).toBe('function');
|
|
17
|
+
expect(erroredShards.has('anything')).toBe(false);
|
|
18
|
+
});
|
|
19
|
+
});
|
|
20
|
+
describe('activateShard — unwind on activation failure', () => {
|
|
21
|
+
beforeEach(() => {
|
|
22
|
+
__resetShardRegistryForTest();
|
|
23
|
+
__setDocumentBackend(new MemoryDocumentBackend());
|
|
24
|
+
__setTenantId('tenant-a');
|
|
25
|
+
});
|
|
26
|
+
it('unwinds partial state and records the error when activate throws', async () => {
|
|
27
|
+
const shard = {
|
|
28
|
+
manifest: {
|
|
29
|
+
id: 'broken',
|
|
30
|
+
label: 'Broken',
|
|
31
|
+
version: '0.0.0',
|
|
32
|
+
views: [],
|
|
33
|
+
},
|
|
34
|
+
activate(ctx) {
|
|
35
|
+
ctx.registerView('broken:view', { mount: () => ({ unmount() { } }) });
|
|
36
|
+
throw new Error('dependency missing');
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
registerShard(shard);
|
|
40
|
+
await expect(activateShard('broken')).rejects.toThrow('dependency missing');
|
|
41
|
+
expect(activeShards.has('broken')).toBe(false);
|
|
42
|
+
expect(registeredShards.has('broken')).toBe(true);
|
|
43
|
+
const entry = erroredShards.get('broken');
|
|
44
|
+
expect(entry).toBeDefined();
|
|
45
|
+
expect(entry === null || entry === void 0 ? void 0 : entry.id).toBe('broken');
|
|
46
|
+
expect(entry === null || entry === void 0 ? void 0 : entry.phase).toBe('launch');
|
|
47
|
+
expect(entry === null || entry === void 0 ? void 0 : entry.error).toBeInstanceOf(Error);
|
|
48
|
+
expect(typeof (entry === null || entry === void 0 ? void 0 : entry.timestamp)).toBe('number');
|
|
49
|
+
const { getView } = await import('./registry');
|
|
50
|
+
expect(getView('broken:view')).toBeUndefined();
|
|
51
|
+
});
|
|
52
|
+
it('records phase "autostart" when called with that option', async () => {
|
|
53
|
+
var _a;
|
|
54
|
+
const shard = {
|
|
55
|
+
manifest: { id: 'broken-auto', label: 'B', version: '0.0.0', views: [] },
|
|
56
|
+
activate() {
|
|
57
|
+
throw new Error('no');
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
registerShard(shard);
|
|
61
|
+
await expect(activateShard('broken-auto', { phase: 'autostart' })).rejects.toThrow('no');
|
|
62
|
+
expect((_a = erroredShards.get('broken-auto')) === null || _a === void 0 ? void 0 : _a.phase).toBe('autostart');
|
|
63
|
+
});
|
|
64
|
+
it('clears the error entry when the shard is re-registered', async () => {
|
|
65
|
+
const broken = {
|
|
66
|
+
manifest: { id: 'reborn', label: 'R', version: '0.0.0', views: [] },
|
|
67
|
+
activate() {
|
|
68
|
+
throw new Error('first try');
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
registerShard(broken);
|
|
72
|
+
await expect(activateShard('reborn')).rejects.toThrow('first try');
|
|
73
|
+
expect(erroredShards.has('reborn')).toBe(true);
|
|
74
|
+
const fixed = {
|
|
75
|
+
manifest: { id: 'reborn', label: 'R', version: '0.0.1', views: [] },
|
|
76
|
+
activate() { },
|
|
77
|
+
};
|
|
78
|
+
registerShard(fixed);
|
|
79
|
+
expect(erroredShards.has('reborn')).toBe(false);
|
|
80
|
+
});
|
|
81
|
+
it('clears the error entry when activation eventually succeeds', async () => {
|
|
82
|
+
let shouldFail = true;
|
|
83
|
+
const shard = {
|
|
84
|
+
manifest: { id: 'flaky', label: 'F', version: '0.0.0', views: [] },
|
|
85
|
+
activate() {
|
|
86
|
+
if (shouldFail)
|
|
87
|
+
throw new Error('first try');
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
registerShard(shard);
|
|
91
|
+
await expect(activateShard('flaky')).rejects.toThrow('first try');
|
|
92
|
+
expect(erroredShards.has('flaky')).toBe(true);
|
|
93
|
+
shouldFail = false;
|
|
94
|
+
await activateShard('flaky');
|
|
95
|
+
expect(erroredShards.has('flaky')).toBe(false);
|
|
96
|
+
expect(activeShards.has('flaky')).toBe(true);
|
|
97
|
+
});
|
|
98
|
+
});
|
|
@@ -9,6 +9,19 @@ import type { Shard, ShardContext } from './types';
|
|
|
9
9
|
*/
|
|
10
10
|
export declare const registeredShards: Map<string, Shard>;
|
|
11
11
|
export declare const activeShards: Map<string, Shard>;
|
|
12
|
+
/**
|
|
13
|
+
* Reactive map of shard ids that failed to activate. Populated by
|
|
14
|
+
* `activateShard`'s catch block; cleared when the shard is successfully
|
|
15
|
+
* re-registered or activated. Read-only for shards; intended for diagnostic
|
|
16
|
+
* and admin tooling that wants to surface broken shards to the user.
|
|
17
|
+
*/
|
|
18
|
+
export interface ShardErrorEntry {
|
|
19
|
+
id: string;
|
|
20
|
+
error: unknown;
|
|
21
|
+
phase: 'autostart' | 'launch';
|
|
22
|
+
timestamp: number;
|
|
23
|
+
}
|
|
24
|
+
export declare const erroredShards: Map<string, ShardErrorEntry>;
|
|
12
25
|
/**
|
|
13
26
|
* Register (or re-register) a shard with the framework so it can later be
|
|
14
27
|
* activated. Records the shard in `registeredShards` but does not run
|
|
@@ -20,16 +33,31 @@ export declare const activeShards: Map<string, Shard>;
|
|
|
20
33
|
* activated on next launch.
|
|
21
34
|
*/
|
|
22
35
|
export declare function registerShard(shard: Shard): void;
|
|
36
|
+
export interface ActivateShardOpts {
|
|
37
|
+
/**
|
|
38
|
+
* Where this activation was initiated from. Determines the `phase` field
|
|
39
|
+
* recorded in `erroredShards` if activation fails. Defaults to 'launch'
|
|
40
|
+
* (the common case — required by an app being launched).
|
|
41
|
+
*/
|
|
42
|
+
phase?: 'autostart' | 'launch';
|
|
43
|
+
}
|
|
23
44
|
/**
|
|
24
45
|
* Activate a registered shard. Builds a `ShardContext`, calls `shard.activate`,
|
|
25
46
|
* verifies that every view declared in the manifest received a factory, then
|
|
26
47
|
* calls `shard.autostart` if defined. Idempotent — calling on an already-active
|
|
27
48
|
* shard is a no-op.
|
|
28
49
|
*
|
|
50
|
+
* If `shard.activate` throws, partial state (registered views, verbs,
|
|
51
|
+
* contributions, document handles, actions, env subscription) is unwound
|
|
52
|
+
* and the failure is recorded in `erroredShards` before the error is
|
|
53
|
+
* re-thrown. Callers in `host.ts` (autostart loop) and `launchApp`
|
|
54
|
+
* (required-shard loop) decide how to react.
|
|
55
|
+
*
|
|
29
56
|
* @param id - The `ShardManifest.id` of the shard to activate. Must be registered.
|
|
30
|
-
* @
|
|
57
|
+
* @param opts - Optional. `phase` is recorded in `erroredShards` on failure (default 'launch').
|
|
58
|
+
* @throws If the shard is not registered, if `shard.activate` throws, or if a manifest view has no factory after activation.
|
|
31
59
|
*/
|
|
32
|
-
export declare function activateShard(id: string): Promise<void>;
|
|
60
|
+
export declare function activateShard(id: string, opts?: ActivateShardOpts): Promise<void>;
|
|
33
61
|
/**
|
|
34
62
|
* Deactivate an active shard. Calls `shard.deactivate`, flushes and disposes
|
|
35
63
|
* all document handles, unregisters all view factories, and removes the shard
|
|
@@ -47,6 +47,7 @@ export const registeredShards = $state(new Map());
|
|
|
47
47
|
*/
|
|
48
48
|
const active = new Map();
|
|
49
49
|
export const activeShards = $state(new Map());
|
|
50
|
+
export const erroredShards = $state(new Map());
|
|
50
51
|
/**
|
|
51
52
|
* Register (or re-register) a shard with the framework so it can later be
|
|
52
53
|
* activated. Records the shard in `registeredShards` but does not run
|
|
@@ -63,6 +64,9 @@ export function registerShard(shard) {
|
|
|
63
64
|
deactivateShard(id);
|
|
64
65
|
}
|
|
65
66
|
registeredShards.set(id, shard);
|
|
67
|
+
// Re-registering wipes any prior error: the new shard module gets a
|
|
68
|
+
// clean slate, and a hot-reload of a fixed shard removes the stale entry.
|
|
69
|
+
erroredShards.delete(id);
|
|
66
70
|
}
|
|
67
71
|
/**
|
|
68
72
|
* Activate a registered shard. Builds a `ShardContext`, calls `shard.activate`,
|
|
@@ -70,11 +74,18 @@ export function registerShard(shard) {
|
|
|
70
74
|
* calls `shard.autostart` if defined. Idempotent — calling on an already-active
|
|
71
75
|
* shard is a no-op.
|
|
72
76
|
*
|
|
77
|
+
* If `shard.activate` throws, partial state (registered views, verbs,
|
|
78
|
+
* contributions, document handles, actions, env subscription) is unwound
|
|
79
|
+
* and the failure is recorded in `erroredShards` before the error is
|
|
80
|
+
* re-thrown. Callers in `host.ts` (autostart loop) and `launchApp`
|
|
81
|
+
* (required-shard loop) decide how to react.
|
|
82
|
+
*
|
|
73
83
|
* @param id - The `ShardManifest.id` of the shard to activate. Must be registered.
|
|
74
|
-
* @
|
|
84
|
+
* @param opts - Optional. `phase` is recorded in `erroredShards` on failure (default 'launch').
|
|
85
|
+
* @throws If the shard is not registered, if `shard.activate` throws, or if a manifest view has no factory after activation.
|
|
75
86
|
*/
|
|
76
|
-
export async function activateShard(id) {
|
|
77
|
-
var _a, _b, _c, _d, _e;
|
|
87
|
+
export async function activateShard(id, opts) {
|
|
88
|
+
var _a, _b, _c, _d, _e, _f;
|
|
78
89
|
const shard = registeredShards.get(id);
|
|
79
90
|
if (!shard) {
|
|
80
91
|
throw new Error(`Cannot activate shard "${id}": not registered`);
|
|
@@ -201,24 +212,57 @@ export async function activateShard(id) {
|
|
|
201
212
|
}
|
|
202
213
|
active.set(id, entry);
|
|
203
214
|
activeShards.set(id, shard);
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
215
|
+
try {
|
|
216
|
+
await shard.activate(ctx);
|
|
217
|
+
for (const view of shard.manifest.views) {
|
|
218
|
+
if (!entry.viewIds.has(view.id)) {
|
|
219
|
+
throw new Error(`Shard "${id}" declared view "${view.id}" in its manifest but registered no factory for it.`);
|
|
220
|
+
}
|
|
208
221
|
}
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
222
|
+
// Hydrate env state if the shard declared it via ctx.env().
|
|
223
|
+
if (envState.proxy && envState.defaults) {
|
|
224
|
+
try {
|
|
225
|
+
const stored = await fetchEnvState(id);
|
|
226
|
+
const merged = Object.assign({}, envState.defaults, stored);
|
|
227
|
+
Object.assign(envState.proxy, merged);
|
|
228
|
+
}
|
|
229
|
+
catch (err) {
|
|
230
|
+
console.warn(`[sh3] Failed to hydrate env state for shard "${id}":`, err instanceof Error ? err.message : err);
|
|
231
|
+
}
|
|
216
232
|
}
|
|
217
|
-
|
|
218
|
-
|
|
233
|
+
}
|
|
234
|
+
catch (err) {
|
|
235
|
+
// Unwind partial state. Mirror deactivateShard's body, minus the
|
|
236
|
+
// shard.deactivate?.() call — the shard never finished activating.
|
|
237
|
+
// Each cleanup fn runs inside its own swallow so a teardown failure
|
|
238
|
+
// cannot mask the original activation error.
|
|
239
|
+
for (const fn of entry.cleanupFns) {
|
|
240
|
+
try {
|
|
241
|
+
void fn();
|
|
242
|
+
}
|
|
243
|
+
catch (_g) {
|
|
244
|
+
// intentionally swallowed: original error is what matters.
|
|
245
|
+
}
|
|
219
246
|
}
|
|
247
|
+
for (const name of entry.verbNames)
|
|
248
|
+
fwUnregisterVerb(name);
|
|
249
|
+
for (const viewId of entry.viewIds)
|
|
250
|
+
unregisterView(viewId);
|
|
251
|
+
clearSelectionForShard(id);
|
|
252
|
+
active.delete(id);
|
|
253
|
+
activeShards.delete(id);
|
|
254
|
+
erroredShards.set(id, {
|
|
255
|
+
id,
|
|
256
|
+
error: err,
|
|
257
|
+
phase: (_e = opts === null || opts === void 0 ? void 0 : opts.phase) !== null && _e !== void 0 ? _e : 'launch',
|
|
258
|
+
timestamp: Date.now(),
|
|
259
|
+
});
|
|
260
|
+
console.error(`[sh3] Shard "${id}" failed to activate:`, err);
|
|
261
|
+
throw err;
|
|
220
262
|
}
|
|
221
|
-
|
|
263
|
+
// Activation succeeded — clear any prior error record for this shard.
|
|
264
|
+
erroredShards.delete(id);
|
|
265
|
+
void ((_f = shard.autostart) === null || _f === void 0 ? void 0 : _f.call(shard, ctx));
|
|
222
266
|
}
|
|
223
267
|
/**
|
|
224
268
|
* Deactivate an active shard. Calls `shard.deactivate`, flushes and disposes
|
|
@@ -288,4 +332,5 @@ export function __resetShardRegistryForTest() {
|
|
|
288
332
|
active.clear();
|
|
289
333
|
activeShards.clear();
|
|
290
334
|
registeredShards.clear();
|
|
335
|
+
erroredShards.clear();
|
|
291
336
|
}
|
|
@@ -78,10 +78,6 @@
|
|
|
78
78
|
let focusLocked = $state(false);
|
|
79
79
|
let targetShard = $state<string | null>(null);
|
|
80
80
|
|
|
81
|
-
function toggleFocusLock(): void {
|
|
82
|
-
focusLocked = !focusLocked;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
81
|
// Toolbar slot registry
|
|
86
82
|
const toolbarRegistry = new ToolbarSlotRegistry();
|
|
87
83
|
toolbarRegistry.register({ id: 'mode', order: 10, visible: () => true, component: ModeSlot });
|
|
@@ -101,6 +97,7 @@
|
|
|
101
97
|
const found = getActiveViewId(child);
|
|
102
98
|
if (found !== null) return found;
|
|
103
99
|
}
|
|
100
|
+
return null;
|
|
104
101
|
}
|
|
105
102
|
// slot node
|
|
106
103
|
return node.viewId ?? null;
|
|
@@ -13,6 +13,7 @@ import { pwdVerb, cdVerb, whoamiVerb } from './session';
|
|
|
13
13
|
import { envVerb } from './env';
|
|
14
14
|
import { lsVerb } from './ls';
|
|
15
15
|
import { catVerb } from './cat';
|
|
16
|
+
import { resetVerb } from './reset';
|
|
16
17
|
export function registerV1Verbs(ctx) {
|
|
17
18
|
ctx.registerVerb(makeHelpVerb());
|
|
18
19
|
ctx.registerVerb(clearVerb);
|
|
@@ -33,4 +34,5 @@ export function registerV1Verbs(ctx) {
|
|
|
33
34
|
ctx.registerVerb(whoamiVerb);
|
|
34
35
|
ctx.registerVerb(lsVerb);
|
|
35
36
|
ctx.registerVerb(catVerb);
|
|
37
|
+
ctx.registerVerb(resetVerb);
|
|
36
38
|
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { wipeUserData } from '../../platform/index';
|
|
2
|
+
export const resetVerb = {
|
|
3
|
+
name: 'reset',
|
|
4
|
+
summary: 'Wipe SH3 state and reload (preserves documents). Requires --confirm.',
|
|
5
|
+
async run(ctx, args) {
|
|
6
|
+
if (!args.includes('--confirm')) {
|
|
7
|
+
ctx.scrollback.push({
|
|
8
|
+
kind: 'status',
|
|
9
|
+
text: 'reset: erases the package registry, all zone state (including Tauri stores), ' +
|
|
10
|
+
'and theme overrides, then reloads. Documents are preserved. ' +
|
|
11
|
+
'Re-run as `reset --confirm` to proceed.',
|
|
12
|
+
level: 'warn',
|
|
13
|
+
ts: Date.now(),
|
|
14
|
+
});
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
ctx.scrollback.push({
|
|
18
|
+
kind: 'status',
|
|
19
|
+
text: 'reset: wiping data…',
|
|
20
|
+
level: 'info',
|
|
21
|
+
ts: Date.now(),
|
|
22
|
+
});
|
|
23
|
+
await wipeUserData();
|
|
24
|
+
location.reload();
|
|
25
|
+
},
|
|
26
|
+
};
|