@hachej/boring-ui-kit 0.4.26 → 0.4.28

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 (80) hide show
  1. package/README.md +15 -2
  2. package/bin/boring-ui-blocks.mjs +1 -1
  3. package/dist/chat-dictation.d.ts +27 -0
  4. package/dist/chat-dictation.js +71 -0
  5. package/dist/chat-react-session.d.ts +170 -0
  6. package/dist/chat-react-session.js +367 -0
  7. package/dist/chat-react.d.ts +2 -0
  8. package/dist/chat-react.js +1 -0
  9. package/dist/chat-text.d.ts +9 -0
  10. package/dist/chat-text.js +4 -0
  11. package/dist/chat-voice-message.d.ts +65 -0
  12. package/dist/chat-voice-message.js +235 -0
  13. package/dist/chat-voice.d.ts +7 -0
  14. package/dist/chat-voice.js +4 -0
  15. package/dist/experience-catalog.d.ts +2 -2
  16. package/dist/experience-region-tree.d.ts +1 -1
  17. package/dist/markdown-editor-labels.d.ts +83 -0
  18. package/dist/markdown-editor-labels.js +75 -0
  19. package/dist/markdown-editor.d.ts +7 -1
  20. package/dist/markdown-editor.js +28 -18
  21. package/dist/markdown-extensions.d.ts +4 -2
  22. package/dist/markdown-extensions.js +2 -2
  23. package/dist/markdown-preserved.d.ts +5 -1
  24. package/dist/markdown-preserved.js +15 -4
  25. package/dist/markdown-proposal-diff.d.ts +3 -1
  26. package/dist/markdown-proposal-diff.js +6 -5
  27. package/dist/text-draft-controls.d.ts +5 -1
  28. package/dist/text-draft-controls.js +6 -5
  29. package/dist/text-draft-labels.d.ts +26 -0
  30. package/dist/text-draft-labels.js +47 -0
  31. package/dist/voice-attempt.d.ts +33 -0
  32. package/dist/voice-attempt.js +122 -0
  33. package/package.json +9 -5
  34. package/r/agent-target.json +5 -5
  35. package/r/app-shell.json +14 -14
  36. package/r/board-viewer.json +7 -6
  37. package/r/button.json +1 -1
  38. package/r/dock.json +2 -2
  39. package/r/documents.json +12 -12
  40. package/r/feedback-preview.json +2 -2
  41. package/r/feedback.json +7 -7
  42. package/r/file-tree.json +4 -4
  43. package/r/highlights.json +3 -3
  44. package/r/html-viewer.json +3 -3
  45. package/r/looks-viewer.json +3 -2
  46. package/r/maquette-viewer.json +1 -1
  47. package/r/markdown-editor.json +3 -3
  48. package/r/mockup-viewer.json +1 -1
  49. package/r/pi-ambient.json +11 -11
  50. package/r/pi-app-kinds.json +10 -10
  51. package/r/pi-app.json +13 -13
  52. package/r/pi-artifacts.json +4 -4
  53. package/r/pi-chat-approval.json +3 -3
  54. package/r/pi-chat-attachments.json +1 -1
  55. package/r/pi-chat-markdown.json +2 -2
  56. package/r/pi-chat-mentions.json +1 -1
  57. package/r/pi-chat-pickers.json +1 -1
  58. package/r/pi-chat-plugins.json +11 -11
  59. package/r/pi-chat-question.json +2 -2
  60. package/r/pi-chat-slash.json +2 -2
  61. package/r/pi-chat-subagent.json +2 -2
  62. package/r/pi-chat-voice.json +5 -5
  63. package/r/pi-chat.json +22 -22
  64. package/r/pi-workspace.json +4 -4
  65. package/r/placed-agent.json +4 -4
  66. package/r/provider-setup.json +1 -1
  67. package/r/registry.json +193 -181
  68. package/r/requests-board.json +1 -1
  69. package/r/shared-chat.json +9 -9
  70. package/r/task-list-viewer.json +2 -2
  71. package/r/utils.json +14 -2
  72. package/r/version-history.json +2 -2
  73. package/r/viewer-code.json +2 -2
  74. package/r/viewer-frame.json +7 -7
  75. package/r/viewer-html.json +4 -4
  76. package/r/viewer-image.json +2 -2
  77. package/r/viewer-markdown.json +5 -5
  78. package/r/viewer-pdf.json +4 -4
  79. package/r/viewers.json +6 -6
  80. package/r/windows.json +3 -3
@@ -10,12 +10,15 @@ import { createPortal } from 'react-dom';
10
10
  import { EditorContent, useEditor, useEditorState } from '@tiptap/react';
11
11
  import { BubbleMenu } from '@tiptap/react/menus';
12
12
  import { createMarkdownExtensions } from './markdown-extensions.js';
13
- import { checkMarkdownRichSafety, describeUnsafe, finishRichMarkdown } from './markdown-safety.js';
13
+ import { checkMarkdownRichSafety, finishRichMarkdown } from './markdown-safety.js';
14
14
  import { captureMarkdownSource, serializeMarkdownEdit } from './markdown-source.js';
15
15
  import { MarkdownIcon } from './markdown-icons.js';
16
16
  import { MarkdownProposalDiff } from './markdown-proposal-diff.js';
17
+ import { updatePreservedMarkdownLabels } from './markdown-preserved.js';
18
+ import { resolveMarkdownEditorLabels } from './markdown-editor-labels.js';
17
19
  import { createMountedMarkdownTools } from './markdown-mounted.js';
18
20
  export { checkMarkdownRichSafety } from './markdown-safety.js';
21
+ export { markdownEditorLabels } from './markdown-editor-labels.js';
19
22
  /** Borrows the controller. The host owns document selection, access and disposal. */
20
23
  export function MarkdownEditor(props) {
21
24
  const active = useRef(props.controller);
@@ -65,10 +68,6 @@ function useKeyboardInset(active) {
65
68
  }, [active]);
66
69
  return inset;
67
70
  }
68
- const menuLabels = {
69
- en: { moreActions: 'More actions', editorMode: 'Editor mode', formatted: 'Formatted', markdown: 'Markdown', showOutline: 'Show outline' },
70
- fr: { moreActions: 'Plus d’actions', editorMode: 'Mode d’édition', formatted: 'Mis en forme', markdown: 'Markdown', showOutline: 'Afficher le plan' },
71
- };
72
71
  /** Unstyled native fallback. Installed recipes bind the host's shadcn dropdown. */
73
72
  function NativeMarkdownViewOptions({ labels, mode, onModeChange, richDisabledReason, outline, input }) {
74
73
  const InputControl = input ?? 'input';
@@ -78,7 +77,10 @@ function NativeMarkdownViewOptions({ labels, mode, onModeChange, richDisabledRea
78
77
  menu.current.open = false; };
79
78
  return _jsxs("details", { ref: menu, "data-boring": "markdown-menu", children: [_jsx("summary", { "aria-label": labels.moreActions, children: _jsx(MarkdownIcon, { name: "More" }) }), _jsxs("fieldset", { children: [_jsx("legend", { children: labels.editorMode }), _jsxs("label", { title: richDisabledReason, children: [_jsx(InputControl, { type: "radio", name: group, "aria-label": labels.formatted, "aria-describedby": richDisabledReason ? `${group}-reason` : undefined, checked: mode === 'rich', disabled: !!richDisabledReason, onChange: () => choose(() => onModeChange('rich')) }), _jsx(MarkdownIcon, { name: "Heading1" }), labels.formatted, richDisabledReason && _jsx("span", { id: `${group}-reason`, children: richDisabledReason })] }), _jsxs("label", { children: [_jsx(InputControl, { type: "radio", name: group, "aria-label": labels.markdown, checked: mode === 'source', onChange: () => choose(() => onModeChange('source')) }), _jsx(MarkdownIcon, { name: "Code" }), labels.markdown] })] }), outline && _jsxs("label", { children: [_jsx(InputControl, { type: "checkbox", "aria-label": labels.showOutline, checked: outline.shown, onChange: event => choose(() => outline.onChange(event.currentTarget.checked)) }), _jsx(MarkdownIcon, { name: "BulletList" }), labels.showOutline] })] });
80
79
  }
81
- function MarkdownEditorSession({ highlights, onQuoteSelection, onHighlightActivate, components, controller, title = 'Document', initialMode = 'rich', placeholder = 'Start writing', className, onMountedTools, header = true, showOutline = header, mode: controlledMode, onModeChange, onRichSafety, resolveImage, formattingToolbar = 'selection', locale = 'en', autosave = true, onAutosaveChange, onSaveResult, activeController: active }) {
80
+ function MarkdownEditorSession({ highlights, onQuoteSelection, onHighlightActivate, components, controller, title: titleProp, initialMode = 'rich', placeholder: placeholderProp, className, onMountedTools, header = true, showOutline = header, mode: controlledMode, onModeChange, onRichSafety, resolveImage, formattingToolbar = 'selection', locale = 'en', labels: labelOverrides, autosave = true, onAutosaveChange, onSaveResult, activeController: active }) {
81
+ const text = useMemo(() => resolveMarkdownEditorLabels(locale, labelOverrides), [locale, labelOverrides]);
82
+ const title = titleProp ?? text.defaultTitle;
83
+ const placeholder = placeholderProp ?? text.defaultPlaceholder;
82
84
  const Control = components?.button ?? 'button';
83
85
  const FormattingControl = components?.formattingControl ?? 'button';
84
86
  const Bubble = components?.bubble ?? Fragment;
@@ -107,6 +109,10 @@ function MarkdownEditorSession({ highlights, onQuoteSelection, onHighlightActiva
107
109
  activeMode.current = mode;
108
110
  const images = useRef(resolveImage);
109
111
  images.current = resolveImage;
112
+ const preservedLabels = useRef(text);
113
+ preservedLabels.current = text;
114
+ const richPlaceholder = useRef(placeholder);
115
+ richPlaceholder.current = placeholder;
110
116
  useEffect(() => { onRichSafety?.(safety); }, [safety, onRichSafety]);
111
117
  const [error, setError] = useState(null);
112
118
  const interaction = useRef(false);
@@ -120,10 +126,10 @@ function MarkdownEditorSession({ highlights, onQuoteSelection, onHighlightActiva
120
126
  };
121
127
  const editor = useEditor({
122
128
  immediatelyRender: false,
123
- extensions: createMarkdownExtensions({ placeholder, images: () => images.current, source: () => controller.getSnapshot().text.replace(/\r\n/g, '\n') }),
129
+ extensions: createMarkdownExtensions({ placeholder: () => richPlaceholder.current, images: () => images.current, preservedLabels: () => preservedLabels.current, source: () => controller.getSnapshot().text.replace(/\r\n/g, '\n') }),
124
130
  content: '',
125
131
  editable: !state.readOnly && state.lifecycle === 'active',
126
- editorProps: { attributes: { role: 'textbox', 'aria-label': `${title} rich text`, 'aria-multiline': 'true', tabindex: '0', class: 'boring-markdown-content' } },
132
+ editorProps: { attributes: { role: 'textbox', 'aria-label': text.richName(title), 'aria-multiline': 'true', tabindex: '0', class: 'boring-markdown-content' } },
127
133
  onUpdate: ({ editor: current, transaction }) => {
128
134
  if (active.current !== controller || !interaction.current || !transaction.docChanged || controller.getSnapshot().lifecycle !== 'active' || controller.getSnapshot().readOnly)
129
135
  return;
@@ -154,6 +160,10 @@ function MarkdownEditorSession({ highlights, onQuoteSelection, onHighlightActiva
154
160
  projected.current = state.text;
155
161
  }, [editor, state.text]);
156
162
  useEffect(() => { editor?.setEditable(!state.readOnly && state.lifecycle === 'active'); }, [editor, state.readOnly, state.lifecycle]);
163
+ useEffect(() => { if (editor && !editor.isDestroyed)
164
+ updatePreservedMarkdownLabels(editor.view.dom, text); }, [editor, text]);
165
+ useEffect(() => { if (editor && !editor.isDestroyed)
166
+ editor.view.dispatch(editor.state.tr.setMeta('addToHistory', false)); }, [editor, placeholder]);
157
167
  const mounted = useMemo(() => {
158
168
  if (mode === 'rich' && !editor)
159
169
  return null;
@@ -262,7 +272,7 @@ function MarkdownEditorSession({ highlights, onQuoteSelection, onHighlightActiva
262
272
  if (kind === 'saved')
263
273
  setError(null);
264
274
  if (kind === 'denied' || kind === 'conflict' || kind === 'unavailable' || kind === 'file_too_large')
265
- 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.');
275
+ setError(typeof reason === 'string' && reason ? reason : kind === 'conflict' ? text.conflictFallback : text.saveFallback);
266
276
  };
267
277
  const saving = useMarkdownAutosave(controller, autosave, result => { report(result); onSaveResult?.(result); });
268
278
  useEffect(() => { onAutosaveChange?.(saving.enabled ? saving.immediate : null); return () => onAutosaveChange?.(null); }, [onAutosaveChange, saving.enabled, saving.immediate]);
@@ -272,12 +282,12 @@ function MarkdownEditorSession({ highlights, onQuoteSelection, onHighlightActiva
272
282
  void saving.manual();
273
283
  else
274
284
  void controller.flush(controller.actions.selection()).then(report, () => { if (active.current === controller)
275
- setError('The save could not be completed. Your text is still available.'); });
285
+ setError(text.saveFailed); });
276
286
  };
277
287
  const run = (operation) => {
278
288
  setError(null);
279
289
  void operation().then(report, () => { if (active.current === controller)
280
- setError('The operation could not be completed. Your text is still available.'); });
290
+ setError(text.operationFailed); });
281
291
  };
282
292
  const richOnly = unavailable || mode === 'source' || !editor;
283
293
  const [linkDraft, setLinkDraft] = useState(null);
@@ -309,9 +319,9 @@ function MarkdownEditorSession({ highlights, onQuoteSelection, onHighlightActiva
309
319
  command(() => href === '' ? chain().extendMarkRange('link').unsetLink().run() : chain().extendMarkRange('link').setLink({ href }).run());
310
320
  setLinkDraft(null);
311
321
  };
312
- 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())] })] });
322
+ const formatTools = _jsxs(_Fragment, { children: [tool(text.bold, 'Bold', marks?.bold, () => chain().toggleBold().run()), tool(text.italic, 'Italic', marks?.italic, () => chain().toggleItalic().run()), tool(text.strike, 'Strike', marks?.strike, () => chain().toggleStrike().run()), tool(text.highlight, 'Highlight', marks?.highlight, () => chain().toggleHighlight().run()), tool(text.code, 'Code', marks?.code, () => chain().toggleCode().run()), separator, tool(text.heading(1), 'Heading1', marks?.h1, () => chain().toggleHeading({ level: 1 }).run()), tool(text.heading(2), 'Heading2', marks?.h2, () => chain().toggleHeading({ level: 2 }).run()), tool(text.heading(3), 'Heading3', marks?.h3, () => chain().toggleHeading({ level: 3 }).run()), separator, tool(text.bulletList, 'BulletList', marks?.bullet, () => chain().toggleBulletList().run()), tool(text.numberedList, 'OrderedList', marks?.ordered, () => chain().toggleOrderedList().run()), tool(text.taskList, 'TaskList', marks?.task, () => chain().toggleTaskList().run()), separator, tool(text.quote, 'Quote', marks?.quote, () => chain().toggleBlockquote().run()), tool(text.codeBlock, 'CodeBlock', marks?.codeBlock, () => chain().toggleCodeBlock().run()), tool(text.link, 'Link', marks?.link || linkDraft !== null, openLink), tool(text.table, 'Table', marks?.table, () => marks?.table ? chain().deleteTable().run() : chain().insertTable({ rows: 3, cols: 3, withHeaderRow: true }).run()), tool(text.horizontalRule, 'Rule', false, () => chain().setHorizontalRule().run()), marks?.table && _jsxs(_Fragment, { children: [separator, tool(text.addTableRow, 'Rows', false, () => chain().addRowAfter().run()), tool(text.addTableColumn, 'Columns', false, () => chain().addColumnAfter().run()), tool(text.deleteTable, 'Trash', false, () => chain().deleteTable().run())] })] });
313
323
  const viewOptions = {
314
- labels: menuLabels[locale], mode, onModeChange: switchMode, richDisabledReason: richOffered ? undefined : describeUnsafe(safety),
324
+ labels: text, mode, onModeChange: switchMode, richDisabledReason: richOffered || safety.safe ? undefined : text.richOff(safety.reasons),
315
325
  outline: showOutline ? { shown: outlineShown, onChange: setOutlineShown } : undefined,
316
326
  };
317
327
  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))
@@ -321,11 +331,11 @@ function MarkdownEditorSession({ highlights, onQuoteSelection, onHighlightActiva
321
331
  if (!unavailable && !uncertain)
322
332
  save();
323
333
  }
324
- }, 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' }), ViewOptions ? _jsx(ViewOptions, { ...viewOptions }) : _jsx(NativeMarkdownViewOptions, { ...viewOptions, input: components?.input }), mode === 'rich' && _jsx(FormattingControl, { type: "button", "aria-label": locale === 'fr' ? 'Mise en forme' : 'Formatting', title: locale === 'fr' ? 'Mise en forme' : 'Formatting', "aria-pressed": toolbar === 'pinned', onClick: () => setToolbar(toolbar === 'pinned' ? 'selection' : 'pinned'), children: _jsx(MarkdownIcon, { name: "Type" }) }), !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') {
334
+ }, children: [header && _jsxs("header", { children: [_jsx("strong", { children: title }), state.readOnly && _jsx("span", { children: text.readOnly }), _jsx("span", { role: "status", children: state.lifecycle === 'disposed' ? text.closed : pending ? text.saving : uncertain ? text.saveUnconfirmed : conflict ? text.changedElsewhere : state.dirty ? text.unsaved : text.saved }), ViewOptions ? _jsx(ViewOptions, { ...viewOptions }) : _jsx(NativeMarkdownViewOptions, { ...viewOptions, input: components?.input }), mode === 'rich' && _jsx(FormattingControl, { type: "button", "aria-label": text.formatting, title: text.formatting, "aria-pressed": toolbar === 'pinned', onClick: () => setToolbar(toolbar === 'pinned' ? 'selection' : 'pinned'), children: _jsx(MarkdownIcon, { name: "Type" }) }), !saving.enabled && _jsx(Control, { type: "button", disabled: unavailable || !state.dirty || pending || uncertain, onClick: save, children: text.save })] }), _jsx(TextDraftControls, { labels: text, 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.textFormatting, className: "boring-markdown-toolbar", children: formatTools }), mode === 'rich' && dockVisible && createPortal(_jsxs("div", { role: "toolbar", "aria-label": text.textFormatting, 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.textFormatting, className: "boring-viewer boring-markdown-bubble", children: _jsxs(Bubble, { children: [tool(text.bold, 'Bold', marks?.bold, () => chain().toggleBold().run()), tool(text.italic, 'Italic', marks?.italic, () => chain().toggleItalic().run()), tool(text.highlight, 'Highlight', marks?.highlight, () => chain().toggleHighlight().run()), tool(text.code, 'Code', marks?.code, () => chain().toggleCode().run()), tool(text.link, 'Link', marks?.link || linkDraft !== null, openLink)] }) }), mode === 'rich' && linkDraft !== null && _jsxs("form", { className: "boring-markdown-link", "aria-label": text.linkAddress, onSubmit: event => { event.preventDefault(); applyLink(); }, children: [_jsx(LinkInput, { type: "text", inputMode: "url", autoFocus: true, "aria-label": text.linkAddress, placeholder: "https://", value: linkDraft, onChange: event => setLinkDraft(event.currentTarget.value), onKeyDown: event => { if (event.key === 'Escape') {
325
335
  event.preventDefault();
326
336
  setLinkDraft(null);
327
337
  editor?.commands.focus();
328
- } } }), _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 && outlineShown && 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: () => {
338
+ } } }), _jsx(Control, { type: "submit", children: linkDraft.trim() === '' && marks?.link ? text.removeLink : text.applyLink }), _jsx(Control, { type: "button", onClick: () => { setLinkDraft(null); editor?.commands.focus(); }, children: text.cancel })] }), !richOffered && _jsx("p", { role: "note", className: "boring-markdown-notice", "data-testid": "markdown-source-only", children: safety.safe ? '' : text.richOff(safety.reasons) }), showOutline && outlineShown && mode === 'rich' && outline && outline.headings.length > 0 && _jsx("nav", { "aria-label": text.documentOutline, children: _jsx("ol", { children: outline.headings.map((heading, index) => _jsx("li", { "data-heading-level": heading.level, children: _jsx(Control, { type: "button", "aria-label": text.goToHeading(heading.text || text.untitledHeading), disabled: state.lifecycle !== 'active', onClick: () => {
329
339
  if (!editor || editor.isDestroyed || active.current !== controller || controller.getSnapshot().lifecycle !== 'active'
330
340
  || controller.getSnapshot().bufferVersion !== state.bufferVersion || editor.state.doc !== outline.document)
331
341
  return;
@@ -338,9 +348,9 @@ function MarkdownEditorSession({ highlights, onQuoteSelection, onHighlightActiva
338
348
  if (active.current === controller && result.kind !== 'applied' && result.kind !== 'proposed')
339
349
  setError(result.reason);
340
350
  }).catch(() => { if (active.current === controller)
341
- setError('The heading could not be selected'); });
342
- }, 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'
343
- ? _jsx(Source, { ref: sourceInput, className: "boring-markdown-source", "aria-label": `${title} Markdown source`, value: state.text, readOnly: unavailable, spellCheck: false, onSelect: event => onQuoteSelection?.(captureMarkdownQuote(state.text, { start: event.currentTarget.selectionStart, end: event.currentTarget.selectionEnd })), onChange: event => controller.actions.edit(event.currentTarget.value) })
351
+ setError(text.headingNotSelected); });
352
+ }, children: heading.text || text.untitledHeading }) }, heading.position)) }) }), uncertain && _jsxs("div", { role: "alert", className: "boring-markdown-alert", children: [text.uncertainNotice, _jsx(Control, { type: "button", onClick: () => run(controller.actions.reconcile), children: text.checkSaveOutcome }), _jsx(Control, { type: "button", onClick: () => run(controller.actions.abandon), children: text.stopChecking })] }), conflict && _jsxs("div", { role: "alert", className: "boring-markdown-alert", children: [text.conflictNotice, _jsx(Control, { type: "button", disabled: pending || uncertain, onClick: () => run(controller.actions.refresh), children: text.checkSavedVersion }), _jsx(Control, { type: "button", disabled: pending || uncertain, onClick: () => run(controller.actions.discardToRemote), children: text.discardLocal })] }), (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 || text.proposedEdit }), _jsxs("details", { children: [_jsx("summary", { children: text.reviewChanges }), _jsx(MarkdownProposalDiff, { components: components, proposal: proposal, labels: text }), _jsxs("details", { children: [_jsx("summary", { children: text.exactBeforeAfter }), _jsx("h4", { children: text.before }), _jsx("pre", { children: proposal.before }), _jsx("h4", { children: text.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 ? text.adopted : text.acceptAndSave }), _jsx(Control, { type: "button", onClick: () => controller.actions.reject(proposal.id), children: text.dismissProposal })] }, proposal.id)), mode === 'source'
353
+ ? _jsx(Source, { ref: sourceInput, className: "boring-markdown-source", "aria-label": text.sourceName(title), placeholder: placeholder, value: state.text, readOnly: unavailable, spellCheck: false, onSelect: event => onQuoteSelection?.(captureMarkdownQuote(state.text, { start: event.currentTarget.selectionStart, end: event.currentTarget.selectionEnd })), onChange: event => controller.actions.edit(event.currentTarget.value) })
344
354
  : _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'))
345
355
  event.preventDefault(); }, onAuxClickCapture: event => { if (event.target instanceof Element && event.target.closest('a'))
346
356
  event.preventDefault(); }, children: _jsx(EditorContent, { editor: editor }) })] });
@@ -1,11 +1,13 @@
1
1
  import type { Extensions } from '@tiptap/core';
2
+ import { preservedMarkdownNodes } from './markdown-preserved.js';
2
3
  /** Maps an image address in the document to a URL the browser may load, or `undefined` to keep the image inert. */
3
4
  export type MarkdownImageResolver = (source: string) => string | undefined;
4
5
  /** Only an image carried inside the document itself loads by default. Anything else needs the host's resolver. */
5
6
  export declare const inlineImagesOnly: MarkdownImageResolver;
6
7
  export interface MarkdownExtensionOptions {
7
- readonly placeholder?: string;
8
+ readonly placeholder?: string | (() => string);
8
9
  readonly images?: () => MarkdownImageResolver | undefined;
10
+ readonly preservedLabels?: Parameters<typeof preservedMarkdownNodes>[0];
9
11
  readonly source?: () => string;
10
12
  }
11
13
  /**
@@ -13,5 +15,5 @@ export interface MarkdownExtensionOptions {
13
15
  * strike, links), task lists, GFM tables, images and highlight. Underline is off because Markdown has no underline.
14
16
  * Images are inert: without a host resolver they render as a labelled placeholder and never make a request.
15
17
  */
16
- export declare function createMarkdownExtensions({ placeholder, images, source }?: MarkdownExtensionOptions): Extensions;
18
+ export declare function createMarkdownExtensions({ placeholder, images, preservedLabels, source }?: MarkdownExtensionOptions): Extensions;
17
19
  //# sourceMappingURL=markdown-extensions.d.ts.map
@@ -143,7 +143,7 @@ export const inlineImagesOnly = source => /^data:image\/(?:png|jpe?g|gif|webp);b
143
143
  * strike, links), task lists, GFM tables, images and highlight. Underline is off because Markdown has no underline.
144
144
  * Images are inert: without a host resolver they render as a labelled placeholder and never make a request.
145
145
  */
146
- export function createMarkdownExtensions({ placeholder, images, source = () => '' } = {}) {
146
+ export function createMarkdownExtensions({ placeholder, images, preservedLabels, source = () => '' } = {}) {
147
147
  const InertImage = Image.extend({
148
148
  renderHTML({ node }) {
149
149
  const source = String(node.attrs['src'] ?? '');
@@ -161,7 +161,7 @@ export function createMarkdownExtensions({ placeholder, images, source = () => '
161
161
  });
162
162
  return [
163
163
  sourceStarterKit(source, () => schema).configure({ link: { openOnClick: false, autolink: false, linkOnPaste: false }, underline: false }),
164
- ...preservedMarkdownNodes(),
164
+ ...preservedMarkdownNodes(preservedLabels),
165
165
  TaskList, TaskItem.configure({ nested: true }),
166
166
  SourceTable.configure({ resizable: false }), TableRow, TableHeader, TableCell,
167
167
  InertImage.configure({ inline: false, allowBase64: true }),
@@ -1,4 +1,8 @@
1
1
  import { Node } from '@tiptap/core';
2
+ import { type MarkdownEditorLabels } from './markdown-editor-labels.js';
3
+ type PreservedLabels = Pick<MarkdownEditorLabels, 'preservedBlockName' | 'frontMatterCode'>;
4
+ export declare function updatePreservedMarkdownLabels(root: HTMLElement, labels: PreservedLabels): void;
2
5
  /** Literal Markdown, never HTML inserted into the host DOM. These atoms are host presentation, not Pi execution. */
3
- export declare function preservedMarkdownNodes(): Node<any, any>[];
6
+ export declare function preservedMarkdownNodes(labels?: () => PreservedLabels): Node<any, any>[];
7
+ export {};
4
8
  //# sourceMappingURL=markdown-preserved.d.ts.map
@@ -1,14 +1,25 @@
1
1
  import { Node } from '@tiptap/core';
2
2
  import { Marked } from 'marked';
3
+ import { markdownEditorLabels } from './markdown-editor-labels.js';
3
4
  const literalHtml = new Marked();
5
+ export function updatePreservedMarkdownLabels(root, labels) {
6
+ for (const block of Array.from(root.querySelectorAll('[data-preserved]'))) {
7
+ const name = block.querySelector('small');
8
+ if (name)
9
+ name.textContent = labels.preservedBlockName(block.dataset['preserved'] ?? 'HTML');
10
+ const code = block.querySelector('[role="textbox"]');
11
+ if (code)
12
+ code.setAttribute('aria-label', labels.frontMatterCode);
13
+ }
14
+ }
4
15
  /** Literal Markdown, never HTML inserted into the host DOM. These atoms are host presentation, not Pi execution. */
5
- export function preservedMarkdownNodes() {
16
+ export function preservedMarkdownNodes(labels = () => markdownEditorLabels.en) {
6
17
  const block = Node.create({
7
18
  name: 'preservedBlock', group: 'block', atom: true,
8
19
  addAttributes: () => ({ raw: { default: '', rendered: false }, label: { default: 'HTML', rendered: false } }),
9
20
  parseHTML: () => [],
10
21
  renderHTML: ({ node }) => ['div', { class: 'boring-markdown-preserved', 'data-preserved': node.attrs['label'] },
11
- ['small', {}, String(node.attrs['label'])], ['pre', {}, String(node.attrs['raw'])]],
22
+ ['small', {}, labels().preservedBlockName(String(node.attrs['label']))], ['pre', {}, String(node.attrs['raw'])]],
12
23
  markdownTokenName: 'preservedBlock',
13
24
  markdownTokenizer: {
14
25
  name: 'preservedBlock', level: 'block',
@@ -33,7 +44,7 @@ export function preservedMarkdownNodes() {
33
44
  dom.className = 'boring-markdown-preserved';
34
45
  dom.dataset['preserved'] = String(node.attrs['label']);
35
46
  const label = document.createElement('small');
36
- label.textContent = String(node.attrs['label']);
47
+ label.textContent = labels().preservedBlockName(String(node.attrs['label']));
37
48
  dom.append(label);
38
49
  if (node.attrs['label'] !== 'Front matter') {
39
50
  const pre = document.createElement('pre');
@@ -45,7 +56,7 @@ export function preservedMarkdownNodes() {
45
56
  code.textContent = String(node.attrs['raw']);
46
57
  code.contentEditable = editor.isEditable ? 'plaintext-only' : 'false';
47
58
  code.setAttribute('role', 'textbox');
48
- code.setAttribute('aria-label', 'Front matter code');
59
+ code.setAttribute('aria-label', labels().frontMatterCode);
49
60
  code.setAttribute('aria-multiline', 'true');
50
61
  code.spellcheck = false;
51
62
  dom.append(code);
@@ -1,6 +1,8 @@
1
1
  import type { MarkdownProposal } from './markdown.js';
2
- export declare function MarkdownProposalDiff({ components, proposal }: {
2
+ import { type MarkdownEditorLabels } from './markdown-editor-labels.js';
3
+ export declare function MarkdownProposalDiff({ components, proposal, labels }: {
3
4
  readonly components?: import('./viewer-components.js').ViewerComponents | undefined;
4
5
  readonly proposal: MarkdownProposal;
6
+ readonly labels?: MarkdownEditorLabels;
5
7
  }): import("react").JSX.Element;
6
8
  //# sourceMappingURL=markdown-proposal-diff.d.ts.map
@@ -1,5 +1,6 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useMemo, useState } from 'react';
3
+ import { markdownEditorLabels } from './markdown-editor-labels.js';
3
4
  const maxMatrixCells = 250_000;
4
5
  const contextLines = 2;
5
6
  function lineAt(values, index) {
@@ -102,22 +103,22 @@ function entries(diff) {
102
103
  }
103
104
  return result;
104
105
  }
105
- function DiffRow({ line }) {
106
+ function DiffRow({ line, labels }) {
106
107
  const terminated = line.value.endsWith('\n');
107
108
  const display = terminated ? line.value.slice(0, -1) : line.value;
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
+ 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') && _jsxs("span", { "data-diff-crlf": "", children: [" ", labels.crlfLineEnding] }), !terminated && _jsxs("span", { "data-diff-no-newline": "", children: [" ", labels.noNewline] })] });
109
110
  }
110
- export function MarkdownProposalDiff({ components, proposal }) {
111
+ export function MarkdownProposalDiff({ components, proposal, labels = markdownEditorLabels.en }) {
111
112
  const Control = components?.button ?? 'button';
112
113
  const diff = useMemo(() => review(proposal.before, proposal.after), [proposal.before, proposal.after]);
113
114
  const shown = useMemo(() => entries(diff.lines), [diff]);
114
115
  const [expanded, setExpanded] = useState(() => new Set());
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 => {
116
+ return _jsxs("div", { "data-boring": "proposal-diff", role: "group", "aria-label": labels.proposedLineChanges, children: [diff.fallback && _jsx("p", { role: "status", "data-diff-fallback": "", children: labels.diffFallback }), proposal.before === proposal.after && _jsx("p", { children: labels.noTextChanges }), _jsx("div", { style: { overflowX: 'auto', whiteSpace: 'pre-wrap' }, children: shown.map((entry, index) => entry.kind === 'fold' ? _jsxs("div", { children: [_jsx(Control, { type: "button", "data-diff-fold": "", "aria-expanded": expanded.has(entry.index), onClick: () => setExpanded(current => {
116
117
  const next = new Set(current);
117
118
  if (next.has(entry.index))
118
119
  next.delete(entry.index);
119
120
  else
120
121
  next.add(entry.index);
121
122
  return next;
122
- }), children: [expanded.has(entry.index) ? 'Hide' : 'Show', " ", entry.lines.length, " unchanged lines"] }), expanded.has(entry.index) && entry.lines.map((line, lineIndex) => _jsx(DiffRow, { line: line }, lineIndex))] }, `fold-${entry.index}`) : _jsx(DiffRow, { line: entry }, `line-${index}`)) })] });
123
+ }), children: expanded.has(entry.index) ? labels.hideUnchangedLines(entry.lines.length) : labels.showUnchangedLines(entry.lines.length) }), expanded.has(entry.index) && entry.lines.map((line, lineIndex) => _jsx(DiffRow, { line: line, labels: labels }, lineIndex))] }, `fold-${entry.index}`) : _jsx(DiffRow, { line: entry, labels: labels }, `line-${index}`)) })] });
123
124
  }
@@ -1,11 +1,15 @@
1
1
  import type { TextDraftActions, TextDraftRecoveryState } from './text-draft-types.js';
2
2
  import type { ViewerComponents } from './viewer-components.js';
3
+ import { type TextDraftLabels } from './text-draft-labels.js';
4
+ export type { TextDraftLabels } from './text-draft-labels.js';
3
5
  export interface TextDraftControlsProps {
4
6
  readonly components?: ViewerComponents | undefined;
5
7
  readonly recovery: TextDraftRecoveryState;
6
8
  readonly actions: TextDraftActions;
7
9
  readonly readOnly?: boolean;
8
10
  readonly blocked?: boolean;
11
+ /** Words of the panel. Defaults to English; a host passes the French set or its own. */
12
+ readonly labels?: TextDraftLabels;
9
13
  }
10
- export declare function TextDraftControls({ components, recovery, actions, readOnly, blocked }: TextDraftControlsProps): import("react").JSX.Element | null;
14
+ export declare function TextDraftControls({ components, recovery, actions, readOnly, blocked, labels }: TextDraftControlsProps): import("react").JSX.Element | null;
11
15
  //# 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({ components, recovery, actions, readOnly = false, blocked = false }) {
4
+ import { textDraftLabels } from './text-draft-labels.js';
5
+ export function TextDraftControls({ components, recovery, actions, readOnly = false, blocked = false, labels = textDraftLabels.en }) {
5
6
  const Control = components?.button ?? 'button';
6
7
  const owner = useRef({ actions, busy: false });
7
8
  const [pending, setPending] = useState(false);
@@ -20,15 +21,15 @@ export function TextDraftControls({ components, recovery, actions, readOnly = fa
20
21
  setNotice(undefined);
21
22
  void Promise.resolve().then(action).then(result => {
22
23
  if (owner.current === capturedOwner)
23
- setNotice(result.reason ?? (result.kind === 'restored' ? 'Draft restored locally. Save to publish it.' : result.kind === 'discarded' ? 'Stored draft discarded. Current edits are unchanged.' : undefined));
24
+ setNotice(result.reason ?? (result.kind === 'restored' ? labels.draftRestored : result.kind === 'discarded' ? labels.draftDiscarded : undefined));
24
25
  }, () => { if (owner.current === capturedOwner)
25
- setNotice('Draft storage unavailable. Current edits are unchanged.'); }).finally(() => { if (owner.current === capturedOwner) {
26
+ setNotice(labels.draftStorageFailed); }).finally(() => { if (owner.current === capturedOwner) {
26
27
  capturedOwner.busy = false;
27
28
  setPending(false);
28
29
  } ; });
29
30
  };
30
31
  const discovery = active ? recovery.discovery : undefined;
31
32
  const checkpoint = active ? recovery.checkpoint : undefined;
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';
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
+ const status = !active ? labels.draftRecoveryState(recovery.kind) : discovery?.kind === 'checking' ? labels.draftChecking : checkpoint?.kind === 'pending' ? labels.draftStoring : checkpoint?.kind === 'stored' ? labels.draftStored : checkpoint?.kind === 'failed' ? labels.draftStorageUnavailable : labels.draftEnabled;
34
+ return _jsxs("section", { "data-boring": "text-draft-controls", "aria-label": labels.draftRecovery, children: [_jsx("p", { role: "status", children: status }), _jsx(Control, { type: "button", disabled: !active || pending || blocked || discovery?.kind === 'checking', onClick: () => run(actions.checkDrafts), children: labels.checkStoredDrafts }), _jsx(Control, { type: "button", disabled: !active || pending || readOnly, onClick: () => run(actions.checkpointDraft), children: labels.storeDraft }), discovery?.kind === 'empty' && _jsx("p", { children: labels.noStoredDrafts }), 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: labels.moreStoredDrafts }), discovery.choices.map(choice => _jsxs("div", { role: "group", "aria-label": labels.storedDraft, children: [_jsx("p", { children: choice.compatibility === 'conflict' ? labels.draftConflict : labels.draftMatches }), _jsxs("details", { children: [_jsx("summary", { children: labels.reviewStoredDraft }), _jsx("pre", { children: choice.text })] }), _jsx(Control, { type: "button", disabled: pending || blocked || readOnly || choice.compatibility !== 'exact', onClick: () => run(() => actions.restoreDraft(choice.selection)), children: labels.restoreDraft }), _jsx(Control, { type: "button", disabled: pending || blocked, onClick: () => run(() => actions.discardDraft(choice.selection)), children: labels.discardStoredDraft })] }, `${choice.selection.offerId}:${choice.selection.draft.writerId}:${choice.selection.draft.sequence}`))] }), notice && _jsx("p", { role: "alert", children: notice })] });
34
35
  }
@@ -0,0 +1,26 @@
1
+ /** The draft-recovery panel's words. English is the default; `locale="fr"` selects the French set; a host overrides any key. */
2
+ export interface TextDraftLabels {
3
+ readonly draftRecovery: string;
4
+ /** `kind` is the recovery state when recovery is not active, e.g. `disabled`. */
5
+ readonly draftRecoveryState: (kind: string) => string;
6
+ readonly draftChecking: string;
7
+ readonly draftStoring: string;
8
+ readonly draftStored: string;
9
+ readonly draftStorageUnavailable: string;
10
+ readonly draftEnabled: string;
11
+ readonly checkStoredDrafts: string;
12
+ readonly storeDraft: string;
13
+ readonly noStoredDrafts: string;
14
+ readonly moreStoredDrafts: string;
15
+ readonly storedDraft: string;
16
+ readonly draftConflict: string;
17
+ readonly draftMatches: string;
18
+ readonly reviewStoredDraft: string;
19
+ readonly restoreDraft: string;
20
+ readonly discardStoredDraft: string;
21
+ readonly draftRestored: string;
22
+ readonly draftDiscarded: string;
23
+ readonly draftStorageFailed: string;
24
+ }
25
+ export declare const textDraftLabels: Record<'en' | 'fr', TextDraftLabels>;
26
+ //# sourceMappingURL=text-draft-labels.d.ts.map
@@ -0,0 +1,47 @@
1
+ const frenchRecoveryStates = { disabled: 'désactivée', revoked: 'accès révoqué', expired: 'expirée', disposed: 'fermée' };
2
+ export const textDraftLabels = {
3
+ en: {
4
+ draftRecovery: 'Draft recovery',
5
+ draftRecoveryState: kind => `Draft recovery ${kind}`,
6
+ draftChecking: 'Checking stored drafts',
7
+ draftStoring: 'Storing draft',
8
+ draftStored: 'Draft stored for recovery. Storage is not a save receipt.',
9
+ draftStorageUnavailable: 'Draft recovery storage unavailable',
10
+ draftEnabled: 'Draft recovery enabled',
11
+ checkStoredDrafts: 'Check stored drafts',
12
+ storeDraft: 'Store draft for recovery',
13
+ noStoredDrafts: 'No stored drafts',
14
+ moreStoredDrafts: 'More stored drafts exist than this listing shows.',
15
+ storedDraft: 'Stored draft',
16
+ draftConflict: 'Saved revision changed. Review this draft separately.',
17
+ draftMatches: 'Stored draft matches the checked saved revision.',
18
+ reviewStoredDraft: 'Review stored draft',
19
+ restoreDraft: 'Restore draft',
20
+ discardStoredDraft: 'Discard stored draft',
21
+ draftRestored: 'Draft restored locally. Save to publish it.',
22
+ draftDiscarded: 'Stored draft discarded. Current edits are unchanged.',
23
+ draftStorageFailed: 'Draft storage unavailable. Current edits are unchanged.',
24
+ },
25
+ fr: {
26
+ draftRecovery: 'Récupération de brouillon',
27
+ draftRecoveryState: kind => `Récupération de brouillon : ${frenchRecoveryStates[kind] ?? kind}`,
28
+ draftChecking: 'Recherche des brouillons enregistrés',
29
+ draftStoring: 'Enregistrement du brouillon',
30
+ draftStored: 'Brouillon conservé pour récupération. Ce n’est pas un enregistrement du document.',
31
+ draftStorageUnavailable: 'Stockage des brouillons indisponible',
32
+ draftEnabled: 'Récupération de brouillon activée',
33
+ checkStoredDrafts: 'Chercher les brouillons enregistrés',
34
+ storeDraft: 'Conserver le brouillon pour récupération',
35
+ noStoredDrafts: 'Aucun brouillon enregistré',
36
+ moreStoredDrafts: 'Il existe d’autres brouillons que ceux listés ici.',
37
+ storedDraft: 'Brouillon enregistré',
38
+ draftConflict: 'La version enregistrée a changé. Examinez ce brouillon à part.',
39
+ draftMatches: 'Ce brouillon correspond à la version enregistrée vérifiée.',
40
+ reviewStoredDraft: 'Examiner le brouillon',
41
+ restoreDraft: 'Restaurer le brouillon',
42
+ discardStoredDraft: 'Supprimer le brouillon',
43
+ draftRestored: 'Brouillon restauré localement. Enregistrez pour le publier.',
44
+ draftDiscarded: 'Brouillon supprimé. Vos modifications actuelles sont inchangées.',
45
+ draftStorageFailed: 'Stockage des brouillons indisponible. Vos modifications actuelles sont inchangées.',
46
+ },
47
+ };
@@ -0,0 +1,33 @@
1
+ import type { VoiceCapture, VoiceRecording, VoiceStart } from './voice-capture.js';
2
+ export type VoiceAttempt<Data, Phase extends string = never> = Data & {
3
+ readonly id: number;
4
+ readonly abort: AbortController;
5
+ readonly capture: VoiceCapture;
6
+ readonly maxMs: number;
7
+ phase: 'starting' | 'recording' | 'processing' | Phase;
8
+ seconds: number;
9
+ levels: readonly number[];
10
+ paused: boolean;
11
+ canPause: boolean;
12
+ };
13
+ export interface VoiceAttemptOptions<Data, Phase extends string> {
14
+ readonly valid?: (item: VoiceAttempt<Data, Phase>) => boolean;
15
+ readonly refused?: (result: Extract<VoiceStart, {
16
+ kind: 'refused';
17
+ }>) => void;
18
+ }
19
+ /** Owns only this viewer's microphone attempts, never the borrowed chat or native execution. */
20
+ export declare function useVoiceAttempt<Data extends object, Phase extends string = never>(options?: VoiceAttemptOptions<Data, Phase>): {
21
+ attempts: readonly VoiceAttempt<Data, Phase>[];
22
+ captureSequence: number;
23
+ current: () => readonly VoiceAttempt<Data, Phase>[];
24
+ start: (data: Data, configuredMaxMs?: number) => VoiceAttempt<Data, Phase>;
25
+ stop: (item: VoiceAttempt<Data, Phase>) => Promise<VoiceRecording | null>;
26
+ pause: (item: VoiceAttempt<Data, Phase>) => void;
27
+ resume: (item: VoiceAttempt<Data, Phase>) => void;
28
+ remove: (item: VoiceAttempt<Data, Phase>) => void;
29
+ clear: () => void;
30
+ valid: (item: VoiceAttempt<Data, Phase>) => boolean;
31
+ update: () => void;
32
+ };
33
+ //# sourceMappingURL=voice-attempt.d.ts.map