sh3-core 0.11.2 → 0.11.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) hide show
  1. package/dist/BrandSlot.svelte +80 -0
  2. package/dist/BrandSlot.svelte.d.ts +3 -0
  3. package/dist/BrandSlot.test.d.ts +1 -0
  4. package/dist/BrandSlot.test.js +71 -0
  5. package/dist/Shell.svelte +8 -10
  6. package/dist/actions/ActionPanel.svelte +105 -0
  7. package/dist/actions/ActionPanel.svelte.d.ts +13 -0
  8. package/dist/actions/ActionPanel.test.d.ts +1 -0
  9. package/dist/actions/ActionPanel.test.js +80 -0
  10. package/dist/actions/ContextMenu.svelte +17 -85
  11. package/dist/actions/MenuBar.svelte +57 -0
  12. package/dist/actions/MenuBar.svelte.d.ts +3 -0
  13. package/dist/actions/MenuBar.test.d.ts +1 -0
  14. package/dist/actions/MenuBar.test.js +109 -0
  15. package/dist/actions/MenuButton.svelte +104 -0
  16. package/dist/actions/MenuButton.svelte.d.ts +9 -0
  17. package/dist/actions/MenuButton.test.d.ts +1 -0
  18. package/dist/actions/MenuButton.test.js +88 -0
  19. package/dist/actions/bindings.d.ts +10 -1
  20. package/dist/actions/bindings.js +16 -0
  21. package/dist/actions/bindings.test.js +23 -1
  22. package/dist/actions/contextMenuModel.js +5 -40
  23. package/dist/actions/defaultMenuContainers.d.ts +2 -0
  24. package/dist/actions/defaultMenuContainers.js +7 -0
  25. package/dist/actions/defaultMenuContainers.test.d.ts +1 -0
  26. package/dist/actions/defaultMenuContainers.test.js +23 -0
  27. package/dist/actions/dispatcher.svelte.js +1 -14
  28. package/dist/actions/listActive.d.ts +4 -0
  29. package/dist/actions/listActive.js +42 -0
  30. package/dist/actions/listActive.test.d.ts +1 -0
  31. package/dist/actions/listActive.test.js +86 -0
  32. package/dist/actions/menuBarModel.d.ts +28 -0
  33. package/dist/actions/menuBarModel.js +67 -0
  34. package/dist/actions/menuBarModel.test.d.ts +1 -0
  35. package/dist/actions/menuBarModel.test.js +84 -0
  36. package/dist/actions/paletteModel.js +10 -21
  37. package/dist/actions/paletteModel.test.js +16 -0
  38. package/dist/actions/scope-helpers.d.ts +11 -0
  39. package/dist/actions/scope-helpers.js +51 -0
  40. package/dist/actions/scope-helpers.test.d.ts +1 -0
  41. package/dist/actions/scope-helpers.test.js +62 -0
  42. package/dist/actions/shellActions.test.js +50 -0
  43. package/dist/actions/state.svelte.d.ts +12 -0
  44. package/dist/actions/state.svelte.js +36 -0
  45. package/dist/actions/state.test.js +26 -1
  46. package/dist/actions/types.d.ts +49 -0
  47. package/dist/api.d.ts +5 -0
  48. package/dist/api.js +6 -0
  49. package/dist/apps/lifecycle.js +8 -1
  50. package/dist/apps/lifecycle.test.js +211 -1
  51. package/dist/apps/registry.svelte.d.ts +17 -1
  52. package/dist/apps/registry.svelte.js +20 -1
  53. package/dist/apps/types.d.ts +28 -0
  54. package/dist/assets/favicon.png +0 -0
  55. package/dist/assets/favicon.svg +5 -0
  56. package/dist/color/api.d.ts +38 -0
  57. package/dist/color/api.js +10 -0
  58. package/dist/color/native-fallback.test.d.ts +1 -0
  59. package/dist/color/native-fallback.test.js +43 -0
  60. package/dist/color/primitive.d.ts +2 -0
  61. package/dist/color/primitive.js +40 -0
  62. package/dist/color/primitive.test.d.ts +1 -0
  63. package/dist/color/primitive.test.js +42 -0
  64. package/dist/color/shell-api.d.ts +2 -0
  65. package/dist/color/shell-api.js +11 -0
  66. package/dist/index.d.ts +0 -2
  67. package/dist/index.js +0 -2
  68. package/dist/layout/store.svelte.d.ts +27 -0
  69. package/dist/layout/store.svelte.js +63 -0
  70. package/dist/overlays/ConfirmDialog.svelte +138 -0
  71. package/dist/overlays/ConfirmDialog.svelte.d.ts +13 -0
  72. package/dist/overlays/ConfirmDialog.test.d.ts +1 -0
  73. package/dist/overlays/ConfirmDialog.test.js +123 -0
  74. package/dist/overlays/FloatFrame.svelte +2 -2
  75. package/dist/overlays/ToastItem.svelte +3 -3
  76. package/dist/primitives/base.css +5 -5
  77. package/dist/sh3core-shard/sh3coreShard.svelte.js +20 -0
  78. package/dist/shell-shard/shellShard.svelte.js +0 -4
  79. package/dist/shellRuntime.svelte.d.ts +20 -0
  80. package/dist/shellRuntime.svelte.js +16 -1
  81. package/dist/tokens.css +1 -1
  82. package/dist/version.d.ts +1 -1
  83. package/dist/version.js +1 -1
  84. package/package.json +1 -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, #1e1e1e));
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, #111);
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: #5cb176; }
70
- .toast-warn { border-left-color: #d6a84a; }
71
- .toast-error { border-left-color: #d06060; }
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); }
@@ -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, #6ea8fe);
19
- color: var(--shell-fg, #fff);
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: #fff;
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: #fff;
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: #fff;
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 });
@@ -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
@@ -6,7 +6,9 @@ import { type ToastManager } from './overlays/toast';
6
6
  import { type FloatManager } from './overlays/float';
7
7
  import { type PresetManager } from './overlays/presets';
8
8
  import type { ConflictsApi } from './conflicts/api';
9
+ import type { ColorApi } from './color/api';
9
10
  import { type OpenContextMenuOpts, type OpenPaletteOpts } from './actions/listeners';
11
+ import type { ActiveActionDescriptor } from './actions/types';
10
12
  /**
11
13
  * The process-wide shell singleton exposed to shards and the shell's own
12
14
  * internal code. Provides state zone creation and overlay managers.
@@ -32,6 +34,8 @@ export interface Shell {
32
34
  presets: PresetManager;
33
35
  /** Conflict manager view. Shell-owned modal for conflict arbitration. */
34
36
  conflicts: ConflictsApi;
37
+ /** Color picker. Falls back to native <input type="color"> when no picker shard is contributed. */
38
+ color: ColorApi;
35
39
  /** Actions facade — rebind keys, query bindings, open menus/palette. */
36
40
  actions: ShellActionsApi;
37
41
  }
@@ -50,6 +54,22 @@ export interface ShellActionsApi {
50
54
  openContextMenu(opts: OpenContextMenuOpts): void;
51
55
  /** Open the command palette, optionally pre-filled. */
52
56
  openPalette(opts?: OpenPaletteOpts): void;
57
+ /**
58
+ * Snapshot of all currently-active actions with resolved shortcuts
59
+ * and badges. One descriptor per action id, in tier-innermost-first
60
+ * order. See {@link ActiveActionDescriptor}.
61
+ *
62
+ * Non-reactive: returns a fresh array on each call. Subscribe via
63
+ * {@link ShellActionsApi.onActiveChange} to re-call when the active
64
+ * set or shortcuts change.
65
+ */
66
+ listActive(): ActiveActionDescriptor[];
67
+ /**
68
+ * Subscribe to active-set changes: action registration/unregistration,
69
+ * active app / view / focus transitions, selection changes, user
70
+ * binding edits. Returns an unsubscribe function.
71
+ */
72
+ onActiveChange(cb: () => void): () => void;
53
73
  }
54
74
  /** The process-wide shell instance. Framework-internal code uses this directly; shards receive a scoped view via `ShardContext`. */
55
75
  export declare const shell: Shell;
@@ -21,9 +21,12 @@ import { toastManager } from './overlays/toast';
21
21
  import { floatManager } from './overlays/float';
22
22
  import { presetManager } from './overlays/presets';
23
23
  import { conflictsApi } from './conflicts/shell-api';
24
+ import { colorApi } from './color/shell-api';
24
25
  import { loadUserBindings, saveUserBinding } from './actions/bindings-store';
25
26
  import { openContextMenu as listenersOpenContextMenu, openPalette as listenersOpenPalette, } from './actions/listeners';
26
- import { setUserBindings, getLiveDispatcherState } from './actions/state.svelte';
27
+ import { setUserBindings, getLiveDispatcherState, onActiveChange as onActiveChangeState, __notifyActiveChange, } from './actions/state.svelte';
28
+ import { listActions, onActionsChange } from './actions/registry';
29
+ import { listActiveFromEntries } from './actions/listActive';
27
30
  const shellActions = {
28
31
  async rebind(appId, actionId, shortcut) {
29
32
  await saveUserBinding(appId, actionId, shortcut);
@@ -49,6 +52,17 @@ const shellActions = {
49
52
  },
50
53
  openContextMenu: listenersOpenContextMenu,
51
54
  openPalette: listenersOpenPalette,
55
+ listActive() {
56
+ return listActiveFromEntries(listActions(), getLiveDispatcherState());
57
+ },
58
+ onActiveChange(cb) {
59
+ const offState = onActiveChangeState(cb);
60
+ const offRegistry = onActionsChange(() => { __notifyActiveChange(); });
61
+ return () => {
62
+ offState();
63
+ offRegistry();
64
+ };
65
+ },
52
66
  };
53
67
  /** The process-wide shell instance. Framework-internal code uses this directly; shards receive a scoped view via `ShardContext`. */
54
68
  export const shell = {
@@ -59,5 +73,6 @@ export const shell = {
59
73
  float: floatManager,
60
74
  presets: presetManager,
61
75
  conflicts: conflictsApi,
76
+ color: colorApi,
62
77
  actions: shellActions,
63
78
  };
package/dist/tokens.css CHANGED
@@ -33,7 +33,7 @@
33
33
  --shell-accent-muted: #3a5580;
34
34
 
35
35
  /* Inputs */
36
- --shell-input-bg: #2a2a2a;
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";
2
+ export declare const VERSION = "0.11.6";
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';
2
+ export const VERSION = '0.11.6';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sh3-core",
3
- "version": "0.11.2",
3
+ "version": "0.11.6",
4
4
  "type": "module",
5
5
  "files": [
6
6
  "dist"