smartrte-react 1.0.0-beta.5 → 1.0.0-beta.8

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.
@@ -19,8 +19,10 @@ import { SpecialCharacterPopover } from "./SpecialCharacterPopover.js";
19
19
  import { TableBorderPopover, BORDER_WIDTH_PRESETS } from "./TableBorderPopover.js";
20
20
  import { TableResizeHandles } from "./TableResizeHandles.js";
21
21
  import { MediaOverlay } from "./MediaOverlay.js";
22
+ import { MediaDetailsPopover } from "./MediaDetailsPopover.js";
22
23
  import { ContextMenu } from "./ContextMenu.js";
23
24
  import { ToolbarButton, ToolbarDropdown, ToolbarGroup, ToolbarMenuItem, MobileMoreMenu } from "./ToolbarPrimitives.js";
25
+ import { resolveToolbarTools } from "../toolbarTools.js";
24
26
  import { exportDocxDocument, importStyledDocxDocument, importPdfDocument } from "smartrte-core/foundation";
25
27
  import { printSmartDocumentAsPdf } from "../adapters/pdfPrint.js";
26
28
  import { CanonicalEditorRuntime, } from "../canonicalEditorRuntime.js";
@@ -102,7 +104,7 @@ const parseBorderShorthand = (value) => {
102
104
  const isCollapsedTextSelection = (selection) => selection.type === "text" && selection.anchor.offset === selection.head.offset
103
105
  && selection.anchor.path.length === selection.head.path.length
104
106
  && selection.anchor.path.every((part, index) => part === selection.head.path[index]);
105
- export const CanonicalAuthorityEditor = forwardRef(function CanonicalAuthorityEditor({ defaultValue, preset, renderFormulaHtml, onChange, onHtmlChange, onClipboardDiagnostic, mediaProvider, mediaPicker: MediaPicker = DefaultMediaPicker, mediaManager = true, versionProvider, commentProvider, suggestionProvider, authorId, placeholder = "Type here…", minHeight = 200, maxHeight = 500, readOnly = false, className, onRuntime, }, forwardedRef) {
107
+ export const CanonicalAuthorityEditor = forwardRef(function CanonicalAuthorityEditor({ defaultValue, preset, renderFormulaHtml, onChange, onHtmlChange, onClipboardDiagnostic, mediaProvider, mediaPicker: MediaPicker = DefaultMediaPicker, mediaManager = true, tools, versionProvider, commentProvider, suggestionProvider, authorId, placeholder = "Type here…", minHeight = 200, maxHeight = 500, readOnly = false, className, onRuntime, }, forwardedRef) {
106
108
  ensureStyleSheet();
107
109
  const rootRef = useRef(null);
108
110
  const importRef = useRef(null);
@@ -129,6 +131,7 @@ export const CanonicalAuthorityEditor = forwardRef(function CanonicalAuthorityEd
129
131
  const [recentColors, setRecentColors] = useState({ text: [], background: [], border: [] });
130
132
  const [tableSizePopover, setTableSizePopover] = useState(null);
131
133
  const [tableBorderPopover, setTableBorderPopover] = useState(null);
134
+ const [mediaDetailsPopover, setMediaDetailsPopover] = useState(null);
132
135
  const borderPreviewCheckpointRef = useRef(null);
133
136
  const [formulaLibraryPopover, setFormulaLibraryPopover] = useState(null);
134
137
  const [specialCharPopover, setSpecialCharPopover] = useState(null);
@@ -140,6 +143,8 @@ export const CanonicalAuthorityEditor = forwardRef(function CanonicalAuthorityEd
140
143
  // since nothing was ever applied to the model).
141
144
  const colorPreviewCheckpointRef = useRef(null);
142
145
  const [contextMenu, setContextMenu] = useState(null);
146
+ const [mediaContextMenu, setMediaContextMenu] = useState(null);
147
+ const [mediaResizeTarget, setMediaResizeTarget] = useState(null);
143
148
  // Tracks the exact caret position (path+offset) the link overlay was
144
149
  // last dismissed at (Escape/outside click) - suppresses an instant
145
150
  // re-popup while the caret hasn't moved, without needing a separate
@@ -320,6 +325,19 @@ export const CanonicalAuthorityEditor = forwardRef(function CanonicalAuthorityEd
320
325
  // an `unknown` node (Phase 10's disable-safety contract, unchanged) -
321
326
  // this only hides the *toolbar affordance* for creating new ones.
322
327
  const tablesEnabled = Boolean(runtime.editor.schema.nodes.table);
328
+ // Per-tool toolbar visibility (toolbarTools.ts) - `tools` is a pure
329
+ // UI-visibility layer, composed with (never overriding) the deeper
330
+ // existing gates below: a required provider being absent, or a plugin
331
+ // having been excluded via `preset`, still wins even if a tool is
332
+ // explicitly left enabled here.
333
+ const t = resolveToolbarTools(tools);
334
+ const showInsertTable = t.insertTable && tablesEnabled;
335
+ const showImageTool = t.image && Boolean(mediaProvider);
336
+ const showVideoTool = t.video && Boolean(mediaProvider);
337
+ const showAudioTool = t.audio && Boolean(mediaProvider);
338
+ const showVersionHistoryTool = t.versionHistory && Boolean(versionProvider);
339
+ const showCommentsTool = t.comments && Boolean(commentProvider);
340
+ const showSuggestionsTool = t.suggestions && Boolean(suggestionProvider);
323
341
  const tableSelected = currentTableScope.kind === "table-grid";
324
342
  const selectedTableElement = tableSelected
325
343
  ? runtime.surface.renderer?.mapping.nodeToDom(currentTableScope.tableId)
@@ -348,6 +366,15 @@ export const CanonicalAuthorityEditor = forwardRef(function CanonicalAuthorityEd
348
366
  // node.type.includes("formula") for its own correct "Formula source"
349
367
  // prompt.
350
368
  const resizableAtomSelected = mediaAtomSelected && selectedAtomNode?.type !== "formula" && selectedAtomNode?.type !== "block_formula";
369
+ const selectedAtomId = atomSelected && "nodeId" in currentAtomScope ? currentAtomScope.nodeId : null;
370
+ useEffect(() => {
371
+ if (mediaContextMenu && (!mediaAtomSelected || mediaContextMenu.nodeId !== selectedAtomId))
372
+ setMediaContextMenu(null);
373
+ }, [mediaContextMenu, mediaAtomSelected, selectedAtomId]);
374
+ useEffect(() => {
375
+ if (mediaResizeTarget && (!mediaAtomSelected || mediaResizeTarget.nodeId !== selectedAtomId))
376
+ setMediaResizeTarget(null);
377
+ }, [mediaResizeTarget, mediaAtomSelected, selectedAtomId]);
351
378
  // Context menu scope reduction: link no longer gets a right-click menu -
352
379
  // LinkEditorPopover (already built and wired for the toolbar's Link
353
380
  // button, per §1's investigation) auto-appears, anchored to the link's
@@ -803,13 +830,10 @@ export const CanonicalAuthorityEditor = forwardRef(function CanonicalAuthorityEd
803
830
  * the same way Tab special-cases list.indent/outdent's dynamic params.
804
831
  */
805
832
  /**
806
- * Context menu scope reduction: table-only now. Marks (already on the
807
- * toolbar) and atom/link (moved to their own dedicated overlays -
808
- * MediaOverlay below, and the auto-triggered LinkEditorPopover - see
809
- * linkAnchorElement) no longer register contextMenu contributions at
810
- * all (marks/plugin.ts, atom/plugin.ts) or get hardcoded item blocks
811
- * here - only table plugin contributions (the generic loop) and the
812
- * cell-colour items remain.
833
+ * The generic plugin context menu remains table-scoped. Images use their
834
+ * direct right-click details editor below, other media keeps its dedicated
835
+ * quick-action menu, while marks stay on the toolbar and links use the
836
+ * auto-triggered LinkEditorPopover.
813
837
  */
814
838
  const resolveContextMenuItems = () => {
815
839
  const many = () => ids(128);
@@ -1028,6 +1052,19 @@ export const CanonicalAuthorityEditor = forwardRef(function CanonicalAuthorityEd
1028
1052
  insertBlockAtom("block_image", {
1029
1053
  src: item.url, alt: item.alt || item.title || "Image", decorative: false, status: "ready",
1030
1054
  ...(item.width ? { width: item.width } : {}), ...(item.height ? { height: item.height } : {}),
1055
+ // docs/bugs/media-details-old-editor-field-parity.md: item.license was
1056
+ // already fetched and shown in MediaManager's own library-browser info
1057
+ // panel (MediaManager.tsx), but had nowhere to persist once inserted -
1058
+ // the atom schema had no license attrs at all, so this real,
1059
+ // already-available data was silently discarded at exactly this call
1060
+ // site. `licenseVersion` has no MediaItem.license equivalent (its
1061
+ // shape has no separate version field) - left for the user to fill in
1062
+ // via the media details panel, same as a freshly uploaded image.
1063
+ ...(item.license?.workName || item.license?.licenseText
1064
+ ? { licenseDescription: [item.license.workName, item.license.licenseText].filter(Boolean).join(" - ") } : {}),
1065
+ ...(item.license?.sourceUrl ? { licenseSourceUrl: item.license.sourceUrl } : {}),
1066
+ ...(item.license?.licenseType ? { licenseType: item.license.licenseType } : {}),
1067
+ ...(item.license?.author ? { licenseAttribution: item.license.author } : {}),
1031
1068
  }, createNodeId());
1032
1069
  };
1033
1070
  const commentAuthorId = authorId || "anonymous";
@@ -1211,6 +1248,27 @@ export const CanonicalAuthorityEditor = forwardRef(function CanonicalAuthorityEd
1211
1248
  runtime.focus();
1212
1249
  setLinkPopover(null);
1213
1250
  };
1251
+ const openMediaDetails = (scope, node, element) => {
1252
+ if (scope.kind !== "atomic-node")
1253
+ return;
1254
+ const rect = (element || selectedAtomElement)?.getBoundingClientRect();
1255
+ setMediaDetailsPopover({
1256
+ x: rect ? rect.left : 0, y: rect ? rect.bottom + 6 : 0, scope,
1257
+ initial: {
1258
+ alt: String(node.attrs?.alt || ""),
1259
+ href: String(node.attrs?.href || ""),
1260
+ openInNewTab: node.attrs?.target === "_blank",
1261
+ width: typeof node.attrs?.width === "number" ? node.attrs.width : undefined,
1262
+ borderRadius: typeof node.attrs?.borderRadius === "number" ? node.attrs.borderRadius : undefined,
1263
+ align: node.attrs?.align === "left" || node.attrs?.align === "center" || node.attrs?.align === "right" ? node.attrs.align : undefined,
1264
+ licenseDescription: String(node.attrs?.licenseDescription || ""),
1265
+ licenseSourceUrl: String(node.attrs?.licenseSourceUrl || ""),
1266
+ licenseType: String(node.attrs?.licenseType || ""),
1267
+ licenseVersion: String(node.attrs?.licenseVersion || ""),
1268
+ licenseAttribution: String(node.attrs?.licenseAttribution || ""),
1269
+ },
1270
+ });
1271
+ };
1214
1272
  const editSelectedAtom = (resizeBy) => {
1215
1273
  const scope = atomScope();
1216
1274
  if (scope.kind !== "atomic-node")
@@ -1218,16 +1276,59 @@ export const CanonicalAuthorityEditor = forwardRef(function CanonicalAuthorityEd
1218
1276
  const node = findNode(runtime.editor.document, scope.nodeId);
1219
1277
  if (!node)
1220
1278
  return;
1279
+ if (resizeBy === undefined && !node.type.includes("formula")) {
1280
+ // Images: the old window.prompt("Alt text") one-field edit is
1281
+ // replaced by the full Media details panel (docs/bugs/
1282
+ // media-details-old-editor-field-parity.md) - formulas keep their
1283
+ // existing prompt unchanged below, since this panel's fields
1284
+ // (link/align/radius/license) don't apply to a formula atom at all.
1285
+ openMediaDetails(scope, node);
1286
+ return;
1287
+ }
1221
1288
  const operations = resizeBy === undefined
1222
- ? updateAtom(runtime.editor.document, scope, node.type.includes("formula")
1223
- ? { attrs: { source: window.prompt("Formula source", String(node.attrs?.source || "")) || node.attrs?.source } }
1224
- : { attrs: { alt: window.prompt("Alt text", String(node.attrs?.alt || "")) ?? node.attrs?.alt } }, blockContext())
1289
+ ? updateAtom(runtime.editor.document, scope, { attrs: { source: window.prompt("Formula source", String(node.attrs?.source || "")) || node.attrs?.source } }, blockContext())
1225
1290
  : resizeAtom(runtime.editor.document, scope, {
1226
1291
  width: Math.max(16, Number(node.attrs?.width || 160) + resizeBy),
1227
1292
  height: Math.max(16, Number(node.attrs?.height || 90) + resizeBy),
1228
1293
  }, blockContext());
1229
1294
  runtime.executeOperations(operations, resizeBy === undefined ? {} : { historyGroup: `resize-${scope.nodeId}` });
1230
1295
  };
1296
+ const applyMediaDetails = (draft) => {
1297
+ if (!mediaDetailsPopover)
1298
+ return;
1299
+ const { scope } = mediaDetailsPopover;
1300
+ if (scope.kind !== "atomic-node") {
1301
+ setMediaDetailsPopover(null);
1302
+ return;
1303
+ }
1304
+ const node = findNode(runtime.editor.document, scope.nodeId);
1305
+ const currentWidth = typeof node?.attrs?.width === "number" ? node.attrs.width : undefined;
1306
+ const currentHeight = typeof node?.attrs?.height === "number" ? node.attrs.height : undefined;
1307
+ // Width preserves the original aspect ratio (matches the drag handle
1308
+ // and Resize +/- buttons' own behavior) rather than exposing a separate
1309
+ // Height field the old editor's own screenshot didn't have either.
1310
+ const resizedHeight = draft.width && currentWidth && currentHeight
1311
+ ? Math.round((draft.width * currentHeight) / currentWidth)
1312
+ : currentHeight;
1313
+ setMediaDetailsPopover(null);
1314
+ runtime.executeOperations(updateAtom(runtime.editor.document, scope, {
1315
+ attrs: {
1316
+ alt: draft.alt,
1317
+ href: draft.href || undefined,
1318
+ target: draft.href && draft.openInNewTab ? "_blank" : undefined,
1319
+ width: draft.width ?? currentWidth,
1320
+ height: draft.width ? resizedHeight : currentHeight,
1321
+ borderRadius: draft.borderRadius || undefined,
1322
+ align: draft.align,
1323
+ licenseDescription: draft.licenseDescription || undefined,
1324
+ licenseSourceUrl: draft.licenseSourceUrl || undefined,
1325
+ licenseType: draft.licenseType || undefined,
1326
+ licenseVersion: draft.licenseVersion || undefined,
1327
+ licenseAttribution: draft.licenseAttribution || undefined,
1328
+ },
1329
+ }, blockContext()));
1330
+ runtime.focus();
1331
+ };
1231
1332
  /** MediaOverlay's drag-corner-handle commit - explicit width/height rather than editSelectedAtom's fixed +/-20 increment. */
1232
1333
  const resizeSelectedAtomTo = (width, height) => {
1233
1334
  const scope = atomScope();
@@ -1326,23 +1427,44 @@ export const CanonicalAuthorityEditor = forwardRef(function CanonicalAuthorityEd
1326
1427
  // twice; below 1440px (including mobile) this copy is what's reachable,
1327
1428
  // same as before. "Code" isn't promoted - see the docs/bugs/ writeup on
1328
1429
  // why it stayed the dropdown's sole remaining item at wide widths.
1329
- const textStylesMenuItems = _jsxs(_Fragment, { children: [_jsx(ToolbarMenuItem, { icon: "code", label: "Code", pressed: markCoverage("inlineCode"), disabled: readOnly, onClick: () => toggleMark("inlineCode") }), _jsx(ToolbarMenuItem, { icon: "superscript", label: "Superscript", pressed: markCoverage("superscript"), disabled: readOnly, onClick: () => toggleMark("superscript"), widePromote: true }), _jsx(ToolbarMenuItem, { icon: "subscript", label: "Subscript", pressed: markCoverage("subscript"), disabled: readOnly, onClick: () => toggleMark("subscript"), widePromote: true }), _jsx(ToolbarMenuItem, { icon: "textColor", label: "Text colour", disabled: readOnly, widePromote: true, onClick: (event) => {
1430
+ const textStylesMenuItems = _jsxs(_Fragment, { children: [t.code && _jsx(ToolbarMenuItem, { icon: "code", label: "Code", pressed: markCoverage("inlineCode"), disabled: readOnly, onClick: () => toggleMark("inlineCode") }), t.superscript && _jsx(ToolbarMenuItem, { icon: "superscript", label: "Superscript", pressed: markCoverage("superscript"), disabled: readOnly, onClick: () => toggleMark("superscript"), widePromote: true }), t.subscript && _jsx(ToolbarMenuItem, { icon: "subscript", label: "Subscript", pressed: markCoverage("subscript"), disabled: readOnly, onClick: () => toggleMark("subscript"), widePromote: true }), t.textColor && _jsx(ToolbarMenuItem, { icon: "textColor", label: "Text colour", disabled: readOnly, widePromote: true, onClick: (event) => {
1330
1431
  const rect = event.currentTarget.getBoundingClientRect();
1331
1432
  setColorPopover({ x: rect.left, y: rect.bottom + 4, target: { kind: "mark", markId: "textColor" }, initialValue: currentMarkColor("textColor") });
1332
- } }), _jsx(ToolbarMenuItem, { icon: "backgroundColor", label: "Background colour", disabled: readOnly, widePromote: true, onClick: (event) => {
1433
+ } }), t.backgroundColor && _jsx(ToolbarMenuItem, { icon: "backgroundColor", label: "Background colour", disabled: readOnly, widePromote: true, onClick: (event) => {
1333
1434
  const rect = event.currentTarget.getBoundingClientRect();
1334
1435
  setColorPopover({ x: rect.left, y: rect.bottom + 4, target: { kind: "mark", markId: "backgroundColor" }, initialValue: currentMarkColor("backgroundColor") });
1335
- } }), _jsx(ToolbarMenuItem, { icon: "fontSize", label: "Font size", disabled: readOnly, onClick: () => applyAttributedMark("fontSize"), widePromote: true }), _jsx(ToolbarMenuItem, { icon: "fontFamily", label: "Font family", disabled: readOnly, onClick: () => applyAttributedMark("fontFamily"), widePromote: true })] });
1436
+ } }), t.fontSize && _jsx(ToolbarMenuItem, { icon: "fontSize", label: "Font size", disabled: readOnly, onClick: () => applyAttributedMark("fontSize"), widePromote: true }), t.fontFamily && _jsx(ToolbarMenuItem, { icon: "fontFamily", label: "Font family", disabled: readOnly, onClick: () => applyAttributedMark("fontFamily"), widePromote: true })] });
1437
+ const showMoreTextStyles = t.code || t.superscript || t.subscript || t.textColor || t.backgroundColor || t.fontSize || t.fontFamily;
1336
1438
  const paragraphToolsMenuItems = _jsxs(_Fragment, { children: [_jsx(ToolbarMenuItem, { icon: "moveUp", label: "Move block up", disabled: readOnly, onClick: () => runBlock("up") }), _jsx(ToolbarMenuItem, { icon: "moveDown", label: "Move block down", disabled: readOnly, onClick: () => runBlock("down") }), _jsx(ToolbarMenuItem, { icon: "indent", label: "Indent block", disabled: readOnly, onClick: () => runBlock("indent") }), _jsx(ToolbarMenuItem, { icon: "outdent", label: "Outdent block", disabled: readOnly, onClick: () => runBlock("outdent") })] });
1439
+ /**
1440
+ * Named multi-level marker presets (decimal/lower-alpha/upper-alpha/
1441
+ * roman/outline) - a distinct control from the plain Bulleted/Numbered/
1442
+ * Checklist buttons above, which only toggle a raw list style. Extracted
1443
+ * to a variable so it can be reused verbatim in both the desktop "More
1444
+ * list tools" dropdown and MobileMoreMenu below - it was previously
1445
+ * hardcoded only into the dropdown, meaning a narrow/mobile viewport
1446
+ * (where every ToolbarDropdown hides entirely, per theme.ts's
1447
+ * 639px breakpoint) had no way to reach it at all - see
1448
+ * docs/bugs/list-preset-missing-from-mobile-more-menu.md.
1449
+ */
1450
+ const listPresetSelect = t.listPreset && _jsx("div", { style: { padding: "4px 8px" }, children: _jsxs("select", { "aria-label": "List preset", title: "List type / preset", disabled: readOnly || currentListParts.length !== 1, value: currentListPreset, onChange: (event) => {
1451
+ const preset = event.target.value;
1452
+ if (!preset || currentListParts.length !== 1)
1453
+ return;
1454
+ // A preset choice applies to the whole list, regardless of how deep
1455
+ // the cursor is nested — see outermostListId.
1456
+ const rootId = outermostListId(currentListParts[0].listId);
1457
+ runtime.executeOperations(setListPreset(runtime.editor.document, { ...currentListParts[0], listId: rootId }, { preset }, blockContext()), { preserveSelectionById: true });
1458
+ }, style: { width: "100%" }, children: [_jsx("option", { value: "", children: "List preset" }), SMART_LIST_PRESETS.map((preset) => _jsx("option", { value: preset.id, children: preset.kind === "bullet" ? `Bullet · ${preset.label}` : `Number · ${preset.label}` }, preset.id))] }) });
1337
1459
  const listToolsMenuItems = _jsxs(_Fragment, { children: [_jsx(ToolbarMenuItem, { icon: "checkSquare", label: "Check selected items", pressed: currentListScope.kind === "list-selection" && currentListScope.items.every((item) => findNode(runtime.editor.document, item.itemId)?.attrs?.checked === true), disabled: readOnly || currentList?.attrs?.checkable !== true, onClick: toggleCheckedItems }), _jsx(ToolbarMenuItem, { icon: "indent", label: "Indent list item", disabled: readOnly || !canIndent, onClick: () => runList("indent") }), _jsx(ToolbarMenuItem, { icon: "outdent", label: "Outdent list item", disabled: readOnly || currentListParts.length === 0, onClick: () => runList("outdent") }), _jsx(ToolbarMenuItem, { icon: "moveUp", label: "Move item up", disabled: readOnly || !canMoveUp, onClick: () => runList("up") }), _jsx(ToolbarMenuItem, { icon: "moveDown", label: "Move item down", disabled: readOnly || !canMoveDown, onClick: () => runList("down") }), _jsx(ToolbarMenuItem, { icon: "restart", label: "Restart numbering", disabled: readOnly || !orderedList, onClick: restartNumbering }), _jsx(ToolbarMenuItem, { icon: "continueNumbering", label: "Continue numbering", disabled: readOnly || !orderedList, onClick: () => runtime.executeOperations(continueListNumbering(runtime.editor.document, currentListScope, {}, blockContext()), { preserveSelectionById: true }) })] });
1338
1460
  // Remove link/Insert formula/Special characters also get a standalone
1339
1461
  // widePromote ToolbarButton (see textStylesMenuItems's own comment above
1340
1462
  // for the mechanism) - Insert video/audio and the selected-media actions
1341
1463
  // stay dropdown-only, matching the report's own named tool list.
1342
- const insertMoreMenuItems = _jsxs(_Fragment, { children: [_jsx(ToolbarMenuItem, { icon: "unlink", label: "Remove link", disabled: readOnly, onClick: removeLink, widePromote: true }), _jsx(ToolbarMenuItem, { icon: "video", label: "Insert video", disabled: readOnly || !mediaProvider, onClick: () => setMediaKind("video") }), _jsx(ToolbarMenuItem, { icon: "audio", label: "Insert audio", disabled: readOnly || !mediaProvider, onClick: () => setMediaKind("audio") }), _jsx(ToolbarMenuItem, { icon: "formula", label: "Insert formula", disabled: readOnly, widePromote: true, onClick: (event) => {
1464
+ const insertMoreMenuItems = _jsxs(_Fragment, { children: [t.removeLink && _jsx(ToolbarMenuItem, { icon: "unlink", label: "Remove link", disabled: readOnly, onClick: removeLink, widePromote: true }), showVideoTool && _jsx(ToolbarMenuItem, { icon: "video", label: "Insert video", disabled: readOnly, onClick: () => setMediaKind("video") }), showAudioTool && _jsx(ToolbarMenuItem, { icon: "audio", label: "Insert audio", disabled: readOnly, onClick: () => setMediaKind("audio") }), t.insertFormula && _jsx(ToolbarMenuItem, { icon: "formula", label: "Insert formula", disabled: readOnly, widePromote: true, onClick: (event) => {
1343
1465
  const rect = event.currentTarget.getBoundingClientRect();
1344
1466
  setFormulaLibraryPopover({ x: rect.left, y: rect.bottom + 4 });
1345
- } }), _jsx(ToolbarMenuItem, { icon: "specialChar", label: "Special characters", disabled: readOnly, widePromote: true, onClick: (event) => {
1467
+ } }), t.specialCharacters && _jsx(ToolbarMenuItem, { icon: "specialChar", label: "Special characters", disabled: readOnly, widePromote: true, onClick: (event) => {
1346
1468
  const rect = event.currentTarget.getBoundingClientRect();
1347
1469
  setSpecialCharPopover({ x: rect.left, y: rect.bottom + 4 });
1348
1470
  } }), _jsx(ToolbarMenuItem, { icon: "edit", label: "Edit selected media", disabled: readOnly || !mediaAtomSelected, onClick: () => editSelectedAtom() }), _jsx(ToolbarMenuItem, { icon: "zoomIn", label: "Enlarge selected media", disabled: readOnly || !resizableAtomSelected, onClick: () => editSelectedAtom(20) }), _jsx(ToolbarMenuItem, { icon: "zoomOut", label: "Shrink selected media", disabled: readOnly || !resizableAtomSelected, onClick: () => editSelectedAtom(-20) }), _jsx(ToolbarMenuItem, { icon: "delete", label: "Delete selected media", disabled: readOnly || !atomSelected, onClick: () => runtime.executeOperations(deleteAtom(runtime.editor.document, atomScope(), {}, blockContext())) })] });
@@ -1359,32 +1481,33 @@ export const CanonicalAuthorityEditor = forwardRef(function CanonicalAuthorityEd
1359
1481
  const rect = event.currentTarget.getBoundingClientRect();
1360
1482
  setTableBorderPopover({ x: rect.left, y: rect.bottom + 4, scope: scope, initial: currentCellBorderDraft(scope) });
1361
1483
  } })] });
1362
- const saveCopyMenuItems = _jsxs(_Fragment, { children: [_jsx(ToolbarMenuItem, { label: "Save as HTML", onClick: () => runExport("html") }), _jsx(ToolbarMenuItem, { label: "Save as Markdown", onClick: () => runExport("markdown") }), _jsx(ToolbarMenuItem, { label: "Save as Word document", onClick: () => void runDocxExport() }), _jsx(ToolbarMenuItem, { label: "Save as PDF", onClick: runPdfExport }), _jsx(ToolbarMenuItem, { icon: "json", label: "Save as Smart RTE file", onClick: () => runExport("native") })] });
1363
- const reviewMenuItems = _jsxs(_Fragment, { children: [_jsx(ToolbarMenuItem, { icon: "addComment", label: "Add comment", disabled: !canComment, onClick: startComment }), _jsx(ToolbarMenuItem, { icon: "comments", label: "Comments", pressed: commentPanelOpen, disabled: !commentProvider, onClick: () => setCommentPanelOpen((open) => !open) }), _jsx(ToolbarMenuItem, { icon: "suggest", label: "Suggest deletion", disabled: !canSuggestDelete, onClick: suggestDelete }), _jsx(ToolbarMenuItem, { icon: "suggest", label: "Suggest insertion", disabled: readOnly || !suggestionProvider, onClick: startSuggestInsert }), _jsx(ToolbarMenuItem, { icon: "suggest", label: "Suggest removing this", disabled: readOnly || !suggestionProvider, onClick: suggestBlockRemoval }), _jsx(ToolbarMenuItem, { icon: "suggestions", label: "Suggestions", pressed: suggestionPanelOpen, disabled: !suggestionProvider, onClick: () => setSuggestionPanelOpen((open) => !open) }), _jsx(ToolbarMenuItem, { icon: "showEdits", label: "Show edits", pressed: trackChangesEnabled, title: "Tracks ordinary typing and edits within a single paragraph as suggestions. Merging paragraphs together - typing over a selection that spans multiple paragraphs, or pressing Backspace/Delete at a paragraph boundary - still applies directly and can't be reviewed as a suggestion.", disabled: readOnly || !suggestionProvider, onClick: () => setTrackChangesEnabled((enabled) => !enabled) })] });
1364
- return _jsxs("section", { className: `srte-root srte-editor srte-canonical-authority${className ? ` ${className}` : ""}`, "data-smart-authority": "canonical", children: [_jsxs("div", { className: "srte-toolbar", role: "toolbar", "aria-label": "Formatting toolbar", children: [_jsxs(ToolbarGroup, { children: [_jsx(ToolbarButton, { icon: "bold", label: "Bold", pressed: markCoverage("bold"), disabled: readOnly, onClick: () => toggleMark("bold") }), _jsx(ToolbarButton, { icon: "italic", label: "Italic", pressed: markCoverage("italic"), disabled: readOnly, onClick: () => toggleMark("italic") }), _jsx(ToolbarButton, { icon: "underline", label: "Underline", pressed: markCoverage("underline"), disabled: readOnly, onClick: () => toggleMark("underline") }), _jsx(ToolbarButton, { icon: "strikethrough", label: "Strikethrough", pressed: markCoverage("strikethrough"), disabled: readOnly, onClick: () => toggleMark("strikethrough") }), _jsx(ToolbarButton, { icon: "superscript", label: "Superscript", pressed: markCoverage("superscript"), disabled: readOnly, onClick: () => toggleMark("superscript"), widePromote: true }), _jsx(ToolbarButton, { icon: "subscript", label: "Subscript", pressed: markCoverage("subscript"), disabled: readOnly, onClick: () => toggleMark("subscript"), widePromote: true }), _jsx(ToolbarButton, { icon: "textColor", label: "Text colour", disabled: readOnly, widePromote: true, onClick: (event) => {
1484
+ const saveCopyMenuItems = _jsxs(_Fragment, { children: [t.saveAsHtml && _jsx(ToolbarMenuItem, { label: "Save as HTML", onClick: () => runExport("html") }), t.saveAsMarkdown && _jsx(ToolbarMenuItem, { label: "Save as Markdown", onClick: () => runExport("markdown") }), t.saveAsWord && _jsx(ToolbarMenuItem, { label: "Save as Word document", onClick: () => void runDocxExport() }), t.saveAsPdf && _jsx(ToolbarMenuItem, { label: "Save as PDF", onClick: runPdfExport }), t.saveAsSmartRte && _jsx(ToolbarMenuItem, { icon: "json", label: "Save as Smart RTE file", onClick: () => runExport("native") })] });
1485
+ const showSaveCopy = t.saveAsHtml || t.saveAsMarkdown || t.saveAsWord || t.saveAsPdf || t.saveAsSmartRte;
1486
+ // "Review" splits into two independently-toggleable tools (comments,
1487
+ // suggestions) - not an arbitrary split, it maps directly onto the two
1488
+ // already-separate providers this architecture has (commentProvider,
1489
+ // suggestionProvider). Kept as ONE dropdown trigger (labeled "Review")
1490
+ // rather than two, showing whichever half(s) are actually enabled.
1491
+ const commentsMenuItems = _jsxs(_Fragment, { children: [_jsx(ToolbarMenuItem, { icon: "addComment", label: "Add comment", disabled: !canComment, onClick: startComment }), _jsx(ToolbarMenuItem, { icon: "comments", label: "Comments", pressed: commentPanelOpen, disabled: !commentProvider, onClick: () => setCommentPanelOpen((open) => !open) })] });
1492
+ const suggestionsMenuItems = _jsxs(_Fragment, { children: [_jsx(ToolbarMenuItem, { icon: "suggest", label: "Suggest deletion", disabled: !canSuggestDelete, onClick: suggestDelete }), _jsx(ToolbarMenuItem, { icon: "suggest", label: "Suggest insertion", disabled: readOnly || !suggestionProvider, onClick: startSuggestInsert }), _jsx(ToolbarMenuItem, { icon: "suggest", label: "Suggest removing this", disabled: readOnly || !suggestionProvider, onClick: suggestBlockRemoval }), _jsx(ToolbarMenuItem, { icon: "suggestions", label: "Suggestions", pressed: suggestionPanelOpen, disabled: !suggestionProvider, onClick: () => setSuggestionPanelOpen((open) => !open) }), _jsx(ToolbarMenuItem, { icon: "showEdits", label: "Show edits", pressed: trackChangesEnabled, title: "Tracks ordinary typing and edits within a single paragraph as suggestions. Merging paragraphs together - typing over a selection that spans multiple paragraphs, or pressing Backspace/Delete at a paragraph boundary - still applies directly and can't be reviewed as a suggestion.", disabled: readOnly || !suggestionProvider, onClick: () => setTrackChangesEnabled((enabled) => !enabled) })] });
1493
+ const reviewMenuItems = _jsxs(_Fragment, { children: [showCommentsTool && commentsMenuItems, showCommentsTool && showSuggestionsTool && _jsx("div", { className: "srte-menu-separator" }), showSuggestionsTool && suggestionsMenuItems] });
1494
+ const showReviewDropdown = showCommentsTool || showSuggestionsTool;
1495
+ return _jsxs("section", { className: `srte-root srte-editor srte-canonical-authority${className ? ` ${className}` : ""}`, "data-smart-authority": "canonical", children: [_jsxs("div", { className: "srte-toolbar", role: "toolbar", "aria-label": "Formatting toolbar", children: [_jsxs(ToolbarGroup, { children: [t.bold && _jsx(ToolbarButton, { icon: "bold", label: "Bold", pressed: markCoverage("bold"), disabled: readOnly, onClick: () => toggleMark("bold") }), t.italic && _jsx(ToolbarButton, { icon: "italic", label: "Italic", pressed: markCoverage("italic"), disabled: readOnly, onClick: () => toggleMark("italic") }), t.underline && _jsx(ToolbarButton, { icon: "underline", label: "Underline", pressed: markCoverage("underline"), disabled: readOnly, onClick: () => toggleMark("underline") }), t.strikethrough && _jsx(ToolbarButton, { icon: "strikethrough", label: "Strikethrough", pressed: markCoverage("strikethrough"), disabled: readOnly, onClick: () => toggleMark("strikethrough") }), t.superscript && _jsx(ToolbarButton, { icon: "superscript", label: "Superscript", pressed: markCoverage("superscript"), disabled: readOnly, onClick: () => toggleMark("superscript"), widePromote: true }), t.subscript && _jsx(ToolbarButton, { icon: "subscript", label: "Subscript", pressed: markCoverage("subscript"), disabled: readOnly, onClick: () => toggleMark("subscript"), widePromote: true }), t.textColor && _jsx(ToolbarButton, { icon: "textColor", label: "Text colour", disabled: readOnly, widePromote: true, onClick: (event) => {
1365
1496
  const rect = event.currentTarget.getBoundingClientRect();
1366
1497
  setColorPopover({ x: rect.left, y: rect.bottom + 4, target: { kind: "mark", markId: "textColor" }, initialValue: currentMarkColor("textColor") });
1367
- } }), _jsx(ToolbarButton, { icon: "backgroundColor", label: "Background colour", disabled: readOnly, widePromote: true, onClick: (event) => {
1498
+ } }), t.backgroundColor && _jsx(ToolbarButton, { icon: "backgroundColor", label: "Background colour", disabled: readOnly, widePromote: true, onClick: (event) => {
1368
1499
  const rect = event.currentTarget.getBoundingClientRect();
1369
1500
  setColorPopover({ x: rect.left, y: rect.bottom + 4, target: { kind: "mark", markId: "backgroundColor" }, initialValue: currentMarkColor("backgroundColor") });
1370
- } }), _jsx(ToolbarButton, { icon: "fontSize", label: "Font size", disabled: readOnly, onClick: () => applyAttributedMark("fontSize"), widePromote: true }), _jsx(ToolbarButton, { icon: "fontFamily", label: "Font family", disabled: readOnly, onClick: () => applyAttributedMark("fontFamily"), widePromote: true }), _jsx(ToolbarDropdown, { icon: "textColor", label: "More text styles", priority: 2, children: textStylesMenuItems })] }), _jsxs(ToolbarGroup, { children: [_jsxs("select", { "aria-label": "Block type", title: "Block type", value: currentBlockType, disabled: readOnly, onChange: (event) => transactBlock(setBlockTypeCommand(runtime.editor.document, blockScope(), {
1501
+ } }), t.fontSize && _jsx(ToolbarButton, { icon: "fontSize", label: "Font size", disabled: readOnly, onClick: () => applyAttributedMark("fontSize"), widePromote: true }), t.fontFamily && _jsx(ToolbarButton, { icon: "fontFamily", label: "Font family", disabled: readOnly, onClick: () => applyAttributedMark("fontFamily"), widePromote: true }), showMoreTextStyles && _jsx(ToolbarDropdown, { icon: "textColor", label: "More text styles", priority: 2, children: textStylesMenuItems })] }), _jsxs(ToolbarGroup, { children: [t.blockType && _jsxs("select", { "aria-label": "Block type", title: "Block type", value: currentBlockType, disabled: readOnly, onChange: (event) => transactBlock(setBlockTypeCommand(runtime.editor.document, blockScope(), {
1371
1502
  type: event.target.value === "paragraph" ? "paragraph" : event.target.value === "code_block" ? "code_block" : "heading",
1372
1503
  attrs: event.target.value.startsWith("heading-") ? { level: Number(event.target.value.slice(8)) } : {},
1373
- }, blockContext())), children: [_jsx("option", { value: "paragraph", children: "Paragraph" }), Array.from({ length: 6 }, (_, index) => _jsxs("option", { value: `heading-${index + 1}`, children: ["Heading ", index + 1] }, index + 1)), _jsx("option", { value: "code_block", children: "Code block" })] }), _jsx(ToolbarButton, { icon: "alignLeft", label: "Align left", ariaLabel: "Align left", iconOnly: true, disabled: readOnly, onClick: () => transactBlock(setBlockAttributes(runtime.editor.document, blockScope(), { attrs: { align: "left" } }, blockContext())) }), _jsx(ToolbarButton, { icon: "alignCenter", label: "Align center", ariaLabel: "Align center", iconOnly: true, disabled: readOnly, onClick: () => transactBlock(setBlockAttributes(runtime.editor.document, blockScope(), { attrs: { align: "center" } }, blockContext())) }), _jsx(ToolbarButton, { icon: "alignRight", label: "Align right", ariaLabel: "Align right", iconOnly: true, disabled: readOnly, onClick: () => transactBlock(setBlockAttributes(runtime.editor.document, blockScope(), { attrs: { align: "right" } }, blockContext())) }), _jsx(ToolbarButton, { icon: "alignJustify", label: "Justify", ariaLabel: "Align justify", iconOnly: true, disabled: readOnly, onClick: () => transactBlock(setBlockAttributes(runtime.editor.document, blockScope(), { attrs: { align: "justify" } }, blockContext())) }), _jsx(ToolbarButton, { icon: "quote", label: "Quote", ariaLabel: "Blockquote", disabled: readOnly, onClick: toggleBlockquote }), _jsx(ToolbarDropdown, { icon: "moveUp", label: "More paragraph tools", priority: 2, children: paragraphToolsMenuItems })] }), _jsxs(ToolbarGroup, { children: [_jsx(ToolbarButton, { icon: "bulletedList", label: "Bulleted list", ariaLabel: "Bulleted list", pressed: listStyleActive("disc"), disabled: readOnly, onClick: () => toggleList("disc") }), _jsx(ToolbarButton, { icon: "numberedList", label: "Numbered list", ariaLabel: "Numbered list", pressed: listStyleActive("decimal"), disabled: readOnly, onClick: () => toggleList("decimal") }), _jsx(ToolbarButton, { icon: "checklist", label: "Checklist", ariaLabel: "Checklist", pressed: listStyleActive("disc", true), disabled: readOnly, onClick: () => toggleList("disc", true) }), _jsxs(ToolbarDropdown, { icon: "restart", label: "More list tools", priority: 2, children: [_jsx("div", { style: { padding: "4px 8px" }, children: _jsxs("select", { "aria-label": "List preset", title: "List type / preset", disabled: readOnly || currentListParts.length !== 1, value: currentListPreset, onChange: (event) => {
1374
- const preset = event.target.value;
1375
- if (!preset || currentListParts.length !== 1)
1376
- return;
1377
- // A preset choice applies to the whole list, regardless of how deep
1378
- // the cursor is nested — see outermostListId.
1379
- const rootId = outermostListId(currentListParts[0].listId);
1380
- runtime.executeOperations(setListPreset(runtime.editor.document, { ...currentListParts[0], listId: rootId }, { preset }, blockContext()), { preserveSelectionById: true });
1381
- }, style: { width: "100%" }, children: [_jsx("option", { value: "", children: "List preset" }), SMART_LIST_PRESETS.map((preset) => _jsx("option", { value: preset.id, children: preset.kind === "bullet" ? `Bullet · ${preset.label}` : `Number · ${preset.label}` }, preset.id))] }) }), _jsx("div", { className: "srte-menu-separator" }), listToolsMenuItems] })] }), _jsxs(ToolbarGroup, { children: [_jsx(ToolbarButton, { icon: "link", label: "Link", ariaLabel: "Insert or edit link", disabled: readOnly, onClick: openLinkPopover }), _jsx(ToolbarButton, { icon: "image", label: "Image", ariaLabel: "Insert image", disabled: readOnly || !mediaProvider, onClick: () => setMediaKind("image") }), _jsx(ToolbarButton, { icon: "unlink", label: "Remove link", disabled: readOnly, onClick: removeLink, widePromote: true }), _jsx(ToolbarButton, { icon: "formula", label: "Insert formula", disabled: readOnly, widePromote: true, onClick: (event) => {
1504
+ }, blockContext())), children: [_jsx("option", { value: "paragraph", children: "Paragraph" }), Array.from({ length: 6 }, (_, index) => _jsxs("option", { value: `heading-${index + 1}`, children: ["Heading ", index + 1] }, index + 1)), _jsx("option", { value: "code_block", children: "Code block" })] }), t.alignLeft && _jsx(ToolbarButton, { icon: "alignLeft", label: "Align left", ariaLabel: "Align left", iconOnly: true, disabled: readOnly, onClick: () => transactBlock(setBlockAttributes(runtime.editor.document, blockScope(), { attrs: { align: "left" } }, blockContext())) }), t.alignCenter && _jsx(ToolbarButton, { icon: "alignCenter", label: "Align center", ariaLabel: "Align center", iconOnly: true, disabled: readOnly, onClick: () => transactBlock(setBlockAttributes(runtime.editor.document, blockScope(), { attrs: { align: "center" } }, blockContext())) }), t.alignRight && _jsx(ToolbarButton, { icon: "alignRight", label: "Align right", ariaLabel: "Align right", iconOnly: true, disabled: readOnly, onClick: () => transactBlock(setBlockAttributes(runtime.editor.document, blockScope(), { attrs: { align: "right" } }, blockContext())) }), t.alignJustify && _jsx(ToolbarButton, { icon: "alignJustify", label: "Justify", ariaLabel: "Align justify", iconOnly: true, disabled: readOnly, onClick: () => transactBlock(setBlockAttributes(runtime.editor.document, blockScope(), { attrs: { align: "justify" } }, blockContext())) }), t.quote && _jsx(ToolbarButton, { icon: "quote", label: "Quote", ariaLabel: "Blockquote", disabled: readOnly, onClick: toggleBlockquote }), _jsx(ToolbarDropdown, { icon: "moveUp", label: "More paragraph tools", priority: 2, children: paragraphToolsMenuItems })] }), _jsxs(ToolbarGroup, { children: [t.bulletedList && _jsx(ToolbarButton, { icon: "bulletedList", label: "Bulleted list", ariaLabel: "Bulleted list", pressed: listStyleActive("disc"), disabled: readOnly, onClick: () => toggleList("disc") }), t.numberedList && _jsx(ToolbarButton, { icon: "numberedList", label: "Numbered list", ariaLabel: "Numbered list", pressed: listStyleActive("decimal"), disabled: readOnly, onClick: () => toggleList("decimal") }), t.checklist && _jsx(ToolbarButton, { icon: "checklist", label: "Checklist", ariaLabel: "Checklist", pressed: listStyleActive("disc", true), disabled: readOnly, onClick: () => toggleList("disc", true) }), _jsxs(ToolbarDropdown, { icon: "restart", label: "More list tools", priority: 2, children: [listPresetSelect, t.listPreset && _jsx("div", { className: "srte-menu-separator" }), listToolsMenuItems] })] }), _jsxs(ToolbarGroup, { children: [t.link && _jsx(ToolbarButton, { icon: "link", label: "Link", ariaLabel: "Insert or edit link", disabled: readOnly, onClick: openLinkPopover }), showImageTool && _jsx(ToolbarButton, { icon: "image", label: "Image", ariaLabel: "Insert image", disabled: readOnly, onClick: () => setMediaKind("image") }), t.removeLink && _jsx(ToolbarButton, { icon: "unlink", label: "Remove link", disabled: readOnly, onClick: removeLink, widePromote: true }), t.insertFormula && _jsx(ToolbarButton, { icon: "formula", label: "Insert formula", disabled: readOnly, widePromote: true, onClick: (event) => {
1382
1505
  const rect = event.currentTarget.getBoundingClientRect();
1383
1506
  setFormulaLibraryPopover({ x: rect.left, y: rect.bottom + 4 });
1384
- } }), _jsx(ToolbarButton, { icon: "specialChar", label: "Special characters", disabled: readOnly, widePromote: true, onClick: (event) => {
1507
+ } }), t.specialCharacters && _jsx(ToolbarButton, { icon: "specialChar", label: "Special characters", disabled: readOnly, widePromote: true, onClick: (event) => {
1385
1508
  const rect = event.currentTarget.getBoundingClientRect();
1386
1509
  setSpecialCharPopover({ x: rect.left, y: rect.bottom + 4 });
1387
- } }), _jsx(ToolbarDropdown, { icon: "video", label: "More to insert", priority: 2, children: insertMoreMenuItems })] }), tablesEnabled && _jsxs(ToolbarGroup, { children: [_jsx(ToolbarButton, { icon: "table", label: "Insert table", ariaLabel: "Insert table", disabled: readOnly, onClick: (event) => {
1510
+ } }), _jsx(ToolbarDropdown, { icon: "video", label: "More to insert", priority: 2, children: insertMoreMenuItems })] }), showInsertTable && _jsxs(ToolbarGroup, { children: [_jsx(ToolbarButton, { icon: "table", label: "Insert table", ariaLabel: "Insert table", disabled: readOnly, onClick: (event) => {
1388
1511
  const rect = event.currentTarget.getBoundingClientRect();
1389
1512
  setTableSizePopover({ x: rect.left, y: rect.bottom + 4 });
1390
1513
  } }), _jsx(ToolbarDropdown, { icon: "mergeCells", label: "Table tools", priority: 2, children: tableToolsMenuItems })] }), _jsx("input", { ref: importRef, type: "file", accept: ".html,.htm,.md,.markdown,.docx,.pdf,text/html,text/markdown,application/vnd.openxmlformats-officedocument.wordprocessingml.document,application/pdf", hidden: true, onChange: (event) => {
@@ -1392,11 +1515,11 @@ export const CanonicalAuthorityEditor = forwardRef(function CanonicalAuthorityEd
1392
1515
  if (file)
1393
1516
  void runImport(file);
1394
1517
  event.currentTarget.value = "";
1395
- } }), _jsxs(ToolbarGroup, { children: [_jsx(ToolbarButton, { icon: "import", label: "Import", ariaLabel: "Import document", disabled: readOnly, onClick: () => importRef.current?.click() }), _jsx(ToolbarDropdown, { icon: "saveCopy", label: "Save a copy", priority: 2, children: saveCopyMenuItems }), _jsx(ToolbarButton, { icon: "history", label: "Version history", ariaLabel: "Version history", disabled: readOnly || !versionProvider, onClick: () => setVersionHistoryOpen(true) }), _jsx(ToolbarDropdown, { icon: "comments", label: "Review", priority: 2, children: reviewMenuItems })] }), _jsxs(ToolbarGroup, { children: [_jsx(ToolbarButton, { icon: "undo", label: "Undo", ariaLabel: "Undo", disabled: readOnly, onClick: () => { runtime.editor.undo(); runtime.focus(); } }), _jsx(ToolbarButton, { icon: "redo", label: "Redo", ariaLabel: "Redo", disabled: readOnly, onClick: () => { runtime.editor.redo(); runtime.focus(); } })] }), _jsxs(MobileMoreMenu, { children: [textStylesMenuItems, _jsx("div", { className: "srte-menu-separator" }), paragraphToolsMenuItems, _jsx("div", { className: "srte-menu-separator" }), listToolsMenuItems, _jsx("div", { className: "srte-menu-separator" }), insertMoreMenuItems, tablesEnabled && _jsxs(_Fragment, { children: [_jsx("div", { className: "srte-menu-separator" }), tableToolsMenuItems] }), _jsx("div", { className: "srte-menu-separator" }), saveCopyMenuItems, _jsx("div", { className: "srte-menu-separator" }), reviewMenuItems] })] }), mediaKind === "image" && mediaManager && mediaProvider && _jsx(MediaManager, { open: true, onClose: () => setMediaKind(null), adapter: mediaManagerAdapter, onSelect: selectFromMediaManager }), mediaKind && mediaProvider && !(mediaKind === "image" && mediaManager) && _jsx(MediaPicker, { kind: mediaKind, onPick: (file) => void insertMediaFile(mediaKind, file), onCancel: () => setMediaKind(null) }), versionProvider && _jsx(VersionHistoryPanel, { open: versionHistoryOpen, onClose: () => setVersionHistoryOpen(false), runtime: runtime, versionProvider: versionProvider }), commentProvider && rootRef.current && runtime.surface.renderer?.mapping && _jsx(CommentMarkers, { positions: runtime.editor.positions, mapping: runtime.surface.renderer.mapping, containerElement: rootRef.current, threads: threads, activeThreadId: activeThreadId, onSelectThread: selectThread }), commentProvider && _jsx(CommentThreadPanel, { open: commentPanelOpen, onClose: () => { setCommentPanelOpen(false); setPendingCommentRange(null); }, threads: threads, activeThreadId: activeThreadId, onSelectThread: selectThread, pendingRange: pendingCommentRange, onCreateThread: createThread, onReply: replyToThread, onResolve: setThreadResolved, onDelete: deleteThread, busyThreadId: busyThreadId }), suggestionProvider && rootRef.current && runtime.surface.renderer?.mapping && _jsx(StructuralSuggestionMarkers, { positions: runtime.editor.positions, mapping: runtime.surface.renderer.mapping, containerElement: rootRef.current, suggestions: structuralSuggestions, activeSuggestionId: activeSuggestionId, onSelectSuggestion: selectSuggestion }), suggestionProvider && _jsx(SuggestionPanel, { open: suggestionPanelOpen, onClose: () => { setSuggestionPanelOpen(false); setPendingSuggestInsertAt(null); }, document: runtime.editor.document, structuralSuggestions: structuralSuggestions, activeId: activeSuggestionId, onSelect: selectSuggestion, pendingInsert: Boolean(pendingSuggestInsertAt), onSubmitInsert: submitSuggestInsert, onAcceptInline: acceptInlineSuggestion, onRejectInline: rejectInlineSuggestion, onAcceptStructural: acceptStructural, onRejectStructural: rejectStructural, busyId: busySuggestionId }), linkPopover && _jsx(LinkEditorPopover, { x: linkPopover.x, y: linkPopover.y, initialHref: linkPopover.href, initialOpenInNewTab: linkPopover.openInNewTab, showTextInput: linkPopover.collapsed && !linkPopover.editingExisting, showRemove: linkPopover.editingExisting, autoFocus: !linkPopover.autoTriggered, onApply: applyLink, onRemove: removeLink, onCancel: () => {
1518
+ } }), _jsxs(ToolbarGroup, { children: [t.import && _jsx(ToolbarButton, { icon: "import", label: "Import", ariaLabel: "Import document", disabled: readOnly, onClick: () => importRef.current?.click() }), showSaveCopy && _jsx(ToolbarDropdown, { icon: "saveCopy", label: "Save a copy", priority: 2, children: saveCopyMenuItems }), showVersionHistoryTool && _jsx(ToolbarButton, { icon: "history", label: "Version history", ariaLabel: "Version history", disabled: readOnly, onClick: () => setVersionHistoryOpen(true) }), showReviewDropdown && _jsx(ToolbarDropdown, { icon: "comments", label: "Review", priority: 2, children: reviewMenuItems })] }), _jsxs(ToolbarGroup, { children: [t.undo && _jsx(ToolbarButton, { icon: "undo", label: "Undo", ariaLabel: "Undo", disabled: readOnly, onClick: () => { runtime.editor.undo(); runtime.focus(); } }), t.redo && _jsx(ToolbarButton, { icon: "redo", label: "Redo", ariaLabel: "Redo", disabled: readOnly, onClick: () => { runtime.editor.redo(); runtime.focus(); } })] }), _jsxs(MobileMoreMenu, { children: [textStylesMenuItems, _jsx("div", { className: "srte-menu-separator" }), paragraphToolsMenuItems, _jsx("div", { className: "srte-menu-separator" }), listPresetSelect, listToolsMenuItems, _jsx("div", { className: "srte-menu-separator" }), insertMoreMenuItems, showInsertTable && _jsxs(_Fragment, { children: [_jsx("div", { className: "srte-menu-separator" }), tableToolsMenuItems] }), showSaveCopy && _jsxs(_Fragment, { children: [_jsx("div", { className: "srte-menu-separator" }), saveCopyMenuItems] }), showReviewDropdown && _jsxs(_Fragment, { children: [_jsx("div", { className: "srte-menu-separator" }), reviewMenuItems] })] })] }), mediaKind === "image" && mediaManager && mediaProvider && _jsx(MediaManager, { open: true, onClose: () => setMediaKind(null), adapter: mediaManagerAdapter, onSelect: selectFromMediaManager }), mediaKind && mediaProvider && !(mediaKind === "image" && mediaManager) && _jsx(MediaPicker, { kind: mediaKind, onPick: (file) => void insertMediaFile(mediaKind, file), onCancel: () => setMediaKind(null) }), showVersionHistoryTool && _jsx(VersionHistoryPanel, { open: versionHistoryOpen, onClose: () => setVersionHistoryOpen(false), runtime: runtime, versionProvider: versionProvider }), showCommentsTool && rootRef.current && runtime.surface.renderer?.mapping && _jsx(CommentMarkers, { positions: runtime.editor.positions, mapping: runtime.surface.renderer.mapping, containerElement: rootRef.current, threads: threads, activeThreadId: activeThreadId, onSelectThread: selectThread }), showCommentsTool && _jsx(CommentThreadPanel, { open: commentPanelOpen, onClose: () => { setCommentPanelOpen(false); setPendingCommentRange(null); }, threads: threads, activeThreadId: activeThreadId, onSelectThread: selectThread, pendingRange: pendingCommentRange, onCreateThread: createThread, onReply: replyToThread, onResolve: setThreadResolved, onDelete: deleteThread, busyThreadId: busyThreadId }), showSuggestionsTool && rootRef.current && runtime.surface.renderer?.mapping && _jsx(StructuralSuggestionMarkers, { positions: runtime.editor.positions, mapping: runtime.surface.renderer.mapping, containerElement: rootRef.current, suggestions: structuralSuggestions, activeSuggestionId: activeSuggestionId, onSelectSuggestion: selectSuggestion }), showSuggestionsTool && _jsx(SuggestionPanel, { open: suggestionPanelOpen, onClose: () => { setSuggestionPanelOpen(false); setPendingSuggestInsertAt(null); }, document: runtime.editor.document, structuralSuggestions: structuralSuggestions, activeId: activeSuggestionId, onSelect: selectSuggestion, pendingInsert: Boolean(pendingSuggestInsertAt), onSubmitInsert: submitSuggestInsert, onAcceptInline: acceptInlineSuggestion, onRejectInline: rejectInlineSuggestion, onAcceptStructural: acceptStructural, onRejectStructural: rejectStructural, busyId: busySuggestionId }), linkPopover && _jsx(LinkEditorPopover, { x: linkPopover.x, y: linkPopover.y, initialHref: linkPopover.href, initialOpenInNewTab: linkPopover.openInNewTab, showTextInput: linkPopover.collapsed && !linkPopover.editingExisting, showRemove: linkPopover.editingExisting, autoFocus: !linkPopover.autoTriggered, onApply: applyLink, onRemove: removeLink, onCancel: () => {
1396
1519
  setLinkOverlayDismissedAt(runtime.editor.selection.head);
1397
1520
  setLinkPopover(null);
1398
1521
  runtime.focus();
1399
- } }), !readOnly && mediaAtomSelected && selectedAtomElement && _jsx(MediaOverlay, { atomElement: selectedAtomElement, alt: typeof selectedAtomNode?.attrs?.alt === "string" ? selectedAtomNode.attrs.alt : "", width: typeof selectedAtomNode?.attrs?.width === "number" ? selectedAtomNode.attrs.width : undefined, height: typeof selectedAtomNode?.attrs?.height === "number" ? selectedAtomNode.attrs.height : undefined, src: typeof selectedAtomNode?.attrs?.src === "string" ? selectedAtomNode.attrs.src : undefined, resizable: resizableAtomSelected, onEdit: () => editSelectedAtom(), onResize: (by) => editSelectedAtom(by), onResizeTo: resizeSelectedAtomTo, onDelete: deleteSelectedAtom, onDismiss: () => runtime.focus() }), colorPopover && _jsx(ColorPickerPopover, { x: colorPopover.x, y: colorPopover.y, label: colorPopover.target.kind === "mark"
1522
+ } }), !readOnly && mediaResizeTarget && mediaResizeTarget.nodeId === selectedAtomId && mediaAtomSelected && selectedAtomElement && _jsx(MediaOverlay, { atomElement: selectedAtomElement, alt: typeof selectedAtomNode?.attrs?.alt === "string" ? selectedAtomNode.attrs.alt : "", width: typeof selectedAtomNode?.attrs?.width === "number" ? selectedAtomNode.attrs.width : undefined, height: typeof selectedAtomNode?.attrs?.height === "number" ? selectedAtomNode.attrs.height : undefined, src: typeof selectedAtomNode?.attrs?.src === "string" ? selectedAtomNode.attrs.src : undefined, showMenu: false, resizable: resizableAtomSelected, onEdit: () => undefined, onResize: () => undefined, onResizeTo: resizeSelectedAtomTo, onDelete: () => undefined, onDismiss: () => setMediaResizeTarget(null) }), !readOnly && mediaContextMenu && mediaAtomSelected && selectedAtomElement && _jsx(MediaOverlay, { atomElement: selectedAtomElement, alt: typeof selectedAtomNode?.attrs?.alt === "string" ? selectedAtomNode.attrs.alt : "", width: typeof selectedAtomNode?.attrs?.width === "number" ? selectedAtomNode.attrs.width : undefined, height: typeof selectedAtomNode?.attrs?.height === "number" ? selectedAtomNode.attrs.height : undefined, src: typeof selectedAtomNode?.attrs?.src === "string" ? selectedAtomNode.attrs.src : undefined, x: mediaContextMenu.x, y: mediaContextMenu.y, resizable: resizableAtomSelected, onEdit: () => { setMediaContextMenu(null); editSelectedAtom(); }, onResize: (by) => { setMediaContextMenu(null); editSelectedAtom(by); }, onResizeTo: resizeSelectedAtomTo, onDelete: () => { setMediaContextMenu(null); deleteSelectedAtom(); }, onDismiss: () => { setMediaContextMenu(null); runtime.focus(); } }), colorPopover && _jsx(ColorPickerPopover, { x: colorPopover.x, y: colorPopover.y, label: colorPopover.target.kind === "mark"
1400
1523
  ? (colorPopover.target.markId === "textColor" ? "Text colour" : "Background colour")
1401
1524
  : colorPopover.target.attr === "textColor" ? "Cell text colour" : "Cell background colour", ...(colorPopover.initialValue ? { initialValue: colorPopover.initialValue } : {}), recentColors: recentColors[colorBucketFor(colorPopover.target)], onPreview: previewColor, onApply: applyColor, onCancel: cancelColorPopover }), tableSizePopover && _jsx(TableSizePickerPopover, { x: tableSizePopover.x, y: tableSizePopover.y, onInsert: (rows, columns) => {
1402
1525
  setTableSizePopover(null);
@@ -1405,7 +1528,7 @@ export const CanonicalAuthorityEditor = forwardRef(function CanonicalAuthorityEd
1405
1528
  }, onCancel: () => {
1406
1529
  setTableSizePopover(null);
1407
1530
  runtime.focus();
1408
- } }), tableBorderPopover && _jsx(TableBorderPopover, { x: tableBorderPopover.x, y: tableBorderPopover.y, initial: tableBorderPopover.initial, recentColors: recentColors.border, onPreview: previewCellBorder, onApply: applyCellBorderCommit, onCancel: cancelTableBorderPopover }), formulaLibraryPopover && _jsx(FormulaLibraryPopover, { x: formulaLibraryPopover.x, y: formulaLibraryPopover.y, onInsert: insertFormulaFromLibrary, onCancel: () => { setFormulaLibraryPopover(null); runtime.focus(); } }), specialCharPopover && _jsx(SpecialCharacterPopover, { x: specialCharPopover.x, y: specialCharPopover.y, recentCharacters: recentSpecialChars, onInsert: insertSpecialCharacter, onCancel: () => { setSpecialCharPopover(null); runtime.focus(); } }), !readOnly && selectedTableElement && _jsx(TableResizeHandles, { tableElement: selectedTableElement, onResizeColumn: resizeTableColumn, onResizeRow: resizeTableRow }), contextMenu && _jsx(ContextMenu, { x: contextMenu.x, y: contextMenu.y, items: resolveContextMenuItems(), onDismiss: () => { setContextMenu(null); runtime.focus(); } }), _jsx("div", { ref: rootRef, className: "srte-editor", "data-placeholder": placeholder, contentEditable: !readOnly, suppressContentEditableWarning: true, role: "textbox", "aria-label": "Smart RTE editing surface", "aria-multiline": "true", onMouseDown: (event) => {
1531
+ } }), tableBorderPopover && _jsx(TableBorderPopover, { x: tableBorderPopover.x, y: tableBorderPopover.y, initial: tableBorderPopover.initial, recentColors: recentColors.border, onPreview: previewCellBorder, onApply: applyCellBorderCommit, onCancel: cancelTableBorderPopover }), mediaDetailsPopover && _jsx(MediaDetailsPopover, { x: mediaDetailsPopover.x, y: mediaDetailsPopover.y, initial: mediaDetailsPopover.initial, onApply: applyMediaDetails, onCancel: () => { setMediaDetailsPopover(null); runtime.focus(); } }), formulaLibraryPopover && _jsx(FormulaLibraryPopover, { x: formulaLibraryPopover.x, y: formulaLibraryPopover.y, onInsert: insertFormulaFromLibrary, onCancel: () => { setFormulaLibraryPopover(null); runtime.focus(); } }), specialCharPopover && _jsx(SpecialCharacterPopover, { x: specialCharPopover.x, y: specialCharPopover.y, recentCharacters: recentSpecialChars, onInsert: insertSpecialCharacter, onCancel: () => { setSpecialCharPopover(null); runtime.focus(); } }), !readOnly && selectedTableElement && _jsx(TableResizeHandles, { tableElement: selectedTableElement, onResizeColumn: resizeTableColumn, onResizeRow: resizeTableRow }), contextMenu && _jsx(ContextMenu, { x: contextMenu.x, y: contextMenu.y, items: resolveContextMenuItems(), onDismiss: () => { setContextMenu(null); runtime.focus(); } }), _jsx("div", { ref: rootRef, className: "srte-editor", "data-placeholder": placeholder, contentEditable: !readOnly, suppressContentEditableWarning: true, role: "textbox", "aria-label": "Smart RTE editing surface", "aria-multiline": "true", onMouseDown: (event) => {
1409
1532
  // A Ctrl/Cmd+click on a link is meant to open it (onClick below),
1410
1533
  // not move the caret into it - but native mousedown's own default
1411
1534
  // action *is* the caret-repositioning, and mousedown fires (and
@@ -1420,27 +1543,45 @@ export const CanonicalAuthorityEditor = forwardRef(function CanonicalAuthorityEd
1420
1543
  // completed render in a varying number of passes, so the overlay
1421
1544
  // could already be open before the flag was ever set.)
1422
1545
  const anchor = event.target?.closest("a[href]");
1423
- if (anchor && (event.metaKey || event.ctrlKey))
1546
+ const linkedMedia = event.target?.closest("[data-smart-href]");
1547
+ if ((anchor || linkedMedia) && (event.metaKey || event.ctrlKey))
1424
1548
  event.preventDefault();
1425
1549
  }, onClick: (event) => {
1426
1550
  const anchor = event.target?.closest("a[href]");
1427
- if (!anchor || !(event.metaKey || event.ctrlKey))
1551
+ // An image atom with a Link set (docs/bugs/media-details-old-editor-field-parity.md)
1552
+ // - plain data attributes, not a real <a> wrapper, in the live DOM
1553
+ // (see surface/renderer.ts's own reasoning); Ctrl/Cmd+click opens it
1554
+ // the same way a real text link does, matching this project's only
1555
+ // existing link-opening convention rather than inventing a second one.
1556
+ const linkedMedia = event.target?.closest("[data-smart-href]");
1557
+ const href = anchor?.href || linkedMedia?.getAttribute("data-smart-href");
1558
+ if (href && (event.metaKey || event.ctrlKey)) {
1559
+ event.preventDefault();
1560
+ window.open(href, "_blank", "noopener,noreferrer");
1428
1561
  return;
1429
- event.preventDefault();
1430
- window.open(anchor.href, "_blank", "noopener,noreferrer");
1562
+ }
1563
+ const imageTarget = event.target?.closest("[data-smart-type=\"image\"], [data-smart-type=\"block_image\"]");
1564
+ if (!imageTarget)
1565
+ return;
1566
+ setMediaContextMenu(null);
1567
+ const mappedImage = runtime.surface.renderer?.mapping.domToNode(imageTarget);
1568
+ if (mappedImage)
1569
+ setMediaResizeTarget({ nodeId: mappedImage.nodeId });
1431
1570
  }, onContextMenu: (event) => {
1432
1571
  if (readOnly)
1433
1572
  return;
1434
1573
  event.preventDefault();
1574
+ setMediaContextMenu(null);
1435
1575
  // Atom selection (unlike caret/text placement) is not native
1436
1576
  // browser behavior - it's InputController's own clickListener
1437
1577
  // (surface/input.ts), bound to the "click" DOM event, which never
1438
1578
  // fires for a right-click. Without this, right-clicking an atom
1439
1579
  // directly (no preceding left-click) left whatever selection was
1440
1580
  // already there in place, so atomic-node scope never resolved and
1441
- // MediaOverlay (which shows whenever atomSelected is true) never
1442
- // appeared - right-clicking media looked like right-clicking
1443
- // nothing. Mirrors clickListener's own atom-selection logic
1581
+ // the media interaction (details editor for images, quick-action
1582
+ // menu for other media) never appeared - right-clicking media looked
1583
+ // like right-clicking nothing. Mirrors clickListener's own
1584
+ // atom-selection logic
1444
1585
  // exactly.
1445
1586
  const atomTarget = event.target?.closest("[data-smart-atomic]");
1446
1587
  const mapped = atomTarget ? runtime.surface.renderer?.mapping.domToNode(atomTarget) : null;
@@ -1450,6 +1591,19 @@ export const CanonicalAuthorityEditor = forwardRef(function CanonicalAuthorityEd
1450
1591
  runtime.editor.setSelection({ type: "node", anchor: range.from, head: range.to }, { source: "api" });
1451
1592
  runtime.surface.renderer?.render(runtime.editor.document, runtime.editor.selection);
1452
1593
  }
1594
+ if (mapped.node.type !== "divider" && mapped.node.type !== "unknown") {
1595
+ setContextMenu(null);
1596
+ const isImage = mapped.node.type === "image" || mapped.node.type === "block_image";
1597
+ if (isImage) {
1598
+ const scope = runtime.editor.resolveScope({ want: "atomic-node" });
1599
+ setMediaResizeTarget({ nodeId: mapped.nodeId });
1600
+ if (scope.kind === "atomic-node")
1601
+ openMediaDetails(scope, mapped.node, atomTarget || undefined);
1602
+ return;
1603
+ }
1604
+ setMediaContextMenu({ x: event.clientX, y: event.clientY, nodeId: mapped.nodeId });
1605
+ return;
1606
+ }
1453
1607
  }
1454
1608
  else if (isCollapsedTextSelection(runtime.editor.selection)) {
1455
1609
  // Chromium/Firefox reposition the native caret to the click
@@ -1480,10 +1634,8 @@ export const CanonicalAuthorityEditor = forwardRef(function CanonicalAuthorityEd
1480
1634
  runtime.surface.renderer?.render(runtime.editor.document, runtime.editor.selection);
1481
1635
  }
1482
1636
  }
1483
- // Table-only now (context menu scope reduction) - media/link moved
1484
- // to their own dedicated overlays, marks live on the toolbar only.
1485
- // A right-click that doesn't resolve to a table cell opens nothing
1486
- // at all, rather than an empty "No actions here" menu.
1637
+ // A right-click that is not handled by the media branch above and
1638
+ // does not resolve to a table cell opens no generic menu.
1487
1639
  const rightClickTableScope = runtime.editor.resolveScope({ want: "table-grid" });
1488
1640
  if ("kind" in rightClickTableScope && rightClickTableScope.kind === "table-grid") {
1489
1641
  setContextMenu({ x: event.clientX, y: event.clientY });
@@ -0,0 +1,35 @@
1
+ export type MediaAlign = "left" | "center" | "right" | undefined;
2
+ export interface MediaDetailsDraft {
3
+ alt: string;
4
+ href: string;
5
+ openInNewTab: boolean;
6
+ width: number | undefined;
7
+ borderRadius: number | undefined;
8
+ align: MediaAlign;
9
+ licenseDescription: string;
10
+ licenseSourceUrl: string;
11
+ licenseType: string;
12
+ licenseVersion: string;
13
+ licenseAttribution: string;
14
+ }
15
+ export interface MediaDetailsPopoverProps {
16
+ x: number;
17
+ y: number;
18
+ initial: MediaDetailsDraft;
19
+ onApply: (draft: MediaDetailsDraft) => void;
20
+ onCancel: () => void;
21
+ }
22
+ /**
23
+ * "Media details" - replaces the crude window.prompt("Alt text") edit path
24
+ * (docs/bugs/media-details-old-editor-field-parity.md) with a real settings
25
+ * panel matching the old editor's own field set, following this project's
26
+ * established compact-popover pattern (position:fixed + JS-measured
27
+ * placement, Apply/Cancel commit - same shape as TableBorderPopover for a
28
+ * multi-field dialog like this one, rather than LinkEditorPopover's simpler
29
+ * live-Enter-to-apply form).
30
+ *
31
+ * Opened directly by an image's right-click interaction; the details panel
32
+ * is the image editing surface, while resize handles remain available
33
+ * independently around the selected image.
34
+ */
35
+ export declare function MediaDetailsPopover({ x, y, initial, onApply, onCancel }: MediaDetailsPopoverProps): import("react/jsx-runtime").JSX.Element;