sh3-core 0.17.2 → 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.
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 +119 -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 +361 -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 +10 -0
  84. package/dist/shards/ctx-fetch.test.d.ts +1 -0
  85. package/dist/shards/ctx-fetch.test.js +66 -0
  86. package/dist/shards/types.d.ts +13 -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
@@ -0,0 +1,106 @@
1
+ /**
2
+ * A reference to a document passed to file handler callbacks and app launch args.
3
+ */
4
+ export interface FileRef {
5
+ path: string;
6
+ tenantId: string;
7
+ /** True when the file was identified as binary content. */
8
+ binary: boolean;
9
+ }
10
+ /** A single header pattern rule. */
11
+ export type FileHandlerPattern = {
12
+ type: 'startsWith';
13
+ value: string;
14
+ } | {
15
+ type: 'includes';
16
+ value: string;
17
+ } | {
18
+ type: 'regex';
19
+ pattern: string;
20
+ flags?: string;
21
+ };
22
+ /**
23
+ * Optional header-based disambiguation used when multiple handlers share
24
+ * an extension. The dispatcher reads the file head and tests these rules
25
+ * to narrow the candidate set.
26
+ */
27
+ export interface FileHandlerHeader {
28
+ /**
29
+ * Max bytes/chars to read from the file head.
30
+ * @default 256
31
+ */
32
+ readBytes?: number;
33
+ /**
34
+ * Declarative patterns tested in order — first match wins.
35
+ * Evaluated before `predicate`.
36
+ */
37
+ patterns?: FileHandlerPattern[];
38
+ /**
39
+ * Escape-hatch for logic that cannot be expressed as patterns.
40
+ * Receives `string` for text files, `Uint8Array` for binary.
41
+ */
42
+ predicate?: (header: string | Uint8Array) => boolean;
43
+ }
44
+ /** File matching rules for a handler. */
45
+ export interface FileHandlerMatch {
46
+ /**
47
+ * File extensions this handler claims, e.g. `['.svg', '.svgz']`.
48
+ * Compared case-insensitively by the dispatcher.
49
+ */
50
+ extensions: string[];
51
+ /**
52
+ * When true, this handler accepts binary files.
53
+ * @default false
54
+ */
55
+ binary?: boolean;
56
+ /** Optional header rules for disambiguation when extensions collide. */
57
+ header?: FileHandlerHeader;
58
+ }
59
+ /**
60
+ * How the dispatcher should open the matched file.
61
+ *
62
+ * - `view` — the shard's callback is called; the shard mounts the view
63
+ * itself via its own `ctx` closure. Placement is the dispatcher's concern.
64
+ * - `app` — the dispatcher calls `launchApp(appId, { args: { file } })`.
65
+ * The app reads `ctx.args.file` in `activate()`.
66
+ */
67
+ export type FileHandlerOpen = {
68
+ type: 'view';
69
+ /**
70
+ * Called by the dispatcher with the resolved file. The shard closes
71
+ * over its own ShardContext — it wires the file to internal state and
72
+ * mounts its view using its existing shell access.
73
+ */
74
+ open: (file: FileRef) => void | Promise<void>;
75
+ } | {
76
+ type: 'app';
77
+ /** Id of the app to launch. Must be registered. */
78
+ appId: string;
79
+ };
80
+ /**
81
+ * Descriptor registered at contribution point `sh3.file-handler`.
82
+ *
83
+ * @example
84
+ * ```ts
85
+ * ctx.contributions.register('sh3.file-handler', {
86
+ * label: 'SVG Viewer',
87
+ * match: { extensions: ['.svg'] },
88
+ * open: {
89
+ * type: 'view',
90
+ * open: (file) => { pendingFile.set(file); shell.openViewInCurrentLayout(viewId); },
91
+ * },
92
+ * } satisfies FileHandlerDescriptor);
93
+ * ```
94
+ */
95
+ export interface FileHandlerDescriptor {
96
+ /** Human-readable name shown in "open with" UI. */
97
+ label: string;
98
+ match: FileHandlerMatch;
99
+ open: FileHandlerOpen;
100
+ /**
101
+ * Tie-breaking priority. Higher wins when multiple handlers match after
102
+ * extension + header filtering.
103
+ * @default 0
104
+ */
105
+ priority?: number;
106
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,6 @@
1
+ import type { GestureOptions, GestureHandle } from './types';
2
+ export interface GestureRegistry {
3
+ register(opts: GestureOptions): GestureHandle;
4
+ destroy(): void;
5
+ }
6
+ export declare function createGestureRegistry(defaultContainer: HTMLElement | null): GestureRegistry;
@@ -0,0 +1,190 @@
1
+ import { claim, revoke } from './pointerClaim';
2
+ let _nextId = 0;
3
+ function nextId() { return `sh3:gesture:${_nextId++}`; }
4
+ function ancestorCount(el) {
5
+ let n = 0;
6
+ let cur = el;
7
+ while (cur) {
8
+ n++;
9
+ cur = cur.parentElement;
10
+ }
11
+ return n;
12
+ }
13
+ function activeModifiers(e) {
14
+ const mods = [];
15
+ if (e.ctrlKey)
16
+ mods.push('Control');
17
+ if (e.shiftKey)
18
+ mods.push('Shift');
19
+ if (e.altKey)
20
+ mods.push('Alt');
21
+ if (e.metaKey)
22
+ mods.push('Meta');
23
+ return mods;
24
+ }
25
+ function modifiersMatch(required, active) {
26
+ if (required.length !== active.length)
27
+ return false;
28
+ return required.every(m => active.includes(m));
29
+ }
30
+ export function createGestureRegistry(defaultContainer) {
31
+ if (!defaultContainer) {
32
+ return { register: () => ({ enable() { }, disable() { }, destroy() { } }), destroy() { } };
33
+ }
34
+ const handlers = [];
35
+ const activePointers = new Map();
36
+ function onPointerDown(e) {
37
+ var _a, _b, _c, _d, _e, _f, _g;
38
+ if (!e.isPrimary)
39
+ return;
40
+ const mods = activeModifiers(e);
41
+ for (const h of handlers) {
42
+ if (!h.enabled || h.destroyed)
43
+ continue;
44
+ if (h.opts.type === 'pan' || h.opts.type === 'drag') {
45
+ const opts = h.opts;
46
+ const btn = (_a = opts.button) !== null && _a !== void 0 ? _a : 0;
47
+ if (e.button !== btn)
48
+ continue;
49
+ if (!modifiersMatch((_b = opts.modifiers) !== null && _b !== void 0 ? _b : [], mods))
50
+ continue;
51
+ const container = (_c = opts.container) !== null && _c !== void 0 ? _c : defaultContainer;
52
+ if (!container || !container.contains(e.target))
53
+ continue;
54
+ const depth = ancestorCount(container);
55
+ const granted = claim(e.pointerId, { ownerId: h.id, axis: (_d = opts.axis) !== null && _d !== void 0 ? _d : 'xy', priority: 'normal', depth });
56
+ if (!granted)
57
+ continue;
58
+ activePointers.set(e.pointerId, { handlerId: h.id, startX: e.clientX, startY: e.clientY, lastX: e.clientX, lastY: e.clientY });
59
+ const ev = { pointerId: e.pointerId, x: e.clientX, y: e.clientY, dx: 0, dy: 0 };
60
+ opts.onStart(ev);
61
+ document.addEventListener('pointermove', onPointerMove);
62
+ document.addEventListener('pointerup', onPointerUp);
63
+ document.addEventListener('pointercancel', onPointerCancel);
64
+ return;
65
+ }
66
+ if (h.opts.type === 'button') {
67
+ const opts = h.opts;
68
+ if (e.button !== opts.button)
69
+ continue;
70
+ if (!modifiersMatch((_e = opts.modifiers) !== null && _e !== void 0 ? _e : [], mods))
71
+ continue;
72
+ const container = (_f = opts.container) !== null && _f !== void 0 ? _f : defaultContainer;
73
+ if (!container || !container.contains(e.target))
74
+ continue;
75
+ const on = (_g = opts.on) !== null && _g !== void 0 ? _g : 'down';
76
+ if (on === 'down' || on === 'both') {
77
+ opts.onButton({ button: e.button, modifiers: mods, x: e.clientX, y: e.clientY });
78
+ }
79
+ }
80
+ }
81
+ }
82
+ function onPointerMove(e) {
83
+ const state = activePointers.get(e.pointerId);
84
+ if (!state)
85
+ return;
86
+ const h = handlers.find(h => h.id === state.handlerId);
87
+ if (!h || !h.enabled)
88
+ return;
89
+ const opts = h.opts;
90
+ const ev = {
91
+ pointerId: e.pointerId,
92
+ x: e.clientX, y: e.clientY,
93
+ dx: e.clientX - state.startX, dy: e.clientY - state.startY,
94
+ };
95
+ state.lastX = e.clientX;
96
+ state.lastY = e.clientY;
97
+ opts.onMove(ev);
98
+ }
99
+ function onPointerUp(e) {
100
+ var _a;
101
+ const state = activePointers.get(e.pointerId);
102
+ if (!state)
103
+ return;
104
+ activePointers.delete(e.pointerId);
105
+ revoke(e.pointerId, state.handlerId);
106
+ document.removeEventListener('pointermove', onPointerMove);
107
+ document.removeEventListener('pointerup', onPointerUp);
108
+ document.removeEventListener('pointercancel', onPointerCancel);
109
+ const h = handlers.find(h => h.id === state.handlerId);
110
+ if (!h)
111
+ return;
112
+ const opts = h.opts;
113
+ const dx = e.clientX - state.startX;
114
+ const dy = e.clientY - state.startY;
115
+ if (opts.type === 'drag') {
116
+ const threshold = (_a = opts.clickThreshold) !== null && _a !== void 0 ? _a : 6;
117
+ const dist = Math.sqrt(dx * dx + dy * dy);
118
+ opts.onEnd({ pointerId: e.pointerId, x: e.clientX, y: e.clientY, dx, dy, cancelled: false, wasClick: dist < threshold });
119
+ }
120
+ else {
121
+ opts.onEnd({ pointerId: e.pointerId, x: e.clientX, y: e.clientY, dx, dy, cancelled: false });
122
+ }
123
+ }
124
+ function onPointerCancel(e) {
125
+ const state = activePointers.get(e.pointerId);
126
+ if (!state)
127
+ return;
128
+ activePointers.delete(e.pointerId);
129
+ revoke(e.pointerId, state.handlerId);
130
+ document.removeEventListener('pointermove', onPointerMove);
131
+ document.removeEventListener('pointerup', onPointerUp);
132
+ document.removeEventListener('pointercancel', onPointerCancel);
133
+ const h = handlers.find(h => h.id === state.handlerId);
134
+ if (!h)
135
+ return;
136
+ const opts = h.opts;
137
+ const dx = state.lastX - state.startX;
138
+ const dy = state.lastY - state.startY;
139
+ if (opts.type === 'drag') {
140
+ opts.onEnd({ pointerId: e.pointerId, x: state.lastX, y: state.lastY, dx, dy, cancelled: true, wasClick: false });
141
+ }
142
+ else {
143
+ opts.onEnd({ pointerId: e.pointerId, x: state.lastX, y: state.lastY, dx, dy, cancelled: true });
144
+ }
145
+ }
146
+ function onWheel(e) {
147
+ var _a, _b;
148
+ const mods = activeModifiers(e);
149
+ for (const h of handlers) {
150
+ if (!h.enabled || h.destroyed)
151
+ continue;
152
+ if (h.opts.type !== 'scroll')
153
+ continue;
154
+ const opts = h.opts;
155
+ if (!modifiersMatch((_a = opts.modifiers) !== null && _a !== void 0 ? _a : [], mods))
156
+ continue;
157
+ const container = (_b = opts.container) !== null && _b !== void 0 ? _b : defaultContainer;
158
+ if (!container || !container.contains(e.target))
159
+ continue;
160
+ opts.onScroll({ deltaX: e.deltaX, deltaY: e.deltaY, deltaZ: e.deltaZ, modifiers: mods });
161
+ return;
162
+ }
163
+ }
164
+ defaultContainer.addEventListener('pointerdown', onPointerDown);
165
+ defaultContainer.addEventListener('wheel', onWheel);
166
+ return {
167
+ register(opts) {
168
+ const id = nextId();
169
+ const entry = { id, opts, enabled: false, destroyed: false };
170
+ handlers.push(entry);
171
+ return {
172
+ enable() { if (!entry.destroyed)
173
+ entry.enabled = true; },
174
+ disable() { entry.enabled = false; },
175
+ destroy() {
176
+ entry.enabled = false;
177
+ entry.destroyed = true;
178
+ const idx = handlers.indexOf(entry);
179
+ if (idx >= 0)
180
+ handlers.splice(idx, 1);
181
+ },
182
+ };
183
+ },
184
+ destroy() {
185
+ defaultContainer.removeEventListener('pointerdown', onPointerDown);
186
+ defaultContainer.removeEventListener('wheel', onWheel);
187
+ handlers.length = 0;
188
+ },
189
+ };
190
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,119 @@
1
+ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
2
+ import { createGestureRegistry } from './gestureRegistry';
3
+ import { __resetForTest } from './pointerClaim';
4
+ beforeEach(() => {
5
+ __resetForTest();
6
+ });
7
+ afterEach(() => {
8
+ document.body.innerHTML = '';
9
+ });
10
+ function makeEl() {
11
+ const el = document.createElement('div');
12
+ document.body.appendChild(el);
13
+ return el;
14
+ }
15
+ function fire(target, type, init) {
16
+ target.dispatchEvent(new PointerEvent(type, Object.assign({ bubbles: true }, init)));
17
+ }
18
+ function fireWheel(el, init) {
19
+ el.dispatchEvent(new WheelEvent('wheel', Object.assign({ bubbles: true }, init)));
20
+ }
21
+ describe('GestureRegistry — pan', () => {
22
+ it('calls onStart/onMove/onEnd for a matching left-drag', () => {
23
+ const container = makeEl();
24
+ const registry = createGestureRegistry(container);
25
+ const onStart = vi.fn();
26
+ const onMove = vi.fn();
27
+ const onEnd = vi.fn();
28
+ const handle = registry.register({
29
+ type: 'pan', button: 0, modifiers: [], axis: 'xy',
30
+ onStart, onMove, onEnd,
31
+ });
32
+ handle.enable();
33
+ fire(container, 'pointerdown', { pointerId: 1, button: 0, clientX: 10, clientY: 10, isPrimary: true });
34
+ expect(onStart).toHaveBeenCalledTimes(1);
35
+ fire(document, 'pointermove', { pointerId: 1, clientX: 20, clientY: 20, isPrimary: true });
36
+ expect(onMove).toHaveBeenCalledTimes(1);
37
+ fire(document, 'pointerup', { pointerId: 1, clientX: 20, clientY: 20, isPrimary: true });
38
+ expect(onEnd).toHaveBeenCalledTimes(1);
39
+ expect(onEnd.mock.calls[0][0]).toMatchObject({ cancelled: false });
40
+ registry.destroy();
41
+ });
42
+ it('does not fire when handle is disabled', () => {
43
+ const container = makeEl();
44
+ const registry = createGestureRegistry(container);
45
+ const onStart = vi.fn();
46
+ registry.register({ type: 'pan', button: 0, modifiers: [], axis: 'xy', onStart, onMove: vi.fn(), onEnd: vi.fn() });
47
+ // handle stays disabled
48
+ fire(container, 'pointerdown', { pointerId: 1, button: 0, clientX: 10, clientY: 10, isPrimary: true });
49
+ expect(onStart).not.toHaveBeenCalled();
50
+ registry.destroy();
51
+ });
52
+ it('does not fire after destroy()', () => {
53
+ const container = makeEl();
54
+ const registry = createGestureRegistry(container);
55
+ const onStart = vi.fn();
56
+ const handle = registry.register({ type: 'pan', button: 0, modifiers: [], axis: 'xy', onStart, onMove: vi.fn(), onEnd: vi.fn() });
57
+ handle.enable();
58
+ handle.destroy();
59
+ fire(container, 'pointerdown', { pointerId: 1, button: 0, clientX: 10, clientY: 10, isPrimary: true });
60
+ expect(onStart).not.toHaveBeenCalled();
61
+ registry.destroy();
62
+ });
63
+ it('delivers cancelled:true on pointercancel', () => {
64
+ const container = makeEl();
65
+ const registry = createGestureRegistry(container);
66
+ const onEnd = vi.fn();
67
+ const handle = registry.register({ type: 'pan', button: 0, modifiers: [], axis: 'xy', onStart: vi.fn(), onMove: vi.fn(), onEnd });
68
+ handle.enable();
69
+ fire(container, 'pointerdown', { pointerId: 1, button: 0, clientX: 10, clientY: 10, isPrimary: true });
70
+ fire(document, 'pointercancel', { pointerId: 1, isPrimary: true });
71
+ expect(onEnd.mock.calls[0][0]).toMatchObject({ cancelled: true });
72
+ registry.destroy();
73
+ });
74
+ });
75
+ describe('GestureRegistry — scroll', () => {
76
+ it('calls onScroll for wheel events on container', () => {
77
+ const container = makeEl();
78
+ const registry = createGestureRegistry(container);
79
+ const onScroll = vi.fn();
80
+ const handle = registry.register({ type: 'scroll', modifiers: [], onScroll });
81
+ handle.enable();
82
+ fireWheel(container, { deltaY: -100, bubbles: true });
83
+ expect(onScroll).toHaveBeenCalledTimes(1);
84
+ expect(onScroll.mock.calls[0][0]).toMatchObject({ deltaY: -100 });
85
+ registry.destroy();
86
+ });
87
+ it('does not fire when handle is disabled', () => {
88
+ const container = makeEl();
89
+ const registry = createGestureRegistry(container);
90
+ const onScroll = vi.fn();
91
+ registry.register({ type: 'scroll', modifiers: [], onScroll });
92
+ // not enabled
93
+ fireWheel(container, { deltaY: -100, bubbles: true });
94
+ expect(onScroll).not.toHaveBeenCalled();
95
+ registry.destroy();
96
+ });
97
+ });
98
+ describe('GestureRegistry — button', () => {
99
+ it('calls onButton for matching button', () => {
100
+ const container = makeEl();
101
+ const registry = createGestureRegistry(container);
102
+ const onButton = vi.fn();
103
+ const handle = registry.register({ type: 'button', button: 2, modifiers: [], on: 'down', onButton });
104
+ handle.enable();
105
+ fire(container, 'pointerdown', { pointerId: 1, button: 2, clientX: 5, clientY: 5, isPrimary: true });
106
+ expect(onButton).toHaveBeenCalledTimes(1);
107
+ registry.destroy();
108
+ });
109
+ it('does not fire for wrong button', () => {
110
+ const container = makeEl();
111
+ const registry = createGestureRegistry(container);
112
+ const onButton = vi.fn();
113
+ const handle = registry.register({ type: 'button', button: 2, modifiers: [], on: 'down', onButton });
114
+ handle.enable();
115
+ fire(container, 'pointerdown', { pointerId: 1, button: 0, clientX: 5, clientY: 5, isPrimary: true });
116
+ expect(onButton).not.toHaveBeenCalled();
117
+ registry.destroy();
118
+ });
119
+ });
@@ -0,0 +1,6 @@
1
+ export { claim, revoke, getOwner, isOwner } from './pointerClaim';
2
+ export { createGestureRegistry } from './gestureRegistry';
3
+ export type { GestureRegistry } from './gestureRegistry';
4
+ export type { GestureHandle, GestureOptions, GestureType, Axis, ClaimPriority, ClaimEntry, PanEvent, ScrollEvent, ButtonEvent, } from './types';
5
+ /** Internal utility — used by framework gesture sites. Not re-exported via api.ts. */
6
+ export declare function ancestorCount(el: Element): number;
@@ -0,0 +1,12 @@
1
+ export { claim, revoke, getOwner, isOwner } from './pointerClaim';
2
+ export { createGestureRegistry } from './gestureRegistry';
3
+ /** Internal utility — used by framework gesture sites. Not re-exported via api.ts. */
4
+ export function ancestorCount(el) {
5
+ let n = 0;
6
+ let cur = el;
7
+ while (cur) {
8
+ n++;
9
+ cur = cur.parentElement;
10
+ }
11
+ return n;
12
+ }
@@ -0,0 +1,7 @@
1
+ import type { ClaimEntry } from './types';
2
+ export declare function claim(pointerId: number, entry: ClaimEntry): boolean;
3
+ export declare function revoke(pointerId: number, ownerId: string): void;
4
+ export declare function getOwner(pointerId: number): ClaimEntry | null;
5
+ export declare function isOwner(pointerId: number, ownerId: string): boolean;
6
+ /** Test-only reset. */
7
+ export declare function __resetForTest(): void;
@@ -0,0 +1,36 @@
1
+ const claims = new Map();
2
+ export function claim(pointerId, entry) {
3
+ const existing = claims.get(pointerId);
4
+ if (!existing) {
5
+ claims.set(pointerId, entry);
6
+ return true;
7
+ }
8
+ // Normal priority always beats edge priority.
9
+ if (existing.priority === 'edge' && entry.priority === 'normal') {
10
+ claims.set(pointerId, entry);
11
+ return true;
12
+ }
13
+ // Innermost-wins on same priority.
14
+ if (entry.priority === existing.priority && entry.depth > existing.depth) {
15
+ claims.set(pointerId, entry);
16
+ return true;
17
+ }
18
+ return false;
19
+ }
20
+ export function revoke(pointerId, ownerId) {
21
+ const existing = claims.get(pointerId);
22
+ if ((existing === null || existing === void 0 ? void 0 : existing.ownerId) === ownerId)
23
+ claims.delete(pointerId);
24
+ }
25
+ export function getOwner(pointerId) {
26
+ var _a;
27
+ return (_a = claims.get(pointerId)) !== null && _a !== void 0 ? _a : null;
28
+ }
29
+ export function isOwner(pointerId, ownerId) {
30
+ var _a;
31
+ return ((_a = claims.get(pointerId)) === null || _a === void 0 ? void 0 : _a.ownerId) === ownerId;
32
+ }
33
+ /** Test-only reset. */
34
+ export function __resetForTest() {
35
+ claims.clear();
36
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,64 @@
1
+ import { describe, it, expect, beforeEach } from 'vitest';
2
+ import { claim, revoke, getOwner, isOwner, __resetForTest } from './pointerClaim';
3
+ beforeEach(() => __resetForTest());
4
+ describe('claim', () => {
5
+ it('grants when no existing claim', () => {
6
+ const ok = claim(1, { ownerId: 'a', axis: 'x', priority: 'normal', depth: 5 });
7
+ expect(ok).toBe(true);
8
+ });
9
+ it('innermost-wins: deeper claim beats shallower', () => {
10
+ var _a;
11
+ claim(1, { ownerId: 'shallow', axis: 'x', priority: 'normal', depth: 3 });
12
+ const ok = claim(1, { ownerId: 'deep', axis: 'x', priority: 'normal', depth: 7 });
13
+ expect(ok).toBe(true);
14
+ expect((_a = getOwner(1)) === null || _a === void 0 ? void 0 : _a.ownerId).toBe('deep');
15
+ });
16
+ it('rejects claim from shallower node', () => {
17
+ var _a;
18
+ claim(1, { ownerId: 'deep', axis: 'x', priority: 'normal', depth: 7 });
19
+ const ok = claim(1, { ownerId: 'shallow', axis: 'x', priority: 'normal', depth: 3 });
20
+ expect(ok).toBe(false);
21
+ expect((_a = getOwner(1)) === null || _a === void 0 ? void 0 : _a.ownerId).toBe('deep');
22
+ });
23
+ it('normal priority beats edge priority regardless of depth', () => {
24
+ var _a;
25
+ claim(1, { ownerId: 'edge', axis: 'x', priority: 'edge', depth: 10 });
26
+ const ok = claim(1, { ownerId: 'app', axis: 'x', priority: 'normal', depth: 2 });
27
+ expect(ok).toBe(true);
28
+ expect((_a = getOwner(1)) === null || _a === void 0 ? void 0 : _a.ownerId).toBe('app');
29
+ });
30
+ it('edge priority cannot beat edge priority with same depth', () => {
31
+ claim(1, { ownerId: 'fw1', axis: 'x', priority: 'edge', depth: 5 });
32
+ const ok = claim(1, { ownerId: 'fw2', axis: 'x', priority: 'edge', depth: 5 });
33
+ expect(ok).toBe(false);
34
+ });
35
+ });
36
+ describe('revoke', () => {
37
+ it('clears the claim for that pointer', () => {
38
+ claim(1, { ownerId: 'a', axis: 'x', priority: 'normal', depth: 5 });
39
+ revoke(1, 'a');
40
+ expect(getOwner(1)).toBeNull();
41
+ });
42
+ it('is a no-op when ownerId does not match', () => {
43
+ var _a;
44
+ claim(1, { ownerId: 'a', axis: 'x', priority: 'normal', depth: 5 });
45
+ revoke(1, 'b');
46
+ expect((_a = getOwner(1)) === null || _a === void 0 ? void 0 : _a.ownerId).toBe('a');
47
+ });
48
+ it('is safe to call for unknown pointerId', () => {
49
+ expect(() => revoke(99, 'x')).not.toThrow();
50
+ });
51
+ });
52
+ describe('isOwner', () => {
53
+ it('returns true when ownerId matches', () => {
54
+ claim(1, { ownerId: 'a', axis: 'x', priority: 'normal', depth: 5 });
55
+ expect(isOwner(1, 'a')).toBe(true);
56
+ });
57
+ it('returns false when ownerId does not match', () => {
58
+ claim(1, { ownerId: 'a', axis: 'x', priority: 'normal', depth: 5 });
59
+ expect(isOwner(1, 'b')).toBe(false);
60
+ });
61
+ it('returns false for unclaimed pointer', () => {
62
+ expect(isOwner(42, 'a')).toBe(false);
63
+ });
64
+ });
@@ -0,0 +1,83 @@
1
+ export type Axis = 'x' | 'y' | 'xy' | 'any';
2
+ export type ClaimPriority = 'edge' | 'normal';
3
+ export interface ClaimEntry {
4
+ ownerId: string;
5
+ axis: Axis;
6
+ priority: ClaimPriority;
7
+ /** DOM ancestor count at claim time — higher = deeper = wins on ties. */
8
+ depth: number;
9
+ }
10
+ export type GestureType = 'pan' | 'drag' | 'button' | 'scroll';
11
+ export type ButtonEventOn = 'down' | 'up' | 'both';
12
+ export interface PanEvent {
13
+ pointerId: number;
14
+ x: number;
15
+ y: number;
16
+ dx: number;
17
+ dy: number;
18
+ cancelled?: boolean;
19
+ }
20
+ export interface ScrollEvent {
21
+ deltaX: number;
22
+ deltaY: number;
23
+ deltaZ: number;
24
+ modifiers: string[];
25
+ }
26
+ export interface ButtonEvent {
27
+ button: number;
28
+ modifiers: string[];
29
+ x: number;
30
+ y: number;
31
+ }
32
+ export interface PanOptions {
33
+ type: 'pan';
34
+ /** Mouse button number. 0=left, 1=middle, 2=right. Default: 0. */
35
+ button?: number;
36
+ /** Required modifier keys. E.g. ['Control']. Empty array means no modifiers. */
37
+ modifiers?: string[];
38
+ /** Axis to claim. Default: 'xy'. */
39
+ axis?: Axis;
40
+ /** Claim region. Defaults to shard root element if omitted. */
41
+ container?: HTMLElement;
42
+ onStart: (e: PanEvent) => void;
43
+ onMove: (e: PanEvent) => void;
44
+ onEnd: (e: PanEvent & {
45
+ cancelled?: boolean;
46
+ }) => void;
47
+ }
48
+ export interface DragOptions {
49
+ type: 'drag';
50
+ button?: number;
51
+ modifiers?: string[];
52
+ /** Movement px threshold below which release is treated as a click. Default: 6. */
53
+ clickThreshold?: number;
54
+ axis?: Axis;
55
+ container?: HTMLElement;
56
+ onStart: (e: PanEvent) => void;
57
+ onMove: (e: PanEvent) => void;
58
+ onEnd: (e: PanEvent & {
59
+ cancelled?: boolean;
60
+ wasClick: boolean;
61
+ }) => void;
62
+ }
63
+ export interface ButtonOptions {
64
+ type: 'button';
65
+ button: number;
66
+ modifiers?: string[];
67
+ on?: ButtonEventOn;
68
+ container?: HTMLElement;
69
+ onButton: (e: ButtonEvent) => void;
70
+ }
71
+ export interface ScrollOptions {
72
+ type: 'scroll';
73
+ modifiers?: string[];
74
+ axis?: 'x' | 'y' | 'xy';
75
+ container?: HTMLElement;
76
+ onScroll: (e: ScrollEvent) => void;
77
+ }
78
+ export type GestureOptions = PanOptions | DragOptions | ButtonOptions | ScrollOptions;
79
+ export interface GestureHandle {
80
+ enable(): void;
81
+ disable(): void;
82
+ destroy(): void;
83
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -4,6 +4,7 @@ export { __setActiveScope, __setTenantId, __setDocumentBackend } from './host';
4
4
  export type { Backend } from './state/types';
5
5
  export type { DocumentBackend } from './documents/types';
6
6
  export { HttpDocumentBackend } from './documents/http-backend';
7
+ export { IndexedDBDocumentBackend, MemoryDocumentBackend } from './documents/backends';
7
8
  export { __setEnvServerUrl } from './env/index';
8
9
  export { installPackage, uninstallPackage, listInstalledPackages, loadInstalledPackages, } from './registry/index';
9
10
  export type { InstalledPackage, InstallResult, PackageMeta } from './registry/types';
@@ -8,6 +8,7 @@
8
8
  export { registerShard, registerApp, bootstrap, __setBackend, setLocalOwner } from './host';
9
9
  export { __setActiveScope, __setTenantId, __setDocumentBackend } from './host';
10
10
  export { HttpDocumentBackend } from './documents/http-backend';
11
+ export { IndexedDBDocumentBackend, MemoryDocumentBackend } from './documents/backends';
11
12
  export { __setEnvServerUrl } from './env/index';
12
13
  // Install API (host-only).
13
14
  export { installPackage, uninstallPackage, listInstalledPackages, loadInstalledPackages, } from './registry/index';