sh3-core 0.19.5 → 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.
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';
@@ -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 {};
@@ -0,0 +1,132 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+ import { sh3 } from '../sh3Runtime.svelte';
3
+ import { createDocumentPicker } from './picker-primitive';
4
+ vi.mock('../sh3Runtime.svelte', () => ({
5
+ sh3: {
6
+ popup: {
7
+ show: vi.fn(),
8
+ },
9
+ },
10
+ }));
11
+ const mockShow = sh3.popup.show;
12
+ function mockPopup() {
13
+ let capturedCommit = null;
14
+ let capturedCancel = null;
15
+ const handle = {
16
+ close: vi.fn(),
17
+ };
18
+ mockShow.mockImplementation((_Content, _opts, props) => {
19
+ capturedCommit = props.onCommit;
20
+ capturedCancel = props.onCancel;
21
+ return handle;
22
+ });
23
+ return {
24
+ commit: (v) => {
25
+ capturedCommit === null || capturedCommit === void 0 ? void 0 : capturedCommit(v);
26
+ },
27
+ cancel: () => {
28
+ capturedCancel === null || capturedCancel === void 0 ? void 0 : capturedCancel();
29
+ },
30
+ dismiss: () => {
31
+ handle.close();
32
+ },
33
+ handle,
34
+ };
35
+ }
36
+ beforeEach(() => {
37
+ vi.clearAllMocks();
38
+ });
39
+ describe('createDocumentPicker', () => {
40
+ const sampleDoc = { shardId: 'my-shard', path: 'readme.md' };
41
+ describe('open()', () => {
42
+ it('resolves with OpenerValue when user commits', async () => {
43
+ const listFn = async () => [{ shardId: 'my-shard', path: 'readme.md', size: 100, lastModified: 0 }];
44
+ const picker = createDocumentPicker(listFn);
45
+ const popup = mockPopup();
46
+ const promise = picker.open();
47
+ await vi.waitFor(() => expect(mockShow).toHaveBeenCalledOnce());
48
+ popup.commit(sampleDoc);
49
+ const result = await promise;
50
+ expect(result).toEqual({ shardId: 'my-shard', path: 'readme.md' });
51
+ });
52
+ it('resolves with null when user cancels', async () => {
53
+ const listFn = async () => [];
54
+ const picker = createDocumentPicker(listFn);
55
+ const popup = mockPopup();
56
+ const promise = picker.open();
57
+ await vi.waitFor(() => expect(mockShow).toHaveBeenCalledOnce());
58
+ popup.cancel();
59
+ const result = await promise;
60
+ expect(result).toBeNull();
61
+ });
62
+ it('resolves with null when popup is dismissed externally', async () => {
63
+ const listFn = async () => [];
64
+ const picker = createDocumentPicker(listFn);
65
+ const popup = mockPopup();
66
+ const promise = picker.open();
67
+ await vi.waitFor(() => expect(mockShow).toHaveBeenCalledOnce());
68
+ popup.dismiss();
69
+ const result = await promise;
70
+ expect(result).toBeNull();
71
+ });
72
+ it('rejects when listFn fails', async () => {
73
+ const listFn = async () => { throw new Error('network error'); };
74
+ const picker = createDocumentPicker(listFn);
75
+ const promise = picker.open();
76
+ await expect(promise).rejects.toThrow('network error');
77
+ expect(mockShow).not.toHaveBeenCalled();
78
+ });
79
+ it('uses anchor element position when provided', async () => {
80
+ const listFn = async () => [];
81
+ const picker = createDocumentPicker(listFn);
82
+ mockPopup();
83
+ const el = document.createElement('div');
84
+ picker.open({ anchor: el });
85
+ await vi.waitFor(() => expect(mockShow).toHaveBeenCalledOnce());
86
+ const call = mockShow.mock.calls[0];
87
+ expect(call[1].anchor).toEqual({ x: 0, y: 0 });
88
+ });
89
+ });
90
+ describe('save()', () => {
91
+ it('resolves with SaverValue string when user commits a filename', async () => {
92
+ const listFn = async () => [];
93
+ const picker = createDocumentPicker(listFn);
94
+ const popup = mockPopup();
95
+ const promise = picker.save();
96
+ await vi.waitFor(() => expect(mockShow).toHaveBeenCalledOnce());
97
+ popup.commit('my-shard/report.txt');
98
+ const result = await promise;
99
+ expect(result).toBe('my-shard/report.txt');
100
+ });
101
+ it('resolves with null when user cancels', async () => {
102
+ const listFn = async () => [];
103
+ const picker = createDocumentPicker(listFn);
104
+ const popup = mockPopup();
105
+ const promise = picker.save();
106
+ await vi.waitFor(() => expect(mockShow).toHaveBeenCalledOnce());
107
+ popup.cancel();
108
+ const result = await promise;
109
+ expect(result).toBeNull();
110
+ });
111
+ it('passes suggestedName as prop', async () => {
112
+ const listFn = async () => [];
113
+ const picker = createDocumentPicker(listFn);
114
+ mockPopup();
115
+ picker.save({ suggestedName: 'draft.txt' });
116
+ await vi.waitFor(() => expect(mockShow).toHaveBeenCalledOnce());
117
+ const call = mockShow.mock.calls[0];
118
+ expect(call[2].suggestedName).toBe('draft.txt');
119
+ });
120
+ });
121
+ it('defaults anchor to viewport center when not provided', async () => {
122
+ const listFn = async () => [];
123
+ const picker = createDocumentPicker(listFn);
124
+ mockPopup();
125
+ const w = window.innerWidth;
126
+ const h = window.innerHeight;
127
+ picker.open();
128
+ await vi.waitFor(() => expect(mockShow).toHaveBeenCalledOnce());
129
+ const call = mockShow.mock.calls[0];
130
+ expect(call[1].anchor).toEqual({ x: w / 2, y: h / 2 });
131
+ });
132
+ });
@@ -0,0 +1,7 @@
1
+ import type { DocumentPickerApi, DocListFn } from './picker-api';
2
+ /**
3
+ * Create a document picker API bound to a document listing function.
4
+ * The listFn is derived from the shard's document zone + browse permission
5
+ * and baked in at construction time so callers don't pass their own scope.
6
+ */
7
+ export declare function createDocumentPicker(listFn: DocListFn): DocumentPickerApi;
@@ -0,0 +1,58 @@
1
+ import { sh3 } from '../sh3Runtime.svelte';
2
+ import DocumentBrowser from '../primitives/widgets/_DocumentBrowser.svelte';
3
+ /**
4
+ * Create a document picker API bound to a document listing function.
5
+ * The listFn is derived from the shard's document zone + browse permission
6
+ * and baked in at construction time so callers don't pass their own scope.
7
+ */
8
+ export function createDocumentPicker(listFn) {
9
+ function anchorOrDefault(anchor) {
10
+ if (anchor) {
11
+ const rect = anchor.getBoundingClientRect();
12
+ return { x: rect.left + rect.width / 2, y: rect.top };
13
+ }
14
+ return { x: window.innerWidth / 2, y: window.innerHeight / 2 };
15
+ }
16
+ async function open(opts) {
17
+ const docs = await listFn();
18
+ return new Promise((resolve) => {
19
+ const handle = sh3.popup.show(DocumentBrowser, { anchor: anchorOrDefault(opts === null || opts === void 0 ? void 0 : opts.anchor) }, {
20
+ mode: 'open',
21
+ docs,
22
+ onCommit: (value) => {
23
+ resolve(value);
24
+ },
25
+ onCancel: () => {
26
+ resolve(null);
27
+ },
28
+ });
29
+ const origClose = handle.close;
30
+ handle.close = () => {
31
+ origClose();
32
+ resolve(null);
33
+ };
34
+ });
35
+ }
36
+ async function save(opts) {
37
+ const docs = await listFn();
38
+ return new Promise((resolve) => {
39
+ const handle = sh3.popup.show(DocumentBrowser, { anchor: anchorOrDefault(opts === null || opts === void 0 ? void 0 : opts.anchor) }, {
40
+ mode: 'save',
41
+ docs,
42
+ suggestedName: opts === null || opts === void 0 ? void 0 : opts.suggestedName,
43
+ onCommit: (value) => {
44
+ resolve(value);
45
+ },
46
+ onCancel: () => {
47
+ resolve(null);
48
+ },
49
+ });
50
+ const origClose = handle.close;
51
+ handle.close = () => {
52
+ origClose();
53
+ resolve(null);
54
+ };
55
+ });
56
+ }
57
+ return { open, save };
58
+ }
@@ -17,18 +17,20 @@
17
17
  onCommit,
18
18
  onCancel,
19
19
  close,
20
+ suggestedName = '',
20
21
  }: {
21
22
  mode: 'open' | 'save';
22
23
  docs: DocEntry[];
23
24
  onCommit: (value: OpenerValue | SaverValue) => void;
24
25
  onCancel: () => void;
25
26
  close: () => void;
27
+ suggestedName?: string;
26
28
  } = $props();
27
29
 
28
30
  let shardId = $state<string | null>(null);
29
31
  let prefix = $state('');
30
32
  let selectedFile = $state<DocEntry | null>(null);
31
- let filename = $state('');
33
+ let filename = $state(suggestedName);
32
34
  let activeIdx = $state(0);
33
35
  let listEl = $state<HTMLElement | undefined>(undefined);
34
36
 
@@ -5,6 +5,7 @@ type $$ComponentProps = {
5
5
  onCommit: (value: OpenerValue | SaverValue) => void;
6
6
  onCancel: () => void;
7
7
  close: () => void;
8
+ suggestedName?: string;
8
9
  };
9
10
  declare const DocumentBrowser: import("svelte").Component<$$ComponentProps, {}, "">;
10
11
  type DocumentBrowser = ReturnType<typeof DocumentBrowser>;
@@ -28,6 +28,7 @@ import { createZoneManager } from '../state/manage';
28
28
  import { PERMISSION_STATE_MANAGE } from '../state/types';
29
29
  import { PERMISSION_DOCUMENTS_BROWSE, PERMISSION_DOCUMENTS_READ, PERMISSION_DOCUMENTS_WRITE, } from '../documents/types';
30
30
  import { createBrowseCapability } from '../documents/browse';
31
+ import { createDocumentPicker } from '../documents/picker-primitive';
31
32
  import { createShardKeysApi } from '../keys/client';
32
33
  import { PERMISSION_KEYS_MINT } from '../keys/types';
33
34
  import { subscribe } from '../keys/revocation-bus.svelte';
@@ -88,7 +89,7 @@ export function registerShard(shard) {
88
89
  * @throws If the shard is not registered, if `shard.activate` throws, or if a manifest view has no factory after activation.
89
90
  */
90
91
  export async function activateShard(id, opts) {
91
- var _a, _b, _c, _d, _e, _f, _g;
92
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l;
92
93
  const shard = registeredShards.get(id);
93
94
  if (!shard) {
94
95
  throw new Error(`Cannot activate shard "${id}": not registered`);
@@ -143,6 +144,13 @@ export async function activateShard(id, opts) {
143
144
  return off;
144
145
  },
145
146
  };
147
+ const hasBrowse = (_a = shard.manifest.permissions) === null || _a === void 0 ? void 0 : _a.includes(PERMISSION_DOCUMENTS_BROWSE);
148
+ const browseCap = hasBrowse
149
+ ? createBrowseCapability(getTenantId(), getDocumentBackend(), {
150
+ canRead: (_c = (_b = shard.manifest.permissions) === null || _b === void 0 ? void 0 : _b.includes(PERMISSION_DOCUMENTS_READ)) !== null && _c !== void 0 ? _c : false,
151
+ canWrite: (_e = (_d = shard.manifest.permissions) === null || _d === void 0 ? void 0 : _d.includes(PERMISSION_DOCUMENTS_WRITE)) !== null && _e !== void 0 ? _e : false,
152
+ })
153
+ : undefined;
146
154
  const ctx = {
147
155
  state: (schema) => sh3.state(id, schema),
148
156
  registerView: (viewId, factory) => {
@@ -212,19 +220,20 @@ export async function activateShard(id, opts) {
212
220
  get tenantId() {
213
221
  return getTenantId();
214
222
  },
215
- zones: ((_a = shard.manifest.permissions) === null || _a === void 0 ? void 0 : _a.includes(PERMISSION_STATE_MANAGE))
223
+ zones: ((_f = shard.manifest.permissions) === null || _f === void 0 ? void 0 : _f.includes(PERMISSION_STATE_MANAGE))
216
224
  ? createZoneManager()
217
225
  : undefined,
218
- browse: ((_b = shard.manifest.permissions) === null || _b === void 0 ? void 0 : _b.includes(PERMISSION_DOCUMENTS_BROWSE))
219
- ? createBrowseCapability(getTenantId(), getDocumentBackend(), {
220
- canRead: shard.manifest.permissions.includes(PERMISSION_DOCUMENTS_READ),
221
- canWrite: shard.manifest.permissions.includes(PERMISSION_DOCUMENTS_WRITE),
222
- })
223
- : undefined,
224
- keys: ((_c = shard.manifest.permissions) === null || _c === void 0 ? void 0 : _c.includes(PERMISSION_KEYS_MINT))
226
+ browse: browseCap,
227
+ documentPicker: browseCap
228
+ ? createDocumentPicker(() => browseCap.listDocuments())
229
+ : createDocumentPicker(async () => {
230
+ const docs = await getDocumentBackend().list(getTenantId(), id);
231
+ return docs.map(d => (Object.assign(Object.assign({}, d), { shardId: id })));
232
+ }),
233
+ keys: ((_g = shard.manifest.permissions) === null || _g === void 0 ? void 0 : _g.includes(PERMISSION_KEYS_MINT))
225
234
  ? createShardKeysApi({
226
235
  shardId: id,
227
- shardPermissions: (_d = shard.manifest.permissions) !== null && _d !== void 0 ? _d : [],
236
+ shardPermissions: (_h = shard.manifest.permissions) !== null && _h !== void 0 ? _h : [],
228
237
  })
229
238
  : undefined,
230
239
  contributions,
@@ -241,7 +250,7 @@ export async function activateShard(id, opts) {
241
250
  sh3: makeSh3Api({
242
251
  callerKind: 'shard',
243
252
  callerShardId: id,
244
- zones: ((_e = shard.manifest.permissions) === null || _e === void 0 ? void 0 : _e.includes(PERMISSION_STATE_MANAGE))
253
+ zones: ((_j = shard.manifest.permissions) === null || _j === void 0 ? void 0 : _j.includes(PERMISSION_STATE_MANAGE))
245
254
  ? createZoneManager()
246
255
  : undefined,
247
256
  }),
@@ -290,7 +299,7 @@ export async function activateShard(id, opts) {
290
299
  try {
291
300
  void fn();
292
301
  }
293
- catch (_h) {
302
+ catch (_m) {
294
303
  // intentionally swallowed: original error is what matters.
295
304
  }
296
305
  }
@@ -304,7 +313,7 @@ export async function activateShard(id, opts) {
304
313
  erroredShards.set(id, {
305
314
  id,
306
315
  error: err,
307
- phase: (_f = opts === null || opts === void 0 ? void 0 : opts.phase) !== null && _f !== void 0 ? _f : 'launch',
316
+ phase: (_k = opts === null || opts === void 0 ? void 0 : opts.phase) !== null && _k !== void 0 ? _k : 'launch',
308
317
  timestamp: Date.now(),
309
318
  });
310
319
  console.error(`[sh3] Shard "${id}" failed to activate:`, err);
@@ -312,7 +321,7 @@ export async function activateShard(id, opts) {
312
321
  }
313
322
  // Activation succeeded — clear any prior error record for this shard.
314
323
  erroredShards.delete(id);
315
- void ((_g = shard.autostart) === null || _g === void 0 ? void 0 : _g.call(shard, ctx));
324
+ void ((_l = shard.autostart) === null || _l === void 0 ? void 0 : _l.call(shard, ctx));
316
325
  }
317
326
  /**
318
327
  * Deactivate an active shard. Calls `shard.deactivate`, flushes and disposes
@@ -2,6 +2,7 @@ import type { StateZones } from '../state/zones.svelte';
2
2
  import type { ZoneSchema, ZoneManager } from '../state/types';
3
3
  import type { DocumentHandle, DocumentHandleOptions } from '../documents/types';
4
4
  import type { BrowseCapability } from '../documents/browse';
5
+ import type { DocumentPickerApi } from '../documents/picker-api';
5
6
  import type { EnvState } from '../env/types';
6
7
  import type { Verb } from '../verbs/types';
7
8
  import type { Sh3Api } from '../verbs/types';
@@ -287,6 +288,13 @@ export interface ShardContext {
287
288
  * owning shard's own `ctx.documents()` handle.
288
289
  */
289
290
  browse?: BrowseCapability;
291
+ /**
292
+ * Programmatic document picker. Opens the DocumentBrowser popup scoped
293
+ * to this shard's document zone (or wider tree if the shard has the
294
+ * `documents:browse` permission). Returns the selected document reference
295
+ * or null if the user cancelled.
296
+ */
297
+ documentPicker: DocumentPickerApi;
290
298
  /**
291
299
  * Mint/list/revoke keys minted by this shard. Only available when the
292
300
  * manifest declares the `keys:mint` permission.
package/dist/version.d.ts CHANGED
@@ -1,2 +1,2 @@
1
1
  /** Auto-generated from package.json — do not edit manually. */
2
- export declare const VERSION = "0.19.5";
2
+ export declare const VERSION = "0.19.6";
package/dist/version.js CHANGED
@@ -1,2 +1,2 @@
1
1
  /** Auto-generated from package.json — do not edit manually. */
2
- export const VERSION = '0.19.5';
2
+ export const VERSION = '0.19.6';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sh3-core",
3
- "version": "0.19.5",
3
+ "version": "0.19.6",
4
4
  "type": "module",
5
5
  "files": [
6
6
  "dist"