sh3-core 0.13.2 → 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 (60) hide show
  1. package/dist/actions/MenuButton.svelte +2 -1
  2. package/dist/actions/contextMenuModel.d.ts +1 -1
  3. package/dist/actions/contextMenuModel.js +2 -1
  4. package/dist/actions/dispatcher.svelte.d.ts +1 -1
  5. package/dist/actions/dispatcher.svelte.js +2 -1
  6. package/dist/actions/listActive.d.ts +1 -1
  7. package/dist/actions/listActive.js +2 -1
  8. package/dist/actions/listeners.d.ts +1 -1
  9. package/dist/actions/listeners.js +6 -5
  10. package/dist/actions/menuBarModel.js +3 -2
  11. package/dist/actions/paletteModel.js +2 -1
  12. package/dist/actions/resolveLabel.test.d.ts +1 -0
  13. package/dist/actions/resolveLabel.test.js +14 -0
  14. package/dist/actions/types.d.ts +12 -1
  15. package/dist/actions/types.js +7 -1
  16. package/dist/app/store/AppUpdateAvailableModal.svelte +87 -0
  17. package/dist/app/store/AppUpdateAvailableModal.svelte.d.ts +11 -0
  18. package/dist/app/store/InstalledView.svelte +8 -54
  19. package/dist/app/store/UninstallAppDialog.svelte +86 -0
  20. package/dist/app/store/UninstallAppDialog.svelte.d.ts +10 -0
  21. package/dist/app/store/permissionConfirm.d.ts +4 -0
  22. package/dist/app/store/permissionConfirm.js +28 -0
  23. package/dist/app/store/storeShard.svelte.d.ts +8 -1
  24. package/dist/app/store/storeShard.svelte.js +42 -9
  25. package/dist/app/store/updatePackage.test.d.ts +1 -0
  26. package/dist/app/store/updatePackage.test.js +34 -0
  27. package/dist/app/store/verbs.d.ts +1 -0
  28. package/dist/app/store/verbs.js +79 -5
  29. package/dist/app/store/verbs.test.d.ts +1 -0
  30. package/dist/app/store/verbs.test.js +56 -0
  31. package/dist/app-appearance/AppAppearanceModal.svelte +174 -0
  32. package/dist/app-appearance/AppAppearanceModal.svelte.d.ts +8 -0
  33. package/dist/app-appearance/appearanceShard.svelte.d.ts +2 -0
  34. package/dist/app-appearance/appearanceShard.svelte.js +61 -0
  35. package/dist/app-appearance/appearanceState.svelte.d.ts +15 -0
  36. package/dist/app-appearance/appearanceState.svelte.js +59 -0
  37. package/dist/app-appearance/appearanceState.test.d.ts +1 -0
  38. package/dist/app-appearance/appearanceState.test.js +30 -0
  39. package/dist/app-appearance/index.d.ts +3 -0
  40. package/dist/app-appearance/index.js +2 -0
  41. package/dist/app-appearance/types.d.ts +11 -0
  42. package/dist/app-appearance/types.js +1 -0
  43. package/dist/apps/types.d.ts +7 -0
  44. package/dist/assets/iconIds.generated.d.ts +2 -0
  45. package/dist/assets/iconIds.generated.js +154 -0
  46. package/dist/host.js +2 -1
  47. package/dist/primitives/widgets/IconPicker.svelte +115 -0
  48. package/dist/primitives/widgets/IconPicker.svelte.d.ts +9 -0
  49. package/dist/primitives/widgets/IconPicker.svelte.test.d.ts +1 -0
  50. package/dist/primitives/widgets/IconPicker.svelte.test.js +43 -0
  51. package/dist/projects-shard/ProjectManage.svelte +14 -4
  52. package/dist/sh3core-shard/ShellHome.svelte +64 -38
  53. package/dist/sh3core-shard/appActions.d.ts +13 -0
  54. package/dist/sh3core-shard/appActions.js +181 -0
  55. package/dist/sh3core-shard/appActions.test.d.ts +1 -0
  56. package/dist/sh3core-shard/appActions.test.js +25 -0
  57. package/dist/sh3core-shard/sh3coreShard.svelte.js +2 -0
  58. package/dist/version.d.ts +1 -1
  59. package/dist/version.js +1 -1
  60. package/package.json +2 -2
@@ -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 @@
1
+ export {};
@@ -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
+ }
@@ -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>
@@ -0,0 +1,86 @@
1
+ <script lang="ts">
2
+ /*
3
+ * Confirms uninstall of an installed app launched from the home-card
4
+ * Uninstall action. Modeled on DeleteProjectDialog. The actual uninstall
5
+ * (server + local installer) is delegated to the caller via onConfirm.
6
+ */
7
+
8
+ interface Props {
9
+ appId: string;
10
+ appLabel: string;
11
+ version: string;
12
+ onConfirm: () => Promise<void>;
13
+ close: () => void;
14
+ }
15
+
16
+ let { appId, appLabel, version, onConfirm, close }: Props = $props();
17
+ let busy = $state(false);
18
+ let error = $state<string | null>(null);
19
+
20
+ async function confirm() {
21
+ if (busy) return;
22
+ busy = true;
23
+ error = null;
24
+ try {
25
+ await onConfirm();
26
+ close();
27
+ } catch (e) {
28
+ error = (e as Error).message;
29
+ } finally {
30
+ busy = false;
31
+ }
32
+ }
33
+ </script>
34
+
35
+ <div class="uninstall-dialog">
36
+ <h2>Uninstall {appLabel}?</h2>
37
+ <p class="hint">
38
+ Package id: <code>{appId}</code> · current version: <code>v{version}</code>
39
+ </p>
40
+ <p>
41
+ The package will be removed from this server. App data stored in your
42
+ browser will remain until you clear it manually.
43
+ </p>
44
+ {#if error}<p class="error">{error}</p>{/if}
45
+ <div class="actions">
46
+ <button type="button" class="danger" onclick={confirm} disabled={busy}>
47
+ {busy ? 'Uninstalling…' : 'Uninstall'}
48
+ </button>
49
+ <button type="button" onclick={close} disabled={busy}>Cancel</button>
50
+ </div>
51
+ </div>
52
+
53
+ <style>
54
+ .uninstall-dialog {
55
+ padding: 16px 20px;
56
+ max-width: 460px;
57
+ color: var(--shell-fg);
58
+ background: var(--shell-bg);
59
+ font: inherit;
60
+ }
61
+ h2 { margin: 0 0 8px; font-size: 16px; }
62
+ p { margin: 4px 0; font-size: 13px; }
63
+ .hint { color: var(--shell-fg-muted); font-size: 12px; }
64
+ .error { color: var(--shell-error, #c33); }
65
+ code {
66
+ font-family: var(--shell-font-mono, monospace);
67
+ background: var(--shell-bg-elevated);
68
+ padding: 0 4px;
69
+ border-radius: var(--shell-radius-sm, 3px);
70
+ }
71
+ .actions { display: flex; gap: 8px; margin-top: 16px; }
72
+ .actions button {
73
+ background: var(--shell-bg-elevated);
74
+ color: var(--shell-fg);
75
+ border: 1px solid var(--shell-border);
76
+ border-radius: var(--shell-radius-sm, 3px);
77
+ padding: 6px 14px; font: inherit; cursor: pointer;
78
+ }
79
+ .actions button.danger {
80
+ background: var(--shell-error, #c33);
81
+ color: #fff;
82
+ border-color: var(--shell-error, #c33);
83
+ }
84
+ .actions button:hover { border-color: var(--shell-accent); }
85
+ .actions button:disabled { opacity: 0.5; cursor: not-allowed; }
86
+ </style>
@@ -0,0 +1,10 @@
1
+ interface Props {
2
+ appId: string;
3
+ appLabel: string;
4
+ version: string;
5
+ onConfirm: () => Promise<void>;
6
+ close: () => void;
7
+ }
8
+ declare const UninstallAppDialog: import("svelte").Component<Props, {}, "">;
9
+ type UninstallAppDialog = ReturnType<typeof UninstallAppDialog>;
10
+ export default UninstallAppDialog;
@@ -0,0 +1,4 @@
1
+ export declare function openPermissionConfirmModal(pkg: {
2
+ label: string;
3
+ version: string;
4
+ }, toVersion: string, added: string[], removed: string[]): Promise<boolean>;
@@ -0,0 +1,28 @@
1
+ /*
2
+ * Shared permission-diff confirmation flow for the `update` path. Opens
3
+ * PermissionConfirmModal via modalManager and resolves to the user's choice.
4
+ * Used by InstalledView's Update button and the home-card "Check for
5
+ * updates" context-menu action.
6
+ */
7
+ import { modalManager } from '../../overlays/modal';
8
+ import PermissionConfirmModal from './PermissionConfirmModal.svelte';
9
+ export function openPermissionConfirmModal(pkg, toVersion, added, removed) {
10
+ return new Promise((resolve) => {
11
+ const props = {
12
+ mode: 'update',
13
+ pkg: { label: pkg.label, version: toVersion },
14
+ fromVersion: pkg.version,
15
+ added,
16
+ removed,
17
+ onConfirm: () => {
18
+ handle.close();
19
+ resolve(true);
20
+ },
21
+ onCancel: () => {
22
+ handle.close();
23
+ resolve(false);
24
+ },
25
+ };
26
+ const handle = modalManager.open(PermissionConfirmModal, props);
27
+ });
28
+ }
@@ -3,6 +3,12 @@ import type { StateZones } from '../../state/zones.svelte';
3
3
  import type { ResolvedPackage } from '../../registry/client';
4
4
  import type { InstalledPackage } from '../../registry/types';
5
5
  import type { EnvState } from '../../env/types';
6
+ /**
7
+ * Pick a version entry from a resolved package. When `requested` is
8
+ * undefined returns `latest`; when set, finds the matching entry by
9
+ * exact version string. Throws if the requested version is absent.
10
+ */
11
+ export declare function pickVersion(pkg: ResolvedPackage, requested: string | undefined): typeof pkg.latest;
6
12
  /**
7
13
  * Compute added and removed permissions between two manifest snapshots.
8
14
  * Order within each array follows the input order of `newPerms` (for added)
@@ -34,7 +40,8 @@ export interface StoreContext {
34
40
  isAdmin: boolean;
35
41
  refreshCatalog(): Promise<void>;
36
42
  refreshInstalled(): Promise<void>;
37
- updatePackage(id: string, confirmPermissionChange?: (added: string[], removed: string[]) => Promise<boolean>): Promise<void>;
43
+ updatePackage(id: string, confirmPermissionChange?: (added: string[], removed: string[]) => Promise<boolean>, version?: string): Promise<void>;
44
+ uninstallPackage(id: string): Promise<void>;
38
45
  addRegistry(url: string): Promise<void>;
39
46
  removeRegistry(url: string): Promise<void>;
40
47
  }