sh3-core 0.11.4 → 0.11.7
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/BrandSlot.svelte +80 -0
- package/dist/BrandSlot.svelte.d.ts +3 -0
- package/dist/BrandSlot.test.d.ts +1 -0
- package/dist/BrandSlot.test.js +71 -0
- package/dist/Shell.svelte +8 -10
- package/dist/actions/ActionPanel.svelte +143 -0
- package/dist/actions/ActionPanel.svelte.d.ts +13 -0
- package/dist/actions/ActionPanel.test.d.ts +1 -0
- package/dist/actions/ActionPanel.test.js +168 -0
- package/dist/actions/ContextMenu.svelte +17 -85
- package/dist/actions/MenuBar.svelte +57 -0
- package/dist/actions/MenuBar.svelte.d.ts +3 -0
- package/dist/actions/MenuBar.test.d.ts +1 -0
- package/dist/actions/MenuBar.test.js +109 -0
- package/dist/actions/MenuButton.svelte +150 -0
- package/dist/actions/MenuButton.svelte.d.ts +10 -0
- package/dist/actions/MenuButton.test.d.ts +1 -0
- package/dist/actions/MenuButton.test.js +125 -0
- package/dist/actions/contextMenuModel.d.ts +10 -0
- package/dist/actions/contextMenuModel.js +44 -9
- package/dist/actions/contextMenuModel.test.js +28 -1
- package/dist/actions/defaultMenuContainers.d.ts +2 -0
- package/dist/actions/defaultMenuContainers.js +7 -0
- package/dist/actions/defaultMenuContainers.test.d.ts +1 -0
- package/dist/actions/defaultMenuContainers.test.js +23 -0
- package/dist/actions/listeners.d.ts +4 -0
- package/dist/actions/listeners.js +77 -17
- package/dist/actions/listeners.test.js +50 -0
- package/dist/actions/menuBarModel.d.ts +42 -0
- package/dist/actions/menuBarModel.js +110 -0
- package/dist/actions/menuBarModel.test.d.ts +1 -0
- package/dist/actions/menuBarModel.test.js +158 -0
- package/dist/actions/palette-scorer.d.ts +4 -0
- package/dist/actions/palette-scorer.js +5 -0
- package/dist/actions/palette-scorer.test.js +9 -1
- package/dist/actions/paletteModel.d.ts +7 -1
- package/dist/actions/paletteModel.js +26 -1
- package/dist/actions/paletteModel.test.js +43 -0
- package/dist/actions/registry.js +5 -0
- package/dist/actions/registry.test.js +12 -0
- package/dist/actions/types.d.ts +48 -1
- package/dist/actions/types.test.d.ts +1 -0
- package/dist/actions/types.test.js +31 -0
- package/dist/apps/lifecycle.js +8 -1
- package/dist/apps/lifecycle.test.js +211 -1
- package/dist/apps/registry.svelte.d.ts +17 -1
- package/dist/apps/registry.svelte.js +20 -1
- package/dist/apps/types.d.ts +28 -0
- package/dist/assets/icons.svg +5 -0
- package/dist/documents/backends.d.ts +2 -0
- package/dist/documents/backends.js +55 -0
- package/dist/documents/backends.test.d.ts +1 -1
- package/dist/documents/backends.test.js +69 -1
- package/dist/documents/browse.d.ts +18 -0
- package/dist/documents/browse.js +13 -0
- package/dist/documents/browse.test.js +47 -0
- package/dist/documents/handle.js +23 -0
- package/dist/documents/handle.test.js +51 -0
- package/dist/documents/http-backend.d.ts +1 -0
- package/dist/documents/http-backend.js +19 -0
- package/dist/documents/http-backend.test.js +42 -0
- package/dist/documents/types.d.ts +29 -1
- package/dist/documents/types.js +4 -0
- package/dist/documents/types.test.d.ts +1 -0
- package/dist/documents/types.test.js +20 -0
- package/dist/layout/LayoutRenderer.browser.test.js +196 -0
- package/dist/layout/SlotContainer.svelte +13 -8
- package/dist/layout/SlotDropZone.svelte +44 -9
- package/dist/layout/__screenshots__/LayoutRenderer.browser.test.ts/LayoutRenderer-browser---E-7-fixed-slot-drop-protection-still-accepts-a-strip-drop-into-a-fixed-tabs-node-1.png +0 -0
- package/dist/layout/__screenshots__/LayoutRenderer.browser.test.ts/LayoutRenderer-browser---E-8-same-strip-reorder-keeps-the-active-pane-populated-after-moving-the-second-tab-to-first-1.png +0 -0
- package/dist/layout/ops.d.ts +10 -0
- package/dist/layout/ops.js +30 -2
- package/dist/layout/ops.test.js +111 -1
- package/dist/layout/slotHostPool.svelte.d.ts +7 -1
- package/dist/layout/slotHostPool.svelte.js +27 -8
- package/dist/layout/store.svelte.d.ts +27 -0
- package/dist/layout/store.svelte.js +63 -0
- package/dist/overlays/ConfirmDialog.svelte +138 -0
- package/dist/overlays/ConfirmDialog.svelte.d.ts +13 -0
- package/dist/overlays/ConfirmDialog.test.d.ts +1 -0
- package/dist/overlays/ConfirmDialog.test.js +123 -0
- package/dist/overlays/FloatFrame.svelte +2 -2
- package/dist/overlays/ToastItem.svelte +3 -3
- package/dist/primitives/base.css +5 -5
- package/dist/sh3core-shard/sh3coreShard.svelte.js +38 -4
- package/dist/shell-shard/shellShard.svelte.js +0 -4
- package/dist/tokens.css +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +2 -1
|
@@ -34,6 +34,7 @@ import { acquireSlotHost, releaseSlotHost } from './slotHostPool.svelte';
|
|
|
34
34
|
import { normalizeInitialLayout } from './presets';
|
|
35
35
|
import { collectTreeSlotRefs } from './tree-walk';
|
|
36
36
|
import { bindPresetBlob, unbindPresetBlob } from '../overlays/presets';
|
|
37
|
+
import { getRegisteredApp } from '../apps/registry.svelte';
|
|
37
38
|
// ---------- orphan cleanup of pre-phase-8 shell layout key ----------------
|
|
38
39
|
// Legacy pre-phase-8 orphan cleanup. The literal '__shell__' here is
|
|
39
40
|
// intentional — it clears data written under the old reserved id before
|
|
@@ -168,6 +169,59 @@ export function acquireAppSlotHolds() {
|
|
|
168
169
|
appEntry.heldSlotIds.push(slotId);
|
|
169
170
|
}
|
|
170
171
|
}
|
|
172
|
+
/**
|
|
173
|
+
* Rebuild the currently-attached app's active preset from a fresh copy
|
|
174
|
+
* of `app.initialLayout`. Discards in-place customizations (split sizes,
|
|
175
|
+
* tab order, drops, floats) and re-scopes slot-host refcount holds so
|
|
176
|
+
* the pool tears down hosts the new tree no longer references.
|
|
177
|
+
*
|
|
178
|
+
* The active preset name is preserved unless the app's initialLayout no
|
|
179
|
+
* longer declares it (the app shipped a new version that dropped the
|
|
180
|
+
* preset name); in that case the active preset is updated to the first
|
|
181
|
+
* canonical preset.
|
|
182
|
+
*
|
|
183
|
+
* Used by the `sh3.app.reset-layout` action — a recovery affordance
|
|
184
|
+
* available from the command palette only. Other presets are left alone;
|
|
185
|
+
* the user's customizations on them survive.
|
|
186
|
+
*
|
|
187
|
+
* Note: this re-scopes holds for the reset path only. The
|
|
188
|
+
* `presetManager.switch` slot-hold leak (TODO above on
|
|
189
|
+
* `acquireAppSlotHolds`) is a separate concern.
|
|
190
|
+
*/
|
|
191
|
+
export function resetActivePresetToDefault() {
|
|
192
|
+
if (!appEntry) {
|
|
193
|
+
throw new Error('resetActivePresetToDefault: no app attached');
|
|
194
|
+
}
|
|
195
|
+
const app = getRegisteredApp(appEntry.appId);
|
|
196
|
+
if (!app) {
|
|
197
|
+
throw new Error(`resetActivePresetToDefault: attached app "${appEntry.appId}" not in registry`);
|
|
198
|
+
}
|
|
199
|
+
const canonical = normalizeInitialLayout(app.initialLayout);
|
|
200
|
+
const blob = appEntry.proxy;
|
|
201
|
+
const targetName = blob.activePreset;
|
|
202
|
+
let target = canonical.find((p) => p.name === targetName);
|
|
203
|
+
if (!target) {
|
|
204
|
+
target = canonical[0];
|
|
205
|
+
blob.activePreset = target.name;
|
|
206
|
+
}
|
|
207
|
+
// Release old slot holds before swapping the tree so the pool's
|
|
208
|
+
// microtask sees no live refs and tears down hosts that the new
|
|
209
|
+
// tree doesn't re-acquire.
|
|
210
|
+
for (const slotId of appEntry.heldSlotIds) {
|
|
211
|
+
releaseSlotHost(slotId);
|
|
212
|
+
}
|
|
213
|
+
appEntry.heldSlotIds = [];
|
|
214
|
+
// Deep-clone so the canonical object isn't aliased with future
|
|
215
|
+
// attaches or with the app's source `LayoutPreset` objects.
|
|
216
|
+
const freshTree = structuredClone(target.variants.default);
|
|
217
|
+
blob.presets[blob.activePreset].default = freshTree;
|
|
218
|
+
// Re-acquire holds against the new tree (mirrors acquireAppSlotHolds).
|
|
219
|
+
const refs = collectTreeSlotRefs(freshTree);
|
|
220
|
+
for (const { slotId, viewId, label, meta } of refs) {
|
|
221
|
+
acquireSlotHost(slotId, viewId, label, meta);
|
|
222
|
+
appEntry.heldSlotIds.push(slotId);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
171
225
|
/**
|
|
172
226
|
* Detach the currently-attached app. Releases its refcount holds; the
|
|
173
227
|
* pool's microtask cleanup drops the pooled hosts if they also have no
|
|
@@ -275,3 +329,12 @@ export function __resetLayoutStoreForTest() {
|
|
|
275
329
|
HOME_TREE.floats.length = 0;
|
|
276
330
|
HOME_TREE.docked = HOME_LAYOUT;
|
|
277
331
|
}
|
|
332
|
+
/**
|
|
333
|
+
* Test-only inspection: returns a shallow copy of the currently-attached
|
|
334
|
+
* app's held slot ids, in acquisition order. Returns `null` when no app
|
|
335
|
+
* is attached. Not exported from `src/index.ts` — tests import this
|
|
336
|
+
* submodule path directly.
|
|
337
|
+
*/
|
|
338
|
+
export function __inspectAppEntryHeldSlotIdsForTest() {
|
|
339
|
+
return appEntry ? [...appEntry.heldSlotIds] : null;
|
|
340
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
/*
|
|
3
|
+
* ConfirmDialog — small reusable confirmation primitive for destructive
|
|
4
|
+
* or otherwise non-trivial actions. Mounted via modalManager.open():
|
|
5
|
+
*
|
|
6
|
+
* modalManager.open(ConfirmDialog, {
|
|
7
|
+
* title: 'Reset layout?',
|
|
8
|
+
* body: 'This discards your customizations.',
|
|
9
|
+
* confirmLabel: 'Reset',
|
|
10
|
+
* confirmTone: 'danger',
|
|
11
|
+
* onConfirm: () => doReset(),
|
|
12
|
+
* });
|
|
13
|
+
*
|
|
14
|
+
* Backdrop click does NOT dismiss (no dismissOnBackdrop on the modal).
|
|
15
|
+
* Escape dismisses via the modal manager's shared listener.
|
|
16
|
+
* Default focus is the Cancel button so destructive actions don't fire on
|
|
17
|
+
* stray Enter presses.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
let {
|
|
21
|
+
title,
|
|
22
|
+
body,
|
|
23
|
+
confirmLabel = 'Confirm',
|
|
24
|
+
cancelLabel = 'Cancel',
|
|
25
|
+
confirmTone = 'default',
|
|
26
|
+
onConfirm,
|
|
27
|
+
onCancel,
|
|
28
|
+
close,
|
|
29
|
+
}: {
|
|
30
|
+
title: string;
|
|
31
|
+
body: string;
|
|
32
|
+
confirmLabel?: string;
|
|
33
|
+
cancelLabel?: string;
|
|
34
|
+
confirmTone?: 'default' | 'danger';
|
|
35
|
+
onConfirm: () => void | Promise<void>;
|
|
36
|
+
onCancel?: () => void;
|
|
37
|
+
close: () => void;
|
|
38
|
+
} = $props();
|
|
39
|
+
|
|
40
|
+
let cancelBtn: HTMLButtonElement | undefined = $state();
|
|
41
|
+
let busy = $state(false);
|
|
42
|
+
|
|
43
|
+
$effect(() => {
|
|
44
|
+
cancelBtn?.focus();
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
async function handleConfirm(): Promise<void> {
|
|
48
|
+
if (busy) return;
|
|
49
|
+
busy = true;
|
|
50
|
+
try {
|
|
51
|
+
await onConfirm();
|
|
52
|
+
} finally {
|
|
53
|
+
close();
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function handleCancel(): void {
|
|
58
|
+
if (busy) return;
|
|
59
|
+
onCancel?.();
|
|
60
|
+
close();
|
|
61
|
+
}
|
|
62
|
+
</script>
|
|
63
|
+
|
|
64
|
+
<div class="confirm-dialog">
|
|
65
|
+
<div class="confirm-dialog-title">{title}</div>
|
|
66
|
+
<div class="confirm-dialog-body">{body}</div>
|
|
67
|
+
<div class="confirm-dialog-actions">
|
|
68
|
+
<button
|
|
69
|
+
type="button"
|
|
70
|
+
class="confirm-dialog-btn confirm-dialog-btn-cancel"
|
|
71
|
+
data-confirm-dialog-cancel
|
|
72
|
+
bind:this={cancelBtn}
|
|
73
|
+
onclick={handleCancel}
|
|
74
|
+
disabled={busy}
|
|
75
|
+
>
|
|
76
|
+
{cancelLabel}
|
|
77
|
+
</button>
|
|
78
|
+
<button
|
|
79
|
+
type="button"
|
|
80
|
+
class="confirm-dialog-btn"
|
|
81
|
+
class:confirm-dialog-btn-danger={confirmTone === 'danger'}
|
|
82
|
+
class:confirm-dialog-btn-default={confirmTone === 'default'}
|
|
83
|
+
data-confirm-dialog-confirm
|
|
84
|
+
onclick={handleConfirm}
|
|
85
|
+
disabled={busy}
|
|
86
|
+
>
|
|
87
|
+
{confirmLabel}
|
|
88
|
+
</button>
|
|
89
|
+
</div>
|
|
90
|
+
</div>
|
|
91
|
+
|
|
92
|
+
<style>
|
|
93
|
+
.confirm-dialog {
|
|
94
|
+
display: flex;
|
|
95
|
+
flex-direction: column;
|
|
96
|
+
gap: 16px;
|
|
97
|
+
padding: 20px 24px;
|
|
98
|
+
min-width: 360px;
|
|
99
|
+
max-width: 480px;
|
|
100
|
+
}
|
|
101
|
+
.confirm-dialog-title {
|
|
102
|
+
font-size: 16px;
|
|
103
|
+
font-weight: 600;
|
|
104
|
+
color: var(--shell-fg);
|
|
105
|
+
}
|
|
106
|
+
.confirm-dialog-body {
|
|
107
|
+
font-size: 13px;
|
|
108
|
+
color: var(--shell-fg-muted, var(--shell-fg));
|
|
109
|
+
line-height: 1.5;
|
|
110
|
+
}
|
|
111
|
+
.confirm-dialog-actions {
|
|
112
|
+
display: flex;
|
|
113
|
+
justify-content: flex-end;
|
|
114
|
+
gap: 8px;
|
|
115
|
+
margin-top: 4px;
|
|
116
|
+
}
|
|
117
|
+
.confirm-dialog-btn {
|
|
118
|
+
font-size: 13px;
|
|
119
|
+
padding: 6px 14px;
|
|
120
|
+
border-radius: var(--shell-radius-sm, 4px);
|
|
121
|
+
border: 1px solid var(--shell-border-strong);
|
|
122
|
+
background: transparent;
|
|
123
|
+
color: var(--shell-fg);
|
|
124
|
+
cursor: pointer;
|
|
125
|
+
}
|
|
126
|
+
.confirm-dialog-btn:disabled {
|
|
127
|
+
opacity: 0.6;
|
|
128
|
+
cursor: not-allowed;
|
|
129
|
+
}
|
|
130
|
+
.confirm-dialog-btn-default {
|
|
131
|
+
background: var(--shell-bg-elevated);
|
|
132
|
+
}
|
|
133
|
+
.confirm-dialog-btn-danger {
|
|
134
|
+
background: transparent;
|
|
135
|
+
color: var(--shell-error, #d32f2f);
|
|
136
|
+
border-color: var(--shell-error, #d32f2f);
|
|
137
|
+
}
|
|
138
|
+
</style>
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
type $$ComponentProps = {
|
|
2
|
+
title: string;
|
|
3
|
+
body: string;
|
|
4
|
+
confirmLabel?: string;
|
|
5
|
+
cancelLabel?: string;
|
|
6
|
+
confirmTone?: 'default' | 'danger';
|
|
7
|
+
onConfirm: () => void | Promise<void>;
|
|
8
|
+
onCancel?: () => void;
|
|
9
|
+
close: () => void;
|
|
10
|
+
};
|
|
11
|
+
declare const ConfirmDialog: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
12
|
+
type ConfirmDialog = ReturnType<typeof ConfirmDialog>;
|
|
13
|
+
export default ConfirmDialog;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
2
|
+
import { tick } from 'svelte';
|
|
3
|
+
import { modalManager } from './modal';
|
|
4
|
+
import { registerLayerRoot, unregisterLayerRoot } from './roots';
|
|
5
|
+
import ConfirmDialog from './ConfirmDialog.svelte';
|
|
6
|
+
function makeLayerRoot() {
|
|
7
|
+
const el = document.createElement('div');
|
|
8
|
+
el.style.position = 'relative';
|
|
9
|
+
document.body.appendChild(el);
|
|
10
|
+
registerLayerRoot('modal', el);
|
|
11
|
+
return el;
|
|
12
|
+
}
|
|
13
|
+
function teardownLayerRoot(el) {
|
|
14
|
+
unregisterLayerRoot('modal');
|
|
15
|
+
el.remove();
|
|
16
|
+
}
|
|
17
|
+
describe('ConfirmDialog', () => {
|
|
18
|
+
let layerRoot;
|
|
19
|
+
beforeEach(() => {
|
|
20
|
+
layerRoot = makeLayerRoot();
|
|
21
|
+
});
|
|
22
|
+
afterEach(() => {
|
|
23
|
+
modalManager.closeAll();
|
|
24
|
+
teardownLayerRoot(layerRoot);
|
|
25
|
+
});
|
|
26
|
+
it('renders title and body', async () => {
|
|
27
|
+
modalManager.open(ConfirmDialog, {
|
|
28
|
+
title: 'Reset layout?',
|
|
29
|
+
body: 'This discards your customizations.',
|
|
30
|
+
onConfirm: () => { },
|
|
31
|
+
});
|
|
32
|
+
await tick();
|
|
33
|
+
const box = layerRoot.querySelector('.modal-box');
|
|
34
|
+
expect(box.textContent).toContain('Reset layout?');
|
|
35
|
+
expect(box.textContent).toContain('This discards your customizations.');
|
|
36
|
+
});
|
|
37
|
+
it('Cancel button calls onCancel and closes', async () => {
|
|
38
|
+
const onCancel = vi.fn();
|
|
39
|
+
const onConfirm = vi.fn();
|
|
40
|
+
modalManager.open(ConfirmDialog, {
|
|
41
|
+
title: 't',
|
|
42
|
+
body: 'b',
|
|
43
|
+
onCancel,
|
|
44
|
+
onConfirm,
|
|
45
|
+
});
|
|
46
|
+
await tick();
|
|
47
|
+
const cancelBtn = layerRoot.querySelector('[data-confirm-dialog-cancel]');
|
|
48
|
+
cancelBtn.click();
|
|
49
|
+
await tick();
|
|
50
|
+
expect(onCancel).toHaveBeenCalledOnce();
|
|
51
|
+
expect(onConfirm).not.toHaveBeenCalled();
|
|
52
|
+
expect(layerRoot.querySelector('.sh3-modal-host')).toBeNull();
|
|
53
|
+
});
|
|
54
|
+
it('Confirm button calls onConfirm and closes', async () => {
|
|
55
|
+
const onConfirm = vi.fn();
|
|
56
|
+
modalManager.open(ConfirmDialog, {
|
|
57
|
+
title: 't',
|
|
58
|
+
body: 'b',
|
|
59
|
+
onConfirm,
|
|
60
|
+
});
|
|
61
|
+
await tick();
|
|
62
|
+
const confirmBtn = layerRoot.querySelector('[data-confirm-dialog-confirm]');
|
|
63
|
+
confirmBtn.click();
|
|
64
|
+
await tick();
|
|
65
|
+
expect(onConfirm).toHaveBeenCalledOnce();
|
|
66
|
+
expect(layerRoot.querySelector('.sh3-modal-host')).toBeNull();
|
|
67
|
+
});
|
|
68
|
+
it('awaits async onConfirm before closing', async () => {
|
|
69
|
+
let resolveFn = () => { };
|
|
70
|
+
const onConfirm = vi.fn(() => new Promise((resolve) => {
|
|
71
|
+
resolveFn = resolve;
|
|
72
|
+
}));
|
|
73
|
+
modalManager.open(ConfirmDialog, {
|
|
74
|
+
title: 't',
|
|
75
|
+
body: 'b',
|
|
76
|
+
onConfirm,
|
|
77
|
+
});
|
|
78
|
+
await tick();
|
|
79
|
+
const confirmBtn = layerRoot.querySelector('[data-confirm-dialog-confirm]');
|
|
80
|
+
confirmBtn.click();
|
|
81
|
+
await tick();
|
|
82
|
+
// Modal still open — onConfirm hasn't resolved yet.
|
|
83
|
+
expect(layerRoot.querySelector('.sh3-modal-host')).not.toBeNull();
|
|
84
|
+
resolveFn();
|
|
85
|
+
await tick();
|
|
86
|
+
await tick();
|
|
87
|
+
expect(layerRoot.querySelector('.sh3-modal-host')).toBeNull();
|
|
88
|
+
});
|
|
89
|
+
it('confirmTone: "danger" applies the danger class to the confirm button', async () => {
|
|
90
|
+
modalManager.open(ConfirmDialog, {
|
|
91
|
+
title: 't',
|
|
92
|
+
body: 'b',
|
|
93
|
+
confirmTone: 'danger',
|
|
94
|
+
onConfirm: () => { },
|
|
95
|
+
});
|
|
96
|
+
await tick();
|
|
97
|
+
const confirmBtn = layerRoot.querySelector('[data-confirm-dialog-confirm]');
|
|
98
|
+
expect(confirmBtn.classList.contains('confirm-dialog-btn-danger')).toBe(true);
|
|
99
|
+
});
|
|
100
|
+
it('uses provided confirmLabel and cancelLabel', async () => {
|
|
101
|
+
modalManager.open(ConfirmDialog, {
|
|
102
|
+
title: 't',
|
|
103
|
+
body: 'b',
|
|
104
|
+
confirmLabel: 'Wipe',
|
|
105
|
+
cancelLabel: 'Keep',
|
|
106
|
+
onConfirm: () => { },
|
|
107
|
+
});
|
|
108
|
+
await tick();
|
|
109
|
+
expect(layerRoot.querySelector('[data-confirm-dialog-confirm]').textContent).toContain('Wipe');
|
|
110
|
+
expect(layerRoot.querySelector('[data-confirm-dialog-cancel]').textContent).toContain('Keep');
|
|
111
|
+
});
|
|
112
|
+
it('default focus is the Cancel button', async () => {
|
|
113
|
+
modalManager.open(ConfirmDialog, {
|
|
114
|
+
title: 't',
|
|
115
|
+
body: 'b',
|
|
116
|
+
onConfirm: () => { },
|
|
117
|
+
});
|
|
118
|
+
await tick();
|
|
119
|
+
await tick(); // focus is set in $effect after mount
|
|
120
|
+
const cancelBtn = layerRoot.querySelector('[data-confirm-dialog-cancel]');
|
|
121
|
+
expect(document.activeElement).toBe(cancelBtn);
|
|
122
|
+
});
|
|
123
|
+
});
|
|
@@ -109,7 +109,7 @@
|
|
|
109
109
|
position: absolute;
|
|
110
110
|
display: flex;
|
|
111
111
|
flex-direction: column;
|
|
112
|
-
background: var(--shell-grad-bg-elevated, var(--shell-bg-elevated
|
|
112
|
+
background: var(--shell-grad-bg-elevated, var(--shell-bg-elevated));
|
|
113
113
|
color: var(--shell-fg);
|
|
114
114
|
border: 1px solid var(--shell-border-strong);
|
|
115
115
|
border-radius: var(--shell-radius);
|
|
@@ -121,7 +121,7 @@
|
|
|
121
121
|
align-items: center;
|
|
122
122
|
justify-content: space-between;
|
|
123
123
|
padding: 4px 8px;
|
|
124
|
-
background: var(--shell-bg,
|
|
124
|
+
background: var(--shell-grad-bg-sunken, var(--shell-bg-sunken));
|
|
125
125
|
cursor: move;
|
|
126
126
|
user-select: none;
|
|
127
127
|
border-bottom: 1px solid var(--shell-border-strong);
|
|
@@ -66,9 +66,9 @@
|
|
|
66
66
|
.toast-message { flex: 1; }
|
|
67
67
|
|
|
68
68
|
.toast-info { border-left-color: var(--shell-accent); }
|
|
69
|
-
.toast-success { border-left-color:
|
|
70
|
-
.toast-warn { border-left-color:
|
|
71
|
-
.toast-error { border-left-color:
|
|
69
|
+
.toast-success { border-left-color: var(--shell-success); }
|
|
70
|
+
.toast-warn { border-left-color: var(--shell-warning); }
|
|
71
|
+
.toast-error { border-left-color: var(--shell-error); }
|
|
72
72
|
|
|
73
73
|
@keyframes toast-in {
|
|
74
74
|
from { opacity: 0; transform: translateY(8px); }
|
package/dist/primitives/base.css
CHANGED
|
@@ -15,8 +15,8 @@ input[type="submit"],
|
|
|
15
15
|
input[type="reset"],
|
|
16
16
|
.shell-base-button {
|
|
17
17
|
padding: 6px 14px;
|
|
18
|
-
background: var(--shell-accent
|
|
19
|
-
color: var(--shell-fg
|
|
18
|
+
background: var(--shell-accent);
|
|
19
|
+
color: var(--shell-fg-on-accent);
|
|
20
20
|
border: none;
|
|
21
21
|
border-radius: var(--shell-radius);
|
|
22
22
|
cursor: pointer;
|
|
@@ -113,7 +113,7 @@ input[type="radio"].shell-base-radio {
|
|
|
113
113
|
content: "";
|
|
114
114
|
width: 8px;
|
|
115
115
|
height: 8px;
|
|
116
|
-
background:
|
|
116
|
+
background: var(--shell-fg-on-accent);
|
|
117
117
|
clip-path: polygon(14% 44%, 0 60%, 40% 100%, 100% 20%, 85% 8%, 38% 70%);
|
|
118
118
|
}
|
|
119
119
|
|
|
@@ -122,7 +122,7 @@ input[type="radio"].shell-base-radio {
|
|
|
122
122
|
width: 6px;
|
|
123
123
|
height: 6px;
|
|
124
124
|
border-radius: 50%;
|
|
125
|
-
background:
|
|
125
|
+
background: var(--shell-fg-on-accent);
|
|
126
126
|
}
|
|
127
127
|
|
|
128
128
|
.shell-base-check:focus-visible,
|
|
@@ -167,7 +167,7 @@ input[type="checkbox"].shell-base-switch {
|
|
|
167
167
|
.shell-base-switch:checked { background: var(--shell-accent); }
|
|
168
168
|
.shell-base-switch:checked::before {
|
|
169
169
|
transform: translateX(12px);
|
|
170
|
-
background:
|
|
170
|
+
background: var(--shell-fg-on-accent);
|
|
171
171
|
}
|
|
172
172
|
|
|
173
173
|
.shell-base-switch:focus-visible {
|
|
@@ -24,10 +24,13 @@
|
|
|
24
24
|
import { mount, unmount } from 'svelte';
|
|
25
25
|
import ShellHome from './ShellHome.svelte';
|
|
26
26
|
import KeysAndPeers from '../shell/views/KeysAndPeers.svelte';
|
|
27
|
+
import ConfirmDialog from '../overlays/ConfirmDialog.svelte';
|
|
27
28
|
import { VERSION } from '../version';
|
|
28
29
|
import { __setBindingsZone } from '../actions/bindings-store';
|
|
29
30
|
import { registeredApps } from '../apps/registry.svelte';
|
|
30
31
|
import { launchApp } from '../apps/lifecycle';
|
|
32
|
+
import { resetActivePresetToDefault } from '../layout/store.svelte';
|
|
33
|
+
import { modalManager } from '../overlays/modal';
|
|
31
34
|
export const sh3coreShard = {
|
|
32
35
|
manifest: {
|
|
33
36
|
id: '__sh3core__',
|
|
@@ -57,6 +60,23 @@ export const sh3coreShard = {
|
|
|
57
60
|
import('../actions/listeners').then(({ openPalette }) => openPalette());
|
|
58
61
|
},
|
|
59
62
|
});
|
|
63
|
+
ctx.actions.register({
|
|
64
|
+
id: 'sh3.app.reset-layout',
|
|
65
|
+
label: 'Reset Current Layout',
|
|
66
|
+
scope: ['app'],
|
|
67
|
+
paletteItem: true,
|
|
68
|
+
contextItem: false,
|
|
69
|
+
run() {
|
|
70
|
+
modalManager.open(ConfirmDialog, {
|
|
71
|
+
title: 'Reset layout?',
|
|
72
|
+
body: 'This discards the current arrangement of the active preset and ' +
|
|
73
|
+
'rebuilds it from the app default. Floats will be removed.',
|
|
74
|
+
confirmLabel: 'Reset',
|
|
75
|
+
confirmTone: 'danger',
|
|
76
|
+
onConfirm: () => resetActivePresetToDefault(),
|
|
77
|
+
});
|
|
78
|
+
},
|
|
79
|
+
});
|
|
60
80
|
const factory = {
|
|
61
81
|
mount(container, _context) {
|
|
62
82
|
const instance = mount(ShellHome, { target: container });
|
|
@@ -76,9 +96,22 @@ export const sh3coreShard = {
|
|
|
76
96
|
};
|
|
77
97
|
ctx.registerView('sh3core:home', factory);
|
|
78
98
|
ctx.registerView('shell:keys-and-peers', keysFactory);
|
|
79
|
-
//
|
|
80
|
-
//
|
|
81
|
-
//
|
|
99
|
+
// Launcher parent — submenu drill host. No `run` needed: the
|
|
100
|
+
// dispatcher's default behavior opens a sub-palette filtered to
|
|
101
|
+
// `submenuOf === 'sh3.app.launch'`. The single parent replaces the
|
|
102
|
+
// per-app palette entries that used to flood the idle palette.
|
|
103
|
+
ctx.actions.register({
|
|
104
|
+
id: 'sh3.app.launch',
|
|
105
|
+
label: 'Launch app',
|
|
106
|
+
scope: ['home', 'app'],
|
|
107
|
+
submenu: true,
|
|
108
|
+
paletteItem: true,
|
|
109
|
+
});
|
|
110
|
+
// Dynamic launcher children: one per registered app, kept in sync
|
|
111
|
+
// as packages install/uninstall via the registry. Children inherit
|
|
112
|
+
// the parent's surface placement, so they don't set paletteItem
|
|
113
|
+
// themselves. Direct text match (e.g. typing the app name) still
|
|
114
|
+
// surfaces them at the root palette via the scorer.
|
|
82
115
|
const launcherUnregisters = new Map();
|
|
83
116
|
$effect.root(() => {
|
|
84
117
|
$effect(() => {
|
|
@@ -89,8 +122,9 @@ export const sh3coreShard = {
|
|
|
89
122
|
continue;
|
|
90
123
|
const off = ctx.actions.register({
|
|
91
124
|
id: `sh3.app.launch:${id}`,
|
|
92
|
-
label:
|
|
125
|
+
label: app.manifest.label,
|
|
93
126
|
scope: ['home', 'app'],
|
|
127
|
+
submenuOf: 'sh3.app.launch',
|
|
94
128
|
run() {
|
|
95
129
|
void launchApp(id);
|
|
96
130
|
},
|
|
@@ -176,10 +176,6 @@ export function makeShellApiForTest() {
|
|
|
176
176
|
export const shellShard = {
|
|
177
177
|
manifest,
|
|
178
178
|
activate(ctx) {
|
|
179
|
-
if (!ctx.isAdmin) {
|
|
180
|
-
// Non-admin: don't expose the view. Nothing to register.
|
|
181
|
-
return;
|
|
182
|
-
}
|
|
183
179
|
registerV1Verbs(ctx);
|
|
184
180
|
const shell = makeShellApi(ctx);
|
|
185
181
|
// The AZERTY `²` key (top-left on FR keyboards, below Escape) opens the
|
package/dist/tokens.css
CHANGED
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
--shell-accent-muted: #3a5580;
|
|
34
34
|
|
|
35
35
|
/* Inputs */
|
|
36
|
-
--shell-input-bg:
|
|
36
|
+
--shell-input-bg: var(--shell-bg-sunken);
|
|
37
37
|
--shell-input-border-focus: var(--shell-accent);
|
|
38
38
|
--shell-focus-ring: 0 0 0 2px color-mix(in srgb, var(--shell-accent) 40%, transparent);
|
|
39
39
|
|
package/dist/version.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
/** Auto-generated from package.json — do not edit manually. */
|
|
2
|
-
export declare const VERSION = "0.11.
|
|
2
|
+
export declare const VERSION = "0.11.7";
|
package/dist/version.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
/** Auto-generated from package.json — do not edit manually. */
|
|
2
|
-
export const VERSION = '0.11.
|
|
2
|
+
export const VERSION = '0.11.7';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sh3-core",
|
|
3
|
-
"version": "0.11.
|
|
3
|
+
"version": "0.11.7",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"files": [
|
|
6
6
|
"dist"
|
|
@@ -57,6 +57,7 @@
|
|
|
57
57
|
"@testing-library/svelte": "^5.3.1",
|
|
58
58
|
"@tsconfig/svelte": "^5.0.4",
|
|
59
59
|
"@vitest/browser": "^2.1.9",
|
|
60
|
+
"fake-indexeddb": "^6.2.5",
|
|
60
61
|
"happy-dom": "^15.11.7",
|
|
61
62
|
"playwright": "^1.59.1",
|
|
62
63
|
"svelte": "^5.0.0",
|