sh3-core 0.19.3 → 0.19.6

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 (43) hide show
  1. package/dist/api.d.ts +5 -0
  2. package/dist/api.js +3 -0
  3. package/dist/chrome/CompactChrome.svelte +34 -1
  4. package/dist/chrome/CompactChrome.svelte.test.js +4 -2
  5. package/dist/chrome/FloatsSheet.svelte +236 -0
  6. package/dist/chrome/FloatsSheet.svelte.d.ts +7 -0
  7. package/dist/chrome/FloatsSheet.svelte.test.d.ts +1 -0
  8. package/dist/chrome/FloatsSheet.svelte.test.js +155 -0
  9. package/dist/documents/picker-api.d.ts +31 -0
  10. package/dist/documents/picker-api.js +1 -0
  11. package/dist/documents/picker-api.test.d.ts +1 -0
  12. package/dist/documents/picker-api.test.js +132 -0
  13. package/dist/documents/picker-primitive.d.ts +7 -0
  14. package/dist/documents/picker-primitive.js +58 -0
  15. package/dist/layout/compact/CompactRenderer.svelte +8 -2
  16. package/dist/layout/compact/rootStore.svelte.d.ts +20 -0
  17. package/dist/layout/compact/rootStore.svelte.js +59 -0
  18. package/dist/layout/compact/rootStore.svelte.test.d.ts +1 -0
  19. package/dist/layout/compact/rootStore.svelte.test.js +54 -0
  20. package/dist/layout/floats.d.ts +27 -0
  21. package/dist/layout/floats.js +20 -0
  22. package/dist/layout/floats.test.js +34 -1
  23. package/dist/layout/inspection.js +25 -2
  24. package/dist/layout/inspection.svelte.test.js +49 -0
  25. package/dist/overlays/FloatLayer.svelte +12 -1
  26. package/dist/overlays/float.d.ts +7 -0
  27. package/dist/overlays/float.js +76 -6
  28. package/dist/overlays/float.test.js +170 -0
  29. package/dist/primitives/widgets/DocumentFilePicker.d.ts +25 -0
  30. package/dist/primitives/widgets/DocumentFilePicker.js +74 -0
  31. package/dist/primitives/widgets/DocumentFilePicker.svelte +144 -0
  32. package/dist/primitives/widgets/DocumentFilePicker.svelte.d.ts +18 -0
  33. package/dist/primitives/widgets/DocumentOpener.svelte +36 -0
  34. package/dist/primitives/widgets/DocumentOpener.svelte.d.ts +17 -0
  35. package/dist/primitives/widgets/DocumentSaver.svelte +36 -0
  36. package/dist/primitives/widgets/DocumentSaver.svelte.d.ts +17 -0
  37. package/dist/primitives/widgets/_DocumentBrowser.svelte +339 -0
  38. package/dist/primitives/widgets/_DocumentBrowser.svelte.d.ts +12 -0
  39. package/dist/shards/activate.svelte.js +23 -14
  40. package/dist/shards/types.d.ts +8 -0
  41. package/dist/version.d.ts +1 -1
  42. package/dist/version.js +1 -1
  43. package/package.json +1 -1
package/dist/api.d.ts CHANGED
@@ -30,6 +30,7 @@ export { inspectActiveLayout, spliceIntoActiveLayout, dockIntoActiveLayout, focu
30
30
  export type { DocumentHandle, DocumentHandleOptions, DocumentFormat, DocumentMeta, DocumentChange, AutosaveController, } from './documents/types';
31
31
  export { PERMISSION_DOCUMENTS_BROWSE, PERMISSION_DOCUMENTS_READ, PERMISSION_DOCUMENTS_WRITE, } from './documents/types';
32
32
  export type { BrowseCapability } from './documents/browse';
33
+ export type { DocumentPickerApi, DocumentOpenOptions, DocumentSaveOptions } from './documents/picker-api';
33
34
  export type { ContributionsApi } from './contributions/types';
34
35
  export type { SyncPolicy, SyncPolicyRule, DocStatus, ConflictFile, ConflictBranch, } from './documents/sync-types';
35
36
  export { PERMISSION_SYNC_PEER, PERMISSION_SYNC_POLICY } from './documents/sync-types';
@@ -87,5 +88,9 @@ export { default as Select } from './primitives/widgets/Select.svelte';
87
88
  export type { SelectOption } from './primitives/widgets/Select';
88
89
  export { default as AppPicker } from './primitives/widgets/AppPicker.svelte';
89
90
  export { default as UserPicker } from './primitives/widgets/UserPicker.svelte';
91
+ export { default as DocumentFilePicker } from './primitives/widgets/DocumentFilePicker.svelte';
92
+ export { default as DocumentOpener } from './primitives/widgets/DocumentOpener.svelte';
93
+ export { default as DocumentSaver } from './primitives/widgets/DocumentSaver.svelte';
94
+ export type { OpenerValue, SaverValue } from './primitives/widgets/DocumentFilePicker';
90
95
  export type { FieldKind, FieldAddress, FieldView, ControllableFieldDescriptor, ImperativeFieldDescriptor, ElementRefFieldDescriptor, ReadonlyFieldDescriptor, FieldsApi, DecorationHandle, } from './fields/types';
91
96
  export { fieldAddressToString, fieldAddressFromString } from './fields/address';
package/dist/api.js CHANGED
@@ -97,4 +97,7 @@ export { default as FilePicker } from './primitives/widgets/FilePicker.svelte';
97
97
  export { default as Select } from './primitives/widgets/Select.svelte';
98
98
  export { default as AppPicker } from './primitives/widgets/AppPicker.svelte';
99
99
  export { default as UserPicker } from './primitives/widgets/UserPicker.svelte';
100
+ export { default as DocumentFilePicker } from './primitives/widgets/DocumentFilePicker.svelte';
101
+ export { default as DocumentOpener } from './primitives/widgets/DocumentOpener.svelte';
102
+ export { default as DocumentSaver } from './primitives/widgets/DocumentSaver.svelte';
100
103
  export { fieldAddressToString, fieldAddressFromString } from './fields/address';
@@ -12,14 +12,23 @@
12
12
  import { sh3 } from '../sh3Runtime.svelte';
13
13
  import { layoutStore, getActiveRoot } from '../layout/store.svelte';
14
14
  import { derive } from '../layout/compact/derive';
15
+ import {
16
+ compactRootStore,
17
+ resolveCompactBodyRoot,
18
+ } from '../layout/compact/rootStore.svelte';
15
19
  import { getLiveDispatcherState } from '../actions/state.svelte';
16
20
  import { getRegisteredApp } from '../apps/registry.svelte';
17
21
  import { returnToHome } from '../apps/lifecycle';
18
22
  import Button from '../primitives/Button.svelte';
19
23
  import MenuSheet from './MenuSheet.svelte';
24
+ import FloatsSheet from './FloatsSheet.svelte';
20
25
  import type { DrawerAnchor } from '../layout/compact/types';
21
26
 
22
- const rendering = $derived(derive(layoutStore.root));
27
+ // Drawer toggles re-derive against whatever is currently the body —
28
+ // when the user is on a float, its role-tagged slots drive the chrome,
29
+ // not the docked tree's.
30
+ const bodyRoot = $derived(resolveCompactBodyRoot());
31
+ const rendering = $derived(derive(bodyRoot));
23
32
  const dispatcher = $derived(getLiveDispatcherState());
24
33
  const onHome = $derived(getActiveRoot() === 'home');
25
34
  const appLabel = $derived.by(() => {
@@ -47,13 +56,28 @@
47
56
  return bestLabel;
48
57
  });
49
58
 
59
+ const floatTitle = $derived.by(() => {
60
+ const cur = compactRootStore.current;
61
+ if (cur.kind !== 'float') return null;
62
+ const f = layoutStore.tree.floats.find((x) => x.id === cur.floatId);
63
+ if (!f) return null;
64
+ if (f.title) return f.title;
65
+ if (f.content.type === 'tabs') {
66
+ const t = f.content.tabs[f.content.activeTab] ?? f.content.tabs[0];
67
+ return t?.label ?? null;
68
+ }
69
+ return null;
70
+ });
71
+
50
72
  const title = $derived.by(() => {
73
+ if (floatTitle) return floatTitle;
51
74
  const carouselLabel = topmostCarouselLabel;
52
75
  if (carouselLabel) return `${appLabel} › ${carouselLabel}`;
53
76
  return appLabel;
54
77
  });
55
78
 
56
79
  let menuOpen = $state(false);
80
+ let floatsOpen = $state(false);
57
81
 
58
82
  function toggleDrawer(anchor: DrawerAnchor) {
59
83
  sh3.drawers.toggle(anchor);
@@ -92,11 +116,20 @@
92
116
  <div class="title">{title}</div>
93
117
  <div class="trailing">
94
118
  <Button variant="icon" icon="command" ariaLabel="Open command palette" title="Open command palette" onclick={openPalette} />
119
+ <Button
120
+ variant="icon"
121
+ icon="layers"
122
+ ariaLabel="Floats"
123
+ title="Floats"
124
+ pressed={floatsOpen}
125
+ onclick={() => { floatsOpen = !floatsOpen; }}
126
+ />
95
127
  <Button variant="icon" icon="ellipsis-vertical" ariaLabel="Open menu" title="Open menu" onclick={() => { menuOpen = true; }} />
96
128
  </div>
97
129
  </header>
98
130
 
99
131
  <MenuSheet open={menuOpen} onClose={() => (menuOpen = false)} />
132
+ <FloatsSheet open={floatsOpen} onClose={() => (floatsOpen = false)} />
100
133
 
101
134
  <style>
102
135
  .sh3-compact-chrome {
@@ -65,7 +65,7 @@ describe('CompactChrome (dom)', () => {
65
65
  expect(host.querySelector('.leading [data-sh3-anchor="right"] button')).not.toBeNull();
66
66
  expect(host.querySelector('.leading [data-sh3-anchor="top"] button')).toBeNull();
67
67
  });
68
- it('renders palette + overflow buttons in the trailing section', () => {
68
+ it('renders palette + floats + overflow buttons in the trailing section', () => {
69
69
  attachApp(fakeApp());
70
70
  switchToApp();
71
71
  flushSync();
@@ -74,7 +74,9 @@ describe('CompactChrome (dom)', () => {
74
74
  mounted = mount(CompactChromeAny, { target: host });
75
75
  flushSync();
76
76
  const trailing = host.querySelectorAll('.trailing button');
77
- expect(trailing.length).toBe(2);
77
+ expect(trailing.length).toBe(3);
78
+ const labels = Array.from(trailing).map((b) => b.getAttribute('aria-label'));
79
+ expect(labels).toEqual(['Open command palette', 'Floats', 'Open menu']);
78
80
  });
79
81
  });
80
82
  describe('CompactChrome — home button', () => {
@@ -0,0 +1,236 @@
1
+ <script lang="ts">
2
+ /*
3
+ * FloatsSheet — bottom-anchored navigation sheet for compact mode.
4
+ *
5
+ * Lists the active-layout entry plus one row per non-dismissable float.
6
+ * Tapping a row calls compactRootStore.setRoot(...) and closes the sheet.
7
+ * Dismissable pickers (anchored popovers) are excluded — they aren't
8
+ * "places to go", they're transient overlays.
9
+ *
10
+ * The active-layout row label tracks the active app (or "Home" when no
11
+ * app is attached). The current row is marked with data-current="true"
12
+ * for styling.
13
+ */
14
+ import { layoutStore, getActiveRoot } from '../layout/store.svelte';
15
+ import { compactRootStore } from '../layout/compact/rootStore.svelte';
16
+ import { floatManager } from '../overlays/float';
17
+ import { getLiveDispatcherState } from '../actions/state.svelte';
18
+ import { getRegisteredApp } from '../apps/registry.svelte';
19
+ import type { FloatEntry } from '../layout/types';
20
+
21
+ let { open, onClose }: { open: boolean; onClose: () => void } = $props();
22
+
23
+ const dispatcher = $derived(getLiveDispatcherState());
24
+ const dockedLabel = $derived.by(() => {
25
+ if (getActiveRoot() === 'home') return 'Home';
26
+ const id = dispatcher.activeAppId;
27
+ if (!id) return 'Home';
28
+ return getRegisteredApp(id)?.manifest.label ?? id;
29
+ });
30
+
31
+ function floatLabel(f: FloatEntry): string {
32
+ if (f.title) return f.title;
33
+ if (f.content.type === 'tabs') {
34
+ const t = f.content.tabs[f.content.activeTab] ?? f.content.tabs[0];
35
+ if (t) return t.label;
36
+ }
37
+ return f.id;
38
+ }
39
+
40
+ const rows = $derived.by(() => {
41
+ const out: { id: 'docked' | string; label: string }[] = [
42
+ { id: 'docked', label: dockedLabel },
43
+ ];
44
+ for (const f of layoutStore.tree.floats) {
45
+ if (f.dismissable) continue;
46
+ out.push({ id: f.id, label: floatLabel(f) });
47
+ }
48
+ return out;
49
+ });
50
+
51
+ function isCurrent(rowId: 'docked' | string): boolean {
52
+ const cur = compactRootStore.current;
53
+ if (rowId === 'docked') return cur.kind === 'docked';
54
+ return cur.kind === 'float' && cur.floatId === rowId;
55
+ }
56
+
57
+ function activate(rowId: 'docked' | string): void {
58
+ if (rowId === 'docked') {
59
+ compactRootStore.reset();
60
+ } else {
61
+ compactRootStore.setRoot({ kind: 'float', floatId: rowId });
62
+ }
63
+ onClose();
64
+ }
65
+
66
+ // ----- swipe-to-close --------------------------------------------------
67
+ // Horizontal pointer drag on a float row past 40% of its width closes the
68
+ // float. The active-layout row is non-swipeable. Document-level pointer
69
+ // listeners survive the pointer leaving the row mid-drag, mirroring the
70
+ // float-frame drag pattern.
71
+ const SWIPE_THRESHOLD = 0.4;
72
+ let swipingId = $state<string | null>(null);
73
+ let swipeDx = $state(0);
74
+ let swipeStartX = 0;
75
+ let swipePointerId: number | null = null;
76
+ let swipeRowEl: HTMLElement | null = null;
77
+
78
+ function rowOffset(rowId: 'docked' | string): number {
79
+ return swipingId === rowId ? swipeDx : 0;
80
+ }
81
+
82
+ function rowTransition(rowId: 'docked' | string): string {
83
+ return swipingId === rowId ? 'none' : 'transform 160ms ease-out';
84
+ }
85
+
86
+ function onRowPointerDown(e: PointerEvent, rowId: 'docked' | string): void {
87
+ if (rowId === 'docked') return;
88
+ if (e.button !== 0) return;
89
+ if (swipingId !== null) return;
90
+ swipingId = rowId;
91
+ swipeStartX = e.clientX;
92
+ swipeDx = 0;
93
+ swipePointerId = e.pointerId;
94
+ // Prefer e.currentTarget; fall back to a DOM query so synthetic events
95
+ // (vitest dispatchEvent) that may not populate currentTarget still
96
+ // resolve the row width for the threshold check.
97
+ swipeRowEl =
98
+ (e.currentTarget as HTMLElement | null) ??
99
+ (document.querySelector(`[data-sh3-floats-row="${rowId}"]`) as HTMLElement | null);
100
+ document.addEventListener('pointermove', onSwipeMove);
101
+ document.addEventListener('pointerup', onSwipeUp);
102
+ document.addEventListener('pointercancel', onSwipeCancel);
103
+ }
104
+
105
+ function onSwipeMove(e: PointerEvent): void {
106
+ if (e.pointerId !== swipePointerId) return;
107
+ swipeDx = e.clientX - swipeStartX;
108
+ }
109
+
110
+ function endSwipe(): void {
111
+ document.removeEventListener('pointermove', onSwipeMove);
112
+ document.removeEventListener('pointerup', onSwipeUp);
113
+ document.removeEventListener('pointercancel', onSwipeCancel);
114
+ swipingId = null;
115
+ swipePointerId = null;
116
+ swipeStartX = 0;
117
+ swipeDx = 0;
118
+ swipeRowEl = null;
119
+ }
120
+
121
+ function onSwipeUp(e: PointerEvent): void {
122
+ if (e.pointerId !== swipePointerId) return;
123
+ const id = swipingId;
124
+ const width = swipeRowEl?.clientWidth ?? 0;
125
+ const dx = swipeDx;
126
+ endSwipe();
127
+ if (id && id !== 'docked' && width > 0 && Math.abs(dx) >= width * SWIPE_THRESHOLD) {
128
+ floatManager.close(id);
129
+ }
130
+ }
131
+
132
+ function onSwipeCancel(e: PointerEvent): void {
133
+ if (e.pointerId !== swipePointerId) return;
134
+ endSwipe();
135
+ }
136
+ </script>
137
+
138
+ {#if open}
139
+ <div
140
+ class="backdrop"
141
+ onclick={onClose}
142
+ onkeydown={(e) => { if (e.key === 'Escape') onClose(); }}
143
+ role="presentation"
144
+ ></div>
145
+ <div class="sheet" role="dialog" aria-label="Floats" data-sh3-region="floats-sheet">
146
+ <div class="scroll">
147
+ {#each rows as row (row.id)}
148
+ <button
149
+ class="row"
150
+ data-sh3-floats-row={row.id}
151
+ data-current={isCurrent(row.id) ? 'true' : 'false'}
152
+ onclick={() => activate(row.id)}
153
+ onpointerdown={(e) => onRowPointerDown(e, row.id)}
154
+ style:transform="translateX({rowOffset(row.id)}px)"
155
+ style:transition={rowTransition(row.id)}
156
+ >
157
+ <span class="label">{row.label}</span>
158
+ {#if row.id === 'docked'}
159
+ <span class="kind">layout</span>
160
+ {:else}
161
+ <span class="kind">float</span>
162
+ {/if}
163
+ </button>
164
+ {/each}
165
+ </div>
166
+ <button class="cancel" onclick={onClose}>Cancel</button>
167
+ </div>
168
+ {/if}
169
+
170
+ <style>
171
+ .backdrop {
172
+ position: absolute;
173
+ inset: 0;
174
+ background: var(--sh3-overlay-backdrop, rgba(0, 0, 0, 0.35));
175
+ pointer-events: auto;
176
+ z-index: var(--sh3-z-layer-4);
177
+ }
178
+ .sheet {
179
+ position: absolute;
180
+ left: 0;
181
+ right: 0;
182
+ bottom: 0;
183
+ max-height: 70vh;
184
+ display: flex;
185
+ flex-direction: column;
186
+ background: var(--sh3-bg);
187
+ color: var(--sh3-fg);
188
+ border-top: 1px solid var(--sh3-border);
189
+ box-shadow: var(--sh3-shadow-md, 0 -4px 16px rgba(0, 0, 0, 0.2));
190
+ pointer-events: auto;
191
+ z-index: var(--sh3-z-layer-4);
192
+ }
193
+ .scroll {
194
+ flex: 1;
195
+ min-height: 0;
196
+ overflow: auto;
197
+ padding: var(--sh3-pad-sm) 0;
198
+ }
199
+ .row {
200
+ display: flex;
201
+ align-items: center;
202
+ gap: var(--sh3-pad-sm);
203
+ width: 100%;
204
+ padding: var(--sh3-pad-md);
205
+ border: none;
206
+ background: none;
207
+ color: var(--sh3-fg);
208
+ text-align: left;
209
+ cursor: pointer;
210
+ /* Suppress browser-claimed horizontal pan so swipe-to-close survives
211
+ past the system scroll-claim threshold on Android/iOS. */
212
+ touch-action: pan-y;
213
+ user-select: none;
214
+ }
215
+ .row[data-current='true'] {
216
+ background: var(--sh3-bg-sunken);
217
+ font-weight: 600;
218
+ }
219
+ .row:active { background: var(--sh3-bg-sunken); }
220
+ .label { flex: 1; }
221
+ .kind {
222
+ color: var(--sh3-fg-muted);
223
+ font-size: 11px;
224
+ text-transform: uppercase;
225
+ letter-spacing: 0.05em;
226
+ }
227
+ .cancel {
228
+ padding: var(--sh3-pad-md);
229
+ border: none;
230
+ border-top: 1px solid var(--sh3-border);
231
+ background: var(--sh3-bg-elevated);
232
+ color: var(--sh3-fg);
233
+ font-weight: 600;
234
+ cursor: pointer;
235
+ }
236
+ </style>
@@ -0,0 +1,7 @@
1
+ type $$ComponentProps = {
2
+ open: boolean;
3
+ onClose: () => void;
4
+ };
5
+ declare const FloatsSheet: import("svelte").Component<$$ComponentProps, {}, "">;
6
+ type FloatsSheet = ReturnType<typeof FloatsSheet>;
7
+ export default FloatsSheet;
@@ -0,0 +1 @@
1
+ export {};
@@ -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
+ });
@@ -0,0 +1,31 @@
1
+ import type { DocEntry, OpenerValue, SaverValue } from '../primitives/widgets/DocumentFilePicker';
2
+ /** Function that returns the document tree for the picker to browse. */
3
+ export type DocListFn = () => Promise<DocEntry[]>;
4
+ /** Options for `ctx.documentPicker.open()`. */
5
+ export interface DocumentOpenOptions {
6
+ /** Element to anchor the popup to. Defaults to viewport center. */
7
+ anchor?: HTMLElement;
8
+ }
9
+ /** Options for `ctx.documentPicker.save()`. */
10
+ export interface DocumentSaveOptions {
11
+ /** Pre-fill the filename input in the save dialog. */
12
+ suggestedName?: string;
13
+ /** Element to anchor the popup to. Defaults to viewport center. */
14
+ anchor?: HTMLElement;
15
+ }
16
+ /** Programmatic document picker API — available on `ctx.documentPicker`. */
17
+ export interface DocumentPickerApi {
18
+ /**
19
+ * Open the document browser in "open" mode. The user browses and selects
20
+ * an existing document. Returns the selected `{shardId, path}` or null
21
+ * if cancelled or dismissed externally.
22
+ */
23
+ open(opts?: DocumentOpenOptions): Promise<OpenerValue | null>;
24
+ /**
25
+ * Open the document browser in "save" mode. The user navigates to a
26
+ * folder and provides a filename. Returns the full path string or null
27
+ * if cancelled or dismissed externally.
28
+ */
29
+ save(opts?: DocumentSaveOptions): Promise<SaverValue | null>;
30
+ }
31
+ export type { OpenerValue, SaverValue };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};