sh3-core 0.17.0 → 0.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/Sh3.svelte +107 -39
- package/dist/__screenshots__/handheld.browser.test.ts/handheld-viewport-flip-e2e-viewport-override-flips-chrome-and-body-branches-1.png +0 -0
- package/dist/actions/CommandPalette.svelte +1 -2
- package/dist/actions/listActionsFromEntries.test.js +29 -0
- package/dist/actions/listActive.js +2 -0
- package/dist/actions/listeners.js +16 -1
- package/dist/actions/programmatic-dispatch.svelte.test.js +9 -2
- package/dist/actions/types.d.ts +8 -0
- package/dist/api.d.ts +8 -1
- package/dist/app/store/storeShard.svelte.js +1 -21
- package/dist/app/store/version.d.ts +11 -0
- package/dist/app/store/version.js +39 -0
- package/dist/app/store/version.test.d.ts +1 -0
- package/dist/app/store/version.test.js +44 -0
- package/dist/apps/lifecycle.d.ts +6 -0
- package/dist/apps/lifecycle.js +5 -2
- package/dist/apps/lifecycle.test.js +30 -0
- package/dist/apps/types.d.ts +12 -0
- package/dist/assets/iconIds.generated.d.ts +1 -1
- package/dist/assets/iconIds.generated.js +5 -0
- package/dist/assets/icons.svg +31 -0
- package/dist/auth/auth.svelte.js +18 -8
- package/dist/auth/types.d.ts +6 -0
- package/dist/chrome/CompactChrome.svelte +130 -0
- package/dist/chrome/CompactChrome.svelte.d.ts +3 -0
- package/dist/chrome/CompactChrome.svelte.test.d.ts +1 -0
- package/dist/chrome/CompactChrome.svelte.test.js +174 -0
- package/dist/chrome/MenuSheet.svelte +224 -0
- package/dist/chrome/MenuSheet.svelte.d.ts +7 -0
- package/dist/chrome/MenuSheet.svelte.test.d.ts +1 -0
- package/dist/chrome/MenuSheet.svelte.test.js +46 -0
- package/dist/createShell.d.ts +9 -0
- package/dist/createShell.js +20 -7
- package/dist/createShell.remoteAuth.test.d.ts +1 -0
- package/dist/createShell.remoteAuth.test.js +71 -0
- package/dist/documents/http-backend.js +12 -11
- package/dist/env/client.js +11 -5
- package/dist/files/types.d.ts +106 -0
- package/dist/files/types.js +1 -0
- package/dist/gestures/gestureRegistry.d.ts +6 -0
- package/dist/gestures/gestureRegistry.js +190 -0
- package/dist/gestures/gestureRegistry.test.d.ts +1 -0
- package/dist/gestures/gestureRegistry.test.js +119 -0
- package/dist/gestures/index.d.ts +6 -0
- package/dist/gestures/index.js +12 -0
- package/dist/gestures/pointerClaim.d.ts +7 -0
- package/dist/gestures/pointerClaim.js +36 -0
- package/dist/gestures/pointerClaim.test.d.ts +1 -0
- package/dist/gestures/pointerClaim.test.js +64 -0
- package/dist/gestures/types.d.ts +83 -0
- package/dist/gestures/types.js +1 -0
- package/dist/handheld.browser.test.d.ts +1 -0
- package/dist/handheld.browser.test.js +90 -0
- package/dist/host-entry.d.ts +1 -0
- package/dist/host-entry.js +1 -0
- package/dist/layout/LayoutRenderer.browser.test.js +15 -3
- package/dist/layout/LayoutRenderer.svelte +27 -3
- package/dist/layout/LayoutRenderer.svelte.d.ts +4 -1
- 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
- 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
- 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
- package/dist/layout/compact/CarouselTabs.svelte +361 -0
- package/dist/layout/compact/CarouselTabs.svelte.d.ts +10 -0
- package/dist/layout/compact/CarouselTabs.svelte.test.d.ts +1 -0
- package/dist/layout/compact/CarouselTabs.svelte.test.js +300 -0
- package/dist/layout/compact/CompactRenderer.svelte +53 -0
- package/dist/layout/compact/CompactRenderer.svelte.d.ts +3 -0
- package/dist/layout/compact/CompactRenderer.svelte.test.d.ts +1 -0
- package/dist/layout/compact/CompactRenderer.svelte.test.js +125 -0
- package/dist/layout/compact/derive.d.ts +3 -0
- package/dist/layout/compact/derive.js +157 -0
- package/dist/layout/compact/derive.test.d.ts +1 -0
- package/dist/layout/compact/derive.test.js +197 -0
- package/dist/layout/compact/drawerStore.svelte.d.ts +21 -0
- package/dist/layout/compact/drawerStore.svelte.js +75 -0
- package/dist/layout/compact/drawerStore.svelte.test.d.ts +1 -0
- package/dist/layout/compact/drawerStore.svelte.test.js +43 -0
- package/dist/layout/compact/enrichCarousels.d.ts +8 -0
- package/dist/layout/compact/enrichCarousels.js +44 -0
- package/dist/layout/compact/enrichCarousels.test.d.ts +1 -0
- package/dist/layout/compact/enrichCarousels.test.js +88 -0
- package/dist/layout/compact/resolveRole.d.ts +6 -0
- package/dist/layout/compact/resolveRole.js +13 -0
- package/dist/layout/compact/resolveRole.test.d.ts +1 -0
- package/dist/layout/compact/resolveRole.test.js +18 -0
- package/dist/layout/compact/types.d.ts +30 -0
- package/dist/layout/compact/types.js +15 -0
- package/dist/layout/drag.svelte.js +13 -0
- package/dist/layout/presets.compactVariant.test.d.ts +1 -0
- package/dist/layout/presets.compactVariant.test.js +27 -0
- package/dist/layout/presets.d.ts +12 -0
- package/dist/layout/presets.js +16 -0
- package/dist/layout/store.drawers.svelte.test.d.ts +1 -0
- package/dist/layout/store.drawers.svelte.test.js +49 -0
- package/dist/layout/store.schemaVersion.test.d.ts +1 -0
- package/dist/layout/store.schemaVersion.test.js +35 -0
- package/dist/layout/store.svelte.js +52 -2
- package/dist/layout/types.d.ts +51 -1
- package/dist/layout/types.js +1 -1
- package/dist/layout/types.test.d.ts +1 -0
- package/dist/layout/types.test.js +26 -0
- package/dist/overlays/DrawerSurface.svelte +141 -0
- package/dist/overlays/DrawerSurface.svelte.d.ts +12 -0
- package/dist/overlays/DrawerSurface.svelte.test.d.ts +1 -0
- package/dist/overlays/DrawerSurface.svelte.test.js +67 -0
- package/dist/overlays/ModalFrame.svelte +3 -1
- package/dist/overlays/ModalFrame.svelte.d.ts +1 -0
- package/dist/overlays/OverlayRoots.svelte +12 -9
- package/dist/overlays/floatDismiss.js +5 -0
- package/dist/overlays/focusTrap.d.ts +11 -1
- package/dist/overlays/focusTrap.js +11 -9
- package/dist/overlays/modal.js +1 -0
- package/dist/overlays/popup.js +4 -0
- package/dist/overlays/types.d.ts +10 -1
- package/dist/primitives/Button.svelte +18 -0
- package/dist/primitives/Button.svelte.d.ts +6 -0
- package/dist/primitives/ResizableSplitter.svelte +71 -11
- package/dist/primitives/ResizableSplitter.svelte.d.ts +8 -0
- package/dist/primitives/ResizableSplitter.svelte.test.d.ts +1 -0
- package/dist/primitives/ResizableSplitter.svelte.test.js +74 -0
- package/dist/server-shard/types.d.ts +2 -1
- package/dist/sh3Api/headless.js +9 -1
- package/dist/sh3Api/headless.svelte.test.js +45 -1
- package/dist/sh3Runtime.svelte.d.ts +36 -0
- package/dist/sh3Runtime.svelte.js +33 -0
- package/dist/shards/activate.svelte.js +10 -0
- package/dist/shards/ctx-fetch.test.d.ts +1 -0
- package/dist/shards/ctx-fetch.test.js +66 -0
- package/dist/shards/types.d.ts +22 -1
- package/dist/tokens.css +3 -2
- package/dist/transport/apiFetch.d.ts +1 -0
- package/dist/transport/apiFetch.js +65 -0
- package/dist/transport/apiFetch.test.d.ts +1 -0
- package/dist/transport/apiFetch.test.js +37 -0
- package/dist/transport/authToken.d.ts +2 -0
- package/dist/transport/authToken.js +53 -0
- package/dist/transport/authToken.test.d.ts +1 -0
- package/dist/transport/authToken.test.js +33 -0
- package/dist/verbs/types.d.ts +5 -2
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/viewport/classify.d.ts +8 -0
- package/dist/viewport/classify.js +20 -0
- package/dist/viewport/classify.test.d.ts +1 -0
- package/dist/viewport/classify.test.js +32 -0
- package/dist/viewport/store.browser.test.d.ts +1 -0
- package/dist/viewport/store.browser.test.js +33 -0
- package/dist/viewport/store.svelte.d.ts +9 -0
- package/dist/viewport/store.svelte.js +71 -0
- package/dist/viewport/store.svelte.test.d.ts +1 -0
- package/dist/viewport/store.svelte.test.js +54 -0
- package/dist/viewport/types.d.ts +9 -0
- package/dist/viewport/types.js +6 -0
- package/package.json +1 -1
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { describe, expect, it, vi, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { MemoryDocumentBackend } from '../documents/backends';
|
|
3
|
+
import { __setDocumentBackend, __setTenantId } from '../documents/config';
|
|
4
|
+
import { __setEnvServerUrl } from '../env/index';
|
|
5
|
+
import { registerShard, activateShard, __resetShardRegistryForTest, } from './activate.svelte';
|
|
6
|
+
import { __resetViewRegistryForTest } from './registry';
|
|
7
|
+
describe('ctx.fetch', () => {
|
|
8
|
+
let originalFetch;
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
originalFetch = globalThis.fetch;
|
|
11
|
+
__resetShardRegistryForTest();
|
|
12
|
+
__resetViewRegistryForTest();
|
|
13
|
+
__setDocumentBackend(new MemoryDocumentBackend());
|
|
14
|
+
__setTenantId('tenant-test');
|
|
15
|
+
__setEnvServerUrl('https://example.com');
|
|
16
|
+
});
|
|
17
|
+
afterEach(() => {
|
|
18
|
+
globalThis.fetch = originalFetch;
|
|
19
|
+
__setEnvServerUrl('');
|
|
20
|
+
});
|
|
21
|
+
it('resolves relative paths against the configured serverUrl', async () => {
|
|
22
|
+
const calls = [];
|
|
23
|
+
globalThis.fetch = vi.fn(async (input) => {
|
|
24
|
+
calls.push(String(input));
|
|
25
|
+
return new Response('ok');
|
|
26
|
+
});
|
|
27
|
+
let captured = null;
|
|
28
|
+
registerShard({
|
|
29
|
+
manifest: { id: 'test', label: 'test', version: '0.0.0', views: [] },
|
|
30
|
+
activate(ctx) { captured = ctx; },
|
|
31
|
+
});
|
|
32
|
+
await activateShard('test');
|
|
33
|
+
await captured.fetch('/api/foo');
|
|
34
|
+
expect(calls[0]).toBe('https://example.com/api/foo');
|
|
35
|
+
});
|
|
36
|
+
it('passes absolute URLs through unchanged', async () => {
|
|
37
|
+
const calls = [];
|
|
38
|
+
globalThis.fetch = vi.fn(async (input) => {
|
|
39
|
+
calls.push(String(input));
|
|
40
|
+
return new Response('ok');
|
|
41
|
+
});
|
|
42
|
+
let captured = null;
|
|
43
|
+
registerShard({
|
|
44
|
+
manifest: { id: 'test', label: 'test', version: '0.0.0', views: [] },
|
|
45
|
+
activate(ctx) { captured = ctx; },
|
|
46
|
+
});
|
|
47
|
+
await activateShard('test');
|
|
48
|
+
await captured.fetch('https://other.example.com/api/bar');
|
|
49
|
+
expect(calls[0]).toBe('https://other.example.com/api/bar');
|
|
50
|
+
});
|
|
51
|
+
it('prepends a slash to bare relative paths', async () => {
|
|
52
|
+
const calls = [];
|
|
53
|
+
globalThis.fetch = vi.fn(async (input) => {
|
|
54
|
+
calls.push(String(input));
|
|
55
|
+
return new Response('ok');
|
|
56
|
+
});
|
|
57
|
+
let captured = null;
|
|
58
|
+
registerShard({
|
|
59
|
+
manifest: { id: 'test', label: 'test', version: '0.0.0', views: [] },
|
|
60
|
+
activate(ctx) { captured = ctx; },
|
|
61
|
+
});
|
|
62
|
+
await activateShard('test');
|
|
63
|
+
await captured.fetch('api/baz');
|
|
64
|
+
expect(calls[0]).toBe('https://example.com/api/baz');
|
|
65
|
+
});
|
|
66
|
+
});
|
package/dist/shards/types.d.ts
CHANGED
|
@@ -8,7 +8,7 @@ import type { Sh3Api } from '../verbs/types';
|
|
|
8
8
|
import type { ShardContextKeys } from '../keys/types';
|
|
9
9
|
import type { ContributionsApi } from '../contributions/types';
|
|
10
10
|
import type { ActionsApi } from '../actions/types';
|
|
11
|
-
import type { TreeRootRef } from '../layout/types';
|
|
11
|
+
import type { TreeRootRef, SlotRole } from '../layout/types';
|
|
12
12
|
export { PERMISSION_KEYS_MINT, type ShardContextKeys, type ApiKeyPublic, type MintOpts, ScopeEscalationError, ConsentDeniedError } from '../keys/types';
|
|
13
13
|
/**
|
|
14
14
|
* The object returned by `ViewFactory.mount`. The framework calls
|
|
@@ -38,6 +38,14 @@ export interface ViewHandle {
|
|
|
38
38
|
closable?: boolean | {
|
|
39
39
|
canClose(): Promise<boolean>;
|
|
40
40
|
};
|
|
41
|
+
/**
|
|
42
|
+
* View-level slot-role default. The compact renderer reads this when
|
|
43
|
+
* the containing slot's `role` is unset; slot-level always wins.
|
|
44
|
+
*
|
|
45
|
+
* Lets a view declare "I'm a sidebar by nature" without forcing the
|
|
46
|
+
* app author to know. See `layout/compact/resolveRole.ts`.
|
|
47
|
+
*/
|
|
48
|
+
defaultRole?: SlotRole;
|
|
41
49
|
}
|
|
42
50
|
/**
|
|
43
51
|
* Context passed to `ViewFactory.mount` so the view knows which layout
|
|
@@ -197,6 +205,19 @@ export interface ShardContext {
|
|
|
197
205
|
registerVerb(verb: Verb): void;
|
|
198
206
|
/** Obtain a file-oriented document handle scoped to this shard. */
|
|
199
207
|
documents(options: DocumentHandleOptions): DocumentHandle;
|
|
208
|
+
/**
|
|
209
|
+
* Cross-origin-safe HTTP helper. Resolves relative `/api/...` paths
|
|
210
|
+
* against the configured serverUrl. In Tauri, routes through
|
|
211
|
+
* @tauri-apps/plugin-http (no browser CORS, cookies via reqwest).
|
|
212
|
+
* On web, behaves like `fetch` with `credentials: 'include'`.
|
|
213
|
+
*
|
|
214
|
+
* Server-shards must use this instead of global `fetch` to remain
|
|
215
|
+
* compatible with cross-origin clients (e.g. Tauri Android).
|
|
216
|
+
*
|
|
217
|
+
* @param path - Relative `/api/...` path or fully-qualified URL.
|
|
218
|
+
* @param init - Standard RequestInit.
|
|
219
|
+
*/
|
|
220
|
+
fetch(path: string, init?: RequestInit): Promise<Response>;
|
|
200
221
|
/**
|
|
201
222
|
* Declare environment state for this shard and receive a hydrated snapshot.
|
|
202
223
|
* Env state is server-authoritative, fetched once at activation, and
|
package/dist/tokens.css
CHANGED
|
@@ -79,8 +79,9 @@
|
|
|
79
79
|
* source of truth for the layer stack. No component outside the overlay
|
|
80
80
|
* layer managers is permitted to write a z-index.
|
|
81
81
|
*/
|
|
82
|
-
--sh3-z-layer-0: 0; /* docked layout (content area)
|
|
83
|
-
--sh3-z-layer-
|
|
82
|
+
--sh3-z-layer-0: 0; /* docked layout (content area) */
|
|
83
|
+
--sh3-z-layer-drawers: 50; /* compact-mode drawer surfaces */
|
|
84
|
+
--sh3-z-layer-1: 100; /* floating panels (deferred) */
|
|
84
85
|
--sh3-z-layer-2: 200; /* drag preview */
|
|
85
86
|
--sh3-z-layer-3: 300; /* popups, context menus */
|
|
86
87
|
--sh3-z-layer-4: 400; /* modals */
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function apiFetch(url: string, init?: RequestInit): Promise<Response>;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* apiFetch — single source of truth for HTTP calls against the
|
|
3
|
+
* configured sh3-server, transparently using Tauri's plugin-http
|
|
4
|
+
* when running inside a Tauri webview.
|
|
5
|
+
*
|
|
6
|
+
* Browser fetch enforces CORS and respects SameSite cookie rules,
|
|
7
|
+
* which breaks cross-origin requests from a Tauri webview at e.g.
|
|
8
|
+
* `tauri://localhost` to a remote sh3-server. plugin-http (reqwest
|
|
9
|
+
* under the hood) is not subject to browser CORS and uses a
|
|
10
|
+
* per-host cookie store, so existing same-origin auth keeps working
|
|
11
|
+
* across origins.
|
|
12
|
+
*
|
|
13
|
+
* The plugin-http import is dynamic and behind try/catch — same
|
|
14
|
+
* defensive pattern as `platform/index.ts`. Vite code-splits it
|
|
15
|
+
* into a Tauri-only chunk that never loads in web builds.
|
|
16
|
+
*/
|
|
17
|
+
import { getAuthToken } from './authToken';
|
|
18
|
+
let tauriFetch = null;
|
|
19
|
+
let tauriProbed = false;
|
|
20
|
+
function inTauriRuntime() {
|
|
21
|
+
// The plugin's fetch implementation calls into IPC via window.__TAURI__,
|
|
22
|
+
// so the *package being installed* is not enough — we have to be running
|
|
23
|
+
// inside a Tauri webview where these globals are injected.
|
|
24
|
+
if (typeof window === 'undefined')
|
|
25
|
+
return false;
|
|
26
|
+
return '__TAURI_INTERNALS__' in window || '__TAURI__' in window;
|
|
27
|
+
}
|
|
28
|
+
async function getTauriFetch() {
|
|
29
|
+
if (tauriProbed)
|
|
30
|
+
return tauriFetch;
|
|
31
|
+
tauriProbed = true;
|
|
32
|
+
if (!inTauriRuntime())
|
|
33
|
+
return null;
|
|
34
|
+
try {
|
|
35
|
+
// Static analysis is skipped so Vite/svelte-package don't try to bundle
|
|
36
|
+
// the optional Tauri-only dependency in pure-web builds.
|
|
37
|
+
const specifier = '@tauri-apps/plugin-http';
|
|
38
|
+
const mod = await import(/* @vite-ignore */ specifier);
|
|
39
|
+
tauriFetch = mod.fetch;
|
|
40
|
+
}
|
|
41
|
+
catch (_a) {
|
|
42
|
+
tauriFetch = null;
|
|
43
|
+
}
|
|
44
|
+
return tauriFetch;
|
|
45
|
+
}
|
|
46
|
+
export async function apiFetch(url, init) {
|
|
47
|
+
var _a;
|
|
48
|
+
// Inject Authorization: Bearer <session-token> if a session is active
|
|
49
|
+
// and the caller didn't already supply one. Cookies don't survive the
|
|
50
|
+
// cross-origin hop (SameSite=Lax + plugin-http has no cookie store),
|
|
51
|
+
// so the header carries the session for both transports uniformly.
|
|
52
|
+
const token = getAuthToken();
|
|
53
|
+
let finalInit = init !== null && init !== void 0 ? init : {};
|
|
54
|
+
if (token) {
|
|
55
|
+
const headers = new Headers((_a = finalInit.headers) !== null && _a !== void 0 ? _a : {});
|
|
56
|
+
if (!headers.has('Authorization')) {
|
|
57
|
+
headers.set('Authorization', `Bearer ${token}`);
|
|
58
|
+
finalInit = Object.assign(Object.assign({}, finalInit), { headers });
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
const tf = await getTauriFetch();
|
|
62
|
+
if (tf)
|
|
63
|
+
return tf(url, finalInit);
|
|
64
|
+
return fetch(url, Object.assign({ credentials: 'include' }, finalInit));
|
|
65
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { describe, expect, it, vi, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
describe('apiFetch', () => {
|
|
3
|
+
let originalFetch;
|
|
4
|
+
beforeEach(() => {
|
|
5
|
+
originalFetch = globalThis.fetch;
|
|
6
|
+
vi.resetModules();
|
|
7
|
+
});
|
|
8
|
+
afterEach(() => {
|
|
9
|
+
globalThis.fetch = originalFetch;
|
|
10
|
+
});
|
|
11
|
+
it('uses global fetch with credentials:include when Tauri plugin-http is unavailable', async () => {
|
|
12
|
+
const calls = [];
|
|
13
|
+
globalThis.fetch = vi.fn(async (input, init) => {
|
|
14
|
+
calls.push([input, init]);
|
|
15
|
+
return new Response('ok');
|
|
16
|
+
});
|
|
17
|
+
const { apiFetch } = await import('./apiFetch');
|
|
18
|
+
const res = await apiFetch('https://example.com/api/foo', { method: 'GET' });
|
|
19
|
+
expect(await res.text()).toBe('ok');
|
|
20
|
+
expect(calls).toHaveLength(1);
|
|
21
|
+
const [input, init] = calls[0];
|
|
22
|
+
expect(input).toBe('https://example.com/api/foo');
|
|
23
|
+
expect(init.credentials).toBe('include');
|
|
24
|
+
expect(init.method).toBe('GET');
|
|
25
|
+
});
|
|
26
|
+
it('lets caller-provided credentials override the default', async () => {
|
|
27
|
+
const calls = [];
|
|
28
|
+
globalThis.fetch = vi.fn(async (input, init) => {
|
|
29
|
+
calls.push([input, init]);
|
|
30
|
+
return new Response('ok');
|
|
31
|
+
});
|
|
32
|
+
const { apiFetch } = await import('./apiFetch');
|
|
33
|
+
await apiFetch('https://example.com/api/foo', { credentials: 'omit' });
|
|
34
|
+
const [, init] = calls[0];
|
|
35
|
+
expect(init.credentials).toBe('omit');
|
|
36
|
+
});
|
|
37
|
+
});
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Auth-token store used by `apiFetch` to attach `Authorization: Bearer
|
|
3
|
+
* <token>` to every request when a session exists.
|
|
4
|
+
*
|
|
5
|
+
* The browser auth flow relies on Set-Cookie + same-origin requests to
|
|
6
|
+
* keep the session alive. That breaks down for cross-origin Tauri
|
|
7
|
+
* clients (e.g. Android): the WebView's origin is `tauri://localhost`
|
|
8
|
+
* and the sh3-server lives on a different domain, so the session
|
|
9
|
+
* cookie is treated as cross-site and dropped under SameSite=Lax. On
|
|
10
|
+
* top of that, `@tauri-apps/plugin-http` (reqwest under the hood)
|
|
11
|
+
* doesn't enable a persistent cookie store by default, so even if
|
|
12
|
+
* SameSite weren't an issue cookies wouldn't survive between calls.
|
|
13
|
+
*
|
|
14
|
+
* Carrying the session token in a header sidesteps both problems.
|
|
15
|
+
* `auth.svelte.ts` populates this store after login/register/initFromBoot
|
|
16
|
+
* and clears it on logout. The token is mirrored to localStorage so an
|
|
17
|
+
* app restart doesn't dump the user back at the sign-in wall.
|
|
18
|
+
*/
|
|
19
|
+
const KEY = 'sh3:authToken';
|
|
20
|
+
let token = null;
|
|
21
|
+
let restored = false;
|
|
22
|
+
function restore() {
|
|
23
|
+
if (restored)
|
|
24
|
+
return;
|
|
25
|
+
restored = true;
|
|
26
|
+
if (typeof localStorage === 'undefined')
|
|
27
|
+
return;
|
|
28
|
+
try {
|
|
29
|
+
token = localStorage.getItem(KEY);
|
|
30
|
+
}
|
|
31
|
+
catch (_a) {
|
|
32
|
+
token = null;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
export function setAuthToken(value) {
|
|
36
|
+
restored = true;
|
|
37
|
+
token = value;
|
|
38
|
+
if (typeof localStorage === 'undefined')
|
|
39
|
+
return;
|
|
40
|
+
try {
|
|
41
|
+
if (value)
|
|
42
|
+
localStorage.setItem(KEY, value);
|
|
43
|
+
else
|
|
44
|
+
localStorage.removeItem(KEY);
|
|
45
|
+
}
|
|
46
|
+
catch (_a) {
|
|
47
|
+
// localStorage may be disabled (private browsing); in-memory copy still works.
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
export function getAuthToken() {
|
|
51
|
+
restore();
|
|
52
|
+
return token;
|
|
53
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { describe, expect, it, beforeEach, vi } from 'vitest';
|
|
2
|
+
describe('authToken', () => {
|
|
3
|
+
beforeEach(() => {
|
|
4
|
+
vi.resetModules();
|
|
5
|
+
if (typeof localStorage !== 'undefined')
|
|
6
|
+
localStorage.clear();
|
|
7
|
+
});
|
|
8
|
+
it('returns null when nothing stored', async () => {
|
|
9
|
+
const { getAuthToken } = await import('./authToken');
|
|
10
|
+
expect(getAuthToken()).toBeNull();
|
|
11
|
+
});
|
|
12
|
+
it('round-trips set/get', async () => {
|
|
13
|
+
const { setAuthToken, getAuthToken } = await import('./authToken');
|
|
14
|
+
setAuthToken('sh3s_deadbeef');
|
|
15
|
+
expect(getAuthToken()).toBe('sh3s_deadbeef');
|
|
16
|
+
});
|
|
17
|
+
it('persists across module re-imports via localStorage', async () => {
|
|
18
|
+
const first = await import('./authToken');
|
|
19
|
+
first.setAuthToken('sh3s_persisted');
|
|
20
|
+
vi.resetModules();
|
|
21
|
+
const second = await import('./authToken');
|
|
22
|
+
expect(second.getAuthToken()).toBe('sh3s_persisted');
|
|
23
|
+
});
|
|
24
|
+
it('clear removes from storage', async () => {
|
|
25
|
+
const { setAuthToken, getAuthToken } = await import('./authToken');
|
|
26
|
+
setAuthToken('sh3s_x');
|
|
27
|
+
setAuthToken(null);
|
|
28
|
+
expect(getAuthToken()).toBeNull();
|
|
29
|
+
vi.resetModules();
|
|
30
|
+
const reloaded = await import('./authToken');
|
|
31
|
+
expect(reloaded.getAuthToken()).toBeNull();
|
|
32
|
+
});
|
|
33
|
+
});
|
package/dist/verbs/types.d.ts
CHANGED
|
@@ -128,11 +128,14 @@ export interface Sh3Api {
|
|
|
128
128
|
scrollback: ScrollbackEntry[];
|
|
129
129
|
}>;
|
|
130
130
|
/**
|
|
131
|
-
* Read-only snapshot of every action registered across every shard.
|
|
132
|
-
*
|
|
131
|
+
* Read-only snapshot of every action registered across every shard.
|
|
132
|
+
* - `activeOnly`: filter to currently-dispatchable actions.
|
|
133
|
+
* - `submenuOf`: restrict to children of the named parent action id
|
|
134
|
+
* (mirrors the palette sub-drill filter).
|
|
133
135
|
*/
|
|
134
136
|
listActions(opts?: {
|
|
135
137
|
activeOnly?: boolean;
|
|
138
|
+
submenuOf?: string;
|
|
136
139
|
}): ActionDescriptor[];
|
|
137
140
|
/**
|
|
138
141
|
* Programmatically dispatch a registered action by id. Same semantics as
|
package/dist/version.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
/** Auto-generated from package.json — do not edit manually. */
|
|
2
|
-
export declare const VERSION = "0.
|
|
2
|
+
export declare const VERSION = "0.19.0";
|
package/dist/version.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
/** Auto-generated from package.json — do not edit manually. */
|
|
2
|
-
export const VERSION = '0.
|
|
2
|
+
export const VERSION = '0.19.0';
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* classify — derives the viewport class from multi-signal input.
|
|
3
|
+
*
|
|
4
|
+
* Why multi-signal: a 6.7" phone reports ~393 CSS px wide *with* coarse
|
|
5
|
+
* pointer + high DPR; a narrow desktop window reports the same width
|
|
6
|
+
* *with* fine pointer + DPR 1-2. CSS pixels alone undercount physical
|
|
7
|
+
* compactness on high-DPI mobile, so width is one signal among three.
|
|
8
|
+
*
|
|
9
|
+
* The thresholds (720, 1100) are not load-bearing — they're tunable in
|
|
10
|
+
* a single place. Adjust with a corresponding test row.
|
|
11
|
+
*/
|
|
12
|
+
export function classify(i) {
|
|
13
|
+
if (i.coarsePointer && i.noHover)
|
|
14
|
+
return 'compact';
|
|
15
|
+
if (i.width < 720)
|
|
16
|
+
return 'compact';
|
|
17
|
+
if (i.coarsePointer && i.dpr >= 2 && i.width < 1100)
|
|
18
|
+
return 'compact';
|
|
19
|
+
return 'desktop';
|
|
20
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Table-driven tests for the viewport classifier. Each row is a real
|
|
3
|
+
* device (or a deliberately-chosen edge case) with the signal tuple
|
|
4
|
+
* expected from a real browser/matchMedia in that device's typical
|
|
5
|
+
* orientation.
|
|
6
|
+
*/
|
|
7
|
+
import { describe, it, expect } from 'vitest';
|
|
8
|
+
import { classify } from './classify';
|
|
9
|
+
const ROWS = [
|
|
10
|
+
{ name: 'phone portrait', width: 393, coarsePointer: true, noHover: true, dpr: 3, expected: 'compact' },
|
|
11
|
+
{ name: 'phone landscape', width: 852, coarsePointer: true, noHover: true, dpr: 3, expected: 'compact' },
|
|
12
|
+
{ name: 'tablet portrait', width: 768, coarsePointer: true, noHover: true, dpr: 2, expected: 'compact' },
|
|
13
|
+
{ name: 'tablet landscape', width: 1024, coarsePointer: true, noHover: true, dpr: 2, expected: 'compact' },
|
|
14
|
+
{ name: 'desktop wide', width: 1920, coarsePointer: false, noHover: false, dpr: 1, expected: 'desktop' },
|
|
15
|
+
{ name: 'desktop narrow window', width: 700, coarsePointer: false, noHover: false, dpr: 1, expected: 'compact' },
|
|
16
|
+
{ name: 'iPad with mouse attached', width: 1024, coarsePointer: false, noHover: false, dpr: 2, expected: 'desktop' },
|
|
17
|
+
{ name: 'small laptop', width: 1366, coarsePointer: false, noHover: false, dpr: 1, expected: 'desktop' },
|
|
18
|
+
{ name: 'phone with stylus (hover ok)', width: 412, coarsePointer: true, noHover: false, dpr: 3, expected: 'compact' },
|
|
19
|
+
];
|
|
20
|
+
describe('classify', () => {
|
|
21
|
+
for (const row of ROWS) {
|
|
22
|
+
it(`${row.name} → ${row.expected}`, () => {
|
|
23
|
+
const result = classify({
|
|
24
|
+
width: row.width,
|
|
25
|
+
coarsePointer: row.coarsePointer,
|
|
26
|
+
noHover: row.noHover,
|
|
27
|
+
dpr: row.dpr,
|
|
28
|
+
});
|
|
29
|
+
expect(result).toBe(row.expected);
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Real-signal viewport classification — boots the viewport store under
|
|
3
|
+
* Chromium and asserts initial class + override behavior. happy-dom's
|
|
4
|
+
* matchMedia stub is shallow, so this is the contract pin against a
|
|
5
|
+
* real browser engine.
|
|
6
|
+
*/
|
|
7
|
+
import { describe, it, expect, beforeEach } from 'vitest';
|
|
8
|
+
import { viewportStore } from './store.svelte';
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
viewportStore.__reset();
|
|
11
|
+
});
|
|
12
|
+
describe('viewport store under real browser', () => {
|
|
13
|
+
it('returns a viewport class consistent with the test runner window', () => {
|
|
14
|
+
const info = viewportStore.current;
|
|
15
|
+
expect(info.class).toMatch(/desktop|compact/);
|
|
16
|
+
expect(info.width).toBeGreaterThan(0);
|
|
17
|
+
expect(info.height).toBeGreaterThan(0);
|
|
18
|
+
});
|
|
19
|
+
it('override(compact) immediately flips class', () => {
|
|
20
|
+
const fires = [];
|
|
21
|
+
const unsub = viewportStore.subscribe((i) => fires.push(i.class));
|
|
22
|
+
viewportStore.override('compact');
|
|
23
|
+
expect(viewportStore.current.class).toBe('compact');
|
|
24
|
+
expect(fires).toContain('compact');
|
|
25
|
+
unsub();
|
|
26
|
+
});
|
|
27
|
+
it('override(null) restores auto-derived class', () => {
|
|
28
|
+
viewportStore.override('compact');
|
|
29
|
+
expect(viewportStore.pinned).toBe('compact');
|
|
30
|
+
viewportStore.override(null);
|
|
31
|
+
expect(viewportStore.pinned).toBeNull();
|
|
32
|
+
});
|
|
33
|
+
});
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { ViewportClass, ViewportInfo } from './types';
|
|
2
|
+
export declare const viewportStore: {
|
|
3
|
+
readonly current: ViewportInfo;
|
|
4
|
+
subscribe(cb: (i: ViewportInfo) => void): () => void;
|
|
5
|
+
override(cls: ViewportClass | null): void;
|
|
6
|
+
readonly pinned: ViewportClass | null;
|
|
7
|
+
/** Test-only reset hook. Not exported from index.ts. */
|
|
8
|
+
__reset(): void;
|
|
9
|
+
};
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* viewport store — reactive ViewportInfo with subscribe + override.
|
|
3
|
+
*
|
|
4
|
+
* Subscribes to window.resize, screen.orientation.change, and matchMedia
|
|
5
|
+
* change events for `pointer: coarse` / `hover: none`. Re-runs classify(),
|
|
6
|
+
* notifies subscribers only on class change (not every resize tick).
|
|
7
|
+
*
|
|
8
|
+
* Module is loaded eagerly by createShell, but the listener-wiring branch
|
|
9
|
+
* is gated on `typeof window` so node-only test runs don't crash.
|
|
10
|
+
*/
|
|
11
|
+
import { classify } from './classify';
|
|
12
|
+
let pinnedClass = $state(null);
|
|
13
|
+
const subscribers = new Set();
|
|
14
|
+
function read() {
|
|
15
|
+
if (typeof window === 'undefined') {
|
|
16
|
+
return { class: 'desktop', width: 1920, height: 1080, coarsePointer: false, noHover: false, dpr: 1 };
|
|
17
|
+
}
|
|
18
|
+
const width = window.innerWidth;
|
|
19
|
+
const height = window.innerHeight;
|
|
20
|
+
const coarsePointer = window.matchMedia('(pointer: coarse)').matches;
|
|
21
|
+
const noHover = window.matchMedia('(hover: none)').matches;
|
|
22
|
+
const dpr = window.devicePixelRatio;
|
|
23
|
+
const cls = pinnedClass !== null && pinnedClass !== void 0 ? pinnedClass : classify({ width, coarsePointer, noHover, dpr });
|
|
24
|
+
return { class: cls, width, height, coarsePointer, noHover, dpr };
|
|
25
|
+
}
|
|
26
|
+
let cached = $state(read());
|
|
27
|
+
function recompute() {
|
|
28
|
+
const next = read();
|
|
29
|
+
const prevClass = cached.class;
|
|
30
|
+
cached = next;
|
|
31
|
+
if (next.class !== prevClass) {
|
|
32
|
+
for (const cb of subscribers)
|
|
33
|
+
cb(next);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
if (typeof window !== 'undefined') {
|
|
37
|
+
window.addEventListener('resize', recompute);
|
|
38
|
+
window.matchMedia('(pointer: coarse)').addEventListener('change', recompute);
|
|
39
|
+
window.matchMedia('(hover: none)').addEventListener('change', recompute);
|
|
40
|
+
if (typeof screen !== 'undefined' && screen.orientation) {
|
|
41
|
+
screen.orientation.addEventListener('change', recompute);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
export const viewportStore = {
|
|
45
|
+
get current() {
|
|
46
|
+
return cached;
|
|
47
|
+
},
|
|
48
|
+
subscribe(cb) {
|
|
49
|
+
subscribers.add(cb);
|
|
50
|
+
return () => { subscribers.delete(cb); };
|
|
51
|
+
},
|
|
52
|
+
override(cls) {
|
|
53
|
+
if (pinnedClass === cls)
|
|
54
|
+
return;
|
|
55
|
+
pinnedClass = cls;
|
|
56
|
+
recompute();
|
|
57
|
+
// recompute only fires subscribers on class change. If override pins
|
|
58
|
+
// to the same class as the auto-derived one, the visible class doesn't
|
|
59
|
+
// change. Force-fire so consumers re-read the override state.
|
|
60
|
+
for (const cb of subscribers)
|
|
61
|
+
cb(cached);
|
|
62
|
+
},
|
|
63
|
+
get pinned() {
|
|
64
|
+
return pinnedClass;
|
|
65
|
+
},
|
|
66
|
+
/** Test-only reset hook. Not exported from index.ts. */
|
|
67
|
+
__reset() {
|
|
68
|
+
pinnedClass = null;
|
|
69
|
+
subscribers.clear();
|
|
70
|
+
},
|
|
71
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* DOM tests for the viewport store. happy-dom's matchMedia stub returns
|
|
3
|
+
* { matches: false, addEventListener, removeEventListener } so we can
|
|
4
|
+
* verify the wiring without driving the real browser engine.
|
|
5
|
+
*
|
|
6
|
+
* These tests intentionally use the store's __reset hook between cases
|
|
7
|
+
* to isolate subscriber state.
|
|
8
|
+
*/
|
|
9
|
+
import { describe, it, expect, beforeEach } from 'vitest';
|
|
10
|
+
import { viewportStore } from './store.svelte';
|
|
11
|
+
describe('viewport store (dom)', () => {
|
|
12
|
+
beforeEach(() => {
|
|
13
|
+
viewportStore.__reset();
|
|
14
|
+
});
|
|
15
|
+
it('current returns a ViewportInfo with shape', () => {
|
|
16
|
+
const info = viewportStore.current;
|
|
17
|
+
expect(info.class).toMatch(/desktop|compact/);
|
|
18
|
+
expect(typeof info.width).toBe('number');
|
|
19
|
+
expect(typeof info.height).toBe('number');
|
|
20
|
+
});
|
|
21
|
+
it('override(compact) flips class to compact', () => {
|
|
22
|
+
viewportStore.override('compact');
|
|
23
|
+
expect(viewportStore.current.class).toBe('compact');
|
|
24
|
+
expect(viewportStore.pinned).toBe('compact');
|
|
25
|
+
});
|
|
26
|
+
it('override(null) restores pinned to null', () => {
|
|
27
|
+
viewportStore.override('compact');
|
|
28
|
+
viewportStore.override(null);
|
|
29
|
+
expect(viewportStore.pinned).toBeNull();
|
|
30
|
+
});
|
|
31
|
+
it('subscribers fire on class change via override', () => {
|
|
32
|
+
const fires = [];
|
|
33
|
+
const unsub = viewportStore.subscribe((i) => fires.push(i.class));
|
|
34
|
+
viewportStore.override('compact');
|
|
35
|
+
expect(fires).toContain('compact');
|
|
36
|
+
unsub();
|
|
37
|
+
});
|
|
38
|
+
it('subscribers fire on override even when class does not change', () => {
|
|
39
|
+
// Auto-class on happy-dom is desktop (default width 1024 + fine pointer).
|
|
40
|
+
// override('desktop') should still fire so consumers see pin state change.
|
|
41
|
+
const fires = [];
|
|
42
|
+
const unsub = viewportStore.subscribe((i) => fires.push(i.class));
|
|
43
|
+
viewportStore.override('desktop');
|
|
44
|
+
expect(fires.length).toBeGreaterThan(0);
|
|
45
|
+
unsub();
|
|
46
|
+
});
|
|
47
|
+
it('unsubscribe stops further notifications', () => {
|
|
48
|
+
const fires = [];
|
|
49
|
+
const unsub = viewportStore.subscribe((i) => fires.push(i.class));
|
|
50
|
+
unsub();
|
|
51
|
+
viewportStore.override('compact');
|
|
52
|
+
expect(fires).toEqual([]);
|
|
53
|
+
});
|
|
54
|
+
});
|