@hachej/boring-ui-kit 0.4.26 → 0.4.27
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 +15 -2
- package/dist/chat-dictation.d.ts +27 -0
- package/dist/chat-dictation.js +71 -0
- package/dist/chat-react-session.d.ts +170 -0
- package/dist/chat-react-session.js +367 -0
- package/dist/chat-react.d.ts +2 -0
- package/dist/chat-react.js +1 -0
- package/dist/chat-text.d.ts +9 -0
- package/dist/chat-text.js +4 -0
- package/dist/chat-voice-message.d.ts +65 -0
- package/dist/chat-voice-message.js +235 -0
- package/dist/chat-voice.d.ts +7 -0
- package/dist/chat-voice.js +4 -0
- package/dist/experience-catalog.d.ts +2 -2
- package/dist/experience-region-tree.d.ts +1 -1
- package/dist/markdown-editor-labels.d.ts +83 -0
- package/dist/markdown-editor-labels.js +75 -0
- package/dist/markdown-editor.d.ts +7 -1
- package/dist/markdown-editor.js +28 -18
- package/dist/markdown-extensions.d.ts +4 -2
- package/dist/markdown-extensions.js +2 -2
- package/dist/markdown-preserved.d.ts +5 -1
- package/dist/markdown-preserved.js +15 -4
- package/dist/markdown-proposal-diff.d.ts +3 -1
- package/dist/markdown-proposal-diff.js +6 -5
- package/dist/text-draft-controls.d.ts +5 -1
- package/dist/text-draft-controls.js +6 -5
- package/dist/text-draft-labels.d.ts +26 -0
- package/dist/text-draft-labels.js +47 -0
- package/dist/voice-attempt.d.ts +33 -0
- package/dist/voice-attempt.js +122 -0
- package/package.json +6 -2
- package/r/agent-target.json +4 -4
- package/r/app-shell.json +14 -14
- package/r/board-viewer.json +7 -6
- package/r/button.json +1 -1
- package/r/dock.json +2 -2
- package/r/documents.json +12 -12
- package/r/feedback-preview.json +1 -1
- package/r/feedback.json +7 -7
- package/r/file-tree.json +4 -4
- package/r/highlights.json +3 -3
- package/r/html-viewer.json +3 -3
- package/r/looks-viewer.json +3 -2
- package/r/maquette-viewer.json +1 -1
- package/r/markdown-editor.json +3 -3
- package/r/mockup-viewer.json +1 -1
- package/r/pi-ambient.json +9 -9
- package/r/pi-app-kinds.json +10 -10
- package/r/pi-app.json +13 -13
- package/r/pi-artifacts.json +2 -2
- package/r/pi-chat-approval.json +2 -2
- package/r/pi-chat-attachments.json +1 -1
- package/r/pi-chat-markdown.json +2 -2
- package/r/pi-chat-mentions.json +1 -1
- package/r/pi-chat-pickers.json +1 -1
- package/r/pi-chat-plugins.json +11 -11
- package/r/pi-chat-question.json +1 -1
- package/r/pi-chat-slash.json +2 -2
- package/r/pi-chat-subagent.json +1 -1
- package/r/pi-chat-voice.json +5 -5
- package/r/pi-chat.json +19 -19
- package/r/pi-workspace.json +4 -4
- package/r/placed-agent.json +4 -4
- package/r/provider-setup.json +1 -1
- package/r/registry.json +180 -168
- package/r/requests-board.json +1 -1
- package/r/shared-chat.json +9 -9
- package/r/task-list-viewer.json +2 -2
- package/r/utils.json +14 -2
- package/r/version-history.json +2 -2
- package/r/viewer-code.json +2 -2
- package/r/viewer-frame.json +7 -7
- package/r/viewer-html.json +4 -4
- package/r/viewer-image.json +2 -2
- package/r/viewer-markdown.json +5 -5
- package/r/viewer-pdf.json +3 -3
- package/r/viewers.json +6 -6
- package/r/windows.json +3 -3
package/dist/markdown-editor.js
CHANGED
|
@@ -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,
|
|
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
|
|
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':
|
|
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' ?
|
|
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(
|
|
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(
|
|
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(
|
|
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:
|
|
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:
|
|
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 ?
|
|
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(
|
|
342
|
-
}, children: heading.text ||
|
|
343
|
-
? _jsx(Source, { ref: sourceInput, className: "boring-markdown-source", "aria-label":
|
|
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',
|
|
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
|
-
|
|
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') &&
|
|
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":
|
|
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:
|
|
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
|
-
|
|
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' ?
|
|
24
|
+
setNotice(result.reason ?? (result.kind === 'restored' ? labels.draftRestored : result.kind === 'discarded' ? labels.draftDiscarded : undefined));
|
|
24
25
|
}, () => { if (owner.current === capturedOwner)
|
|
25
|
-
setNotice(
|
|
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 ?
|
|
33
|
-
return _jsxs("section", { "data-boring": "text-draft-controls", "aria-label":
|
|
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
|