sh3-core 0.19.1 → 0.19.5
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 +3 -1
- package/dist/actions/menuBarModel.js +8 -0
- package/dist/actions/menuBarModel.test.js +61 -0
- package/dist/api.d.ts +4 -0
- package/dist/api.js +3 -0
- package/dist/app/admin/ApiKeysView.svelte +6 -5
- package/dist/app/store/PermissionConfirmModal.svelte +23 -0
- package/dist/app/store/PermissionConfirmModal.svelte.d.ts +1 -0
- package/dist/app/store/StoreView.svelte +6 -1
- package/dist/chrome/CompactChrome.svelte +34 -1
- package/dist/chrome/CompactChrome.svelte.test.js +11 -6
- package/dist/chrome/FloatsSheet.svelte +236 -0
- package/dist/chrome/FloatsSheet.svelte.d.ts +7 -0
- package/dist/chrome/FloatsSheet.svelte.test.d.ts +1 -0
- package/dist/chrome/FloatsSheet.svelte.test.js +155 -0
- package/dist/env/client.d.ts +5 -4
- package/dist/env/client.js +11 -17
- package/dist/env/serverUrl.d.ts +2 -0
- package/dist/env/serverUrl.js +8 -0
- package/dist/gestures/index.d.ts +17 -0
- package/dist/gestures/index.js +27 -0
- package/dist/keys/client.js +6 -7
- package/dist/keys/revocation-bus.svelte.js +11 -1
- package/dist/layout/compact/CarouselTabs.svelte +150 -14
- package/dist/layout/compact/CarouselTabs.svelte.test.js +222 -2
- package/dist/layout/compact/CompactRenderer.svelte +9 -3
- package/dist/layout/compact/CompactRenderer.svelte.test.js +5 -3
- package/dist/layout/compact/derive.js +7 -16
- package/dist/layout/compact/derive.test.js +30 -9
- package/dist/layout/compact/rootStore.svelte.d.ts +20 -0
- package/dist/layout/compact/rootStore.svelte.js +59 -0
- package/dist/layout/compact/rootStore.svelte.test.d.ts +1 -0
- package/dist/layout/compact/rootStore.svelte.test.js +54 -0
- package/dist/layout/drag.svelte.js +16 -3
- package/dist/layout/floats.d.ts +27 -0
- package/dist/layout/floats.js +20 -0
- package/dist/layout/floats.test.js +34 -1
- package/dist/layout/inspection.d.ts +20 -9
- package/dist/layout/inspection.js +91 -13
- package/dist/layout/inspection.svelte.test.d.ts +1 -0
- package/dist/layout/inspection.svelte.test.js +163 -0
- package/dist/layout/store.schemaVersion.test.js +2 -2
- package/dist/layout/types.d.ts +11 -8
- package/dist/layout/types.js +1 -1
- package/dist/layout/types.test.js +2 -2
- package/dist/overlays/FloatFrame.svelte +93 -22
- package/dist/overlays/FloatLayer.svelte +12 -1
- package/dist/overlays/float.d.ts +7 -0
- package/dist/overlays/float.js +76 -6
- package/dist/overlays/float.test.js +170 -0
- package/dist/primitives/ResizableSplitter.svelte +42 -8
- package/dist/primitives/widgets/DocumentFilePicker.d.ts +25 -0
- package/dist/primitives/widgets/DocumentFilePicker.js +74 -0
- package/dist/primitives/widgets/DocumentFilePicker.svelte +144 -0
- package/dist/primitives/widgets/DocumentFilePicker.svelte.d.ts +18 -0
- package/dist/primitives/widgets/DocumentOpener.svelte +36 -0
- package/dist/primitives/widgets/DocumentOpener.svelte.d.ts +17 -0
- package/dist/primitives/widgets/DocumentSaver.svelte +36 -0
- package/dist/primitives/widgets/DocumentSaver.svelte.d.ts +17 -0
- package/dist/primitives/widgets/_DocumentBrowser.svelte +337 -0
- package/dist/primitives/widgets/_DocumentBrowser.svelte.d.ts +11 -0
- package/dist/registry/checkFetch.d.ts +6 -0
- package/dist/registry/checkFetch.js +23 -0
- package/dist/sh3/views/KeysAndPeers.svelte +4 -3
- package/dist/shards/activate-runtime.test.js +99 -1
- package/dist/shards/activate.svelte.js +12 -3
- package/dist/shards/registry.d.ts +8 -1
- package/dist/shards/registry.js +13 -2
- package/dist/shards/registry.test.js +25 -4
- package/dist/shards/types.d.ts +14 -1
- package/dist/shell-shard/ScrollbackView.svelte +145 -67
- package/dist/shell-shard/ScrollbackView.svelte.test.d.ts +1 -0
- package/dist/shell-shard/ScrollbackView.svelte.test.js +182 -0
- package/dist/shell-shard/dispatch-gating.test.js +38 -2
- package/dist/shell-shard/dispatch.js +9 -1
- package/dist/shell-shard/registry-resolve.test.js +50 -0
- package/dist/shell-shard/registry.d.ts +2 -1
- package/dist/shell-shard/registry.js +12 -2
- package/dist/shell-shard/verbs/help.js +5 -4
- package/dist/shell-shard/verbs/help.svelte.test.js +5 -2
- package/dist/verbs/types.d.ts +10 -5
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { tick } from 'svelte';
|
|
3
|
+
import { renderWithShell } from '../__test__/render';
|
|
4
|
+
import { resetFramework } from '../__test__/reset';
|
|
5
|
+
import { __resetLayoutStoreForTest, layoutStore, } from '../layout/store.svelte';
|
|
6
|
+
import { compactRootStore, __resetCompactRootStoreForTest, } from '../layout/compact/rootStore.svelte';
|
|
7
|
+
import FloatsSheet from './FloatsSheet.svelte';
|
|
8
|
+
function makeFloat(id, title) {
|
|
9
|
+
return {
|
|
10
|
+
id,
|
|
11
|
+
content: {
|
|
12
|
+
type: 'tabs',
|
|
13
|
+
tabs: [{ slotId: `s-${id}`, viewId: 'v', label: title }],
|
|
14
|
+
activeTab: 0,
|
|
15
|
+
},
|
|
16
|
+
position: { x: 0, y: 0 },
|
|
17
|
+
size: { w: 200, h: 200 },
|
|
18
|
+
title,
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
describe('FloatsSheet', () => {
|
|
22
|
+
beforeEach(() => {
|
|
23
|
+
resetFramework();
|
|
24
|
+
__resetCompactRootStoreForTest();
|
|
25
|
+
__resetLayoutStoreForTest();
|
|
26
|
+
});
|
|
27
|
+
afterEach(() => {
|
|
28
|
+
document.body.innerHTML = '';
|
|
29
|
+
});
|
|
30
|
+
it('renders the active-layout row even when no floats exist', async () => {
|
|
31
|
+
const { container } = renderWithShell(FloatsSheet, {
|
|
32
|
+
open: true,
|
|
33
|
+
onClose: () => { },
|
|
34
|
+
});
|
|
35
|
+
await tick();
|
|
36
|
+
const rows = container.querySelectorAll('[data-sh3-floats-row]');
|
|
37
|
+
expect(rows.length).toBe(1);
|
|
38
|
+
expect(rows[0].getAttribute('data-sh3-floats-row')).toBe('docked');
|
|
39
|
+
});
|
|
40
|
+
it('lists one row per non-dismissable float, excluding pickers', async () => {
|
|
41
|
+
layoutStore.tree.floats.push(makeFloat('f-1', 'Notes'));
|
|
42
|
+
layoutStore.tree.floats.push(makeFloat('f-2', 'Editor'));
|
|
43
|
+
layoutStore.tree.floats.push(Object.assign(Object.assign({}, makeFloat('f-3', 'Picker')), { dismissable: true }));
|
|
44
|
+
const { container } = renderWithShell(FloatsSheet, {
|
|
45
|
+
open: true,
|
|
46
|
+
onClose: () => { },
|
|
47
|
+
});
|
|
48
|
+
await tick();
|
|
49
|
+
const rows = container.querySelectorAll('[data-sh3-floats-row]');
|
|
50
|
+
expect(rows.length).toBe(3); // docked + 2 non-dismissable
|
|
51
|
+
const ids = Array.from(rows).map((r) => r.getAttribute('data-sh3-floats-row'));
|
|
52
|
+
expect(ids).toEqual(['docked', 'f-1', 'f-2']);
|
|
53
|
+
});
|
|
54
|
+
it('tapping a float row sets compactRootStore + calls onClose', async () => {
|
|
55
|
+
layoutStore.tree.floats.push(makeFloat('f-9', 'Notes'));
|
|
56
|
+
let closed = false;
|
|
57
|
+
const { container } = renderWithShell(FloatsSheet, {
|
|
58
|
+
open: true,
|
|
59
|
+
onClose: () => { closed = true; },
|
|
60
|
+
});
|
|
61
|
+
await tick();
|
|
62
|
+
const row = container.querySelector('[data-sh3-floats-row="f-9"]');
|
|
63
|
+
row.click();
|
|
64
|
+
expect(compactRootStore.current).toEqual({ kind: 'float', floatId: 'f-9' });
|
|
65
|
+
expect(closed).toBe(true);
|
|
66
|
+
});
|
|
67
|
+
it('tapping the docked row resets compactRootStore + calls onClose', async () => {
|
|
68
|
+
layoutStore.tree.floats.push(makeFloat('f-10', 'Notes'));
|
|
69
|
+
compactRootStore.setRoot({ kind: 'float', floatId: 'f-10' });
|
|
70
|
+
let closed = false;
|
|
71
|
+
const { container } = renderWithShell(FloatsSheet, {
|
|
72
|
+
open: true,
|
|
73
|
+
onClose: () => { closed = true; },
|
|
74
|
+
});
|
|
75
|
+
await tick();
|
|
76
|
+
const row = container.querySelector('[data-sh3-floats-row="docked"]');
|
|
77
|
+
row.click();
|
|
78
|
+
expect(compactRootStore.current).toEqual({ kind: 'docked' });
|
|
79
|
+
expect(closed).toBe(true);
|
|
80
|
+
});
|
|
81
|
+
it('marks the current row with data-current="true"', async () => {
|
|
82
|
+
layoutStore.tree.floats.push(makeFloat('f-11', 'Notes'));
|
|
83
|
+
compactRootStore.setRoot({ kind: 'float', floatId: 'f-11' });
|
|
84
|
+
const { container } = renderWithShell(FloatsSheet, {
|
|
85
|
+
open: true,
|
|
86
|
+
onClose: () => { },
|
|
87
|
+
});
|
|
88
|
+
await tick();
|
|
89
|
+
const cur = container.querySelector('[data-current="true"]');
|
|
90
|
+
expect(cur === null || cur === void 0 ? void 0 : cur.getAttribute('data-sh3-floats-row')).toBe('f-11');
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
import { floatManager, bindFloatStore, __resetFloatManagerForTest, } from '../overlays/float';
|
|
94
|
+
function fakePointer(type, x, y, id = 1) {
|
|
95
|
+
const ev = new Event(type, { bubbles: true, cancelable: true });
|
|
96
|
+
Object.assign(ev, { pointerId: id, clientX: x, clientY: y, pointerType: 'touch', button: 0 });
|
|
97
|
+
return ev;
|
|
98
|
+
}
|
|
99
|
+
describe('FloatsSheet — swipe to close', () => {
|
|
100
|
+
beforeEach(() => {
|
|
101
|
+
resetFramework();
|
|
102
|
+
__resetCompactRootStoreForTest();
|
|
103
|
+
__resetLayoutStoreForTest();
|
|
104
|
+
__resetFloatManagerForTest();
|
|
105
|
+
bindFloatStore(layoutStore.floats, () => ({ w: 360, h: 740 }));
|
|
106
|
+
});
|
|
107
|
+
afterEach(() => {
|
|
108
|
+
document.body.innerHTML = '';
|
|
109
|
+
});
|
|
110
|
+
it('swiping a float row past 40% width closes that float', async () => {
|
|
111
|
+
const id = floatManager.open('test:view', { title: 'Notes' });
|
|
112
|
+
expect(layoutStore.floats.find((f) => f.id === id)).toBeTruthy();
|
|
113
|
+
const { container } = renderWithShell(FloatsSheet, {
|
|
114
|
+
open: true,
|
|
115
|
+
onClose: () => { },
|
|
116
|
+
});
|
|
117
|
+
await tick();
|
|
118
|
+
const row = container.querySelector(`[data-sh3-floats-row="${id}"]`);
|
|
119
|
+
Object.defineProperty(row, 'clientWidth', { configurable: true, value: 360 });
|
|
120
|
+
row.dispatchEvent(fakePointer('pointerdown', 10, 10, 1));
|
|
121
|
+
document.dispatchEvent(fakePointer('pointermove', 200, 10, 1));
|
|
122
|
+
document.dispatchEvent(fakePointer('pointerup', 200, 10, 1));
|
|
123
|
+
await tick();
|
|
124
|
+
expect(layoutStore.floats.find((f) => f.id === id)).toBeUndefined();
|
|
125
|
+
});
|
|
126
|
+
it('does not let the docked row be swiped (no throw, no state change)', async () => {
|
|
127
|
+
const { container } = renderWithShell(FloatsSheet, {
|
|
128
|
+
open: true,
|
|
129
|
+
onClose: () => { },
|
|
130
|
+
});
|
|
131
|
+
await tick();
|
|
132
|
+
const row = container.querySelector('[data-sh3-floats-row="docked"]');
|
|
133
|
+
Object.defineProperty(row, 'clientWidth', { configurable: true, value: 360 });
|
|
134
|
+
row.dispatchEvent(fakePointer('pointerdown', 10, 10, 2));
|
|
135
|
+
document.dispatchEvent(fakePointer('pointermove', 300, 10, 2));
|
|
136
|
+
document.dispatchEvent(fakePointer('pointerup', 300, 10, 2));
|
|
137
|
+
await tick();
|
|
138
|
+
expect(compactRootStore.current).toEqual({ kind: 'docked' });
|
|
139
|
+
});
|
|
140
|
+
it('swiping less than 40% width does not close', async () => {
|
|
141
|
+
const id = floatManager.open('test:view', { title: 'Notes' });
|
|
142
|
+
const { container } = renderWithShell(FloatsSheet, {
|
|
143
|
+
open: true,
|
|
144
|
+
onClose: () => { },
|
|
145
|
+
});
|
|
146
|
+
await tick();
|
|
147
|
+
const row = container.querySelector(`[data-sh3-floats-row="${id}"]`);
|
|
148
|
+
Object.defineProperty(row, 'clientWidth', { configurable: true, value: 360 });
|
|
149
|
+
row.dispatchEvent(fakePointer('pointerdown', 10, 10, 3));
|
|
150
|
+
document.dispatchEvent(fakePointer('pointermove', 50, 10, 3));
|
|
151
|
+
document.dispatchEvent(fakePointer('pointerup', 50, 10, 3));
|
|
152
|
+
await tick();
|
|
153
|
+
expect(layoutStore.floats.find((f) => f.id === id)).toBeTruthy();
|
|
154
|
+
});
|
|
155
|
+
});
|
package/dist/env/client.d.ts
CHANGED
|
@@ -2,10 +2,7 @@
|
|
|
2
2
|
* Env state client — fetches and updates per-shard environment state
|
|
3
3
|
* from the server.
|
|
4
4
|
*/
|
|
5
|
-
|
|
6
|
-
export declare function __setEnvServerUrl(url: string): void;
|
|
7
|
-
/** Return the configured server URL. */
|
|
8
|
-
export declare function getEnvServerUrl(): string;
|
|
5
|
+
export { getEnvServerUrl, __setEnvServerUrl } from './serverUrl';
|
|
9
6
|
/**
|
|
10
7
|
* Fetch env state for a shard from the server.
|
|
11
8
|
* Returns an empty object if the server has no stored state.
|
|
@@ -24,6 +21,10 @@ export interface ServerInstallResult {
|
|
|
24
21
|
missing?: Array<{
|
|
25
22
|
id: string;
|
|
26
23
|
}>;
|
|
24
|
+
warnings?: Array<{
|
|
25
|
+
level: 'warn';
|
|
26
|
+
message: string;
|
|
27
|
+
}>;
|
|
27
28
|
}
|
|
28
29
|
/**
|
|
29
30
|
* Install a package on the server via multipart upload.
|
package/dist/env/client.js
CHANGED
|
@@ -4,22 +4,14 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { getAuthHeader, isAdmin } from '../auth/index';
|
|
6
6
|
import { apiFetch } from '../transport/apiFetch';
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
/** Configure the server URL for env state operations. */
|
|
10
|
-
export function __setEnvServerUrl(url) {
|
|
11
|
-
serverUrl = url;
|
|
12
|
-
}
|
|
13
|
-
/** Return the configured server URL. */
|
|
14
|
-
export function getEnvServerUrl() {
|
|
15
|
-
return serverUrl;
|
|
16
|
-
}
|
|
7
|
+
import { getEnvServerUrl } from './serverUrl';
|
|
8
|
+
export { getEnvServerUrl, __setEnvServerUrl } from './serverUrl';
|
|
17
9
|
/**
|
|
18
10
|
* Fetch env state for a shard from the server.
|
|
19
11
|
* Returns an empty object if the server has no stored state.
|
|
20
12
|
*/
|
|
21
13
|
export async function fetchEnvState(shardId) {
|
|
22
|
-
const res = await apiFetch(`${
|
|
14
|
+
const res = await apiFetch(`${getEnvServerUrl()}/api/env-state/${encodeURIComponent(shardId)}`, {
|
|
23
15
|
credentials: 'omit',
|
|
24
16
|
});
|
|
25
17
|
if (!res.ok) {
|
|
@@ -41,7 +33,7 @@ export async function putEnvState(shardId, state) {
|
|
|
41
33
|
const headers = { 'Content-Type': 'application/json' };
|
|
42
34
|
if (auth)
|
|
43
35
|
headers['Authorization'] = auth;
|
|
44
|
-
const res = await apiFetch(`${
|
|
36
|
+
const res = await apiFetch(`${getEnvServerUrl()}/api/env-state/${encodeURIComponent(shardId)}`, {
|
|
45
37
|
method: 'PUT',
|
|
46
38
|
headers,
|
|
47
39
|
body: JSON.stringify(state),
|
|
@@ -65,6 +57,7 @@ export async function putEnvState(shardId, state) {
|
|
|
65
57
|
* back server-side.
|
|
66
58
|
*/
|
|
67
59
|
export async function serverInstallPackage(manifest, clientBundle, serverBundle) {
|
|
60
|
+
var _a;
|
|
68
61
|
if (!isAdmin())
|
|
69
62
|
throw new Error('Cannot install: not elevated to admin');
|
|
70
63
|
const auth = getAuthHeader();
|
|
@@ -77,7 +70,7 @@ export async function serverInstallPackage(manifest, clientBundle, serverBundle)
|
|
|
77
70
|
const headers = {};
|
|
78
71
|
if (auth)
|
|
79
72
|
headers['Authorization'] = auth;
|
|
80
|
-
const res = await apiFetch(`${
|
|
73
|
+
const res = await apiFetch(`${getEnvServerUrl()}/api/packages/install`, {
|
|
81
74
|
method: 'POST',
|
|
82
75
|
headers,
|
|
83
76
|
body: form,
|
|
@@ -88,7 +81,7 @@ export async function serverInstallPackage(manifest, clientBundle, serverBundle)
|
|
|
88
81
|
try {
|
|
89
82
|
body = await res.json();
|
|
90
83
|
}
|
|
91
|
-
catch ( /* non-JSON */
|
|
84
|
+
catch ( /* non-JSON */_b) { /* non-JSON */ }
|
|
92
85
|
return {
|
|
93
86
|
ok: false,
|
|
94
87
|
error: typeof body.error === 'string' ? body.error : `HTTP ${res.status}`,
|
|
@@ -96,7 +89,8 @@ export async function serverInstallPackage(manifest, clientBundle, serverBundle)
|
|
|
96
89
|
missing: Array.isArray(body.missing) ? body.missing : undefined,
|
|
97
90
|
};
|
|
98
91
|
}
|
|
99
|
-
|
|
92
|
+
const body = await res.json();
|
|
93
|
+
return { ok: true, warnings: (_a = body.warnings) !== null && _a !== void 0 ? _a : [] };
|
|
100
94
|
}
|
|
101
95
|
/**
|
|
102
96
|
* Uninstall a package from the server.
|
|
@@ -109,7 +103,7 @@ export async function serverUninstallPackage(id) {
|
|
|
109
103
|
const headers = { 'Content-Type': 'application/json' };
|
|
110
104
|
if (auth)
|
|
111
105
|
headers['Authorization'] = auth;
|
|
112
|
-
const res = await apiFetch(`${
|
|
106
|
+
const res = await apiFetch(`${getEnvServerUrl()}/api/packages/uninstall`, {
|
|
113
107
|
method: 'POST',
|
|
114
108
|
headers,
|
|
115
109
|
body: JSON.stringify({ id }),
|
|
@@ -124,7 +118,7 @@ export async function serverUninstallPackage(id) {
|
|
|
124
118
|
* Fetch the list of packages installed on the server.
|
|
125
119
|
*/
|
|
126
120
|
export async function fetchServerPackages() {
|
|
127
|
-
const res = await apiFetch(`${
|
|
121
|
+
const res = await apiFetch(`${getEnvServerUrl()}/api/packages`, { credentials: 'omit' });
|
|
128
122
|
if (!res.ok)
|
|
129
123
|
return [];
|
|
130
124
|
return await res.json();
|
package/dist/gestures/index.d.ts
CHANGED
|
@@ -4,3 +4,20 @@ export type { GestureRegistry } from './gestureRegistry';
|
|
|
4
4
|
export type { GestureHandle, GestureOptions, GestureType, Axis, ClaimPriority, ClaimEntry, PanEvent, ScrollEvent, ButtonEvent, } from './types';
|
|
5
5
|
/** Internal utility — used by framework gesture sites. Not re-exported via api.ts. */
|
|
6
6
|
export declare function ancestorCount(el: Element): number;
|
|
7
|
+
/**
|
|
8
|
+
* Width of the reserved gutter at each side edge, in CSS pixels.
|
|
9
|
+
* Pointer-downs that land within this strip of either side of a swipe-aware
|
|
10
|
+
* surface (carousel, future side drawers) initiate the gesture unconditionally
|
|
11
|
+
* — content-specific bailouts (editable target, native horizontal scroll) are
|
|
12
|
+
* suppressed. Outside the gutter, those bailouts still apply so taps on
|
|
13
|
+
* inputs and horizontally-scrollable regions behave normally.
|
|
14
|
+
*/
|
|
15
|
+
export declare const EDGE_PX = 24;
|
|
16
|
+
/**
|
|
17
|
+
* Always-on diagnostic for premature gesture-end paths (claim stolen, foreign
|
|
18
|
+
* pointercancel, foreign pointerup, etc.). The log only fires on anomalies, so
|
|
19
|
+
* a healthy drag is silent in production. Include `pointerType` so the user
|
|
20
|
+
* can tell touch from mouse from pen at a glance — the touch-only auto-release
|
|
21
|
+
* bug class doesn't reproduce with a mouse.
|
|
22
|
+
*/
|
|
23
|
+
export declare function logGesture(label: string, ev: PointerEvent | null, extra?: Record<string, unknown>): void;
|
package/dist/gestures/index.js
CHANGED
|
@@ -10,3 +10,30 @@ export function ancestorCount(el) {
|
|
|
10
10
|
}
|
|
11
11
|
return n;
|
|
12
12
|
}
|
|
13
|
+
/**
|
|
14
|
+
* Width of the reserved gutter at each side edge, in CSS pixels.
|
|
15
|
+
* Pointer-downs that land within this strip of either side of a swipe-aware
|
|
16
|
+
* surface (carousel, future side drawers) initiate the gesture unconditionally
|
|
17
|
+
* — content-specific bailouts (editable target, native horizontal scroll) are
|
|
18
|
+
* suppressed. Outside the gutter, those bailouts still apply so taps on
|
|
19
|
+
* inputs and horizontally-scrollable regions behave normally.
|
|
20
|
+
*/
|
|
21
|
+
export const EDGE_PX = 24;
|
|
22
|
+
/**
|
|
23
|
+
* Always-on diagnostic for premature gesture-end paths (claim stolen, foreign
|
|
24
|
+
* pointercancel, foreign pointerup, etc.). The log only fires on anomalies, so
|
|
25
|
+
* a healthy drag is silent in production. Include `pointerType` so the user
|
|
26
|
+
* can tell touch from mouse from pen at a glance — the touch-only auto-release
|
|
27
|
+
* bug class doesn't reproduce with a mouse.
|
|
28
|
+
*/
|
|
29
|
+
export function logGesture(label, ev, extra) {
|
|
30
|
+
var _a, _b;
|
|
31
|
+
if (typeof console === 'undefined')
|
|
32
|
+
return;
|
|
33
|
+
const tgt = ev === null || ev === void 0 ? void 0 : ev.target;
|
|
34
|
+
const tag = (_a = tgt === null || tgt === void 0 ? void 0 : tgt.tagName) !== null && _a !== void 0 ? _a : '-';
|
|
35
|
+
const raw = (_b = tgt === null || tgt === void 0 ? void 0 : tgt.className) !== null && _b !== void 0 ? _b : '';
|
|
36
|
+
const cls = typeof raw === 'string' ? raw : '';
|
|
37
|
+
// eslint-disable-next-line no-console
|
|
38
|
+
console.log('[sh3:gesture]', label, Object.assign({ pointerId: ev === null || ev === void 0 ? void 0 : ev.pointerId, pointerType: ev === null || ev === void 0 ? void 0 : ev.pointerType, type: ev === null || ev === void 0 ? void 0 : ev.type, target: cls ? `${tag}.${cls}` : tag }, extra));
|
|
39
|
+
}
|
package/dist/keys/client.js
CHANGED
|
@@ -9,6 +9,8 @@
|
|
|
9
9
|
import { ConsentDeniedError, ScopeEscalationError } from './types';
|
|
10
10
|
import { requestConsent } from './consent.svelte';
|
|
11
11
|
import { emit } from './revocation-bus.svelte';
|
|
12
|
+
import { apiFetch } from '../transport/apiFetch';
|
|
13
|
+
import { getEnvServerUrl } from '../env/serverUrl';
|
|
12
14
|
export function createShardKeysApi(params) {
|
|
13
15
|
const { shardId, shardPermissions } = params;
|
|
14
16
|
const assertScopesSubset = (scopes) => {
|
|
@@ -24,18 +26,16 @@ export function createShardKeysApi(params) {
|
|
|
24
26
|
const approved = await requestConsent(shardId, opts);
|
|
25
27
|
if (!approved)
|
|
26
28
|
throw new ConsentDeniedError();
|
|
27
|
-
const ticketRes = await
|
|
29
|
+
const ticketRes = await apiFetch(`${getEnvServerUrl()}/api/keys/consent`, {
|
|
28
30
|
method: 'POST',
|
|
29
|
-
credentials: 'include',
|
|
30
31
|
headers: { 'content-type': 'application/json' },
|
|
31
32
|
body: JSON.stringify(Object.assign({ shardId }, opts)),
|
|
32
33
|
});
|
|
33
34
|
if (!ticketRes.ok)
|
|
34
35
|
throw new Error(`Consent ticket failed: ${ticketRes.status}`);
|
|
35
36
|
const { ticket } = await ticketRes.json();
|
|
36
|
-
const mintRes = await
|
|
37
|
+
const mintRes = await apiFetch(`${getEnvServerUrl()}/api/keys`, {
|
|
37
38
|
method: 'POST',
|
|
38
|
-
credentials: 'include',
|
|
39
39
|
headers: { 'content-type': 'application/json' },
|
|
40
40
|
body: JSON.stringify({ ticket }),
|
|
41
41
|
});
|
|
@@ -44,16 +44,15 @@ export function createShardKeysApi(params) {
|
|
|
44
44
|
return mintRes.json();
|
|
45
45
|
},
|
|
46
46
|
async list() {
|
|
47
|
-
const res = await
|
|
47
|
+
const res = await apiFetch(`${getEnvServerUrl()}/api/keys`);
|
|
48
48
|
if (!res.ok)
|
|
49
49
|
throw new Error(`List failed: ${res.status}`);
|
|
50
50
|
const all = (await res.json());
|
|
51
51
|
return all.filter((k) => k.mintedByShardId === shardId);
|
|
52
52
|
},
|
|
53
53
|
async revoke(id) {
|
|
54
|
-
const res = await
|
|
54
|
+
const res = await apiFetch(`${getEnvServerUrl()}/api/keys/${encodeURIComponent(id)}`, {
|
|
55
55
|
method: 'DELETE',
|
|
56
|
-
credentials: 'include',
|
|
57
56
|
});
|
|
58
57
|
if (!res.ok && res.status !== 404)
|
|
59
58
|
throw new Error(`Revoke failed: ${res.status}`);
|
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
* The bus is populated by a server-sent events stream on /api/keys/events
|
|
7
7
|
* (wired by the sh3 runtime at boot) and/or by local revoke() calls.
|
|
8
8
|
*/
|
|
9
|
+
import { getEnvServerUrl } from '../env/serverUrl';
|
|
10
|
+
import { getAuthToken } from '../transport/authToken';
|
|
9
11
|
const handlersByShard = new Map();
|
|
10
12
|
/**
|
|
11
13
|
* Recently-emitted (shardId → Set<keyId>) with per-entry TTL timers.
|
|
@@ -78,7 +80,15 @@ export function emit(shardId, keyId) {
|
|
|
78
80
|
export function startServerSideStream() {
|
|
79
81
|
if (typeof EventSource === 'undefined')
|
|
80
82
|
return () => { };
|
|
81
|
-
|
|
83
|
+
// EventSource cannot send custom headers, so cross-origin auth (Tauri remote)
|
|
84
|
+
// is handled by passing the session token as a query param. Same-origin
|
|
85
|
+
// builds fall back to cookies via withCredentials.
|
|
86
|
+
const base = getEnvServerUrl();
|
|
87
|
+
const token = getAuthToken();
|
|
88
|
+
const url = token
|
|
89
|
+
? `${base}/api/keys/events?token=${encodeURIComponent(token)}`
|
|
90
|
+
: `${base}/api/keys/events`;
|
|
91
|
+
const es = new EventSource(url, token ? {} : { withCredentials: true });
|
|
82
92
|
es.onmessage = (msg) => {
|
|
83
93
|
try {
|
|
84
94
|
const ev = JSON.parse(msg.data);
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
import SlotContainer from '../SlotContainer.svelte';
|
|
20
20
|
import SlotDropZone from '../SlotDropZone.svelte';
|
|
21
21
|
import { claim, revoke, isOwner } from '../../gestures/pointerClaim';
|
|
22
|
-
import { ancestorCount } from '../../gestures';
|
|
22
|
+
import { ancestorCount, EDGE_PX, logGesture } from '../../gestures';
|
|
23
23
|
|
|
24
24
|
let {
|
|
25
25
|
node,
|
|
@@ -62,6 +62,27 @@
|
|
|
62
62
|
let dragging = $state(false);
|
|
63
63
|
let claimed = $state(false);
|
|
64
64
|
let activePointerId: number | null = null;
|
|
65
|
+
/**
|
|
66
|
+
* Set to true when the pointer-down landed inside the left/right edge
|
|
67
|
+
* gutter. Gutter-initiated drags get the "invincible" treatment: once
|
|
68
|
+
* the axis-lock threshold is crossed we explicitly call
|
|
69
|
+
* `setPointerCapture` on the carousel container so the descendant the
|
|
70
|
+
* touch originally landed on (textarea, scroll region, preview canvas
|
|
71
|
+
* — see the earlier `cancel-our-id` log targets) no longer receives the
|
|
72
|
+
* pointer stream and therefore cannot fire `pointercancel` to abort us.
|
|
73
|
+
*
|
|
74
|
+
* Mid-track drags don't get capture — the documented contract is that
|
|
75
|
+
* a descendant claiming the gesture there wins.
|
|
76
|
+
*/
|
|
77
|
+
let startedInGutter = false;
|
|
78
|
+
/**
|
|
79
|
+
* Soft window (`performance.now()` deadline) during which a single
|
|
80
|
+
* `pointercancel` is treated as the synthetic Android transfer-cancel
|
|
81
|
+
* that fires when `setPointerCapture` moves implicit capture from a
|
|
82
|
+
* descendant up to the container. Without this filter, the carousel's
|
|
83
|
+
* own capture call would immediately abort itself.
|
|
84
|
+
*/
|
|
85
|
+
let ignoreCancelUntil = 0;
|
|
65
86
|
|
|
66
87
|
type PointerSnapshot = { id: number; x: number; y: number; t: number };
|
|
67
88
|
let downSnap: PointerSnapshot | null = null;
|
|
@@ -72,6 +93,16 @@
|
|
|
72
93
|
const COMMIT_FRACTION = 0.4;
|
|
73
94
|
const COMMIT_VELOCITY = 500;
|
|
74
95
|
const RUBBER_BAND_MAX_FRACTION = 0.3;
|
|
96
|
+
/** Window (ms) during which the post-transfer pointercancel is swallowed. */
|
|
97
|
+
const TRANSFER_CANCEL_WINDOW_MS = 100;
|
|
98
|
+
|
|
99
|
+
function dbg(label: string, ev: PointerEvent | null): void {
|
|
100
|
+
logGesture(`carousel:${label}`, ev, {
|
|
101
|
+
claimed,
|
|
102
|
+
dragging,
|
|
103
|
+
downId: downSnap?.id,
|
|
104
|
+
});
|
|
105
|
+
}
|
|
75
106
|
/**
|
|
76
107
|
* If the pointer takes longer than this to cross the axis-lock threshold,
|
|
77
108
|
* treat the gesture as text-selection intent (slow drag over text) and
|
|
@@ -80,16 +111,45 @@
|
|
|
80
111
|
*/
|
|
81
112
|
const SLOW_DRAG_TIMEOUT_MS = 250;
|
|
82
113
|
|
|
114
|
+
/**
|
|
115
|
+
* True only when an ancestor has scrollable horizontal overflow that
|
|
116
|
+
* *actually* overflows. `overflow-x: auto` on a wrapper that doesn't
|
|
117
|
+
* exceed its clientWidth used to disqualify every swipe inside it — a
|
|
118
|
+
* common false positive for body views that set `overflow: auto` to get
|
|
119
|
+
* vertical scrolling. We now also require `scrollWidth > clientWidth`
|
|
120
|
+
* so only genuinely scrollable regions block the carousel.
|
|
121
|
+
*/
|
|
83
122
|
function hasNativeHorizontalScroll(target: EventTarget | null): boolean {
|
|
84
123
|
let el = target as HTMLElement | null;
|
|
85
124
|
while (el && el !== containerEl) {
|
|
86
125
|
const ox = getComputedStyle(el).overflowX;
|
|
87
|
-
if (ox === 'auto' || ox === 'scroll')
|
|
126
|
+
if ((ox === 'auto' || ox === 'scroll') && el.scrollWidth > el.clientWidth) {
|
|
127
|
+
return true;
|
|
128
|
+
}
|
|
88
129
|
el = el.parentElement;
|
|
89
130
|
}
|
|
90
131
|
return false;
|
|
91
132
|
}
|
|
92
133
|
|
|
134
|
+
/**
|
|
135
|
+
* True when the pointer-down x sits in the left or right EDGE_PX strip of
|
|
136
|
+
* the carousel container. Edge-gutter pointer-downs claim the swipe
|
|
137
|
+
* regardless of editable-target or native-horizontal-scroll content
|
|
138
|
+
* underneath — the gutter is documented as an unconditional swipe
|
|
139
|
+
* initiation zone. Without this, any input or `overflow: auto` region
|
|
140
|
+
* touching the edge silently kills edge swipes.
|
|
141
|
+
*/
|
|
142
|
+
function isInEdgeGutter(clientX: number): boolean {
|
|
143
|
+
if (!containerEl || containerWidth === 0) return false;
|
|
144
|
+
// rect.left for the offset, containerWidth (reactive, ResizeObserver-fed)
|
|
145
|
+
// for the width — getBoundingClientRect().width is 0 under happy-dom and
|
|
146
|
+
// similar non-layout DOMs, while containerWidth is seeded from the
|
|
147
|
+
// ResizeObserver path used everywhere else in this component.
|
|
148
|
+
const rect = containerEl.getBoundingClientRect();
|
|
149
|
+
const local = clientX - rect.left;
|
|
150
|
+
return local < EDGE_PX || local > containerWidth - EDGE_PX;
|
|
151
|
+
}
|
|
152
|
+
|
|
93
153
|
function isEditableTarget(target: EventTarget | null): boolean {
|
|
94
154
|
const el = target as HTMLElement | null;
|
|
95
155
|
if (!el || typeof el.tagName !== 'string') return false;
|
|
@@ -118,16 +178,28 @@
|
|
|
118
178
|
}
|
|
119
179
|
|
|
120
180
|
function onPointerDown(ev: PointerEvent) {
|
|
121
|
-
|
|
122
|
-
|
|
181
|
+
// Structural bails — apply regardless of where the touch lands. Without
|
|
182
|
+
// these the carousel can't even set up a meaningful gesture.
|
|
123
183
|
if (tabCount < 2) return;
|
|
124
184
|
if (containerWidth === 0) return;
|
|
125
185
|
// Multi-touch (pinch-zoom etc.) is not a swipe — bail.
|
|
126
186
|
if (ev.isPrimary === false) return;
|
|
187
|
+
// Edge gutter override: pointer-downs in the left/right EDGE_PX strip
|
|
188
|
+
// claim the swipe unconditionally. This is the published invariant for
|
|
189
|
+
// edge initiation; bailing here would re-introduce the bug where any
|
|
190
|
+
// editable target or overflow-auto wrapper sitting near the screen edge
|
|
191
|
+
// silently kills the gesture.
|
|
192
|
+
const inGutter = isInEdgeGutter(ev.clientX);
|
|
193
|
+
if (!inGutter) {
|
|
194
|
+
if (isEditableTarget(ev.target)) return;
|
|
195
|
+
if (hasNativeHorizontalScroll(ev.target)) return;
|
|
196
|
+
}
|
|
127
197
|
const depth = containerEl ? ancestorCount(containerEl) : 0;
|
|
128
198
|
const claimGranted = claim(ev.pointerId, { ownerId: 'sh3:carousel', axis: 'x', priority: 'normal', depth });
|
|
129
199
|
if (!claimGranted) return;
|
|
130
200
|
activePointerId = ev.pointerId;
|
|
201
|
+
startedInGutter = inGutter;
|
|
202
|
+
ignoreCancelUntil = 0;
|
|
131
203
|
downSnap = { id: ev.pointerId, x: ev.clientX, y: ev.clientY, t: performance.now() };
|
|
132
204
|
lastSnap = { ...downSnap };
|
|
133
205
|
dragging = false;
|
|
@@ -140,7 +212,11 @@
|
|
|
140
212
|
|
|
141
213
|
function onPointerMove(ev: PointerEvent) {
|
|
142
214
|
if (!downSnap || ev.pointerId !== downSnap.id) return;
|
|
143
|
-
if (!isOwner(ev.pointerId, 'sh3:carousel')) {
|
|
215
|
+
if (!isOwner(ev.pointerId, 'sh3:carousel')) {
|
|
216
|
+
dbg('claim-stolen', ev);
|
|
217
|
+
endGesture();
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
144
220
|
const dx = ev.clientX - downSnap.x;
|
|
145
221
|
const dy = ev.clientY - downSnap.y;
|
|
146
222
|
if (!claimed) {
|
|
@@ -159,12 +235,29 @@
|
|
|
159
235
|
if (!horizDominates) return;
|
|
160
236
|
claimed = true;
|
|
161
237
|
dragging = true;
|
|
162
|
-
//
|
|
163
|
-
//
|
|
164
|
-
//
|
|
165
|
-
//
|
|
166
|
-
//
|
|
167
|
-
//
|
|
238
|
+
// Gutter-initiated drags get the "invincible" guarantee: transfer
|
|
239
|
+
// implicit pointer capture from the descendant the touch landed on
|
|
240
|
+
// (a TEXTAREA, scroll region, etc.) up to the carousel container so
|
|
241
|
+
// those descendants can no longer fire pointercancel mid-pan. The
|
|
242
|
+
// transfer itself fires one synthetic pointercancel on Android (see
|
|
243
|
+
// CarouselTabs commit 638d75a for the original incident); the
|
|
244
|
+
// ignoreCancelUntil window below swallows just that one cancel —
|
|
245
|
+
// every other cancel still aborts as before.
|
|
246
|
+
//
|
|
247
|
+
// Mid-track drags intentionally skip this: the contract there is
|
|
248
|
+
// that a descendant claiming the gesture wins, matching the
|
|
249
|
+
// "carousel aborts if it's caught by an inner element" behavior
|
|
250
|
+
// the rest of the bail logic already encodes.
|
|
251
|
+
if (startedInGutter && containerEl) {
|
|
252
|
+
try {
|
|
253
|
+
containerEl.setPointerCapture(ev.pointerId);
|
|
254
|
+
ignoreCancelUntil = performance.now() + TRANSFER_CANCEL_WINDOW_MS;
|
|
255
|
+
} catch {
|
|
256
|
+
// setPointerCapture can throw if the pointer is no longer
|
|
257
|
+
// active (race with platform-level cancel). Safe to swallow —
|
|
258
|
+
// the gesture will run on implicit capture as before.
|
|
259
|
+
}
|
|
260
|
+
}
|
|
168
261
|
// Clear any selection that began during the pre-claim window so
|
|
169
262
|
// it doesn't visually streak across the slide while dragging.
|
|
170
263
|
if (typeof window !== 'undefined') {
|
|
@@ -177,8 +270,14 @@
|
|
|
177
270
|
}
|
|
178
271
|
|
|
179
272
|
function onPointerUp(ev: PointerEvent) {
|
|
180
|
-
if (!downSnap
|
|
181
|
-
|
|
273
|
+
if (!downSnap) return;
|
|
274
|
+
// Filter by pointer id. A pointerup for a different pointer (e.g. a
|
|
275
|
+
// second finger lifting, a stylus releasing while a finger drag is
|
|
276
|
+
// active, or a phantom multi-pointer) must NOT tear down our gesture —
|
|
277
|
+
// earlier behavior here was to call endGesture() on any unrelated
|
|
278
|
+
// pointerup, which dropped legitimate drags on multi-touch devices.
|
|
279
|
+
if (ev.pointerId !== downSnap.id) {
|
|
280
|
+
dbg('pointerup-other-id', ev);
|
|
182
281
|
return;
|
|
183
282
|
}
|
|
184
283
|
const dx = ev.clientX - downSnap.x;
|
|
@@ -198,12 +297,47 @@
|
|
|
198
297
|
* mid-drag would be read as a release at the current dx and trip
|
|
199
298
|
* the commit threshold, "auto-completing" the swipe with the
|
|
200
299
|
* finger still down.
|
|
300
|
+
*
|
|
301
|
+
* MUST filter by pointer id. The previous unconditional implementation
|
|
302
|
+
* meant any pointercancel — palm contact, ghost touches, a stylus
|
|
303
|
+
* cancellation while a finger drag was active — terminated the carousel
|
|
304
|
+
* gesture. That's the most likely cause of "drag releases mid-pan on
|
|
305
|
+
* some screens" reports.
|
|
201
306
|
*/
|
|
202
|
-
function onPointerCancel(
|
|
307
|
+
function onPointerCancel(ev: PointerEvent) {
|
|
308
|
+
if (!downSnap) return;
|
|
309
|
+
if (ev.pointerId !== downSnap.id) {
|
|
310
|
+
dbg('cancel-other-id', ev);
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
// Single-shot swallow for the synthetic Android transfer-cancel that
|
|
314
|
+
// fires right after we call `setPointerCapture` on a gutter swipe.
|
|
315
|
+
// Clear the deadline once consumed so a real cancel later in the same
|
|
316
|
+
// gesture still aborts normally.
|
|
317
|
+
if (performance.now() < ignoreCancelUntil) {
|
|
318
|
+
dbg('cancel-ignored-transfer', ev);
|
|
319
|
+
ignoreCancelUntil = 0;
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
dbg('cancel-our-id', ev);
|
|
203
323
|
endGesture();
|
|
204
324
|
}
|
|
205
325
|
|
|
206
326
|
function endGesture() {
|
|
327
|
+
// Release explicit pointer capture if we acquired it for a gutter
|
|
328
|
+
// swipe. Browsers auto-release on pointerup/cancel, but doing it
|
|
329
|
+
// defensively here keeps the container's `hasPointerCapture` state
|
|
330
|
+
// clean if endGesture is called from a path that didn't naturally
|
|
331
|
+
// release (claim-stolen, unmount, etc.).
|
|
332
|
+
if (startedInGutter && containerEl && activePointerId !== null) {
|
|
333
|
+
try {
|
|
334
|
+
if (containerEl.hasPointerCapture?.(activePointerId)) {
|
|
335
|
+
containerEl.releasePointerCapture(activePointerId);
|
|
336
|
+
}
|
|
337
|
+
} catch {
|
|
338
|
+
// Defensive — happy-dom / older browsers may not implement it.
|
|
339
|
+
}
|
|
340
|
+
}
|
|
207
341
|
if (activePointerId !== null) {
|
|
208
342
|
revoke(activePointerId, 'sh3:carousel');
|
|
209
343
|
activePointerId = null;
|
|
@@ -216,6 +350,8 @@
|
|
|
216
350
|
dragging = false;
|
|
217
351
|
claimed = false;
|
|
218
352
|
dragDelta = 0;
|
|
353
|
+
startedInGutter = false;
|
|
354
|
+
ignoreCancelUntil = 0;
|
|
219
355
|
}
|
|
220
356
|
|
|
221
357
|
function commitOrSnap(dx: number, velocity: number) {
|