sh3-core 0.13.1 → 0.13.3
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 +62 -13
- package/dist/__test__/setup-dom.js +5 -0
- package/dist/actions/MenuButton.svelte +2 -1
- package/dist/actions/contextMenuModel.d.ts +1 -1
- package/dist/actions/contextMenuModel.js +2 -1
- package/dist/actions/dispatcher.svelte.d.ts +1 -1
- package/dist/actions/dispatcher.svelte.js +2 -1
- package/dist/actions/listActive.d.ts +1 -1
- package/dist/actions/listActive.js +2 -1
- package/dist/actions/listeners.d.ts +1 -1
- package/dist/actions/listeners.js +6 -5
- package/dist/actions/menuBarModel.js +3 -2
- package/dist/actions/paletteModel.js +2 -1
- package/dist/actions/resolveLabel.test.js +14 -0
- package/dist/actions/types.d.ts +12 -1
- package/dist/actions/types.js +7 -1
- package/dist/api.d.ts +3 -0
- package/dist/api.js +3 -0
- package/dist/app/store/AppUpdateAvailableModal.svelte +87 -0
- package/dist/app/store/AppUpdateAvailableModal.svelte.d.ts +11 -0
- package/dist/app/store/InstalledView.svelte +8 -54
- package/dist/app/store/UninstallAppDialog.svelte +86 -0
- package/dist/app/store/UninstallAppDialog.svelte.d.ts +10 -0
- package/dist/app/store/permissionConfirm.d.ts +4 -0
- package/dist/app/store/permissionConfirm.js +28 -0
- package/dist/app/store/storeShard.svelte.d.ts +8 -1
- package/dist/app/store/storeShard.svelte.js +42 -9
- package/dist/app/store/updatePackage.test.d.ts +1 -0
- package/dist/app/store/updatePackage.test.js +34 -0
- package/dist/app/store/verbs.d.ts +1 -0
- package/dist/app/store/verbs.js +79 -5
- package/dist/app/store/verbs.test.d.ts +1 -0
- package/dist/app/store/verbs.test.js +56 -0
- package/dist/app-appearance/AppAppearanceModal.svelte +174 -0
- package/dist/app-appearance/AppAppearanceModal.svelte.d.ts +8 -0
- package/dist/app-appearance/appearanceShard.svelte.d.ts +2 -0
- package/dist/app-appearance/appearanceShard.svelte.js +61 -0
- package/dist/app-appearance/appearanceState.svelte.d.ts +15 -0
- package/dist/app-appearance/appearanceState.svelte.js +59 -0
- package/dist/app-appearance/appearanceState.test.d.ts +1 -0
- package/dist/app-appearance/appearanceState.test.js +30 -0
- package/dist/app-appearance/index.d.ts +3 -0
- package/dist/app-appearance/index.js +2 -0
- package/dist/app-appearance/types.d.ts +11 -0
- package/dist/app-appearance/types.js +1 -0
- package/dist/apps/lifecycle.js +10 -2
- package/dist/apps/types.d.ts +18 -4
- package/dist/apps/workspace-rekey.d.ts +1 -0
- package/dist/apps/workspace-rekey.js +35 -0
- package/dist/apps/workspace-rekey.test.d.ts +1 -0
- package/dist/apps/workspace-rekey.test.js +23 -0
- package/dist/assets/iconIds.generated.d.ts +2 -0
- package/dist/assets/iconIds.generated.js +154 -0
- package/dist/auth/admin-users.svelte.d.ts +9 -0
- package/dist/auth/admin-users.svelte.js +42 -0
- package/dist/auth/admin-users.test.d.ts +1 -0
- package/dist/auth/admin-users.test.js +52 -0
- package/dist/createShell.js +5 -5
- package/dist/documents/config.d.ts +5 -1
- package/dist/documents/config.js +16 -8
- package/dist/documents/index.d.ts +1 -1
- package/dist/documents/index.js +1 -1
- package/dist/host-entry.d.ts +1 -1
- package/dist/host-entry.js +1 -1
- package/dist/host.d.ts +1 -1
- package/dist/host.js +9 -2
- package/dist/primitives/Button.svelte +50 -4
- package/dist/primitives/Button.svelte.d.ts +3 -1
- package/dist/primitives/Collapsible.svelte +110 -0
- package/dist/primitives/Collapsible.svelte.d.ts +14 -0
- package/dist/primitives/widgets/AppPicker.svelte +41 -0
- package/dist/primitives/widgets/AppPicker.svelte.d.ts +9 -0
- package/dist/primitives/widgets/AppPicker.svelte.test.d.ts +1 -0
- package/dist/primitives/widgets/AppPicker.svelte.test.js +26 -0
- package/dist/primitives/widgets/AppPicker.test.d.ts +1 -0
- package/dist/primitives/widgets/AppPicker.test.js +74 -0
- package/dist/primitives/widgets/ColorSwatch.svelte +7 -2
- package/dist/primitives/widgets/ColorSwatch.svelte.d.ts +2 -1
- package/dist/primitives/widgets/ColorSwatch.svelte.test.d.ts +1 -0
- package/dist/primitives/widgets/ColorSwatch.svelte.test.js +31 -0
- package/dist/primitives/widgets/Field.svelte +4 -2
- package/dist/primitives/widgets/Field.svelte.d.ts +2 -2
- package/dist/primitives/widgets/Field.svelte.test.d.ts +1 -0
- package/dist/primitives/widgets/Field.svelte.test.js +33 -0
- package/dist/primitives/widgets/FilePicker.svelte +2 -2
- package/dist/primitives/widgets/FilePicker.svelte.d.ts +2 -2
- package/dist/primitives/widgets/FilePicker.svelte.test.d.ts +1 -0
- package/dist/primitives/widgets/FilePicker.svelte.test.js +31 -0
- package/dist/primitives/widgets/IconPicker.svelte +115 -0
- package/dist/primitives/widgets/IconPicker.svelte.d.ts +9 -0
- package/dist/primitives/widgets/IconPicker.svelte.test.d.ts +1 -0
- package/dist/primitives/widgets/IconPicker.svelte.test.js +43 -0
- package/dist/primitives/widgets/IconToggleGroup.svelte +4 -4
- package/dist/primitives/widgets/IconToggleGroup.svelte.d.ts +3 -3
- package/dist/primitives/widgets/IconToggleGroup.svelte.test.d.ts +1 -0
- package/dist/primitives/widgets/IconToggleGroup.svelte.test.js +40 -0
- package/dist/primitives/widgets/NumberInput.svelte +19 -9
- package/dist/primitives/widgets/NumberInput.svelte.d.ts +2 -2
- package/dist/primitives/widgets/NumberInput.svelte.test.d.ts +1 -0
- package/dist/primitives/widgets/NumberInput.svelte.test.js +48 -0
- package/dist/primitives/widgets/PickerList.d.ts +24 -0
- package/dist/primitives/widgets/PickerList.js +21 -0
- package/dist/primitives/widgets/PickerList.svelte +150 -0
- package/dist/primitives/widgets/PickerList.svelte.d.ts +16 -0
- package/dist/primitives/widgets/PickerList.svelte.test.d.ts +1 -0
- package/dist/primitives/widgets/PickerList.svelte.test.js +31 -0
- package/dist/primitives/widgets/PickerList.test.d.ts +1 -0
- package/dist/primitives/widgets/PickerList.test.js +218 -0
- package/dist/primitives/widgets/RangeSlider.svelte +11 -4
- package/dist/primitives/widgets/RangeSlider.svelte.d.ts +2 -2
- package/dist/primitives/widgets/RangeSlider.svelte.test.d.ts +1 -0
- package/dist/primitives/widgets/RangeSlider.svelte.test.js +38 -0
- package/dist/primitives/widgets/Segmented.svelte +4 -4
- package/dist/primitives/widgets/Segmented.svelte.d.ts +3 -3
- package/dist/primitives/widgets/Segmented.svelte.test.d.ts +1 -0
- package/dist/primitives/widgets/Segmented.svelte.test.js +25 -0
- package/dist/primitives/widgets/Select.svelte +4 -4
- package/dist/primitives/widgets/Select.svelte.d.ts +3 -3
- package/dist/primitives/widgets/Select.svelte.test.d.ts +1 -0
- package/dist/primitives/widgets/Select.svelte.test.js +37 -0
- package/dist/primitives/widgets/Slider.svelte +4 -2
- package/dist/primitives/widgets/Slider.svelte.d.ts +2 -2
- package/dist/primitives/widgets/Slider.svelte.test.d.ts +1 -0
- package/dist/primitives/widgets/Slider.svelte.test.js +22 -0
- package/dist/primitives/widgets/SliderGroup.svelte +4 -2
- package/dist/primitives/widgets/SliderGroup.svelte.d.ts +2 -2
- package/dist/primitives/widgets/SliderGroup.svelte.test.d.ts +1 -0
- package/dist/primitives/widgets/SliderGroup.svelte.test.js +34 -0
- package/dist/primitives/widgets/Textarea.svelte +5 -2
- package/dist/primitives/widgets/Textarea.svelte.d.ts +2 -2
- package/dist/primitives/widgets/Textarea.svelte.test.d.ts +1 -0
- package/dist/primitives/widgets/Textarea.svelte.test.js +29 -0
- package/dist/primitives/widgets/UserPicker.svelte +53 -0
- package/dist/primitives/widgets/UserPicker.svelte.d.ts +9 -0
- package/dist/primitives/widgets/UserPicker.svelte.test.d.ts +1 -0
- package/dist/primitives/widgets/UserPicker.svelte.test.js +30 -0
- package/dist/primitives/widgets/UserPicker.test.d.ts +1 -0
- package/dist/primitives/widgets/UserPicker.test.js +115 -0
- package/dist/primitives/widgets/_contract.d.ts +27 -0
- package/dist/primitives/widgets/_contract.js +10 -0
- package/dist/projects/session-state.svelte.d.ts +17 -0
- package/dist/projects/session-state.svelte.js +39 -0
- package/dist/projects/session-state.test.d.ts +1 -0
- package/dist/projects/session-state.test.js +55 -0
- package/dist/projects-shard/DeleteProjectDialog.svelte +150 -0
- package/dist/projects-shard/DeleteProjectDialog.svelte.d.ts +12 -0
- package/dist/projects-shard/DeleteProjectDialog.test.d.ts +1 -0
- package/dist/projects-shard/DeleteProjectDialog.test.js +120 -0
- package/dist/projects-shard/ProjectManage.svelte +219 -0
- package/dist/projects-shard/ProjectManage.svelte.d.ts +8 -0
- package/dist/projects-shard/ProjectsSection.svelte +120 -0
- package/dist/projects-shard/ProjectsSection.svelte.d.ts +3 -0
- package/dist/projects-shard/index.d.ts +4 -0
- package/dist/projects-shard/index.js +4 -0
- package/dist/projects-shard/projectsApi.d.ts +20 -0
- package/dist/projects-shard/projectsApi.js +44 -0
- package/dist/projects-shard/projectsApi.test.d.ts +1 -0
- package/dist/projects-shard/projectsApi.test.js +71 -0
- package/dist/projects-shard/projectsShard.svelte.d.ts +10 -0
- package/dist/projects-shard/projectsShard.svelte.js +148 -0
- package/dist/sh3core-shard/ShellHome.svelte +83 -39
- package/dist/sh3core-shard/appActions.d.ts +13 -0
- package/dist/sh3core-shard/appActions.js +181 -0
- package/dist/sh3core-shard/appActions.test.d.ts +1 -0
- package/dist/sh3core-shard/appActions.test.js +25 -0
- package/dist/sh3core-shard/sh3coreShard.svelte.js +2 -0
- package/dist/shards/activate-scopeid.test.d.ts +1 -0
- package/dist/shards/{activate-tenantid.test.js → activate-scopeid.test.js} +6 -6
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +2 -2
- /package/dist/{shards/activate-tenantid.test.d.ts → actions/resolveLabel.test.d.ts} +0 -0
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* `__app-appearance__` shard — owns the user-zone for per-app overrides
|
|
3
|
+
* and registers the `app.customize` element-scope action. The state
|
|
4
|
+
* itself lives in appearanceState.svelte.ts (so unit tests don't have
|
|
5
|
+
* to boot a real ShardContext). This file binds the zone on activate
|
|
6
|
+
* and unbinds on deactivate, and contributes the action.
|
|
7
|
+
*/
|
|
8
|
+
import { VERSION } from '../version';
|
|
9
|
+
import { listRegisteredApps } from '../api';
|
|
10
|
+
import { getSelection } from '../actions/selection.svelte';
|
|
11
|
+
import { modalManager } from '../overlays/modal';
|
|
12
|
+
import AppAppearanceModal from './AppAppearanceModal.svelte';
|
|
13
|
+
import { __bindZone, __unbindZone, } from './appearanceState.svelte';
|
|
14
|
+
function readSelection() {
|
|
15
|
+
const sel = getSelection();
|
|
16
|
+
if (!sel || sel.type !== 'app')
|
|
17
|
+
return null;
|
|
18
|
+
return sel.ref;
|
|
19
|
+
}
|
|
20
|
+
function runCustomize(_ctx) {
|
|
21
|
+
var _a;
|
|
22
|
+
const ref = readSelection();
|
|
23
|
+
if (!ref)
|
|
24
|
+
return;
|
|
25
|
+
const m = listRegisteredApps().find((x) => x.id === ref.appId);
|
|
26
|
+
const props = {
|
|
27
|
+
appId: ref.appId,
|
|
28
|
+
appLabel: (_a = m === null || m === void 0 ? void 0 : m.label) !== null && _a !== void 0 ? _a : ref.appId,
|
|
29
|
+
};
|
|
30
|
+
modalManager.open(AppAppearanceModal, props);
|
|
31
|
+
}
|
|
32
|
+
export const appearanceShard = {
|
|
33
|
+
manifest: {
|
|
34
|
+
id: '__app-appearance__',
|
|
35
|
+
label: 'App Appearance',
|
|
36
|
+
version: VERSION,
|
|
37
|
+
views: [],
|
|
38
|
+
},
|
|
39
|
+
activate(ctx) {
|
|
40
|
+
const zone = ctx.state({
|
|
41
|
+
user: { overrides: {} },
|
|
42
|
+
});
|
|
43
|
+
__bindZone(zone);
|
|
44
|
+
const customize = {
|
|
45
|
+
id: 'app.customize',
|
|
46
|
+
label: 'Customize…',
|
|
47
|
+
scope: { element: 'app' },
|
|
48
|
+
contextItem: true,
|
|
49
|
+
group: 'appearance',
|
|
50
|
+
run: runCustomize,
|
|
51
|
+
};
|
|
52
|
+
ctx.actions.register(customize);
|
|
53
|
+
},
|
|
54
|
+
autostart() {
|
|
55
|
+
// Self-start so the `app.customize` action is registered before the
|
|
56
|
+
// user right-clicks a home card. No imperative work required.
|
|
57
|
+
},
|
|
58
|
+
deactivate() {
|
|
59
|
+
__unbindZone();
|
|
60
|
+
},
|
|
61
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { StateZones } from '../state/zones.svelte';
|
|
2
|
+
import type { AppAppearance } from './types';
|
|
3
|
+
export interface AppearanceZoneSchema {
|
|
4
|
+
user: {
|
|
5
|
+
overrides: Record<string, AppAppearance>;
|
|
6
|
+
};
|
|
7
|
+
}
|
|
8
|
+
/** Bind the shard's zone to this module. Called by the shard's activate. */
|
|
9
|
+
export declare function __bindZone(s: StateZones<AppearanceZoneSchema>): void;
|
|
10
|
+
/** Unbind the zone (deactivate). */
|
|
11
|
+
export declare function __unbindZone(): void;
|
|
12
|
+
export declare function getAppearance(appId: string): AppAppearance | undefined;
|
|
13
|
+
export declare function setAppearance(appId: string, value: AppAppearance | undefined): void;
|
|
14
|
+
/** Test-only: replace the bound zone with a memory shim. */
|
|
15
|
+
export declare function __resetForTests(): void;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Per-user-per-browser visual overrides for apps. The store + helpers
|
|
3
|
+
* live separately from the shard so the state can be unit-tested without
|
|
4
|
+
* booting the shard system, and so the AppAppearanceModal can import
|
|
5
|
+
* get/set without creating an import cycle through the shard's modal
|
|
6
|
+
* import.
|
|
7
|
+
*
|
|
8
|
+
* EXPLICITLY TEMPORARY. A future ADR is expected to add icon/color
|
|
9
|
+
* fields to the app manifest itself.
|
|
10
|
+
*/
|
|
11
|
+
let zoneState = null;
|
|
12
|
+
/** Bind the shard's zone to this module. Called by the shard's activate. */
|
|
13
|
+
export function __bindZone(s) {
|
|
14
|
+
zoneState = s;
|
|
15
|
+
}
|
|
16
|
+
/** Unbind the zone (deactivate). */
|
|
17
|
+
export function __unbindZone() {
|
|
18
|
+
zoneState = null;
|
|
19
|
+
}
|
|
20
|
+
function isEmpty(v) {
|
|
21
|
+
return v.icon === undefined && v.color === undefined && v.label === undefined;
|
|
22
|
+
}
|
|
23
|
+
export function getAppearance(appId) {
|
|
24
|
+
const map = zoneState === null || zoneState === void 0 ? void 0 : zoneState.user.overrides;
|
|
25
|
+
if (!map)
|
|
26
|
+
return undefined;
|
|
27
|
+
const v = map[appId];
|
|
28
|
+
if (!v)
|
|
29
|
+
return undefined;
|
|
30
|
+
if (isEmpty(v))
|
|
31
|
+
return undefined;
|
|
32
|
+
return v;
|
|
33
|
+
}
|
|
34
|
+
export function setAppearance(appId, value) {
|
|
35
|
+
if (!zoneState)
|
|
36
|
+
return;
|
|
37
|
+
const map = zoneState.user.overrides;
|
|
38
|
+
const next = Object.assign({}, map);
|
|
39
|
+
if (value === undefined || isEmpty(value)) {
|
|
40
|
+
delete next[appId];
|
|
41
|
+
}
|
|
42
|
+
else {
|
|
43
|
+
next[appId] = value;
|
|
44
|
+
}
|
|
45
|
+
zoneState.user.overrides = next;
|
|
46
|
+
}
|
|
47
|
+
/** Test-only: replace the bound zone with a memory shim. */
|
|
48
|
+
export function __resetForTests() {
|
|
49
|
+
zoneState = {
|
|
50
|
+
ephemeral: {},
|
|
51
|
+
session: {},
|
|
52
|
+
workspace: {},
|
|
53
|
+
user: { overrides: {} },
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
// Initialise the memory shim at module load so tests can call set/get
|
|
57
|
+
// without first invoking activate. Production replaces this via
|
|
58
|
+
// __bindZone() inside appearanceShard.activate().
|
|
59
|
+
__resetForTests();
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from 'vitest';
|
|
2
|
+
import { setAppearance, getAppearance, __resetForTests } from './appearanceState.svelte';
|
|
3
|
+
describe('appearanceShard get/set', () => {
|
|
4
|
+
beforeEach(() => __resetForTests());
|
|
5
|
+
it('returns undefined for an unset app', () => {
|
|
6
|
+
expect(getAppearance('foo')).toBeUndefined();
|
|
7
|
+
});
|
|
8
|
+
it('round-trips an icon override', () => {
|
|
9
|
+
setAppearance('foo', { icon: 'house' });
|
|
10
|
+
expect(getAppearance('foo')).toEqual({ icon: 'house' });
|
|
11
|
+
});
|
|
12
|
+
it('round-trips a color override', () => {
|
|
13
|
+
setAppearance('foo', { color: '#ff0000' });
|
|
14
|
+
expect(getAppearance('foo')).toEqual({ color: '#ff0000' });
|
|
15
|
+
});
|
|
16
|
+
it('round-trips a label override', () => {
|
|
17
|
+
setAppearance('foo', { label: 'My Label' });
|
|
18
|
+
expect(getAppearance('foo')).toEqual({ label: 'My Label' });
|
|
19
|
+
});
|
|
20
|
+
it('clears the entry when all fields are undefined', () => {
|
|
21
|
+
setAppearance('foo', { icon: 'house', color: '#ff0000', label: 'X' });
|
|
22
|
+
setAppearance('foo', { icon: undefined, color: undefined, label: undefined });
|
|
23
|
+
expect(getAppearance('foo')).toBeUndefined();
|
|
24
|
+
});
|
|
25
|
+
it('clears the entry when given undefined directly', () => {
|
|
26
|
+
setAppearance('foo', { icon: 'house' });
|
|
27
|
+
setAppearance('foo', undefined);
|
|
28
|
+
expect(getAppearance('foo')).toBeUndefined();
|
|
29
|
+
});
|
|
30
|
+
});
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-app per-user-per-browser visual override. Stored in the user zone
|
|
3
|
+
* of the __app-appearance__ shard. Every field is optional — an empty
|
|
4
|
+
* AppAppearance object is treated as "no override" and removed from the
|
|
5
|
+
* map by setAppearance().
|
|
6
|
+
*/
|
|
7
|
+
export interface AppAppearance {
|
|
8
|
+
icon?: string;
|
|
9
|
+
color?: string;
|
|
10
|
+
label?: string;
|
|
11
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/apps/lifecycle.js
CHANGED
|
@@ -22,6 +22,8 @@ import { clearSelectionUnconditional } from '../actions/selection.svelte';
|
|
|
22
22
|
import { loadUserBindings } from '../actions/bindings-store';
|
|
23
23
|
import { toastManager } from '../overlays/toast';
|
|
24
24
|
import { clearAppNavEntries } from '../navigation/back-stack';
|
|
25
|
+
import { getActiveScopeId } from '../documents/config';
|
|
26
|
+
import { sessionState } from '../projects/session-state.svelte';
|
|
25
27
|
// ---------- last-active-app user zone ------------------------------------
|
|
26
28
|
/**
|
|
27
29
|
* Framework-reserved user-zone slot storing which app to boot into on
|
|
@@ -54,13 +56,19 @@ export function clearLastApp() {
|
|
|
54
56
|
}
|
|
55
57
|
// ---------- app-context state factories ----------------------------------
|
|
56
58
|
const appContexts = new Map();
|
|
57
|
-
function
|
|
59
|
+
function resolveLaunchScope() {
|
|
60
|
+
var _a;
|
|
61
|
+
return (_a = sessionState.activeProjectId) !== null && _a !== void 0 ? _a : getActiveScopeId();
|
|
62
|
+
}
|
|
63
|
+
function getOrCreateAppContext(appId, scopeId) {
|
|
58
64
|
var _a;
|
|
59
65
|
let ctx = appContexts.get(appId);
|
|
60
66
|
if (!ctx) {
|
|
61
67
|
const app = getRegisteredApp(appId);
|
|
68
|
+
const scope = scopeId !== null && scopeId !== void 0 ? scopeId : resolveLaunchScope();
|
|
62
69
|
ctx = {
|
|
63
|
-
|
|
70
|
+
scopeId: scope,
|
|
71
|
+
state: (schema) => createStateZones(`__app__:${appId}:scope:${scope}`, schema),
|
|
64
72
|
zones: ((_a = app === null || app === void 0 ? void 0 : app.manifest.permissions) === null || _a === void 0 ? void 0 : _a.includes(PERMISSION_STATE_MANAGE))
|
|
65
73
|
? createZoneManager()
|
|
66
74
|
: undefined,
|
package/dist/apps/types.d.ts
CHANGED
|
@@ -74,6 +74,13 @@ export interface AppManifest {
|
|
|
74
74
|
* When absent, the canonical fallback is used. See MenuContainer.
|
|
75
75
|
*/
|
|
76
76
|
menus?: MenuContainer[];
|
|
77
|
+
/**
|
|
78
|
+
* Optional default home-card icon — a lucide icon id from the bundled
|
|
79
|
+
* sprite (see `src/assets/icons.svg`). User-set per-browser overrides
|
|
80
|
+
* (via the home-card Customize action) take precedence; if neither is
|
|
81
|
+
* set the framework falls back to `box`.
|
|
82
|
+
*/
|
|
83
|
+
icon?: string;
|
|
77
84
|
}
|
|
78
85
|
/**
|
|
79
86
|
* Context object passed to `App.activate`. Provides app-scoped state zones
|
|
@@ -82,10 +89,17 @@ export interface AppManifest {
|
|
|
82
89
|
*/
|
|
83
90
|
export interface AppContext {
|
|
84
91
|
/**
|
|
85
|
-
*
|
|
86
|
-
*
|
|
87
|
-
*
|
|
88
|
-
*
|
|
92
|
+
* The scope (personal or project) this app instance is bound to for its
|
|
93
|
+
* lifetime. Set at launch from `session.activeProjectId`, falling back to
|
|
94
|
+
* the user's personal scope. The app's document handles are bound to this
|
|
95
|
+
* scope and cannot reach across scopes — exiting a scope unloads the app.
|
|
96
|
+
*/
|
|
97
|
+
scopeId: string;
|
|
98
|
+
/**
|
|
99
|
+
* App-scoped state zones. The shardId underneath is the app id plus the
|
|
100
|
+
* scope, so `state({ workspace: { x: 0 } }).workspace.x = 1` persists to
|
|
101
|
+
* `sh3:workspace:__app__:<appId>:scope:<scopeId>` with no collision risk
|
|
102
|
+
* against any shard of the same name and no leakage between scopes.
|
|
89
103
|
*/
|
|
90
104
|
state<T extends ZoneSchema>(schema: T): StateZones<T>;
|
|
91
105
|
/**
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function migrateLegacyWorkspaceKeys(personalScopeId: string): void;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Workspace state-zone key migration.
|
|
3
|
+
*
|
|
4
|
+
* Per ADR-002 amendment (2026-05-04), the workspace zone is keyed by
|
|
5
|
+
* `(scopeId, appId)`. Pre-existing localStorage entries written under
|
|
6
|
+
* the old `sh3:workspace:__app__:<appId>` prefix are rewritten to
|
|
7
|
+
* `sh3:workspace:__app__:<appId>:scope:<personalScopeId>` on first
|
|
8
|
+
* boot after upgrade. Idempotent — re-running on already-migrated
|
|
9
|
+
* entries is a no-op.
|
|
10
|
+
*
|
|
11
|
+
* Only entries whose shardId starts with the framework `__app__:`
|
|
12
|
+
* marker are migrated; bare shard keys are left alone.
|
|
13
|
+
*/
|
|
14
|
+
const APP_PREFIX = 'sh3:workspace:__app__:';
|
|
15
|
+
const SCOPE_MARKER = ':scope:';
|
|
16
|
+
export function migrateLegacyWorkspaceKeys(personalScopeId) {
|
|
17
|
+
if (typeof localStorage === 'undefined')
|
|
18
|
+
return;
|
|
19
|
+
const toMove = [];
|
|
20
|
+
for (let i = 0; i < localStorage.length; i++) {
|
|
21
|
+
const key = localStorage.key(i);
|
|
22
|
+
if (!key || !key.startsWith(APP_PREFIX))
|
|
23
|
+
continue;
|
|
24
|
+
if (key.includes(SCOPE_MARKER))
|
|
25
|
+
continue;
|
|
26
|
+
toMove.push([key, `${key}${SCOPE_MARKER}${personalScopeId}`]);
|
|
27
|
+
}
|
|
28
|
+
for (const [oldKey, newKey] of toMove) {
|
|
29
|
+
const value = localStorage.getItem(oldKey);
|
|
30
|
+
if (value !== null) {
|
|
31
|
+
localStorage.setItem(newKey, value);
|
|
32
|
+
localStorage.removeItem(oldKey);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from 'vitest';
|
|
2
|
+
import { migrateLegacyWorkspaceKeys } from './workspace-rekey';
|
|
3
|
+
describe('migrateLegacyWorkspaceKeys', () => {
|
|
4
|
+
beforeEach(() => localStorage.clear());
|
|
5
|
+
it('rewrites legacy app keys to scope-suffixed keys', () => {
|
|
6
|
+
localStorage.setItem('sh3:workspace:__app__:notes', JSON.stringify({ x: 1 }));
|
|
7
|
+
localStorage.setItem('sh3:workspace:__app__:files', JSON.stringify({ y: 2 }));
|
|
8
|
+
migrateLegacyWorkspaceKeys('user-1');
|
|
9
|
+
expect(localStorage.getItem('sh3:workspace:__app__:notes')).toBeNull();
|
|
10
|
+
expect(localStorage.getItem('sh3:workspace:__app__:notes:scope:user-1')).toBe(JSON.stringify({ x: 1 }));
|
|
11
|
+
expect(localStorage.getItem('sh3:workspace:__app__:files:scope:user-1')).toBe(JSON.stringify({ y: 2 }));
|
|
12
|
+
});
|
|
13
|
+
it('is idempotent', () => {
|
|
14
|
+
localStorage.setItem('sh3:workspace:__app__:notes:scope:user-1', JSON.stringify({ x: 1 }));
|
|
15
|
+
migrateLegacyWorkspaceKeys('user-1');
|
|
16
|
+
expect(localStorage.getItem('sh3:workspace:__app__:notes:scope:user-1')).toBe(JSON.stringify({ x: 1 }));
|
|
17
|
+
});
|
|
18
|
+
it('does not touch non-app workspace keys', () => {
|
|
19
|
+
localStorage.setItem('sh3:workspace:my-shard', JSON.stringify({ z: 3 }));
|
|
20
|
+
migrateLegacyWorkspaceKeys('user-1');
|
|
21
|
+
expect(localStorage.getItem('sh3:workspace:my-shard')).toBe(JSON.stringify({ z: 3 }));
|
|
22
|
+
});
|
|
23
|
+
});
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
export declare const ICON_IDS: readonly ["activity", "align-horizontal-justify-center", "align-horizontal-justify-end", "align-horizontal-justify-start", "app-window", "archive", "archive-restore", "axis-3d", "box", "brick-wall", "bug", "building-2", "cable", "calendar", "camera", "check", "chevron-down", "chevron-right", "circle-check", "circle-dot", "circle-minus", "circle-x", "clipboard", "clipboard-paste", "clock", "compass", "component", "copy", "cpu", "crop", "crosshair", "crown", "dollar-sign", "download", "droplet", "eraser", "euro", "external-link", "eye", "eye-off", "file", "file-archive", "file-diff", "file-plus", "file-text", "flame", "flip-horizontal-2", "flip-vertical-2", "folder", "folder-open", "folder-plus", "folder-tree", "gallery-vertical-end", "gamepad-2", "gauge", "gem", "git-branch", "git-commit-horizontal", "git-merge", "globe", "grid-2x2", "grid-3x3", "group", "hard-drive", "heart", "history", "house", "image", "info", "joystick", "key", "layers", "layout-dashboard", "layout-grid", "layout-list", "layout-panel-left", "layout-panel-top", "layout-template", "lightbulb", "link", "list-ordered", "list-tree", "lock", "log-out", "magnet", "mail", "map", "maximize", "minimize", "moon", "mouse-pointer", "move", "move-3d", "music", "navigation", "network", "notebook-pen", "palette", "pause", "pencil", "pipette", "play", "plus", "pointer", "pound-sterling", "receipt", "redo-2", "refresh-cw", "rocket", "rotate-3d", "rotate-ccw", "rotate-cw", "ruler", "save", "scissors", "scroll-text", "search", "send", "server", "settings", "shield", "skull", "sliders-horizontal", "snowflake", "sparkles", "square", "square-terminal", "star", "sun", "sword", "table-properties", "target", "texture", "timer", "trash-2", "triangle-alert", "type", "undo-2", "ungroup", "unity", "upload", "user", "users", "video", "volume-2", "wand-sparkles", "wind", "x", "zap", "zoom-in", "zoom-out"];
|
|
2
|
+
export type IconId = (typeof ICON_IDS)[number];
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
// GENERATED — do not edit. See scripts/sync-icon-ids.ts
|
|
2
|
+
export const ICON_IDS = [
|
|
3
|
+
'activity',
|
|
4
|
+
'align-horizontal-justify-center',
|
|
5
|
+
'align-horizontal-justify-end',
|
|
6
|
+
'align-horizontal-justify-start',
|
|
7
|
+
'app-window',
|
|
8
|
+
'archive',
|
|
9
|
+
'archive-restore',
|
|
10
|
+
'axis-3d',
|
|
11
|
+
'box',
|
|
12
|
+
'brick-wall',
|
|
13
|
+
'bug',
|
|
14
|
+
'building-2',
|
|
15
|
+
'cable',
|
|
16
|
+
'calendar',
|
|
17
|
+
'camera',
|
|
18
|
+
'check',
|
|
19
|
+
'chevron-down',
|
|
20
|
+
'chevron-right',
|
|
21
|
+
'circle-check',
|
|
22
|
+
'circle-dot',
|
|
23
|
+
'circle-minus',
|
|
24
|
+
'circle-x',
|
|
25
|
+
'clipboard',
|
|
26
|
+
'clipboard-paste',
|
|
27
|
+
'clock',
|
|
28
|
+
'compass',
|
|
29
|
+
'component',
|
|
30
|
+
'copy',
|
|
31
|
+
'cpu',
|
|
32
|
+
'crop',
|
|
33
|
+
'crosshair',
|
|
34
|
+
'crown',
|
|
35
|
+
'dollar-sign',
|
|
36
|
+
'download',
|
|
37
|
+
'droplet',
|
|
38
|
+
'eraser',
|
|
39
|
+
'euro',
|
|
40
|
+
'external-link',
|
|
41
|
+
'eye',
|
|
42
|
+
'eye-off',
|
|
43
|
+
'file',
|
|
44
|
+
'file-archive',
|
|
45
|
+
'file-diff',
|
|
46
|
+
'file-plus',
|
|
47
|
+
'file-text',
|
|
48
|
+
'flame',
|
|
49
|
+
'flip-horizontal-2',
|
|
50
|
+
'flip-vertical-2',
|
|
51
|
+
'folder',
|
|
52
|
+
'folder-open',
|
|
53
|
+
'folder-plus',
|
|
54
|
+
'folder-tree',
|
|
55
|
+
'gallery-vertical-end',
|
|
56
|
+
'gamepad-2',
|
|
57
|
+
'gauge',
|
|
58
|
+
'gem',
|
|
59
|
+
'git-branch',
|
|
60
|
+
'git-commit-horizontal',
|
|
61
|
+
'git-merge',
|
|
62
|
+
'globe',
|
|
63
|
+
'grid-2x2',
|
|
64
|
+
'grid-3x3',
|
|
65
|
+
'group',
|
|
66
|
+
'hard-drive',
|
|
67
|
+
'heart',
|
|
68
|
+
'history',
|
|
69
|
+
'house',
|
|
70
|
+
'image',
|
|
71
|
+
'info',
|
|
72
|
+
'joystick',
|
|
73
|
+
'key',
|
|
74
|
+
'layers',
|
|
75
|
+
'layout-dashboard',
|
|
76
|
+
'layout-grid',
|
|
77
|
+
'layout-list',
|
|
78
|
+
'layout-panel-left',
|
|
79
|
+
'layout-panel-top',
|
|
80
|
+
'layout-template',
|
|
81
|
+
'lightbulb',
|
|
82
|
+
'link',
|
|
83
|
+
'list-ordered',
|
|
84
|
+
'list-tree',
|
|
85
|
+
'lock',
|
|
86
|
+
'log-out',
|
|
87
|
+
'magnet',
|
|
88
|
+
'mail',
|
|
89
|
+
'map',
|
|
90
|
+
'maximize',
|
|
91
|
+
'minimize',
|
|
92
|
+
'moon',
|
|
93
|
+
'mouse-pointer',
|
|
94
|
+
'move',
|
|
95
|
+
'move-3d',
|
|
96
|
+
'music',
|
|
97
|
+
'navigation',
|
|
98
|
+
'network',
|
|
99
|
+
'notebook-pen',
|
|
100
|
+
'palette',
|
|
101
|
+
'pause',
|
|
102
|
+
'pencil',
|
|
103
|
+
'pipette',
|
|
104
|
+
'play',
|
|
105
|
+
'plus',
|
|
106
|
+
'pointer',
|
|
107
|
+
'pound-sterling',
|
|
108
|
+
'receipt',
|
|
109
|
+
'redo-2',
|
|
110
|
+
'refresh-cw',
|
|
111
|
+
'rocket',
|
|
112
|
+
'rotate-3d',
|
|
113
|
+
'rotate-ccw',
|
|
114
|
+
'rotate-cw',
|
|
115
|
+
'ruler',
|
|
116
|
+
'save',
|
|
117
|
+
'scissors',
|
|
118
|
+
'scroll-text',
|
|
119
|
+
'search',
|
|
120
|
+
'send',
|
|
121
|
+
'server',
|
|
122
|
+
'settings',
|
|
123
|
+
'shield',
|
|
124
|
+
'skull',
|
|
125
|
+
'sliders-horizontal',
|
|
126
|
+
'snowflake',
|
|
127
|
+
'sparkles',
|
|
128
|
+
'square',
|
|
129
|
+
'square-terminal',
|
|
130
|
+
'star',
|
|
131
|
+
'sun',
|
|
132
|
+
'sword',
|
|
133
|
+
'table-properties',
|
|
134
|
+
'target',
|
|
135
|
+
'texture',
|
|
136
|
+
'timer',
|
|
137
|
+
'trash-2',
|
|
138
|
+
'triangle-alert',
|
|
139
|
+
'type',
|
|
140
|
+
'undo-2',
|
|
141
|
+
'ungroup',
|
|
142
|
+
'unity',
|
|
143
|
+
'upload',
|
|
144
|
+
'user',
|
|
145
|
+
'users',
|
|
146
|
+
'video',
|
|
147
|
+
'volume-2',
|
|
148
|
+
'wand-sparkles',
|
|
149
|
+
'wind',
|
|
150
|
+
'x',
|
|
151
|
+
'zap',
|
|
152
|
+
'zoom-in',
|
|
153
|
+
'zoom-out',
|
|
154
|
+
];
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { AuthUser } from './types';
|
|
2
|
+
export declare const usersAdminState: {
|
|
3
|
+
users: AuthUser[];
|
|
4
|
+
loading: boolean;
|
|
5
|
+
error: string | null;
|
|
6
|
+
};
|
|
7
|
+
export declare function refreshAdminUsers(): Promise<void>;
|
|
8
|
+
/** Test-only reset. Clears state and any in-flight promise. */
|
|
9
|
+
export declare function __resetAdminUsersForTest(): void;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Shared admin-users cache.
|
|
3
|
+
*
|
|
4
|
+
* Wraps a single GET /api/admin/users behind a $state slot so multiple
|
|
5
|
+
* admin-form components (e.g., UserPicker instances inside ProjectManage,
|
|
6
|
+
* a future invite-user dialog, etc.) share one fetch without prop-drilling
|
|
7
|
+
* the user list. Concurrent calls reuse the in-flight promise; subsequent
|
|
8
|
+
* calls after completion fetch fresh data.
|
|
9
|
+
*/
|
|
10
|
+
export const usersAdminState = $state({ users: [], loading: false, error: null });
|
|
11
|
+
let inflight = null;
|
|
12
|
+
export function refreshAdminUsers() {
|
|
13
|
+
if (inflight)
|
|
14
|
+
return inflight;
|
|
15
|
+
usersAdminState.loading = true;
|
|
16
|
+
usersAdminState.error = null;
|
|
17
|
+
inflight = (async () => {
|
|
18
|
+
try {
|
|
19
|
+
const res = await fetch('/api/admin/users', { credentials: 'include' });
|
|
20
|
+
if (!res.ok) {
|
|
21
|
+
usersAdminState.error = `GET /api/admin/users failed: ${res.status}`;
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
usersAdminState.users = (await res.json());
|
|
25
|
+
}
|
|
26
|
+
catch (e) {
|
|
27
|
+
usersAdminState.error = e.message;
|
|
28
|
+
}
|
|
29
|
+
finally {
|
|
30
|
+
usersAdminState.loading = false;
|
|
31
|
+
inflight = null;
|
|
32
|
+
}
|
|
33
|
+
})();
|
|
34
|
+
return inflight;
|
|
35
|
+
}
|
|
36
|
+
/** Test-only reset. Clears state and any in-flight promise. */
|
|
37
|
+
export function __resetAdminUsersForTest() {
|
|
38
|
+
usersAdminState.users = [];
|
|
39
|
+
usersAdminState.loading = false;
|
|
40
|
+
usersAdminState.error = null;
|
|
41
|
+
inflight = null;
|
|
42
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
2
|
+
import { usersAdminState, refreshAdminUsers, __resetAdminUsersForTest, } from './admin-users.svelte';
|
|
3
|
+
beforeEach(() => {
|
|
4
|
+
__resetAdminUsersForTest();
|
|
5
|
+
vi.stubGlobal('fetch', vi.fn());
|
|
6
|
+
});
|
|
7
|
+
describe('refreshAdminUsers', () => {
|
|
8
|
+
it('GETs /api/admin/users and populates the state', async () => {
|
|
9
|
+
globalThis.fetch.mockResolvedValue({
|
|
10
|
+
ok: true,
|
|
11
|
+
json: async () => [
|
|
12
|
+
{ id: 'u-1', username: 'alice', displayName: 'Alice', role: 'user', createdAt: '', updatedAt: '' },
|
|
13
|
+
{ id: 'u-2', username: 'bob', displayName: 'Bob', role: 'admin', createdAt: '', updatedAt: '' },
|
|
14
|
+
],
|
|
15
|
+
});
|
|
16
|
+
await refreshAdminUsers();
|
|
17
|
+
expect(globalThis.fetch).toHaveBeenCalledWith('/api/admin/users', expect.objectContaining({ credentials: 'include' }));
|
|
18
|
+
expect(usersAdminState.users).toHaveLength(2);
|
|
19
|
+
expect(usersAdminState.users[0].username).toBe('alice');
|
|
20
|
+
expect(usersAdminState.loading).toBe(false);
|
|
21
|
+
expect(usersAdminState.error).toBeNull();
|
|
22
|
+
});
|
|
23
|
+
it('sets error and clears loading on non-ok response', async () => {
|
|
24
|
+
globalThis.fetch.mockResolvedValue({ ok: false, status: 403 });
|
|
25
|
+
await refreshAdminUsers();
|
|
26
|
+
expect(usersAdminState.users).toEqual([]);
|
|
27
|
+
expect(usersAdminState.loading).toBe(false);
|
|
28
|
+
expect(usersAdminState.error).toMatch(/403/);
|
|
29
|
+
});
|
|
30
|
+
it('sets error and clears loading on network throw', async () => {
|
|
31
|
+
globalThis.fetch.mockRejectedValue(new Error('network down'));
|
|
32
|
+
await refreshAdminUsers();
|
|
33
|
+
expect(usersAdminState.error).toBe('network down');
|
|
34
|
+
expect(usersAdminState.loading).toBe(false);
|
|
35
|
+
});
|
|
36
|
+
it('concurrent calls share one in-flight promise', async () => {
|
|
37
|
+
let resolveFetch = null;
|
|
38
|
+
globalThis.fetch.mockImplementation(() => new Promise((res) => { resolveFetch = res; }));
|
|
39
|
+
const a = refreshAdminUsers();
|
|
40
|
+
const b = refreshAdminUsers();
|
|
41
|
+
expect(globalThis.fetch).toHaveBeenCalledTimes(1);
|
|
42
|
+
resolveFetch({ ok: true, json: async () => [] });
|
|
43
|
+
await Promise.all([a, b]);
|
|
44
|
+
expect(globalThis.fetch).toHaveBeenCalledTimes(1);
|
|
45
|
+
});
|
|
46
|
+
it('a follow-up call after completion fetches again', async () => {
|
|
47
|
+
globalThis.fetch.mockResolvedValue({ ok: true, json: async () => [] });
|
|
48
|
+
await refreshAdminUsers();
|
|
49
|
+
await refreshAdminUsers();
|
|
50
|
+
expect(globalThis.fetch).toHaveBeenCalledTimes(2);
|
|
51
|
+
});
|
|
52
|
+
});
|
package/dist/createShell.js
CHANGED
|
@@ -12,7 +12,7 @@ import { registerShard, registerApp, bootstrap, __setBackend, setLocalOwner, } f
|
|
|
12
12
|
import { resolvePlatform } from './platform/index';
|
|
13
13
|
import { hydrateTokenOverrides } from './theme';
|
|
14
14
|
import { __setEnvServerUrl } from './env/index';
|
|
15
|
-
import {
|
|
15
|
+
import { __setActiveScope } from './documents/config';
|
|
16
16
|
import { initFromBoot } from './auth/index';
|
|
17
17
|
import SignInWall from './auth/SignInWall.svelte';
|
|
18
18
|
import { loadBundleModule } from './registry/loader';
|
|
@@ -54,13 +54,13 @@ export async function createShell(config) {
|
|
|
54
54
|
}
|
|
55
55
|
// 4. Auth decision point
|
|
56
56
|
if (platform.localOwner) {
|
|
57
|
-
// Local-owner (Tauri/dev): no auth, no sign-in,
|
|
57
|
+
// Local-owner (Tauri/dev): no auth, no sign-in, scope is 'local'.
|
|
58
58
|
// setLocalOwner() already called above — admin is assumed.
|
|
59
|
-
|
|
59
|
+
__setActiveScope('local');
|
|
60
60
|
}
|
|
61
61
|
else if (bootConfig) {
|
|
62
62
|
initFromBoot(sUrl, bootConfig);
|
|
63
|
-
|
|
63
|
+
__setActiveScope(bootConfig.tenantId);
|
|
64
64
|
const { auth, session } = bootConfig;
|
|
65
65
|
// Hard gate: no session, auth required, no guest allowed → sign-in wall
|
|
66
66
|
if (!session && auth.required && !auth.guestAllowed) {
|
|
@@ -70,7 +70,7 @@ export async function createShell(config) {
|
|
|
70
70
|
if (res.ok) {
|
|
71
71
|
bootConfig = await res.json();
|
|
72
72
|
initFromBoot(sUrl, bootConfig);
|
|
73
|
-
|
|
73
|
+
__setActiveScope(bootConfig.tenantId);
|
|
74
74
|
}
|
|
75
75
|
}
|
|
76
76
|
}
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import type { DocumentBackend } from './types';
|
|
2
|
+
export declare function getActiveScopeId(): string;
|
|
3
|
+
/** @deprecated use getActiveScopeId — kept until callers migrate. */
|
|
2
4
|
export declare function getTenantId(): string;
|
|
3
5
|
export declare function getDocumentBackend(): DocumentBackend;
|
|
4
|
-
/** Host-only. Set the
|
|
6
|
+
/** Host-only. Set the active scope id before bootstrap(). */
|
|
7
|
+
export declare function __setActiveScope(id: string): void;
|
|
8
|
+
/** @deprecated use __setActiveScope — kept until callers migrate. */
|
|
5
9
|
export declare function __setTenantId(id: string): void;
|
|
6
10
|
/** Host-only. Swap the document backend before bootstrap(). */
|
|
7
11
|
export declare function __setDocumentBackend(b: DocumentBackend): void;
|