sh3-core 0.11.2 → 0.11.4
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/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/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/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 +41 -0
- package/dist/api.d.ts +5 -0
- package/dist/api.js +6 -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/shellRuntime.svelte.d.ts +20 -0
- package/dist/shellRuntime.svelte.js +16 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
|
@@ -1,4 +1,13 @@
|
|
|
1
1
|
import { type Platform } from './shortcuts';
|
|
2
|
-
import type { Action } from './types';
|
|
2
|
+
import type { Action, BindingSource } from './types';
|
|
3
3
|
export type BindingOverrides = Record<string, string | null>;
|
|
4
4
|
export declare function effectiveShortcut(action: Action, overrides: BindingOverrides, platform?: Platform): string | null;
|
|
5
|
+
/**
|
|
6
|
+
* Like {@link effectiveShortcut}, but also reports the binding source so
|
|
7
|
+
* consumers (e.g., Help views) can distinguish "no shortcut assigned"
|
|
8
|
+
* from "user turned this off" — both of which yield a `null` shortcut.
|
|
9
|
+
*/
|
|
10
|
+
export declare function effectiveShortcutWithSource(action: Action, overrides: BindingOverrides, platform?: Platform): {
|
|
11
|
+
shortcut: string | null;
|
|
12
|
+
source: BindingSource;
|
|
13
|
+
};
|
package/dist/actions/bindings.js
CHANGED
|
@@ -15,3 +15,19 @@ export function effectiveShortcut(action, overrides, platform = 'other') {
|
|
|
15
15
|
return null;
|
|
16
16
|
return resolveMod(action.defaultShortcut, platform);
|
|
17
17
|
}
|
|
18
|
+
/**
|
|
19
|
+
* Like {@link effectiveShortcut}, but also reports the binding source so
|
|
20
|
+
* consumers (e.g., Help views) can distinguish "no shortcut assigned"
|
|
21
|
+
* from "user turned this off" — both of which yield a `null` shortcut.
|
|
22
|
+
*/
|
|
23
|
+
export function effectiveShortcutWithSource(action, overrides, platform = 'other') {
|
|
24
|
+
if (action.id in overrides) {
|
|
25
|
+
const o = overrides[action.id];
|
|
26
|
+
if (o === null)
|
|
27
|
+
return { shortcut: null, source: 'disabled' };
|
|
28
|
+
return { shortcut: canonicalizeShortcut(o), source: 'user' };
|
|
29
|
+
}
|
|
30
|
+
if (!action.defaultShortcut)
|
|
31
|
+
return { shortcut: null, source: 'none' };
|
|
32
|
+
return { shortcut: resolveMod(action.defaultShortcut, platform), source: 'default' };
|
|
33
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import { effectiveShortcut } from './bindings';
|
|
2
|
+
import { effectiveShortcut, effectiveShortcutWithSource } from './bindings';
|
|
3
3
|
const mkAction = (overrides = {}) => (Object.assign({ id: 'shard.save', label: 'Save', scope: 'home', run: () => { } }, overrides));
|
|
4
4
|
describe('effectiveShortcut', () => {
|
|
5
5
|
it('returns defaultShortcut when no override', () => {
|
|
@@ -28,3 +28,25 @@ describe('effectiveShortcut', () => {
|
|
|
28
28
|
expect(effectiveShortcut(a, { 'shard.save': 'Ctrl+K' }, 'mac')).toBe('Ctrl+K');
|
|
29
29
|
});
|
|
30
30
|
});
|
|
31
|
+
describe('effectiveShortcutWithSource', () => {
|
|
32
|
+
it('returns default + "default" when no override and defaultShortcut present', () => {
|
|
33
|
+
const a = mkAction({ defaultShortcut: 'Mod+S' });
|
|
34
|
+
expect(effectiveShortcutWithSource(a, {}, 'other'))
|
|
35
|
+
.toEqual({ shortcut: 'Ctrl+S', source: 'default' });
|
|
36
|
+
});
|
|
37
|
+
it('returns canonicalized override + "user" when user rebound to a key', () => {
|
|
38
|
+
const a = mkAction({ defaultShortcut: 'Mod+S' });
|
|
39
|
+
expect(effectiveShortcutWithSource(a, { 'shard.save': 'Ctrl+K' }, 'other'))
|
|
40
|
+
.toEqual({ shortcut: 'Ctrl+K', source: 'user' });
|
|
41
|
+
});
|
|
42
|
+
it('returns null + "disabled" when user rebound to null', () => {
|
|
43
|
+
const a = mkAction({ defaultShortcut: 'Mod+S' });
|
|
44
|
+
expect(effectiveShortcutWithSource(a, { 'shard.save': null }, 'other'))
|
|
45
|
+
.toEqual({ shortcut: null, source: 'disabled' });
|
|
46
|
+
});
|
|
47
|
+
it('returns null + "none" when no default and no override', () => {
|
|
48
|
+
const a = mkAction();
|
|
49
|
+
expect(effectiveShortcutWithSource(a, {}, 'other'))
|
|
50
|
+
.toEqual({ shortcut: null, source: 'none' });
|
|
51
|
+
});
|
|
52
|
+
});
|
|
@@ -3,44 +3,9 @@
|
|
|
3
3
|
* dispatcher state, returns a tiered, deduplicated, shortcut-annotated
|
|
4
4
|
* item list the Svelte component renders without further logic.
|
|
5
5
|
*/
|
|
6
|
-
import {
|
|
6
|
+
import { TIER_ORDER } from './dispatcher.svelte';
|
|
7
7
|
import { effectiveShortcut } from './bindings';
|
|
8
|
-
|
|
9
|
-
return Array.isArray(scope) ? scope : [scope];
|
|
10
|
-
}
|
|
11
|
-
function scopeToTier(scope) {
|
|
12
|
-
if (scope === 'home')
|
|
13
|
-
return 'home';
|
|
14
|
-
if (scope === 'app')
|
|
15
|
-
return 'app';
|
|
16
|
-
if (typeof scope === 'string' && scope.startsWith('view:'))
|
|
17
|
-
return 'view';
|
|
18
|
-
if (typeof scope === 'string' && scope.startsWith('focus:'))
|
|
19
|
-
return 'focus';
|
|
20
|
-
return 'element';
|
|
21
|
-
}
|
|
22
|
-
function innermostActiveTier(scope, state, owner) {
|
|
23
|
-
// Build a map of tier → active scopes for this action, then walk
|
|
24
|
-
// TIER_ORDER from innermost → outermost to find the first active tier.
|
|
25
|
-
const scopes = normalizeScope(scope);
|
|
26
|
-
const tierBuckets = {};
|
|
27
|
-
for (const s of scopes) {
|
|
28
|
-
const tier = scopeToTier(s);
|
|
29
|
-
if (!tierBuckets[tier])
|
|
30
|
-
tierBuckets[tier] = [];
|
|
31
|
-
tierBuckets[tier].push(s);
|
|
32
|
-
}
|
|
33
|
-
for (const tier of TIER_ORDER) {
|
|
34
|
-
const bucket = tierBuckets[tier];
|
|
35
|
-
if (!bucket)
|
|
36
|
-
continue;
|
|
37
|
-
for (const s of bucket) {
|
|
38
|
-
if (isScopeActive(s, state, owner))
|
|
39
|
-
return tier;
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
return null;
|
|
43
|
-
}
|
|
8
|
+
import { scopeToTier, innermostActiveScope } from './scope-helpers';
|
|
44
9
|
export function buildContextMenuModel(entries, state) {
|
|
45
10
|
var _a;
|
|
46
11
|
const byTier = {
|
|
@@ -52,11 +17,11 @@ export function buildContextMenuModel(entries, state) {
|
|
|
52
17
|
continue;
|
|
53
18
|
if (seen.has(entry.action.id))
|
|
54
19
|
continue;
|
|
55
|
-
const
|
|
56
|
-
if (!
|
|
20
|
+
const winning = innermostActiveScope(entry.action.scope, state, entry.ownerShardId);
|
|
21
|
+
if (!winning)
|
|
57
22
|
continue;
|
|
58
23
|
seen.add(entry.action.id);
|
|
59
|
-
byTier[
|
|
24
|
+
byTier[scopeToTier(winning)].push({
|
|
60
25
|
id: entry.action.id,
|
|
61
26
|
label: entry.action.label,
|
|
62
27
|
shortcut: effectiveShortcut(entry.action, state.bindings, state.platform),
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* state snapshots.
|
|
6
6
|
*/
|
|
7
7
|
import { effectiveShortcut } from './bindings';
|
|
8
|
+
import { scopeToTier, normalizeScope } from './scope-helpers';
|
|
8
9
|
export const TIER_ORDER = ['element', 'focus', 'view', 'app', 'home'];
|
|
9
10
|
export function isScopeActive(scope, state, ownerShardId) {
|
|
10
11
|
if (scope === 'home') {
|
|
@@ -31,20 +32,6 @@ export function isScopeActive(scope, state, ownerShardId) {
|
|
|
31
32
|
}
|
|
32
33
|
return false;
|
|
33
34
|
}
|
|
34
|
-
function scopeToTier(scope) {
|
|
35
|
-
if (scope === 'home')
|
|
36
|
-
return 'home';
|
|
37
|
-
if (scope === 'app')
|
|
38
|
-
return 'app';
|
|
39
|
-
if (typeof scope === 'string' && scope.startsWith('view:'))
|
|
40
|
-
return 'view';
|
|
41
|
-
if (typeof scope === 'string' && scope.startsWith('focus:'))
|
|
42
|
-
return 'focus';
|
|
43
|
-
return 'element';
|
|
44
|
-
}
|
|
45
|
-
function normalizeScope(scope) {
|
|
46
|
-
return Array.isArray(scope) ? scope : [scope];
|
|
47
|
-
}
|
|
48
35
|
export function buildTierIndex(entries, state) {
|
|
49
36
|
const idx = {
|
|
50
37
|
element: new Map(),
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { ActionEntry } from './registry';
|
|
2
|
+
import { type DispatcherState } from './dispatcher.svelte';
|
|
3
|
+
import type { ActiveActionDescriptor } from './types';
|
|
4
|
+
export declare function listActiveFromEntries(entries: ActionEntry[], state: DispatcherState): ActiveActionDescriptor[];
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Pure read-side producer for `shell.actions.listActive()`.
|
|
3
|
+
*
|
|
4
|
+
* Given a snapshot of the action registry and the dispatcher state,
|
|
5
|
+
* return one `ActiveActionDescriptor` per currently-active action, in
|
|
6
|
+
* tier-innermost-first order (element > focus > view > app > home),
|
|
7
|
+
* with registration order preserved within a tier.
|
|
8
|
+
*
|
|
9
|
+
* The shell's live wrapper calls this with `listActions()` + `getLiveDispatcherState()`.
|
|
10
|
+
*/
|
|
11
|
+
import { TIER_ORDER, } from './dispatcher.svelte';
|
|
12
|
+
import { effectiveShortcutWithSource } from './bindings';
|
|
13
|
+
import { innermostActiveScope, scopeBadge, scopeToTier } from './scope-helpers';
|
|
14
|
+
export function listActiveFromEntries(entries, state) {
|
|
15
|
+
const byTier = {
|
|
16
|
+
element: [], focus: [], view: [], app: [], home: [],
|
|
17
|
+
};
|
|
18
|
+
const seen = new Set();
|
|
19
|
+
for (const entry of entries) {
|
|
20
|
+
if (seen.has(entry.action.id))
|
|
21
|
+
continue;
|
|
22
|
+
const winning = innermostActiveScope(entry.action.scope, state, entry.ownerShardId);
|
|
23
|
+
if (!winning)
|
|
24
|
+
continue;
|
|
25
|
+
seen.add(entry.action.id);
|
|
26
|
+
const { shortcut, source } = effectiveShortcutWithSource(entry.action, state.bindings, state.platform);
|
|
27
|
+
byTier[scopeToTier(winning)].push({
|
|
28
|
+
id: entry.action.id,
|
|
29
|
+
label: entry.action.label,
|
|
30
|
+
effectiveShortcut: shortcut,
|
|
31
|
+
bindingSource: source,
|
|
32
|
+
scope: winning,
|
|
33
|
+
scopeBadge: scopeBadge(winning),
|
|
34
|
+
group: entry.action.group,
|
|
35
|
+
icon: entry.action.icon,
|
|
36
|
+
ownerShardId: entry.ownerShardId,
|
|
37
|
+
paletteItem: entry.action.paletteItem !== false,
|
|
38
|
+
contextItem: entry.action.contextItem !== false,
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
return TIER_ORDER.flatMap((tier) => byTier[tier]);
|
|
42
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { listActiveFromEntries } from './listActive';
|
|
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('listActiveFromEntries', () => {
|
|
9
|
+
it('omits actions whose scope is not active', () => {
|
|
10
|
+
const entries = [mkEntry({ id: 'p', scope: 'app' })];
|
|
11
|
+
expect(listActiveFromEntries(entries, mkState())).toEqual([]);
|
|
12
|
+
});
|
|
13
|
+
it('includes an active home action with default shortcut resolution', () => {
|
|
14
|
+
const entries = [mkEntry({
|
|
15
|
+
id: 'open', label: 'Open', scope: 'home', defaultShortcut: 'Mod+O',
|
|
16
|
+
})];
|
|
17
|
+
const out = listActiveFromEntries(entries, mkState({ platform: 'mac' }));
|
|
18
|
+
expect(out).toHaveLength(1);
|
|
19
|
+
expect(out[0]).toMatchObject({
|
|
20
|
+
id: 'open', label: 'Open',
|
|
21
|
+
effectiveShortcut: 'Meta+O', bindingSource: 'default',
|
|
22
|
+
scope: 'home', scopeBadge: null,
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
it('reports innermost tier for multi-scope action', () => {
|
|
26
|
+
const entries = [mkEntry({
|
|
27
|
+
id: 'm', scope: ['app', 'view:editor'],
|
|
28
|
+
}, '__sh3core__')];
|
|
29
|
+
const state = mkState({
|
|
30
|
+
activeAppId: 'a', autostartShards: new Set(['__sh3core__']),
|
|
31
|
+
mountedViewIds: new Set(['editor']),
|
|
32
|
+
});
|
|
33
|
+
const out = listActiveFromEntries(entries, state);
|
|
34
|
+
expect(out[0].scope).toBe('view:editor');
|
|
35
|
+
expect(out[0].scopeBadge).toBe('view:editor');
|
|
36
|
+
});
|
|
37
|
+
it('reports bindingSource=disabled when user null-rebound', () => {
|
|
38
|
+
const entries = [mkEntry({
|
|
39
|
+
id: 'x', scope: 'home', defaultShortcut: 'Mod+S',
|
|
40
|
+
})];
|
|
41
|
+
const state = mkState({ bindings: { x: null } });
|
|
42
|
+
const out = listActiveFromEntries(entries, state);
|
|
43
|
+
expect(out[0].effectiveShortcut).toBeNull();
|
|
44
|
+
expect(out[0].bindingSource).toBe('disabled');
|
|
45
|
+
});
|
|
46
|
+
it('reports bindingSource=none when no default and no override', () => {
|
|
47
|
+
const entries = [mkEntry({ id: 'x', scope: 'home' })];
|
|
48
|
+
const out = listActiveFromEntries(entries, mkState());
|
|
49
|
+
expect(out[0].effectiveShortcut).toBeNull();
|
|
50
|
+
expect(out[0].bindingSource).toBe('none');
|
|
51
|
+
});
|
|
52
|
+
it('carries paletteItem/contextItem defaults and overrides', () => {
|
|
53
|
+
const entries = [mkEntry({
|
|
54
|
+
id: 'a', scope: 'home', paletteItem: false,
|
|
55
|
+
})];
|
|
56
|
+
const out = listActiveFromEntries(entries, mkState());
|
|
57
|
+
expect(out[0].paletteItem).toBe(false);
|
|
58
|
+
expect(out[0].contextItem).toBe(true); // defaults to true
|
|
59
|
+
});
|
|
60
|
+
it('dedupes by action id', () => {
|
|
61
|
+
const entries = [
|
|
62
|
+
mkEntry({ id: 'dup', scope: 'home' }, 'shard.a'),
|
|
63
|
+
mkEntry({ id: 'dup', scope: 'home' }, 'shard.b'),
|
|
64
|
+
];
|
|
65
|
+
const out = listActiveFromEntries(entries, mkState());
|
|
66
|
+
expect(out).toHaveLength(1);
|
|
67
|
+
expect(out[0].ownerShardId).toBe('shard.a');
|
|
68
|
+
});
|
|
69
|
+
it('orders innermost tier first, stable by registration inside a tier', () => {
|
|
70
|
+
// `app` and `view:editor` both activate under an active app with the
|
|
71
|
+
// owning shard autostart-registered and the view mounted.
|
|
72
|
+
const entries = [
|
|
73
|
+
mkEntry({ id: 'a1', scope: 'app', label: 'A1' }, '__sh3core__'),
|
|
74
|
+
mkEntry({ id: 'v1', scope: 'view:editor', label: 'V1' }, '__sh3core__'),
|
|
75
|
+
mkEntry({ id: 'a2', scope: 'app', label: 'A2' }, '__sh3core__'),
|
|
76
|
+
];
|
|
77
|
+
const state = mkState({
|
|
78
|
+
activeAppId: 'a',
|
|
79
|
+
autostartShards: new Set(['__sh3core__']),
|
|
80
|
+
mountedViewIds: new Set(['editor']),
|
|
81
|
+
});
|
|
82
|
+
const out = listActiveFromEntries(entries, state);
|
|
83
|
+
// view tier first (innermost of the two), then app tier in reg order.
|
|
84
|
+
expect(out.map((d) => d.id)).toEqual(['v1', 'a1', 'a2']);
|
|
85
|
+
});
|
|
86
|
+
});
|
|
@@ -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
|
@@ -54,3 +54,44 @@ export interface ResolvedAction {
|
|
|
54
54
|
ownerShardId: string;
|
|
55
55
|
effectiveShortcut: string | null;
|
|
56
56
|
}
|
|
57
|
+
/**
|
|
58
|
+
* Where an action's effective shortcut came from:
|
|
59
|
+
* - `'default'` — `defaultShortcut` resolved for the current platform
|
|
60
|
+
* - `'user'` — the user supplied an explicit override
|
|
61
|
+
* - `'disabled'` — the user rebound to `null` to disable dispatch
|
|
62
|
+
* - `'none'` — no default and no override
|
|
63
|
+
*/
|
|
64
|
+
export type BindingSource = 'default' | 'user' | 'disabled' | 'none';
|
|
65
|
+
/**
|
|
66
|
+
* Read-only snapshot describing one action that is currently active.
|
|
67
|
+
* Produced by `shell.actions.listActive()`. One descriptor per action id,
|
|
68
|
+
* reporting the innermost active scope (same tier order as keyboard
|
|
69
|
+
* dispatch: element > focus > view > app > home).
|
|
70
|
+
*/
|
|
71
|
+
export interface ActiveActionDescriptor {
|
|
72
|
+
/** Stable action id as registered. */
|
|
73
|
+
id: string;
|
|
74
|
+
/** Human-readable label as registered. */
|
|
75
|
+
label: string;
|
|
76
|
+
/**
|
|
77
|
+
* Shortcut string as it would dispatch right now (platform-resolved,
|
|
78
|
+
* user-rebind applied). `null` when `bindingSource` is `'disabled'` or
|
|
79
|
+
* `'none'`.
|
|
80
|
+
*/
|
|
81
|
+
effectiveShortcut: string | null;
|
|
82
|
+
/**
|
|
83
|
+
* Where the effective shortcut came from. Help UIs can distinguish
|
|
84
|
+
* user-disabled (render greyed) from no-shortcut (hide).
|
|
85
|
+
*/
|
|
86
|
+
bindingSource: BindingSource;
|
|
87
|
+
/** The innermost active tier of the action's scope. */
|
|
88
|
+
scope: AtomicScope;
|
|
89
|
+
/** Display hint: `null` for home/app, else full string or element type. */
|
|
90
|
+
scopeBadge: string | null;
|
|
91
|
+
/** Carried through from the registered action. */
|
|
92
|
+
group?: string;
|
|
93
|
+
icon?: string;
|
|
94
|
+
ownerShardId: string;
|
|
95
|
+
paletteItem: boolean;
|
|
96
|
+
contextItem: boolean;
|
|
97
|
+
}
|
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';
|
package/dist/api.js
CHANGED
|
@@ -32,6 +32,7 @@ export { inspectActiveLayout, spliceIntoActiveLayout, dockIntoActiveLayout, focu
|
|
|
32
32
|
export { PERMISSION_DOCUMENTS_BROWSE, PERMISSION_DOCUMENTS_READ, PERMISSION_DOCUMENTS_WRITE, } from './documents/types';
|
|
33
33
|
export { PERMISSION_SYNC_PEER, PERMISSION_SYNC_POLICY } from './documents/sync-types';
|
|
34
34
|
export { CONFLICT_RENDERER_POINT, ConflictPermissionError, ConflictSessionOrphanedError, } from './conflicts/api';
|
|
35
|
+
export { COLOR_PICKER_POINT } from './color/api';
|
|
35
36
|
// Shard introspection — read-only reactive maps exposing which shards are
|
|
36
37
|
// known to the host and which are currently active. Intended for diagnostic
|
|
37
38
|
// and tooling shards that need to visualize framework state. Phase 9
|
|
@@ -65,3 +66,8 @@ export const FRAMEWORK_SHARD_IDS = [
|
|
|
65
66
|
];
|
|
66
67
|
// Theme token override API (shell-level theming support).
|
|
67
68
|
export { setTokenOverrides, clearTokenOverrides, getTokenOverrides, } from './theme';
|
|
69
|
+
// UI primitives for shards and apps. Must live on the public shim surface
|
|
70
|
+
// (api.ts) — not just index.ts — so dynamically loaded bundles can resolve
|
|
71
|
+
// `import { Button } from 'sh3-core'` against the runtime shim in loader.ts.
|
|
72
|
+
export { default as Button } from './primitives/Button.svelte';
|
|
73
|
+
export { provideIcons, getIconSprite } from './primitives/icon-context';
|
|
Binary file
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" data-svg-designer-scene-id="13a98ace">
|
|
2
|
+
<rect id="07433d30" x="2" y="2" width="20" height="20" rx="2" fill="#a15c67" stroke="#ff7373" stroke-width="0.5" opacity="1"/>
|
|
3
|
+
<text id="deb1fa9e" x="2.0101" y="16.2415" font-family="monospace" font-size="12" font-weight="bold" fill="#2b0f2b" opacity="0.8">SH3</text>
|
|
4
|
+
<text id="055bb24a" x="2.0709" y="15.3866" font-family="monospace" font-size="12" font-weight="bold" fill="#fae3b6" opacity="1">SH3</text>
|
|
5
|
+
</svg>
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/** Contribution-point id for color pickers. */
|
|
2
|
+
export declare const COLOR_PICKER_POINT = "sh3.color-picker";
|
|
3
|
+
export interface ColorPickOptions {
|
|
4
|
+
/** Initial color as '#rrggbb'. If omitted, contributors choose a default. */
|
|
5
|
+
initial?: string;
|
|
6
|
+
/** Request alpha support. Ignored by contributors that don't support it. */
|
|
7
|
+
alpha?: boolean;
|
|
8
|
+
/** Anchor element for floating/positioned pickers. Native fallback ignores. */
|
|
9
|
+
anchor?: HTMLElement;
|
|
10
|
+
/** Display title (e.g. 'Background color'). Native fallback ignores. */
|
|
11
|
+
title?: string;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* A color-picker contribution. Higher `priority` wins; the inlined native
|
|
15
|
+
* fallback is priority 0, so contributors typically use >= 10.
|
|
16
|
+
*
|
|
17
|
+
* `open(opts)` resolves with '#rrggbb' (or '#rrggbbaa' when the contributor
|
|
18
|
+
* honors `opts.alpha`) or `null` if the user dismissed.
|
|
19
|
+
*/
|
|
20
|
+
export interface ColorContribution {
|
|
21
|
+
id: string;
|
|
22
|
+
priority?: number;
|
|
23
|
+
open(opts: ColorPickOptions): Promise<string | null>;
|
|
24
|
+
}
|
|
25
|
+
/** Shell-level color API, mounted at `shell.color`. */
|
|
26
|
+
export interface ColorApi {
|
|
27
|
+
/**
|
|
28
|
+
* Open a color picker. Resolves with the chosen color as '#rrggbb'
|
|
29
|
+
* (or '#rrggbbaa' when a contributor honors `opts.alpha === true`),
|
|
30
|
+
* or `null` if the user dismissed.
|
|
31
|
+
*
|
|
32
|
+
* On browsers without `<input type="color">` cancel-event support, the
|
|
33
|
+
* returned promise may not resolve if the user closes the native
|
|
34
|
+
* fallback without committing. Install a color-picker shard (e.g.
|
|
35
|
+
* sh3-editor) to avoid this.
|
|
36
|
+
*/
|
|
37
|
+
pick(opts?: ColorPickOptions): Promise<string | null>;
|
|
38
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Public types for the color-picker contribution point.
|
|
3
|
+
*
|
|
4
|
+
* A contribution registered at COLOR_PICKER_POINT is picked by the
|
|
5
|
+
* primitive in ./primitive.ts when `shell.color.pick()` is called.
|
|
6
|
+
* The native <input type="color"> fallback is inlined there at priority 0;
|
|
7
|
+
* contributed pickers typically register at priority >= 10.
|
|
8
|
+
*/
|
|
9
|
+
/** Contribution-point id for color pickers. */
|
|
10
|
+
export const COLOR_PICKER_POINT = 'sh3.color-picker';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { register, __resetContributionsForTest } from '../contributions/registry';
|
|
3
|
+
import { COLOR_PICKER_POINT } from './api';
|
|
4
|
+
import { pickColor } from './primitive';
|
|
5
|
+
describe('native fallback', () => {
|
|
6
|
+
beforeEach(() => __resetContributionsForTest());
|
|
7
|
+
afterEach(() => {
|
|
8
|
+
document.querySelectorAll('input[type="color"]').forEach((el) => el.remove());
|
|
9
|
+
});
|
|
10
|
+
it('creates an input[type=color], clicks it, and resolves on change', async () => {
|
|
11
|
+
const p = pickColor({ initial: '#ff8800' });
|
|
12
|
+
const el = document.querySelector('input[type="color"]');
|
|
13
|
+
expect(el).not.toBeNull();
|
|
14
|
+
expect(el.value).toBe('#ff8800');
|
|
15
|
+
el.value = '#112233';
|
|
16
|
+
el.dispatchEvent(new Event('change'));
|
|
17
|
+
await expect(p).resolves.toBe('#112233');
|
|
18
|
+
expect(document.querySelector('input[type="color"]')).toBeNull();
|
|
19
|
+
});
|
|
20
|
+
it('resolves null on cancel', async () => {
|
|
21
|
+
const p = pickColor();
|
|
22
|
+
const el = document.querySelector('input[type="color"]');
|
|
23
|
+
expect(el).not.toBeNull();
|
|
24
|
+
el.dispatchEvent(new Event('cancel'));
|
|
25
|
+
await expect(p).resolves.toBeNull();
|
|
26
|
+
expect(document.querySelector('input[type="color"]')).toBeNull();
|
|
27
|
+
});
|
|
28
|
+
it('ignores malformed initial values without crashing', () => {
|
|
29
|
+
pickColor({ initial: 'not-a-color' });
|
|
30
|
+
const el = document.querySelector('input[type="color"]');
|
|
31
|
+
expect(el).not.toBeNull();
|
|
32
|
+
expect(el.value).not.toBe('not-a-color');
|
|
33
|
+
});
|
|
34
|
+
it('priority-≥1 contributor bypasses native entirely', async () => {
|
|
35
|
+
register(COLOR_PICKER_POINT, {
|
|
36
|
+
id: 'c',
|
|
37
|
+
priority: 10,
|
|
38
|
+
open: () => Promise.resolve('#abcdef'),
|
|
39
|
+
});
|
|
40
|
+
await expect(pickColor()).resolves.toBe('#abcdef');
|
|
41
|
+
expect(document.querySelector('input[type="color"]')).toBeNull();
|
|
42
|
+
});
|
|
43
|
+
});
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Color-picker selection primitive.
|
|
3
|
+
*
|
|
4
|
+
* Reads contributions registered at COLOR_PICKER_POINT, sorts by
|
|
5
|
+
* priority descending, and calls `open(opts)` on the winner. The
|
|
6
|
+
* inlined native fallback at priority 0 guarantees there is always
|
|
7
|
+
* at least one candidate.
|
|
8
|
+
*/
|
|
9
|
+
import { list } from '../contributions/registry';
|
|
10
|
+
import { COLOR_PICKER_POINT } from './api';
|
|
11
|
+
const nativeFallback = {
|
|
12
|
+
id: 'sh3.color-picker.native',
|
|
13
|
+
priority: 0,
|
|
14
|
+
open: ({ initial }) => new Promise((resolve) => {
|
|
15
|
+
const el = document.createElement('input');
|
|
16
|
+
el.type = 'color';
|
|
17
|
+
if (initial && /^#[0-9a-f]{6}$/i.test(initial))
|
|
18
|
+
el.value = initial;
|
|
19
|
+
let settled = false;
|
|
20
|
+
const settle = (v) => {
|
|
21
|
+
if (settled)
|
|
22
|
+
return;
|
|
23
|
+
settled = true;
|
|
24
|
+
el.remove();
|
|
25
|
+
resolve(v);
|
|
26
|
+
};
|
|
27
|
+
el.addEventListener('change', () => settle(el.value));
|
|
28
|
+
el.addEventListener('cancel', () => settle(null));
|
|
29
|
+
el.style.position = 'fixed';
|
|
30
|
+
el.style.left = '-9999px';
|
|
31
|
+
document.body.appendChild(el);
|
|
32
|
+
el.click();
|
|
33
|
+
}),
|
|
34
|
+
};
|
|
35
|
+
export function pickColor(opts = {}) {
|
|
36
|
+
const contributed = list(COLOR_PICKER_POINT);
|
|
37
|
+
const winner = [...contributed, nativeFallback]
|
|
38
|
+
.sort((a, b) => { var _a, _b; return ((_a = b.priority) !== null && _a !== void 0 ? _a : 0) - ((_b = a.priority) !== null && _b !== void 0 ? _b : 0); })[0];
|
|
39
|
+
return winner.open(opts);
|
|
40
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
2
|
+
import { register, __resetContributionsForTest } from '../contributions/registry';
|
|
3
|
+
import { COLOR_PICKER_POINT } from './api';
|
|
4
|
+
import { pickColor } from './primitive';
|
|
5
|
+
describe('pickColor / selection', () => {
|
|
6
|
+
beforeEach(() => __resetContributionsForTest());
|
|
7
|
+
it('picks the highest-priority contributor', async () => {
|
|
8
|
+
const low = {
|
|
9
|
+
id: 'low',
|
|
10
|
+
priority: 5,
|
|
11
|
+
open: vi.fn(() => Promise.resolve('#111111')),
|
|
12
|
+
};
|
|
13
|
+
const high = {
|
|
14
|
+
id: 'high',
|
|
15
|
+
priority: 50,
|
|
16
|
+
open: vi.fn(() => Promise.resolve('#222222')),
|
|
17
|
+
};
|
|
18
|
+
register(COLOR_PICKER_POINT, low);
|
|
19
|
+
register(COLOR_PICKER_POINT, high);
|
|
20
|
+
await expect(pickColor({ initial: '#ff0000' })).resolves.toBe('#222222');
|
|
21
|
+
expect(high.open).toHaveBeenCalledWith({ initial: '#ff0000' });
|
|
22
|
+
expect(low.open).not.toHaveBeenCalled();
|
|
23
|
+
});
|
|
24
|
+
it('passes opts through unchanged', async () => {
|
|
25
|
+
const open = vi.fn(() => Promise.resolve(null));
|
|
26
|
+
register(COLOR_PICKER_POINT, { id: 'c', priority: 10, open });
|
|
27
|
+
const opts = { initial: '#abcdef', alpha: true, title: 'Pick' };
|
|
28
|
+
await pickColor(opts);
|
|
29
|
+
expect(open).toHaveBeenCalledWith(opts);
|
|
30
|
+
});
|
|
31
|
+
it('re-reads contributions on each call (no caching)', async () => {
|
|
32
|
+
const first = vi.fn(() => Promise.resolve('#111111'));
|
|
33
|
+
const unreg = register(COLOR_PICKER_POINT, { id: 'a', priority: 10, open: first });
|
|
34
|
+
await pickColor();
|
|
35
|
+
unreg();
|
|
36
|
+
const second = vi.fn(() => Promise.resolve('#222222'));
|
|
37
|
+
register(COLOR_PICKER_POINT, { id: 'b', priority: 10, open: second });
|
|
38
|
+
await pickColor();
|
|
39
|
+
expect(first).toHaveBeenCalledTimes(1);
|
|
40
|
+
expect(second).toHaveBeenCalledTimes(1);
|
|
41
|
+
});
|
|
42
|
+
});
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* shell.color assembled API.
|
|
3
|
+
*
|
|
4
|
+
* The selector + inlined native fallback live in primitive.ts;
|
|
5
|
+
* this file just binds `pick` onto the object exposed on the shell
|
|
6
|
+
* singleton, mirroring conflicts/shell-api.ts.
|
|
7
|
+
*/
|
|
8
|
+
import { pickColor } from './primitive';
|
|
9
|
+
export const colorApi = {
|
|
10
|
+
pick: pickColor,
|
|
11
|
+
};
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,4 @@
|
|
|
1
1
|
export * from './api';
|
|
2
2
|
export { default as Shell } from './Shell.svelte';
|
|
3
|
-
export { default as Button } from './primitives/Button.svelte';
|
|
4
|
-
export { provideIcons, getIconSprite, type ButtonVariant } from './primitives/icon-context';
|
|
5
3
|
export type { ArtifactManifest } from './artifact';
|
|
6
4
|
export * from './shell-shard/protocol';
|
package/dist/index.js
CHANGED
|
@@ -11,6 +11,4 @@
|
|
|
11
11
|
*/
|
|
12
12
|
export * from './api';
|
|
13
13
|
export { default as Shell } from './Shell.svelte';
|
|
14
|
-
export { default as Button } from './primitives/Button.svelte';
|
|
15
|
-
export { provideIcons, getIconSprite } from './primitives/icon-context';
|
|
16
14
|
export * from './shell-shard/protocol';
|
|
@@ -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/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.4";
|
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.4';
|