@hachej/boring-ui-kit 0.1.110 → 0.2.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 (95) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +255 -239
  3. package/THIRD_PARTY_NOTICES.md +85 -0
  4. package/dist/anchor-conformance.d.ts +78 -0
  5. package/dist/anchor-conformance.js +288 -0
  6. package/dist/canvas-document.d.ts +25 -0
  7. package/dist/canvas-document.js +141 -0
  8. package/dist/canvas-editor-native.d.ts +22 -0
  9. package/dist/canvas-editor-native.js +112 -0
  10. package/dist/canvas-editor.d.ts +6 -0
  11. package/dist/canvas-editor.js +13 -0
  12. package/dist/canvas-mounted.d.ts +41 -0
  13. package/dist/canvas-mounted.js +180 -0
  14. package/dist/canvas-proposals.d.ts +11 -0
  15. package/dist/canvas-proposals.js +45 -0
  16. package/dist/canvas-records.d.ts +3 -0
  17. package/dist/canvas-records.js +39 -0
  18. package/dist/canvas.d.ts +92 -0
  19. package/dist/canvas.js +264 -0
  20. package/dist/contracts.d.ts +125 -0
  21. package/dist/contracts.js +1 -0
  22. package/dist/experience-catalog.d.ts +142 -0
  23. package/dist/experience-catalog.js +25 -0
  24. package/dist/experience-compose.d.ts +83 -0
  25. package/dist/experience-compose.js +218 -0
  26. package/dist/experience-composition-schema.d.ts +29 -0
  27. package/dist/experience-composition-schema.js +22 -0
  28. package/dist/experience-document-viewer.d.ts +13 -0
  29. package/dist/experience-document-viewer.js +54 -0
  30. package/dist/experience-document.d.ts +64 -0
  31. package/dist/experience-document.js +234 -0
  32. package/dist/experience-region-text.d.ts +3 -0
  33. package/dist/experience-region-text.js +84 -0
  34. package/dist/experience-region-tree.d.ts +22 -0
  35. package/dist/experience-region-tree.js +50 -0
  36. package/dist/experience-regions.d.ts +9 -0
  37. package/dist/experience-regions.js +122 -0
  38. package/dist/experience-renderer.d.ts +16 -0
  39. package/dist/experience-renderer.js +37 -0
  40. package/dist/experience.d.ts +12 -0
  41. package/dist/experience.js +23 -0
  42. package/dist/file-tree-shadcn.d.ts +8 -0
  43. package/dist/file-tree-shadcn.js +14 -0
  44. package/dist/file-tree-view.d.ts +64 -0
  45. package/dist/file-tree-view.js +256 -0
  46. package/dist/file-tree.d.ts +57 -0
  47. package/dist/file-tree.js +204 -0
  48. package/dist/html-preview.d.ts +2 -0
  49. package/dist/html-preview.js +47 -0
  50. package/dist/html-viewer.d.ts +13 -0
  51. package/dist/html-viewer.js +69 -0
  52. package/dist/html.d.ts +21 -0
  53. package/dist/html.js +21 -0
  54. package/dist/index.d.ts +2 -472
  55. package/dist/index.js +1 -2671
  56. package/dist/markdown-editor.d.ts +28 -0
  57. package/dist/markdown-editor.js +230 -0
  58. package/dist/markdown-extensions.d.ts +16 -0
  59. package/dist/markdown-extensions.js +96 -0
  60. package/dist/markdown-icons.d.ts +244 -0
  61. package/dist/markdown-icons.js +32 -0
  62. package/dist/markdown-mounted.d.ts +63 -0
  63. package/dist/markdown-mounted.js +263 -0
  64. package/dist/markdown-parser.d.ts +4 -0
  65. package/dist/markdown-parser.js +33 -0
  66. package/dist/markdown-proposal-diff.d.ts +5 -0
  67. package/dist/markdown-proposal-diff.js +122 -0
  68. package/dist/markdown-safety.d.ts +21 -0
  69. package/dist/markdown-safety.js +135 -0
  70. package/dist/markdown.d.ts +86 -0
  71. package/dist/markdown.js +121 -0
  72. package/dist/native-chat.d.ts +150 -0
  73. package/dist/native-chat.js +376 -0
  74. package/dist/pi.d.ts +12 -0
  75. package/dist/pi.js +1 -0
  76. package/dist/public-text-buffer.d.ts +5 -0
  77. package/dist/public-text-buffer.js +1 -0
  78. package/dist/remote-chat.d.ts +76 -0
  79. package/dist/remote-chat.js +324 -0
  80. package/dist/resources.d.ts +34 -0
  81. package/dist/resources.js +1 -0
  82. package/dist/text-buffer-state.d.ts +24 -0
  83. package/dist/text-buffer-state.js +1 -0
  84. package/dist/text-buffer.d.ts +52 -0
  85. package/dist/text-buffer.js +315 -0
  86. package/dist/text-draft-controls.d.ts +9 -0
  87. package/dist/text-draft-controls.js +33 -0
  88. package/dist/text-draft-types.d.ts +111 -0
  89. package/dist/text-draft-types.js +1 -0
  90. package/dist/text-drafts.d.ts +29 -0
  91. package/dist/text-drafts.js +378 -0
  92. package/package.json +226 -58
  93. package/styles/file-tree.css +46 -0
  94. package/dist/styles.css +0 -2308
  95. package/dist/tokens.css +0 -155
@@ -0,0 +1,315 @@
1
+ import { PublicationNotDispatchedError, parsePublicationResult, publicationDigest } from '@hachej/boring-files/publication';
2
+ import { createTextDrafts } from './text-drafts.js';
3
+ import { randomUUID } from '@hachej/boring-files/platform';
4
+ export function freeze(value) {
5
+ if (value !== null && typeof value === 'object') {
6
+ Object.freeze(value);
7
+ for (const nested of Object.values(value))
8
+ freeze(nested);
9
+ }
10
+ return value;
11
+ }
12
+ function sameLocator(left, right) {
13
+ return left.resource.providerId === right.resource.providerId && left.resource.path === right.resource.path
14
+ && left.view.kind === right.view.kind
15
+ && (left.view.kind === 'published' || (right.view.kind === 'working' && left.view.viewId === right.view.viewId));
16
+ }
17
+ export function sameBase(left, right) {
18
+ return sameLocator(left.target, right.target) && left.kind === right.kind
19
+ && (left.kind === 'absent' || (right.kind === 'revision' && left.target.revision === right.target.revision));
20
+ }
21
+ export function createTextBuffer(options) {
22
+ const { instanceId, epoch, client } = options;
23
+ if (!instanceId || !epoch)
24
+ throw new TypeError('Viewer instance and epoch are required');
25
+ const identity = freeze({ ...options.identity });
26
+ if ([identity.scopeId, identity.principalId, identity.initiatorId].some(value => typeof value !== 'string' || !value))
27
+ throw new TypeError('Expected authenticated identity is required');
28
+ const initial = options.source;
29
+ const base = initial.kind === 'saved'
30
+ ? { kind: 'revision', target: structuredClone(initial.snapshot.ref) }
31
+ : { kind: 'absent', target: structuredClone(initial.target) };
32
+ let savedText = initial.kind === 'saved' ? options.readText(initial.snapshot) : '';
33
+ let state = freeze({ identity, text: initial.kind === 'saved' ? savedText : initial.text ?? '', base, bufferVersion: 0, readOnly: options.readOnly === true, dirty: initial.kind === 'new', lifecycle: 'active', save: { kind: 'idle' }, remote: null, recovery: { kind: 'disabled' } });
34
+ const listeners = new Set();
35
+ let attempt;
36
+ /** A save whose lookup found no receipt. Refresh, Discard and Abandon may release it and keep the local text as a draft. */
37
+ let unreceipted;
38
+ let refreshSequence = 0;
39
+ let drafts;
40
+ function update(next) {
41
+ state = freeze(next);
42
+ drafts?.capture();
43
+ notify();
44
+ }
45
+ function notify() {
46
+ for (const listener of [...listeners]) {
47
+ try {
48
+ listener();
49
+ }
50
+ catch (error) {
51
+ queueMicrotask(() => { if (options.onListenerError)
52
+ options.onListenerError(error);
53
+ else
54
+ throw error; });
55
+ }
56
+ }
57
+ }
58
+ function selection() {
59
+ return freeze({ target: { instanceId, epoch, subject: { scopeId: identity.scopeId, base: state.base, bufferVersion: state.bufferVersion } } });
60
+ }
61
+ function selected(value) {
62
+ return value.target.instanceId === instanceId && value.target.epoch === epoch
63
+ && value.target.subject.scopeId === identity.scopeId
64
+ && value.target.subject.bufferVersion === state.bufferVersion && sameBase(value.target.subject.base, state.base);
65
+ }
66
+ function unknown(current, reason) {
67
+ return { kind: 'unknown', operationId: current.request.operationId, reason };
68
+ }
69
+ function acknowledged(receipt, current, digest) {
70
+ if (receipt.operationId !== current.request.operationId || receipt.argumentDigest !== digest || !receipt.evidenceRef
71
+ || receipt.principalId !== identity.principalId || receipt.scopeId !== identity.scopeId
72
+ || receipt.initiatorId !== identity.initiatorId || receipt.changes.length !== 1)
73
+ return undefined;
74
+ const change = receipt.changes[0];
75
+ const expected = current.selection.target.subject.base;
76
+ if (!change || !change.after || !change.after.revision || !sameLocator(change.after, expected.target))
77
+ return undefined;
78
+ if (expected.kind === 'absent')
79
+ return change.kind === 'create' && change.before === null ? change.after : undefined;
80
+ return change.kind === 'replace' && change.before !== null && sameLocator(change.before, expected.target)
81
+ && change.before.revision === expected.target.revision ? change.after : undefined;
82
+ }
83
+ async function settle(current, value) {
84
+ let outcome;
85
+ try {
86
+ outcome = parsePublicationResult(value);
87
+ }
88
+ catch {
89
+ outcome = unknown(current, 'Publication returned a malformed acknowledgement');
90
+ }
91
+ if (outcome.kind === 'partial' || (outcome.kind === 'unknown' && outcome.operationId !== current.request.operationId)) {
92
+ outcome = unknown(current, 'Publication returned an outcome for a different operation or atomicity');
93
+ }
94
+ let result;
95
+ if (outcome.kind === 'committed') {
96
+ let ref;
97
+ try {
98
+ ref = acknowledged(outcome.receipt, current, await current.digest);
99
+ }
100
+ catch {
101
+ ref = undefined;
102
+ }
103
+ result = ref
104
+ ? { kind: 'saved', selection: current.selection, ref: structuredClone(ref), receipt: structuredClone(outcome.receipt) }
105
+ : unknown(current, 'Publication acknowledgement does not match the selected buffer');
106
+ }
107
+ else
108
+ result = outcome;
109
+ if (result.kind === 'saved')
110
+ drafts?.acknowledge(current.draft, current.text);
111
+ if (state.lifecycle !== 'disposed')
112
+ options.sync?.();
113
+ if (attempt !== current)
114
+ return result;
115
+ if (result.kind !== 'unknown')
116
+ attempt = undefined;
117
+ if (state.lifecycle === 'disposed')
118
+ return result;
119
+ if (result.kind === 'saved') {
120
+ savedText = current.text;
121
+ update({ ...state, base: { kind: 'revision', target: result.ref }, dirty: state.text !== savedText, save: { kind: 'settled', result }, remote: null });
122
+ }
123
+ else
124
+ update({ ...state, save: { kind: 'settled', result } });
125
+ return result;
126
+ }
127
+ const flush = (value, signal) => {
128
+ if (state.lifecycle === 'disposed')
129
+ return Promise.resolve({ kind: 'unavailable', reason: 'Viewer is disposed' });
130
+ if (state.readOnly)
131
+ return Promise.resolve({ kind: 'denied', reason: 'Viewer is read-only' });
132
+ if (attempt) {
133
+ const previous = attempt.selection.target;
134
+ if (previous.instanceId === value.target.instanceId && previous.epoch === value.target.epoch
135
+ && previous.subject.scopeId === value.target.subject.scopeId
136
+ && previous.subject.bufferVersion === value.target.subject.bufferVersion && sameBase(previous.subject.base, value.target.subject.base))
137
+ return attempt.promise;
138
+ return Promise.resolve(unknown(attempt, 'Reconcile or await the earlier save before saving another buffer'));
139
+ }
140
+ if (!selected(value))
141
+ return Promise.resolve({ kind: 'conflict', current: state.base.kind === 'revision' ? [state.base.target] : [], reason: 'Save selection is stale or belongs to another viewer' });
142
+ const publish = client.publish?.bind(client);
143
+ if (!publish)
144
+ return Promise.resolve({ kind: 'unavailable', reason: 'Publication is unavailable' });
145
+ if (signal?.aborted)
146
+ return Promise.resolve({ kind: 'denied', reason: 'Save was cancelled before publication' });
147
+ const captured = structuredClone(value);
148
+ const selectedBase = captured.target.subject.base;
149
+ const bytes = new TextEncoder().encode(state.text);
150
+ if (new TextDecoder('utf-8', { ignoreBOM: true }).decode(bytes) !== state.text)
151
+ return Promise.resolve({ kind: 'denied', reason: 'Document text contains invalid Unicode' });
152
+ const request = {
153
+ operationId: randomUUID(), atomicity: 'all-or-nothing',
154
+ changes: [selectedBase.kind === 'absent'
155
+ ? { kind: 'create', target: selectedBase.target, expected: { kind: 'absent' }, bytes, mediaType: options.mediaType }
156
+ : { kind: 'replace', target: selectedBase.target, bytes, mediaType: options.mediaType }],
157
+ };
158
+ const current = {
159
+ selection: freeze(captured), text: state.text, request, digest: publicationDigest(request), draft: drafts?.capture(),
160
+ promise: Promise.resolve().then(async () => {
161
+ try {
162
+ await current.digest;
163
+ }
164
+ catch {
165
+ return settle(current, { kind: 'unavailable', reason: 'Publication request could not be prepared' });
166
+ }
167
+ let outcome;
168
+ try {
169
+ outcome = await publish(request, signal);
170
+ }
171
+ catch (error) {
172
+ outcome = error instanceof PublicationNotDispatchedError && error.operationId === request.operationId
173
+ ? { kind: 'unavailable', reason: error.message }
174
+ : unknown(current, 'Publication acknowledgement was lost');
175
+ }
176
+ return settle(current, outcome);
177
+ }),
178
+ };
179
+ attempt = current;
180
+ update({ ...state, save: { kind: 'pending', operationId: request.operationId } });
181
+ return current.promise;
182
+ };
183
+ const reconcile = async () => {
184
+ const current = attempt;
185
+ if (!current)
186
+ return state.save.kind === 'settled' ? state.save.result : { kind: 'unavailable', reason: 'No save needs reconciliation' };
187
+ await current.promise;
188
+ if (attempt !== current)
189
+ return current.promise;
190
+ if (!client.lookup)
191
+ return unknown(current, 'Operation lookup is unavailable');
192
+ let outcome;
193
+ try {
194
+ outcome = await client.lookup(current.request.operationId);
195
+ }
196
+ catch {
197
+ return unknown(current, 'Operation lookup failed');
198
+ }
199
+ if (outcome && typeof outcome === 'object' && 'kind' in outcome && outcome.kind === 'not-found') {
200
+ if (attempt === current)
201
+ unreceipted = current;
202
+ return unknown(current, 'No retained receipt; the save must not be replayed blindly. Refresh or discard to keep the text as a draft.');
203
+ }
204
+ return settle(current, outcome);
205
+ };
206
+ async function refresh(discard) {
207
+ if (state.lifecycle === 'disposed')
208
+ return { kind: 'unavailable', reason: 'Viewer is disposed' };
209
+ const released = attempt !== undefined && attempt === unreceipted ? attempt : undefined;
210
+ if (attempt !== released)
211
+ return { kind: 'unavailable', reason: 'A save must be acknowledged before refreshing' };
212
+ const sequence = ++refreshSequence;
213
+ const previousBase = state.base;
214
+ const previousVersion = state.bufferVersion;
215
+ const read = await client.read({ target: previousBase.target, revision: { kind: 'latest' } });
216
+ if (!disposed())
217
+ options.sync?.();
218
+ if (disposed() || sequence !== refreshSequence || attempt !== released || !sameBase(previousBase, state.base))
219
+ return read;
220
+ if (read.kind !== 'available' && read.kind !== 'missing')
221
+ return read;
222
+ if (read.kind === 'available' && !sameLocator(read.snapshot.ref, previousBase.target))
223
+ return { kind: 'unavailable', reason: 'Read returned a different resource' };
224
+ const remote = read.kind === 'available' ? { kind: 'revision', target: structuredClone(read.snapshot.ref) } : { kind: 'absent', target: previousBase.target };
225
+ if (released) {
226
+ attempt = undefined;
227
+ unreceipted = undefined;
228
+ update({ ...state, save: { kind: 'idle' } });
229
+ }
230
+ // A document that was saved and is now missing is deleted remotely. Never turn it into empty content.
231
+ if (read.kind === 'missing' && previousBase.kind === 'revision') {
232
+ update({ ...state, remote });
233
+ return read;
234
+ }
235
+ if (sameBase(remote, state.base) && !discard) {
236
+ if (state.remote !== null)
237
+ update({ ...state, remote: null });
238
+ return read;
239
+ }
240
+ if (state.dirty && (!discard || state.bufferVersion !== previousVersion)) {
241
+ update({ ...state, remote });
242
+ return read;
243
+ }
244
+ let decoded;
245
+ try {
246
+ decoded = read.kind === 'available' ? options.readText(read.snapshot) : options.emptyText ?? '';
247
+ if (disposed() || sequence !== refreshSequence || attempt || !sameBase(previousBase, state.base) || state.bufferVersion !== previousVersion)
248
+ return read;
249
+ options.replaceText?.(decoded);
250
+ if (disposed() || sequence !== refreshSequence || attempt || !sameBase(previousBase, state.base) || state.bufferVersion !== previousVersion)
251
+ return read;
252
+ }
253
+ catch {
254
+ return { kind: 'unavailable', reason: 'Remote document could not be decoded or validated' };
255
+ }
256
+ const discardedDraft = discard ? drafts?.currentRef() : undefined;
257
+ savedText = decoded;
258
+ update({ ...state, text: savedText, base: remote, bufferVersion: state.bufferVersion + 1, dirty: remote.kind === 'absent', remote: null, save: { kind: 'idle' } });
259
+ if (discardedDraft)
260
+ drafts?.remove(discardedDraft);
261
+ return read;
262
+ }
263
+ /** Give up waiting for an unknown save and refresh, keeping the draft. The save is never reported committed and never replayed. */
264
+ const abandon = async () => {
265
+ const current = attempt;
266
+ if (!current || state.save.kind !== 'settled' || state.save.result.kind !== 'unknown')
267
+ return { kind: 'unavailable', reason: 'No unconfirmed save to abandon' };
268
+ unreceipted = current;
269
+ return refresh(false);
270
+ };
271
+ function disposed() { return state.lifecycle === 'disposed'; }
272
+ function observe(text, force = false) {
273
+ if (disposed())
274
+ throw new Error('Viewer is disposed');
275
+ if (typeof text !== 'string')
276
+ throw new TypeError('Document text must be a string');
277
+ if (force || text !== state.text)
278
+ update({ ...state, text, bufferVersion: state.bufferVersion + 1, dirty: state.base.kind === 'absent' || text !== savedText });
279
+ }
280
+ drafts = createTextDrafts({
281
+ binding: options.drafts, emptyText: options.emptyText ?? '', snapshot: () => state, selection, selected, sameBase, sameLocator,
282
+ pending: () => attempt !== undefined, read: () => client.read({ target: state.base.target, revision: { kind: 'latest' } }),
283
+ sync: () => options.sync?.(),
284
+ changed: recovery => { state = freeze({ ...state, recovery }); notify(); },
285
+ restore: (text, guard) => {
286
+ options.replaceText?.(text);
287
+ if (!guard())
288
+ return false;
289
+ update({ ...state, text, bufferVersion: state.bufferVersion + 1, dirty: true, save: { kind: 'idle' }, remote: null });
290
+ return true;
291
+ },
292
+ });
293
+ state = freeze({ ...state, recovery: drafts.state() });
294
+ if (state.dirty && state.text)
295
+ drafts.capture();
296
+ return {
297
+ ...drafts.actions,
298
+ getSnapshot: () => state,
299
+ subscribe: (listener) => { if (disposed())
300
+ return () => { }; listeners.add(listener); return () => { listeners.delete(listener); }; },
301
+ edit: (text, force = false) => {
302
+ if (disposed())
303
+ throw new Error('Viewer is disposed');
304
+ if (state.readOnly)
305
+ throw new Error('Viewer is read-only');
306
+ observe(text, force);
307
+ },
308
+ observe, selection, refresh, abandon, reconcile, flush,
309
+ dispose: () => { if (!disposed()) {
310
+ drafts?.dispose();
311
+ update({ ...state, lifecycle: 'disposed' });
312
+ listeners.clear();
313
+ } },
314
+ };
315
+ }
@@ -0,0 +1,9 @@
1
+ import type { TextDraftActions, TextDraftRecoveryState } from './text-draft-types.js';
2
+ export interface TextDraftControlsProps {
3
+ readonly recovery: TextDraftRecoveryState;
4
+ readonly actions: TextDraftActions;
5
+ readonly readOnly?: boolean;
6
+ readonly blocked?: boolean;
7
+ }
8
+ export declare function TextDraftControls({ recovery, actions, readOnly, blocked }: TextDraftControlsProps): import("react").JSX.Element | null;
9
+ //# sourceMappingURL=text-draft-controls.d.ts.map
@@ -0,0 +1,33 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
+ import { useLayoutEffect, useRef, useState } from 'react';
4
+ export function TextDraftControls({ recovery, actions, readOnly = false, blocked = false }) {
5
+ const owner = useRef({ actions, busy: false });
6
+ const [pending, setPending] = useState(false);
7
+ const [notice, setNotice] = useState();
8
+ useLayoutEffect(() => { owner.current = { actions, busy: false }; setPending(false); setNotice(undefined); return () => { if (owner.current?.actions === actions)
9
+ owner.current = undefined; }; }, [actions]);
10
+ if (recovery.kind === 'disabled')
11
+ return null;
12
+ const active = recovery.kind === 'active';
13
+ const run = (action) => {
14
+ if (!owner.current || owner.current.busy || owner.current.actions !== actions)
15
+ return;
16
+ const capturedOwner = owner.current;
17
+ capturedOwner.busy = true;
18
+ setPending(true);
19
+ setNotice(undefined);
20
+ void Promise.resolve().then(action).then(result => {
21
+ if (owner.current === capturedOwner)
22
+ setNotice(result.reason ?? (result.kind === 'restored' ? 'Draft restored locally. Save to publish it.' : result.kind === 'discarded' ? 'Stored draft discarded. Current edits are unchanged.' : undefined));
23
+ }, () => { if (owner.current === capturedOwner)
24
+ setNotice('Draft storage unavailable. Current edits are unchanged.'); }).finally(() => { if (owner.current === capturedOwner) {
25
+ capturedOwner.busy = false;
26
+ setPending(false);
27
+ } ; });
28
+ };
29
+ const discovery = active ? recovery.discovery : undefined;
30
+ const checkpoint = active ? recovery.checkpoint : undefined;
31
+ const status = !active ? `Draft recovery ${recovery.kind}` : discovery?.kind === 'checking' ? 'Checking stored drafts' : checkpoint?.kind === 'pending' ? 'Storing draft' : checkpoint?.kind === 'stored' ? 'Draft stored for recovery. Storage is not a save receipt.' : checkpoint?.kind === 'failed' ? 'Draft recovery storage unavailable' : 'Draft recovery enabled';
32
+ return _jsxs("section", { "data-boring": "text-draft-controls", "aria-label": "Draft recovery", children: [_jsx("p", { role: "status", children: status }), _jsx("button", { type: "button", disabled: !active || pending || blocked || discovery?.kind === 'checking', onClick: () => run(actions.checkDrafts), children: "Check stored drafts" }), _jsx("button", { type: "button", disabled: !active || pending || readOnly, onClick: () => run(actions.checkpointDraft), children: "Store draft for recovery" }), discovery?.kind === 'empty' && _jsx("p", { children: "No stored drafts" }), discovery && 'reason' in discovery && _jsx("p", { role: "alert", children: discovery.reason }), checkpoint?.kind === 'failed' && _jsx("p", { role: "alert", children: checkpoint.result.reason }), discovery?.kind === 'offered' && _jsxs(_Fragment, { children: [discovery.truncated && _jsx("p", { children: "More stored drafts exist than this listing shows." }), discovery.choices.map(choice => _jsxs("div", { role: "group", "aria-label": "Stored draft", children: [_jsx("p", { children: choice.compatibility === 'conflict' ? 'Saved revision changed. Review this draft separately.' : 'Stored draft matches the checked saved revision.' }), _jsxs("details", { children: [_jsx("summary", { children: "Review stored draft" }), _jsx("pre", { children: choice.text })] }), _jsx("button", { type: "button", disabled: pending || blocked || readOnly || choice.compatibility !== 'exact', onClick: () => run(() => actions.restoreDraft(choice.selection)), children: "Restore draft" }), _jsx("button", { type: "button", disabled: pending || blocked, onClick: () => run(() => actions.discardDraft(choice.selection)), children: "Discard stored draft" })] }, `${choice.selection.offerId}:${choice.selection.draft.writerId}:${choice.selection.draft.sequence}`))] }), notice && _jsx("p", { role: "alert", children: notice })] });
33
+ }
@@ -0,0 +1,111 @@
1
+ import type { ResourceAccess, ResourceExpectation, ResourceLocator } from '@hachej/boring-files';
2
+ import type { SaveSelection } from './resources.js';
3
+ export interface TextDraftKey {
4
+ readonly identity: Pick<ResourceAccess, 'principalId' | 'scopeId' | 'initiatorId'>;
5
+ readonly providerInstanceId: string;
6
+ readonly target: ResourceLocator;
7
+ readonly format: string;
8
+ }
9
+ export interface TextDraftRef {
10
+ readonly key: TextDraftKey;
11
+ readonly base: ResourceExpectation;
12
+ readonly writerId: string;
13
+ readonly sequence: number;
14
+ }
15
+ export interface TextDraft {
16
+ readonly version: 1;
17
+ readonly ref: TextDraftRef;
18
+ readonly text: string;
19
+ readonly createdAt: number;
20
+ readonly expiresAt: number;
21
+ }
22
+ export type TextDraftFailure = {
23
+ readonly kind: 'denied' | 'expired' | 'unavailable' | 'unknown';
24
+ readonly reason: string;
25
+ };
26
+ export interface TextDraftStore {
27
+ readonly list: (key: TextDraftKey, limit: number) => Promise<{
28
+ readonly kind: 'available';
29
+ readonly drafts: readonly TextDraft[];
30
+ readonly truncated: boolean;
31
+ } | TextDraftFailure>;
32
+ readonly write: (draft: TextDraft) => Promise<{
33
+ readonly kind: 'stored' | 'superseded';
34
+ } | TextDraftFailure>;
35
+ readonly remove: (ref: TextDraftRef) => Promise<{
36
+ readonly kind: 'removed' | 'missing' | 'superseded';
37
+ } | TextDraftFailure>;
38
+ }
39
+ export interface TextDraftOptions {
40
+ readonly store: TextDraftStore;
41
+ readonly providerInstanceId: string;
42
+ readonly signal: AbortSignal;
43
+ readonly expiresAt: number;
44
+ readonly retentionMs: number;
45
+ readonly maxBytes?: number;
46
+ readonly maxDrafts?: number;
47
+ }
48
+ export interface TextDraftBinding extends TextDraftOptions {
49
+ readonly format: string;
50
+ readonly validateText?: (text: string) => void;
51
+ }
52
+ export interface TextDraftChoiceSelection {
53
+ readonly offerId: string;
54
+ readonly draft: TextDraftRef;
55
+ readonly viewer: SaveSelection;
56
+ readonly observedCurrent: ResourceExpectation;
57
+ }
58
+ export interface TextDraftChoice {
59
+ readonly selection: TextDraftChoiceSelection;
60
+ readonly text: string;
61
+ readonly createdAt: number;
62
+ readonly expiresAt: number;
63
+ readonly compatibility: 'exact' | 'conflict';
64
+ }
65
+ export type TextDraftDiscovery = {
66
+ readonly kind: 'idle' | 'checking' | 'empty';
67
+ } | {
68
+ readonly kind: 'offered';
69
+ readonly choices: readonly TextDraftChoice[];
70
+ readonly truncated: boolean;
71
+ } | TextDraftFailure;
72
+ export type TextDraftCheckpoint = {
73
+ readonly kind: 'idle';
74
+ } | {
75
+ readonly kind: 'pending' | 'stored';
76
+ readonly ref: TextDraftRef;
77
+ } | {
78
+ readonly kind: 'failed';
79
+ readonly ref: TextDraftRef;
80
+ readonly operation: 'write' | 'remove';
81
+ readonly result: TextDraftFailure;
82
+ };
83
+ export type TextDraftRecoveryState = {
84
+ readonly kind: 'disabled' | 'revoked' | 'expired' | 'disposed';
85
+ } | {
86
+ readonly kind: 'active';
87
+ readonly checkpoint: TextDraftCheckpoint;
88
+ readonly discovery: TextDraftDiscovery;
89
+ };
90
+ export type TextDraftChoiceFailure = TextDraftFailure | {
91
+ readonly kind: 'conflict';
92
+ readonly reason: string;
93
+ };
94
+ export type TextDraftCheckpointResult = {
95
+ readonly kind: 'stored';
96
+ readonly ref: TextDraftRef;
97
+ } | {
98
+ readonly kind: 'clean' | 'superseded';
99
+ } | TextDraftFailure;
100
+ export interface TextDraftActions {
101
+ readonly checkDrafts: () => Promise<TextDraftDiscovery>;
102
+ readonly checkpointDraft: () => Promise<TextDraftCheckpointResult>;
103
+ readonly restoreDraft: (choice: TextDraftChoiceSelection) => Promise<{
104
+ readonly kind: 'restored';
105
+ readonly selection: SaveSelection;
106
+ } | TextDraftChoiceFailure>;
107
+ readonly discardDraft: (choice: TextDraftChoiceSelection) => Promise<{
108
+ readonly kind: 'discarded';
109
+ } | TextDraftChoiceFailure>;
110
+ }
111
+ //# sourceMappingURL=text-draft-types.d.ts.map
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,29 @@
1
+ import type { ReadResult, ResourceExpectation, ResourceLocator } from '@hachej/boring-files';
2
+ import type { SaveSelection } from './resources.js';
3
+ import type { TextBufferState } from './text-buffer-state.js';
4
+ import type { TextDraftActions, TextDraftBinding, TextDraftChoiceFailure, TextDraftRecoveryState, TextDraftRef } from './text-draft-types.js';
5
+ interface Owner {
6
+ readonly binding: TextDraftBinding | undefined;
7
+ readonly emptyText: string;
8
+ readonly snapshot: () => TextBufferState;
9
+ readonly selection: () => SaveSelection;
10
+ readonly selected: (selection: SaveSelection) => boolean;
11
+ readonly sameBase: (left: ResourceExpectation, right: ResourceExpectation) => boolean;
12
+ readonly sameLocator: (left: ResourceLocator, right: ResourceLocator) => boolean;
13
+ readonly pending: () => boolean;
14
+ readonly read: () => Promise<ReadResult>;
15
+ readonly sync: () => void;
16
+ readonly changed: (state: TextDraftRecoveryState) => void;
17
+ readonly restore: (text: string, guard: () => boolean) => boolean;
18
+ }
19
+ export declare function createTextDrafts(owner: Owner): {
20
+ actions: TextDraftActions;
21
+ capture: () => TextDraftRef | undefined;
22
+ remove: (ref: TextDraftRef) => Promise<TextDraftChoiceFailure | undefined>;
23
+ currentRef: () => TextDraftRef | undefined;
24
+ state: () => TextDraftRecoveryState;
25
+ acknowledge: (ref: TextDraftRef | undefined, text: string) => void;
26
+ dispose: () => void;
27
+ };
28
+ export {};
29
+ //# sourceMappingURL=text-drafts.d.ts.map