@hachej/boring-ui-kit 0.1.111 → 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,112 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useLayoutEffect, useMemo, useRef, useState, useSyncExternalStore } from 'react';
4
+ import { useEditor, useValue, GeoShapeGeoStyle, LANGUAGES } from '@tldraw/editor';
5
+ import { Tldraw, iconTypes, DEFAULT_EMBED_DEFINITIONS } from 'tldraw';
6
+ import { createMountedCanvasTools } from './canvas-mounted.js';
7
+ import { CanvasProposals } from './canvas-proposals.js';
8
+ const fontKeys = [
9
+ 'tldraw_draw', 'tldraw_draw_italic', 'tldraw_draw_bold', 'tldraw_draw_italic_bold',
10
+ 'tldraw_sans', 'tldraw_sans_italic', 'tldraw_sans_bold', 'tldraw_sans_italic_bold',
11
+ 'tldraw_serif', 'tldraw_serif_italic', 'tldraw_serif_bold', 'tldraw_serif_italic_bold',
12
+ 'tldraw_mono', 'tldraw_mono_italic', 'tldraw_mono_bold', 'tldraw_mono_italic_bold',
13
+ ];
14
+ function assetSignature(assets) {
15
+ const requirements = [
16
+ ...fontKeys.map((key) => [`fonts.${key}`, assets?.fonts?.[key]]),
17
+ ...iconTypes.map((key) => [`icons.${key}`, assets?.icons?.[key]]),
18
+ ...LANGUAGES.map(({ locale }) => [`translations.${locale}`, assets?.translations?.[locale]]),
19
+ ...DEFAULT_EMBED_DEFINITIONS.map(({ type }) => [`embedIcons.${type}`, assets?.embedIcons?.[type]]),
20
+ ];
21
+ for (const [key, value] of requirements)
22
+ if (typeof value !== 'string' || !value.trim())
23
+ throw new TypeError(`Canvas asset URL is required: ${key}`);
24
+ return JSON.stringify(requirements);
25
+ }
26
+ export default function NativeCanvas({ controller, assetUrls, title = 'Canvas', className, height = 480, licenseKey, onMount, onMountedTools }) {
27
+ const state = useSyncExternalStore(controller.subscribe, controller.getSnapshot, controller.getSnapshot);
28
+ const [error, setError] = useState(null);
29
+ const [busy, setBusy] = useState(false);
30
+ const [nativeEditor, setNativeEditor] = useState(null);
31
+ const mounted = useRef(true);
32
+ const revokeTools = useRef(null);
33
+ useLayoutEffect(() => { mounted.current = true; return () => { mounted.current = false; }; }, []);
34
+ const signature = useMemo(() => assetSignature(assetUrls), [assetUrls]);
35
+ const assets = useMemo(() => ({ fonts: { ...assetUrls.fonts }, icons: { ...assetUrls.icons }, translations: { ...assetUrls.translations }, embedIcons: { ...assetUrls.embedIcons } }), [signature]);
36
+ const outcome = state.save.kind === 'settled' ? state.save.result : undefined;
37
+ const pending = state.save.kind === 'pending';
38
+ const uncertain = outcome?.kind === 'unknown';
39
+ const conflict = outcome?.kind === 'conflict' || state.remote !== null;
40
+ const unavailable = state.readOnly || state.lifecycle === 'disposed';
41
+ const run = async (action) => {
42
+ if (!mounted.current || controller.getSnapshot().lifecycle === 'disposed')
43
+ return;
44
+ setBusy(true);
45
+ setError(null);
46
+ try {
47
+ const result = await action();
48
+ if (mounted.current && (result.kind === 'unavailable' || result.kind === 'denied' || result.kind === 'conflict' || result.kind === 'unknown'))
49
+ setError(result.reason);
50
+ }
51
+ catch (cause) {
52
+ if (mounted.current)
53
+ setError(cause instanceof Error ? cause.message : 'Canvas operation failed');
54
+ }
55
+ finally {
56
+ if (mounted.current)
57
+ setBusy(false);
58
+ }
59
+ };
60
+ const save = () => run(() => controller.flush(controller.actions.selection()));
61
+ const attach = (editor) => {
62
+ if (controller.getSnapshot().readOnly || controller.getSnapshot().lifecycle === 'disposed')
63
+ editor.updateInstanceState({ isReadonly: true });
64
+ editor.registerExternalAssetHandler('file', null);
65
+ editor.registerExternalAssetHandler('url', null);
66
+ for (const type of externalContentTypes)
67
+ editor.registerExternalContentHandler(type, null);
68
+ const cleanup = onMount?.(editor);
69
+ setNativeEditor(editor);
70
+ return () => { revokeTools.current?.(); setNativeEditor(current => current === editor ? null : current); cleanup?.(); };
71
+ };
72
+ return _jsxs("section", { className: className, "data-boring": "canvas-editor", "data-dirty": state.dirty || undefined, onKeyDownCapture: event => {
73
+ if ((event.ctrlKey || event.metaKey) && event.key.toLowerCase() === 's') {
74
+ event.preventDefault();
75
+ if (!unavailable && !pending && !uncertain && !busy && !state.problem)
76
+ void save();
77
+ }
78
+ }, children: [_jsxs("header", { children: [_jsx("strong", { children: title }), state.readOnly && _jsx("span", { children: "Read only" }), _jsx("span", { role: "status", children: state.lifecycle === 'disposed' ? 'Closed' : pending ? 'Saving' : uncertain ? 'Save unconfirmed' : conflict ? 'Changed elsewhere' : state.dirty ? 'Unsaved changes' : 'Saved' }), _jsx("button", { type: "button", disabled: unavailable || !state.dirty || pending || uncertain || busy || state.problem !== null, onClick: () => void save(), children: "Save" }), _jsx("button", { type: "button", disabled: state.lifecycle === 'disposed' || pending || uncertain || busy, onClick: () => void run(controller.actions.refresh), children: "Check saved version" })] }), uncertain && _jsxs("div", { role: "alert", children: ["Save acknowledgement was lost. Your current canvas is retained.", _jsx("button", { type: "button", disabled: busy, onClick: () => void run(controller.actions.reconcile), children: "Check save outcome" }), _jsx("button", { type: "button", disabled: busy || state.lifecycle === 'disposed', onClick: () => void run(controller.actions.abandon), children: "Keep draft and refresh" })] }), conflict && _jsxs("div", { role: "alert", children: ["The saved canvas changed. Your local edits have been kept.", _jsx("button", { type: "button", disabled: pending || uncertain || busy, onClick: () => void run(controller.actions.discardToRemote), children: "Discard local changes and reload" })] }), state.problem && _jsx("p", { role: "alert", children: state.problem }), (outcome?.kind === 'denied' || outcome?.kind === 'unavailable') && _jsx("p", { role: "alert", children: outcome.reason }), error && _jsx("p", { role: "alert", children: error }), _jsx(CanvasProposals, { controller: controller, state: state, editor: nativeEditor, busy: busy, run: run }), state.lifecycle === 'active' && _jsx("div", { "data-boring": "canvas-stage", style: { position: 'relative', height }, onClickCapture: event => { if (event.target instanceof Element && event.target.closest('a'))
79
+ event.preventDefault(); }, onAuxClickCapture: event => { if (event.target instanceof Element && event.target.closest('a'))
80
+ event.preventDefault(); }, children: _jsxs(Tldraw, { store: controller.store, hideUi: true, locale: "en", autoFocus: false, assetUrls: assets, ...(licenseKey === undefined ? {} : { licenseKey }), onMount: attach, children: [_jsx(CanvasTools, { controller: controller }), _jsx(MountedCommands, { controller: controller, revokeTools: revokeTools, ...(onMountedTools ? { onMountedTools } : {}) })] }) })] });
81
+ }
82
+ const externalContentTypes = ['text', 'files', 'file-replace', 'embed', 'svg-text', 'url', 'tldraw', 'excalidraw'];
83
+ const choices = [
84
+ ['select', 'Select'], ['hand', 'Hand'], ['draw', 'Draw'], ['geo', 'Rectangle'], ['text', 'Text'],
85
+ ['arrow', 'Arrow'], ['line', 'Line'], ['note', 'Note'], ['frame', 'Frame'], ['highlight', 'Highlight'], ['eraser', 'Erase'],
86
+ ];
87
+ function CanvasTools({ controller }) {
88
+ const editor = useEditor();
89
+ const state = useValue('canvas controls', () => ({ tool: editor.getCurrentToolId(), readOnly: controller.getSnapshot().readOnly || editor.getIsReadonly(), undo: editor.getCanUndo(), redo: editor.getCanRedo() }), [editor]);
90
+ const act = (action, changesDocument = true) => {
91
+ if (!editor.isDisposed && controller.getSnapshot().lifecycle === 'active'
92
+ && (!changesDocument || (!controller.getSnapshot().readOnly && !editor.getIsReadonly())))
93
+ action();
94
+ };
95
+ return _jsxs("div", { role: "toolbar", "aria-label": "Canvas tools", style: { position: 'absolute', top: 8, left: 8, zIndex: 300, display: 'flex', flexWrap: 'wrap', gap: 4 }, children: [choices.map(([id, label]) => _jsx("button", { type: "button", "aria-pressed": state.tool === id, disabled: state.readOnly && id !== 'hand' && id !== 'select', onClick: () => act(() => { if (id === 'geo')
96
+ editor.setStyleForNextShapes(GeoShapeGeoStyle, 'rectangle'); editor.setCurrentTool(id); }, id !== 'hand' && id !== 'select'), children: label }, id)), _jsx("button", { type: "button", disabled: state.readOnly || !state.undo, onClick: () => act(() => { editor.undo(); }), children: "Undo" }), _jsx("button", { type: "button", disabled: state.readOnly || !state.redo, onClick: () => act(() => { editor.redo(); }), children: "Redo" }), _jsx("button", { type: "button", onClick: () => act(() => { editor.zoomToFit(); }, false), children: "Fit canvas" })] });
97
+ }
98
+ function MountedCommands({ controller, onMountedTools, revokeTools }) {
99
+ const editor = useEditor();
100
+ const binding = useMemo(() => createMountedCanvasTools({ controller, editor }), [controller, editor]);
101
+ useLayoutEffect(() => {
102
+ binding.activate();
103
+ revokeTools.current = binding.dispose;
104
+ return () => { binding.dispose(); if (revokeTools.current === binding.dispose)
105
+ revokeTools.current = null; };
106
+ }, [binding, revokeTools]);
107
+ useLayoutEffect(() => {
108
+ onMountedTools?.(binding.tools);
109
+ return () => { onMountedTools?.(null); };
110
+ }, [binding, onMountedTools]);
111
+ return null;
112
+ }
@@ -0,0 +1,6 @@
1
+ export type { CanvasMountedSubject, CanvasMountedInspection, CanvasMountedTools } from './canvas-mounted.js';
2
+ import type { CanvasEditorProps } from './canvas-editor-native.js';
3
+ export type { CanvasAssetUrls, CanvasEditorProps, CanvasFontUrls } from './canvas-editor-native.js';
4
+ /** Borrows one controller/store. The host supplies native CSS, fonts and any production license. */
5
+ export declare function CanvasEditor(props: CanvasEditorProps): import("react").JSX.Element;
6
+ //# sourceMappingURL=canvas-editor.d.ts.map
@@ -0,0 +1,13 @@
1
+ 'use client';
2
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { lazy, Suspense, useState, useSyncExternalStore } from 'react';
4
+ import { TextDraftControls } from './text-draft-controls.js';
5
+ const NativeCanvas = lazy(() => import('./canvas-editor-native.js'));
6
+ /** Borrows one controller/store. The host supplies native CSS, fonts and any production license. */
7
+ export function CanvasEditor(props) {
8
+ const state = useSyncExternalStore(props.controller.subscribe, props.controller.getSnapshot, props.controller.getSnapshot);
9
+ const [mount, setMount] = useState({ controller: props.controller, sequence: 0 });
10
+ if (mount.controller !== props.controller)
11
+ setMount({ controller: props.controller, sequence: mount.sequence + 1 });
12
+ return _jsxs(_Fragment, { children: [_jsx(TextDraftControls, { recovery: state.recovery, actions: props.controller.actions, readOnly: state.readOnly, blocked: state.lifecycle !== 'active' || state.save.kind === 'pending' || (state.save.kind === 'settled' && state.save.result.kind === 'unknown') }, mount.sequence), _jsx(Suspense, { fallback: _jsx("p", { role: "status", children: "Loading canvas" }), children: _jsx(NativeCanvas, { ...props }, mount.sequence) })] });
13
+ }
@@ -0,0 +1,41 @@
1
+ import type { BoxLike, Editor, TLCamera, TLPageId, TLShape, TLShapeId } from '@tldraw/editor';
2
+ import type { PresentationCommand, ViewerTarget } from './contracts.js';
3
+ import type { CanvasController, CanvasProposalInput } from './canvas.js';
4
+ import type { SaveSelection } from './resources.js';
5
+ export type CanvasMountedSubject = SaveSelection['target']['subject'] & {
6
+ readonly mountId: string;
7
+ readonly pageId: TLPageId;
8
+ };
9
+ export interface CanvasMountedInspection {
10
+ readonly selection: SaveSelection;
11
+ readonly dirty: boolean;
12
+ readonly pageId: TLPageId;
13
+ readonly shapes: readonly TLShape[];
14
+ readonly selectedShapeIds: readonly TLShapeId[];
15
+ readonly camera: TLCamera;
16
+ readonly viewport: Readonly<BoxLike>;
17
+ }
18
+ export interface CanvasMountedTools {
19
+ readonly getTarget: () => ViewerTarget<CanvasMountedSubject> | null;
20
+ readonly inspect: PresentationCommand<{
21
+ readonly expiresAt: number;
22
+ }, CanvasMountedInspection, CanvasMountedSubject>;
23
+ readonly select: PresentationCommand<{
24
+ readonly expiresAt: number;
25
+ readonly shapeIds: readonly string[];
26
+ }, void, CanvasMountedSubject>;
27
+ readonly frame: PresentationCommand<{
28
+ readonly expiresAt: number;
29
+ readonly shapeIds: readonly string[];
30
+ }, void, CanvasMountedSubject>;
31
+ readonly propose: PresentationCommand<CanvasProposalInput, void, CanvasMountedSubject>;
32
+ }
33
+ export declare function createMountedCanvasTools({ controller, editor }: {
34
+ readonly controller: CanvasController;
35
+ readonly editor: Editor;
36
+ }): {
37
+ tools: CanvasMountedTools;
38
+ activate: () => void;
39
+ dispose: () => void;
40
+ };
41
+ //# sourceMappingURL=canvas-mounted.d.ts.map
@@ -0,0 +1,180 @@
1
+ import { randomUUID } from '@hachej/boring-files/platform';
2
+ import { freeze, sameBase } from './text-buffer.js';
3
+ const expirySchema = { type: 'integer', minimum: Number.MIN_SAFE_INTEGER, maximum: Number.MAX_SAFE_INTEGER };
4
+ function expiry(value) {
5
+ if (!value || typeof value !== 'object' || !('expiresAt' in value) || typeof value.expiresAt !== 'number' || !Number.isSafeInteger(value.expiresAt))
6
+ throw new TypeError('Command expiry is required');
7
+ return { expiresAt: value.expiresAt };
8
+ }
9
+ function shapesInput(value) {
10
+ const deadline = expiry(value);
11
+ if (!value || typeof value !== 'object' || !('shapeIds' in value) || !Array.isArray(value.shapeIds))
12
+ throw new TypeError('Shape IDs are required');
13
+ const shapeIds = [];
14
+ for (const id of value.shapeIds) {
15
+ if (typeof id !== 'string' || !id)
16
+ throw new TypeError('Shape IDs must be nonempty strings');
17
+ if (!shapeIds.includes(id))
18
+ shapeIds.push(id);
19
+ }
20
+ return { ...deadline, shapeIds };
21
+ }
22
+ const shapesSchema = { type: 'object', properties: { expiresAt: expirySchema, shapeIds: { type: 'array', items: { type: 'string', minLength: 1 } } }, required: ['expiresAt', 'shapeIds'] };
23
+ function finiteBounds(box) {
24
+ return [box.x, box.y, box.w, box.h, box.x + box.w, box.y + box.h].every(Number.isFinite) && box.w >= 0 && box.h >= 0;
25
+ }
26
+ export function createMountedCanvasTools({ controller, editor }) {
27
+ const mountId = randomUUID();
28
+ let alive = false;
29
+ const available = () => alive && !editor.isDisposed && editor.store === controller.store
30
+ && editor.getContainer().isConnected && controller.getSnapshot().lifecycle === 'active' && !controller.getSnapshot().problem;
31
+ const viewportReady = () => {
32
+ const screen = editor.getContainer().getBoundingClientRect();
33
+ const viewport = editor.getViewportScreenBounds();
34
+ return [screen.x, screen.y, screen.width, screen.height].every(Number.isFinite) && screen.width > 0 && screen.height > 0
35
+ && finiteBounds(viewport) && Math.abs(viewport.x - screen.x) < 1e-6 && Math.abs(viewport.y - screen.y) < 1e-6
36
+ && Math.abs(viewport.w - screen.width) < 1e-6 && Math.abs(viewport.h - screen.height) < 1e-6;
37
+ };
38
+ const getTarget = () => {
39
+ if (!available())
40
+ return null;
41
+ const pageId = editor.getCurrentPageId();
42
+ let selected;
43
+ try {
44
+ selected = controller.actions.selection().target;
45
+ }
46
+ catch (error) {
47
+ if (!available())
48
+ return null;
49
+ throw error;
50
+ }
51
+ if (!available() || editor.getCurrentPageId() !== pageId)
52
+ return null;
53
+ return freeze({ ...selected, subject: { ...selected.subject, mountId, pageId } });
54
+ };
55
+ const check = (target, expiresAt, signal) => {
56
+ if (!available())
57
+ return { kind: 'unavailable', reason: 'Mounted canvas is unavailable' };
58
+ if (signal?.aborted)
59
+ return { kind: 'denied', reason: 'Command was cancelled' };
60
+ if (expiresAt <= Date.now())
61
+ return { kind: 'stale', reason: 'Command expired' };
62
+ const selected = getTarget();
63
+ if (!selected)
64
+ return { kind: 'unavailable', reason: 'Mounted canvas changed during capture' };
65
+ if (selected.instanceId !== target.instanceId || selected.epoch !== target.epoch
66
+ || selected.subject.mountId !== target.subject.mountId || selected.subject.pageId !== target.subject.pageId
67
+ || selected.subject.scopeId !== target.subject.scopeId || selected.subject.bufferVersion !== target.subject.bufferVersion
68
+ || !sameBase(selected.subject.base, target.subject.base))
69
+ return { kind: 'stale', reason: 'Mounted command target changed' };
70
+ if (!available())
71
+ return { kind: 'unavailable', reason: 'Mounted canvas is unavailable' };
72
+ if (signal?.aborted)
73
+ return { kind: 'denied', reason: 'Command was cancelled' };
74
+ if (expiresAt <= Date.now())
75
+ return { kind: 'stale', reason: 'Command expired' };
76
+ return null;
77
+ };
78
+ const operate = async (target, input, frame, signal) => {
79
+ const captured = structuredClone(target), parsed = shapesInput(input);
80
+ const refused = check(captured, parsed.expiresAt, signal);
81
+ if (refused)
82
+ return refused;
83
+ const shapes = editor.getCurrentPageShapes();
84
+ const ids = [];
85
+ for (const requested of parsed.shapeIds) {
86
+ const shape = shapes.find(shape => shape.id === requested);
87
+ if (!shape)
88
+ return { kind: 'denied', reason: 'Shape is not on the current page' };
89
+ ids.push(shape.id);
90
+ }
91
+ if (frame) {
92
+ if (!ids.length)
93
+ return { kind: 'denied', reason: 'Framing requires at least one shape' };
94
+ if (editor.getCameraOptions().isLocked)
95
+ return { kind: 'denied', reason: 'Canvas camera is locked' };
96
+ if (!viewportReady())
97
+ return { kind: 'unavailable', reason: 'Canvas viewport is unavailable or awaiting layout' };
98
+ let bounds;
99
+ for (const id of ids) {
100
+ const box = editor.getShapePageBounds(id);
101
+ if (!box || !finiteBounds(box))
102
+ return { kind: 'unavailable', reason: 'Shape bounds are unavailable' };
103
+ if (!bounds)
104
+ bounds = { x: box.x, y: box.y, w: box.w, h: box.h };
105
+ else {
106
+ const x = Math.min(bounds.x, box.x), y = Math.min(bounds.y, box.y);
107
+ bounds = { x, y, w: Math.max(bounds.x + bounds.w, box.x + box.w) - x, h: Math.max(bounds.y + bounds.h, box.y + box.h) - y };
108
+ }
109
+ }
110
+ if (!bounds || !finiteBounds(bounds))
111
+ return { kind: 'unavailable', reason: 'Shape bounds are unavailable' };
112
+ const before = check(captured, parsed.expiresAt, signal);
113
+ if (before)
114
+ return before;
115
+ editor.zoomToBounds(bounds, { immediate: true });
116
+ const after = check(captured, parsed.expiresAt, signal);
117
+ if (after)
118
+ return after;
119
+ if (!viewportReady())
120
+ return { kind: 'unavailable', reason: 'Canvas viewport changed during framing' };
121
+ const actual = editor.getViewportPageBounds();
122
+ if (!finiteBounds(actual) || actual.w <= 0 || actual.h <= 0 || actual.x > bounds.x + 1e-6 || actual.y > bounds.y + 1e-6
123
+ || actual.x + actual.w < bounds.x + bounds.w - 1e-6 || actual.y + actual.h < bounds.y + bounds.h - 1e-6)
124
+ return { kind: 'unavailable', reason: 'Camera constraints prevented framing' };
125
+ }
126
+ else {
127
+ const before = check(captured, parsed.expiresAt, signal);
128
+ if (before)
129
+ return before;
130
+ editor.setSelectedShapes(ids);
131
+ const after = check(captured, parsed.expiresAt, signal);
132
+ if (after)
133
+ return after;
134
+ const selected = editor.getSelectedShapeIds();
135
+ if (selected.length !== ids.length || !ids.every(id => selected.includes(id)))
136
+ return { kind: 'unavailable', reason: 'Shape selection was not applied' };
137
+ }
138
+ return { kind: 'applied', value: undefined };
139
+ };
140
+ const tools = {
141
+ getTarget,
142
+ inspect: {
143
+ name: 'inspect_mounted_canvas', input: { jsonSchema: { type: 'object', properties: { expiresAt: expirySchema }, required: ['expiresAt'] }, parse: expiry },
144
+ invoke: async (target, input, signal) => {
145
+ const captured = structuredClone(target), parsed = expiry(input), refused = check(captured, parsed.expiresAt, signal);
146
+ if (refused)
147
+ return refused;
148
+ const selection = controller.actions.selection();
149
+ const viewport = editor.getViewportPageBounds();
150
+ const value = freeze(structuredClone({ selection, dirty: controller.getSnapshot().dirty, pageId: editor.getCurrentPageId(), shapes: editor.getCurrentPageShapes(),
151
+ selectedShapeIds: editor.getSelectedShapeIds(), camera: editor.getCamera(), viewport: { x: viewport.x, y: viewport.y, w: viewport.w, h: viewport.h } }));
152
+ return check(captured, parsed.expiresAt, signal) ?? { kind: 'applied', value };
153
+ },
154
+ },
155
+ select: { name: 'select_mounted_canvas', input: { jsonSchema: shapesSchema, parse: shapesInput }, invoke: (target, input, signal) => operate(target, input, false, signal) },
156
+ frame: { name: 'frame_mounted_canvas', input: { jsonSchema: shapesSchema, parse: shapesInput }, invoke: (target, input, signal) => operate(target, input, true, signal) },
157
+ propose: {
158
+ name: 'propose_mounted_canvas_edits', input: controller.tools.propose.input,
159
+ invoke: async (target, input, signal) => {
160
+ const captured = structuredClone(target), parsed = controller.tools.propose.input.parse(input);
161
+ const refused = check(captured, parsed.expiresAt, signal);
162
+ if (refused)
163
+ return refused;
164
+ if (controller.getSnapshot().readOnly || editor.getIsReadonly())
165
+ return { kind: 'denied', reason: 'Canvas is read-only' };
166
+ const result = await controller.tools.propose.invoke(captured, parsed, signal);
167
+ if (result.kind === 'proposed') {
168
+ const lateRefusal = check(captured, parsed.expiresAt, signal)
169
+ ?? (editor.getIsReadonly() ? { kind: 'denied', reason: 'Canvas is read-only' } : null);
170
+ if (lateRefusal) {
171
+ controller.actions.reject(result.proposalId);
172
+ return lateRefusal;
173
+ }
174
+ }
175
+ return result.kind === 'proposed' ? { ...result, base: captured } : result;
176
+ },
177
+ },
178
+ };
179
+ return { tools, activate: () => { alive = true; }, dispose: () => { alive = false; } };
180
+ }
@@ -0,0 +1,11 @@
1
+ import type { Editor } from '@tldraw/editor';
2
+ import type { CanvasController, CanvasState } from './canvas.js';
3
+ import type { SaveResult } from './resources.js';
4
+ export declare function CanvasProposals({ controller, state, editor, busy, run }: {
5
+ readonly controller: CanvasController;
6
+ readonly state: CanvasState;
7
+ readonly editor: Editor | null;
8
+ readonly busy: boolean;
9
+ readonly run: (action: () => Promise<SaveResult>) => Promise<void>;
10
+ }): import("react").JSX.Element | null;
11
+ //# sourceMappingURL=canvas-proposals.d.ts.map
@@ -0,0 +1,45 @@
1
+ 'use client';
2
+ import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
3
+ import { useMemo } from 'react';
4
+ import { useValue } from '@tldraw/editor';
5
+ import { sameBase } from './text-buffer.js';
6
+ function object(value) {
7
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
8
+ }
9
+ function changedFields(before, after, path = '') {
10
+ if (JSON.stringify(before) === JSON.stringify(after))
11
+ return [];
12
+ if ((object(before) || before === undefined) && (object(after) || after === undefined)) {
13
+ const left = object(before) ? before : {}, right = object(after) ? after : {};
14
+ const keys = [...new Set([...Object.keys(left), ...Object.keys(right)])].sort();
15
+ if (keys.length)
16
+ return keys.flatMap(key => changedFields(Object.hasOwn(left, key) ? left[key] : undefined, Object.hasOwn(right, key) ? right[key] : undefined, `${path}/${key.replaceAll('~', '~0').replaceAll('/', '~1')}`));
17
+ }
18
+ return [{ field: path || 'Record', before: before === undefined ? 'Absent' : JSON.stringify(before), after: after === undefined ? 'Absent' : JSON.stringify(after) }];
19
+ }
20
+ function ProposalChanges({ proposal }) {
21
+ const changes = useMemo(() => {
22
+ const before = new Map(Object.values(proposal.before.store).map(record => [String(record.id), record]));
23
+ const after = new Map(Object.values(proposal.after.store).map(record => [String(record.id), record]));
24
+ return [...new Set([...before.keys(), ...after.keys()])].flatMap(id => {
25
+ const fields = changedFields(before.get(id), after.get(id));
26
+ return fields.length ? [{ id, kind: !before.has(id) ? 'Added' : !after.has(id) ? 'Removed' : 'Changed', fields }] : [];
27
+ });
28
+ }, [proposal]);
29
+ return _jsxs("details", { children: [_jsxs("summary", { children: ["Review ", changes.length, " changed ", changes.length === 1 ? 'record' : 'records'] }), changes.map(change => _jsxs("div", { children: [_jsxs("h4", { children: [change.kind, ": ", change.id] }), _jsxs("table", { children: [_jsx("thead", { children: _jsxs("tr", { children: [_jsx("th", { scope: "col", children: "Field" }), _jsx("th", { scope: "col", children: "Before" }), _jsx("th", { scope: "col", children: "After" })] }) }), _jsx("tbody", { children: change.fields.map(row => _jsxs("tr", { children: [_jsx("th", { scope: "row", children: row.field }), _jsx("td", { children: _jsx("pre", { style: { whiteSpace: 'pre-wrap', overflowWrap: 'anywhere' }, children: row.before }) }), _jsx("td", { children: _jsx("pre", { style: { whiteSpace: 'pre-wrap', overflowWrap: 'anywhere' }, children: row.after }) })] }, row.field)) })] })] }, change.id))] });
30
+ }
31
+ export function CanvasProposals({ controller, state, editor, busy, run }) {
32
+ const nativeReadOnly = useValue('canvas proposal readonly', () => !editor || editor.isDisposed || editor.getIsReadonly(), [editor]);
33
+ const blocked = busy || nativeReadOnly || state.readOnly || state.lifecycle !== 'active' || state.problem !== null
34
+ || state.save.kind === 'pending' || (state.save.kind === 'settled' && state.save.result.kind === 'unknown');
35
+ if (!state.proposals.length)
36
+ return null;
37
+ return _jsx("aside", { "aria-label": "Canvas proposals", children: state.proposals.map(proposal => {
38
+ const stale = state.bufferVersion !== proposal.base.target.subject.bufferVersion || !sameBase(state.base, proposal.base.target.subject.base);
39
+ return _jsxs("article", { "data-boring": "canvas-proposal", children: [_jsx("h3", { children: proposal.summary || 'Canvas change proposal' }), _jsx(ProposalChanges, { proposal: proposal }), proposal.adopted ? _jsx("p", { children: "Applied locally. Publication status is shown above." }) : stale ? _jsx("p", { children: "Canvas changed since this proposal." }) : null, !proposal.adopted && _jsx("button", { type: "button", disabled: blocked || stale, onClick: () => {
40
+ if (!editor || editor.isDisposed || editor.getIsReadonly())
41
+ return;
42
+ void run(() => controller.actions.accept(proposal.id));
43
+ }, children: "Accept and save" }), _jsx("button", { type: "button", disabled: busy || state.lifecycle !== 'active', onClick: () => controller.actions.reject(proposal.id), children: "Dismiss" })] }, proposal.id);
44
+ }) });
45
+ }
@@ -0,0 +1,3 @@
1
+ import type { TLStoreSchema, TLStoreSnapshot } from '@tldraw/tlschema';
2
+ export declare function parseCanvasRecords(value: unknown, schema: TLStoreSchema): TLStoreSnapshot;
3
+ //# sourceMappingURL=canvas-records.d.ts.map
@@ -0,0 +1,39 @@
1
+ const shapeTypes = new Set(['arrow', 'draw', 'frame', 'geo', 'group', 'highlight', 'line', 'note', 'text']);
2
+ function object(value) {
3
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
4
+ }
5
+ function allowed(record) {
6
+ if (record.typeName === 'asset')
7
+ throw new TypeError('Canvas assets require a separately qualified asset adapter');
8
+ if (record.typeName === 'shape' && !shapeTypes.has(record.type))
9
+ throw new TypeError('Unsupported canvas shape');
10
+ if (record.typeName === 'binding' && record.type !== 'arrow')
11
+ throw new TypeError('Unsupported canvas binding');
12
+ if (record.typeName === 'user' && record.imageUrl !== '')
13
+ throw new TypeError('Canvas author images require a separately qualified asset adapter');
14
+ if (!['document', 'page', 'shape', 'binding', 'user'].includes(record.typeName))
15
+ throw new TypeError('Only canvas document records may be published');
16
+ }
17
+ export function parseCanvasRecords(value, schema) {
18
+ if (!object(value) || Object.keys(value).some(key => key !== 'schema' && key !== 'store') || !object(value['schema']) || !object(value['store']))
19
+ throw new TypeError('Expected a canvas document snapshot');
20
+ const serialized = schema.serialize(), supplied = value['schema'], sequences = supplied['sequences'];
21
+ if (supplied['schemaVersion'] !== serialized.schemaVersion || !object(sequences)
22
+ || Object.keys(sequences).length !== Object.keys(serialized.sequences).length
23
+ || Object.entries(serialized.sequences).some(([key, version]) => sequences[key] !== version))
24
+ throw new TypeError('Canvas schema migration is not qualified');
25
+ const records = [];
26
+ for (const [id, item] of Object.entries(value['store'])) {
27
+ if (!object(item))
28
+ throw new TypeError('Invalid canvas record');
29
+ const type = Object.values(schema.types).find(candidate => candidate.typeName === item['typeName']);
30
+ if (!type || type.scope !== 'document')
31
+ throw new TypeError('Canvas snapshots cannot contain unknown or session records');
32
+ const record = type.validate(item);
33
+ if (record.id !== id)
34
+ throw new TypeError('Canvas record key and identity differ');
35
+ allowed(record);
36
+ records.push(record);
37
+ }
38
+ return structuredClone({ schema: serialized, store: Object.fromEntries(records.map(record => [record.id, record])) });
39
+ }
@@ -0,0 +1,92 @@
1
+ import type { TLStore, TLStoreSnapshot } from '@tldraw/editor';
2
+ import type { ResourceAccess, ResourceClient, ResourceLocator, ResourceSnapshot } from '@hachej/boring-files';
3
+ import type { CanvasEdit } from './canvas-document.js';
4
+ import type { PresentationCommand, PresentationResult } from './contracts.js';
5
+ import type { SaveResult, SaveSelection } from './resources.js';
6
+ import { type TextBufferState } from './text-buffer.js';
7
+ import type { TextDraftOptions } from './text-draft-types.js';
8
+ export { canvasMediaType } from './canvas-document.js';
9
+ export type CanvasSource = {
10
+ readonly kind: 'saved';
11
+ readonly snapshot: ResourceSnapshot;
12
+ } | {
13
+ readonly kind: 'new';
14
+ readonly target: ResourceLocator;
15
+ };
16
+ export interface CanvasOptions {
17
+ readonly drafts?: TextDraftOptions;
18
+ /** Borrow one editor's store; the host owns its lifetime and native schema. */
19
+ readonly store: TLStore;
20
+ readonly source: CanvasSource;
21
+ readonly client: ResourceClient;
22
+ readonly identity: Pick<ResourceAccess, 'scopeId' | 'principalId' | 'initiatorId'>;
23
+ readonly instanceId: string;
24
+ readonly epoch: string;
25
+ readonly readOnly?: boolean;
26
+ readonly onListenerError?: (error: unknown) => void;
27
+ }
28
+ export interface CanvasProposal {
29
+ readonly id: string;
30
+ readonly base: SaveSelection;
31
+ readonly before: TLStoreSnapshot;
32
+ readonly after: TLStoreSnapshot;
33
+ readonly edits: readonly CanvasEdit[];
34
+ readonly summary: string;
35
+ readonly adopted: boolean;
36
+ }
37
+ type CanvasSubject = SaveSelection['target']['subject'];
38
+ export interface CanvasInspection {
39
+ readonly selection: SaveSelection;
40
+ readonly document: TLStoreSnapshot;
41
+ readonly dirty: boolean;
42
+ }
43
+ export interface CanvasProposalInput {
44
+ readonly expiresAt: number;
45
+ readonly edits: readonly CanvasEdit[];
46
+ readonly summary: string;
47
+ }
48
+ export interface CanvasTools {
49
+ readonly inspect: PresentationCommand<{
50
+ readonly expiresAt: number;
51
+ }, CanvasInspection, CanvasSubject>;
52
+ readonly propose: PresentationCommand<CanvasProposalInput, void, CanvasSubject>;
53
+ }
54
+ export type CanvasState = Omit<TextBufferState, 'text'> & {
55
+ readonly document: TLStoreSnapshot;
56
+ readonly problem: string | null;
57
+ readonly proposals: readonly CanvasProposal[];
58
+ };
59
+ /** Revision-checked document publication over a borrowed native tldraw store. */
60
+ export declare function createCanvasController(options: CanvasOptions): {
61
+ store: TLStore;
62
+ getSnapshot: () => CanvasState;
63
+ subscribe: (listener: () => void) => (() => void);
64
+ actions: {
65
+ selection: () => SaveSelection;
66
+ refresh: () => Promise<import("@hachej/boring-files").ReadResult>;
67
+ discardToRemote: () => Promise<import("@hachej/boring-files").ReadResult>;
68
+ reconcile: () => Promise<SaveResult>;
69
+ abandon: () => Promise<import("@hachej/boring-files").ReadResult>;
70
+ checkDrafts: () => Promise<import("./text-draft-types.js").TextDraftDiscovery>;
71
+ checkpointDraft: () => Promise<import("./text-draft-types.js").TextDraftCheckpointResult>;
72
+ restoreDraft: (choice: import("./text-draft-types.js").TextDraftChoiceSelection) => Promise<{
73
+ readonly kind: "restored";
74
+ readonly selection: SaveSelection;
75
+ } | import("./text-draft-types.js").TextDraftChoiceFailure>;
76
+ discardDraft: (choice: import("./text-draft-types.js").TextDraftChoiceSelection) => Promise<{
77
+ readonly kind: "discarded";
78
+ } | import("./text-draft-types.js").TextDraftChoiceFailure>;
79
+ propose: (base: SaveSelection, edits: readonly CanvasEdit[], summary?: string) => PresentationResult<void, {
80
+ readonly scopeId: string;
81
+ readonly base: import("@hachej/boring-files").ResourceExpectation;
82
+ readonly bufferVersion: number;
83
+ }>;
84
+ accept: (proposalId: string) => Promise<SaveResult>;
85
+ reject: (proposalId: string) => void;
86
+ };
87
+ tools: Readonly<CanvasTools>;
88
+ flush: (selection: SaveSelection, signal?: AbortSignal) => Promise<SaveResult>;
89
+ dispose: () => void;
90
+ };
91
+ export type CanvasController = ReturnType<typeof createCanvasController>;
92
+ //# sourceMappingURL=canvas.d.ts.map