sh3-core 0.12.0 → 0.13.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/__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 +17 -6
- package/dist/actions/listeners.test.js +42 -2
- package/dist/api.d.ts +16 -0
- package/dist/api.js +14 -0
- package/dist/apps/lifecycle.js +3 -0
- package/dist/apps/lifecycle.test.js +45 -0
- package/dist/host.js +12 -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 +127 -0
- package/dist/primitives/widgets/Field.svelte.d.ts +20 -0
- package/dist/primitives/widgets/FilePicker.d.ts +3 -0
- package/dist/primitives/widgets/FilePicker.js +19 -0
- package/dist/primitives/widgets/FilePicker.svelte +82 -0
- package/dist/primitives/widgets/FilePicker.svelte.d.ts +14 -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 +89 -0
- package/dist/primitives/widgets/IconToggleGroup.svelte.d.ts +17 -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 +176 -0
- package/dist/primitives/widgets/NumberInput.svelte.d.ts +18 -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 +128 -0
- package/dist/primitives/widgets/RangeSlider.svelte.d.ts +14 -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 +89 -0
- package/dist/primitives/widgets/Segmented.svelte.d.ts +11 -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 +167 -0
- package/dist/primitives/widgets/Select.svelte.d.ts +15 -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 +208 -0
- package/dist/primitives/widgets/Slider.svelte.d.ts +16 -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 +61 -0
- package/dist/primitives/widgets/SliderGroup.svelte.d.ts +19 -0
- package/dist/primitives/widgets/Textarea.svelte +84 -0
- package/dist/primitives/widgets/Textarea.svelte.d.ts +17 -0
- package/dist/primitives/widgets/_select-listbox.svelte +228 -0
- package/dist/primitives/widgets/_select-listbox.svelte.d.ts +18 -0
- 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 +34 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Web platform emitter for the navigation back-cascade.
|
|
3
|
+
*
|
|
4
|
+
* Browser back/forward (and mouse X1/X2 buttons, which the browser maps
|
|
5
|
+
* to history navigation) all funnel through `popstate`. The event itself
|
|
6
|
+
* doesn't report direction, so we maintain a three-state "sandwich":
|
|
7
|
+
*
|
|
8
|
+
* position 0: { sh3: 'anchor' } ← popstate here = back was pressed
|
|
9
|
+
* position 1: { sh3: 'main' } ← resting position
|
|
10
|
+
* position 2: { sh3: 'forward-bumper' } ← popstate here = forward was pressed
|
|
11
|
+
*
|
|
12
|
+
* On every consumed signal we re-anchor to 'main' so the user never
|
|
13
|
+
* navigates out of SH3 via the back/forward chord.
|
|
14
|
+
*
|
|
15
|
+
* Page reload re-runs install. The two extra synthetic history entries
|
|
16
|
+
* are accepted noise; no URL changes.
|
|
17
|
+
*
|
|
18
|
+
* Known limitation: third-party `pushState` (HMR, libraries) clobbers the
|
|
19
|
+
* forward-bumper. SH3 framework code must not call pushState directly.
|
|
20
|
+
*/
|
|
21
|
+
import { dispatchBack, dispatchForward } from './back-stack';
|
|
22
|
+
let installed = false;
|
|
23
|
+
let listener = null;
|
|
24
|
+
export function installWebEmitter() {
|
|
25
|
+
if (installed)
|
|
26
|
+
return;
|
|
27
|
+
installed = true;
|
|
28
|
+
listener = (e) => {
|
|
29
|
+
var _a;
|
|
30
|
+
const tag = (_a = e.state) === null || _a === void 0 ? void 0 : _a.sh3;
|
|
31
|
+
if (tag === 'anchor') {
|
|
32
|
+
dispatchBack();
|
|
33
|
+
history.forward();
|
|
34
|
+
}
|
|
35
|
+
else if (tag === 'forward-bumper') {
|
|
36
|
+
dispatchForward();
|
|
37
|
+
history.back();
|
|
38
|
+
}
|
|
39
|
+
// tag === 'main' (or undefined) → echo from our own correction; ignore.
|
|
40
|
+
};
|
|
41
|
+
window.addEventListener('popstate', listener);
|
|
42
|
+
history.replaceState({ sh3: 'anchor' }, '');
|
|
43
|
+
history.pushState({ sh3: 'main' }, '');
|
|
44
|
+
history.pushState({ sh3: 'forward-bumper' }, '');
|
|
45
|
+
history.back();
|
|
46
|
+
}
|
|
47
|
+
/** @internal — test cleanup. Removes the listener; does not unwind history. */
|
|
48
|
+
export function __uninstallWebEmitterForTest() {
|
|
49
|
+
if (listener) {
|
|
50
|
+
window.removeEventListener('popstate', listener);
|
|
51
|
+
listener = null;
|
|
52
|
+
}
|
|
53
|
+
installed = false;
|
|
54
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Note: happy-dom does not simulate `history.back()` / `history.forward()`
|
|
3
|
+
* (state stays put). We can't drive the listener via the real navigation
|
|
4
|
+
* API in this environment, so we test what we own — listener wiring and
|
|
5
|
+
* dispatch routing — by dispatching synthetic PopStateEvents directly.
|
|
6
|
+
* The trailing `history.back()` in installWebEmitter is a no-op here but
|
|
7
|
+
* is real in browsers and Tauri webviews; manual verification (Task 7 of
|
|
8
|
+
* the plan) covers the actual navigation behavior.
|
|
9
|
+
*/
|
|
10
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
11
|
+
import { installWebEmitter, __uninstallWebEmitterForTest } from './platform-web';
|
|
12
|
+
import { __resetBackStackForTest, __setLifecycleHandlersForTest, } from './back-stack';
|
|
13
|
+
import { activeApp, breadcrumbApp } from '../apps/registry.svelte';
|
|
14
|
+
beforeEach(() => {
|
|
15
|
+
__resetBackStackForTest();
|
|
16
|
+
activeApp.id = null;
|
|
17
|
+
breadcrumbApp.id = null;
|
|
18
|
+
});
|
|
19
|
+
afterEach(() => {
|
|
20
|
+
__uninstallWebEmitterForTest();
|
|
21
|
+
});
|
|
22
|
+
// happy-dom's PopStateEvent constructor ignores the `state` init dict, so
|
|
23
|
+
// we attach state manually via defineProperty before dispatching.
|
|
24
|
+
function firePopState(state) {
|
|
25
|
+
const e = new PopStateEvent('popstate');
|
|
26
|
+
Object.defineProperty(e, 'state', { value: state });
|
|
27
|
+
window.dispatchEvent(e);
|
|
28
|
+
}
|
|
29
|
+
const fireAnchor = () => firePopState({ sh3: 'anchor' });
|
|
30
|
+
const fireForwardBumper = () => firePopState({ sh3: 'forward-bumper' });
|
|
31
|
+
const fireMain = () => firePopState({ sh3: 'main' });
|
|
32
|
+
describe('platform-web — sentinel install', () => {
|
|
33
|
+
it('installs without throwing and pushes the three sentinel entries', () => {
|
|
34
|
+
const before = history.length;
|
|
35
|
+
installWebEmitter();
|
|
36
|
+
// pushState appends; replaceState doesn't. We expect at least +2 entries
|
|
37
|
+
// (the two pushStates; replaceState replaced the user's current entry).
|
|
38
|
+
expect(history.length).toBeGreaterThanOrEqual(before + 2);
|
|
39
|
+
});
|
|
40
|
+
it('install is idempotent — second call does nothing', () => {
|
|
41
|
+
installWebEmitter();
|
|
42
|
+
const after1 = history.length;
|
|
43
|
+
installWebEmitter();
|
|
44
|
+
expect(history.length).toBe(after1);
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
describe('platform-web — back signal', () => {
|
|
48
|
+
it('an anchor-tagged popstate fires dispatchBack', () => {
|
|
49
|
+
activeApp.id = 'app';
|
|
50
|
+
const returnToHome = vi.fn();
|
|
51
|
+
__setLifecycleHandlersForTest({ returnToHome, launchApp: vi.fn() });
|
|
52
|
+
installWebEmitter();
|
|
53
|
+
fireAnchor();
|
|
54
|
+
expect(returnToHome).toHaveBeenCalledTimes(1);
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
describe('platform-web — forward signal', () => {
|
|
58
|
+
it('a forward-bumper-tagged popstate fires dispatchForward', () => {
|
|
59
|
+
activeApp.id = null;
|
|
60
|
+
breadcrumbApp.id = 'last-app';
|
|
61
|
+
const launchApp = vi.fn();
|
|
62
|
+
__setLifecycleHandlersForTest({ returnToHome: vi.fn(), launchApp });
|
|
63
|
+
installWebEmitter();
|
|
64
|
+
fireForwardBumper();
|
|
65
|
+
expect(launchApp).toHaveBeenCalledWith('last-app');
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
describe('platform-web — main echo is ignored', () => {
|
|
69
|
+
it('a popstate tagged main does not fire dispatch', () => {
|
|
70
|
+
activeApp.id = 'app';
|
|
71
|
+
const returnToHome = vi.fn();
|
|
72
|
+
__setLifecycleHandlersForTest({ returnToHome, launchApp: vi.fn() });
|
|
73
|
+
installWebEmitter();
|
|
74
|
+
fireMain();
|
|
75
|
+
expect(returnToHome).not.toHaveBeenCalled();
|
|
76
|
+
});
|
|
77
|
+
it('a popstate with no state (untagged) does not fire dispatch', () => {
|
|
78
|
+
activeApp.id = 'app';
|
|
79
|
+
const returnToHome = vi.fn();
|
|
80
|
+
__setLifecycleHandlersForTest({ returnToHome, launchApp: vi.fn() });
|
|
81
|
+
installWebEmitter();
|
|
82
|
+
firePopState(null);
|
|
83
|
+
expect(returnToHome).not.toHaveBeenCalled();
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
describe('platform-web — uninstall', () => {
|
|
87
|
+
it('after __uninstall, popstates no longer fire dispatch', () => {
|
|
88
|
+
activeApp.id = 'app';
|
|
89
|
+
const returnToHome = vi.fn();
|
|
90
|
+
__setLifecycleHandlersForTest({ returnToHome, launchApp: vi.fn() });
|
|
91
|
+
installWebEmitter();
|
|
92
|
+
__uninstallWebEmitterForTest();
|
|
93
|
+
fireAnchor();
|
|
94
|
+
expect(returnToHome).not.toHaveBeenCalled();
|
|
95
|
+
});
|
|
96
|
+
});
|
package/dist/overlays/modal.js
CHANGED
|
@@ -39,6 +39,7 @@
|
|
|
39
39
|
import { mount, unmount } from 'svelte';
|
|
40
40
|
import ModalFrame from './ModalFrame.svelte';
|
|
41
41
|
import { getLayerRoot } from './roots';
|
|
42
|
+
import { registerDismissable } from '../navigation/back-stack';
|
|
42
43
|
const stack = [];
|
|
43
44
|
let escapeInstalled = false;
|
|
44
45
|
let backdrop = null;
|
|
@@ -116,6 +117,12 @@ function openModal(Content, props, options) {
|
|
|
116
117
|
const handle = {
|
|
117
118
|
close: () => removeEntry(entry),
|
|
118
119
|
};
|
|
120
|
+
const dismissReg = registerDismissable(() => handle.close());
|
|
121
|
+
const originalClose = handle.close;
|
|
122
|
+
handle.close = () => {
|
|
123
|
+
dismissReg.unregister();
|
|
124
|
+
originalClose();
|
|
125
|
+
};
|
|
119
126
|
const frame = mount(ModalFrame, {
|
|
120
127
|
target: host,
|
|
121
128
|
props: {
|
|
@@ -53,3 +53,38 @@ describe('modal — backdrop dismiss policy', () => {
|
|
|
53
53
|
expect(layerRoot.querySelector('.sh3-modal-host')).not.toBeNull();
|
|
54
54
|
});
|
|
55
55
|
});
|
|
56
|
+
describe('modal — back-cascade integration', () => {
|
|
57
|
+
let layerRoot;
|
|
58
|
+
beforeEach(() => {
|
|
59
|
+
layerRoot = makeLayerRoot();
|
|
60
|
+
});
|
|
61
|
+
afterEach(() => {
|
|
62
|
+
modalManager.closeAll();
|
|
63
|
+
teardownLayerRoot(layerRoot);
|
|
64
|
+
});
|
|
65
|
+
it('opens register a dismissable; back closes the topmost modal', async () => {
|
|
66
|
+
const { dispatchBack, __resetBackStackForTest } = await import('../navigation/back-stack');
|
|
67
|
+
__resetBackStackForTest();
|
|
68
|
+
modalManager.open(DummyFrame, {});
|
|
69
|
+
modalManager.open(DummyFrame, {});
|
|
70
|
+
await tick();
|
|
71
|
+
expect(layerRoot.querySelectorAll('.sh3-modal-host').length).toBe(2);
|
|
72
|
+
dispatchBack();
|
|
73
|
+
await tick();
|
|
74
|
+
expect(layerRoot.querySelectorAll('.sh3-modal-host').length).toBe(1);
|
|
75
|
+
dispatchBack();
|
|
76
|
+
await tick();
|
|
77
|
+
expect(layerRoot.querySelectorAll('.sh3-modal-host').length).toBe(0);
|
|
78
|
+
});
|
|
79
|
+
it('programmatic close unregisters the dismissable (no double-close)', async () => {
|
|
80
|
+
const { dispatchBack, __resetBackStackForTest } = await import('../navigation/back-stack');
|
|
81
|
+
__resetBackStackForTest();
|
|
82
|
+
const handle = modalManager.open(DummyFrame, {});
|
|
83
|
+
await tick();
|
|
84
|
+
handle.close();
|
|
85
|
+
handle.close();
|
|
86
|
+
await tick();
|
|
87
|
+
expect(() => dispatchBack()).not.toThrow();
|
|
88
|
+
expect(layerRoot.querySelectorAll('.sh3-modal-host').length).toBe(0);
|
|
89
|
+
});
|
|
90
|
+
});
|
package/dist/overlays/popup.js
CHANGED
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
import { mount, unmount } from 'svelte';
|
|
29
29
|
import PopupFrame from './PopupFrame.svelte';
|
|
30
30
|
import { getLayerRoot } from './roots';
|
|
31
|
+
import { registerDismissable } from '../navigation/back-stack';
|
|
31
32
|
/**
|
|
32
33
|
* Convert a PopupAnchor to a DOMRect.
|
|
33
34
|
* - HTMLElement: uses its live bounding rect.
|
|
@@ -91,6 +92,12 @@ function showPopup(Content, options, props) {
|
|
|
91
92
|
const handle = {
|
|
92
93
|
close: () => removeEntry(entry),
|
|
93
94
|
};
|
|
95
|
+
const dismissReg = registerDismissable(() => handle.close());
|
|
96
|
+
const originalClose = handle.close;
|
|
97
|
+
handle.close = () => {
|
|
98
|
+
dismissReg.unregister();
|
|
99
|
+
originalClose();
|
|
100
|
+
};
|
|
94
101
|
const frame = mount(PopupFrame, {
|
|
95
102
|
target: host,
|
|
96
103
|
props: {
|
|
@@ -93,3 +93,36 @@ describe('popup — P.2 HTMLElement anchor regression', () => {
|
|
|
93
93
|
anchor.remove();
|
|
94
94
|
});
|
|
95
95
|
});
|
|
96
|
+
describe('popup — back-cascade integration', () => {
|
|
97
|
+
let layerRoot;
|
|
98
|
+
beforeEach(() => {
|
|
99
|
+
vi.stubGlobal('innerWidth', 2000);
|
|
100
|
+
vi.stubGlobal('innerHeight', 2000);
|
|
101
|
+
layerRoot = makeLayerRoot();
|
|
102
|
+
});
|
|
103
|
+
afterEach(() => {
|
|
104
|
+
__resetPopupManagerForTest();
|
|
105
|
+
teardownLayerRoot(layerRoot);
|
|
106
|
+
vi.unstubAllGlobals();
|
|
107
|
+
});
|
|
108
|
+
it('opening a popup registers a dismissable; back closes it', async () => {
|
|
109
|
+
const { dispatchBack, __resetBackStackForTest } = await import('../navigation/back-stack');
|
|
110
|
+
__resetBackStackForTest();
|
|
111
|
+
popupManager.show(DummyFrame, { anchor: { x: 100, y: 100 } }, {});
|
|
112
|
+
await tick();
|
|
113
|
+
expect(layerRoot.querySelector('.sh3-popup-host')).not.toBeNull();
|
|
114
|
+
dispatchBack();
|
|
115
|
+
await tick();
|
|
116
|
+
expect(layerRoot.querySelector('.sh3-popup-host')).toBeNull();
|
|
117
|
+
});
|
|
118
|
+
it('programmatic close does not leave a stale dismissable', async () => {
|
|
119
|
+
const { dispatchBack, __resetBackStackForTest } = await import('../navigation/back-stack');
|
|
120
|
+
__resetBackStackForTest();
|
|
121
|
+
const handle = popupManager.show(DummyFrame, { anchor: { x: 100, y: 100 } }, {});
|
|
122
|
+
await tick();
|
|
123
|
+
handle.close();
|
|
124
|
+
await tick();
|
|
125
|
+
expect(() => dispatchBack()).not.toThrow();
|
|
126
|
+
expect(layerRoot.querySelector('.sh3-popup-host')).toBeNull();
|
|
127
|
+
});
|
|
128
|
+
});
|
package/dist/platform/index.d.ts
CHANGED
|
@@ -8,3 +8,18 @@ export interface PlatformResult {
|
|
|
8
8
|
localOwner: boolean;
|
|
9
9
|
}
|
|
10
10
|
export declare function resolvePlatform(): Promise<PlatformResult>;
|
|
11
|
+
/**
|
|
12
|
+
* Erase all persisted SH3 state for this user/device.
|
|
13
|
+
*
|
|
14
|
+
* Wipes:
|
|
15
|
+
* - Tauri plugin-store files (`workspace.json`, `user.json`) when running
|
|
16
|
+
* inside a Tauri webview
|
|
17
|
+
* - localStorage keys with the `sh3:` prefix (web zones + theme overrides;
|
|
18
|
+
* also active inside the Tauri webview)
|
|
19
|
+
* - the `sh3-packages` IndexedDB database (installed bundle registry)
|
|
20
|
+
*
|
|
21
|
+
* Intentionally preserves the `sh3-documents` IndexedDB so user-authored
|
|
22
|
+
* content survives a reset. Callers are expected to follow this with a
|
|
23
|
+
* `location.reload()` so a clean boot picks up the cleared state.
|
|
24
|
+
*/
|
|
25
|
+
export declare function wipeUserData(): Promise<void>;
|
package/dist/platform/index.js
CHANGED
|
@@ -31,3 +31,50 @@ export async function resolvePlatform() {
|
|
|
31
31
|
return { backends: null, localOwner: DEV };
|
|
32
32
|
}
|
|
33
33
|
}
|
|
34
|
+
/**
|
|
35
|
+
* Erase all persisted SH3 state for this user/device.
|
|
36
|
+
*
|
|
37
|
+
* Wipes:
|
|
38
|
+
* - Tauri plugin-store files (`workspace.json`, `user.json`) when running
|
|
39
|
+
* inside a Tauri webview
|
|
40
|
+
* - localStorage keys with the `sh3:` prefix (web zones + theme overrides;
|
|
41
|
+
* also active inside the Tauri webview)
|
|
42
|
+
* - the `sh3-packages` IndexedDB database (installed bundle registry)
|
|
43
|
+
*
|
|
44
|
+
* Intentionally preserves the `sh3-documents` IndexedDB so user-authored
|
|
45
|
+
* content survives a reset. Callers are expected to follow this with a
|
|
46
|
+
* `location.reload()` so a clean boot picks up the cleared state.
|
|
47
|
+
*/
|
|
48
|
+
export async function wipeUserData() {
|
|
49
|
+
// Tauri stores live on disk via plugin-store; localStorage/IDB don't reach them.
|
|
50
|
+
try {
|
|
51
|
+
const { LazyStore } = await import('@tauri-apps/plugin-store');
|
|
52
|
+
const workspace = new LazyStore('workspace.json', { defaults: {}, autoSave: true });
|
|
53
|
+
const user = new LazyStore('user.json', { defaults: {}, autoSave: true });
|
|
54
|
+
await workspace.clear();
|
|
55
|
+
await user.clear();
|
|
56
|
+
}
|
|
57
|
+
catch (_a) {
|
|
58
|
+
// Not in Tauri — nothing to clear on disk.
|
|
59
|
+
}
|
|
60
|
+
if (typeof localStorage !== 'undefined') {
|
|
61
|
+
const keys = [];
|
|
62
|
+
for (let i = 0; i < localStorage.length; i++) {
|
|
63
|
+
const k = localStorage.key(i);
|
|
64
|
+
if (k && k.startsWith('sh3:'))
|
|
65
|
+
keys.push(k);
|
|
66
|
+
}
|
|
67
|
+
for (const k of keys)
|
|
68
|
+
localStorage.removeItem(k);
|
|
69
|
+
}
|
|
70
|
+
if (typeof indexedDB !== 'undefined') {
|
|
71
|
+
await new Promise((resolve) => {
|
|
72
|
+
const req = indexedDB.deleteDatabase('sh3-packages');
|
|
73
|
+
req.onsuccess = () => resolve();
|
|
74
|
+
// Swallow errors/blocked: a stale lock shouldn't abort the wipe;
|
|
75
|
+
// the next boot will re-create the DB empty either way.
|
|
76
|
+
req.onerror = () => resolve();
|
|
77
|
+
req.onblocked = () => resolve();
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
}
|
package/dist/primitives/base.css
CHANGED
|
@@ -52,7 +52,8 @@ input[type="tel"],
|
|
|
52
52
|
input[type="number"],
|
|
53
53
|
textarea,
|
|
54
54
|
.shell-base-input {
|
|
55
|
-
padding: var(--shell-
|
|
55
|
+
padding: 0 var(--shell-field-pad-x);
|
|
56
|
+
height: var(--shell-field-height-md);
|
|
56
57
|
background: var(--shell-input-bg);
|
|
57
58
|
color: var(--shell-fg);
|
|
58
59
|
border: 1px solid var(--shell-border);
|
|
@@ -82,6 +83,8 @@ textarea:disabled,
|
|
|
82
83
|
textarea {
|
|
83
84
|
resize: vertical;
|
|
84
85
|
min-height: calc(var(--shell-line) * 3em);
|
|
86
|
+
height: auto;
|
|
87
|
+
padding: var(--shell-pad-sm) var(--shell-field-pad-x);
|
|
85
88
|
}
|
|
86
89
|
|
|
87
90
|
/* ── Checkbox & radio ────────────────────────────────────────────────── */
|
|
@@ -103,8 +106,10 @@ input[type="radio"].shell-base-radio {
|
|
|
103
106
|
.shell-base-check { border-radius: var(--shell-radius-sm); }
|
|
104
107
|
.shell-base-radio { border-radius: 50%; }
|
|
105
108
|
|
|
106
|
-
|
|
107
|
-
.shell-base-
|
|
109
|
+
/* Need the input[type] prefix to outweigh the base
|
|
110
|
+
`input[type="checkbox"].shell-base-check` rule's specificity. */
|
|
111
|
+
input[type="checkbox"].shell-base-check:checked,
|
|
112
|
+
input[type="radio"].shell-base-radio:checked {
|
|
108
113
|
background: var(--shell-accent);
|
|
109
114
|
border-color: var(--shell-accent);
|
|
110
115
|
}
|
|
@@ -145,11 +150,13 @@ input[type="checkbox"].shell-base-switch {
|
|
|
145
150
|
width: 28px;
|
|
146
151
|
height: 16px;
|
|
147
152
|
margin: 0;
|
|
148
|
-
background: var(--shell-
|
|
153
|
+
background: var(--shell-bg-sunken);
|
|
154
|
+
border: 1px solid var(--shell-border-strong);
|
|
149
155
|
border-radius: 999px;
|
|
150
156
|
cursor: pointer;
|
|
151
|
-
transition: background 120ms ease;
|
|
157
|
+
transition: background 120ms ease, border-color 120ms ease, box-shadow 120ms ease;
|
|
152
158
|
flex-shrink: 0;
|
|
159
|
+
box-sizing: border-box;
|
|
153
160
|
}
|
|
154
161
|
|
|
155
162
|
.shell-base-switch::before {
|
|
@@ -164,7 +171,11 @@ input[type="checkbox"].shell-base-switch {
|
|
|
164
171
|
transition: transform 120ms ease;
|
|
165
172
|
}
|
|
166
173
|
|
|
167
|
-
.shell-base-switch:checked {
|
|
174
|
+
input[type="checkbox"].shell-base-switch:checked {
|
|
175
|
+
background: var(--shell-accent);
|
|
176
|
+
border-color: var(--shell-accent);
|
|
177
|
+
box-shadow: 0 0 0 2px color-mix(in srgb, var(--shell-accent) 25%, transparent);
|
|
178
|
+
}
|
|
168
179
|
.shell-base-switch:checked::before {
|
|
169
180
|
transform: translateX(12px);
|
|
170
181
|
background: var(--shell-fg-on-accent);
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { shell } from '../../shellRuntime.svelte';
|
|
3
|
+
|
|
4
|
+
let {
|
|
5
|
+
value = $bindable('#000000'),
|
|
6
|
+
label,
|
|
7
|
+
disabled = false,
|
|
8
|
+
size = 'md',
|
|
9
|
+
}: {
|
|
10
|
+
value?: string;
|
|
11
|
+
label?: string;
|
|
12
|
+
disabled?: boolean;
|
|
13
|
+
size?: 'sm' | 'md';
|
|
14
|
+
} = $props();
|
|
15
|
+
|
|
16
|
+
let trigger: HTMLButtonElement | undefined;
|
|
17
|
+
|
|
18
|
+
async function open() {
|
|
19
|
+
if (disabled) return;
|
|
20
|
+
const result = await shell.color.pick({ initial: value, anchor: trigger });
|
|
21
|
+
if (result !== null && result !== undefined) value = result;
|
|
22
|
+
}
|
|
23
|
+
</script>
|
|
24
|
+
|
|
25
|
+
<label class="sh3-swatch" class:sh3-swatch--sm={size === 'sm'}>
|
|
26
|
+
{#if label}<span class="sh3-swatch__label">{label}</span>{/if}
|
|
27
|
+
<button
|
|
28
|
+
type="button"
|
|
29
|
+
class="sh3-swatch__btn"
|
|
30
|
+
bind:this={trigger}
|
|
31
|
+
{disabled}
|
|
32
|
+
onclick={open}
|
|
33
|
+
aria-label="{label ?? 'Color'} (current {value})"
|
|
34
|
+
style:--swatch-color={value}
|
|
35
|
+
><span class="sh3-swatch__dot"></span><span class="sh3-swatch__hex">{value}</span></button>
|
|
36
|
+
</label>
|
|
37
|
+
|
|
38
|
+
<style>
|
|
39
|
+
.sh3-swatch { display: inline-flex; flex-direction: column; gap: 4px; }
|
|
40
|
+
.sh3-swatch__label { font-size: 0.75rem; color: var(--shell-fg-muted); }
|
|
41
|
+
.sh3-swatch__btn {
|
|
42
|
+
display: inline-flex; align-items: center; gap: 8px;
|
|
43
|
+
height: var(--shell-field-height-md);
|
|
44
|
+
padding: 0 var(--shell-field-pad-x);
|
|
45
|
+
background: var(--shell-input-bg);
|
|
46
|
+
border: 1px solid var(--shell-border);
|
|
47
|
+
border-radius: var(--shell-widget-radius);
|
|
48
|
+
color: var(--shell-fg);
|
|
49
|
+
cursor: pointer;
|
|
50
|
+
font: inherit;
|
|
51
|
+
}
|
|
52
|
+
.sh3-swatch--sm .sh3-swatch__btn { height: var(--shell-field-height-sm); }
|
|
53
|
+
.sh3-swatch__btn:hover:not(:disabled) {
|
|
54
|
+
background: var(--shell-bg-elevated);
|
|
55
|
+
filter: none;
|
|
56
|
+
}
|
|
57
|
+
.sh3-swatch__btn:focus-visible { outline: none; box-shadow: var(--shell-focus-ring); }
|
|
58
|
+
.sh3-swatch__btn:disabled { opacity: 0.55; cursor: not-allowed; }
|
|
59
|
+
.sh3-swatch__dot {
|
|
60
|
+
width: 16px; height: 16px;
|
|
61
|
+
border: 1px solid var(--shell-border-strong);
|
|
62
|
+
border-radius: var(--shell-radius-sm);
|
|
63
|
+
background: var(--swatch-color);
|
|
64
|
+
}
|
|
65
|
+
.sh3-swatch__hex { font-family: var(--shell-font-mono); font-size: 0.75rem; }
|
|
66
|
+
</style>
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
type $$ComponentProps = {
|
|
2
|
+
value?: string;
|
|
3
|
+
label?: string;
|
|
4
|
+
disabled?: boolean;
|
|
5
|
+
size?: 'sm' | 'md';
|
|
6
|
+
};
|
|
7
|
+
declare const ColorSwatch: import("svelte").Component<$$ComponentProps, {}, "value">;
|
|
8
|
+
type ColorSwatch = ReturnType<typeof ColorSwatch>;
|
|
9
|
+
export default ColorSwatch;
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Snippet } from 'svelte';
|
|
3
|
+
|
|
4
|
+
let {
|
|
5
|
+
value = $bindable(''),
|
|
6
|
+
type = 'text',
|
|
7
|
+
label,
|
|
8
|
+
placeholder,
|
|
9
|
+
prefix,
|
|
10
|
+
suffix,
|
|
11
|
+
helper,
|
|
12
|
+
error,
|
|
13
|
+
disabled = false,
|
|
14
|
+
invalid = false,
|
|
15
|
+
size = 'md',
|
|
16
|
+
required = false,
|
|
17
|
+
autocomplete,
|
|
18
|
+
onchange,
|
|
19
|
+
}: {
|
|
20
|
+
value?: string;
|
|
21
|
+
type?: 'text' | 'email' | 'password' | 'search' | 'url' | 'tel';
|
|
22
|
+
label?: string;
|
|
23
|
+
placeholder?: string;
|
|
24
|
+
prefix?: Snippet;
|
|
25
|
+
suffix?: Snippet;
|
|
26
|
+
helper?: string;
|
|
27
|
+
error?: string;
|
|
28
|
+
disabled?: boolean;
|
|
29
|
+
invalid?: boolean;
|
|
30
|
+
size?: 'sm' | 'md';
|
|
31
|
+
required?: boolean;
|
|
32
|
+
autocomplete?: AutoFill;
|
|
33
|
+
onchange?: (next: string) => void;
|
|
34
|
+
} = $props();
|
|
35
|
+
|
|
36
|
+
const showError = $derived(invalid && !!error);
|
|
37
|
+
const helperText = $derived(showError ? error : helper);
|
|
38
|
+
</script>
|
|
39
|
+
|
|
40
|
+
<label class="sh3-field" class:sh3-field--invalid={invalid} class:sh3-field--sm={size === 'sm'}>
|
|
41
|
+
{#if label}<span class="sh3-field__label">{label}{#if required}<span aria-hidden="true"> *</span>{/if}</span>{/if}
|
|
42
|
+
<span class="sh3-field__row">
|
|
43
|
+
{#if prefix}<span class="sh3-field__affix">{@render prefix()}</span>{/if}
|
|
44
|
+
<input
|
|
45
|
+
class="sh3-field__input"
|
|
46
|
+
{type}
|
|
47
|
+
{placeholder}
|
|
48
|
+
{disabled}
|
|
49
|
+
{required}
|
|
50
|
+
{autocomplete}
|
|
51
|
+
aria-invalid={invalid || undefined}
|
|
52
|
+
bind:value
|
|
53
|
+
onblur={() => onchange?.(value)}
|
|
54
|
+
/>
|
|
55
|
+
{#if suffix}<span class="sh3-field__affix">{@render suffix()}</span>{/if}
|
|
56
|
+
</span>
|
|
57
|
+
{#if helperText}<span class="sh3-field__helper" class:sh3-field__helper--error={showError}>{helperText}</span>{/if}
|
|
58
|
+
</label>
|
|
59
|
+
|
|
60
|
+
<style>
|
|
61
|
+
.sh3-field {
|
|
62
|
+
display: inline-flex;
|
|
63
|
+
flex-direction: column;
|
|
64
|
+
gap: 4px;
|
|
65
|
+
font-family: var(--shell-font-ui);
|
|
66
|
+
font-size: 0.8125rem;
|
|
67
|
+
color: var(--shell-fg);
|
|
68
|
+
}
|
|
69
|
+
.sh3-field__label {
|
|
70
|
+
color: var(--shell-fg-muted);
|
|
71
|
+
font-size: 0.75rem;
|
|
72
|
+
}
|
|
73
|
+
.sh3-field__row {
|
|
74
|
+
display: inline-flex;
|
|
75
|
+
align-items: stretch;
|
|
76
|
+
background: var(--shell-input-bg);
|
|
77
|
+
border: 1px solid var(--shell-border);
|
|
78
|
+
border-radius: var(--shell-widget-radius);
|
|
79
|
+
height: var(--shell-field-height-md);
|
|
80
|
+
transition: border-color var(--shell-motion-fast) var(--shell-ease-standard),
|
|
81
|
+
box-shadow var(--shell-motion-fast) var(--shell-ease-standard);
|
|
82
|
+
}
|
|
83
|
+
.sh3-field--sm .sh3-field__row { height: var(--shell-field-height-sm); }
|
|
84
|
+
.sh3-field__row:focus-within {
|
|
85
|
+
border-color: var(--shell-input-border-focus);
|
|
86
|
+
box-shadow: var(--shell-focus-ring);
|
|
87
|
+
}
|
|
88
|
+
.sh3-field--invalid .sh3-field__row {
|
|
89
|
+
border-color: var(--shell-error);
|
|
90
|
+
}
|
|
91
|
+
.sh3-field--invalid .sh3-field__row:focus-within {
|
|
92
|
+
box-shadow: 0 0 0 2px color-mix(in srgb, var(--shell-error) 40%, transparent);
|
|
93
|
+
}
|
|
94
|
+
.sh3-field__input {
|
|
95
|
+
flex: 1 1 auto;
|
|
96
|
+
height: 100%;
|
|
97
|
+
padding: 0 var(--shell-field-pad-x);
|
|
98
|
+
background: transparent;
|
|
99
|
+
border: none;
|
|
100
|
+
color: inherit;
|
|
101
|
+
font: inherit;
|
|
102
|
+
outline: none;
|
|
103
|
+
}
|
|
104
|
+
/* The .sh3-field__row owns the focus ring; suppress base.css's
|
|
105
|
+
global input:focus-visible box-shadow so it doesn't double up. */
|
|
106
|
+
.sh3-field__input:focus,
|
|
107
|
+
.sh3-field__input:focus-visible {
|
|
108
|
+
outline: none;
|
|
109
|
+
box-shadow: none;
|
|
110
|
+
border: none;
|
|
111
|
+
}
|
|
112
|
+
.sh3-field__input:disabled { color: var(--shell-fg-muted); cursor: not-allowed; }
|
|
113
|
+
.sh3-field__affix {
|
|
114
|
+
display: inline-flex;
|
|
115
|
+
align-items: center;
|
|
116
|
+
padding: 0 var(--shell-field-pad-x);
|
|
117
|
+
color: var(--shell-fg-muted);
|
|
118
|
+
flex-shrink: 0;
|
|
119
|
+
}
|
|
120
|
+
.sh3-field__affix:first-child { padding-right: 0; }
|
|
121
|
+
.sh3-field__affix:last-child { padding-left: 0; }
|
|
122
|
+
.sh3-field__helper {
|
|
123
|
+
color: var(--shell-fg-muted);
|
|
124
|
+
font-size: 0.75rem;
|
|
125
|
+
}
|
|
126
|
+
.sh3-field__helper--error { color: var(--shell-error); }
|
|
127
|
+
</style>
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
type $$ComponentProps = {
|
|
3
|
+
value?: string;
|
|
4
|
+
type?: 'text' | 'email' | 'password' | 'search' | 'url' | 'tel';
|
|
5
|
+
label?: string;
|
|
6
|
+
placeholder?: string;
|
|
7
|
+
prefix?: Snippet;
|
|
8
|
+
suffix?: Snippet;
|
|
9
|
+
helper?: string;
|
|
10
|
+
error?: string;
|
|
11
|
+
disabled?: boolean;
|
|
12
|
+
invalid?: boolean;
|
|
13
|
+
size?: 'sm' | 'md';
|
|
14
|
+
required?: boolean;
|
|
15
|
+
autocomplete?: AutoFill;
|
|
16
|
+
onchange?: (next: string) => void;
|
|
17
|
+
};
|
|
18
|
+
declare const Field: import("svelte").Component<$$ComponentProps, {}, "value">;
|
|
19
|
+
type Field = ReturnType<typeof Field>;
|
|
20
|
+
export default Field;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export function extractValue(files, multiple) {
|
|
2
|
+
if (!files || files.length === 0)
|
|
3
|
+
return multiple ? [] : null;
|
|
4
|
+
if (multiple)
|
|
5
|
+
return Array.from(files);
|
|
6
|
+
return files[0];
|
|
7
|
+
}
|
|
8
|
+
export function displayName(value) {
|
|
9
|
+
if (value === null)
|
|
10
|
+
return '';
|
|
11
|
+
if (Array.isArray(value)) {
|
|
12
|
+
if (value.length === 0)
|
|
13
|
+
return '';
|
|
14
|
+
if (value.length === 1)
|
|
15
|
+
return value[0].name;
|
|
16
|
+
return `${value.length} files`;
|
|
17
|
+
}
|
|
18
|
+
return value.name;
|
|
19
|
+
}
|