@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
@@ -0,0 +1,235 @@
1
+ 'use client';
2
+ import { useCallback, useEffect, useLayoutEffect, useRef, useState, useSyncExternalStore } from 'react';
3
+ import { appendToDraft, isVoicePath, mentionedPaths, mentionToken, voiceRefusalLabel, withoutMentions } from './chat-text.js';
4
+ import { useVoiceAttempt } from './voice-attempt.js';
5
+ import { useDictation } from './chat-dictation.js';
6
+ const targetOf = (controller) => {
7
+ const { identity, conversationId } = controller.getSnapshot();
8
+ return JSON.stringify([identity.runtimeId, identity.scopeId, identity.principalId, conversationId]);
9
+ };
10
+ const noSubscribe = () => () => { };
11
+ /** Local admission preparation only. Native send/reconcile owns the submission; uploaded bytes stay in the host workspace. */
12
+ export function useVoiceMessage({ controller, config, attachments, labels, admitted, activeController }) {
13
+ const selected = config ? controller : undefined;
14
+ const readObservation = useCallback(() => {
15
+ if (!selected)
16
+ return '';
17
+ const snapshot = selected.getSnapshot(), send = snapshot.send;
18
+ return JSON.stringify([targetOf(selected), snapshot.disposed, send.kind, 'attempt' in send ? send.attempt.requestId : undefined]);
19
+ }, [selected]);
20
+ const observation = useSyncExternalStore(selected?.subscribe ?? noSubscribe, readObservation, readObservation);
21
+ const latest = useRef({ controller, config, admitted, activeController });
22
+ useLayoutEffect(() => { latest.current = { controller, config, admitted, activeController }; });
23
+ const [problem, setProblem] = useState();
24
+ const core = useVoiceAttempt({
25
+ valid: item => latest.current.controller === item.controller && (!latest.current.activeController || latest.current.activeController.current === item.controller) && Boolean(latest.current.config) && !item.controller.getSnapshot().disposed && targetOf(item.controller) === item.target,
26
+ refused: result => setProblem(labels[voiceRefusalLabel[result.code ?? 'failed']]),
27
+ });
28
+ const { valid, remove, clear, update } = core;
29
+ const active = () => core.current().find(item => item.phase === 'starting' || item.phase === 'recording') ?? core.current().find(item => item.phase === 'processing' || item.phase === 'sending' || item.phase === 'unconfirmed');
30
+ const cancel = () => { const item = active(); if (item)
31
+ remove(item); };
32
+ const target = selected ? targetOf(selected) : undefined;
33
+ const configured = Boolean(config);
34
+ // eslint-disable-next-line react-hooks/exhaustive-deps, react-hooks/set-state-in-effect -- another controller, conversation or config drops the attempts and the problem, once per change
35
+ useEffect(() => { clear(); setProblem(undefined); }, [controller, target, configured]);
36
+ useEffect(() => {
37
+ for (const item of [...core.current()]) {
38
+ if (!valid(item)) {
39
+ remove(item);
40
+ continue;
41
+ }
42
+ if (item.phase !== 'unconfirmed')
43
+ continue;
44
+ const snapshot = selected?.getSnapshot();
45
+ if (snapshot?.send.kind === 'admitted' && 'attempt' in snapshot.send && snapshot.send.attempt.requestId === item.requestId) {
46
+ remove(item);
47
+ latest.current.admitted?.();
48
+ // eslint-disable-next-line react-hooks/set-state-in-effect -- the chat's send state settles an unconfirmed attempt; update() republishes the attempt ref
49
+ }
50
+ else if (snapshot?.send.kind === 'blocked') {
51
+ item.phase = 'blocked';
52
+ update();
53
+ }
54
+ }
55
+ // eslint-disable-next-line react-hooks/exhaustive-deps -- runs when the chat's observation changes; remove/update are per-render closures over refs
56
+ }, [selected, observation]);
57
+ const send = async (item, audioOnly = false) => {
58
+ if (!valid(item) || !item.path || item.phase === 'sending' || item.phase === 'unconfirmed')
59
+ return;
60
+ const transcript = audioOnly ? '' : item.transcript ?? '';
61
+ const text = [...item.mentions.map(mentionToken), mentionToken(item.path), transcript].filter(Boolean).join(' ');
62
+ const before = item.controller.getSnapshot();
63
+ if (before.send.kind === 'unknown') {
64
+ // This recording was never submitted. Do not bind it to another message's uncertain request.
65
+ const draft = item.controller.getSnapshot().draft.text;
66
+ if (transcript)
67
+ item.controller.setText(appendToDraft(draft, transcript));
68
+ item.phase = 'blocked';
69
+ update();
70
+ return;
71
+ }
72
+ item.phase = 'sending';
73
+ update();
74
+ let submittedDraft;
75
+ try {
76
+ const pending = item.controller.send('followUp', { text, restore: { text: transcript } });
77
+ const after = item.controller.getSnapshot();
78
+ // Native send synchronously publishes this immutable draft in validation or at its outbox position; equal text is not identity.
79
+ submittedDraft = after.send.kind === 'validating' && (before.send.kind !== 'validating' || after.send.draft !== before.send.draft)
80
+ ? after.send.draft : after.outbox[before.outbox.length];
81
+ const result = await pending;
82
+ if (!valid(item))
83
+ return;
84
+ if (result) {
85
+ remove(item);
86
+ latest.current.admitted?.();
87
+ return;
88
+ }
89
+ }
90
+ catch {
91
+ if (!valid(item))
92
+ return;
93
+ const draft = item.controller.getSnapshot().draft.text;
94
+ if (transcript)
95
+ item.controller.setText(appendToDraft(draft, transcript));
96
+ }
97
+ const native = item.controller.getSnapshot().send;
98
+ if (native.kind === 'admitted' && 'attempt' in native && submittedDraft !== undefined && native.attempt.draft === submittedDraft) {
99
+ remove(item);
100
+ latest.current.admitted?.();
101
+ return;
102
+ }
103
+ const ownUnknown = native.kind === 'unknown' && 'attempt' in native && submittedDraft !== undefined && native.attempt.draft === submittedDraft;
104
+ item.phase = native.kind === 'blocked' || (native.kind === 'unknown' && 'attempt' in native && !ownUnknown) ? 'blocked' : 'unconfirmed';
105
+ if (ownUnknown)
106
+ item.requestId = native.attempt.requestId;
107
+ update();
108
+ };
109
+ const process = async (item) => {
110
+ if (!valid(item) || !item.file)
111
+ return;
112
+ item.phase = 'processing';
113
+ update();
114
+ // Cache each successful half independently: retrying transcription must not upload the same recording again.
115
+ await Promise.all([
116
+ item.path ? undefined : (async () => {
117
+ try {
118
+ const results = await item.upload?.([item.file], item.abort.signal);
119
+ const path = results?.length === 1 ? results[0]?.path : undefined;
120
+ if (valid(item) && path && isVoicePath(path))
121
+ item.path = path;
122
+ }
123
+ catch { /* Failed uploads remain pending for explicit retry. */ }
124
+ })(),
125
+ item.transcript !== undefined ? undefined : (async () => {
126
+ try {
127
+ const text = (await item.transcribe(item.file, item.abort.signal)).trim();
128
+ if (valid(item) && text)
129
+ item.transcript = text;
130
+ }
131
+ catch { /* Never send automatically when transcription fails. */ }
132
+ })(),
133
+ ]);
134
+ if (!valid(item))
135
+ return;
136
+ if (!item.path) {
137
+ item.phase = 'upload-failed';
138
+ update();
139
+ }
140
+ else if (!item.transcript) {
141
+ item.phase = 'transcribe-failed';
142
+ update();
143
+ }
144
+ else
145
+ await send(item);
146
+ };
147
+ const finish = () => {
148
+ const item = active();
149
+ if (!item || !valid(item))
150
+ return;
151
+ if (item.phase === 'starting') {
152
+ cancel();
153
+ return;
154
+ }
155
+ if (item.phase !== 'recording')
156
+ return;
157
+ const draft = item.controller.getSnapshot().draft.text;
158
+ item.mentions = mentionedPaths(draft);
159
+ item.controller.setText(withoutMentions(draft));
160
+ void (async () => {
161
+ const recording = await core.stop(item);
162
+ if (!valid(item))
163
+ return;
164
+ if (!recording) {
165
+ remove(item);
166
+ setProblem(labels.voiceNothingHeard);
167
+ update();
168
+ return;
169
+ }
170
+ const type = recording.mimeType.split(';')[0], ext = type === 'audio/wav' ? 'wav' : type === 'audio/mp4' ? 'mp4' : type === 'audio/ogg' ? 'ogg' : 'webm';
171
+ const stamp = new Date().toISOString().replace(/[-:]/g, '').replace('T', '-').replace(/\.(\d+)Z$/, '-$1');
172
+ item.file = new File([recording.audio], `voice-${stamp}.${ext}`, { type });
173
+ await process(item);
174
+ })();
175
+ };
176
+ const start = () => {
177
+ if (!controller || !config || active() || controller.getSnapshot().disposed || controller.getSnapshot().send.kind === 'unknown')
178
+ return;
179
+ setProblem(undefined);
180
+ core.start({ controller, mentions: [], transcribe: config.transcribe.bind(config), upload: config.upload?.bind(config) ?? attachments?.upload.bind(attachments), target: targetOf(controller) }, config.maxMs);
181
+ };
182
+ // eslint-disable-next-line react-hooks/refs -- the attempts live in a ref that every change pairs with update(); render reads the list as of that update
183
+ const item = active();
184
+ // eslint-disable-next-line react-hooks/refs -- the attempts live in a ref that every change pairs with update(); render reads the list as of that update
185
+ const pending = core.attempts.filter(item => item.phase !== 'starting' && item.phase !== 'recording').map(item => ({
186
+ id: item.id, path: item.path, phase: item.phase, seconds: item.seconds,
187
+ retry: () => { if (item.phase === 'upload-failed' || item.phase === 'transcribe-failed')
188
+ void process(item); },
189
+ audioOnly: item.phase === 'transcribe-failed' && item.path ? () => { if (item.phase === 'transcribe-failed')
190
+ void send(item, true); } : undefined,
191
+ discard: () => { if (valid(item))
192
+ remove(item); },
193
+ }));
194
+ // eslint-disable-next-line react-hooks/refs -- the capture sequence is a ref bumped with update(); render reads it as of that update
195
+ return { canStart: Boolean(controller && config && !item && !controller.getSnapshot().disposed && controller.getSnapshot().send.kind !== 'unknown'), phase: item?.phase ?? 'idle', seconds: item?.seconds ?? 0, levels: item?.levels ?? [], paused: item?.paused ?? false,
196
+ pause: item?.phase === 'recording' && item.canPause ? () => core.pause(item) : undefined,
197
+ resume: item?.phase === 'recording' && item.canPause ? () => core.resume(item) : undefined,
198
+ captureSequence: core.captureSequence, problem, pending, start, finish, cancel };
199
+ }
200
+ /** Selects retained voice messages or text-only dictation without putting send/draft policy in copied shells. */
201
+ export function useChatVoice({ controller, voice, attachments, labels, onDone }) {
202
+ const message = useVoiceMessage({ controller, config: voice?.send === 'message' ? voice : undefined, attachments, labels, admitted: () => onDone('sent') });
203
+ const dictation = useDictation({
204
+ transcribe: voice?.send === 'message' ? undefined : voice?.transcribe, labels, maxMs: voice?.maxMs, target: controller,
205
+ onText: async (spoken, signal) => {
206
+ if (!controller || signal.aborted)
207
+ return;
208
+ const fill = () => {
209
+ if (signal.aborted)
210
+ return;
211
+ controller.setText(appendToDraft(controller.getSnapshot().draft.text, spoken));
212
+ onDone('drafted');
213
+ };
214
+ if (voice?.send !== 'auto') {
215
+ fill();
216
+ return;
217
+ }
218
+ const draft = controller.getSnapshot().draft.text;
219
+ const paths = mentionedPaths(draft);
220
+ controller.setText(withoutMentions(draft));
221
+ try {
222
+ const sent = await controller.send(undefined, { text: [...paths.map(mentionToken), spoken].join(' '), restore: { text: spoken } });
223
+ if (!signal.aborted)
224
+ onDone(sent ? 'sent' : 'drafted');
225
+ }
226
+ catch {
227
+ fill();
228
+ }
229
+ },
230
+ });
231
+ const { cancel } = dictation, send = voice?.send, configured = Boolean(voice);
232
+ useEffect(() => () => cancel(), [controller, send, configured, cancel]);
233
+ const phase = message.phase === 'starting' || message.phase === 'recording' ? message.phase : message.phase === 'processing' || message.phase === 'sending' ? 'transcribing' : 'idle';
234
+ return send === 'message' ? { ...message, phase, levels: message.levels ?? [], paused: message.paused ?? false, pause: message.pause, resume: message.resume } : dictation;
235
+ }
@@ -0,0 +1,7 @@
1
+ export { useVoiceAttempt } from './voice-attempt.js';
2
+ export type { VoiceAttempt, VoiceAttemptOptions } from './voice-attempt.js';
3
+ export { useDictation } from './chat-dictation.js';
4
+ export type { Dictation, DictationPhase, DictationLabels, DictationOptions } from './chat-dictation.js';
5
+ export { useChatVoice, useVoiceMessage } from './chat-voice-message.js';
6
+ export type { ChatVoiceHandle, ChatVoiceOptions, PendingVoice, VoiceMessageConfig, VoiceMessageHandle, VoicePhase, VoiceRecorderOptions } from './chat-voice-message.js';
7
+ //# sourceMappingURL=chat-voice.d.ts.map
@@ -0,0 +1,4 @@
1
+ 'use client';
2
+ export { useVoiceAttempt } from './voice-attempt.js';
3
+ export { useDictation } from './chat-dictation.js';
4
+ export { useChatVoice, useVoiceMessage } from './chat-voice-message.js';
@@ -33,8 +33,8 @@ export declare const layoutProps: {
33
33
  maxElements: z.ZodDefault<z.ZodNumber>;
34
34
  minWidth: z.ZodDefault<z.ZodNumber>;
35
35
  regenerate: z.ZodDefault<z.ZodArray<z.ZodEnum<{
36
- open: "open";
37
36
  phase: "phase";
37
+ open: "open";
38
38
  request: "request";
39
39
  }>>>;
40
40
  prompt: z.ZodOptional<z.ZodString>;
@@ -127,8 +127,8 @@ export declare const experienceCatalog: import("@json-render/core").Catalog<{
127
127
  maxElements: z.ZodDefault<z.ZodNumber>;
128
128
  minWidth: z.ZodDefault<z.ZodNumber>;
129
129
  regenerate: z.ZodDefault<z.ZodArray<z.ZodEnum<{
130
- open: "open";
131
130
  phase: "phase";
131
+ open: "open";
132
132
  request: "request";
133
133
  }>>>;
134
134
  prompt: z.ZodOptional<z.ZodString>;
@@ -13,7 +13,7 @@ export declare function experienceRegion(descriptor: ExperienceDescriptor, name:
13
13
  candidates: string[];
14
14
  maxElements: number;
15
15
  minWidth: number;
16
- regenerate: ("open" | "phase" | "request")[];
16
+ regenerate: ("phase" | "open" | "request")[];
17
17
  kinds?: string[] | undefined;
18
18
  prompt?: string | undefined;
19
19
  };
@@ -0,0 +1,83 @@
1
+ import type { MarkdownEditorMenuLabels } from './viewer-components.js';
2
+ import { type TextDraftLabels } from './text-draft-labels.js';
3
+ /**
4
+ * Every word the markdown editor draws itself. `locale` picks the English or French set; `labels` overrides single keys
5
+ * (for example `{ saved: 'Enregistré', sourceName: title => title }`). Text a host supplies (`title`, `placeholder`, a save
6
+ * result's `reason`) is shown as given.
7
+ */
8
+ export interface MarkdownEditorLabels extends MarkdownEditorMenuLabels, TextDraftLabels {
9
+ readonly formatting: string;
10
+ readonly readOnly: string;
11
+ readonly closed: string;
12
+ readonly saving: string;
13
+ readonly saveUnconfirmed: string;
14
+ readonly changedElsewhere: string;
15
+ readonly unsaved: string;
16
+ readonly saved: string;
17
+ readonly save: string;
18
+ readonly defaultTitle: string;
19
+ readonly defaultPlaceholder: string;
20
+ /** Accessible name of the Markdown source box. */
21
+ readonly sourceName: (title: string) => string;
22
+ /** Accessible name of the rich text surface. */
23
+ readonly richName: (title: string) => string;
24
+ readonly textFormatting: string;
25
+ readonly bold: string;
26
+ readonly italic: string;
27
+ readonly strike: string;
28
+ readonly highlight: string;
29
+ readonly code: string;
30
+ readonly heading: (level: 1 | 2 | 3) => string;
31
+ readonly bulletList: string;
32
+ readonly numberedList: string;
33
+ readonly taskList: string;
34
+ readonly quote: string;
35
+ readonly codeBlock: string;
36
+ readonly link: string;
37
+ readonly table: string;
38
+ readonly horizontalRule: string;
39
+ readonly addTableRow: string;
40
+ readonly addTableColumn: string;
41
+ readonly deleteTable: string;
42
+ readonly linkAddress: string;
43
+ readonly applyLink: string;
44
+ readonly removeLink: string;
45
+ readonly cancel: string;
46
+ readonly documentOutline: string;
47
+ readonly goToHeading: (text: string) => string;
48
+ readonly untitledHeading: string;
49
+ readonly headingNotSelected: string;
50
+ readonly uncertainNotice: string;
51
+ readonly checkSaveOutcome: string;
52
+ readonly stopChecking: string;
53
+ readonly conflictNotice: string;
54
+ readonly checkSavedVersion: string;
55
+ readonly discardLocal: string;
56
+ readonly conflictFallback: string;
57
+ readonly saveFallback: string;
58
+ readonly saveFailed: string;
59
+ readonly operationFailed: string;
60
+ /** The source-only notice. `reasons` are the kit's English reason names; the French set translates the known ones. */
61
+ readonly richOff: (reasons: readonly string[]) => string;
62
+ readonly preservedBlockName: (kind: string) => string;
63
+ readonly frontMatterCode: string;
64
+ readonly proposedLineChanges: string;
65
+ readonly diffFallback: string;
66
+ readonly noTextChanges: string;
67
+ readonly crlfLineEnding: string;
68
+ readonly noNewline: string;
69
+ readonly showUnchangedLines: (count: number) => string;
70
+ readonly hideUnchangedLines: (count: number) => string;
71
+ readonly proposedEdit: string;
72
+ readonly reviewChanges: string;
73
+ readonly exactBeforeAfter: string;
74
+ readonly before: string;
75
+ readonly after: string;
76
+ readonly adopted: string;
77
+ readonly acceptAndSave: string;
78
+ readonly dismissProposal: string;
79
+ }
80
+ export type MarkdownEditorLabelOverrides = Partial<MarkdownEditorLabels>;
81
+ export declare const markdownEditorLabels: Record<'en' | 'fr', MarkdownEditorLabels>;
82
+ export declare function resolveMarkdownEditorLabels(locale: 'en' | 'fr', overrides?: MarkdownEditorLabelOverrides): MarkdownEditorLabels;
83
+ //# sourceMappingURL=markdown-editor-labels.d.ts.map
@@ -0,0 +1,75 @@
1
+ import { textDraftLabels } from './text-draft-labels.js';
2
+ const frenchBlocks = { 'Front matter': 'Métadonnées', Footnote: 'Note de bas de page', Reference: 'Référence' };
3
+ const frenchReasons = {
4
+ 'bare carriage returns': 'des retours chariot isolés',
5
+ 'mixed line endings': 'des fins de ligne mélangées',
6
+ 'NUL characters': 'des caractères NUL',
7
+ 'blocks whose source spans the rich editor cannot retain': 'des blocs dont la source ne peut pas être conservée à l’identique',
8
+ 'content the rich editor cannot read': 'un contenu que l’éditeur mis en forme ne sait pas lire',
9
+ };
10
+ export const markdownEditorLabels = {
11
+ en: {
12
+ ...textDraftLabels.en,
13
+ moreActions: 'More actions', editorMode: 'Editor mode', formatted: 'Formatted', markdown: 'Markdown', showOutline: 'Show outline',
14
+ formatting: 'Formatting', readOnly: 'Read only', closed: 'Closed', saving: 'Saving…', saveUnconfirmed: 'Save unconfirmed',
15
+ changedElsewhere: 'Changed elsewhere', unsaved: 'Unsaved changes', saved: 'Saved', save: 'Save',
16
+ defaultTitle: 'Document', defaultPlaceholder: 'Start writing',
17
+ sourceName: title => `${title} Markdown source`, richName: title => `${title} rich text`, textFormatting: 'Text formatting',
18
+ bold: 'Bold', italic: 'Italic', strike: 'Strikethrough', highlight: 'Highlight', code: 'Code',
19
+ heading: level => `Heading ${level}`, bulletList: 'Bullet list', numberedList: 'Numbered list', taskList: 'Task list',
20
+ quote: 'Quote', codeBlock: 'Code block', link: 'Link', table: 'Table', horizontalRule: 'Horizontal rule',
21
+ addTableRow: 'Add table row', addTableColumn: 'Add table column', deleteTable: 'Delete table',
22
+ linkAddress: 'Link address', applyLink: 'Apply link', removeLink: 'Remove link', cancel: 'Cancel',
23
+ documentOutline: 'Document outline', goToHeading: text => `Go to heading: ${text}`, untitledHeading: 'Untitled heading',
24
+ headingNotSelected: 'The heading could not be selected',
25
+ uncertainNotice: 'Save acknowledgement was lost. Keep editing while its outcome is checked.',
26
+ checkSaveOutcome: 'Check save outcome', stopChecking: 'Stop checking and refresh, keeping my draft',
27
+ conflictNotice: 'The saved document changed. Your local text has been kept.',
28
+ checkSavedVersion: 'Check saved version', discardLocal: 'Discard local changes and reload',
29
+ conflictFallback: 'The document changed elsewhere. Your text is still available.',
30
+ saveFallback: 'The document could not be saved. Your text is still available.',
31
+ saveFailed: 'The save could not be completed. Your text is still available.',
32
+ operationFailed: 'The operation could not be completed. Your text is still available.',
33
+ richOff: reasons => `Rich editing is off because this document contains ${reasons.join(', ')}, which it could not keep exactly. You can still edit the Markdown source.`,
34
+ preservedBlockName: kind => kind, frontMatterCode: 'Front matter code',
35
+ proposedLineChanges: 'Proposed line changes', diffFallback: 'Detailed line alignment unavailable; showing changed block.',
36
+ noTextChanges: 'No text changes', crlfLineEnding: 'CRLF line ending', noNewline: 'No newline at end',
37
+ showUnchangedLines: count => `Show ${count} unchanged lines`, hideUnchangedLines: count => `Hide ${count} unchanged lines`,
38
+ proposedEdit: 'Proposed edit', reviewChanges: 'Review text changes', exactBeforeAfter: 'Exact before and after', before: 'Before', after: 'After',
39
+ adopted: 'Adopted', acceptAndSave: 'Accept and save', dismissProposal: 'Dismiss proposal',
40
+ },
41
+ fr: {
42
+ ...textDraftLabels.fr,
43
+ moreActions: 'Plus d’actions', editorMode: 'Mode d’édition', formatted: 'Mis en forme', markdown: 'Markdown', showOutline: 'Afficher le plan',
44
+ formatting: 'Mise en forme', readOnly: 'Lecture seule', closed: 'Fermé', saving: 'Enregistrement…', saveUnconfirmed: 'Enregistrement non confirmé',
45
+ changedElsewhere: 'Modifié ailleurs', unsaved: 'Modifications non enregistrées', saved: 'Enregistré', save: 'Enregistrer',
46
+ defaultTitle: 'Document', defaultPlaceholder: 'Commencez à écrire',
47
+ sourceName: title => `${title}, source Markdown`, richName: title => `${title}, texte mis en forme`, textFormatting: 'Mise en forme du texte',
48
+ bold: 'Gras', italic: 'Italique', strike: 'Barré', highlight: 'Surligné', code: 'Code',
49
+ heading: level => `Titre ${level}`, bulletList: 'Liste à puces', numberedList: 'Liste numérotée', taskList: 'Liste de tâches',
50
+ quote: 'Citation', codeBlock: 'Bloc de code', link: 'Lien', table: 'Tableau', horizontalRule: 'Ligne de séparation',
51
+ addTableRow: 'Ajouter une ligne', addTableColumn: 'Ajouter une colonne', deleteTable: 'Supprimer le tableau',
52
+ linkAddress: 'Adresse du lien', applyLink: 'Appliquer le lien', removeLink: 'Retirer le lien', cancel: 'Annuler',
53
+ documentOutline: 'Plan du document', goToHeading: text => `Aller au titre : ${text}`, untitledHeading: 'Titre sans nom',
54
+ headingNotSelected: 'Le titre n’a pas pu être sélectionné',
55
+ uncertainNotice: 'L’accusé d’enregistrement a été perdu. Continuez à écrire pendant que le résultat est vérifié.',
56
+ checkSaveOutcome: 'Vérifier l’enregistrement', stopChecking: 'Arrêter la vérification et actualiser, en gardant mon brouillon',
57
+ conflictNotice: 'Le document enregistré a changé. Votre texte local a été conservé.',
58
+ checkSavedVersion: 'Vérifier la version enregistrée', discardLocal: 'Abandonner mes modifications et recharger',
59
+ conflictFallback: 'Le document a été modifié ailleurs. Votre texte est toujours disponible.',
60
+ saveFallback: 'Le document n’a pas pu être enregistré. Votre texte est toujours disponible.',
61
+ saveFailed: 'L’enregistrement n’a pas pu aboutir. Votre texte est toujours disponible.',
62
+ operationFailed: 'L’opération n’a pas pu aboutir. Votre texte est toujours disponible.',
63
+ richOff: reasons => `La mise en forme est désactivée car ce document contient ${reasons.map(reason => frenchReasons[reason] ?? reason).join(', ')}, qu’elle ne saurait pas conserver à l’identique. Vous pouvez toujours modifier la source Markdown.`,
64
+ preservedBlockName: kind => frenchBlocks[kind] ?? kind,
65
+ frontMatterCode: 'Code des métadonnées', proposedLineChanges: 'Changements de lignes proposés',
66
+ diffFallback: 'Alignement détaillé indisponible ; affichage du bloc modifié.', noTextChanges: 'Aucun changement de texte',
67
+ crlfLineEnding: 'Fin de ligne CRLF', noNewline: 'Aucun saut de ligne à la fin',
68
+ showUnchangedLines: count => `Afficher ${count} lignes inchangées`, hideUnchangedLines: count => `Masquer ${count} lignes inchangées`,
69
+ proposedEdit: 'Modification proposée', reviewChanges: 'Examiner les changements', exactBeforeAfter: 'Avant et après, à l’identique', before: 'Avant', after: 'Après',
70
+ adopted: 'Adoptée', acceptAndSave: 'Accepter et enregistrer', dismissProposal: 'Ignorer la proposition',
71
+ },
72
+ };
73
+ export function resolveMarkdownEditorLabels(locale, overrides) {
74
+ return overrides ? { ...markdownEditorLabels[locale], ...overrides } : markdownEditorLabels[locale];
75
+ }
@@ -9,10 +9,14 @@ export type { MarkdownAutosave, MarkdownFlush } from './markdown-autosave.js';
9
9
  import type { MarkdownController } from './markdown.js';
10
10
  import { type MarkdownImageResolver } from './markdown-extensions.js';
11
11
  import { type MarkdownRichSafety } from './markdown-safety.js';
12
+ import { type MarkdownEditorLabelOverrides } from './markdown-editor-labels.js';
12
13
  import { type MarkdownMountedTools } from './markdown-mounted.js';
13
14
  export { checkMarkdownRichSafety } from './markdown-safety.js';
14
15
  export type { MarkdownRichSafety } from './markdown-safety.js';
15
16
  export type { MarkdownImageResolver } from './markdown-extensions.js';
17
+ export { markdownEditorLabels } from './markdown-editor-labels.js';
18
+ export type { MarkdownEditorLabels, MarkdownEditorLabelOverrides } from './markdown-editor-labels.js';
19
+ export type { TextDraftLabels } from './text-draft-labels.js';
16
20
  export type { MarkdownMountedTools, MarkdownMountedSubject, MarkdownMountedSelection, MarkdownMountedInspection } from './markdown-mounted.js';
17
21
  export interface MarkdownEditorProps {
18
22
  readonly highlights?: readonly MarkdownDecoration[];
@@ -44,8 +48,10 @@ export interface MarkdownEditorProps {
44
48
  readonly onSaveResult?: (result: SaveResult) => void;
45
49
  /** Selection formatting by default. Pinned shows the full row on desktop or above the phone keyboard while editing. */
46
50
  readonly formattingToolbar?: 'selection' | 'pinned';
47
- /** Locale of the built-in view menu. */
51
+ /** Language of everything the editor draws itself: status, alerts, buttons, accessible names. */
48
52
  readonly locale?: 'en' | 'fr';
53
+ /** Overrides single words of the `locale` set, e.g. `{ saved: 'Enregistré', sourceName: title => title }`. */
54
+ readonly labels?: MarkdownEditorLabelOverrides;
49
55
  }
50
56
  /** Borrows the controller. The host owns document selection, access and disposal. */
51
57
  export declare function MarkdownEditor(props: MarkdownEditorProps): import("react").JSX.Element;