smartrte-react 1.0.0-beta.9 → 1.1.1

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.
@@ -1,6 +1,6 @@
1
1
  import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { forwardRef, useEffect, useImperativeHandle, useMemo, useRef, useState } from "react";
3
- import { atomDeclarations, continueListNumbering, createList, createNodeId, deleteAtom, executeMarkTool, indentBlockCommand, indentList, inlineToolDeclarations, isTextNode, insertAtom, insertTableColumnCommand, insertTableCommand, insertTableRowCommand, mergeTableCellsCommand, moveListItems, moveTableColumnCommand, moveTableRowCommand, setTableCellAttributesCommand, setTableColumnWidthCommand, setTableRowHeightCommand, parseCanonicalListHtml, parseCanonicalListMarkdown, removeTableColumnCommand, removeTableCommand, removeTableRowCommand, resizeAtom, runAtomUpload, restartListNumbering, serializeCanonicalListHtml, serializeCanonicalListMarkdown, setListChecked, setListPreset, setListStyle, setTableHeaderCommand, setBlockAttributes, setBlockTypeCommand, moveBlockCommand, outdentBlockCommand, splitTableCellCommand, unwrapBlocks, wrapBlocks, unwrapList, updateAtom, outdentList, commentRangeFromSelection, rebaseCommentThreadsThroughTransaction, foundationSchema, resolvePos, suggestDeleteCommand, suggestInsertOperation, acceptSuggestionCommand, rejectSuggestionCommand, structuralSuggestionFromNode, acceptStructuralSuggestionCommand, rebaseStructuralSuggestionsThroughTransaction, } from "smartrte-core/foundation";
3
+ import { atomDeclarations, continueListNumbering, createList, createNodeId, deleteAtom, executeMarkTool, indentBlockCommand, indentList, inlineToolDeclarations, isTextNode, insertAtom, insertTableColumnCommand, insertTableCommand, insertTableRowCommand, mergeTableCellsCommand, moveListItems, moveTableColumnCommand, moveTableRowCommand, setTableCellAttributesCommand, setTableColumnWidthCommand, setTableRowHeightCommand, parseCanonicalListHtml, parseCanonicalListMarkdown, removeTableColumnCommand, removeTableCommand, removeTableRowCommand, reportMarkApplication, resizeAtom, runAtomUpload, restartListNumbering, serializeCanonicalListHtml, serializeCanonicalListMarkdown, setListChecked, setListPreset, setListStyle, setTableHeaderCommand, setBlockAttributes, setBlockTypeCommand, moveBlockCommand, outdentBlockCommand, splitTableCellCommand, unwrapBlocks, wrapBlocks, unwrapList, updateAtom, outdentList, commentRangeFromSelection, rebaseCommentThreadsThroughTransaction, foundationSchema, resolvePos, suggestDeleteCommand, suggestInsertOperation, acceptSuggestionCommand, rejectSuggestionCommand, structuralSuggestionFromNode, acceptStructuralSuggestionCommand, rebaseStructuralSuggestionsThroughTransaction, } from "smartrte-core/foundation";
4
4
  import { FOUNDATION_SMART_LIST_PRESETS as SMART_LIST_PRESETS } from "smartrte-core/foundation";
5
5
  import { ensureStyleSheet } from "../theme.js";
6
6
  import { VersionHistoryPanel } from "./VersionHistoryPanel.js";
@@ -17,6 +17,7 @@ import { TableSizePickerPopover } from "./TableSizePickerPopover.js";
17
17
  import { FormulaLibraryPopover } from "./FormulaLibraryPopover.js";
18
18
  import { SpecialCharacterPopover } from "./SpecialCharacterPopover.js";
19
19
  import { TableBorderPopover, BORDER_WIDTH_PRESETS } from "./TableBorderPopover.js";
20
+ import { BlockquoteBorderPopover } from "./BlockquoteBorderPopover.js";
20
21
  import { TableResizeHandles } from "./TableResizeHandles.js";
21
22
  import { MediaOverlay } from "./MediaOverlay.js";
22
23
  import { MediaDetailsPopover } from "./MediaDetailsPopover.js";
@@ -129,10 +130,23 @@ export const CanonicalAuthorityEditor = forwardRef(function CanonicalAuthorityEd
129
130
  const [linkPopover, setLinkPopover] = useState(null);
130
131
  const [colorPopover, setColorPopover] = useState(null);
131
132
  const [recentColors, setRecentColors] = useState({ text: [], background: [], border: [] });
133
+ /**
134
+ * "Let both bullet and number list tool icon work as toggle and on click
135
+ * text should [adopt] respective preset list" - the plain Bulleted
136
+ * list/Numbered list buttons always applied a generic disc/decimal
137
+ * marker, ignoring whatever glyph preset (e.g. "❖ ➢ ■") the user had
138
+ * already picked from the "List preset" dropdown elsewhere in the same
139
+ * session. Session-only React state, matching recentColors' own
140
+ * documented "no localStorage" convention above - not persisted across
141
+ * reloads.
142
+ */
143
+ const [lastListPreset, setLastListPreset] = useState({ bullet: null, ordered: null });
132
144
  const [tableSizePopover, setTableSizePopover] = useState(null);
133
145
  const [tableBorderPopover, setTableBorderPopover] = useState(null);
146
+ const [blockquoteBorderPopover, setBlockquoteBorderPopover] = useState(null);
134
147
  const [mediaDetailsPopover, setMediaDetailsPopover] = useState(null);
135
148
  const borderPreviewCheckpointRef = useRef(null);
149
+ const blockquoteBorderPreviewCheckpointRef = useRef(null);
136
150
  const [formulaLibraryPopover, setFormulaLibraryPopover] = useState(null);
137
151
  const [specialCharPopover, setSpecialCharPopover] = useState(null);
138
152
  const [recentSpecialChars, setRecentSpecialChars] = useState([]);
@@ -272,6 +286,35 @@ export const CanonicalAuthorityEditor = forwardRef(function CanonicalAuthorityEd
272
286
  };
273
287
  const blockScope = () => runtime.editor.resolveScope({ want: "block-range" });
274
288
  const blockContext = () => ({ schema: runtime.editor.schema, positions: runtime.editor.positions });
289
+ /**
290
+ * `SelectionDescription` (resolveScope({want:"describe"})) tracks mark
291
+ * coverage (markCoverage below) but has no equivalent for block
292
+ * *attributes* - `blockTypes` is the closest thing, and it's just node
293
+ * types, not attrs. This mirrors foundation/block/commands.ts's own
294
+ * (core-internal, not exported) blockScopes/selectedBlockIds traversal -
295
+ * a "mixed" scope is a caret/selection spanning more than one real
296
+ * block-range part (e.g. a paragraph plus a list item), each of which
297
+ * carries its own blockIds.
298
+ */
299
+ const selectedLineHeightBlockIds = (scope) => {
300
+ const collect = (candidate) => candidate.kind === "block-range" ? candidate.blockIds
301
+ : candidate.kind === "mixed" ? candidate.parts.flatMap(collect)
302
+ : [];
303
+ return [...new Set(collect(scope))];
304
+ };
305
+ /** `undefined` = no block selected/no override anywhere, a number = every selected block shares that exact value, `"mixed"` = selected blocks disagree - same three-state shape as markCoverage's boolean|"mixed"|undefined. */
306
+ const currentLineHeight = () => {
307
+ const ids = selectedLineHeightBlockIds(blockScope());
308
+ if (!ids.length)
309
+ return undefined;
310
+ const values = ids.map((id) => {
311
+ const value = findNode(runtime.editor.document, id)?.attrs?.lineHeight;
312
+ return typeof value === "number" ? value : undefined;
313
+ });
314
+ const unique = [...new Set(values)];
315
+ return unique.length === 1 ? unique[0] : "mixed";
316
+ };
317
+ const setLineHeight = (value) => transactBlock(setBlockAttributes(runtime.editor.document, blockScope(), { attrs: { lineHeight: value } }, blockContext()));
275
318
  const listScope = () => runtime.editor.resolveScope({ want: "list-selection" });
276
319
  const tableScope = () => runtime.editor.resolveScope({ want: "table-grid" });
277
320
  const atomScope = () => runtime.editor.resolveScope({ want: "atomic-node" });
@@ -347,15 +390,16 @@ export const CanonicalAuthorityEditor = forwardRef(function CanonicalAuthorityEd
347
390
  ? runtime.surface.renderer?.mapping.nodeToDom(currentAtomScope.nodeId)
348
391
  : undefined;
349
392
  const selectedAtomNode = atomSelected ? findNode(runtime.editor.document, currentAtomScope.nodeId) : null;
350
- // A divider (<hr>) is atomic/selectable (so ordinary caret/Backspace/
351
- // Delete behavior around it works, and the toolbar's "Delete selected
352
- // atom" button applies to it), but it has no src/alt/width/height at
353
- // all - MediaOverlay and the media-specific Edit/Resize actions assume
354
- // every atom is a real media item, and unconditionally showing them for
355
- // a divider surfaced nonsensical empty/undefined values with no
356
- // sensible edit or resize target. Media-only UI is gated on this
357
- // instead of plain atomSelected; delete stays available for any atom.
358
- const mediaAtomSelected = atomSelected && selectedAtomNode?.type !== "divider";
393
+ // A divider (<hr>) or page break is atomic/selectable (so ordinary caret/
394
+ // Backspace/Delete behavior around it works, and the toolbar's "Delete
395
+ // selected atom" button applies to it), but neither has any src/alt/
396
+ // width/height at all - MediaOverlay and the media-specific Edit/Resize
397
+ // actions assume every atom is a real media item, and unconditionally
398
+ // showing them for one of these surfaced nonsensical empty/undefined
399
+ // values with no sensible edit or resize target. Media-only UI is gated
400
+ // on this instead of plain atomSelected; delete stays available for any
401
+ // atom.
402
+ const mediaAtomSelected = atomSelected && selectedAtomNode?.type !== "divider" && selectedAtomNode?.type !== "page_break";
359
403
  // Formula (like divider above) has no width/height concept - KaTeX sizes
360
404
  // its own rendering from the source/font-size, not stored dimensions, so
361
405
  // "Enlarge/Shrink selected media" and MediaOverlay's resize handle had no
@@ -366,11 +410,18 @@ export const CanonicalAuthorityEditor = forwardRef(function CanonicalAuthorityEd
366
410
  // node.type.includes("formula") for its own correct "Formula source"
367
411
  // prompt.
368
412
  const resizableAtomSelected = mediaAtomSelected && selectedAtomNode?.type !== "formula" && selectedAtomNode?.type !== "block_formula";
413
+ // A divider/page break has no editable field and no width/height, but it
414
+ // is still a real, selectable, deletable atom - right-clicking one should
415
+ // surface the same kind of Delete-only action menu formula already gets
416
+ // (docs/bugs/horizontal-line-not-visibly-selectable.md), not the silence
417
+ // `mediaAtomSelected` alone produces (that flag exists to hide fields
418
+ // that don't apply, not to hide a delete affordance entirely).
419
+ const actionOnlyAtomSelected = atomSelected && (selectedAtomNode?.type === "divider" || selectedAtomNode?.type === "page_break");
369
420
  const selectedAtomId = atomSelected && "nodeId" in currentAtomScope ? currentAtomScope.nodeId : null;
370
421
  useEffect(() => {
371
- if (mediaContextMenu && (!mediaAtomSelected || mediaContextMenu.nodeId !== selectedAtomId))
422
+ if (mediaContextMenu && (!(mediaAtomSelected || actionOnlyAtomSelected) || mediaContextMenu.nodeId !== selectedAtomId))
372
423
  setMediaContextMenu(null);
373
- }, [mediaContextMenu, mediaAtomSelected, selectedAtomId]);
424
+ }, [mediaContextMenu, mediaAtomSelected, actionOnlyAtomSelected, selectedAtomId]);
374
425
  useEffect(() => {
375
426
  if (mediaResizeTarget && (!mediaAtomSelected || mediaResizeTarget.nodeId !== selectedAtomId))
376
427
  setMediaResizeTarget(null);
@@ -453,9 +504,36 @@ export const CanonicalAuthorityEditor = forwardRef(function CanonicalAuthorityEd
453
504
  return "paragraph";
454
505
  };
455
506
  const currentBlockType = blockTypeAt(runtime.editor.selection.head);
507
+ /**
508
+ * Nearest enclosing blockquote ancestor of `pos` (defaulting to the
509
+ * current selection head), or undefined outside one. For a nested
510
+ * blockquote this resolves to the innermost one - shared by the toggle
511
+ * button's active state, the toggle command itself, the right-click
512
+ * gate, and the blockquote styling context-menu items below, so all
513
+ * four can never drift apart on "which blockquote is this."
514
+ */
515
+ const findBlockquoteAncestor = (pos = runtime.editor.selection.head) => [...runtime.editor.resolve({ pos }).ancestors].reverse().find((node) => node.type === "blockquote");
516
+ const currentBlockquoteActive = Boolean(findBlockquoteAncestor());
517
+ /** A synthetic block-range scope targeting exactly one blockquote node's own id - the shape setBlockAttributes/wrapBlocks/unwrapBlocks all expect. */
518
+ const blockquoteScopeFor = (blockId, pos) => ({
519
+ kind: "block-range",
520
+ blockIds: [blockId],
521
+ promotedFromPartial: false,
522
+ commonParentId: null,
523
+ range: { from: pos, to: pos },
524
+ isolatingAncestorId: null,
525
+ clamped: false,
526
+ });
456
527
  const currentListPreset = currentListParts.length === 1 && typeof rootList?.attrs?.preset === "string"
457
528
  ? rootList.attrs.preset
458
529
  : "";
530
+ // The Bulleted/Numbered list buttons' own split-control style pickers -
531
+ // scoped to one kind each, so unlike the shared "List preset" select above
532
+ // (which lists both kinds together) each only shows/reflects presets of
533
+ // its own kind, and reads as unset (native placeholder) when the active
534
+ // preset (if any) belongs to the other kind.
535
+ const currentBulletPreset = SMART_LIST_PRESETS.find((preset) => preset.id === currentListPreset)?.kind === "bullet" ? currentListPreset : "";
536
+ const currentOrderedPreset = SMART_LIST_PRESETS.find((preset) => preset.id === currentListPreset)?.kind === "ordered" ? currentListPreset : "";
459
537
  const toggleCheckedItems = () => {
460
538
  if (currentListScope.kind !== "list-selection" || currentList?.attrs?.checkable !== true)
461
539
  return;
@@ -503,17 +581,30 @@ export const CanonicalAuthorityEditor = forwardRef(function CanonicalAuthorityEd
503
581
  const toggleList = (style, checkable = false) => {
504
582
  const selectedList = listScope();
505
583
  const context = blockContext();
584
+ // A checklist's marker is a checkbox, not a bullet/number glyph - never
585
+ // substitute a remembered preset for it. Otherwise, reapply whichever
586
+ // preset the user last picked from the "List preset" dropdown for this
587
+ // same kind (bullet/ordered), instead of always resetting to the
588
+ // generic disc/decimal marker - see lastListPreset's own doc comment.
589
+ // setListStyle's own params type has no `preset` field (it's a
590
+ // deliberately distinct command from setListPreset - see setListPreset
591
+ // itself clearing `style` and vice versa), so this is a real branch,
592
+ // not just a differently-shaped params object.
593
+ const rememberedPreset = checkable ? null : lastListPreset[style === "decimal" ? "ordered" : "bullet"];
506
594
  if (selectedList.kind === "list-selection") {
507
595
  const rootId = outermostListId(selectedList.listId);
508
596
  const list = findNode(runtime.editor.document, rootId);
509
597
  const sameStyle = listActiveKind(list) === (style === "decimal" ? "ordered" : "bullet") && Boolean(list?.attrs?.checkable) === checkable;
598
+ const target = { ...selectedList, listId: rootId };
510
599
  const operations = sameStyle
511
600
  // Toggling an already-active style off is deliberately scoped to just
512
601
  // the current item (selectedList), not the whole list.
513
602
  ? unwrapList(runtime.editor.document, selectedList, { splitListIds: ids(4) }, context)
514
603
  // Applying a genuinely different style/type is a whole-list decision
515
604
  // regardless of how deep the cursor is nested.
516
- : setListStyle(runtime.editor.document, { ...selectedList, listId: rootId }, { style, checkable }, context);
605
+ : rememberedPreset
606
+ ? setListPreset(runtime.editor.document, target, { preset: rememberedPreset }, context)
607
+ : setListStyle(runtime.editor.document, target, { style, checkable }, context);
517
608
  runtime.executeOperations(operations, { preserveSelectionById: true });
518
609
  return;
519
610
  }
@@ -523,9 +614,45 @@ export const CanonicalAuthorityEditor = forwardRef(function CanonicalAuthorityEd
523
614
  // per block, on top of one list per originally-flat contiguous group —
524
615
  // over-provision generously rather than compute the exact worst case.
525
616
  runtime.executeOperations(createList(runtime.editor.document, selectedBlocks, {
526
- listIds: ids(Math.max(1, count * 2)), itemIds: ids(Math.max(1, count)), style, checkable,
617
+ listIds: ids(Math.max(1, count * 2)), itemIds: ids(Math.max(1, count)), checkable,
618
+ ...(rememberedPreset ? { preset: rememberedPreset } : { style }),
527
619
  }, context), { preserveSelectionById: true });
528
620
  };
621
+ /**
622
+ * The Bulleted/Numbered list buttons' own split-control style picker
623
+ * (a native `<select>`, not a custom-positioned dropdown - see the
624
+ * `.srte-split-control` styling) - lets the user choose a specific preset
625
+ * directly from the button itself, instead of only ever getting the
626
+ * plain/remembered default from a click and having to visit the separate
627
+ * "More list tools" menu to pick a named style. Applies to an existing
628
+ * list (whole-list, like the shared "List preset" select) or creates a
629
+ * brand-new list with that preset when nothing is selected yet - unlike
630
+ * that shared select, which only ever edits an already-existing list.
631
+ * Scope is resolved fresh here (not from the outer currentListScope/
632
+ * currentListParts consts) to match toggleList's own convention of
633
+ * re-resolving at click time rather than trusting a render-time snapshot.
634
+ */
635
+ const applyListPreset = (preset) => {
636
+ const pickedKind = SMART_LIST_PRESETS.find((candidate) => candidate.id === preset)?.kind;
637
+ const selectedList = listScope();
638
+ const context = blockContext();
639
+ if (selectedList.kind === "list-selection") {
640
+ const parts = listSelectionParts(selectedList);
641
+ if (parts.length !== 1)
642
+ return;
643
+ const rootId = outermostListId(parts[0].listId);
644
+ runtime.executeOperations(setListPreset(runtime.editor.document, { ...parts[0], listId: rootId }, { preset }, context), { preserveSelectionById: true });
645
+ }
646
+ else {
647
+ const selectedBlocks = blockScope();
648
+ const count = selectedBlocks.kind === "block-range" ? selectedBlocks.blockIds.length : 1;
649
+ runtime.executeOperations(createList(runtime.editor.document, selectedBlocks, {
650
+ listIds: ids(Math.max(1, count * 2)), itemIds: ids(Math.max(1, count)), checkable: false, preset,
651
+ }, context), { preserveSelectionById: true });
652
+ }
653
+ if (pickedKind)
654
+ setLastListPreset((previous) => ({ ...previous, [pickedKind]: preset }));
655
+ };
529
656
  const runList = (action) => {
530
657
  const scope = listScope();
531
658
  const context = blockContext();
@@ -596,6 +723,16 @@ export const CanonicalAuthorityEditor = forwardRef(function CanonicalAuthorityEd
596
723
  const value = cell?.attrs?.[attr];
597
724
  return typeof value === "string" ? value : undefined;
598
725
  };
726
+ const currentBlockquoteColor = (blockId, attr) => {
727
+ const value = findNode(runtime.editor.document, blockId)?.attrs?.[attr];
728
+ return typeof value === "string" ? value : undefined;
729
+ };
730
+ /** Blockquote's *effective* left border (only one side exists, unlike table_cell's 4 - no `sides` toggle concept needed at all). */
731
+ const currentBlockquoteBorderDraft = (blockId) => {
732
+ const value = findNode(runtime.editor.document, blockId)?.attrs?.borderLeft;
733
+ const parsed = parseBorderShorthand(value);
734
+ return { style: parsed?.style ?? "solid", widthPx: parsed?.widthPx ?? BORDER_WIDTH_PRESETS[0].px, hex: parsed?.hex ?? "#000000" };
735
+ };
599
736
  const CELL_BORDER_SIDE_KEYS = { top: "borderTop", right: "borderRight", bottom: "borderBottom", left: "borderLeft" };
600
737
  /** Reads the cell's *effective* per-side border (a side's own override, falling back to the legacy uniform `borders` value) to seed the Border options popover. */
601
738
  const currentCellBorderDraft = (scope) => {
@@ -645,6 +782,39 @@ export const CanonicalAuthorityEditor = forwardRef(function CanonicalAuthorityEd
645
782
  setTableBorderPopover(null);
646
783
  runtime.focus();
647
784
  };
785
+ /** Same shape as applyCellBorderDraft, but for the one side a blockquote has - the whole draft always applies to borderLeft, no per-side toggle to reduce down first. */
786
+ const applyBlockquoteBorderDraft = (blockId, draft, options) => {
787
+ const scope = blockquoteScopeFor(blockId, runtime.editor.selection.head);
788
+ runtime.executeOperations(setBlockAttributes(runtime.editor.document, scope, { attrs: { borderLeft: composeBorderShorthand(draft.widthPx, draft.style, draft.hex) } }, blockContext()), { preserveSelectionById: true, ...options });
789
+ };
790
+ const previewBlockquoteBorder = (draft) => {
791
+ if (!blockquoteBorderPopover)
792
+ return;
793
+ if (!blockquoteBorderPreviewCheckpointRef.current)
794
+ blockquoteBorderPreviewCheckpointRef.current = runtime.createCheckpoint();
795
+ runtime.restoreCheckpoint(blockquoteBorderPreviewCheckpointRef.current);
796
+ applyBlockquoteBorderDraft(blockquoteBorderPopover.blockId, draft, { addToHistory: false });
797
+ };
798
+ const applyBlockquoteBorderCommit = (draft) => {
799
+ if (!blockquoteBorderPopover)
800
+ return;
801
+ if (blockquoteBorderPreviewCheckpointRef.current) {
802
+ runtime.restoreCheckpoint(blockquoteBorderPreviewCheckpointRef.current);
803
+ blockquoteBorderPreviewCheckpointRef.current = null;
804
+ }
805
+ applyBlockquoteBorderDraft(blockquoteBorderPopover.blockId, draft, { addToHistory: true });
806
+ recordRecentColor("border", draft.hex);
807
+ setBlockquoteBorderPopover(null);
808
+ runtime.focus();
809
+ };
810
+ const cancelBlockquoteBorderPopover = () => {
811
+ if (blockquoteBorderPreviewCheckpointRef.current) {
812
+ runtime.restoreCheckpoint(blockquoteBorderPreviewCheckpointRef.current);
813
+ blockquoteBorderPreviewCheckpointRef.current = null;
814
+ }
815
+ setBlockquoteBorderPopover(null);
816
+ runtime.focus();
817
+ };
648
818
  const recordRecentColor = (bucket, hex) => {
649
819
  setRecentColors((current) => ({
650
820
  ...current,
@@ -658,12 +828,17 @@ export const CanonicalAuthorityEditor = forwardRef(function CanonicalAuthorityEd
658
828
  if (colorPopover.target.kind === "mark") {
659
829
  applyMarkAttrs(colorPopover.target.markId, { value: hex }, options);
660
830
  }
661
- else {
831
+ else if (colorPopover.target.kind === "cell") {
662
832
  // Uses the scope captured when the popover opened, not a fresh
663
833
  // tableScope() re-resolved against the live selection - see the
664
834
  // colorPopover state's own doc comment for why.
665
835
  runtime.executeOperations(setTableCellAttributesCommand(runtime.editor.document, colorPopover.target.scope, { attrs: { [colorPopover.target.attr]: hex } }, blockContext()), { preserveSelectionById: true, ...options });
666
836
  }
837
+ else {
838
+ // Same captured-id-at-click-time discipline as the cell case above.
839
+ const scope = blockquoteScopeFor(colorPopover.target.blockId, runtime.editor.selection.head);
840
+ runtime.executeOperations(setBlockAttributes(runtime.editor.document, scope, { attrs: { [colorPopover.target.attr]: hex } }, blockContext()), { preserveSelectionById: true, ...options });
841
+ }
667
842
  };
668
843
  /**
669
844
  * Live preview while dragging the in-page saturation/hue picker (or typing
@@ -722,18 +897,10 @@ export const CanonicalAuthorityEditor = forwardRef(function CanonicalAuthorityEd
722
897
  const toggleBlockquote = () => {
723
898
  const scope = blockScope();
724
899
  const resolved = runtime.editor.resolve({ pos: runtime.editor.selection.head });
725
- const ancestor = [...resolved.ancestors].reverse().find((node) => node.type === "blockquote");
900
+ const ancestor = findBlockquoteAncestor();
726
901
  const context = blockContext();
727
902
  if (ancestor) {
728
- const quoteScope = {
729
- kind: "block-range",
730
- blockIds: [ancestor.id],
731
- promotedFromPartial: false,
732
- commonParentId: null,
733
- range: { from: resolved.pos, to: resolved.pos },
734
- isolatingAncestorId: null,
735
- clamped: false,
736
- };
903
+ const quoteScope = blockquoteScopeFor(ancestor.id, resolved.pos);
737
904
  runtime.executeOperations(unwrapBlocks(runtime.editor.document, quoteScope, { type: "blockquote" }, context), { preserveSelectionById: true });
738
905
  return;
739
906
  }
@@ -895,6 +1062,29 @@ export const CanonicalAuthorityEditor = forwardRef(function CanonicalAuthorityEd
895
1062
  onSelect: () => setTableBorderPopover({ x: openX, y: openY, scope: tableGridScope, initial: currentCellBorderDraft(tableGridScope) }),
896
1063
  });
897
1064
  }
1065
+ else {
1066
+ // Table cell checked first (above) - a right-click inside a table
1067
+ // nested inside a blockquote shows cell options, not these.
1068
+ const blockquoteAncestor = findBlockquoteAncestor();
1069
+ if (blockquoteAncestor) {
1070
+ const openX = contextMenu?.x ?? 0;
1071
+ const openY = contextMenu?.y ?? 0;
1072
+ const blockId = blockquoteAncestor.id;
1073
+ items.push({
1074
+ id: "blockquote.contextMenu.backgroundColor",
1075
+ label: "Blockquote background colour",
1076
+ onSelect: () => setColorPopover({ x: openX, y: openY, target: { kind: "blockquote", attr: "backgroundColor", blockId }, initialValue: currentBlockquoteColor(blockId, "backgroundColor") }),
1077
+ }, {
1078
+ id: "blockquote.contextMenu.textColor",
1079
+ label: "Blockquote text colour",
1080
+ onSelect: () => setColorPopover({ x: openX, y: openY, target: { kind: "blockquote", attr: "textColor", blockId }, initialValue: currentBlockquoteColor(blockId, "textColor") }),
1081
+ }, {
1082
+ id: "blockquote.contextMenu.borderOptions",
1083
+ label: "Blockquote border options",
1084
+ onSelect: () => setBlockquoteBorderPopover({ x: openX, y: openY, blockId, initial: currentBlockquoteBorderDraft(blockId) }),
1085
+ });
1086
+ }
1087
+ }
898
1088
  return items;
899
1089
  };
900
1090
  /**
@@ -1397,6 +1587,26 @@ export const CanonicalAuthorityEditor = forwardRef(function CanonicalAuthorityEd
1397
1587
  return false;
1398
1588
  return entry.coverage === "all" ? true : "mixed";
1399
1589
  };
1590
+ /**
1591
+ * Whether clicking this mark tool right now would do anything at all - a
1592
+ * node type can declare `marks: ""` (code_block, every atom) to disallow
1593
+ * every mark, and applyMarkCommand already silently skips owners that
1594
+ * disallow the mark rather than erroring - previously that made these
1595
+ * buttons stay fully clickable no-ops with zero feedback inside a code
1596
+ * block or with an image selected. Reuses reportMarkApplication (already
1597
+ * built for exactly this question, previously only exercised by tests)
1598
+ * rather than re-deriving the schema check: "allowed somewhere" is
1599
+ * ownerCount > ownerIdsSkipped.length, i.e. at least one touched owner
1600
+ * does NOT skip it. Deliberately "somewhere", not "everywhere" - a
1601
+ * selection spanning both a normal paragraph and a code block still
1602
+ * partially applies to the paragraph half today, and disabling here
1603
+ * would take that capability away for no reason.
1604
+ */
1605
+ const markToolAllowed = (id) => {
1606
+ const scope = runtime.editor.resolveScope({ want: "inline-range" });
1607
+ const report = reportMarkApplication(runtime.editor.document, scope, markTool(id).markType, blockContext());
1608
+ return report.ownerCount > report.ownerIdsSkipped.length;
1609
+ };
1400
1610
  const toggleMark = (id) => { executeMarkTool(runtime.editor, markTool(id), "toggle"); runtime.focus(); };
1401
1611
  const openLinkPopover = (event) => {
1402
1612
  const description = runtime.editor.resolveScope({ want: "describe" });
@@ -1427,14 +1637,39 @@ export const CanonicalAuthorityEditor = forwardRef(function CanonicalAuthorityEd
1427
1637
  // twice; below 1440px (including mobile) this copy is what's reachable,
1428
1638
  // same as before. "Code" isn't promoted - see the docs/bugs/ writeup on
1429
1639
  // why it stayed the dropdown's sole remaining item at wide widths.
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) => {
1640
+ const textStylesMenuItems = _jsxs(_Fragment, { children: [t.code && _jsx(ToolbarMenuItem, { icon: "code", label: "Code", pressed: markCoverage("inlineCode"), disabled: readOnly || !markToolAllowed("inlineCode"), onClick: () => toggleMark("inlineCode") }), t.superscript && _jsx(ToolbarMenuItem, { icon: "superscript", label: "Superscript", pressed: markCoverage("superscript"), disabled: readOnly || !markToolAllowed("superscript"), onClick: () => toggleMark("superscript"), widePromote: true }), t.subscript && _jsx(ToolbarMenuItem, { icon: "subscript", label: "Subscript", pressed: markCoverage("subscript"), disabled: readOnly || !markToolAllowed("subscript"), onClick: () => toggleMark("subscript"), widePromote: true }), t.textColor && _jsx(ToolbarMenuItem, { icon: "textColor", label: "Text colour", disabled: readOnly || !markToolAllowed("textColor"), widePromote: true, onClick: (event) => {
1431
1641
  const rect = event.currentTarget.getBoundingClientRect();
1432
1642
  setColorPopover({ x: rect.left, y: rect.bottom + 4, target: { kind: "mark", markId: "textColor" }, initialValue: currentMarkColor("textColor") });
1433
- } }), t.backgroundColor && _jsx(ToolbarMenuItem, { icon: "backgroundColor", label: "Background colour", disabled: readOnly, widePromote: true, onClick: (event) => {
1643
+ } }), t.backgroundColor && _jsx(ToolbarMenuItem, { icon: "backgroundColor", label: "Background colour", disabled: readOnly || !markToolAllowed("backgroundColor"), widePromote: true, onClick: (event) => {
1434
1644
  const rect = event.currentTarget.getBoundingClientRect();
1435
1645
  setColorPopover({ x: rect.left, y: rect.bottom + 4, target: { kind: "mark", markId: "backgroundColor" }, initialValue: currentMarkColor("backgroundColor") });
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 })] });
1646
+ } }), t.fontSize && _jsx(ToolbarMenuItem, { icon: "fontSize", label: "Font size", disabled: readOnly || !markToolAllowed("fontSize"), onClick: () => applyAttributedMark("fontSize"), widePromote: true }), t.fontFamily && _jsx(ToolbarMenuItem, { icon: "fontFamily", label: "Font family", disabled: readOnly || !markToolAllowed("fontFamily"), onClick: () => applyAttributedMark("fontFamily"), widePromote: true })] });
1437
1647
  const showMoreTextStyles = t.code || t.superscript || t.subscript || t.textColor || t.backgroundColor || t.fontSize || t.fontFamily;
1648
+ /**
1649
+ * Line spacing - matches Google Docs' own reference shape: a short preset
1650
+ * list plus a custom/exact value entry, with a checkmark on whichever
1651
+ * preset (or "Default") currently applies. "Default" clears the
1652
+ * attribute entirely (setLineHeight(undefined)) rather than writing an
1653
+ * explicit "1" - the block then renders at the browser/font's own
1654
+ * natural line-height, matching how no override ever forces a value for
1655
+ * align/indent either. A mixed-value selection (blocks disagreeing)
1656
+ * shows no checkmark on any entry, matching every comparison below
1657
+ * evaluating false against the "mixed" sentinel with no special-casing
1658
+ * needed.
1659
+ */
1660
+ const LINE_HEIGHT_PRESETS = [1, 1.15, 1.5, 2, 2.5];
1661
+ const lineHeightMenuItems = _jsxs(_Fragment, { children: [_jsx(ToolbarMenuItem, { label: "Default", pressed: currentLineHeight() === undefined, disabled: readOnly, onClick: () => setLineHeight(undefined) }), LINE_HEIGHT_PRESETS.map((preset) => _jsx(ToolbarMenuItem, { label: String(preset), pressed: currentLineHeight() === preset, disabled: readOnly, onClick: () => setLineHeight(preset) }, preset)), _jsx("div", { className: "srte-menu-separator" }), _jsxs("div", { className: "srte-line-height-custom", style: { padding: "4px 8px", display: "flex", alignItems: "center", gap: 6 }, children: [_jsx("span", { "aria-hidden": "true", style: { fontSize: 12, whiteSpace: "nowrap" }, children: "Custom:" }), _jsx("input", { "aria-label": "Custom line spacing", type: "number", min: 0.1, max: 10, step: 0.05, disabled: readOnly, defaultValue: (() => { const current = currentLineHeight(); return typeof current === "number" && !LINE_HEIGHT_PRESETS.includes(current) ? current : ""; })(), placeholder: "e.g. 1.75", style: { width: "100%", minWidth: 0 }, onKeyDown: (event) => {
1662
+ if (event.key !== "Enter")
1663
+ return;
1664
+ const value = Number(event.currentTarget.value);
1665
+ if (Number.isFinite(value) && value >= 0.1 && value <= 10)
1666
+ setLineHeight(value);
1667
+ event.currentTarget.closest("details")?.removeAttribute("open");
1668
+ }, onBlur: (event) => {
1669
+ const value = Number(event.currentTarget.value);
1670
+ if (event.currentTarget.value && Number.isFinite(value) && value >= 0.1 && value <= 10)
1671
+ setLineHeight(value);
1672
+ } }, String((() => { const current = currentLineHeight(); return typeof current === "number" && !LINE_HEIGHT_PRESETS.includes(current) ? current : "none"; })()))] })] });
1438
1673
  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
1674
  /**
1440
1675
  * Named multi-level marker presets (decimal/lower-alpha/upper-alpha/
@@ -1455,6 +1690,9 @@ export const CanonicalAuthorityEditor = forwardRef(function CanonicalAuthorityEd
1455
1690
  // the cursor is nested — see outermostListId.
1456
1691
  const rootId = outermostListId(currentListParts[0].listId);
1457
1692
  runtime.executeOperations(setListPreset(runtime.editor.document, { ...currentListParts[0], listId: rootId }, { preset }, blockContext()), { preserveSelectionById: true });
1693
+ const pickedKind = SMART_LIST_PRESETS.find((candidate) => candidate.id === preset)?.kind;
1694
+ if (pickedKind)
1695
+ setLastListPreset((previous) => ({ ...previous, [pickedKind]: preset }));
1458
1696
  }, 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))] }) });
1459
1697
  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 }) })] });
1460
1698
  // Remove link/Insert formula/Special characters also get a standalone
@@ -1467,7 +1705,7 @@ export const CanonicalAuthorityEditor = forwardRef(function CanonicalAuthorityEd
1467
1705
  } }), t.specialCharacters && _jsx(ToolbarMenuItem, { icon: "specialChar", label: "Special characters", disabled: readOnly, widePromote: true, onClick: (event) => {
1468
1706
  const rect = event.currentTarget.getBoundingClientRect();
1469
1707
  setSpecialCharPopover({ x: rect.left, y: rect.bottom + 4 });
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())) })] });
1708
+ } }), t.horizontalLine && _jsx(ToolbarMenuItem, { icon: "divider", label: "Horizontal line", disabled: readOnly, widePromote: true, onClick: () => { insertBlockAtom("divider", {}, createNodeId()); runtime.focus(); } }), t.pageBreak && _jsx(ToolbarMenuItem, { icon: "pageBreak", label: "Page break", disabled: readOnly, widePromote: true, onClick: () => { insertBlockAtom("page_break", {}, createNodeId()); runtime.focus(); } }), _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())) })] });
1471
1709
  // "Remove row"/"Remove column"/"Remove table" renamed to "Delete ..." -
1472
1710
  // matches the right-click table context menu's existing wording
1473
1711
  // (table/plugin.ts's table.contextMenu.removeRow/removeColumn/removeTable
@@ -1481,7 +1719,7 @@ export const CanonicalAuthorityEditor = forwardRef(function CanonicalAuthorityEd
1481
1719
  const rect = event.currentTarget.getBoundingClientRect();
1482
1720
  setTableBorderPopover({ x: rect.left, y: rect.bottom + 4, scope: scope, initial: currentCellBorderDraft(scope) });
1483
1721
  } })] });
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") })] });
1722
+ 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: "Print / Save as PDF", onClick: runPdfExport }), t.saveAsSmartRte && _jsx(ToolbarMenuItem, { icon: "json", label: "Save as Smart RTE file", onClick: () => runExport("native") })] });
1485
1723
  const showSaveCopy = t.saveAsHtml || t.saveAsMarkdown || t.saveAsWord || t.saveAsPdf || t.saveAsSmartRte;
1486
1724
  // "Review" splits into two independently-toggleable tools (comments,
1487
1725
  // suggestions) - not an arbitrary split, it maps directly onto the two
@@ -1492,22 +1730,24 @@ export const CanonicalAuthorityEditor = forwardRef(function CanonicalAuthorityEd
1492
1730
  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
1731
  const reviewMenuItems = _jsxs(_Fragment, { children: [showCommentsTool && commentsMenuItems, showCommentsTool && showSuggestionsTool && _jsx("div", { className: "srte-menu-separator" }), showSuggestionsTool && suggestionsMenuItems] });
1494
1732
  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) => {
1733
+ 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 || !markToolAllowed("bold"), onClick: () => toggleMark("bold") }), t.italic && _jsx(ToolbarButton, { icon: "italic", label: "Italic", pressed: markCoverage("italic"), disabled: readOnly || !markToolAllowed("italic"), onClick: () => toggleMark("italic") }), t.underline && _jsx(ToolbarButton, { icon: "underline", label: "Underline", pressed: markCoverage("underline"), disabled: readOnly || !markToolAllowed("underline"), onClick: () => toggleMark("underline") }), t.strikethrough && _jsx(ToolbarButton, { icon: "strikethrough", label: "Strikethrough", pressed: markCoverage("strikethrough"), disabled: readOnly || !markToolAllowed("strikethrough"), onClick: () => toggleMark("strikethrough") }), t.superscript && _jsx(ToolbarButton, { icon: "superscript", label: "Superscript", pressed: markCoverage("superscript"), disabled: readOnly || !markToolAllowed("superscript"), onClick: () => toggleMark("superscript"), widePromote: true }), t.subscript && _jsx(ToolbarButton, { icon: "subscript", label: "Subscript", pressed: markCoverage("subscript"), disabled: readOnly || !markToolAllowed("subscript"), onClick: () => toggleMark("subscript"), widePromote: true }), t.textColor && _jsx(ToolbarButton, { icon: "textColor", label: "Text colour", disabled: readOnly || !markToolAllowed("textColor"), widePromote: true, onClick: (event) => {
1496
1734
  const rect = event.currentTarget.getBoundingClientRect();
1497
1735
  setColorPopover({ x: rect.left, y: rect.bottom + 4, target: { kind: "mark", markId: "textColor" }, initialValue: currentMarkColor("textColor") });
1498
- } }), t.backgroundColor && _jsx(ToolbarButton, { icon: "backgroundColor", label: "Background colour", disabled: readOnly, widePromote: true, onClick: (event) => {
1736
+ } }), t.backgroundColor && _jsx(ToolbarButton, { icon: "backgroundColor", label: "Background colour", disabled: readOnly || !markToolAllowed("backgroundColor"), widePromote: true, onClick: (event) => {
1499
1737
  const rect = event.currentTarget.getBoundingClientRect();
1500
1738
  setColorPopover({ x: rect.left, y: rect.bottom + 4, target: { kind: "mark", markId: "backgroundColor" }, initialValue: currentMarkColor("backgroundColor") });
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(), {
1739
+ } }), t.fontSize && _jsx(ToolbarButton, { icon: "fontSize", label: "Font size", disabled: readOnly || !markToolAllowed("fontSize"), onClick: () => applyAttributedMark("fontSize"), widePromote: true }), t.fontFamily && _jsx(ToolbarButton, { icon: "fontFamily", label: "Font family", disabled: readOnly || !markToolAllowed("fontFamily"), 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", { className: "srte-block-type-select", "aria-label": "Block type", title: "Block type", value: currentBlockType, disabled: readOnly, onChange: (event) => transactBlock(setBlockTypeCommand(runtime.editor.document, blockScope(), {
1502
1740
  type: event.target.value === "paragraph" ? "paragraph" : event.target.value === "code_block" ? "code_block" : "heading",
1503
1741
  attrs: event.target.value.startsWith("heading-") ? { level: Number(event.target.value.slice(8)) } : {},
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) => {
1742
+ }, 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.lineHeight && _jsx(ToolbarDropdown, { icon: "lineHeight", label: "Line spacing", priority: 2, children: lineHeightMenuItems }), t.quote && _jsx(ToolbarButton, { icon: "quote", label: "Quote", ariaLabel: "Blockquote", pressed: currentBlockquoteActive, disabled: readOnly, onClick: toggleBlockquote }), _jsx(ToolbarDropdown, { icon: "moveUp", label: "More paragraph tools", priority: 2, children: paragraphToolsMenuItems })] }), _jsxs(ToolbarGroup, { children: [t.bulletedList && (t.listPreset ? _jsxs("span", { className: "srte-split-control", children: [_jsx(ToolbarButton, { icon: "bulletedList", label: "Bulleted list", ariaLabel: "Bulleted list", pressed: listStyleActive("disc"), disabled: readOnly, narrowIconOnly: true, onClick: () => toggleList("disc") }), _jsxs("select", { "aria-label": "Bulleted list style", title: "Bulleted list style", disabled: readOnly || (currentListScope.kind === "list-selection" && currentListParts.length !== 1), value: currentBulletPreset, onChange: (event) => { const preset = event.target.value; if (preset)
1743
+ applyListPreset(preset); }, children: [_jsx("option", { value: "", children: "Bulleted list style" }), SMART_LIST_PRESETS.filter((preset) => preset.kind === "bullet").map((preset) => _jsx("option", { value: preset.id, children: preset.label }, preset.id))] })] }) : _jsx(ToolbarButton, { icon: "bulletedList", label: "Bulleted list", ariaLabel: "Bulleted list", pressed: listStyleActive("disc"), disabled: readOnly, narrowIconOnly: true, onClick: () => toggleList("disc") })), t.numberedList && (t.listPreset ? _jsxs("span", { className: "srte-split-control", children: [_jsx(ToolbarButton, { icon: "numberedList", label: "Numbered list", ariaLabel: "Numbered list", pressed: listStyleActive("decimal"), disabled: readOnly, narrowIconOnly: true, onClick: () => toggleList("decimal") }), _jsxs("select", { "aria-label": "Numbered list style", title: "Numbered list style", disabled: readOnly || (currentListScope.kind === "list-selection" && currentListParts.length !== 1), value: currentOrderedPreset, onChange: (event) => { const preset = event.target.value; if (preset)
1744
+ applyListPreset(preset); }, children: [_jsx("option", { value: "", children: "Numbered list style" }), SMART_LIST_PRESETS.filter((preset) => preset.kind === "ordered").map((preset) => _jsx("option", { value: preset.id, children: preset.label }, preset.id))] })] }) : _jsx(ToolbarButton, { icon: "numberedList", label: "Numbered list", ariaLabel: "Numbered list", pressed: listStyleActive("decimal"), disabled: readOnly, narrowIconOnly: true, onClick: () => toggleList("decimal") })), t.checklist && _jsx(ToolbarButton, { icon: "checklist", label: "Checklist", ariaLabel: "Checklist", pressed: listStyleActive("disc", true), disabled: readOnly, narrowIconOnly: true, 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 || !markToolAllowed("link"), 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) => {
1505
1745
  const rect = event.currentTarget.getBoundingClientRect();
1506
1746
  setFormulaLibraryPopover({ x: rect.left, y: rect.bottom + 4 });
1507
1747
  } }), t.specialCharacters && _jsx(ToolbarButton, { icon: "specialChar", label: "Special characters", disabled: readOnly, widePromote: true, onClick: (event) => {
1508
1748
  const rect = event.currentTarget.getBoundingClientRect();
1509
1749
  setSpecialCharPopover({ x: rect.left, y: rect.bottom + 4 });
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) => {
1750
+ } }), t.horizontalLine && _jsx(ToolbarButton, { icon: "divider", label: "Horizontal line", disabled: readOnly, widePromote: true, onClick: () => { insertBlockAtom("divider", {}, createNodeId()); runtime.focus(); } }), t.pageBreak && _jsx(ToolbarButton, { icon: "pageBreak", label: "Page break", disabled: readOnly, widePromote: true, onClick: () => { insertBlockAtom("page_break", {}, createNodeId()); runtime.focus(); } }), _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) => {
1511
1751
  const rect = event.currentTarget.getBoundingClientRect();
1512
1752
  setTableSizePopover({ x: rect.left, y: rect.bottom + 4 });
1513
1753
  } }), _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) => {
@@ -1515,20 +1755,22 @@ export const CanonicalAuthorityEditor = forwardRef(function CanonicalAuthorityEd
1515
1755
  if (file)
1516
1756
  void runImport(file);
1517
1757
  event.currentTarget.value = "";
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: () => {
1758
+ } }), _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, t.lineHeight && _jsx("div", { className: "srte-menu-separator" }), t.lineHeight && lineHeightMenuItems, _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: () => {
1519
1759
  setLinkOverlayDismissedAt(runtime.editor.selection.head);
1520
1760
  setLinkPopover(null);
1521
1761
  runtime.focus();
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"
1762
+ } }), !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 || actionOnlyAtomSelected) && 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, editable: mediaAtomSelected, 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"
1523
1763
  ? (colorPopover.target.markId === "textColor" ? "Text colour" : "Background colour")
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) => {
1764
+ : colorPopover.target.kind === "blockquote"
1765
+ ? (colorPopover.target.attr === "textColor" ? "Blockquote text colour" : "Blockquote background colour")
1766
+ : 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) => {
1525
1767
  setTableSizePopover(null);
1526
1768
  insertTable(rows, columns);
1527
1769
  runtime.focus();
1528
1770
  }, onCancel: () => {
1529
1771
  setTableSizePopover(null);
1530
1772
  runtime.focus();
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) => {
1773
+ } }), tableBorderPopover && _jsx(TableBorderPopover, { x: tableBorderPopover.x, y: tableBorderPopover.y, initial: tableBorderPopover.initial, recentColors: recentColors.border, onPreview: previewCellBorder, onApply: applyCellBorderCommit, onCancel: cancelTableBorderPopover }), blockquoteBorderPopover && _jsx(BlockquoteBorderPopover, { x: blockquoteBorderPopover.x, y: blockquoteBorderPopover.y, initial: blockquoteBorderPopover.initial, recentColors: recentColors.border, onPreview: previewBlockquoteBorder, onApply: applyBlockquoteBorderCommit, onCancel: cancelBlockquoteBorderPopover }), 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) => {
1532
1774
  // A Ctrl/Cmd+click on a link is meant to open it (onClick below),
1533
1775
  // not move the caret into it - but native mousedown's own default
1534
1776
  // action *is* the caret-repositioning, and mousedown fires (and
@@ -1591,7 +1833,7 @@ export const CanonicalAuthorityEditor = forwardRef(function CanonicalAuthorityEd
1591
1833
  runtime.editor.setSelection({ type: "node", anchor: range.from, head: range.to }, { source: "api" });
1592
1834
  runtime.surface.renderer?.render(runtime.editor.document, runtime.editor.selection);
1593
1835
  }
1594
- if (mapped.node.type !== "divider" && mapped.node.type !== "unknown") {
1836
+ if (mapped.node.type !== "unknown") {
1595
1837
  setContextMenu(null);
1596
1838
  const isImage = mapped.node.type === "image" || mapped.node.type === "block_image";
1597
1839
  if (isImage) {
@@ -1635,10 +1877,16 @@ export const CanonicalAuthorityEditor = forwardRef(function CanonicalAuthorityEd
1635
1877
  }
1636
1878
  }
1637
1879
  // 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.
1880
+ // does not resolve to a table cell or a blockquote opens no
1881
+ // generic menu. Table cell checked first: a right-click inside a
1882
+ // table nested inside a blockquote should still show cell
1883
+ // options, matching "closest/innermost container wins."
1639
1884
  const rightClickTableScope = runtime.editor.resolveScope({ want: "table-grid" });
1640
1885
  if ("kind" in rightClickTableScope && rightClickTableScope.kind === "table-grid") {
1641
1886
  setContextMenu({ x: event.clientX, y: event.clientY });
1642
1887
  }
1888
+ else if (findBlockquoteAncestor()) {
1889
+ setContextMenu({ x: event.clientX, y: event.clientY });
1890
+ }
1643
1891
  }, style: { minHeight, maxHeight, overflow: "auto" } })] });
1644
1892
  });