@hachej/boring-ui-kit 0.3.1 → 0.4.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.
- package/README.md +46 -18
- package/THIRD_PARTY_NOTICES.md +35 -0
- package/dist/canvas-editor-native.d.ts +4 -1
- package/dist/canvas-editor-native.js +8 -6
- package/dist/canvas-editor.js +1 -1
- package/dist/canvas-proposals.d.ts +3 -1
- package/dist/canvas-proposals.js +4 -3
- package/dist/experience-document-viewer.d.ts +3 -0
- package/dist/experience-document-viewer.js +6 -5
- package/dist/experience.d.ts +3 -0
- package/dist/experience.js +2 -1
- package/dist/file-tree-view.d.ts +76 -3
- package/dist/file-tree-view.js +172 -39
- package/dist/file-tree.d.ts +3 -0
- package/dist/file-tree.js +17 -2
- package/dist/html-viewer.d.ts +4 -0
- package/dist/html-viewer.js +5 -3
- package/dist/markdown-autosave.d.ts +13 -0
- package/dist/markdown-autosave.js +94 -0
- package/dist/markdown-editor.d.ts +16 -0
- package/dist/markdown-editor.js +81 -17
- package/dist/markdown-extensions.js +9 -1
- package/dist/markdown-proposal-diff.d.ts +2 -1
- package/dist/markdown-proposal-diff.js +3 -2
- package/dist/markdown-safety.js +3 -1
- package/dist/native-chat.js +1 -1
- package/dist/remote-chat.js +5 -1
- package/dist/text-buffer.js +3 -2
- package/dist/text-draft-controls.d.ts +3 -1
- package/dist/text-draft-controls.js +3 -2
- package/dist/viewer-components.d.ts +14 -0
- package/dist/viewer-components.js +1 -0
- package/package.json +4 -4
- package/r/app-shell.json +46 -21
- package/r/button.json +19 -11
- package/r/dock.json +2 -2
- package/r/documents.json +59 -0
- package/r/feedback-preview.json +8 -2
- package/r/feedback.json +46 -325
- package/r/file-tree.json +14 -6
- package/r/html-viewer.json +8 -42
- package/r/markdown-editor.json +38 -87
- package/r/pi-ambient.json +15 -8
- package/r/pi-app.json +30 -19
- package/r/pi-chat.json +89 -70
- package/r/pi-workspace.json +7 -5
- package/r/provider-setup.json +9 -4
- package/r/registry.json +505 -878
- package/r/task-list-viewer.json +11 -21
- package/r/theme.json +23 -4
- package/r/version-history.json +24 -0
- package/r/viewers.json +139 -320
- package/styles/file-tree.css +41 -42
- package/dist/file-tree-shadcn.d.ts +0 -8
- package/dist/file-tree-shadcn.js +0 -14
package/dist/markdown-editor.js
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
'use client';
|
|
2
|
-
import { jsx as _jsx,
|
|
2
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { TextDraftControls } from './text-draft-controls.js';
|
|
4
|
-
import {
|
|
4
|
+
import { useMarkdownAutosave } from './markdown-autosave.js';
|
|
5
|
+
import { Fragment, useDeferredValue, useEffect, useLayoutEffect, useMemo, useRef, useState, useSyncExternalStore } from 'react';
|
|
6
|
+
import { createPortal } from 'react-dom';
|
|
5
7
|
import { EditorContent, useEditor, useEditorState } from '@tiptap/react';
|
|
8
|
+
import { BubbleMenu } from '@tiptap/react/menus';
|
|
6
9
|
import { createMarkdownExtensions } from './markdown-extensions.js';
|
|
7
10
|
import { checkMarkdownRichSafety, describeUnsafe, finishRichMarkdown } from './markdown-safety.js';
|
|
8
11
|
import { MarkdownIcon } from './markdown-icons.js';
|
|
@@ -18,7 +21,55 @@ export function MarkdownEditor(props) {
|
|
|
18
21
|
setMount({ controller: props.controller, sequence: mount.sequence + 1 });
|
|
19
22
|
return _jsx(MarkdownEditorSession, { ...props, activeController: active }, mount.sequence);
|
|
20
23
|
}
|
|
21
|
-
|
|
24
|
+
const PHONE_QUERY = '(max-width: 640px)';
|
|
25
|
+
const phoneSnapshot = () => globalThis.matchMedia?.(PHONE_QUERY).matches === true;
|
|
26
|
+
const desktopSnapshot = () => false;
|
|
27
|
+
const subscribePhone = (notify) => {
|
|
28
|
+
const media = globalThis.matchMedia?.(PHONE_QUERY);
|
|
29
|
+
media?.addEventListener('change', notify);
|
|
30
|
+
return () => media?.removeEventListener('change', notify);
|
|
31
|
+
};
|
|
32
|
+
const BUBBLE_OPTIONS = { strategy: 'fixed', placement: 'top', offset: 8, shift: { padding: 12 }, flip: true };
|
|
33
|
+
const bubbleContainer = () => document.body;
|
|
34
|
+
function useEditorFocus(editor) {
|
|
35
|
+
const [focused, setFocused] = useState(false);
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
if (!editor || editor.isDestroyed)
|
|
38
|
+
return;
|
|
39
|
+
const update = () => setFocused(editor.isFocused);
|
|
40
|
+
update();
|
|
41
|
+
editor.on('focus', update);
|
|
42
|
+
editor.on('blur', update);
|
|
43
|
+
return () => { editor.off('focus', update); editor.off('blur', update); };
|
|
44
|
+
}, [editor]);
|
|
45
|
+
return focused;
|
|
46
|
+
}
|
|
47
|
+
/** Height the soft keyboard covers at the bottom of the layout viewport, so a fixed bar can sit directly above it. */
|
|
48
|
+
function useKeyboardInset(active) {
|
|
49
|
+
const [inset, setInset] = useState(0);
|
|
50
|
+
useEffect(() => {
|
|
51
|
+
const viewport = globalThis.visualViewport;
|
|
52
|
+
if (!active || !viewport) {
|
|
53
|
+
setInset(0);
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
const update = () => setInset(Math.max(0, Math.round(globalThis.innerHeight - viewport.offsetTop - viewport.height)));
|
|
57
|
+
update();
|
|
58
|
+
viewport.addEventListener('resize', update);
|
|
59
|
+
viewport.addEventListener('scroll', update);
|
|
60
|
+
return () => { viewport.removeEventListener('resize', update); viewport.removeEventListener('scroll', update); };
|
|
61
|
+
}, [active]);
|
|
62
|
+
return inset;
|
|
63
|
+
}
|
|
64
|
+
function MarkdownEditorSession({ components, controller, title = 'Document', initialMode = 'rich', placeholder = 'Start writing', className, onMountedTools, header = true, showOutline = header, mode: controlledMode, onModeChange, onRichSafety, resolveImage, formattingToolbar = 'pinned', autosave = true, onAutosaveChange, onSaveResult, activeController: active }) {
|
|
65
|
+
const Control = components?.button ?? 'button';
|
|
66
|
+
const Bubble = components?.bubble ?? Fragment;
|
|
67
|
+
const Source = components?.textarea ?? 'textarea';
|
|
68
|
+
const LinkInput = components?.input ?? 'input';
|
|
69
|
+
const phone = useSyncExternalStore(subscribePhone, phoneSnapshot, desktopSnapshot);
|
|
70
|
+
const [pinned, setPinned] = useState(formattingToolbar === 'pinned');
|
|
71
|
+
useEffect(() => { setPinned(formattingToolbar === 'pinned'); }, [formattingToolbar]);
|
|
72
|
+
const showPinned = pinned && !phone;
|
|
22
73
|
const state = useSyncExternalStore(controller.subscribe, controller.getSnapshot, controller.getSnapshot);
|
|
23
74
|
const projected = useRef(null);
|
|
24
75
|
const emitted = useRef(null);
|
|
@@ -160,20 +211,26 @@ function MarkdownEditorSession({ controller, title = 'Document', initialMode = '
|
|
|
160
211
|
const outcome = state.save.kind === 'settled' ? state.save.result : null;
|
|
161
212
|
const uncertain = outcome?.kind === 'unknown';
|
|
162
213
|
const conflict = outcome?.kind === 'conflict' || state.remote !== null;
|
|
163
|
-
const save = () => {
|
|
164
|
-
const selected = controller.actions.selection();
|
|
165
|
-
setError(null);
|
|
166
|
-
void controller.flush(selected).then(report, () => { if (active.current === controller)
|
|
167
|
-
setError('The save could not be completed. Your text is still available.'); });
|
|
168
|
-
};
|
|
169
214
|
/** Shows a refused save or accept; the buffer keeps the text. */
|
|
170
215
|
const report = (result) => {
|
|
171
216
|
if (active.current !== controller || !result || typeof result !== 'object')
|
|
172
217
|
return;
|
|
173
218
|
const { kind, reason } = result;
|
|
219
|
+
if (kind === 'saved')
|
|
220
|
+
setError(null);
|
|
174
221
|
if (kind === 'denied' || kind === 'conflict' || kind === 'unavailable' || kind === 'file_too_large')
|
|
175
222
|
setError(typeof reason === 'string' && reason ? reason : kind === 'conflict' ? 'The document changed elsewhere. Your text is still available.' : 'The document could not be saved. Your text is still available.');
|
|
176
223
|
};
|
|
224
|
+
const saving = useMarkdownAutosave(controller, autosave, result => { report(result); onSaveResult?.(result); });
|
|
225
|
+
useEffect(() => { onAutosaveChange?.(saving.enabled ? saving.immediate : null); return () => onAutosaveChange?.(null); }, [onAutosaveChange, saving.enabled, saving.immediate]);
|
|
226
|
+
const save = () => {
|
|
227
|
+
setError(null);
|
|
228
|
+
if (saving.enabled)
|
|
229
|
+
void saving.manual();
|
|
230
|
+
else
|
|
231
|
+
void controller.flush(controller.actions.selection()).then(report, () => { if (active.current === controller)
|
|
232
|
+
setError('The save could not be completed. Your text is still available.'); });
|
|
233
|
+
};
|
|
177
234
|
const run = (operation) => {
|
|
178
235
|
setError(null);
|
|
179
236
|
void operation().then(report, () => { if (active.current === controller)
|
|
@@ -181,14 +238,19 @@ function MarkdownEditorSession({ controller, title = 'Document', initialMode = '
|
|
|
181
238
|
};
|
|
182
239
|
const richOnly = unavailable || mode === 'source' || !editor;
|
|
183
240
|
const [linkDraft, setLinkDraft] = useState(null);
|
|
241
|
+
// On a phone the full formatting row docks above the soft keyboard while the rich surface has focus.
|
|
242
|
+
const editing = useEditorFocus(editor);
|
|
243
|
+
const dockVisible = phone && !unavailable && !!editor && (editing || linkDraft !== null);
|
|
244
|
+
const keyboardInset = useKeyboardInset(dockVisible);
|
|
184
245
|
const command = (operation) => { interaction.current = true; try {
|
|
185
246
|
operation();
|
|
186
247
|
}
|
|
187
248
|
finally {
|
|
188
249
|
interaction.current = false;
|
|
189
250
|
} };
|
|
190
|
-
const tool = (label, icon, pressed, operation, extra = {}) => (_jsx(
|
|
191
|
-
const
|
|
251
|
+
const tool = (label, icon, pressed, operation, extra = {}) => (_jsx(Control, { type: "button", className: "boring-markdown-tool", "aria-label": label, title: label, "aria-pressed": pressed === true, "data-active": pressed === true || undefined, disabled: richOnly || extra.disabled, onClick: () => command(operation), children: _jsx(MarkdownIcon, { name: icon }) }));
|
|
252
|
+
const Rule = components?.separator;
|
|
253
|
+
const separator = Rule ? _jsx(Rule, { orientation: "vertical", className: "boring-markdown-separator" }) : _jsx("span", { role: "separator", "aria-orientation": "vertical", className: "boring-markdown-separator" });
|
|
192
254
|
const chain = () => editor.chain().focus();
|
|
193
255
|
const openLink = () => setLinkDraft(marks?.href ?? '');
|
|
194
256
|
const applyLink = () => {
|
|
@@ -198,17 +260,19 @@ function MarkdownEditorSession({ controller, title = 'Document', initialMode = '
|
|
|
198
260
|
command(() => href === '' ? chain().extendMarkRange('link').unsetLink().run() : chain().extendMarkRange('link').setLink({ href }).run());
|
|
199
261
|
setLinkDraft(null);
|
|
200
262
|
};
|
|
201
|
-
|
|
263
|
+
const formatTools = _jsxs(_Fragment, { children: [tool('Bold', 'Bold', marks?.bold, () => chain().toggleBold().run()), tool('Italic', 'Italic', marks?.italic, () => chain().toggleItalic().run()), tool('Strikethrough', 'Strike', marks?.strike, () => chain().toggleStrike().run()), tool('Highlight', 'Highlight', marks?.highlight, () => chain().toggleHighlight().run()), tool('Code', 'Code', marks?.code, () => chain().toggleCode().run()), separator, tool('Heading 1', 'Heading1', marks?.h1, () => chain().toggleHeading({ level: 1 }).run()), tool('Heading 2', 'Heading2', marks?.h2, () => chain().toggleHeading({ level: 2 }).run()), tool('Heading 3', 'Heading3', marks?.h3, () => chain().toggleHeading({ level: 3 }).run()), separator, tool('Bullet list', 'BulletList', marks?.bullet, () => chain().toggleBulletList().run()), tool('Numbered list', 'OrderedList', marks?.ordered, () => chain().toggleOrderedList().run()), tool('Task list', 'TaskList', marks?.task, () => chain().toggleTaskList().run()), separator, tool('Quote', 'Quote', marks?.quote, () => chain().toggleBlockquote().run()), tool('Code block', 'CodeBlock', marks?.codeBlock, () => chain().toggleCodeBlock().run()), tool('Link', 'Link', marks?.link || linkDraft !== null, openLink), tool('Table', 'Table', marks?.table, () => marks?.table ? chain().deleteTable().run() : chain().insertTable({ rows: 3, cols: 3, withHeaderRow: true }).run()), tool('Horizontal rule', 'Rule', false, () => chain().setHorizontalRule().run()), marks?.table && _jsxs(_Fragment, { children: [separator, tool('Add table row', 'Rows', false, () => chain().addRowAfter().run()), tool('Add table column', 'Columns', false, () => chain().addColumnAfter().run()), tool('Delete table', 'Trash', false, () => chain().deleteTable().run())] })] });
|
|
264
|
+
return _jsxs("section", { className: className, "data-boring": "markdown-editor", "data-mode": mode, "data-dock": (mode === 'rich' && dockVisible) || undefined, "data-dirty": state.dirty || undefined, onBlurCapture: event => { if (saving.enabled && !event.currentTarget.contains(event.relatedTarget))
|
|
265
|
+
void saving.immediate(); }, onKeyDownCapture: event => {
|
|
202
266
|
if ((event.ctrlKey || event.metaKey) && event.key.toLowerCase() === 's') {
|
|
203
267
|
event.preventDefault();
|
|
204
|
-
if (!unavailable && !
|
|
268
|
+
if (!unavailable && !uncertain)
|
|
205
269
|
save();
|
|
206
270
|
}
|
|
207
|
-
}, children: [header && _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", "aria-pressed": mode === 'source', disabled: !richOffered, onClick: switchMode, children: "Markdown source" }), _jsx(
|
|
271
|
+
}, children: [header && _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' }), mode === 'rich' && !phone && !unavailable && _jsx(Control, { type: "button", className: "boring-markdown-pin", "aria-label": "Pin formatting toolbar", title: "Pin formatting toolbar", "aria-pressed": pinned, onClick: () => setPinned(value => !value), children: "Aa" }), _jsx(Control, { type: "button", "aria-pressed": mode === 'source', disabled: !richOffered, onClick: switchMode, children: "Markdown source" }), !saving.enabled && _jsx(Control, { type: "button", disabled: unavailable || !state.dirty || pending || uncertain, onClick: save, children: "Save" })] }), _jsx(TextDraftControls, { components: components, recovery: state.recovery, actions: controller.actions, readOnly: state.readOnly, blocked: state.lifecycle !== 'active' || state.save.kind === 'pending' || (state.save.kind === 'settled' && state.save.result.kind === 'unknown') }), mode === 'rich' && showPinned && _jsx("div", { role: "toolbar", "aria-label": "Text formatting", className: "boring-markdown-toolbar", children: formatTools }), mode === 'rich' && dockVisible && createPortal(_jsxs("div", { role: "toolbar", "aria-label": "Text formatting", className: "boring-viewer boring-markdown-dock", "data-testid": "markdown-dock", "data-keyboard": keyboardInset > 0 || undefined, style: { bottom: keyboardInset }, onPointerDown: event => event.preventDefault(), children: [Rule && _jsx(Rule, { className: "boring-markdown-dock-rule" }), formatTools] }), document.body), mode === 'rich' && editor && !showPinned && !unavailable && _jsx(BubbleMenu, { editor: editor, updateDelay: 0, options: BUBBLE_OPTIONS, appendTo: bubbleContainer, role: "toolbar", "aria-label": "Text formatting", className: "boring-viewer boring-markdown-bubble", children: _jsxs(Bubble, { children: [tool('Bold', 'Bold', marks?.bold, () => chain().toggleBold().run()), tool('Italic', 'Italic', marks?.italic, () => chain().toggleItalic().run()), tool('Highlight', 'Highlight', marks?.highlight, () => chain().toggleHighlight().run()), tool('Code', 'Code', marks?.code, () => chain().toggleCode().run()), tool('Link', 'Link', marks?.link || linkDraft !== null, openLink)] }) }), mode === 'rich' && linkDraft !== null && _jsxs("form", { className: "boring-markdown-link", "aria-label": "Link address", onSubmit: event => { event.preventDefault(); applyLink(); }, children: [_jsx(LinkInput, { type: "text", inputMode: "url", autoFocus: true, "aria-label": "Link address", placeholder: "https://", value: linkDraft, onChange: event => setLinkDraft(event.currentTarget.value), onKeyDown: event => { if (event.key === 'Escape') {
|
|
208
272
|
event.preventDefault();
|
|
209
273
|
setLinkDraft(null);
|
|
210
274
|
editor?.commands.focus();
|
|
211
|
-
} } }), _jsx(
|
|
275
|
+
} } }), _jsx(Control, { type: "submit", children: linkDraft.trim() === '' && marks?.link ? 'Remove link' : 'Apply link' }), _jsx(Control, { type: "button", onClick: () => { setLinkDraft(null); editor?.commands.focus(); }, children: "Cancel" })] }), !richOffered && _jsx("p", { role: "note", className: "boring-markdown-notice", "data-testid": "markdown-source-only", children: describeUnsafe(safety) }), showOutline && mode === 'rich' && outline && outline.headings.length > 0 && _jsx("nav", { "aria-label": "Document outline", children: _jsx("ol", { children: outline.headings.map((heading, index) => _jsx("li", { "data-heading-level": heading.level, children: _jsx(Control, { type: "button", "aria-label": `Go to heading: ${heading.text || 'Untitled heading'}`, disabled: state.lifecycle !== 'active', onClick: () => {
|
|
212
276
|
if (!editor || editor.isDestroyed || active.current !== controller || controller.getSnapshot().lifecycle !== 'active'
|
|
213
277
|
|| controller.getSnapshot().bufferVersion !== state.bufferVersion || editor.state.doc !== outline.document)
|
|
214
278
|
return;
|
|
@@ -222,8 +286,8 @@ function MarkdownEditorSession({ controller, title = 'Document', initialMode = '
|
|
|
222
286
|
setError(result.reason);
|
|
223
287
|
}).catch(() => { if (active.current === controller)
|
|
224
288
|
setError('The heading could not be selected'); });
|
|
225
|
-
}, children: heading.text || 'Untitled heading' }) }, heading.position)) }) }), uncertain && _jsxs("div", { role: "alert", className: "boring-markdown-alert", children: ["Save acknowledgement was lost. Keep editing while its outcome is checked.", _jsx(
|
|
226
|
-
? _jsx(
|
|
289
|
+
}, children: heading.text || 'Untitled heading' }) }, heading.position)) }) }), uncertain && _jsxs("div", { role: "alert", className: "boring-markdown-alert", children: ["Save acknowledgement was lost. Keep editing while its outcome is checked.", _jsx(Control, { type: "button", onClick: () => run(controller.actions.reconcile), children: "Check save outcome" }), _jsx(Control, { type: "button", onClick: () => run(controller.actions.abandon), children: "Stop checking and refresh, keeping my draft" })] }), conflict && _jsxs("div", { role: "alert", className: "boring-markdown-alert", children: ["The saved document changed. Your local text has been kept.", _jsx(Control, { type: "button", disabled: pending || uncertain, onClick: () => run(controller.actions.refresh), children: "Check saved version" }), _jsx(Control, { type: "button", disabled: pending || uncertain, onClick: () => run(controller.actions.discardToRemote), children: "Discard local changes and reload" })] }), (outcome?.kind === 'denied' || outcome?.kind === 'unavailable' || outcome?.kind === 'file_too_large') && _jsx("p", { role: "alert", className: "boring-markdown-alert", children: outcome.reason }), error && _jsx("p", { role: "alert", className: "boring-markdown-alert", children: error }), state.proposals.map(proposal => _jsxs("article", { "data-boring": "proposal", className: "boring-markdown-proposal", children: [_jsx("h3", { children: proposal.summary || 'Proposed edit' }), _jsxs("details", { children: [_jsx("summary", { children: "Review text changes" }), _jsx(MarkdownProposalDiff, { components: components, proposal: proposal }), _jsxs("details", { children: [_jsx("summary", { children: "Exact before and after" }), _jsx("h4", { children: "Before" }), _jsx("pre", { children: proposal.before }), _jsx("h4", { children: "After" }), _jsx("pre", { children: proposal.after })] })] }), _jsx(Control, { type: "button", disabled: unavailable || pending || uncertain || proposal.adopted, onClick: () => run(() => controller.actions.accept(proposal.id)), children: proposal.adopted ? 'Adopted' : 'Accept and save' }), _jsx(Control, { type: "button", onClick: () => controller.actions.reject(proposal.id), children: "Dismiss proposal" })] }, proposal.id)), mode === 'source'
|
|
290
|
+
? _jsx(Source, { ref: sourceInput, className: "boring-markdown-source", "aria-label": `${title} Markdown source`, value: state.text, readOnly: unavailable, spellCheck: false, onChange: event => controller.actions.edit(event.currentTarget.value) })
|
|
227
291
|
: _jsx("div", { className: "boring-markdown-surface", onKeyDownCapture: markInteraction, onBeforeInputCapture: markInteraction, onInputCapture: markInteraction, onPasteCapture: markInteraction, onDropCapture: markInteraction, onChangeCapture: markInteraction, onClickCapture: event => { markInteraction(); if (event.target instanceof Element && event.target.closest('a'))
|
|
228
292
|
event.preventDefault(); }, onAuxClickCapture: event => { if (event.target instanceof Element && event.target.closest('a'))
|
|
229
293
|
event.preventDefault(); }, children: _jsx(EditorContent, { editor: editor }) })] });
|
|
@@ -5,6 +5,7 @@ import { TaskItem, TaskList } from '@tiptap/extension-list';
|
|
|
5
5
|
import { Table, TableCell, TableHeader, TableRow } from '@tiptap/extension-table';
|
|
6
6
|
import Image from '@tiptap/extension-image';
|
|
7
7
|
import Highlight from '@tiptap/extension-highlight';
|
|
8
|
+
import { Node as TiptapNode } from '@tiptap/core';
|
|
8
9
|
import { createMarkdownParser } from './markdown-parser.js';
|
|
9
10
|
/**
|
|
10
11
|
* The serialiser HTML-encodes text (`&` becomes `&`), which rewrites ordinary prose such as "Notes & Highlights". The parser
|
|
@@ -24,6 +25,13 @@ const PlainTextMarkdown = Markdown.extend({
|
|
|
24
25
|
};
|
|
25
26
|
},
|
|
26
27
|
});
|
|
28
|
+
const RichStarterKit = StarterKit.extend({
|
|
29
|
+
addExtensions() {
|
|
30
|
+
return (this.parent?.() ?? []).map(extension => extension instanceof TiptapNode && extension.name === 'paragraph'
|
|
31
|
+
? extension.extend({ renderMarkdown: (node, helpers) => helpers.renderChildren(node.content ?? []) })
|
|
32
|
+
: extension);
|
|
33
|
+
},
|
|
34
|
+
});
|
|
27
35
|
/**
|
|
28
36
|
* A GFM table keeps the exact text it was written in (unpadded `|a|b|`, `:---:` alignment rows, `_emphasis_`, escaped pipes, empty
|
|
29
37
|
* cells) while its content is unchanged: parsing records the source and the canonical JSON of the rows it produced, and rendering
|
|
@@ -85,7 +93,7 @@ export function createMarkdownExtensions({ placeholder, images } = {}) {
|
|
|
85
93
|
onBeforeCreate() { this.parent?.(); schema = this.editor.schema; },
|
|
86
94
|
});
|
|
87
95
|
return [
|
|
88
|
-
|
|
96
|
+
RichStarterKit.configure({ link: { openOnClick: false, autolink: false, linkOnPaste: false }, underline: false }),
|
|
89
97
|
TaskList, TaskItem.configure({ nested: true }),
|
|
90
98
|
SourceTable.configure({ resizable: false }), TableRow, TableHeader, TableCell,
|
|
91
99
|
InertImage.configure({ inline: false, allowBase64: true }),
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { MarkdownProposal } from './markdown.js';
|
|
2
|
-
export declare function MarkdownProposalDiff({ proposal }: {
|
|
2
|
+
export declare function MarkdownProposalDiff({ components, proposal }: {
|
|
3
|
+
readonly components?: import('./viewer-components.js').ViewerComponents | undefined;
|
|
3
4
|
readonly proposal: MarkdownProposal;
|
|
4
5
|
}): import("react").JSX.Element;
|
|
5
6
|
//# sourceMappingURL=markdown-proposal-diff.d.ts.map
|
|
@@ -107,11 +107,12 @@ function DiffRow({ line }) {
|
|
|
107
107
|
const display = terminated ? line.value.slice(0, -1) : line.value;
|
|
108
108
|
return _jsxs("div", { "data-diff": line.kind, "data-line-ending": terminated ? 'lf' : 'none', children: [_jsx("span", { children: line.kind === 'add' ? '+ ' : line.kind === 'remove' ? '- ' : ' ' }), _jsx("span", { "data-diff-text": "", children: display }), line.kind !== 'same' && line.value.endsWith('\r\n') && _jsx("span", { "data-diff-crlf": "", children: " CRLF line ending" }), !terminated && _jsx("span", { "data-diff-no-newline": "", children: " No newline at end" })] });
|
|
109
109
|
}
|
|
110
|
-
export function MarkdownProposalDiff({ proposal }) {
|
|
110
|
+
export function MarkdownProposalDiff({ components, proposal }) {
|
|
111
|
+
const Control = components?.button ?? 'button';
|
|
111
112
|
const diff = useMemo(() => review(proposal.before, proposal.after), [proposal.before, proposal.after]);
|
|
112
113
|
const shown = useMemo(() => entries(diff.lines), [diff]);
|
|
113
114
|
const [expanded, setExpanded] = useState(() => new Set());
|
|
114
|
-
return _jsxs("div", { "data-boring": "proposal-diff", role: "group", "aria-label": "Proposed line changes", children: [diff.fallback && _jsx("p", { role: "status", "data-diff-fallback": "", children: "Detailed line alignment unavailable; showing changed block." }), proposal.before === proposal.after && _jsx("p", { children: "No text changes" }), _jsx("div", { style: { overflowX: 'auto', whiteSpace: 'pre-wrap' }, children: shown.map((entry, index) => entry.kind === 'fold' ? _jsxs("div", { children: [_jsxs(
|
|
115
|
+
return _jsxs("div", { "data-boring": "proposal-diff", role: "group", "aria-label": "Proposed line changes", children: [diff.fallback && _jsx("p", { role: "status", "data-diff-fallback": "", children: "Detailed line alignment unavailable; showing changed block." }), proposal.before === proposal.after && _jsx("p", { children: "No text changes" }), _jsx("div", { style: { overflowX: 'auto', whiteSpace: 'pre-wrap' }, children: shown.map((entry, index) => entry.kind === 'fold' ? _jsxs("div", { children: [_jsxs(Control, { type: "button", "data-diff-fold": "", "aria-expanded": expanded.has(entry.index), onClick: () => setExpanded(current => {
|
|
115
116
|
const next = new Set(current);
|
|
116
117
|
if (next.has(entry.index))
|
|
117
118
|
next.delete(entry.index);
|
package/dist/markdown-safety.js
CHANGED
|
@@ -7,10 +7,12 @@ export function normalizeMarkdownWhitespace(text) {
|
|
|
7
7
|
const lines = text.split('\n');
|
|
8
8
|
const out = [];
|
|
9
9
|
let fence;
|
|
10
|
-
for (
|
|
10
|
+
for (let line of lines) {
|
|
11
11
|
const opened = FENCE.exec(line);
|
|
12
12
|
if (opened && (!fence || opened[1] === fence))
|
|
13
13
|
fence = fence ? undefined : opened[1];
|
|
14
|
+
if (!fence && !opened && !/^ {4}|^\t/.test(line) && /^(?:[ \t]* [ \t]*)+$/.test(line))
|
|
15
|
+
line = '';
|
|
14
16
|
if (!fence && !opened && line.trim() === '' && out.length > 0 && out[out.length - 1].trim() === '')
|
|
15
17
|
continue;
|
|
16
18
|
out.push(line);
|
package/dist/native-chat.js
CHANGED
|
@@ -202,7 +202,7 @@ export function createNativeChatController(options) {
|
|
|
202
202
|
}
|
|
203
203
|
const cursor = page.next === undefined ? undefined : structuredClone(page.next);
|
|
204
204
|
historyScan = { maxEntryId: scan.maxEntryId, ...(cursor === undefined ? {} : { cursor }), ...(previousId === undefined ? {} : { lastId: previousId }) };
|
|
205
|
-
publish({ history: { kind: 'ready', entries: Object.freeze([...page.items].reverse()), hasMore: cursor !== undefined } });
|
|
205
|
+
publish({ history: { kind: 'ready', entries: Object.freeze([...page.items].reverse().concat(entries)), hasMore: cursor !== undefined } });
|
|
206
206
|
}
|
|
207
207
|
catch (error) {
|
|
208
208
|
if (current())
|
package/dist/remote-chat.js
CHANGED
|
@@ -316,7 +316,11 @@ export async function createRemoteChat(options) {
|
|
|
316
316
|
// The controller only reconciles a submission by request ID; no other transaction work crosses the wire.
|
|
317
317
|
commit: async (change, _context) => change({ submissionByRequest: (_conversationId, requestId) => lookup(requestId) }),
|
|
318
318
|
abort: async () => { await post('abort', {}); },
|
|
319
|
-
entries: async (
|
|
319
|
+
entries: async (query, limit, cursor) => (await call('entries', {}, { limit: String(limit),
|
|
320
|
+
...(query.maxEntryId === undefined ? {} : { maxEntryId: String(query.maxEntryId) }),
|
|
321
|
+
...(query.minEntryId === undefined ? {} : { minEntryId: String(query.minEntryId) }),
|
|
322
|
+
...(query.order === undefined ? {} : { order: query.order }),
|
|
323
|
+
...(cursor === undefined ? {} : { cursor: JSON.stringify(cursor) }) })).page,
|
|
320
324
|
};
|
|
321
325
|
const context = Object.freeze({});
|
|
322
326
|
const close = async () => { const first = unclaimed; unclaimed = undefined; await first?.stop(); };
|
package/dist/text-buffer.js
CHANGED
|
@@ -233,8 +233,9 @@ export function createTextBuffer(options) {
|
|
|
233
233
|
return read;
|
|
234
234
|
}
|
|
235
235
|
if (sameBase(remote, state.base) && !discard) {
|
|
236
|
-
|
|
237
|
-
|
|
236
|
+
const resolved = state.save.kind === 'settled' && state.save.result.kind !== 'saved';
|
|
237
|
+
if (state.remote !== null || resolved)
|
|
238
|
+
update({ ...state, remote: null, save: resolved ? { kind: 'idle' } : state.save });
|
|
238
239
|
return read;
|
|
239
240
|
}
|
|
240
241
|
if (state.dirty && (!discard || state.bufferVersion !== previousVersion)) {
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import type { TextDraftActions, TextDraftRecoveryState } from './text-draft-types.js';
|
|
2
|
+
import type { ViewerComponents } from './viewer-components.js';
|
|
2
3
|
export interface TextDraftControlsProps {
|
|
4
|
+
readonly components?: ViewerComponents | undefined;
|
|
3
5
|
readonly recovery: TextDraftRecoveryState;
|
|
4
6
|
readonly actions: TextDraftActions;
|
|
5
7
|
readonly readOnly?: boolean;
|
|
6
8
|
readonly blocked?: boolean;
|
|
7
9
|
}
|
|
8
|
-
export declare function TextDraftControls({ recovery, actions, readOnly, blocked }: TextDraftControlsProps): import("react").JSX.Element | null;
|
|
10
|
+
export declare function TextDraftControls({ components, recovery, actions, readOnly, blocked }: TextDraftControlsProps): import("react").JSX.Element | null;
|
|
9
11
|
//# sourceMappingURL=text-draft-controls.d.ts.map
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
3
|
import { useLayoutEffect, useRef, useState } from 'react';
|
|
4
|
-
export function TextDraftControls({ recovery, actions, readOnly = false, blocked = false }) {
|
|
4
|
+
export function TextDraftControls({ components, recovery, actions, readOnly = false, blocked = false }) {
|
|
5
|
+
const Control = components?.button ?? 'button';
|
|
5
6
|
const owner = useRef({ actions, busy: false });
|
|
6
7
|
const [pending, setPending] = useState(false);
|
|
7
8
|
const [notice, setNotice] = useState();
|
|
@@ -29,5 +30,5 @@ export function TextDraftControls({ recovery, actions, readOnly = false, blocked
|
|
|
29
30
|
const discovery = active ? recovery.discovery : undefined;
|
|
30
31
|
const checkpoint = active ? recovery.checkpoint : undefined;
|
|
31
32
|
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(
|
|
33
|
+
return _jsxs("section", { "data-boring": "text-draft-controls", "aria-label": "Draft recovery", children: [_jsx("p", { role: "status", children: status }), _jsx(Control, { type: "button", disabled: !active || pending || blocked || discovery?.kind === 'checking', onClick: () => run(actions.checkDrafts), children: "Check stored drafts" }), _jsx(Control, { 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(Control, { type: "button", disabled: pending || blocked || readOnly || choice.compatibility !== 'exact', onClick: () => run(() => actions.restoreDraft(choice.selection)), children: "Restore draft" }), _jsx(Control, { 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
34
|
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { ComponentPropsWithRef, ComponentType, PropsWithChildren } from 'react';
|
|
2
|
+
/** Host presentation for the document renderers. Bindings must forward refs and native input events. */
|
|
3
|
+
export interface ViewerComponents {
|
|
4
|
+
readonly button: ComponentType<ComponentPropsWithRef<'button'>>;
|
|
5
|
+
readonly input: ComponentType<ComponentPropsWithRef<'input'>>;
|
|
6
|
+
readonly textarea: ComponentType<ComponentPropsWithRef<'textarea'>>;
|
|
7
|
+
readonly bubble?: ComponentType<PropsWithChildren>;
|
|
8
|
+
/** The host's separator: vertical between toolbar groups, horizontal above the phone formatting bar. */
|
|
9
|
+
readonly separator?: ComponentType<{
|
|
10
|
+
readonly orientation?: 'horizontal' | 'vertical';
|
|
11
|
+
readonly className?: string;
|
|
12
|
+
}>;
|
|
13
|
+
}
|
|
14
|
+
//# sourceMappingURL=viewer-components.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hachej/boring-ui-kit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"private": false,
|
|
6
6
|
"type": "module",
|
|
@@ -110,7 +110,7 @@
|
|
|
110
110
|
"./file-tree.css": "./styles/file-tree.css"
|
|
111
111
|
},
|
|
112
112
|
"peerDependencies": {
|
|
113
|
-
"@hachej/boring-files": "^0.
|
|
113
|
+
"@hachej/boring-files": "^0.4.0",
|
|
114
114
|
"@earendil-works/pi-durable": "~1.2.0",
|
|
115
115
|
"react": "^19.0.0",
|
|
116
116
|
"react-dom": "^19.0.0",
|
|
@@ -137,7 +137,7 @@
|
|
|
137
137
|
"zod": "^4.6.5",
|
|
138
138
|
"@tldraw/tlschema": "^5.5.2",
|
|
139
139
|
"lucide-react": "^1.51.0",
|
|
140
|
-
"
|
|
140
|
+
"@base-ui/react": "1.9.0"
|
|
141
141
|
},
|
|
142
142
|
"peerDependenciesMeta": {
|
|
143
143
|
"@hachej/boring-files": {
|
|
@@ -221,7 +221,7 @@
|
|
|
221
221
|
"lucide-react": {
|
|
222
222
|
"optional": true
|
|
223
223
|
},
|
|
224
|
-
"
|
|
224
|
+
"@base-ui/react": {
|
|
225
225
|
"optional": true
|
|
226
226
|
}
|
|
227
227
|
},
|