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
@@ -1,19 +1,24 @@
1
1
  <script lang="ts">
2
2
  /*
3
- * BrandSlot — top-bar context indicator. Three states:
4
- * - 'brand' → renders <span>SH3</span>
5
- * - 'app' → renders <span>{label}</span>
6
- * - 'breadcrumb' → renders SH3 + separator + <button>{label}</button>
3
+ * BrandSlot — top-bar context indicator. Mode resolves from the
4
+ * (activeAppId, breadcrumbAppId, activeProjectId) triple:
7
5
  *
8
- * State derives from (activeAppId, breadcrumbAppId). Click on the
9
- * breadcrumb's app button re-launches the app (existing handler).
6
+ * brand → SH3
7
+ * app [App Name]
8
+ * breadcrumb → SH3 › [App Name] (clickable app)
9
+ * project-home → SH3 › [Project] (clickable SH3 → exit project)
10
+ * project-app → [Project] › [App] (no SH3 — use home button to exit)
11
+ * project-breadcrumb → SH3 › [Project] › [App] (clickable SH3, project, app)
10
12
  */
11
13
  import { getLiveDispatcherState } from './actions/state.svelte';
12
- import { launchApp } from './apps/lifecycle';
14
+ import { launchApp, returnToHome } from './apps/lifecycle';
13
15
  import { getBreadcrumbAppId, getRegisteredApp } from './apps/registry.svelte';
16
+ import { sessionState, setActiveProjectId } from './projects/session-state.svelte';
17
+ import { projectsState } from './projects-shard/projectsShard.svelte';
14
18
 
15
19
  const activeAppId = $derived(getLiveDispatcherState().activeAppId);
16
20
  const breadcrumbId = $derived(getBreadcrumbAppId());
21
+ const projectId = $derived(sessionState.activeProjectId);
17
22
 
18
23
  const activeLabel = $derived(
19
24
  activeAppId ? getRegisteredApp(activeAppId)?.manifest.label ?? activeAppId : null,
@@ -21,16 +26,34 @@
21
26
  const breadcrumbLabel = $derived(
22
27
  breadcrumbId ? getRegisteredApp(breadcrumbId)?.manifest.label ?? breadcrumbId : null,
23
28
  );
29
+ const projectLabel = $derived(
30
+ projectId ? projectsState.projects.find((p) => p.id === projectId)?.name ?? projectId : null,
31
+ );
32
+
33
+ type Mode = 'brand' | 'app' | 'breadcrumb' | 'project-home' | 'project-app' | 'project-breadcrumb';
24
34
 
25
- const mode: 'brand' | 'app' | 'breadcrumb' = $derived.by(() => {
35
+ const mode: Mode = $derived.by(() => {
36
+ if (projectId) {
37
+ if (activeAppId) return 'project-app';
38
+ if (breadcrumbId) return 'project-breadcrumb';
39
+ return 'project-home';
40
+ }
26
41
  if (activeAppId) return 'app';
27
42
  if (breadcrumbId) return 'breadcrumb';
28
43
  return 'brand';
29
44
  });
30
45
 
31
- function reopen() {
46
+ function reopenApp() {
32
47
  if (breadcrumbId) void launchApp(breadcrumbId);
33
48
  }
49
+
50
+ function exitProject() {
51
+ setActiveProjectId(null);
52
+ }
53
+
54
+ function reenterProjectHome() {
55
+ if (activeAppId) void returnToHome();
56
+ }
34
57
  </script>
35
58
 
36
59
  <div class="sh3-brand-slot">
@@ -38,12 +61,24 @@
38
61
  <span class="sh3-brand">SH3</span>
39
62
  {:else if mode === 'app'}
40
63
  <span class="sh3-brand sh3-brand-app">{activeLabel}</span>
41
- {:else}
64
+ {:else if mode === 'breadcrumb'}
42
65
  <span class="sh3-brand">SH3</span>
43
66
  <span class="sh3-brand-sep" aria-hidden="true">›</span>
44
- <button type="button" class="sh3-brand-crumb" onclick={reopen}>
45
- {breadcrumbLabel}
46
- </button>
67
+ <button type="button" class="sh3-brand-crumb" onclick={reopenApp}>{breadcrumbLabel}</button>
68
+ {:else if mode === 'project-home'}
69
+ <button type="button" class="sh3-brand sh3-brand-clickable" onclick={exitProject} title="Exit project">SH3</button>
70
+ <span class="sh3-brand-sep" aria-hidden="true">›</span>
71
+ <span class="sh3-brand-project">{projectLabel}</span>
72
+ {:else if mode === 'project-app'}
73
+ <span class="sh3-brand sh3-brand-project">{projectLabel}</span>
74
+ <span class="sh3-brand-sep" aria-hidden="true">›</span>
75
+ <span class="sh3-brand sh3-brand-app">{activeLabel}</span>
76
+ {:else if mode === 'project-breadcrumb'}
77
+ <button type="button" class="sh3-brand sh3-brand-clickable" onclick={exitProject} title="Exit project">SH3</button>
78
+ <span class="sh3-brand-sep" aria-hidden="true">›</span>
79
+ <button type="button" class="sh3-brand-crumb" onclick={reenterProjectHome}>{projectLabel}</button>
80
+ <span class="sh3-brand-sep" aria-hidden="true">›</span>
81
+ <button type="button" class="sh3-brand-crumb" onclick={reopenApp}>{breadcrumbLabel}</button>
47
82
  {/if}
48
83
  </div>
49
84
 
@@ -61,6 +96,9 @@
61
96
  .sh3-brand-app {
62
97
  color: var(--shell-fg);
63
98
  }
99
+ .sh3-brand-project {
100
+ color: var(--shell-fg);
101
+ }
64
102
  .sh3-brand-sep {
65
103
  color: var(--shell-fg-muted);
66
104
  margin: 0 4px;
@@ -77,4 +115,15 @@
77
115
  .sh3-brand-crumb:hover {
78
116
  background: var(--shell-bg-elevated);
79
117
  }
118
+ .sh3-brand-clickable {
119
+ background: transparent;
120
+ border: 0;
121
+ font: inherit;
122
+ cursor: pointer;
123
+ padding: 2px 6px;
124
+ border-radius: var(--shell-radius-sm, 3px);
125
+ }
126
+ .sh3-brand-clickable:hover {
127
+ background: var(--shell-bg-elevated);
128
+ }
80
129
  </style>
@@ -1,4 +1,9 @@
1
1
  import '@testing-library/jest-dom/vitest';
2
+ import { afterEach } from 'vitest';
3
+ import { cleanup } from '@testing-library/svelte';
4
+ // @testing-library/svelte v5 only auto-registers cleanup when vitest
5
+ // globals are enabled. We use globals: false, so wire it manually.
6
+ afterEach(() => cleanup());
2
7
  /**
3
8
  * happy-dom creates per-window Comment subclasses (via WindowContextClassExtender)
4
9
  * but actually instantiates nodes from the base CommentImplementation class. This
@@ -19,6 +19,7 @@
19
19
  import { getLiveDispatcherState } from './state.svelte';
20
20
  import type { DispatcherState } from './dispatcher.svelte';
21
21
  import { resolveSubmenuItems, type MenuBarItem } from './menuBarModel';
22
+ import { resolveLabel } from './types';
22
23
  import type { MenuContainer } from '../apps/types';
23
24
 
24
25
  let { container, items }: {
@@ -47,7 +48,7 @@
47
48
  if (!entry || typeof entry.action.run !== 'function') return;
48
49
  try {
49
50
  void entry.action.run({
50
- action: { id, label: entry.action.label },
51
+ action: { id, label: resolveLabel(entry.action) },
51
52
  appId: state.activeAppId,
52
53
  viewId: state.focusedViewId ?? undefined,
53
54
  selection: state.selection ?? undefined,
@@ -1,6 +1,6 @@
1
1
  import type { ActionEntry } from './registry';
2
2
  import { type DispatcherState, type TierName } from './dispatcher.svelte';
3
- import type { AtomicScope } from './types';
3
+ import { type AtomicScope } from './types';
4
4
  export interface MenuItem {
5
5
  id: string;
6
6
  label: string;
@@ -9,6 +9,7 @@
9
9
  import { TIER_ORDER, isScopeActive, } from './dispatcher.svelte';
10
10
  import { effectiveShortcut } from './bindings';
11
11
  import { scopeToTier, innermostActiveScope, scopeEquals, normalizeScope, } from './scope-helpers';
12
+ import { resolveLabel } from './types';
12
13
  function evalFlag(v) {
13
14
  if (v === undefined)
14
15
  return false;
@@ -18,7 +19,7 @@ function toMenuItem(entry, state) {
18
19
  var _a;
19
20
  return {
20
21
  id: entry.action.id,
21
- label: entry.action.label,
22
+ label: resolveLabel(entry.action),
22
23
  shortcut: effectiveShortcut(entry.action, state.bindings, state.platform),
23
24
  group: (_a = entry.action.group) !== null && _a !== void 0 ? _a : '',
24
25
  icon: entry.action.icon,
@@ -1,4 +1,4 @@
1
- import type { AtomicScope, Selection } from './types';
1
+ import { type AtomicScope, type Selection } from './types';
2
2
  import type { ActionEntry } from './registry';
3
3
  import type { Platform } from './shortcuts';
4
4
  export interface DispatcherState {
@@ -4,6 +4,7 @@
4
4
  * This module exposes testable state transitions; listeners feed it
5
5
  * state snapshots.
6
6
  */
7
+ import { resolveLabel } from './types';
7
8
  import { effectiveShortcut } from './bindings';
8
9
  import { scopeToTier, normalizeScope } from './scope-helpers';
9
10
  export const TIER_ORDER = ['element', 'focus', 'view', 'app', 'home'];
@@ -91,7 +92,7 @@ export function dispatchKeydown(env) {
91
92
  return null;
92
93
  }
93
94
  env.runAction(id, {
94
- action: { id: entry.action.id, label: entry.action.label },
95
+ action: { id: entry.action.id, label: resolveLabel(entry.action) },
95
96
  appId: env.state.activeAppId,
96
97
  viewId: (_a = env.state.focusedViewId) !== null && _a !== void 0 ? _a : undefined,
97
98
  selection: (_b = env.state.selection) !== null && _b !== void 0 ? _b : undefined,
@@ -1,4 +1,4 @@
1
1
  import type { ActionEntry } from './registry';
2
2
  import { type DispatcherState } from './dispatcher.svelte';
3
- import type { ActiveActionDescriptor } from './types';
3
+ import { type ActiveActionDescriptor } from './types';
4
4
  export declare function listActiveFromEntries(entries: ActionEntry[], state: DispatcherState): ActiveActionDescriptor[];
@@ -11,6 +11,7 @@
11
11
  import { TIER_ORDER, } from './dispatcher.svelte';
12
12
  import { effectiveShortcutWithSource } from './bindings';
13
13
  import { innermostActiveScope, scopeBadge, scopeToTier } from './scope-helpers';
14
+ import { resolveLabel } from './types';
14
15
  export function listActiveFromEntries(entries, state) {
15
16
  const byTier = {
16
17
  element: [], focus: [], view: [], app: [], home: [],
@@ -26,7 +27,7 @@ export function listActiveFromEntries(entries, state) {
26
27
  const { shortcut, source } = effectiveShortcutWithSource(entry.action, state.bindings, state.platform);
27
28
  byTier[scopeToTier(winning)].push({
28
29
  id: entry.action.id,
29
- label: entry.action.label,
30
+ label: resolveLabel(entry.action),
30
31
  effectiveShortcut: shortcut,
31
32
  bindingSource: source,
32
33
  scope: winning,
@@ -1,4 +1,4 @@
1
- import type { AtomicScope } from './types';
1
+ import { type AtomicScope } from './types';
2
2
  export interface OpenContextMenuOpts {
3
3
  x: number;
4
4
  y: number;
@@ -8,6 +8,7 @@ import { listActions } from './registry';
8
8
  import { dispatchKeydown } from './dispatcher.svelte';
9
9
  import { getLiveDispatcherState, setFocusedViewId, } from './state.svelte';
10
10
  import { eventToShortcut } from './shortcuts';
11
+ import { resolveLabel } from './types';
11
12
  import ContextMenu from './ContextMenu.svelte';
12
13
  import { buildContextMenuModel, buildContextMenuSubmenu } from './contextMenuModel';
13
14
  import ActionPanel from './ActionPanel.svelte';
@@ -83,7 +84,7 @@ function chainedDispatch(actionId) {
83
84
  }
84
85
  const state = getLiveDispatcherState();
85
86
  runAction(actionId, {
86
- action: { id: entry.action.id, label: entry.action.label },
87
+ action: { id: entry.action.id, label: resolveLabel(entry.action) },
87
88
  appId: state.activeAppId,
88
89
  viewId: (_a = state.focusedViewId) !== null && _a !== void 0 ? _a : undefined,
89
90
  selection: (_b = state.selection) !== null && _b !== void 0 ? _b : undefined,
@@ -132,7 +133,7 @@ function openContextSubmenu(parentId, state, handle, anchor) {
132
133
  return;
133
134
  try {
134
135
  void child.action.run({
135
- action: { id: cid, label: child.action.label },
136
+ action: { id: cid, label: resolveLabel(child.action) },
136
137
  appId: state.activeAppId,
137
138
  viewId: (_a = state.focusedViewId) !== null && _a !== void 0 ? _a : undefined,
138
139
  selection: (_b = state.selection) !== null && _b !== void 0 ? _b : undefined,
@@ -174,7 +175,7 @@ function onContextMenu(ev) {
174
175
  return;
175
176
  try {
176
177
  void entry.action.run({
177
- action: { id, label: entry.action.label },
178
+ action: { id, label: resolveLabel(entry.action) },
178
179
  appId: state.activeAppId,
179
180
  viewId: (_a = state.focusedViewId) !== null && _a !== void 0 ? _a : undefined,
180
181
  selection: (_b = state.selection) !== null && _b !== void 0 ? _b : undefined,
@@ -246,7 +247,7 @@ export function openContextMenu(opts) {
246
247
  return;
247
248
  try {
248
249
  void entry.action.run({
249
- action: { id, label: entry.action.label },
250
+ action: { id, label: resolveLabel(entry.action) },
250
251
  appId: state.activeAppId,
251
252
  viewId: (_a = state.focusedViewId) !== null && _a !== void 0 ? _a : undefined,
252
253
  selection: (_b = state.selection) !== null && _b !== void 0 ? _b : undefined,
@@ -293,7 +294,7 @@ export function openPalette(opts) {
293
294
  return;
294
295
  try {
295
296
  void entry.action.run({
296
- action: { id, label: entry.action.label },
297
+ action: { id, label: resolveLabel(entry.action) },
297
298
  appId: state.activeAppId,
298
299
  viewId: (_a = state.focusedViewId) !== null && _a !== void 0 ? _a : undefined,
299
300
  selection: (_b = state.selection) !== null && _b !== void 0 ? _b : undefined,
@@ -6,6 +6,7 @@
6
6
  */
7
7
  import { effectiveShortcut } from './bindings';
8
8
  import { innermostActiveScope } from './scope-helpers';
9
+ import { resolveLabel } from './types';
9
10
  import { DEFAULT_MENU_CONTAINERS } from './defaultMenuContainers';
10
11
  function evalFlag(v) {
11
12
  if (v === undefined)
@@ -64,7 +65,7 @@ export function resolveMenuItems(entries, state, containerId) {
64
65
  seen.add(entry.action.id);
65
66
  out.push({
66
67
  id: entry.action.id,
67
- label: entry.action.label,
68
+ label: resolveLabel(entry.action),
68
69
  shortcut: effectiveShortcut(entry.action, state.bindings, state.platform),
69
70
  group: (_a = entry.action.group) !== null && _a !== void 0 ? _a : '',
70
71
  icon: entry.action.icon,
@@ -97,7 +98,7 @@ export function resolveSubmenuItems(entries, state, parentId) {
97
98
  seen.add(entry.action.id);
98
99
  out.push({
99
100
  id: entry.action.id,
100
- label: entry.action.label,
101
+ label: resolveLabel(entry.action),
101
102
  shortcut: effectiveShortcut(entry.action, state.bindings, state.platform),
102
103
  group: (_a = entry.action.group) !== null && _a !== void 0 ? _a : '',
103
104
  icon: entry.action.icon,
@@ -14,6 +14,7 @@
14
14
  */
15
15
  import { effectiveShortcut } from './bindings';
16
16
  import { innermostActiveScope, scopeBadge } from './scope-helpers';
17
+ import { resolveLabel } from './types';
17
18
  function evalFlag(v) {
18
19
  if (v === undefined)
19
20
  return false;
@@ -39,7 +40,7 @@ export function buildPaletteCandidates(entries, state, opts = {}) {
39
40
  seen.add(entry.action.id);
40
41
  out.push({
41
42
  id: entry.action.id,
42
- label: entry.action.label,
43
+ label: resolveLabel(entry.action),
43
44
  shortcut: effectiveShortcut(entry.action, state.bindings, state.platform),
44
45
  scopeBadge: scopeBadge(winning),
45
46
  submenu: entry.action.submenu === true,
@@ -0,0 +1,14 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { resolveLabel } from './types';
3
+ describe('resolveLabel', () => {
4
+ it('returns string label as-is', () => {
5
+ const a = { id: 'x', label: 'Hello', scope: 'app' };
6
+ expect(resolveLabel(a)).toBe('Hello');
7
+ });
8
+ it('calls function label and returns the result', () => {
9
+ let n = 0;
10
+ const a = { id: 'x', label: () => `n=${++n}`, scope: 'app' };
11
+ expect(resolveLabel(a)).toBe('n=1');
12
+ expect(resolveLabel(a)).toBe('n=2');
13
+ });
14
+ });
@@ -4,7 +4,13 @@ export type AtomicScope = 'home' | 'app' | `view:${string}` | `focus:${string}`
4
4
  export type ActionScope = AtomicScope | AtomicScope[];
5
5
  export interface Action {
6
6
  id: string;
7
- label: string;
7
+ /**
8
+ * Display label. May be a function for live-evaluated labels (re-read on
9
+ * each menu derive — same cadence as `disabled`/`checked`). Function form
10
+ * is appropriate for runtime-suffixed labels (e.g. `· admin only`); static
11
+ * strings are still the common case.
12
+ */
13
+ label: string | (() => string);
8
14
  scope: ActionScope;
9
15
  contextItem?: boolean;
10
16
  paletteItem?: boolean;
@@ -143,3 +149,8 @@ export interface ActiveActionDescriptor {
143
149
  paletteItem: boolean;
144
150
  contextItem: boolean;
145
151
  }
152
+ /**
153
+ * Resolve an Action's label to a string. Function labels are called on each
154
+ * read; string labels are returned unchanged.
155
+ */
156
+ export declare function resolveLabel(action: Pick<Action, 'label'>): string;
@@ -4,4 +4,10 @@
4
4
  * context menu, or command palette. See the spec at
5
5
  * docs/superpowers/specs/2026-04-22-actions-contexts-design.md.
6
6
  */
7
- export {};
7
+ /**
8
+ * Resolve an Action's label to a string. Function labels are called on each
9
+ * read; string labels are returned unchanged.
10
+ */
11
+ export function resolveLabel(action) {
12
+ return typeof action.label === 'function' ? action.label() : action.label;
13
+ }
package/dist/api.d.ts CHANGED
@@ -55,6 +55,7 @@ export declare const FRAMEWORK_SHARD_IDS: readonly string[];
55
55
  export { setTokenOverrides, clearTokenOverrides, getTokenOverrides, } from './theme';
56
56
  export { default as Button } from './primitives/Button.svelte';
57
57
  export { provideIcons, getIconSprite, type ButtonVariant } from './primitives/icon-context';
58
+ export { default as Collapsible } from './primitives/Collapsible.svelte';
58
59
  export { default as Field } from './primitives/widgets/Field.svelte';
59
60
  export { default as Textarea } from './primitives/widgets/Textarea.svelte';
60
61
  export { default as NumberInput } from './primitives/widgets/NumberInput.svelte';
@@ -69,3 +70,5 @@ export { default as FilePicker } from './primitives/widgets/FilePicker.svelte';
69
70
  export type { FilePickerValue } from './primitives/widgets/FilePicker';
70
71
  export { default as Select } from './primitives/widgets/Select.svelte';
71
72
  export type { SelectOption } from './primitives/widgets/Select';
73
+ export { default as AppPicker } from './primitives/widgets/AppPicker.svelte';
74
+ export { default as UserPicker } from './primitives/widgets/UserPicker.svelte';
package/dist/api.js CHANGED
@@ -73,6 +73,7 @@ export { setTokenOverrides, clearTokenOverrides, getTokenOverrides, } from './th
73
73
  // `import { Button } from 'sh3-core'` against the runtime shim in loader.ts.
74
74
  export { default as Button } from './primitives/Button.svelte';
75
75
  export { provideIcons, getIconSprite } from './primitives/icon-context';
76
+ export { default as Collapsible } from './primitives/Collapsible.svelte';
76
77
  // Controllable widget primitives (ADR-022).
77
78
  export { default as Field } from './primitives/widgets/Field.svelte';
78
79
  export { default as Textarea } from './primitives/widgets/Textarea.svelte';
@@ -85,3 +86,5 @@ export { default as SliderGroup } from './primitives/widgets/SliderGroup.svelte'
85
86
  export { default as ColorSwatch } from './primitives/widgets/ColorSwatch.svelte';
86
87
  export { default as FilePicker } from './primitives/widgets/FilePicker.svelte';
87
88
  export { default as Select } from './primitives/widgets/Select.svelte';
89
+ export { default as AppPicker } from './primitives/widgets/AppPicker.svelte';
90
+ export { default as UserPicker } from './primitives/widgets/UserPicker.svelte';
@@ -0,0 +1,87 @@
1
+ <script lang="ts">
2
+ /*
3
+ * Confirms an available update for an app launched via the home-card
4
+ * "Check for updates" action. If the user clicks Update, onConfirm runs
5
+ * (which performs the actual storeContext.updatePackage call); the
6
+ * permission-diff prompt — when needed — is opened by updatePackage's
7
+ * own confirmPermissionChange callback. Cancel just closes.
8
+ */
9
+
10
+ interface Props {
11
+ appId: string;
12
+ appLabel: string;
13
+ fromVersion: string;
14
+ toVersion: string;
15
+ onConfirm: () => Promise<void>;
16
+ close: () => void;
17
+ }
18
+
19
+ let { appId, appLabel, fromVersion, toVersion, onConfirm, close }: Props = $props();
20
+ let busy = $state(false);
21
+ let error = $state<string | null>(null);
22
+
23
+ async function confirm() {
24
+ if (busy) return;
25
+ busy = true;
26
+ error = null;
27
+ try {
28
+ await onConfirm();
29
+ close();
30
+ } catch (e) {
31
+ error = (e as Error).message;
32
+ } finally {
33
+ busy = false;
34
+ }
35
+ }
36
+ </script>
37
+
38
+ <div class="app-update-modal">
39
+ <h2>Update available</h2>
40
+ <p>
41
+ <strong>{appLabel}</strong> can be updated from <code>v{fromVersion}</code>
42
+ to <code>v{toVersion}</code>.
43
+ </p>
44
+ <p class="hint">Package id: <code>{appId}</code></p>
45
+ {#if error}<p class="error">{error}</p>{/if}
46
+ <div class="actions">
47
+ <button type="button" class="primary" onclick={confirm} disabled={busy}>
48
+ {busy ? 'Updating…' : 'Update'}
49
+ </button>
50
+ <button type="button" onclick={close} disabled={busy}>Cancel</button>
51
+ </div>
52
+ </div>
53
+
54
+ <style>
55
+ .app-update-modal {
56
+ padding: 16px 20px;
57
+ max-width: 460px;
58
+ color: var(--shell-fg);
59
+ background: var(--shell-bg);
60
+ font: inherit;
61
+ }
62
+ h2 { margin: 0 0 8px; font-size: 16px; }
63
+ p { margin: 4px 0; font-size: 13px; }
64
+ .hint { color: var(--shell-fg-muted); font-size: 12px; }
65
+ .error { color: var(--shell-error, #c33); }
66
+ code {
67
+ font-family: var(--shell-font-mono, monospace);
68
+ background: var(--shell-bg-elevated);
69
+ padding: 0 4px;
70
+ border-radius: var(--shell-radius-sm, 3px);
71
+ }
72
+ .actions { display: flex; gap: 8px; margin-top: 16px; }
73
+ .actions button {
74
+ background: var(--shell-bg-elevated);
75
+ color: var(--shell-fg);
76
+ border: 1px solid var(--shell-border);
77
+ border-radius: var(--shell-radius-sm, 3px);
78
+ padding: 6px 14px; font: inherit; cursor: pointer;
79
+ }
80
+ .actions button.primary {
81
+ background: var(--shell-accent);
82
+ color: #fff;
83
+ border-color: var(--shell-accent);
84
+ }
85
+ .actions button:hover { border-color: var(--shell-accent); }
86
+ .actions button:disabled { opacity: 0.5; cursor: not-allowed; }
87
+ </style>
@@ -0,0 +1,11 @@
1
+ interface Props {
2
+ appId: string;
3
+ appLabel: string;
4
+ fromVersion: string;
5
+ toVersion: string;
6
+ onConfirm: () => Promise<void>;
7
+ close: () => void;
8
+ }
9
+ declare const AppUpdateAvailableModal: import("svelte").Component<Props, {}, "">;
10
+ type AppUpdateAvailableModal = ReturnType<typeof AppUpdateAvailableModal>;
11
+ export default AppUpdateAvailableModal;
@@ -10,7 +10,7 @@
10
10
  import { storeContext } from './storeShard.svelte';
11
11
  import { uninstallPackage } from '../../registry/installer';
12
12
  import { serverUninstallPackage } from '../../env/client';
13
- import PermissionConfirmModal from './PermissionConfirmModal.svelte';
13
+ import { openPermissionConfirmModal } from './permissionConfirm';
14
14
  import type { InstalledPackage } from '../../registry/types';
15
15
 
16
16
  const ctx = storeContext;
@@ -19,14 +19,6 @@
19
19
  let updatingIds = $state<Set<string>>(new Set());
20
20
  let updateError = $state<string | null>(null);
21
21
 
22
- let updateModal = $state<null | {
23
- pkg: InstalledPackage;
24
- toVersion: string;
25
- added: string[];
26
- removed: string[];
27
- resolve: (ok: boolean) => void;
28
- }>(null);
29
-
30
22
  async function handleUninstall(id: string) {
31
23
  if (uninstallingIds.has(id)) return;
32
24
 
@@ -46,29 +38,16 @@
46
38
 
47
39
  async function handleUpdate(id: string) {
48
40
  if (updatingIds.has(id)) return;
49
-
50
41
  updatingIds = new Set([...updatingIds, id]);
51
42
  updateError = null;
52
-
53
43
  try {
54
- await ctx.updatePackage(id, (added, removed) => {
55
- return new Promise<boolean>((resolve) => {
56
- const pkg = ctx.state.ephemeral.installed.find(
57
- (p: InstalledPackage) => p.id === id,
58
- );
59
- const target = ctx.state.ephemeral.updatable[id];
60
- if (!pkg || !target) {
61
- resolve(true); // Falls through to the existing behavior.
62
- return;
63
- }
64
- updateModal = {
65
- pkg,
66
- toVersion: target.latest.version,
67
- added,
68
- removed,
69
- resolve,
70
- };
71
- });
44
+ await ctx.updatePackage(id, async (added, removed) => {
45
+ const pkg = ctx.state.ephemeral.installed.find(
46
+ (p: InstalledPackage) => p.id === id,
47
+ );
48
+ const target = ctx.state.ephemeral.updatable[id];
49
+ if (!pkg || !target) return true;
50
+ return openPermissionConfirmModal(pkg, target.latest.version, added, removed);
72
51
  });
73
52
  } catch (err) {
74
53
  updateError = err instanceof Error ? err.message : String(err);
@@ -79,20 +58,6 @@
79
58
  }
80
59
  }
81
60
 
82
- function confirmUpdate() {
83
- const m = updateModal;
84
- if (!m) return;
85
- updateModal = null;
86
- m.resolve(true);
87
- }
88
-
89
- function cancelUpdate() {
90
- const m = updateModal;
91
- if (!m) return;
92
- updateModal = null;
93
- m.resolve(false);
94
- }
95
-
96
61
  function handleRefresh() {
97
62
  ctx.refreshInstalled();
98
63
  }
@@ -164,17 +129,6 @@
164
129
  </ul>
165
130
  {/if}
166
131
 
167
- {#if updateModal}
168
- <PermissionConfirmModal
169
- mode="update"
170
- pkg={{ label: updateModal.pkg.id, version: updateModal.toVersion }}
171
- fromVersion={updateModal.pkg.version}
172
- added={updateModal.added}
173
- removed={updateModal.removed}
174
- onConfirm={confirmUpdate}
175
- onCancel={cancelUpdate}
176
- />
177
- {/if}
178
132
  </div>
179
133
 
180
134
  <style>