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.
Files changed (172) hide show
  1. package/dist/BrandSlot.svelte +62 -13
  2. package/dist/__test__/setup-dom.js +5 -0
  3. package/dist/actions/MenuButton.svelte +2 -1
  4. package/dist/actions/contextMenuModel.d.ts +1 -1
  5. package/dist/actions/contextMenuModel.js +2 -1
  6. package/dist/actions/dispatcher.svelte.d.ts +1 -1
  7. package/dist/actions/dispatcher.svelte.js +2 -1
  8. package/dist/actions/listActive.d.ts +1 -1
  9. package/dist/actions/listActive.js +2 -1
  10. package/dist/actions/listeners.d.ts +1 -1
  11. package/dist/actions/listeners.js +6 -5
  12. package/dist/actions/menuBarModel.js +3 -2
  13. package/dist/actions/paletteModel.js +2 -1
  14. package/dist/actions/resolveLabel.test.js +14 -0
  15. package/dist/actions/types.d.ts +12 -1
  16. package/dist/actions/types.js +7 -1
  17. package/dist/api.d.ts +3 -0
  18. package/dist/api.js +3 -0
  19. package/dist/app/store/AppUpdateAvailableModal.svelte +87 -0
  20. package/dist/app/store/AppUpdateAvailableModal.svelte.d.ts +11 -0
  21. package/dist/app/store/InstalledView.svelte +8 -54
  22. package/dist/app/store/UninstallAppDialog.svelte +86 -0
  23. package/dist/app/store/UninstallAppDialog.svelte.d.ts +10 -0
  24. package/dist/app/store/permissionConfirm.d.ts +4 -0
  25. package/dist/app/store/permissionConfirm.js +28 -0
  26. package/dist/app/store/storeShard.svelte.d.ts +8 -1
  27. package/dist/app/store/storeShard.svelte.js +42 -9
  28. package/dist/app/store/updatePackage.test.d.ts +1 -0
  29. package/dist/app/store/updatePackage.test.js +34 -0
  30. package/dist/app/store/verbs.d.ts +1 -0
  31. package/dist/app/store/verbs.js +79 -5
  32. package/dist/app/store/verbs.test.d.ts +1 -0
  33. package/dist/app/store/verbs.test.js +56 -0
  34. package/dist/app-appearance/AppAppearanceModal.svelte +174 -0
  35. package/dist/app-appearance/AppAppearanceModal.svelte.d.ts +8 -0
  36. package/dist/app-appearance/appearanceShard.svelte.d.ts +2 -0
  37. package/dist/app-appearance/appearanceShard.svelte.js +61 -0
  38. package/dist/app-appearance/appearanceState.svelte.d.ts +15 -0
  39. package/dist/app-appearance/appearanceState.svelte.js +59 -0
  40. package/dist/app-appearance/appearanceState.test.d.ts +1 -0
  41. package/dist/app-appearance/appearanceState.test.js +30 -0
  42. package/dist/app-appearance/index.d.ts +3 -0
  43. package/dist/app-appearance/index.js +2 -0
  44. package/dist/app-appearance/types.d.ts +11 -0
  45. package/dist/app-appearance/types.js +1 -0
  46. package/dist/apps/lifecycle.js +10 -2
  47. package/dist/apps/types.d.ts +18 -4
  48. package/dist/apps/workspace-rekey.d.ts +1 -0
  49. package/dist/apps/workspace-rekey.js +35 -0
  50. package/dist/apps/workspace-rekey.test.d.ts +1 -0
  51. package/dist/apps/workspace-rekey.test.js +23 -0
  52. package/dist/assets/iconIds.generated.d.ts +2 -0
  53. package/dist/assets/iconIds.generated.js +154 -0
  54. package/dist/auth/admin-users.svelte.d.ts +9 -0
  55. package/dist/auth/admin-users.svelte.js +42 -0
  56. package/dist/auth/admin-users.test.d.ts +1 -0
  57. package/dist/auth/admin-users.test.js +52 -0
  58. package/dist/createShell.js +5 -5
  59. package/dist/documents/config.d.ts +5 -1
  60. package/dist/documents/config.js +16 -8
  61. package/dist/documents/index.d.ts +1 -1
  62. package/dist/documents/index.js +1 -1
  63. package/dist/host-entry.d.ts +1 -1
  64. package/dist/host-entry.js +1 -1
  65. package/dist/host.d.ts +1 -1
  66. package/dist/host.js +9 -2
  67. package/dist/primitives/Button.svelte +50 -4
  68. package/dist/primitives/Button.svelte.d.ts +3 -1
  69. package/dist/primitives/Collapsible.svelte +110 -0
  70. package/dist/primitives/Collapsible.svelte.d.ts +14 -0
  71. package/dist/primitives/widgets/AppPicker.svelte +41 -0
  72. package/dist/primitives/widgets/AppPicker.svelte.d.ts +9 -0
  73. package/dist/primitives/widgets/AppPicker.svelte.test.d.ts +1 -0
  74. package/dist/primitives/widgets/AppPicker.svelte.test.js +26 -0
  75. package/dist/primitives/widgets/AppPicker.test.d.ts +1 -0
  76. package/dist/primitives/widgets/AppPicker.test.js +74 -0
  77. package/dist/primitives/widgets/ColorSwatch.svelte +7 -2
  78. package/dist/primitives/widgets/ColorSwatch.svelte.d.ts +2 -1
  79. package/dist/primitives/widgets/ColorSwatch.svelte.test.d.ts +1 -0
  80. package/dist/primitives/widgets/ColorSwatch.svelte.test.js +31 -0
  81. package/dist/primitives/widgets/Field.svelte +4 -2
  82. package/dist/primitives/widgets/Field.svelte.d.ts +2 -2
  83. package/dist/primitives/widgets/Field.svelte.test.d.ts +1 -0
  84. package/dist/primitives/widgets/Field.svelte.test.js +33 -0
  85. package/dist/primitives/widgets/FilePicker.svelte +2 -2
  86. package/dist/primitives/widgets/FilePicker.svelte.d.ts +2 -2
  87. package/dist/primitives/widgets/FilePicker.svelte.test.d.ts +1 -0
  88. package/dist/primitives/widgets/FilePicker.svelte.test.js +31 -0
  89. package/dist/primitives/widgets/IconPicker.svelte +115 -0
  90. package/dist/primitives/widgets/IconPicker.svelte.d.ts +9 -0
  91. package/dist/primitives/widgets/IconPicker.svelte.test.d.ts +1 -0
  92. package/dist/primitives/widgets/IconPicker.svelte.test.js +43 -0
  93. package/dist/primitives/widgets/IconToggleGroup.svelte +4 -4
  94. package/dist/primitives/widgets/IconToggleGroup.svelte.d.ts +3 -3
  95. package/dist/primitives/widgets/IconToggleGroup.svelte.test.d.ts +1 -0
  96. package/dist/primitives/widgets/IconToggleGroup.svelte.test.js +40 -0
  97. package/dist/primitives/widgets/NumberInput.svelte +19 -9
  98. package/dist/primitives/widgets/NumberInput.svelte.d.ts +2 -2
  99. package/dist/primitives/widgets/NumberInput.svelte.test.d.ts +1 -0
  100. package/dist/primitives/widgets/NumberInput.svelte.test.js +48 -0
  101. package/dist/primitives/widgets/PickerList.d.ts +24 -0
  102. package/dist/primitives/widgets/PickerList.js +21 -0
  103. package/dist/primitives/widgets/PickerList.svelte +150 -0
  104. package/dist/primitives/widgets/PickerList.svelte.d.ts +16 -0
  105. package/dist/primitives/widgets/PickerList.svelte.test.d.ts +1 -0
  106. package/dist/primitives/widgets/PickerList.svelte.test.js +31 -0
  107. package/dist/primitives/widgets/PickerList.test.d.ts +1 -0
  108. package/dist/primitives/widgets/PickerList.test.js +218 -0
  109. package/dist/primitives/widgets/RangeSlider.svelte +11 -4
  110. package/dist/primitives/widgets/RangeSlider.svelte.d.ts +2 -2
  111. package/dist/primitives/widgets/RangeSlider.svelte.test.d.ts +1 -0
  112. package/dist/primitives/widgets/RangeSlider.svelte.test.js +38 -0
  113. package/dist/primitives/widgets/Segmented.svelte +4 -4
  114. package/dist/primitives/widgets/Segmented.svelte.d.ts +3 -3
  115. package/dist/primitives/widgets/Segmented.svelte.test.d.ts +1 -0
  116. package/dist/primitives/widgets/Segmented.svelte.test.js +25 -0
  117. package/dist/primitives/widgets/Select.svelte +4 -4
  118. package/dist/primitives/widgets/Select.svelte.d.ts +3 -3
  119. package/dist/primitives/widgets/Select.svelte.test.d.ts +1 -0
  120. package/dist/primitives/widgets/Select.svelte.test.js +37 -0
  121. package/dist/primitives/widgets/Slider.svelte +4 -2
  122. package/dist/primitives/widgets/Slider.svelte.d.ts +2 -2
  123. package/dist/primitives/widgets/Slider.svelte.test.d.ts +1 -0
  124. package/dist/primitives/widgets/Slider.svelte.test.js +22 -0
  125. package/dist/primitives/widgets/SliderGroup.svelte +4 -2
  126. package/dist/primitives/widgets/SliderGroup.svelte.d.ts +2 -2
  127. package/dist/primitives/widgets/SliderGroup.svelte.test.d.ts +1 -0
  128. package/dist/primitives/widgets/SliderGroup.svelte.test.js +34 -0
  129. package/dist/primitives/widgets/Textarea.svelte +5 -2
  130. package/dist/primitives/widgets/Textarea.svelte.d.ts +2 -2
  131. package/dist/primitives/widgets/Textarea.svelte.test.d.ts +1 -0
  132. package/dist/primitives/widgets/Textarea.svelte.test.js +29 -0
  133. package/dist/primitives/widgets/UserPicker.svelte +53 -0
  134. package/dist/primitives/widgets/UserPicker.svelte.d.ts +9 -0
  135. package/dist/primitives/widgets/UserPicker.svelte.test.d.ts +1 -0
  136. package/dist/primitives/widgets/UserPicker.svelte.test.js +30 -0
  137. package/dist/primitives/widgets/UserPicker.test.d.ts +1 -0
  138. package/dist/primitives/widgets/UserPicker.test.js +115 -0
  139. package/dist/primitives/widgets/_contract.d.ts +27 -0
  140. package/dist/primitives/widgets/_contract.js +10 -0
  141. package/dist/projects/session-state.svelte.d.ts +17 -0
  142. package/dist/projects/session-state.svelte.js +39 -0
  143. package/dist/projects/session-state.test.d.ts +1 -0
  144. package/dist/projects/session-state.test.js +55 -0
  145. package/dist/projects-shard/DeleteProjectDialog.svelte +150 -0
  146. package/dist/projects-shard/DeleteProjectDialog.svelte.d.ts +12 -0
  147. package/dist/projects-shard/DeleteProjectDialog.test.d.ts +1 -0
  148. package/dist/projects-shard/DeleteProjectDialog.test.js +120 -0
  149. package/dist/projects-shard/ProjectManage.svelte +219 -0
  150. package/dist/projects-shard/ProjectManage.svelte.d.ts +8 -0
  151. package/dist/projects-shard/ProjectsSection.svelte +120 -0
  152. package/dist/projects-shard/ProjectsSection.svelte.d.ts +3 -0
  153. package/dist/projects-shard/index.d.ts +4 -0
  154. package/dist/projects-shard/index.js +4 -0
  155. package/dist/projects-shard/projectsApi.d.ts +20 -0
  156. package/dist/projects-shard/projectsApi.js +44 -0
  157. package/dist/projects-shard/projectsApi.test.d.ts +1 -0
  158. package/dist/projects-shard/projectsApi.test.js +71 -0
  159. package/dist/projects-shard/projectsShard.svelte.d.ts +10 -0
  160. package/dist/projects-shard/projectsShard.svelte.js +148 -0
  161. package/dist/sh3core-shard/ShellHome.svelte +83 -39
  162. package/dist/sh3core-shard/appActions.d.ts +13 -0
  163. package/dist/sh3core-shard/appActions.js +181 -0
  164. package/dist/sh3core-shard/appActions.test.d.ts +1 -0
  165. package/dist/sh3core-shard/appActions.test.js +25 -0
  166. package/dist/sh3core-shard/sh3coreShard.svelte.js +2 -0
  167. package/dist/shards/activate-scopeid.test.d.ts +1 -0
  168. package/dist/shards/{activate-tenantid.test.js → activate-scopeid.test.js} +6 -6
  169. package/dist/version.d.ts +1 -1
  170. package/dist/version.js +1 -1
  171. package/package.json +2 -2
  172. /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,3 @@
1
+ export { appearanceShard } from './appearanceShard.svelte';
2
+ export { getAppearance, setAppearance } from './appearanceState.svelte';
3
+ export type { AppAppearance } from './types';
@@ -0,0 +1,2 @@
1
+ export { appearanceShard } from './appearanceShard.svelte';
2
+ export { getAppearance, setAppearance } from './appearanceState.svelte';
@@ -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 {};
@@ -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 getOrCreateAppContext(appId) {
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
- state: (schema) => createStateZones(`__app__:${appId}`, schema),
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,
@@ -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
- * App-scoped state zones. The shardId underneath is the app id, so
86
- * `state({ workspace: { x: 0 } }).workspace.x = 1` persists to
87
- * `sh3:workspace:<appId>` with no collision risk against any shard
88
- * of the same name.
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
+ });
@@ -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 { __setTenantId } from './documents/config';
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, tenant is 'local'.
57
+ // Local-owner (Tauri/dev): no auth, no sign-in, scope is 'local'.
58
58
  // setLocalOwner() already called above — admin is assumed.
59
- __setTenantId('local');
59
+ __setActiveScope('local');
60
60
  }
61
61
  else if (bootConfig) {
62
62
  initFromBoot(sUrl, bootConfig);
63
- __setTenantId(bootConfig.tenantId);
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
- __setTenantId(bootConfig.tenantId);
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 tenant id before bootstrap(). */
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;