sh3-core 0.17.2 → 0.19.1

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 (97) hide show
  1. package/dist/Sh3.svelte +59 -4
  2. package/dist/actions/CommandPalette.svelte +1 -2
  3. package/dist/actions/listeners.js +12 -1
  4. package/dist/api.d.ts +4 -0
  5. package/dist/app/store/storeShard.svelte.js +1 -21
  6. package/dist/app/store/version.d.ts +11 -0
  7. package/dist/app/store/version.js +39 -0
  8. package/dist/app/store/version.test.d.ts +1 -0
  9. package/dist/app/store/version.test.js +44 -0
  10. package/dist/apps/lifecycle.d.ts +6 -0
  11. package/dist/apps/lifecycle.js +5 -2
  12. package/dist/apps/lifecycle.test.js +30 -0
  13. package/dist/apps/types.d.ts +12 -0
  14. package/dist/assets/iconIds.generated.d.ts +1 -1
  15. package/dist/assets/iconIds.generated.js +5 -0
  16. package/dist/assets/icons.svg +31 -0
  17. package/dist/auth/auth.svelte.js +18 -8
  18. package/dist/auth/types.d.ts +6 -0
  19. package/dist/chrome/CompactChrome.svelte +54 -20
  20. package/dist/chrome/CompactChrome.svelte.test.js +112 -5
  21. package/dist/createShell.d.ts +9 -0
  22. package/dist/createShell.js +20 -7
  23. package/dist/createShell.remoteAuth.test.d.ts +1 -0
  24. package/dist/createShell.remoteAuth.test.js +71 -0
  25. package/dist/documents/http-backend.js +12 -11
  26. package/dist/env/client.js +11 -5
  27. package/dist/files/types.d.ts +106 -0
  28. package/dist/files/types.js +1 -0
  29. package/dist/gestures/gestureRegistry.d.ts +6 -0
  30. package/dist/gestures/gestureRegistry.js +190 -0
  31. package/dist/gestures/gestureRegistry.test.d.ts +1 -0
  32. package/dist/gestures/gestureRegistry.test.js +120 -0
  33. package/dist/gestures/index.d.ts +6 -0
  34. package/dist/gestures/index.js +12 -0
  35. package/dist/gestures/pointerClaim.d.ts +7 -0
  36. package/dist/gestures/pointerClaim.js +36 -0
  37. package/dist/gestures/pointerClaim.test.d.ts +1 -0
  38. package/dist/gestures/pointerClaim.test.js +64 -0
  39. package/dist/gestures/types.d.ts +83 -0
  40. package/dist/gestures/types.js +1 -0
  41. package/dist/host-entry.d.ts +1 -0
  42. package/dist/host-entry.js +1 -0
  43. package/dist/layout/LayoutRenderer.browser.test.js +15 -3
  44. package/dist/layout/LayoutRenderer.svelte +16 -3
  45. package/dist/layout/LayoutRenderer.svelte.d.ts +2 -0
  46. package/dist/layout/__screenshots__/LayoutRenderer.browser.test.ts/LayoutRenderer-browser---E-3-splitter-drag-updates-split-sizes-when-the-splitter-handle-is-dragged-1.png +0 -0
  47. package/dist/layout/__screenshots__/LayoutRenderer.browser.test.ts/LayoutRenderer-browser---E-5-splitter-collapse-toggle-toggles-collapsed-i--on-double-click-1.png +0 -0
  48. package/dist/layout/__screenshots__/LayoutRenderer.browser.test.ts/LayoutRenderer-browser---E-6-fixed-slots-hides-the-collapse-widget-on-a-fixed-pane-but-keeps-it-on-panes-with-a-non-fixed-neighbor-1.png +0 -0
  49. package/dist/layout/compact/CarouselTabs.svelte +362 -0
  50. package/dist/layout/compact/CarouselTabs.svelte.d.ts +10 -0
  51. package/dist/layout/compact/CarouselTabs.svelte.test.d.ts +1 -0
  52. package/dist/layout/compact/CarouselTabs.svelte.test.js +300 -0
  53. package/dist/layout/compact/CompactRenderer.svelte +1 -1
  54. package/dist/layout/compact/CompactRenderer.svelte.test.js +49 -0
  55. package/dist/layout/compact/derive.js +2 -0
  56. package/dist/layout/compact/derive.test.js +37 -0
  57. package/dist/layout/compact/enrichCarousels.d.ts +8 -0
  58. package/dist/layout/compact/enrichCarousels.js +44 -0
  59. package/dist/layout/compact/enrichCarousels.test.d.ts +1 -0
  60. package/dist/layout/compact/enrichCarousels.test.js +88 -0
  61. package/dist/layout/compact/types.d.ts +3 -0
  62. package/dist/layout/drag.svelte.js +13 -0
  63. package/dist/layout/store.schemaVersion.test.js +2 -2
  64. package/dist/layout/types.d.ts +9 -1
  65. package/dist/layout/types.js +1 -1
  66. package/dist/layout/types.test.d.ts +1 -0
  67. package/dist/layout/types.test.js +26 -0
  68. package/dist/overlays/ModalFrame.svelte +3 -1
  69. package/dist/overlays/ModalFrame.svelte.d.ts +1 -0
  70. package/dist/overlays/floatDismiss.js +5 -0
  71. package/dist/overlays/focusTrap.d.ts +11 -1
  72. package/dist/overlays/focusTrap.js +11 -9
  73. package/dist/overlays/modal.js +1 -0
  74. package/dist/overlays/popup.js +4 -0
  75. package/dist/overlays/types.d.ts +9 -0
  76. package/dist/primitives/Button.svelte +18 -0
  77. package/dist/primitives/Button.svelte.d.ts +6 -0
  78. package/dist/primitives/ResizableSplitter.svelte +71 -11
  79. package/dist/primitives/ResizableSplitter.svelte.d.ts +8 -0
  80. package/dist/primitives/ResizableSplitter.svelte.test.d.ts +1 -0
  81. package/dist/primitives/ResizableSplitter.svelte.test.js +74 -0
  82. package/dist/server-shard/types.d.ts +2 -1
  83. package/dist/shards/activate.svelte.js +16 -0
  84. package/dist/shards/ctx-fetch.test.d.ts +1 -0
  85. package/dist/shards/ctx-fetch.test.js +136 -0
  86. package/dist/shards/types.d.ts +29 -0
  87. package/dist/transport/apiFetch.d.ts +1 -0
  88. package/dist/transport/apiFetch.js +65 -0
  89. package/dist/transport/apiFetch.test.d.ts +1 -0
  90. package/dist/transport/apiFetch.test.js +37 -0
  91. package/dist/transport/authToken.d.ts +2 -0
  92. package/dist/transport/authToken.js +53 -0
  93. package/dist/transport/authToken.test.d.ts +1 -0
  94. package/dist/transport/authToken.test.js +33 -0
  95. package/dist/version.d.ts +1 -1
  96. package/dist/version.js +1 -1
  97. package/package.json +1 -1
package/dist/Sh3.svelte CHANGED
@@ -29,6 +29,9 @@
29
29
  import CompactChrome from './chrome/CompactChrome.svelte';
30
30
  import CompactRenderer from './layout/compact/CompactRenderer.svelte';
31
31
  import { sh3 } from './sh3Runtime.svelte';
32
+ import { claim, revoke } from './gestures/pointerClaim';
33
+
34
+ let contentEl: HTMLElement | undefined = $state();
32
35
 
33
36
  const authenticated = $derived(isAuthenticated());
34
37
  const user = $derived(getUser());
@@ -63,6 +66,43 @@
63
66
  const stop = startServerSideStream();
64
67
  return stop;
65
68
  });
69
+
70
+ // Register left/right edge zones as priority:'edge' pointer claims so that
71
+ // any shard with a priority:'normal' claim automatically beats the shell's
72
+ // edge-swipe gesture (carousel navigation, future side-panel reveals).
73
+ const EDGE_PX = 24;
74
+ $effect(() => {
75
+ const el = contentEl;
76
+ if (!el) return;
77
+
78
+ const edgePointers = new Set<number>();
79
+
80
+ function onPointerDown(e: PointerEvent): void {
81
+ const rect = el.getBoundingClientRect();
82
+ const local = e.clientX - rect.left;
83
+ if (local >= EDGE_PX && local <= rect.width - EDGE_PX) return;
84
+ const granted = claim(e.pointerId, { ownerId: 'sh3:edge', axis: 'x', priority: 'edge', depth: 0 });
85
+ if (granted) edgePointers.add(e.pointerId);
86
+ }
87
+
88
+ function onPointerEnd(e: PointerEvent): void {
89
+ if (!edgePointers.has(e.pointerId)) return;
90
+ revoke(e.pointerId, 'sh3:edge');
91
+ edgePointers.delete(e.pointerId);
92
+ }
93
+
94
+ el.addEventListener('pointerdown', onPointerDown);
95
+ el.addEventListener('pointerup', onPointerEnd);
96
+ el.addEventListener('pointercancel', onPointerEnd);
97
+
98
+ return () => {
99
+ el.removeEventListener('pointerdown', onPointerDown);
100
+ el.removeEventListener('pointerup', onPointerEnd);
101
+ el.removeEventListener('pointercancel', onPointerEnd);
102
+ for (const id of edgePointers) revoke(id, 'sh3:edge');
103
+ edgePointers.clear();
104
+ };
105
+ });
66
106
  </script>
67
107
 
68
108
  <div class="sh3" data-sh3-viewport={viewportClass}>
@@ -106,7 +146,7 @@
106
146
 
107
147
  <GuestBanner />
108
148
 
109
- <main class="sh3-content" data-sh3-region="content" data-sh3-layer="0">
149
+ <main class="sh3-content" data-sh3-region="content" data-sh3-layer="0" bind:this={contentEl}>
110
150
  {#if viewportClass === 'desktop'}
111
151
  <LayoutRenderer />
112
152
  {:else}
@@ -114,9 +154,11 @@
114
154
  {/if}
115
155
  </main>
116
156
 
117
- <footer class="sh3-statusbar" data-sh3-region="statusbar">
118
- <!-- alpha tag moved to Sh3Home title row -->
119
- </footer>
157
+ {#if viewportClass === 'desktop'}
158
+ <footer class="sh3-statusbar" data-sh3-region="statusbar">
159
+ <!-- alpha tag moved to Sh3Home title row -->
160
+ </footer>
161
+ {/if}
120
162
 
121
163
  <OverlayRoots />
122
164
 
@@ -138,6 +180,19 @@
138
180
  position: relative;
139
181
  background: var(--sh3-grad-bg, var(--sh3-bg));
140
182
  color: var(--sh3-fg);
183
+ /* Edge-to-edge platforms (Tauri Android in particular) draw the
184
+ * webview under the system status / navigation bars. env() resolves
185
+ * to 0 elsewhere, so this is a no-op on desktop browsers and
186
+ * windowed Tauri builds. box-sizing on body/html is `content-box`
187
+ * by default, so padding here shrinks the grid area accordingly. */
188
+ box-sizing: border-box;
189
+ padding-top: env(safe-area-inset-top);
190
+ padding-bottom: env(safe-area-inset-bottom);
191
+ padding-left: env(safe-area-inset-left);
192
+ padding-right: env(safe-area-inset-right);
193
+ }
194
+ .sh3[data-sh3-viewport='compact'] {
195
+ grid-template-rows: var(--sh3-tabbar-height) auto 1fr;
141
196
  }
142
197
 
143
198
  .sh3-tabbar {
@@ -35,9 +35,8 @@
35
35
  }
36
36
  </script>
37
37
 
38
- <!-- svelte-ignore a11y_autofocus -->
39
38
  <div class="sh3-palette" role="dialog" aria-label="Command palette" aria-modal="true" tabindex="-1" onkeydown={onKeydown}>
40
- <input class="sh3-palette-input hell-base-input" bind:value={query} autofocus placeholder="Command…" />
39
+ <input class="sh3-palette-input hell-base-input" bind:value={query} placeholder="Command…" />
41
40
  <div class="sh3-palette-list" role="listbox">
42
41
  {#each ranked as item, i (item.id)}
43
42
  <button
@@ -349,7 +349,18 @@ export function openPalette(opts) {
349
349
  }
350
350
  },
351
351
  onClose: () => handle.close(),
352
- }, { dismissOnBackdrop: true });
352
+ }, {
353
+ dismissOnBackdrop: true,
354
+ // On touch-only devices, autofocusing the palette input pops the
355
+ // on-screen keyboard before the user has expressed intent to type
356
+ // — the toolbar-button entry path expects to tap a result, not
357
+ // type. Discriminant: a touch-only device (no hover, coarse
358
+ // pointer) — desktop browsers and laptops with touchscreens
359
+ // still report `(any-hover: hover)`.
360
+ initialFocus: !(typeof window !== 'undefined'
361
+ && typeof window.matchMedia === 'function'
362
+ && window.matchMedia('(hover: none) and (pointer: coarse)').matches),
363
+ });
353
364
  if (previousFocus) {
354
365
  const innerClose = handle.close;
355
366
  handle.close = () => {
package/dist/api.d.ts CHANGED
@@ -19,6 +19,8 @@ export type { EnvState } from './env/types';
19
19
  export type { App, AppManifest, SourceApp, SourceAppManifest, AppContext, } from './apps/types';
20
20
  export { listRegisteredApps, getActiveApp } from './apps/registry.svelte';
21
21
  export { launchApp, returnToHome, unregisterApp } from './apps/lifecycle';
22
+ export type { LaunchAppOptions } from './apps/lifecycle';
23
+ export type { FileRef, FileHandlerDescriptor, FileHandlerMatch, FileHandlerHeader, FileHandlerPattern, FileHandlerOpen, } from './files/types';
22
24
  export { pushNavEntry } from './navigation';
23
25
  export type { NavEntry, NavEntryHandle } from './navigation';
24
26
  export { spawnSatellite } from './sh3Api/window';
@@ -61,6 +63,8 @@ export type { RunVerbOpts, RunVerbResult } from './runtime';
61
63
  export { registerShellMode } from './shell-shard/registerShellMode';
62
64
  export type { ShellModeDescriptor, ShellModeOutput, ShellModeDispatchHandler, ShellModeDispatchInput, ShellModeRunsOn, RichEntryHandle, StreamHandle, } from './shell-shard/contract';
63
65
  export { SHELL_MODE_CONTRIBUTION_POINT } from './shell-shard/contract';
66
+ export type { GestureRegistry, GestureHandle } from './gestures';
67
+ export type { GestureType, Axis, ClaimPriority, ClaimEntry, PanEvent, ScrollEvent, ButtonEvent, PanOptions, DragOptions, ButtonOptions, ScrollOptions, } from './gestures/types';
64
68
  export { VERSION } from './version';
65
69
  export declare const FRAMEWORK_SHARD_IDS: readonly string[];
66
70
  export { setTokenOverrides, clearTokenOverrides, getTokenOverrides, } from './theme';
@@ -26,27 +26,7 @@ import { extractBundlePermissions } from '../../registry/permission-descriptions
26
26
  import { serverInstallPackage, fetchServerPackages, serverUninstallPackage } from '../../env/client';
27
27
  import { VERSION } from '../../version';
28
28
  import { installVerb, uninstallVerb, appinfoVerb, updateVerb } from './verbs';
29
- /**
30
- * Compare two semver-like version strings.
31
- * Returns true only if `available` is strictly greater than `installed`.
32
- * Compares major.minor.patch left-to-right as integers.
33
- * Non-numeric segments are treated as 0.
34
- */
35
- function isNewerVersion(available, installed) {
36
- var _a, _b;
37
- const a = available.split('.').map((s) => parseInt(s, 10) || 0);
38
- const b = installed.split('.').map((s) => parseInt(s, 10) || 0);
39
- const len = Math.max(a.length, b.length);
40
- for (let i = 0; i < len; i++) {
41
- const av = (_a = a[i]) !== null && _a !== void 0 ? _a : 0;
42
- const bv = (_b = b[i]) !== null && _b !== void 0 ? _b : 0;
43
- if (av > bv)
44
- return true;
45
- if (av < bv)
46
- return false;
47
- }
48
- return false;
49
- }
29
+ import { isNewerVersion } from './version';
50
30
  /**
51
31
  * Pick a version entry from a resolved package. When `requested` is
52
32
  * undefined returns `latest`; when set, finds the matching entry by
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Compare two artifact version strings for the app store update check.
3
+ *
4
+ * Handles semver build-metadata suffixes (`+N`): per ADR-013 the build suffix
5
+ * represents artifact iterations of the same release, so `1.0.0+1 > 1.0.0`
6
+ * and `1.0.0+2 > 1.0.0+1`. A canonical release (no suffix) is not considered
7
+ * newer than a suffixed one.
8
+ *
9
+ * Returns true only when `available` is strictly greater than `installed`.
10
+ */
11
+ export declare function isNewerVersion(available: string, installed: string): boolean;
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Compare two artifact version strings for the app store update check.
3
+ *
4
+ * Handles semver build-metadata suffixes (`+N`): per ADR-013 the build suffix
5
+ * represents artifact iterations of the same release, so `1.0.0+1 > 1.0.0`
6
+ * and `1.0.0+2 > 1.0.0+1`. A canonical release (no suffix) is not considered
7
+ * newer than a suffixed one.
8
+ *
9
+ * Returns true only when `available` is strictly greater than `installed`.
10
+ */
11
+ export function isNewerVersion(available, installed) {
12
+ var _a, _b;
13
+ const [aRelease = '', aBuild = ''] = available.split('+');
14
+ const [bRelease = '', bBuild = ''] = installed.split('+');
15
+ const a = aRelease.split('.').map((s) => parseInt(s, 10) || 0);
16
+ const b = bRelease.split('.').map((s) => parseInt(s, 10) || 0);
17
+ const len = Math.max(a.length, b.length);
18
+ for (let i = 0; i < len; i++) {
19
+ const av = (_a = a[i]) !== null && _a !== void 0 ? _a : 0;
20
+ const bv = (_b = b[i]) !== null && _b !== void 0 ? _b : 0;
21
+ if (av > bv)
22
+ return true;
23
+ if (av < bv)
24
+ return false;
25
+ }
26
+ // Release versions are equal — compare build metadata.
27
+ // absent (canonical) < any suffix; numeric suffix compared numerically.
28
+ if (aBuild === bBuild)
29
+ return false;
30
+ if (aBuild === '')
31
+ return false; // canonical is not newer than suffixed
32
+ if (bBuild === '')
33
+ return true; // suffixed is newer than canonical
34
+ const an = parseInt(aBuild, 10);
35
+ const bn = parseInt(bBuild, 10);
36
+ if (!isNaN(an) && !isNaN(bn))
37
+ return an > bn;
38
+ return aBuild > bBuild; // lexicographic fallback for non-numeric suffixes
39
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,44 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { isNewerVersion } from './version';
3
+ describe('isNewerVersion', () => {
4
+ describe('standard semver (no build metadata)', () => {
5
+ it('returns true when available has higher major', () => {
6
+ expect(isNewerVersion('2.0.0', '1.9.9')).toBe(true);
7
+ });
8
+ it('returns true when available has higher minor', () => {
9
+ expect(isNewerVersion('0.2.0', '0.1.9')).toBe(true);
10
+ });
11
+ it('returns true when available has higher patch', () => {
12
+ expect(isNewerVersion('0.1.1', '0.1.0')).toBe(true);
13
+ });
14
+ it('returns false when available is lower', () => {
15
+ expect(isNewerVersion('0.1.0', '1.0.0')).toBe(false);
16
+ });
17
+ it('returns false when versions are equal', () => {
18
+ expect(isNewerVersion('0.1.0', '0.1.0')).toBe(false);
19
+ });
20
+ });
21
+ describe('build metadata suffix (+N)', () => {
22
+ it('returns true when available has suffix and installed does not', () => {
23
+ expect(isNewerVersion('0.1.0+1', '0.1.0')).toBe(true);
24
+ });
25
+ it('returns false when available has no suffix and installed does', () => {
26
+ expect(isNewerVersion('0.1.0', '0.1.0+1')).toBe(false);
27
+ });
28
+ it('returns true when available has higher numeric suffix', () => {
29
+ expect(isNewerVersion('0.1.0+2', '0.1.0+1')).toBe(true);
30
+ });
31
+ it('returns false when available has lower numeric suffix', () => {
32
+ expect(isNewerVersion('0.1.0+1', '0.1.0+2')).toBe(false);
33
+ });
34
+ it('returns false when suffixes are equal', () => {
35
+ expect(isNewerVersion('0.1.0+1', '0.1.0+1')).toBe(false);
36
+ });
37
+ it('higher build suffix does not beat higher release version', () => {
38
+ expect(isNewerVersion('0.1.0+99', '0.2.0')).toBe(false);
39
+ });
40
+ it('higher release version beats any build suffix', () => {
41
+ expect(isNewerVersion('0.2.0', '0.1.0+99')).toBe(true);
42
+ });
43
+ });
44
+ });
@@ -25,6 +25,12 @@ export interface LaunchAppOptions {
25
25
  * "home" view to return to first.
26
26
  */
27
27
  skipSwitchToHome?: boolean;
28
+ /**
29
+ * Arbitrary data passed to the app at launch time. Available as
30
+ * `ctx.args` in `App.activate` and `App.onAppReady`.
31
+ * Ignored on re-entry (resume) — the app's existing context is reused.
32
+ */
33
+ args?: Record<string, unknown>;
28
34
  }
29
35
  /**
30
36
  * Launch an app by id. Activates all required shards (idempotent for
@@ -12,6 +12,7 @@
12
12
  * return-to-home (null). Boot reads it to decide whether to auto-launch.
13
13
  */
14
14
  import { createStateZones } from '../state/zones.svelte';
15
+ import { createGestureRegistry } from '../gestures';
15
16
  import { activateShard, deactivateShard, getShardContext, registeredShards, } from '../shards/activate.svelte';
16
17
  import { attachApp, acquireAppSlotHolds, detachApp, switchToApp, switchToHome, } from '../layout/store.svelte';
17
18
  import { activeApp, breadcrumbApp, getRegisteredApp, registeredApps } from './registry.svelte';
@@ -60,7 +61,7 @@ function resolveLaunchScope() {
60
61
  var _a;
61
62
  return (_a = sessionState.activeProjectId) !== null && _a !== void 0 ? _a : getActiveScopeId();
62
63
  }
63
- function getOrCreateAppContext(appId, scopeId) {
64
+ function getOrCreateAppContext(appId, scopeId, args) {
64
65
  var _a;
65
66
  let ctx = appContexts.get(appId);
66
67
  if (!ctx) {
@@ -68,10 +69,12 @@ function getOrCreateAppContext(appId, scopeId) {
68
69
  const scope = scopeId !== null && scopeId !== void 0 ? scopeId : resolveLaunchScope();
69
70
  ctx = {
70
71
  scopeId: scope,
72
+ args: args !== null && args !== void 0 ? args : {},
71
73
  state: (schema) => createStateZones(`__app__:${appId}:scope:${scope}`, schema),
72
74
  zones: ((_a = app === null || app === void 0 ? void 0 : app.manifest.permissions) === null || _a === void 0 ? void 0 : _a.includes(PERMISSION_STATE_MANAGE))
73
75
  ? createZoneManager()
74
76
  : undefined,
77
+ gestures: createGestureRegistry(typeof document !== 'undefined' ? document.body : null),
75
78
  };
76
79
  appContexts.set(appId, ctx);
77
80
  }
@@ -156,7 +159,7 @@ export async function launchApp(id, opts = {}) {
156
159
  // refcount holds on the app's slots now (pool's factory lookup
157
160
  // happens in a microtask from this call).
158
161
  acquireAppSlotHolds();
159
- void ((_f = app.activate) === null || _f === void 0 ? void 0 : _f.call(app, getOrCreateAppContext(id)));
162
+ void ((_f = app.activate) === null || _f === void 0 ? void 0 : _f.call(app, getOrCreateAppContext(id, undefined, opts.args)));
160
163
  activeApp.id = id;
161
164
  setActiveApp(id, new Set((_g = app.manifest.requiredShards) !== null && _g !== void 0 ? _g : []));
162
165
  void loadUserBindings(id).then(setUserBindings);
@@ -630,3 +630,33 @@ describe('launchApp — scenario D.1 skipLastApp option', () => {
630
630
  expect(readLastApp()).toBe('app-default-last');
631
631
  });
632
632
  });
633
+ // ---------------------------------------------------------------------------
634
+ // Launch args — ctx.args
635
+ // ---------------------------------------------------------------------------
636
+ describe('launchApp — ctx.args', () => {
637
+ beforeEach(resetFramework);
638
+ it('passes args to activate via ctx.args', async () => {
639
+ let receivedArgs;
640
+ const shard = makeShard({ manifest: makeShardManifest({ id: 'shard-args' }) });
641
+ registerShard(shard);
642
+ const app = makeApp({
643
+ manifest: makeAppManifest({ id: 'app-args', requiredShards: ['shard-args'] }),
644
+ activate: (ctx) => { receivedArgs = ctx.args; },
645
+ });
646
+ registerApp(app);
647
+ await launchApp('app-args', { args: { file: { path: '/doc.svg', tenantId: 'alice', binary: false } } });
648
+ expect(receivedArgs).toEqual({ file: { path: '/doc.svg', tenantId: 'alice', binary: false } });
649
+ });
650
+ it('defaults ctx.args to empty object when no args passed', async () => {
651
+ let receivedArgs;
652
+ const shard = makeShard({ manifest: makeShardManifest({ id: 'shard-noargs' }) });
653
+ registerShard(shard);
654
+ const app = makeApp({
655
+ manifest: makeAppManifest({ id: 'app-noargs', requiredShards: ['shard-noargs'] }),
656
+ activate: (ctx) => { receivedArgs = ctx.args; },
657
+ });
658
+ registerApp(app);
659
+ await launchApp('app-noargs');
660
+ expect(receivedArgs).toEqual({});
661
+ });
662
+ });
@@ -1,6 +1,7 @@
1
1
  import type { LayoutNode, LayoutTree, LayoutPreset } from '../layout/types';
2
2
  import type { ZoneSchema, ZoneManager } from '../state/types';
3
3
  import type { StateZones } from '../state/zones.svelte';
4
+ import type { GestureRegistry } from '../gestures';
4
5
  /**
5
6
  * One menu bar container ("File", "Edit", etc.). Apps declare these in
6
7
  * `AppManifest.menus`; when omitted, sh3-core uses DEFAULT_MENU_CONTAINERS.
@@ -95,6 +96,12 @@ export interface AppContext {
95
96
  * scope and cannot reach across scopes — exiting a scope unloads the app.
96
97
  */
97
98
  scopeId: string;
99
+ /**
100
+ * Arguments supplied by the caller at launch time via `LaunchAppOptions.args`.
101
+ * Defaults to `{}` when the app is launched without explicit args.
102
+ * Read-only — apps observe but do not mutate this object.
103
+ */
104
+ readonly args: Record<string, unknown>;
98
105
  /**
99
106
  * App-scoped state zones. The shardId underneath is the app id plus the
100
107
  * scope, so `state({ workspace: { x: 0 } }).workspace.x = 1` persists to
@@ -108,6 +115,11 @@ export interface AppContext {
108
115
  * `if (ctx.zones)` before use.
109
116
  */
110
117
  zones?: ZoneManager;
118
+ /**
119
+ * Per-app gesture binding API. Register Pan, Drag, Button, and Scroll
120
+ * handlers that participate in the shell's pointer arbitration protocol.
121
+ */
122
+ gestures: GestureRegistry;
111
123
  }
112
124
  /**
113
125
  * An app module. An app is a composition document — it declares required
@@ -1,2 +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"];
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", "command", "compass", "component", "copy", "cpu", "crop", "crosshair", "crown", "dollar-sign", "download", "droplet", "ellipsis-vertical", "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", "menu", "minimize", "moon", "mouse-pointer", "move", "move-3d", "music", "navigation", "network", "notebook-pen", "palette", "panel-right", "panel-top", "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
2
  export type IconId = (typeof ICON_IDS)[number];
@@ -25,6 +25,7 @@ export const ICON_IDS = [
25
25
  'clipboard',
26
26
  'clipboard-paste',
27
27
  'clock',
28
+ 'command',
28
29
  'compass',
29
30
  'component',
30
31
  'copy',
@@ -35,6 +36,7 @@ export const ICON_IDS = [
35
36
  'dollar-sign',
36
37
  'download',
37
38
  'droplet',
39
+ 'ellipsis-vertical',
38
40
  'eraser',
39
41
  'euro',
40
42
  'external-link',
@@ -88,6 +90,7 @@ export const ICON_IDS = [
88
90
  'mail',
89
91
  'map',
90
92
  'maximize',
93
+ 'menu',
91
94
  'minimize',
92
95
  'moon',
93
96
  'mouse-pointer',
@@ -98,6 +101,8 @@ export const ICON_IDS = [
98
101
  'network',
99
102
  'notebook-pen',
100
103
  'palette',
104
+ 'panel-right',
105
+ 'panel-top',
101
106
  'pause',
102
107
  'pencil',
103
108
  'pipette',
@@ -1128,4 +1128,35 @@
1128
1128
  <path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4" />
1129
1129
  </symbol>
1130
1130
 
1131
+ <!-- lucide/menu -->
1132
+ <symbol id="menu" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
1133
+ <path d="M4 5h16" />
1134
+ <path d="M4 12h16" />
1135
+ <path d="M4 19h16" />
1136
+ </symbol>
1137
+
1138
+ <!-- lucide/panel-right -->
1139
+ <symbol id="panel-right" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
1140
+ <rect width="18" height="18" x="3" y="3" rx="2" />
1141
+ <path d="M15 3v18" />
1142
+ </symbol>
1143
+
1144
+ <!-- lucide/panel-top -->
1145
+ <symbol id="panel-top" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
1146
+ <rect width="18" height="18" x="3" y="3" rx="2" />
1147
+ <path d="M3 9h18" />
1148
+ </symbol>
1149
+
1150
+ <!-- lucide/command -->
1151
+ <symbol id="command" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
1152
+ <path d="M15 6v12a3 3 0 1 0 3-3H6a3 3 0 1 0 3 3V6a3 3 0 1 0-3 3h12a3 3 0 1 0-3-3" />
1153
+ </symbol>
1154
+
1155
+ <!-- lucide/ellipsis-vertical -->
1156
+ <symbol id="ellipsis-vertical" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
1157
+ <circle cx="12" cy="12" r="1" />
1158
+ <circle cx="12" cy="5" r="1" />
1159
+ <circle cx="12" cy="19" r="1" />
1160
+ </symbol>
1161
+
1131
1162
  </svg>
@@ -8,6 +8,8 @@
8
8
  *
9
9
  * .svelte.ts because it uses $state for reactive auth status.
10
10
  */
11
+ import { apiFetch } from '../transport/apiFetch';
12
+ import { setAuthToken } from '../transport/authToken';
11
13
  /** Reactive auth state. */
12
14
  let currentUser = $state(null);
13
15
  let currentSession = $state(null);
@@ -23,22 +25,28 @@ let authConfig = null;
23
25
  * after fetching /api/boot.
24
26
  */
25
27
  export function initFromBoot(url, config) {
28
+ var _a, _b;
26
29
  serverUrl = url;
27
30
  authConfig = config.auth;
28
31
  currentUser = config.user;
29
32
  currentSession = config.session;
30
33
  guest = !config.session && !config.user;
34
+ // Sync the cross-origin auth-token store. If the boot returned no
35
+ // session but localStorage still holds a stale token, drop it — the
36
+ // server already disowned it (otherwise it would have echoed the
37
+ // session back).
38
+ setAuthToken((_b = (_a = config.session) === null || _a === void 0 ? void 0 : _a.token) !== null && _b !== void 0 ? _b : null);
31
39
  }
32
40
  /**
33
41
  * Log in with username + password. On success, updates reactive state.
34
42
  * Returns { ok: true } or { ok: false, error: string }.
35
43
  */
36
44
  export async function login(username, password) {
45
+ var _a, _b;
37
46
  try {
38
- const res = await fetch(`${serverUrl}/api/auth/login`, {
47
+ const res = await apiFetch(`${serverUrl}/api/auth/login`, {
39
48
  method: 'POST',
40
49
  headers: { 'Content-Type': 'application/json' },
41
- credentials: 'include',
42
50
  body: JSON.stringify({ username, password }),
43
51
  });
44
52
  if (!res.ok) {
@@ -49,9 +57,10 @@ export async function login(username, password) {
49
57
  currentUser = body.user;
50
58
  currentSession = body.session;
51
59
  guest = false;
60
+ setAuthToken((_b = (_a = body.session) === null || _a === void 0 ? void 0 : _a.token) !== null && _b !== void 0 ? _b : null);
52
61
  return { ok: true };
53
62
  }
54
- catch (_a) {
63
+ catch (_c) {
55
64
  return { ok: false, error: 'Network error' };
56
65
  }
57
66
  }
@@ -60,11 +69,11 @@ export async function login(username, password) {
60
69
  * On success, auto-logs in and updates reactive state.
61
70
  */
62
71
  export async function register(username, password, displayName) {
72
+ var _a, _b;
63
73
  try {
64
- const res = await fetch(`${serverUrl}/api/auth/register`, {
74
+ const res = await apiFetch(`${serverUrl}/api/auth/register`, {
65
75
  method: 'POST',
66
76
  headers: { 'Content-Type': 'application/json' },
67
- credentials: 'include',
68
77
  body: JSON.stringify({ username, password, displayName }),
69
78
  });
70
79
  if (!res.ok) {
@@ -75,9 +84,10 @@ export async function register(username, password, displayName) {
75
84
  currentUser = body.user;
76
85
  currentSession = body.session;
77
86
  guest = false;
87
+ setAuthToken((_b = (_a = body.session) === null || _a === void 0 ? void 0 : _a.token) !== null && _b !== void 0 ? _b : null);
78
88
  return { ok: true };
79
89
  }
80
- catch (_a) {
90
+ catch (_c) {
81
91
  return { ok: false, error: 'Network error' };
82
92
  }
83
93
  }
@@ -92,14 +102,14 @@ export async function register(username, password, displayName) {
92
102
  */
93
103
  export async function logout() {
94
104
  try {
95
- await fetch(`${serverUrl}/api/auth/logout`, {
105
+ await apiFetch(`${serverUrl}/api/auth/logout`, {
96
106
  method: 'POST',
97
- credentials: 'include',
98
107
  });
99
108
  }
100
109
  catch (_a) {
101
110
  // Best effort
102
111
  }
112
+ setAuthToken(null);
103
113
  if ((authConfig === null || authConfig === void 0 ? void 0 : authConfig.required) && !authConfig.guestAllowed) {
104
114
  // Policy forbids guest browsing — re-run the boot-time hard gate.
105
115
  // Do not touch reactive state: the page is leaving.
@@ -29,6 +29,12 @@ export interface BootConfig {
29
29
  user: AuthUser | null;
30
30
  session: AuthSession | null;
31
31
  tenantId: string;
32
+ /**
33
+ * Server's `sh3-server` semver. Optional for back-compat with
34
+ * pre-0.18.1 servers that didn't emit it; the cross-origin probe
35
+ * in `Onboarding.svelte` falls back to `'unknown'` when absent.
36
+ */
37
+ version?: string;
32
38
  }
33
39
  /** Global settings shape. */
34
40
  export interface GlobalSettings {