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.
- 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 +105 -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 +80 -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 +104 -0
- package/dist/actions/MenuButton.svelte.d.ts +9 -0
- package/dist/actions/MenuButton.test.d.ts +1 -0
- package/dist/actions/MenuButton.test.js +88 -0
- package/dist/actions/bindings.d.ts +10 -1
- package/dist/actions/bindings.js +16 -0
- package/dist/actions/bindings.test.js +23 -1
- package/dist/actions/contextMenuModel.js +5 -40
- 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/dispatcher.svelte.js +1 -14
- package/dist/actions/listActive.d.ts +4 -0
- package/dist/actions/listActive.js +42 -0
- package/dist/actions/listActive.test.d.ts +1 -0
- package/dist/actions/listActive.test.js +86 -0
- package/dist/actions/menuBarModel.d.ts +28 -0
- package/dist/actions/menuBarModel.js +67 -0
- package/dist/actions/menuBarModel.test.d.ts +1 -0
- package/dist/actions/menuBarModel.test.js +84 -0
- package/dist/actions/paletteModel.js +10 -21
- package/dist/actions/paletteModel.test.js +16 -0
- package/dist/actions/scope-helpers.d.ts +11 -0
- package/dist/actions/scope-helpers.js +51 -0
- package/dist/actions/scope-helpers.test.d.ts +1 -0
- package/dist/actions/scope-helpers.test.js +62 -0
- package/dist/actions/shellActions.test.js +50 -0
- package/dist/actions/state.svelte.d.ts +12 -0
- package/dist/actions/state.svelte.js +36 -0
- package/dist/actions/state.test.js +26 -1
- package/dist/actions/types.d.ts +49 -0
- package/dist/api.d.ts +5 -0
- package/dist/api.js +6 -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/favicon.png +0 -0
- package/dist/assets/favicon.svg +5 -0
- package/dist/color/api.d.ts +38 -0
- package/dist/color/api.js +10 -0
- package/dist/color/native-fallback.test.d.ts +1 -0
- package/dist/color/native-fallback.test.js +43 -0
- package/dist/color/primitive.d.ts +2 -0
- package/dist/color/primitive.js +40 -0
- package/dist/color/primitive.test.d.ts +1 -0
- package/dist/color/primitive.test.js +42 -0
- package/dist/color/shell-api.d.ts +2 -0
- package/dist/color/shell-api.js +11 -0
- package/dist/index.d.ts +0 -2
- package/dist/index.js +0 -2
- 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 +20 -0
- package/dist/shell-shard/shellShard.svelte.js +0 -4
- package/dist/shellRuntime.svelte.d.ts +20 -0
- package/dist/shellRuntime.svelte.js +16 -1
- package/dist/tokens.css +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { ActionEntry } from './registry';
|
|
2
|
+
import type { DispatcherState } from './dispatcher.svelte';
|
|
3
|
+
import type { MenuContainer } from '../apps/types';
|
|
4
|
+
export interface MenuBarItem {
|
|
5
|
+
id: string;
|
|
6
|
+
label: string;
|
|
7
|
+
shortcut: string | null;
|
|
8
|
+
group: string;
|
|
9
|
+
icon: string | undefined;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Resolved container list for the currently-active app:
|
|
13
|
+
* - activeAppId == null → returns []
|
|
14
|
+
* - declared has entries → returns declared, sorted by `order`
|
|
15
|
+
* ascending then declaration order
|
|
16
|
+
* for ties / undefined
|
|
17
|
+
* - declared is undefined → returns a copy of DEFAULT_MENU_CONTAINERS
|
|
18
|
+
*
|
|
19
|
+
* Callers can render unconditionally; the empty-array case naturally
|
|
20
|
+
* suppresses the menu bar at home.
|
|
21
|
+
*/
|
|
22
|
+
export declare function resolveMenuContainers(activeAppId: string | null, declared: readonly MenuContainer[] | undefined): MenuContainer[];
|
|
23
|
+
/**
|
|
24
|
+
* Items targeting `containerId`, filtered by current scope activation
|
|
25
|
+
* and de-duplicated to the innermost active scope per action id (mirrors
|
|
26
|
+
* contextMenuModel).
|
|
27
|
+
*/
|
|
28
|
+
export declare function resolveMenuItems(entries: readonly ActionEntry[], state: DispatcherState, containerId: string): MenuBarItem[];
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Pure model layer for the menu bar: resolves container list for the
|
|
3
|
+
* active app, and resolves per-container item lists by filtering the
|
|
4
|
+
* action registry by `menuItem` + scope-activation. Mirrors the
|
|
5
|
+
* de-duplication semantics of contextMenuModel.
|
|
6
|
+
*/
|
|
7
|
+
import { effectiveShortcut } from './bindings';
|
|
8
|
+
import { innermostActiveScope } from './scope-helpers';
|
|
9
|
+
import { DEFAULT_MENU_CONTAINERS } from './defaultMenuContainers';
|
|
10
|
+
/**
|
|
11
|
+
* Resolved container list for the currently-active app:
|
|
12
|
+
* - activeAppId == null → returns []
|
|
13
|
+
* - declared has entries → returns declared, sorted by `order`
|
|
14
|
+
* ascending then declaration order
|
|
15
|
+
* for ties / undefined
|
|
16
|
+
* - declared is undefined → returns a copy of DEFAULT_MENU_CONTAINERS
|
|
17
|
+
*
|
|
18
|
+
* Callers can render unconditionally; the empty-array case naturally
|
|
19
|
+
* suppresses the menu bar at home.
|
|
20
|
+
*/
|
|
21
|
+
export function resolveMenuContainers(activeAppId, declared) {
|
|
22
|
+
if (activeAppId == null)
|
|
23
|
+
return [];
|
|
24
|
+
if (declared == null)
|
|
25
|
+
return DEFAULT_MENU_CONTAINERS.slice();
|
|
26
|
+
const indexed = declared.map((c, i) => ({ c, i }));
|
|
27
|
+
indexed.sort((a, b) => {
|
|
28
|
+
const ao = a.c.order;
|
|
29
|
+
const bo = b.c.order;
|
|
30
|
+
if (ao != null && bo != null)
|
|
31
|
+
return ao - bo || a.i - b.i;
|
|
32
|
+
if (ao != null)
|
|
33
|
+
return -1;
|
|
34
|
+
if (bo != null)
|
|
35
|
+
return 1;
|
|
36
|
+
return a.i - b.i;
|
|
37
|
+
});
|
|
38
|
+
return indexed.map((x) => x.c);
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Items targeting `containerId`, filtered by current scope activation
|
|
42
|
+
* and de-duplicated to the innermost active scope per action id (mirrors
|
|
43
|
+
* contextMenuModel).
|
|
44
|
+
*/
|
|
45
|
+
export function resolveMenuItems(entries, state, containerId) {
|
|
46
|
+
var _a;
|
|
47
|
+
const out = [];
|
|
48
|
+
const seen = new Set();
|
|
49
|
+
for (const entry of entries) {
|
|
50
|
+
if (entry.action.menuItem !== containerId)
|
|
51
|
+
continue;
|
|
52
|
+
if (seen.has(entry.action.id))
|
|
53
|
+
continue;
|
|
54
|
+
const winning = innermostActiveScope(entry.action.scope, state, entry.ownerShardId);
|
|
55
|
+
if (!winning)
|
|
56
|
+
continue;
|
|
57
|
+
seen.add(entry.action.id);
|
|
58
|
+
out.push({
|
|
59
|
+
id: entry.action.id,
|
|
60
|
+
label: entry.action.label,
|
|
61
|
+
shortcut: effectiveShortcut(entry.action, state.bindings, state.platform),
|
|
62
|
+
group: (_a = entry.action.group) !== null && _a !== void 0 ? _a : '',
|
|
63
|
+
icon: entry.action.icon,
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
return out;
|
|
67
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { resolveMenuContainers, resolveMenuItems, } from './menuBarModel';
|
|
3
|
+
const mkEntry = (a, owner = 'shard.x') => ({
|
|
4
|
+
ownerShardId: owner,
|
|
5
|
+
action: Object.assign({ id: 'a', label: 'A', scope: 'home', run: () => { } }, a),
|
|
6
|
+
});
|
|
7
|
+
const mkState = (o = {}) => (Object.assign({ activeAppId: null, activeAppRequiredShards: new Set(), autostartShards: new Set(), mountedViewIds: new Set(), focusedViewId: null, selection: null, bindings: {}, platform: 'other' }, o));
|
|
8
|
+
describe('resolveMenuContainers', () => {
|
|
9
|
+
it('returns [] when no app is active', () => {
|
|
10
|
+
expect(resolveMenuContainers(null, undefined)).toEqual([]);
|
|
11
|
+
});
|
|
12
|
+
it('returns DEFAULT_MENU_CONTAINERS when app has no manifest.menus', () => {
|
|
13
|
+
const out = resolveMenuContainers('app.a', undefined);
|
|
14
|
+
expect(out.map((c) => c.id)).toEqual(['file', 'edit', 'view', 'window', 'help']);
|
|
15
|
+
});
|
|
16
|
+
it('returns manifest.menus when declared', () => {
|
|
17
|
+
const declared = [
|
|
18
|
+
{ id: 'project', label: 'Project' },
|
|
19
|
+
{ id: 'help', label: 'Help' },
|
|
20
|
+
];
|
|
21
|
+
expect(resolveMenuContainers('app.a', declared).map((c) => c.id))
|
|
22
|
+
.toEqual(['project', 'help']);
|
|
23
|
+
});
|
|
24
|
+
it('sorts by `order` ascending, then by declaration order for ties/undefined', () => {
|
|
25
|
+
const declared = [
|
|
26
|
+
{ id: 'a', label: 'A', order: 10 },
|
|
27
|
+
{ id: 'b', label: 'B' },
|
|
28
|
+
{ id: 'c', label: 'C', order: 5 },
|
|
29
|
+
{ id: 'd', label: 'D' },
|
|
30
|
+
];
|
|
31
|
+
expect(resolveMenuContainers('app.a', declared).map((c) => c.id))
|
|
32
|
+
.toEqual(['c', 'a', 'b', 'd']);
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
describe('resolveMenuItems', () => {
|
|
36
|
+
const stateWithApp = mkState({
|
|
37
|
+
activeAppId: 'app.a',
|
|
38
|
+
activeAppRequiredShards: new Set(['shard.x']),
|
|
39
|
+
});
|
|
40
|
+
it('returns only actions whose menuItem matches the container id', () => {
|
|
41
|
+
const entries = [
|
|
42
|
+
mkEntry({ id: 'open', scope: 'app', menuItem: 'file', label: 'Open' }),
|
|
43
|
+
mkEntry({ id: 'copy', scope: 'app', menuItem: 'edit', label: 'Copy' }),
|
|
44
|
+
mkEntry({ id: 'close', scope: 'app', menuItem: 'file', label: 'Close' }),
|
|
45
|
+
];
|
|
46
|
+
const out = resolveMenuItems(entries, stateWithApp, 'file');
|
|
47
|
+
expect(out.map((i) => i.id)).toEqual(['open', 'close']);
|
|
48
|
+
});
|
|
49
|
+
it('skips actions whose scope is not currently active', () => {
|
|
50
|
+
const entries = [
|
|
51
|
+
mkEntry({ id: 'open', scope: 'app', menuItem: 'file', label: 'Open' }),
|
|
52
|
+
mkEntry({ id: 'help', scope: 'home', menuItem: 'file', label: 'Help' }),
|
|
53
|
+
];
|
|
54
|
+
const out = resolveMenuItems(entries, stateWithApp, 'file');
|
|
55
|
+
expect(out.map((i) => i.id)).toEqual(['open']);
|
|
56
|
+
});
|
|
57
|
+
it('skips actions without a menuItem field', () => {
|
|
58
|
+
const entries = [
|
|
59
|
+
mkEntry({ id: 'a', scope: 'app', menuItem: 'file', label: 'A' }),
|
|
60
|
+
mkEntry({ id: 'b', scope: 'app', label: 'B' }),
|
|
61
|
+
];
|
|
62
|
+
const out = resolveMenuItems(entries, stateWithApp, 'file');
|
|
63
|
+
expect(out.map((i) => i.id)).toEqual(['a']);
|
|
64
|
+
});
|
|
65
|
+
it('returns [] for an unknown container id', () => {
|
|
66
|
+
const entries = [
|
|
67
|
+
mkEntry({ id: 'open', scope: 'app', menuItem: 'file', label: 'Open' }),
|
|
68
|
+
];
|
|
69
|
+
expect(resolveMenuItems(entries, stateWithApp, 'sausage')).toEqual([]);
|
|
70
|
+
});
|
|
71
|
+
it('de-duplicates multi-scope actions by innermost active scope', () => {
|
|
72
|
+
const state = mkState({
|
|
73
|
+
activeAppId: 'app.a',
|
|
74
|
+
activeAppRequiredShards: new Set(['shard.x']),
|
|
75
|
+
autostartShards: new Set(['shard.x']),
|
|
76
|
+
});
|
|
77
|
+
const entries = [
|
|
78
|
+
mkEntry({ id: 'p', scope: ['home', 'app'], menuItem: 'file', label: 'P' }),
|
|
79
|
+
];
|
|
80
|
+
const out = resolveMenuItems(entries, state, 'file');
|
|
81
|
+
expect(out).toHaveLength(1);
|
|
82
|
+
expect(out[0].id).toBe('p');
|
|
83
|
+
});
|
|
84
|
+
});
|
|
@@ -1,22 +1,11 @@
|
|
|
1
|
-
|
|
1
|
+
/*
|
|
2
|
+
* Palette candidate builder — returns every active (paletteItem !== false)
|
|
3
|
+
* action, deduplicated, with shortcut and scope badge resolved. Uses
|
|
4
|
+
* innermost-first scope selection so the badge matches keyboard dispatch
|
|
5
|
+
* and context-menu tiering (audit: RFC #24).
|
|
6
|
+
*/
|
|
2
7
|
import { effectiveShortcut } from './bindings';
|
|
3
|
-
|
|
4
|
-
return Array.isArray(s) ? s : [s];
|
|
5
|
-
}
|
|
6
|
-
function anyScopeActive(scope, state, owner) {
|
|
7
|
-
for (const s of normalize(scope)) {
|
|
8
|
-
if (isScopeActive(s, state, owner))
|
|
9
|
-
return s;
|
|
10
|
-
}
|
|
11
|
-
return null;
|
|
12
|
-
}
|
|
13
|
-
function scopeBadge(scope) {
|
|
14
|
-
if (scope === 'home' || scope === 'app')
|
|
15
|
-
return null;
|
|
16
|
-
if (typeof scope === 'string')
|
|
17
|
-
return scope; // view:X / focus:X
|
|
18
|
-
return scope.element;
|
|
19
|
-
}
|
|
8
|
+
import { innermostActiveScope, scopeBadge } from './scope-helpers';
|
|
20
9
|
export function buildPaletteCandidates(entries, state) {
|
|
21
10
|
const out = [];
|
|
22
11
|
const seen = new Set();
|
|
@@ -25,15 +14,15 @@ export function buildPaletteCandidates(entries, state) {
|
|
|
25
14
|
continue;
|
|
26
15
|
if (seen.has(entry.action.id))
|
|
27
16
|
continue;
|
|
28
|
-
const
|
|
29
|
-
if (!
|
|
17
|
+
const winning = innermostActiveScope(entry.action.scope, state, entry.ownerShardId);
|
|
18
|
+
if (!winning)
|
|
30
19
|
continue;
|
|
31
20
|
seen.add(entry.action.id);
|
|
32
21
|
out.push({
|
|
33
22
|
id: entry.action.id,
|
|
34
23
|
label: entry.action.label,
|
|
35
24
|
shortcut: effectiveShortcut(entry.action, state.bindings, state.platform),
|
|
36
|
-
scopeBadge: scopeBadge(
|
|
25
|
+
scopeBadge: scopeBadge(winning),
|
|
37
26
|
});
|
|
38
27
|
}
|
|
39
28
|
return out;
|
|
@@ -30,4 +30,20 @@ describe('buildPaletteCandidates', () => {
|
|
|
30
30
|
const out = buildPaletteCandidates(entries, state);
|
|
31
31
|
expect(out).toHaveLength(1);
|
|
32
32
|
});
|
|
33
|
+
it('reports the innermost active scope in scopeBadge for multi-tier actions', () => {
|
|
34
|
+
const entries = [
|
|
35
|
+
mkEntry({
|
|
36
|
+
id: 'm', label: 'M',
|
|
37
|
+
scope: ['app', 'view:editor'],
|
|
38
|
+
}, '__sh3core__'),
|
|
39
|
+
];
|
|
40
|
+
const state = mkState({
|
|
41
|
+
activeAppId: 'a',
|
|
42
|
+
autostartShards: new Set(['__sh3core__']),
|
|
43
|
+
mountedViewIds: new Set(['editor']),
|
|
44
|
+
});
|
|
45
|
+
const out = buildPaletteCandidates(entries, state);
|
|
46
|
+
expect(out).toHaveLength(1);
|
|
47
|
+
expect(out[0].scopeBadge).toBe('view:editor');
|
|
48
|
+
});
|
|
33
49
|
});
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { AtomicScope, ActionScope } from './types';
|
|
2
|
+
import { type DispatcherState, type TierName } from './dispatcher.svelte';
|
|
3
|
+
export declare function scopeToTier(scope: AtomicScope): TierName;
|
|
4
|
+
export declare function normalizeScope(scope: ActionScope): AtomicScope[];
|
|
5
|
+
export declare function scopeBadge(scope: AtomicScope): string | null;
|
|
6
|
+
/**
|
|
7
|
+
* Walk TIER_ORDER innermost→outermost; return the first `AtomicScope` of
|
|
8
|
+
* the given action whose tier is currently active. Returns `null` if no
|
|
9
|
+
* scope is active.
|
|
10
|
+
*/
|
|
11
|
+
export declare function innermostActiveScope(scope: ActionScope, state: DispatcherState, ownerShardId: string): AtomicScope | null;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Pure scope/tier helpers shared by the dispatcher, context menu,
|
|
3
|
+
* palette, and listActive(). Keep this file side-effect free — it is
|
|
4
|
+
* imported by both reactive and non-reactive modules.
|
|
5
|
+
*/
|
|
6
|
+
import { isScopeActive, TIER_ORDER, } from './dispatcher.svelte';
|
|
7
|
+
export function scopeToTier(scope) {
|
|
8
|
+
if (scope === 'home')
|
|
9
|
+
return 'home';
|
|
10
|
+
if (scope === 'app')
|
|
11
|
+
return 'app';
|
|
12
|
+
if (typeof scope === 'string' && scope.startsWith('view:'))
|
|
13
|
+
return 'view';
|
|
14
|
+
if (typeof scope === 'string' && scope.startsWith('focus:'))
|
|
15
|
+
return 'focus';
|
|
16
|
+
return 'element';
|
|
17
|
+
}
|
|
18
|
+
export function normalizeScope(scope) {
|
|
19
|
+
return Array.isArray(scope) ? scope : [scope];
|
|
20
|
+
}
|
|
21
|
+
export function scopeBadge(scope) {
|
|
22
|
+
if (scope === 'home' || scope === 'app')
|
|
23
|
+
return null;
|
|
24
|
+
if (typeof scope === 'string')
|
|
25
|
+
return scope;
|
|
26
|
+
return scope.element;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Walk TIER_ORDER innermost→outermost; return the first `AtomicScope` of
|
|
30
|
+
* the given action whose tier is currently active. Returns `null` if no
|
|
31
|
+
* scope is active.
|
|
32
|
+
*/
|
|
33
|
+
export function innermostActiveScope(scope, state, ownerShardId) {
|
|
34
|
+
var _a;
|
|
35
|
+
const scopes = normalizeScope(scope);
|
|
36
|
+
const buckets = {};
|
|
37
|
+
for (const s of scopes) {
|
|
38
|
+
const tier = scopeToTier(s);
|
|
39
|
+
((_a = buckets[tier]) !== null && _a !== void 0 ? _a : (buckets[tier] = [])).push(s);
|
|
40
|
+
}
|
|
41
|
+
for (const tier of TIER_ORDER) {
|
|
42
|
+
const bucket = buckets[tier];
|
|
43
|
+
if (!bucket)
|
|
44
|
+
continue;
|
|
45
|
+
for (const s of bucket) {
|
|
46
|
+
if (isScopeActive(s, state, ownerShardId))
|
|
47
|
+
return s;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { scopeToTier, normalizeScope, scopeBadge, innermostActiveScope, } from './scope-helpers';
|
|
3
|
+
const mkState = (o = {}) => (Object.assign({ activeAppId: null, activeAppRequiredShards: new Set(), autostartShards: new Set(), mountedViewIds: new Set(), focusedViewId: null, selection: null, bindings: {}, platform: 'other' }, o));
|
|
4
|
+
describe('scopeToTier', () => {
|
|
5
|
+
it('maps atoms to tier names', () => {
|
|
6
|
+
expect(scopeToTier('home')).toBe('home');
|
|
7
|
+
expect(scopeToTier('app')).toBe('app');
|
|
8
|
+
expect(scopeToTier('view:editor')).toBe('view');
|
|
9
|
+
expect(scopeToTier('focus:pane-1')).toBe('focus');
|
|
10
|
+
expect(scopeToTier({ element: 'row' })).toBe('element');
|
|
11
|
+
});
|
|
12
|
+
});
|
|
13
|
+
describe('normalizeScope', () => {
|
|
14
|
+
it('wraps single scope', () => {
|
|
15
|
+
expect(normalizeScope('home')).toEqual(['home']);
|
|
16
|
+
});
|
|
17
|
+
it('returns arrays as-is', () => {
|
|
18
|
+
expect(normalizeScope(['home', 'app'])).toEqual(['home', 'app']);
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
describe('scopeBadge', () => {
|
|
22
|
+
it('returns null for home/app', () => {
|
|
23
|
+
expect(scopeBadge('home')).toBeNull();
|
|
24
|
+
expect(scopeBadge('app')).toBeNull();
|
|
25
|
+
});
|
|
26
|
+
it('returns the full string for view/focus scopes', () => {
|
|
27
|
+
expect(scopeBadge('view:editor')).toBe('view:editor');
|
|
28
|
+
expect(scopeBadge('focus:pane-1')).toBe('focus:pane-1');
|
|
29
|
+
});
|
|
30
|
+
it('returns the element type for element scopes', () => {
|
|
31
|
+
expect(scopeBadge({ element: 'row' })).toBe('row');
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
describe('innermostActiveScope', () => {
|
|
35
|
+
it('returns null when no scope is active', () => {
|
|
36
|
+
expect(innermostActiveScope('app', mkState(), 'owner')).toBeNull();
|
|
37
|
+
});
|
|
38
|
+
it('picks the innermost active tier across a multi-scope action', () => {
|
|
39
|
+
const state = mkState({
|
|
40
|
+
activeAppId: 'a', autostartShards: new Set(['owner']),
|
|
41
|
+
mountedViewIds: new Set(['editor']),
|
|
42
|
+
});
|
|
43
|
+
const winner = innermostActiveScope(['app', 'view:editor'], state, 'owner');
|
|
44
|
+
expect(winner).toBe('view:editor');
|
|
45
|
+
});
|
|
46
|
+
it('falls back to outer tier when inner is inactive', () => {
|
|
47
|
+
const state = mkState({
|
|
48
|
+
activeAppId: 'a', autostartShards: new Set(['owner']),
|
|
49
|
+
});
|
|
50
|
+
const winner = innermostActiveScope(['app', 'view:editor'], state, 'owner');
|
|
51
|
+
expect(winner).toBe('app');
|
|
52
|
+
});
|
|
53
|
+
it('element scope beats view scope when both active', () => {
|
|
54
|
+
const state = mkState({
|
|
55
|
+
activeAppId: 'a', autostartShards: new Set(['owner']),
|
|
56
|
+
mountedViewIds: new Set(['editor']),
|
|
57
|
+
selection: { type: 'row', ref: {}, ownerShardId: 'owner' },
|
|
58
|
+
});
|
|
59
|
+
const winner = innermostActiveScope(['view:editor', { element: 'row' }], state, 'owner');
|
|
60
|
+
expect(winner).toEqual({ element: 'row' });
|
|
61
|
+
});
|
|
62
|
+
});
|
|
@@ -2,6 +2,7 @@ import { describe, it, expect, beforeEach } from 'vitest';
|
|
|
2
2
|
import { shell } from '../shellRuntime.svelte';
|
|
3
3
|
import { __setBindingsZone } from './bindings-store';
|
|
4
4
|
import { __resetDispatcherStateForTest, setActiveApp } from './state.svelte';
|
|
5
|
+
import { registerAction, __resetActionsRegistryForTest } from './registry';
|
|
5
6
|
describe('shell.actions facade', () => {
|
|
6
7
|
beforeEach(() => {
|
|
7
8
|
__setBindingsZone({ bindings: {} });
|
|
@@ -20,3 +21,52 @@ describe('shell.actions facade', () => {
|
|
|
20
21
|
expect(now['shard.x.save']).toBeUndefined();
|
|
21
22
|
});
|
|
22
23
|
});
|
|
24
|
+
describe('shell.actions.listActive', () => {
|
|
25
|
+
beforeEach(() => {
|
|
26
|
+
__resetActionsRegistryForTest();
|
|
27
|
+
__resetDispatcherStateForTest();
|
|
28
|
+
});
|
|
29
|
+
it('returns descriptors for currently-active registered actions', () => {
|
|
30
|
+
const dispose = registerAction({
|
|
31
|
+
id: 'home.hello', label: 'Hello', scope: 'home',
|
|
32
|
+
defaultShortcut: 'Mod+H', run: () => { },
|
|
33
|
+
}, 'shard.test');
|
|
34
|
+
const snap = shell.actions.listActive();
|
|
35
|
+
expect(snap.map((d) => d.id)).toContain('home.hello');
|
|
36
|
+
dispose();
|
|
37
|
+
});
|
|
38
|
+
it('snapshot is stable across calls (returns fresh array)', () => {
|
|
39
|
+
registerAction({
|
|
40
|
+
id: 'home.a', label: 'A', scope: 'home', run: () => { },
|
|
41
|
+
}, 'shard.test');
|
|
42
|
+
const a = shell.actions.listActive();
|
|
43
|
+
const b = shell.actions.listActive();
|
|
44
|
+
expect(a).not.toBe(b);
|
|
45
|
+
expect(a.map((d) => d.id)).toEqual(b.map((d) => d.id));
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
describe('shell.actions.onActiveChange', () => {
|
|
49
|
+
beforeEach(() => {
|
|
50
|
+
__resetActionsRegistryForTest();
|
|
51
|
+
__resetDispatcherStateForTest();
|
|
52
|
+
});
|
|
53
|
+
it('fires on dispatcher state change', () => {
|
|
54
|
+
let n = 0;
|
|
55
|
+
const off = shell.actions.onActiveChange(() => { n++; });
|
|
56
|
+
setActiveApp('a', new Set());
|
|
57
|
+
expect(n).toBe(1);
|
|
58
|
+
off();
|
|
59
|
+
});
|
|
60
|
+
it('fires when an action is registered or unregistered', () => {
|
|
61
|
+
let n = 0;
|
|
62
|
+
const off = shell.actions.onActiveChange(() => { n++; });
|
|
63
|
+
const dispose = registerAction({
|
|
64
|
+
id: 't', label: 'T', scope: 'home', run: () => { },
|
|
65
|
+
}, 'shard.test');
|
|
66
|
+
expect(n).toBeGreaterThanOrEqual(1);
|
|
67
|
+
const afterRegister = n;
|
|
68
|
+
dispose();
|
|
69
|
+
expect(n).toBeGreaterThan(afterRegister);
|
|
70
|
+
off();
|
|
71
|
+
});
|
|
72
|
+
});
|
|
@@ -1,4 +1,16 @@
|
|
|
1
1
|
import type { DispatcherState } from './dispatcher.svelte';
|
|
2
|
+
/**
|
|
3
|
+
* Subscribe to any change that could affect the set of currently-active
|
|
4
|
+
* actions or their resolved shortcuts (app/view/focus/selection/bindings
|
|
5
|
+
* transitions). Call sites outside this module (e.g., the shell
|
|
6
|
+
* assembling registry-change notifications) dispatch via
|
|
7
|
+
* {@link __notifyActiveChange}.
|
|
8
|
+
*/
|
|
9
|
+
export declare function onActiveChange(cb: () => void): () => void;
|
|
10
|
+
/** Internal — fired by the shell runtime when the action registry mutates. */
|
|
11
|
+
export declare function __notifyActiveChange(): void;
|
|
12
|
+
/** Test-only alias for the internal notifier. */
|
|
13
|
+
export declare const __notifyActiveChangeForTest: typeof __notifyActiveChange;
|
|
2
14
|
export declare function setActiveApp(appId: string | null, requiredShards: Set<string>): void;
|
|
3
15
|
export declare function setAutostartShards(shards: Set<string>): void;
|
|
4
16
|
export declare function setMountedViewIds(ids: Set<string>): void;
|
|
@@ -18,15 +18,46 @@ let mountedViewIds = $state(new Set());
|
|
|
18
18
|
let focusedViewId = $state(null);
|
|
19
19
|
let userBindings = $state({});
|
|
20
20
|
const platform = detectPlatform();
|
|
21
|
+
const activeChangeListeners = new Set();
|
|
22
|
+
function notifyActiveChange() {
|
|
23
|
+
for (const cb of activeChangeListeners) {
|
|
24
|
+
try {
|
|
25
|
+
cb();
|
|
26
|
+
}
|
|
27
|
+
catch (err) {
|
|
28
|
+
console.error('[sh3] onActiveChange listener threw', err);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Subscribe to any change that could affect the set of currently-active
|
|
34
|
+
* actions or their resolved shortcuts (app/view/focus/selection/bindings
|
|
35
|
+
* transitions). Call sites outside this module (e.g., the shell
|
|
36
|
+
* assembling registry-change notifications) dispatch via
|
|
37
|
+
* {@link __notifyActiveChange}.
|
|
38
|
+
*/
|
|
39
|
+
export function onActiveChange(cb) {
|
|
40
|
+
activeChangeListeners.add(cb);
|
|
41
|
+
return () => { activeChangeListeners.delete(cb); };
|
|
42
|
+
}
|
|
43
|
+
/** Internal — fired by the shell runtime when the action registry mutates. */
|
|
44
|
+
export function __notifyActiveChange() {
|
|
45
|
+
notifyActiveChange();
|
|
46
|
+
}
|
|
47
|
+
/** Test-only alias for the internal notifier. */
|
|
48
|
+
export const __notifyActiveChangeForTest = __notifyActiveChange;
|
|
21
49
|
export function setActiveApp(appId, requiredShards) {
|
|
22
50
|
activeAppId = appId;
|
|
23
51
|
activeAppRequiredShards = new Set(requiredShards);
|
|
52
|
+
notifyActiveChange();
|
|
24
53
|
}
|
|
25
54
|
export function setAutostartShards(shards) {
|
|
26
55
|
autostartShards = new Set(shards);
|
|
56
|
+
notifyActiveChange();
|
|
27
57
|
}
|
|
28
58
|
export function setMountedViewIds(ids) {
|
|
29
59
|
mountedViewIds = new Set(ids);
|
|
60
|
+
notifyActiveChange();
|
|
30
61
|
}
|
|
31
62
|
/**
|
|
32
63
|
* One-shot snapshot: walk the active layout tree and update
|
|
@@ -42,12 +73,15 @@ export function syncMountedViewIdsFromLayout() {
|
|
|
42
73
|
ids.add(r.viewId);
|
|
43
74
|
}
|
|
44
75
|
mountedViewIds = ids;
|
|
76
|
+
notifyActiveChange();
|
|
45
77
|
}
|
|
46
78
|
export function setFocusedViewId(id) {
|
|
47
79
|
focusedViewId = id;
|
|
80
|
+
notifyActiveChange();
|
|
48
81
|
}
|
|
49
82
|
export function setUserBindings(bindings) {
|
|
50
83
|
userBindings = Object.assign({}, bindings);
|
|
84
|
+
notifyActiveChange();
|
|
51
85
|
}
|
|
52
86
|
export function getLiveDispatcherState() {
|
|
53
87
|
return {
|
|
@@ -64,6 +98,7 @@ export function getLiveDispatcherState() {
|
|
|
64
98
|
export function addAutostartShard(id) {
|
|
65
99
|
if (!autostartShards.has(id)) {
|
|
66
100
|
autostartShards = new Set([...autostartShards, id]);
|
|
101
|
+
notifyActiveChange();
|
|
67
102
|
}
|
|
68
103
|
}
|
|
69
104
|
export function __resetDispatcherStateForTest() {
|
|
@@ -73,4 +108,5 @@ export function __resetDispatcherStateForTest() {
|
|
|
73
108
|
mountedViewIds = new Set();
|
|
74
109
|
focusedViewId = null;
|
|
75
110
|
userBindings = {};
|
|
111
|
+
activeChangeListeners.clear();
|
|
76
112
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, it, expect, beforeEach } from 'vitest';
|
|
2
|
-
import { setActiveApp, setFocusedViewId, setMountedViewIds, setUserBindings, getLiveDispatcherState, __resetDispatcherStateForTest, } from './state.svelte';
|
|
2
|
+
import { setActiveApp, setAutostartShards, setFocusedViewId, setMountedViewIds, setUserBindings, getLiveDispatcherState, onActiveChange, __notifyActiveChangeForTest, __resetDispatcherStateForTest, } from './state.svelte';
|
|
3
3
|
import { __resetSelectionForTest, makeSelectionApi } from './selection.svelte';
|
|
4
4
|
describe('live dispatcher state', () => {
|
|
5
5
|
beforeEach(() => {
|
|
@@ -38,3 +38,28 @@ describe('live dispatcher state', () => {
|
|
|
38
38
|
expect((_a = getLiveDispatcherState().selection) === null || _a === void 0 ? void 0 : _a.type).toBe('orb');
|
|
39
39
|
});
|
|
40
40
|
});
|
|
41
|
+
describe('onActiveChange', () => {
|
|
42
|
+
beforeEach(() => {
|
|
43
|
+
__resetDispatcherStateForTest();
|
|
44
|
+
__resetSelectionForTest();
|
|
45
|
+
});
|
|
46
|
+
it('fires on every setter', () => {
|
|
47
|
+
let n = 0;
|
|
48
|
+
const off = onActiveChange(() => { n++; });
|
|
49
|
+
setActiveApp('a', new Set(['s']));
|
|
50
|
+
setAutostartShards(new Set(['s']));
|
|
51
|
+
setMountedViewIds(new Set(['v']));
|
|
52
|
+
setFocusedViewId('v');
|
|
53
|
+
setUserBindings({ foo: 'Ctrl+K' });
|
|
54
|
+
expect(n).toBe(5);
|
|
55
|
+
off();
|
|
56
|
+
setActiveApp(null, new Set());
|
|
57
|
+
expect(n).toBe(5);
|
|
58
|
+
});
|
|
59
|
+
it('fires on external notify (used for registry change)', () => {
|
|
60
|
+
let n = 0;
|
|
61
|
+
onActiveChange(() => { n++; });
|
|
62
|
+
__notifyActiveChangeForTest();
|
|
63
|
+
expect(n).toBe(1);
|
|
64
|
+
});
|
|
65
|
+
});
|
package/dist/actions/types.d.ts
CHANGED
|
@@ -8,6 +8,14 @@ export interface Action {
|
|
|
8
8
|
scope: ActionScope;
|
|
9
9
|
contextItem?: boolean;
|
|
10
10
|
paletteItem?: boolean;
|
|
11
|
+
/**
|
|
12
|
+
* Optional menu container id. When set and the active app's declared
|
|
13
|
+
* (or canonical fallback) menu list contains this id, the action
|
|
14
|
+
* appears in that container's dropdown. Orphaned values render
|
|
15
|
+
* nowhere in the menu bar; the action remains reachable via
|
|
16
|
+
* palette/hotkey/context menu.
|
|
17
|
+
*/
|
|
18
|
+
menuItem?: string;
|
|
11
19
|
defaultShortcut?: string;
|
|
12
20
|
icon?: string;
|
|
13
21
|
group?: string;
|
|
@@ -54,3 +62,44 @@ export interface ResolvedAction {
|
|
|
54
62
|
ownerShardId: string;
|
|
55
63
|
effectiveShortcut: string | null;
|
|
56
64
|
}
|
|
65
|
+
/**
|
|
66
|
+
* Where an action's effective shortcut came from:
|
|
67
|
+
* - `'default'` — `defaultShortcut` resolved for the current platform
|
|
68
|
+
* - `'user'` — the user supplied an explicit override
|
|
69
|
+
* - `'disabled'` — the user rebound to `null` to disable dispatch
|
|
70
|
+
* - `'none'` — no default and no override
|
|
71
|
+
*/
|
|
72
|
+
export type BindingSource = 'default' | 'user' | 'disabled' | 'none';
|
|
73
|
+
/**
|
|
74
|
+
* Read-only snapshot describing one action that is currently active.
|
|
75
|
+
* Produced by `shell.actions.listActive()`. One descriptor per action id,
|
|
76
|
+
* reporting the innermost active scope (same tier order as keyboard
|
|
77
|
+
* dispatch: element > focus > view > app > home).
|
|
78
|
+
*/
|
|
79
|
+
export interface ActiveActionDescriptor {
|
|
80
|
+
/** Stable action id as registered. */
|
|
81
|
+
id: string;
|
|
82
|
+
/** Human-readable label as registered. */
|
|
83
|
+
label: string;
|
|
84
|
+
/**
|
|
85
|
+
* Shortcut string as it would dispatch right now (platform-resolved,
|
|
86
|
+
* user-rebind applied). `null` when `bindingSource` is `'disabled'` or
|
|
87
|
+
* `'none'`.
|
|
88
|
+
*/
|
|
89
|
+
effectiveShortcut: string | null;
|
|
90
|
+
/**
|
|
91
|
+
* Where the effective shortcut came from. Help UIs can distinguish
|
|
92
|
+
* user-disabled (render greyed) from no-shortcut (hide).
|
|
93
|
+
*/
|
|
94
|
+
bindingSource: BindingSource;
|
|
95
|
+
/** The innermost active tier of the action's scope. */
|
|
96
|
+
scope: AtomicScope;
|
|
97
|
+
/** Display hint: `null` for home/app, else full string or element type. */
|
|
98
|
+
scopeBadge: string | null;
|
|
99
|
+
/** Carried through from the registered action. */
|
|
100
|
+
group?: string;
|
|
101
|
+
icon?: string;
|
|
102
|
+
ownerShardId: string;
|
|
103
|
+
paletteItem: boolean;
|
|
104
|
+
contextItem: boolean;
|
|
105
|
+
}
|
package/dist/api.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ export { shell } from './shellRuntime.svelte';
|
|
|
2
2
|
export type { Shell } from './shellRuntime.svelte';
|
|
3
3
|
export type { Shard, ShardManifest, SourceShard, SourceShardManifest, ShardContext, ViewDeclaration, ViewFactory, ViewHandle, MountContext, } from './shards/types';
|
|
4
4
|
export type { LayoutNode, SplitNode, TabsNode, SlotNode, TabEntry, SplitDirection, SizeMode, LayoutTree, FloatEntry, LayoutPreset, CanonicalPreset, TreeRootRef as SlotLocation, } from './layout/types';
|
|
5
|
+
export type { ActiveActionDescriptor, BindingSource, AtomicScope, ActionScope, } from './actions/types';
|
|
5
6
|
export type { FloatManager, FloatOptions } from './overlays/float';
|
|
6
7
|
export type { ModalManager } from './overlays/modal';
|
|
7
8
|
export type { PopupManager } from './overlays/popup';
|
|
@@ -24,6 +25,8 @@ export type { SyncPolicy, SyncPolicyRule, DocStatus, ConflictFile, ConflictBranc
|
|
|
24
25
|
export { PERMISSION_SYNC_PEER, PERMISSION_SYNC_POLICY } from './documents/sync-types';
|
|
25
26
|
export type { ConflictItem, ConflictBranch as ConflictManagerBranch, ResolveOptions as ConflictResolveOptions, ResolveOutcome as ConflictResolveOutcome, ResolveDocumentsInput as ConflictResolveDocumentsInput, DocsResolveOutcome as ConflictDocsResolveOutcome, ConflictRenderer, ConflictRendererProps, ConflictsApi, } from './conflicts/api';
|
|
26
27
|
export { CONFLICT_RENDERER_POINT, ConflictPermissionError, ConflictSessionOrphanedError, } from './conflicts/api';
|
|
28
|
+
export type { ColorPickOptions, ColorContribution, ColorApi, } from './color/api';
|
|
29
|
+
export { COLOR_PICKER_POINT } from './color/api';
|
|
27
30
|
export { registeredShards, activeShards } from './shards/activate.svelte';
|
|
28
31
|
export type { RegistryIndex, PackageEntry, PackageVersion, RequiredDependency, InstalledPackage, InstallResult, PackageMeta, } from './registry/types';
|
|
29
32
|
export type { ResolvedPackage } from './registry/client';
|
|
@@ -47,3 +50,5 @@ export { listVerbs } from './shards/registry';
|
|
|
47
50
|
export { VERSION } from './version';
|
|
48
51
|
export declare const FRAMEWORK_SHARD_IDS: readonly string[];
|
|
49
52
|
export { setTokenOverrides, clearTokenOverrides, getTokenOverrides, } from './theme';
|
|
53
|
+
export { default as Button } from './primitives/Button.svelte';
|
|
54
|
+
export { provideIcons, getIconSprite, type ButtonVariant } from './primitives/icon-context';
|