smartrte-react 1.0.0-beta.11 → 1.0.0-beta.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.0.0-beta.12
4
+
5
+ - Fix toolbar dropdowns/popovers inside a `contain: paint | strict | content` ancestor rendering off-screen or clipped to invisible near that ancestor's own edge — `1.0.0-beta.11`'s `contain` fix corrected the coordinate origin but still clamped against the full viewport instead of the actual clipping box, so a menu near the host's own right/bottom edge could compute a position outside what the ancestor actually lets paint, making it invisible even though its coordinates looked reasonable. Affects `ToolbarDropdown`, `MobileMoreMenu`, the color picker popover, and the table border popover — all four shared the same viewport-only clamping logic. See `docs/bugs/toolbar-overlay-clamped-to-wrong-viewport-inside-contain-paint.md`.
6
+ - The Bulleted list/Numbered list toolbar buttons now act as a toggle that reapplies the last preset you picked for that list kind (via "More list tools") instead of always resetting to the plain default marker. Picking "Diamond bullets" once, turning the list off, then clicking Bulleted list again reapplies diamond bullets directly. Session-only (not persisted), and does not apply to the checklist toggle.
7
+ - Add a small preset picker directly to the Bulleted list/Numbered list toolbar buttons (a split control — the button itself keeps its existing one-click toggle behavior unchanged) so a specific named style can be applied straight from the button, without a trip to "More list tools." Works even when nothing is a list yet (creates one directly with that preset). Hidden only in a narrow sliver of container widths (below ~359px) too tight to fit it — visible at every more realistic width, including inside typical dialogs and split panes; the same presets stay reachable via the existing mobile "List preset" control regardless.
8
+ - Below 480px, the block type ("Paragraph"/"Heading"/...), Bulleted list, Numbered list, and Checklist controls drop their text label and show icon-only, keeping their name/tooltip on hover — frees up meaningful width in genuinely narrow embeddings without losing discoverability.
9
+
3
10
  ## 1.0.0-beta.11
4
11
 
5
12
  - Fix toolbar dropdowns/popovers still landing far from their trigger inside a host container that uses CSS `contain: paint | strict | content` on an ancestor (a real gap in `1.0.0-beta.9`'s transform-only fix — `contain` also creates a `position: fixed` containing block, independent of `transform`). See `docs/bugs/toolbar-overlay-misplaced-inside-contain-ancestor.md`.
@@ -129,6 +129,17 @@ export const CanonicalAuthorityEditor = forwardRef(function CanonicalAuthorityEd
129
129
  const [linkPopover, setLinkPopover] = useState(null);
130
130
  const [colorPopover, setColorPopover] = useState(null);
131
131
  const [recentColors, setRecentColors] = useState({ text: [], background: [], border: [] });
132
+ /**
133
+ * "Let both bullet and number list tool icon work as toggle and on click
134
+ * text should [adopt] respective preset list" - the plain Bulleted
135
+ * list/Numbered list buttons always applied a generic disc/decimal
136
+ * marker, ignoring whatever glyph preset (e.g. "❖ ➢ ■") the user had
137
+ * already picked from the "List preset" dropdown elsewhere in the same
138
+ * session. Session-only React state, matching recentColors' own
139
+ * documented "no localStorage" convention above - not persisted across
140
+ * reloads.
141
+ */
142
+ const [lastListPreset, setLastListPreset] = useState({ bullet: null, ordered: null });
132
143
  const [tableSizePopover, setTableSizePopover] = useState(null);
133
144
  const [tableBorderPopover, setTableBorderPopover] = useState(null);
134
145
  const [mediaDetailsPopover, setMediaDetailsPopover] = useState(null);
@@ -493,6 +504,13 @@ export const CanonicalAuthorityEditor = forwardRef(function CanonicalAuthorityEd
493
504
  const currentListPreset = currentListParts.length === 1 && typeof rootList?.attrs?.preset === "string"
494
505
  ? rootList.attrs.preset
495
506
  : "";
507
+ // The Bulleted/Numbered list buttons' own split-control style pickers -
508
+ // scoped to one kind each, so unlike the shared "List preset" select above
509
+ // (which lists both kinds together) each only shows/reflects presets of
510
+ // its own kind, and reads as unset (native placeholder) when the active
511
+ // preset (if any) belongs to the other kind.
512
+ const currentBulletPreset = SMART_LIST_PRESETS.find((preset) => preset.id === currentListPreset)?.kind === "bullet" ? currentListPreset : "";
513
+ const currentOrderedPreset = SMART_LIST_PRESETS.find((preset) => preset.id === currentListPreset)?.kind === "ordered" ? currentListPreset : "";
496
514
  const toggleCheckedItems = () => {
497
515
  if (currentListScope.kind !== "list-selection" || currentList?.attrs?.checkable !== true)
498
516
  return;
@@ -540,17 +558,30 @@ export const CanonicalAuthorityEditor = forwardRef(function CanonicalAuthorityEd
540
558
  const toggleList = (style, checkable = false) => {
541
559
  const selectedList = listScope();
542
560
  const context = blockContext();
561
+ // A checklist's marker is a checkbox, not a bullet/number glyph - never
562
+ // substitute a remembered preset for it. Otherwise, reapply whichever
563
+ // preset the user last picked from the "List preset" dropdown for this
564
+ // same kind (bullet/ordered), instead of always resetting to the
565
+ // generic disc/decimal marker - see lastListPreset's own doc comment.
566
+ // setListStyle's own params type has no `preset` field (it's a
567
+ // deliberately distinct command from setListPreset - see setListPreset
568
+ // itself clearing `style` and vice versa), so this is a real branch,
569
+ // not just a differently-shaped params object.
570
+ const rememberedPreset = checkable ? null : lastListPreset[style === "decimal" ? "ordered" : "bullet"];
543
571
  if (selectedList.kind === "list-selection") {
544
572
  const rootId = outermostListId(selectedList.listId);
545
573
  const list = findNode(runtime.editor.document, rootId);
546
574
  const sameStyle = listActiveKind(list) === (style === "decimal" ? "ordered" : "bullet") && Boolean(list?.attrs?.checkable) === checkable;
575
+ const target = { ...selectedList, listId: rootId };
547
576
  const operations = sameStyle
548
577
  // Toggling an already-active style off is deliberately scoped to just
549
578
  // the current item (selectedList), not the whole list.
550
579
  ? unwrapList(runtime.editor.document, selectedList, { splitListIds: ids(4) }, context)
551
580
  // Applying a genuinely different style/type is a whole-list decision
552
581
  // regardless of how deep the cursor is nested.
553
- : setListStyle(runtime.editor.document, { ...selectedList, listId: rootId }, { style, checkable }, context);
582
+ : rememberedPreset
583
+ ? setListPreset(runtime.editor.document, target, { preset: rememberedPreset }, context)
584
+ : setListStyle(runtime.editor.document, target, { style, checkable }, context);
554
585
  runtime.executeOperations(operations, { preserveSelectionById: true });
555
586
  return;
556
587
  }
@@ -560,9 +591,45 @@ export const CanonicalAuthorityEditor = forwardRef(function CanonicalAuthorityEd
560
591
  // per block, on top of one list per originally-flat contiguous group —
561
592
  // over-provision generously rather than compute the exact worst case.
562
593
  runtime.executeOperations(createList(runtime.editor.document, selectedBlocks, {
563
- listIds: ids(Math.max(1, count * 2)), itemIds: ids(Math.max(1, count)), style, checkable,
594
+ listIds: ids(Math.max(1, count * 2)), itemIds: ids(Math.max(1, count)), checkable,
595
+ ...(rememberedPreset ? { preset: rememberedPreset } : { style }),
564
596
  }, context), { preserveSelectionById: true });
565
597
  };
598
+ /**
599
+ * The Bulleted/Numbered list buttons' own split-control style picker
600
+ * (a native `<select>`, not a custom-positioned dropdown - see the
601
+ * `.srte-split-control` styling) - lets the user choose a specific preset
602
+ * directly from the button itself, instead of only ever getting the
603
+ * plain/remembered default from a click and having to visit the separate
604
+ * "More list tools" menu to pick a named style. Applies to an existing
605
+ * list (whole-list, like the shared "List preset" select) or creates a
606
+ * brand-new list with that preset when nothing is selected yet - unlike
607
+ * that shared select, which only ever edits an already-existing list.
608
+ * Scope is resolved fresh here (not from the outer currentListScope/
609
+ * currentListParts consts) to match toggleList's own convention of
610
+ * re-resolving at click time rather than trusting a render-time snapshot.
611
+ */
612
+ const applyListPreset = (preset) => {
613
+ const pickedKind = SMART_LIST_PRESETS.find((candidate) => candidate.id === preset)?.kind;
614
+ const selectedList = listScope();
615
+ const context = blockContext();
616
+ if (selectedList.kind === "list-selection") {
617
+ const parts = listSelectionParts(selectedList);
618
+ if (parts.length !== 1)
619
+ return;
620
+ const rootId = outermostListId(parts[0].listId);
621
+ runtime.executeOperations(setListPreset(runtime.editor.document, { ...parts[0], listId: rootId }, { preset }, context), { preserveSelectionById: true });
622
+ }
623
+ else {
624
+ const selectedBlocks = blockScope();
625
+ const count = selectedBlocks.kind === "block-range" ? selectedBlocks.blockIds.length : 1;
626
+ runtime.executeOperations(createList(runtime.editor.document, selectedBlocks, {
627
+ listIds: ids(Math.max(1, count * 2)), itemIds: ids(Math.max(1, count)), checkable: false, preset,
628
+ }, context), { preserveSelectionById: true });
629
+ }
630
+ if (pickedKind)
631
+ setLastListPreset((previous) => ({ ...previous, [pickedKind]: preset }));
632
+ };
566
633
  const runList = (action) => {
567
634
  const scope = listScope();
568
635
  const context = blockContext();
@@ -1517,6 +1584,9 @@ export const CanonicalAuthorityEditor = forwardRef(function CanonicalAuthorityEd
1517
1584
  // the cursor is nested — see outermostListId.
1518
1585
  const rootId = outermostListId(currentListParts[0].listId);
1519
1586
  runtime.executeOperations(setListPreset(runtime.editor.document, { ...currentListParts[0], listId: rootId }, { preset }, blockContext()), { preserveSelectionById: true });
1587
+ const pickedKind = SMART_LIST_PRESETS.find((candidate) => candidate.id === preset)?.kind;
1588
+ if (pickedKind)
1589
+ setLastListPreset((previous) => ({ ...previous, [pickedKind]: preset }));
1520
1590
  }, 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))] }) });
1521
1591
  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 }) })] });
1522
1592
  // Remove link/Insert formula/Special characters also get a standalone
@@ -1560,10 +1630,12 @@ export const CanonicalAuthorityEditor = forwardRef(function CanonicalAuthorityEd
1560
1630
  } }), t.backgroundColor && _jsx(ToolbarButton, { icon: "backgroundColor", label: "Background colour", disabled: readOnly, widePromote: true, onClick: (event) => {
1561
1631
  const rect = event.currentTarget.getBoundingClientRect();
1562
1632
  setColorPopover({ x: rect.left, y: rect.bottom + 4, target: { kind: "mark", markId: "backgroundColor" }, initialValue: currentMarkColor("backgroundColor") });
1563
- } }), 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(), {
1633
+ } }), 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", { className: "srte-block-type-select", "aria-label": "Block type", title: "Block type", value: currentBlockType, disabled: readOnly, onChange: (event) => transactBlock(setBlockTypeCommand(runtime.editor.document, blockScope(), {
1564
1634
  type: event.target.value === "paragraph" ? "paragraph" : event.target.value === "code_block" ? "code_block" : "heading",
1565
1635
  attrs: event.target.value.startsWith("heading-") ? { level: Number(event.target.value.slice(8)) } : {},
1566
- }, 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", 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) => {
1636
+ }, 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", 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)
1637
+ 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)
1638
+ 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, 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) => {
1567
1639
  const rect = event.currentTarget.getBoundingClientRect();
1568
1640
  setFormulaLibraryPopover({ x: rect.left, y: rect.bottom + 4 });
1569
1641
  } }), t.specialCharacters && _jsx(ToolbarButton, { icon: "specialChar", label: "Special characters", disabled: readOnly, widePromote: true, onClick: (event) => {
@@ -1,6 +1,6 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useEffect, useId, useLayoutEffect, useRef, useState } from "react";
3
- import { getFixedPositioningOrigin } from "./fixedPositioning.js";
3
+ import { getFixedPositioningOrigin, getPositioningBounds } from "./fixedPositioning.js";
4
4
  const HEX_PATTERN = /^#([0-9a-f]{3}|[0-9a-f]{6})$/i;
5
5
  const normalizeHex = (value) => {
6
6
  if (!HEX_PATTERN.test(value))
@@ -131,14 +131,13 @@ export function ColorPickerPopover({ x, y, label, initialValue = "#000000", rece
131
131
  return;
132
132
  const margin = 8;
133
133
  const { width, height } = el.getBoundingClientRect();
134
- const viewportWidth = window.innerWidth;
135
- const viewportHeight = window.innerHeight;
136
- const overflowsRight = x + width > viewportWidth - margin;
134
+ const bounds = getPositioningBounds(el);
135
+ const overflowsRight = x + width > bounds.right - margin;
137
136
  const left = overflowsRight ? x - width : x;
138
- const clampedLeft = Math.min(Math.max(margin, left), Math.max(margin, viewportWidth - width - margin));
139
- const overflowsBottom = y + height > viewportHeight - margin;
137
+ const clampedLeft = Math.min(Math.max(bounds.left + margin, left), Math.max(bounds.left + margin, bounds.right - width - margin));
138
+ const overflowsBottom = y + height > bounds.bottom - margin;
140
139
  const top = overflowsBottom ? y - height : y;
141
- const clampedTop = Math.min(Math.max(margin, top), Math.max(margin, viewportHeight - margin));
140
+ const clampedTop = Math.min(Math.max(bounds.top + margin, top), Math.max(bounds.top + margin, bounds.bottom - margin));
142
141
  const origin = getFixedPositioningOrigin(el);
143
142
  setPlacement({ left: clampedLeft - origin.left, top: clampedTop - origin.top });
144
143
  }, [x, y]);
@@ -1,7 +1,7 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useEffect, useLayoutEffect, useRef, useState } from "react";
3
3
  import { ColorPickerPopover } from "./ColorPickerPopover.js";
4
- import { getFixedPositioningOrigin } from "./fixedPositioning.js";
4
+ import { getFixedPositioningOrigin, getPositioningBounds } from "./fixedPositioning.js";
5
5
  // Widths start at 2px, not 1px - table_cell's border-collapse table (see
6
6
  // theme.ts's th/td rule) already renders a 1px ambient default gridline on
7
7
  // every cell. A custom border tied at 1px loses CSS border-collapse's
@@ -68,14 +68,13 @@ export function TableBorderPopover({ x, y, initial, recentColors, onPreview, onA
68
68
  return;
69
69
  const margin = 8;
70
70
  const { width, height } = el.getBoundingClientRect();
71
- const viewportWidth = window.innerWidth;
72
- const viewportHeight = window.innerHeight;
73
- const overflowsRight = x + width > viewportWidth - margin;
71
+ const bounds = getPositioningBounds(el);
72
+ const overflowsRight = x + width > bounds.right - margin;
74
73
  const left = overflowsRight ? x - width : x;
75
- const clampedLeft = Math.min(Math.max(margin, left), Math.max(margin, viewportWidth - width - margin));
76
- const overflowsBottom = y + height > viewportHeight - margin;
74
+ const clampedLeft = Math.min(Math.max(bounds.left + margin, left), Math.max(bounds.left + margin, bounds.right - width - margin));
75
+ const overflowsBottom = y + height > bounds.bottom - margin;
77
76
  const top = overflowsBottom ? y - height : y;
78
- const clampedTop = Math.min(Math.max(margin, top), Math.max(margin, viewportHeight - margin));
77
+ const clampedTop = Math.min(Math.max(bounds.top + margin, top), Math.max(bounds.top + margin, bounds.bottom - margin));
79
78
  const origin = getFixedPositioningOrigin(el);
80
79
  setPlacement({ left: clampedLeft - origin.left, top: clampedTop - origin.top });
81
80
  }, [x, y]);
@@ -66,7 +66,7 @@ export declare const toolbarIcons: {
66
66
  };
67
67
  export type ToolbarIconKey = keyof typeof toolbarIcons;
68
68
  /** A single always-visible toolbar control: icon + plain-language label together, per the redesign's core principle. */
69
- export declare function ToolbarButton({ icon, label, ariaLabel, pressed, disabled, title, onClick, iconOnly, widePromote }: {
69
+ export declare function ToolbarButton({ icon, label, ariaLabel, pressed, disabled, title, onClick, iconOnly, widePromote, narrowIconOnly }: {
70
70
  icon: ToolbarIconKey;
71
71
  label: string;
72
72
  ariaLabel?: string;
@@ -84,6 +84,16 @@ export declare function ToolbarButton({ icon, label, ariaLabel, pressed, disable
84
84
  * docs/bugs/toolbar-priority-collapse-fixed-threshold-no-wide-promotion.md.
85
85
  */
86
86
  widePromote?: boolean;
87
+ /**
88
+ * Unlike `iconOnly` (always icon-only), this drops the label text only
89
+ * below theme.ts's 479px container-width breakpoint - the button shows
90
+ * icon + label at every wider width, matching every other always-visible
91
+ * button, and falls back to icon-only (title/aria-label still carry the
92
+ * name, so it stays a real tooltip) only once space is genuinely tight.
93
+ * A CSS-only toggle (see `[data-srte-narrow-icon-only]` in theme.ts), not
94
+ * JS viewport state, matching how every other responsive tier here works.
95
+ */
96
+ narrowIconOnly?: boolean;
87
97
  }): import("react/jsx-runtime").JSX.Element;
88
98
  /** One row inside an open dropdown menu - closes the dropdown after acting, matching ContextMenu.tsx's own dismiss-on-action convention. */
89
99
  export declare function ToolbarMenuItem({ icon, label, ariaLabel, pressed, disabled, title, onClick, widePromote }: {
@@ -1,7 +1,7 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useEffect, useLayoutEffect, useRef, useState } from "react";
3
3
  import { AlignCenter, AlignJustify, AlignLeft, AlignRight, AlignVerticalSpaceAround, ALargeSmall, Baseline, Bold, CheckSquare, ChevronDown, Code, Columns3, Download, Eye, FileJson, History, Image as ImageIcon, IndentDecrease, IndentIncrease, Italic, Link2, ListChecks, ListOrdered, ListRestart, List as ListIcon, MessageSquare, MessageSquarePlus, MoreHorizontal, MoveDown, MoveLeft, MoveRight, MoveUp, Music, Omega, PanelTop, PenLine, Pilcrow, Quote, Redo2, Rows3, ScissorsLineDashed, SeparatorHorizontal, Square, Sigma, Strikethrough, Subscript, Superscript, Table2, Trash2, Type, Underline, Undo2, Unlink2, Upload, Video, Pencil, ZoomIn, ZoomOut, } from "lucide-react";
4
- import { getFixedPositioningOrigin } from "./fixedPositioning.js";
4
+ import { getFixedPositioningOrigin, getPositioningBounds } from "./fixedPositioning.js";
5
5
  /**
6
6
  * The toolbar's icon set (Phase: Direction B toolbar redesign, 2026-08-29).
7
7
  * Lucide, per the design proposal's §11 recommendation - MIT-licensed,
@@ -80,8 +80,8 @@ export const toolbarIcons = {
80
80
  lineHeight: _jsx(AlignVerticalSpaceAround, { ...stroke }),
81
81
  };
82
82
  /** A single always-visible toolbar control: icon + plain-language label together, per the redesign's core principle. */
83
- export function ToolbarButton({ icon, label, ariaLabel, pressed, disabled, title, onClick, iconOnly, widePromote }) {
84
- return _jsxs("button", { type: "button", className: "srte-tool-button", "aria-label": ariaLabel ?? label, "aria-pressed": pressed, title: title ?? ariaLabel ?? label, disabled: disabled, onMouseDown: (event) => event.preventDefault(), onClick: onClick, ...(widePromote ? { "data-srte-wide-promote": "true" } : {}), children: [toolbarIcons[icon], !iconOnly && _jsx("span", { children: label })] });
83
+ export function ToolbarButton({ icon, label, ariaLabel, pressed, disabled, title, onClick, iconOnly, widePromote, narrowIconOnly }) {
84
+ return _jsxs("button", { type: "button", className: "srte-tool-button", "aria-label": ariaLabel ?? label, "aria-pressed": pressed, title: title ?? ariaLabel ?? label, disabled: disabled, onMouseDown: (event) => event.preventDefault(), onClick: onClick, ...(widePromote ? { "data-srte-wide-promote": "true" } : {}), ...(narrowIconOnly ? { "data-srte-narrow-icon-only": "true" } : {}), children: [toolbarIcons[icon], !iconOnly && _jsx("span", { children: label })] });
85
85
  }
86
86
  /** One row inside an open dropdown menu - closes the dropdown after acting, matching ContextMenu.tsx's own dismiss-on-action convention. */
87
87
  export function ToolbarMenuItem({ icon, label, ariaLabel, pressed, disabled, title, onClick, widePromote }) {
@@ -157,11 +157,10 @@ export function ToolbarDropdown({ icon, label, priority, children }) {
157
157
  const menu = details.querySelector(":scope > .srte-menu");
158
158
  const menuWidth = menu?.offsetWidth ?? 210;
159
159
  const menuHeight = menu?.offsetHeight ?? 0;
160
- const viewportWidth = window.innerWidth;
161
- const viewportHeight = window.innerHeight;
162
- const left = Math.min(Math.max(margin, triggerRect.left), Math.max(margin, viewportWidth - menuWidth - margin));
163
- const overflowsBottom = triggerRect.bottom + 6 + menuHeight > viewportHeight - margin;
164
- const top = overflowsBottom ? Math.max(margin, triggerRect.top - menuHeight - 6) : triggerRect.bottom + 6;
160
+ const bounds = getPositioningBounds(details);
161
+ const left = Math.min(Math.max(bounds.left + margin, triggerRect.left), Math.max(bounds.left + margin, bounds.right - menuWidth - margin));
162
+ const overflowsBottom = triggerRect.bottom + 6 + menuHeight > bounds.bottom - margin;
163
+ const top = overflowsBottom ? Math.max(bounds.top + margin, triggerRect.top - menuHeight - 6) : triggerRect.bottom + 6;
165
164
  const origin = getFixedPositioningOrigin(details);
166
165
  setPlacement({ left: left - origin.left, top: top - origin.top });
167
166
  };
@@ -215,17 +214,17 @@ export function MobileMoreMenu({ children }) {
215
214
  const margin = 8;
216
215
  const triggerRect = details.getBoundingClientRect();
217
216
  const menu = details.querySelector(":scope > .srte-menu");
218
- const menuWidth = menu?.offsetWidth ?? Math.min(280, window.innerWidth - 16);
217
+ const bounds = getPositioningBounds(details);
218
+ const menuWidth = menu?.offsetWidth ?? Math.min(280, bounds.right - bounds.left - 16);
219
219
  const menuHeight = menu?.offsetHeight ?? 0;
220
- const viewportWidth = window.innerWidth;
221
- const viewportHeight = window.innerHeight;
222
220
  // Right-aligned to the trigger, matching this menu's original
223
- // CSS-only `right: 0` intent - clamped into the viewport instead of
224
- // being allowed to run off it or get clipped by an ancestor.
221
+ // CSS-only `right: 0` intent - clamped into the effective bounds
222
+ // instead of being allowed to run off them or get clipped by an
223
+ // ancestor.
225
224
  const preferredLeft = triggerRect.right - menuWidth;
226
- const left = Math.min(Math.max(margin, preferredLeft), Math.max(margin, viewportWidth - menuWidth - margin));
227
- const overflowsBottom = triggerRect.bottom + 6 + menuHeight > viewportHeight - margin;
228
- const top = overflowsBottom ? Math.max(margin, triggerRect.top - menuHeight - 6) : triggerRect.bottom + 6;
225
+ const left = Math.min(Math.max(bounds.left + margin, preferredLeft), Math.max(bounds.left + margin, bounds.right - menuWidth - margin));
226
+ const overflowsBottom = triggerRect.bottom + 6 + menuHeight > bounds.bottom - margin;
227
+ const top = overflowsBottom ? Math.max(bounds.top + margin, triggerRect.top - menuHeight - 6) : triggerRect.bottom + 6;
229
228
  const origin = getFixedPositioningOrigin(details);
230
229
  setPlacement({ left: left - origin.left, top: top - origin.top });
231
230
  };
@@ -40,3 +40,36 @@ export declare function getFixedPositioningOrigin(el: HTMLElement): {
40
40
  left: number;
41
41
  top: number;
42
42
  };
43
+ /**
44
+ * Correcting the coordinate origin (above) is only half the fix - every
45
+ * overlay in this codebase also clamps its own placement so a menu never
46
+ * renders off-screen, and every one of those clamps was computed against
47
+ * `window.innerWidth`/`innerHeight`, the *full browser viewport*. That's
48
+ * wrong when the containing-block ancestor is one with `contain: paint`
49
+ * (or `strict`/`content`): CSS guarantees nothing painted by a descendant -
50
+ * including a `position: fixed` one whose containing block it is - can
51
+ * ever be visible outside that ancestor's own border box, no matter what
52
+ * `left`/`top` says. Reported live: a toolbar dropdown near the middle of
53
+ * a `contain: paint` dialog rendered hugging the dialog's own right edge
54
+ * (barely fit), and one further right ("More to insert") was positioned
55
+ * entirely outside the dialog's box and so never painted at all - both
56
+ * fully explained by clamping against the wide browser window instead of
57
+ * the actual, much narrower, clipping ancestor.
58
+ *
59
+ * Deliberately NOT applied for a containing block established only by
60
+ * `transform`/`perspective`/`filter`/`backdrop-filter`/`will-change` with
61
+ * no `contain` of its own - none of those clip overflow, so a menu is free
62
+ * to visually extend past that ancestor's box exactly as it could before
63
+ * any of this positioning logic existed. Verified directly: constraining
64
+ * to such an ancestor's bounds regardless made a mobile "More tools" menu
65
+ * genuinely too tall to fit inside a short transformed host overlap its
66
+ * own trigger instead of just extending past the host's edge (harmless,
67
+ * since nothing there clips it) - a real regression from being too
68
+ * conservative, not a fix.
69
+ */
70
+ export declare function getPositioningBounds(el: HTMLElement): {
71
+ left: number;
72
+ top: number;
73
+ right: number;
74
+ bottom: number;
75
+ };
@@ -58,3 +58,46 @@ export function getFixedPositioningOrigin(el) {
58
58
  const rect = container.getBoundingClientRect();
59
59
  return { left: rect.left, top: rect.top };
60
60
  }
61
+ /**
62
+ * Correcting the coordinate origin (above) is only half the fix - every
63
+ * overlay in this codebase also clamps its own placement so a menu never
64
+ * renders off-screen, and every one of those clamps was computed against
65
+ * `window.innerWidth`/`innerHeight`, the *full browser viewport*. That's
66
+ * wrong when the containing-block ancestor is one with `contain: paint`
67
+ * (or `strict`/`content`): CSS guarantees nothing painted by a descendant -
68
+ * including a `position: fixed` one whose containing block it is - can
69
+ * ever be visible outside that ancestor's own border box, no matter what
70
+ * `left`/`top` says. Reported live: a toolbar dropdown near the middle of
71
+ * a `contain: paint` dialog rendered hugging the dialog's own right edge
72
+ * (barely fit), and one further right ("More to insert") was positioned
73
+ * entirely outside the dialog's box and so never painted at all - both
74
+ * fully explained by clamping against the wide browser window instead of
75
+ * the actual, much narrower, clipping ancestor.
76
+ *
77
+ * Deliberately NOT applied for a containing block established only by
78
+ * `transform`/`perspective`/`filter`/`backdrop-filter`/`will-change` with
79
+ * no `contain` of its own - none of those clip overflow, so a menu is free
80
+ * to visually extend past that ancestor's box exactly as it could before
81
+ * any of this positioning logic existed. Verified directly: constraining
82
+ * to such an ancestor's bounds regardless made a mobile "More tools" menu
83
+ * genuinely too tall to fit inside a short transformed host overlap its
84
+ * own trigger instead of just extending past the host's edge (harmless,
85
+ * since nothing there clips it) - a real regression from being too
86
+ * conservative, not a fix.
87
+ */
88
+ export function getPositioningBounds(el) {
89
+ const viewport = { left: 0, top: 0, right: window.innerWidth, bottom: window.innerHeight };
90
+ const container = findFixedPositioningContainer(el);
91
+ if (!container)
92
+ return viewport;
93
+ const containerStyle = window.getComputedStyle(container);
94
+ if (!containerStyle.contain || !/\b(paint|strict|content)\b/.test(containerStyle.contain))
95
+ return viewport;
96
+ const rect = container.getBoundingClientRect();
97
+ return {
98
+ left: Math.max(viewport.left, rect.left),
99
+ top: Math.max(viewport.top, rect.top),
100
+ right: Math.min(viewport.right, rect.right),
101
+ bottom: Math.min(viewport.bottom, rect.bottom),
102
+ };
103
+ }
package/dist/theme.d.ts CHANGED
@@ -1,3 +1,3 @@
1
1
  export type SrteTheme = 'light' | 'dark';
2
- export declare const SRTE_DEFAULT_CSS = "\n.srte-editor {\n --srte-background: var(--card, #ffffff);\n --srte-canvas: var(--background, #ffffff);\n --srte-foreground: var(--foreground, #0f172a);\n --srte-muted: var(--muted, #f1f5f9);\n --srte-muted-foreground: var(--muted-foreground, #64748b);\n --srte-ring: var(--ring, #0284c7);\n --srte-radius: var(--radius, 0.625rem);\n --srte-bg: var(--srte-canvas);\n --srte-text: var(--srte-foreground);\n --srte-text-muted: var(--srte-muted-foreground);\n --srte-border: var(--border, #e2e8f0);\n --srte-border-light: var(--srte-border);\n --srte-toolbar-bg: var(--srte-background);\n --srte-input-bg: var(--srte-background);\n --srte-input-text: var(--srte-foreground);\n --srte-input-border: var(--srte-border);\n --srte-modal-backdrop: rgba(0, 0, 0, 0.35);\n --srte-modal-backdrop-filter: blur(2px);\n --srte-modal-bg: var(--card, #ffffff);\n --srte-modal-text: var(--foreground, #000000);\n --srte-menu-bg: var(--srte-background);\n --srte-menu-text: var(--srte-foreground);\n --srte-menu-shadow: 0 8px 24px rgba(0, 0, 0, 0.18);\n --srte-accent: #0284c7;\n --srte-accent-bg: rgba(2, 132, 199, 0.12);\n --srte-danger: #dc2626;\n --srte-primary: #2563eb;\n /* --srte-primary on --srte-accent-bg measures 4.43:1, under WCAG AA's\n 4.5:1 for normal text - used only for the pressed toolbar-button state\n (see .srte-tool-button[aria-pressed=\"true\"]), not a replacement for\n --srte-primary generally. */\n --srte-primary-pressed: #1d4ed8;\n --srte-on-primary: #ffffff;\n --srte-cancel-bg: #f3f4f6;\n --srte-code-bg: #f6f8fa;\n --srte-code-text: #24292f;\n}\n.srte-editor.srte-dark {\n --srte-background: var(--card, #1e293b);\n --srte-canvas: var(--background, #0f172a);\n --srte-foreground: var(--foreground, #f8fafc);\n --srte-muted: var(--muted, #334155);\n --srte-muted-foreground: var(--muted-foreground, #94a3b8);\n --srte-ring: var(--ring, #38bdf8);\n --srte-bg: var(--srte-canvas);\n --srte-text: var(--srte-foreground);\n --srte-text-muted: var(--srte-muted-foreground);\n --srte-border: var(--border, #334155);\n --srte-border-light: var(--srte-border);\n --srte-toolbar-bg: var(--srte-background);\n --srte-input-bg: var(--srte-background);\n --srte-input-text: var(--srte-foreground);\n --srte-input-border: var(--srte-border);\n --srte-modal-backdrop: rgba(0, 0, 0, 0.22);\n --srte-modal-backdrop-filter: blur(10px) saturate(0.9);\n --srte-modal-bg: var(--card, #1e293b);\n --srte-modal-text: var(--foreground, #e0e0e0);\n --srte-menu-bg: var(--srte-background);\n --srte-menu-text: var(--srte-foreground);\n --srte-menu-shadow: 0 8px 24px rgba(0, 0, 0, 0.4);\n --srte-accent: #38bdf8;\n --srte-accent-bg: rgba(56, 189, 248, 0.16);\n --srte-danger: #ef4444;\n --srte-primary: #3b82f6;\n --srte-primary-pressed: var(--srte-primary);\n --srte-on-primary: #ffffff;\n --srte-cancel-bg: #333333;\n --srte-code-bg: #111827;\n --srte-code-text: #e5e7eb;\n}\n.srte-editor {\n border-radius: var(--srte-radius);\n font-family: \"IBM Plex Sans\", ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, \"Segoe UI\", sans-serif;\n container: srte-editor / inline-size;\n}\n.srte-canonical-authority > .srte-editor[contenteditable] {\n width: 100%;\n padding: 16px 20px;\n box-sizing: border-box;\n border: 1px solid var(--srte-border);\n border-radius: 0 0 var(--srte-radius) var(--srte-radius);\n outline: none;\n background: var(--srte-canvas);\n color: var(--srte-foreground);\n caret-color: var(--srte-foreground);\n line-height: 1.6;\n}\n.srte-toolbar {\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n gap: 4px;\n width: 100%;\n min-height: 48px;\n padding: 8px;\n box-sizing: border-box;\n border-bottom: 1px solid var(--srte-border);\n background: var(--srte-background);\n color: var(--srte-foreground);\n position: sticky;\n top: 0;\n z-index: 10;\n}\n.srte-toolbar-group {\n display: inline-flex;\n align-items: center;\n gap: 2px;\n min-width: 0;\n}\n.srte-toolbar-group + .srte-toolbar-group::before {\n content: \"\";\n width: 1px;\n height: 20px;\n margin: 0 5px 0 3px;\n background: var(--srte-border);\n}\n.srte-tool-button,\n.srte-toolbar select {\n height: 32px;\n min-width: 32px;\n box-sizing: border-box;\n border: 1px solid transparent;\n border-radius: 8px;\n background: transparent;\n color: var(--srte-foreground);\n font: 500 13px/1 \"IBM Plex Sans\", ui-sans-serif, system-ui, sans-serif;\n}\n.srte-tool-button {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n gap: 3px;\n padding: 0 7px;\n cursor: pointer;\n}\n.srte-toolbar select {\n max-width: 132px;\n padding: 0 26px 0 9px;\n border-color: var(--srte-border);\n background: var(--srte-input-bg);\n cursor: pointer;\n}\n.srte-tool-button:hover,\n.srte-toolbar select:hover,\n.srte-toolbar-menu[open] > .srte-menu-trigger {\n background: var(--srte-muted);\n border-color: var(--srte-border);\n}\n.srte-tool-button.srte-active,\n.srte-tool-button[aria-pressed=\"true\"] {\n /* var(--srte-primary) (#2563eb) on var(--srte-accent-bg) measured 4.43:1,\n just under WCAG AA's 4.5:1 for normal text (axe-core, Phase 11 Tier 3) -\n a dedicated, darker pressed-state color keeps the same hue family\n while clearing the threshold, without changing --srte-primary's other,\n already-compliant usages (e.g. against solid backgrounds). */\n color: var(--srte-primary-pressed);\n background: var(--srte-accent-bg);\n border-color: color-mix(in srgb, var(--srte-primary) 35%, transparent);\n}\n.srte-tool-button:focus-visible,\n.srte-toolbar select:focus-visible,\n.srte-menu-item:focus-visible {\n outline: 2px solid var(--srte-ring);\n outline-offset: 1px;\n}\n.srte-tool-button:disabled,\n.srte-menu-item:disabled,\n.srte-toolbar select:disabled {\n cursor: not-allowed;\n opacity: .4;\n}\n.srte-toolbar-menu {\n position: relative;\n}\n.srte-toolbar-menu > summary {\n list-style: none;\n}\n.srte-toolbar-menu > summary::-webkit-details-marker {\n display: none;\n}\n.srte-menu {\n position: absolute;\n top: calc(100% + 6px);\n left: 0;\n z-index: 80;\n min-width: 210px;\n padding: 4px;\n overflow: hidden;\n border: 1px solid var(--srte-border);\n border-radius: 12px;\n background: var(--srte-menu-bg);\n color: var(--srte-menu-text);\n box-shadow: var(--srte-menu-shadow);\n}\n/*\n * ToolbarDropdown's own menu panel (docs/bugs/\n * toolbar-dropdown-clipped-by-host-overflow-hidden.md): position:fixed with\n * JS-measured left/top (ToolbarPrimitives.tsx) instead of the default\n * position:absolute above, so it always escapes an ancestor host container's\n * own overflow:hidden regardless of DOM depth - MobileMoreMenu's overflow\n * menu (no data-srte-menu-fixed attribute) is unaffected and keeps its\n * existing position:absolute/right-anchored behavior.\n */\n.srte-menu[data-srte-menu-fixed=\"true\"] {\n position: fixed;\n top: 0;\n left: 0;\n}\n.srte-menu-item {\n display: flex;\n align-items: center;\n gap: 8px;\n width: 100%;\n height: 36px;\n padding: 0 8px;\n border: 0;\n border-radius: 8px;\n background: transparent;\n color: inherit;\n cursor: pointer;\n font: 500 13px/1 \"IBM Plex Sans\", ui-sans-serif, system-ui, sans-serif;\n text-align: left;\n white-space: nowrap;\n}\n.srte-menu-item:hover {\n background: var(--srte-muted);\n}\n.srte-menu-check {\n margin-left: auto;\n color: var(--srte-primary);\n font-weight: 700;\n}\n.srte-menu-separator {\n height: 1px;\n margin: 4px;\n background: var(--srte-border);\n}\n.srte-mobile-more { display: none; }\n.srte-toolbar .srte-command-proxy { display: none; }\n.srte-split-control {\n display: inline-flex;\n}\n.srte-split-control > .srte-tool-button:first-child {\n border-radius: 8px 0 0 8px;\n}\n.srte-split-control > select {\n width: 27px;\n padding: 0;\n border-radius: 0 8px 8px 0;\n border-left: 0;\n appearance: none;\n -webkit-appearance: none;\n color: transparent;\n background-color: var(--srte-muted);\n background-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='14' height='14' viewBox='0 0 24 24' fill='none' stroke='%2364748b' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m7 10 5 5 5-5'/%3E%3C/svg%3E\");\n background-repeat: no-repeat;\n background-position: center;\n cursor: pointer;\n}\n.srte-editor [contenteditable] ul[data-srte-checklist=\"true\"] > li {\n display: grid;\n grid-template-columns: 1.1em minmax(0, 1fr);\n column-gap: .45em;\n align-items: start;\n padding-left: 0;\n}\n.srte-editor [contenteditable] ol[data-srte-list-preset=\"ordered-decimal-paren\"][data-srte-list-depth=\"0\"] > li::marker {\n content: counter(list-item, decimal) \") \";\n}\n.srte-editor [contenteditable] ol[data-srte-list-preset=\"ordered-decimal-paren\"][data-srte-list-depth=\"1\"] > li::marker {\n content: counter(list-item, lower-alpha) \") \";\n}\n.srte-editor [contenteditable] ol[data-srte-list-preset=\"ordered-decimal-paren\"][data-srte-list-depth=\"2\"] > li::marker,\n.srte-editor [contenteditable] ol[data-srte-list-preset=\"ordered-decimal-paren\"][data-srte-list-depth=\"3\"] > li::marker {\n content: counter(list-item, lower-roman) \") \";\n}\n.srte-editor [contenteditable] ol[data-srte-list-preset=\"ordered-decimal\"][data-srte-list-depth=\"0\"] > li::marker {\n content: counter(list-item, decimal) \". \";\n}\n.srte-editor [contenteditable] ol[data-srte-list-preset=\"ordered-decimal\"][data-srte-list-depth=\"1\"] > li::marker {\n content: counter(list-item, lower-alpha) \". \";\n}\n.srte-editor [contenteditable] ol[data-srte-list-preset=\"ordered-decimal\"][data-srte-list-depth=\"2\"] > li::marker,\n.srte-editor [contenteditable] ol[data-srte-list-preset=\"ordered-decimal\"][data-srte-list-depth=\"3\"] > li::marker {\n content: counter(list-item, lower-roman) \". \";\n}\n.srte-editor [contenteditable] ol[data-srte-list-preset=\"ordered-upper-alpha\"][data-srte-list-depth=\"0\"] > li::marker {\n content: counter(list-item, upper-alpha) \". \";\n}\n.srte-editor [contenteditable] ol[data-srte-list-preset=\"ordered-upper-alpha\"][data-srte-list-depth=\"1\"] > li::marker {\n content: counter(list-item, lower-alpha) \". \";\n}\n.srte-editor [contenteditable] ol[data-srte-list-preset=\"ordered-upper-alpha\"][data-srte-list-depth=\"2\"] > li::marker,\n.srte-editor [contenteditable] ol[data-srte-list-preset=\"ordered-upper-alpha\"][data-srte-list-depth=\"3\"] > li::marker {\n content: counter(list-item, lower-roman) \". \";\n}\n.srte-editor [contenteditable] ol[data-srte-list-preset=\"ordered-upper-roman\"][data-srte-list-depth=\"0\"] > li::marker {\n content: counter(list-item, upper-roman) \". \";\n}\n.srte-editor [contenteditable] ol[data-srte-list-preset=\"ordered-upper-roman\"][data-srte-list-depth=\"1\"] > li::marker {\n content: counter(list-item, upper-alpha) \". \";\n}\n.srte-editor [contenteditable] ol[data-srte-list-preset=\"ordered-upper-roman\"][data-srte-list-depth=\"2\"] > li::marker,\n.srte-editor [contenteditable] ol[data-srte-list-preset=\"ordered-upper-roman\"][data-srte-list-depth=\"3\"] > li::marker {\n content: counter(list-item, decimal) \". \";\n}\n.srte-editor [contenteditable] ol[data-srte-list-preset=\"ordered-leading-zero\"][data-srte-list-depth=\"0\"] > li::marker {\n content: counter(list-item, decimal-leading-zero) \". \";\n}\n.srte-editor [contenteditable] ol[data-srte-list-preset=\"ordered-leading-zero\"][data-srte-list-depth=\"1\"] > li::marker {\n content: counter(list-item, lower-alpha) \". \";\n}\n.srte-editor [contenteditable] ol[data-srte-list-preset=\"ordered-leading-zero\"][data-srte-list-depth=\"2\"] > li::marker,\n.srte-editor [contenteditable] ol[data-srte-list-preset=\"ordered-leading-zero\"][data-srte-list-depth=\"3\"] > li::marker {\n content: counter(list-item, lower-roman) \". \";\n}\n.srte-editor [contenteditable] ul[data-srte-list-preset=\"bullet-disc\"][data-srte-list-depth=\"0\"] > li::marker {\n content: \"\u25CF \";\n}\n.srte-editor [contenteditable] ul[data-srte-list-preset=\"bullet-disc\"][data-srte-list-depth=\"1\"] > li::marker {\n content: \"\u25CB \";\n}\n.srte-editor [contenteditable] ul[data-srte-list-preset=\"bullet-disc\"][data-srte-list-depth=\"2\"] > li::marker,\n.srte-editor [contenteditable] ul[data-srte-list-preset=\"bullet-disc\"][data-srte-list-depth=\"3\"] > li::marker {\n content: \"\u25A0 \";\n}\n.srte-editor [contenteditable] ol[data-srte-list-preset=\"ordered-outline\"] > li::marker {\n content: counters(list-item, \".\") \". \";\n}\n.srte-editor [contenteditable] ul[data-srte-list-preset=\"bullet-diamond\"][data-srte-list-depth=\"0\"] > li::marker { content: \"\u2756 \"; }\n.srte-editor [contenteditable] ul[data-srte-list-preset=\"bullet-diamond\"][data-srte-list-depth=\"1\"] > li::marker { content: \"\u27A2 \"; }\n.srte-editor [contenteditable] ul[data-srte-list-preset=\"bullet-diamond\"][data-srte-list-depth=\"2\"] > li::marker { content: \"\u25A0 \"; }\n.srte-editor [contenteditable] ul[data-srte-list-preset=\"bullet-square\"][data-srte-list-depth=\"0\"] > li::marker { content: \"\u25A1 \"; }\n.srte-editor [contenteditable] ul[data-srte-list-preset=\"bullet-square\"][data-srte-list-depth=\"1\"] > li::marker { content: \"\u25A3 \"; }\n.srte-editor [contenteditable] ul[data-srte-list-preset=\"bullet-square\"][data-srte-list-depth=\"2\"] > li::marker { content: \"\u25AA \"; }\n.srte-editor [contenteditable] ul[data-srte-list-preset=\"bullet-arrow\"][data-srte-list-depth=\"0\"] > li::marker { content: \"\u279C \"; }\n.srte-editor [contenteditable] ul[data-srte-list-preset=\"bullet-arrow\"][data-srte-list-depth=\"1\"] > li::marker { content: \"\u25C6 \"; }\n.srte-editor [contenteditable] ul[data-srte-list-preset=\"bullet-arrow\"][data-srte-list-depth=\"2\"] > li::marker { content: \"\u25CF \"; }\n.srte-editor [contenteditable] ul[data-srte-list-preset=\"bullet-star\"][data-srte-list-depth=\"0\"] > li::marker { content: \"\u2605 \"; }\n.srte-editor [contenteditable] ul[data-srte-list-preset=\"bullet-star\"][data-srte-list-depth=\"1\"] > li::marker { content: \"\u25CB \"; }\n.srte-editor [contenteditable] ul[data-srte-list-preset=\"bullet-star\"][data-srte-list-depth=\"2\"] > li::marker { content: \"\u25A0 \"; }\n.srte-editor [contenteditable] ul[data-srte-list-preset=\"bullet-arrow-circle\"][data-srte-list-depth=\"0\"] > li::marker { content: \"\u27A2 \"; }\n.srte-editor [contenteditable] ul[data-srte-list-preset=\"bullet-arrow-circle\"][data-srte-list-depth=\"1\"] > li::marker { content: \"\u25CB \"; }\n.srte-editor [contenteditable] ul[data-srte-list-preset=\"bullet-arrow-circle\"][data-srte-list-depth=\"2\"] > li::marker { content: \"\u25A0 \"; }\n.srte-editor [contenteditable] ul[data-srte-checklist=\"true\"] > li > [data-srte-check] {\n display: inline-flex;\n grid-column: 1;\n grid-row: 1;\n align-items: center;\n justify-content: center;\n width: 1.1em;\n height: 1.6em;\n min-width: 0;\n box-sizing: border-box;\n padding: 0;\n border: 0;\n border-radius: 0;\n background: transparent;\n color: inherit;\n font: inherit;\n cursor: pointer;\n appearance: none;\n outline: none;\n margin: 0;\n line-height: 1.6;\n vertical-align: top;\n position: relative;\n}\n.srte-editor [contenteditable] ul[data-srte-checklist=\"true\"] > li > [data-srte-check]:focus-visible {\n outline: 2px solid var(--srte-ring);\n outline-offset: 2px;\n}\n.srte-editor [contenteditable] ul[data-srte-checklist=\"true\"] > li > [data-srte-check]::before {\n content: \"\";\n width: .9em;\n height: .9em;\n box-sizing: border-box;\n border: 1.5px solid var(--srte-muted-foreground);\n border-radius: 3px;\n background: var(--srte-canvas);\n}\n.srte-editor [contenteditable] ul[data-srte-checklist=\"true\"] > li > [data-srte-check][data-checked=\"true\"]::before {\n border-color: var(--srte-primary);\n background: var(--srte-primary);\n box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--srte-on-primary) 25%, transparent);\n}\n.srte-editor [contenteditable] ul[data-srte-checklist=\"true\"] > li > [data-srte-check][data-checked=\"true\"]::after {\n content: \"\";\n position: absolute;\n width: .42em;\n height: .22em;\n border-left: 1.5px solid var(--srte-on-primary);\n border-bottom: 1.5px solid var(--srte-on-primary);\n transform: translateY(-.08em) rotate(-45deg);\n}\n.srte-editor [contenteditable] ul[data-srte-checklist=\"true\"] > li > :is(p,h1,h2,h3,h4,h5,h6,blockquote,pre) {\n grid-column: 2;\n min-width: 0;\n margin-top: 0;\n}\n.srte-editor [contenteditable] ul[data-srte-checklist=\"true\"] > li > :is(ul,ol) {\n grid-column: 2;\n}\n.srte-editor:focus-within {\n border-color: var(--srte-ring) !important;\n box-shadow: 0 0 0 2px color-mix(in srgb, var(--srte-ring) 18%, transparent);\n}\n/*\n * @container (element width), not @media (viewport width): a host can embed\n * the editor at any fraction of the browser window - a split pane next to a\n * preview panel is the case that surfaced this (docs/bugs/\n * toolbar-wide-promotion-fires-on-viewport-width-not-container-width.md). A\n * plain @media breakpoint here would fire based on the *window*, wrongly\n * switching tiers for an editor instance that's actually much narrower (or\n * wider) than the window - .srte-editor already establishes its own\n * container: srte-editor / inline-size (see the .srte-editor rule above),\n * so @container always reflects this specific instance's real rendered width regardless of host\n * layout, with no @media fallback needed (container queries have full\n * support across this project's three target engines).\n */\n@container srte-editor (max-width: 639px) {\n .srte-toolbar { gap: 3px; padding: 6px; }\n .srte-tool-button, .srte-toolbar select { height: 40px; min-width: 40px; }\n .srte-toolbar-group[data-srte-priority=\"3\"],\n .srte-toolbar-menu[data-srte-priority=\"2\"] { display: none; }\n .srte-mobile-more { display: block; }\n /* left/top are no longer set here - ToolbarPrimitives.tsx now computes\n them via inline style (JS-measured, position:fixed, viewport-clamped;\n see MobileMoreMenu's own doc comment), which always wins over any\n value set here regardless of specificity or source order. Sizing/\n scroll behavior stays defined here since inline positioning doesn't\n touch those properties. */\n .srte-mobile-more .srte-menu {\n width: min(280px, calc(100vw - 16px));\n min-width: 0;\n max-height: min(70dvh, 480px);\n overflow-x: hidden;\n overflow-y: auto;\n overscroll-behavior: contain;\n -webkit-overflow-scrolling: touch;\n }\n .srte-menu-item { height: 40px; }\n}\n/*\n * Wide-viewport promotion (docs/bugs/toolbar-priority-collapse-fixed-threshold-no-wide-promotion.md):\n * below this, the single 639px breakpoint above was this system's ONLY\n * threshold - every dropdown-grouped tool stayed hidden inside its dropdown\n * at every width from 640px up to and past a 2200px+ desktop, since nothing\n * ever measured or scaled with the extra room. A handful of tools frequent\n * enough to matter once there's genuinely spare room (Superscript,\n * Subscript, Text colour, Background colour, Font size, Font family, Remove\n * link, Insert formula, Special characters) get a standalone always-visible\n * ToolbarButton copy (data-srte-wide-promote, rendered directly in the\n * toolbar row) that appears past this breakpoint, while their existing\n * dropdown/mobile-menu ToolbarMenuItem copy (the exact same\n * data-srte-wide-promote attribute, on the .srte-menu-item element instead)\n * hides so the tool isn't offered twice. Below this breakpoint - including\n * all of mobile AND the 640-1439px tablet/typical-laptop band, which keeps\n * today's grouped-dropdown layout unchanged - the standalone copy stays\n * hidden and the dropdown copy is what's reachable.\n *\n * 1440px, not 1280px: Playwright's own default test viewport is exactly\n * 1280x720, and the vast majority of this suite's toolbar interactions run\n * at that default without ever calling page.setViewportSize - a 1280px\n * threshold would have flipped nearly every existing toolbar test's\n * dropdown-item locators (role=menuitem) over to CSS display:none out from\n * under them. 1440px clears that default with room to spare while still\n * comfortably covering \"wide desktop\" (the ~2264px width the original\n * report was filed against).\n *\n * @container only, no @media fallback - same reasoning as the mobile tier\n * above (docs/bugs/toolbar-wide-promotion-fires-on-viewport-width-not-container-width.md):\n * a plain @media(min-width:1440px) fired whenever the *browser window* was\n * wide, even when this editor instance was embedded in a much narrower\n * split pane - promoting 9 extra buttons into a group with no room for\n * them, which squeezed/overlapped the group's existing buttons instead of\n * wrapping cleanly.\n */\n.srte-tool-button[data-srte-wide-promote=\"true\"] { display: none; }\n@container srte-editor (min-width: 1440px) {\n .srte-tool-button[data-srte-wide-promote=\"true\"] { display: inline-flex; }\n .srte-menu-item[data-srte-wide-promote=\"true\"] { display: none; }\n}\n.srte-editor [contenteditable] blockquote {\n border-left: 4px solid var(--srte-accent);\n margin: 0.75em 0;\n padding: 0.5em 1em;\n /* --srte-muted, not the retired --srte-surface-subtle (docs/bugs/\n srte-surface-subtle-not-host-theme-aware.md): every other neutral\n surface/text token here (--srte-background, --srte-foreground,\n --srte-muted, --srte-border) is defined as var(--host-token, fallback)\n so it correctly follows a host's OWN light/dark state even if\n .srte-dark never gets applied for some host-side wiring reason -\n --srte-surface-subtle was the one exception, a bare hardcoded hex with\n no host integration, which is exactly what let a real host (dark mode,\n .srte-dark not reaching this editor instance) render this blockquote\n with a light-mode background and dark-mode text simultaneously. */\n background: var(--srte-muted);\n color: var(--srte-text);\n}\n.srte-editor [contenteditable] p,\n.srte-editor [contenteditable] h1,\n.srte-editor [contenteditable] h2,\n.srte-editor [contenteditable] h3 {\n color: inherit;\n}\n.srte-editor [contenteditable] p {\n display: block;\n margin: 0 0 0.75em;\n font-size: 1em;\n font-weight: 400;\n line-height: 1.6;\n}\n.srte-editor [contenteditable] p[data-srte-caret-boundary=\"true\"] {\n min-height: 1.6em;\n}\n.srte-editor [contenteditable] h1,\n.srte-editor [contenteditable] h2,\n.srte-editor [contenteditable] h3 {\n display: block;\n margin: 0.75em 0 0.4em;\n font-weight: 700;\n line-height: 1.25;\n}\n.srte-editor [contenteditable] h1 {\n font-size: 2em;\n}\n.srte-editor [contenteditable] h2 {\n font-size: 1.5em;\n}\n.srte-editor [contenteditable] h3 {\n font-size: 1.25em;\n}\n.srte-editor [contenteditable] > :first-child {\n margin-top: 0;\n}\n.srte-editor [contenteditable] ul {\n list-style-type: disc;\n list-style-position: outside;\n margin: 0.75em 0;\n padding-left: 1.75em;\n}\n.srte-editor [contenteditable] ol {\n list-style-type: decimal;\n list-style-position: outside;\n margin: 0.75em 0;\n padding-left: 1.75em;\n}\n.srte-editor [contenteditable] li {\n display: list-item;\n margin: 0.25em 0;\n padding-left: 0.25em;\n}\n.srte-editor [contenteditable] li::marker {\n color: currentColor;\n}\n.srte-editor [contenteditable] table {\n width: 100%;\n margin: 0.75em 0;\n border-collapse: collapse;\n}\n.srte-editor [contenteditable] th,\n.srte-editor [contenteditable] td {\n padding: 8px;\n border: 1px solid var(--srte-border);\n vertical-align: top;\n}\n.srte-editor [contenteditable] [data-smart-cell-selected=\"true\"] {\n background: color-mix(in srgb, var(--srte-primary) 12%, var(--srte-canvas));\n box-shadow: inset 0 0 0 2px color-mix(in srgb, var(--srte-primary) 65%, transparent);\n}\n.srte-editor [contenteditable][data-smart-cell-selection-active] ::selection {\n background: transparent;\n color: inherit;\n}\n.srte-editor [contenteditable] th {\n background: var(--srte-muted);\n font-weight: 600;\n text-align: left;\n}\n.srte-editor [contenteditable] td > h1,\n.srte-editor [contenteditable] td > h2,\n.srte-editor [contenteditable] td > h3,\n.srte-editor [contenteditable] th > h1,\n.srte-editor [contenteditable] th > h2,\n.srte-editor [contenteditable] th > h3 {\n margin: 0 0 0.4em;\n overflow-wrap: anywhere;\n}\n.srte-editor [contenteditable] pre {\n display: block;\n margin: 0.75em 0;\n padding: 12px 14px;\n overflow-x: auto;\n border: 1px solid var(--srte-border);\n border-radius: 6px;\n background: var(--srte-code-bg);\n color: var(--srte-code-text);\n font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, \"Liberation Mono\", monospace;\n font-size: 0.9em;\n line-height: 1.55;\n white-space: pre-wrap;\n}\n.srte-editor [contenteditable] pre code {\n padding: 0;\n border: 0;\n background: transparent;\n color: inherit;\n font: inherit;\n}\n.srte-editor [contenteditable] code:not(pre code) {\n padding: 0.1em 0.35em;\n border-radius: 3px;\n background: var(--srte-code-bg);\n color: var(--srte-code-text);\n font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, \"Liberation Mono\", monospace;\n font-size: 0.9em;\n}\n.srte-editor [contenteditable] a,\n.srte-editor [contenteditable] a:visited {\n color: var(--srte-primary) !important;\n text-decoration: underline !important;\n text-decoration-thickness: 1px !important;\n text-underline-offset: 2px !important;\n cursor: pointer;\n}\n.srte-editor [contenteditable] a:hover {\n color: var(--srte-accent) !important;\n}\n.srte-editor [contenteditable] a:focus-visible {\n outline: 2px solid var(--srte-accent);\n outline-offset: 2px;\n}\n.srte-editor [contenteditable] sub,\n.srte-editor [contenteditable] sup {\n line-height: 0;\n}\n/*\n * A page break has no native visual meaning in a continuously-scrolling\n * web editor - its entire purpose is what happens at export/print time\n * (see atom/formats.ts's atomToHtml doc comment). This gives it a clearly\n * distinct live-editor appearance from the plain solid <hr> a horizontal\n * line renders as by default, so the two are never confused: a dashed\n * line with an explicit \"Page break\" label, per the feature's own design\n * decision.\n */\n.srte-editor [contenteditable] [data-smart-type=\"page_break\"] {\n display: block;\n position: relative;\n height: 0;\n margin: 1.5em 0;\n border: none;\n border-top: 2px dashed var(--srte-border);\n cursor: default;\n}\n.srte-editor [contenteditable] [data-smart-type=\"page_break\"]::after {\n content: \"Page break\";\n position: absolute;\n top: -0.7em;\n left: 50%;\n transform: translateX(-50%);\n background: var(--srte-canvas);\n padding: 0 0.6em;\n font-size: 11px;\n font-weight: 600;\n text-transform: uppercase;\n letter-spacing: 0.05em;\n color: var(--srte-text-muted);\n white-space: nowrap;\n}\n";
2
+ export declare const SRTE_DEFAULT_CSS = "\n.srte-editor {\n --srte-background: var(--card, #ffffff);\n --srte-canvas: var(--background, #ffffff);\n --srte-foreground: var(--foreground, #0f172a);\n --srte-muted: var(--muted, #f1f5f9);\n --srte-muted-foreground: var(--muted-foreground, #64748b);\n --srte-ring: var(--ring, #0284c7);\n --srte-radius: var(--radius, 0.625rem);\n --srte-bg: var(--srte-canvas);\n --srte-text: var(--srte-foreground);\n --srte-text-muted: var(--srte-muted-foreground);\n --srte-border: var(--border, #e2e8f0);\n --srte-border-light: var(--srte-border);\n --srte-toolbar-bg: var(--srte-background);\n --srte-input-bg: var(--srte-background);\n --srte-input-text: var(--srte-foreground);\n --srte-input-border: var(--srte-border);\n --srte-modal-backdrop: rgba(0, 0, 0, 0.35);\n --srte-modal-backdrop-filter: blur(2px);\n --srte-modal-bg: var(--card, #ffffff);\n --srte-modal-text: var(--foreground, #000000);\n --srte-menu-bg: var(--srte-background);\n --srte-menu-text: var(--srte-foreground);\n --srte-menu-shadow: 0 8px 24px rgba(0, 0, 0, 0.18);\n --srte-accent: #0284c7;\n --srte-accent-bg: rgba(2, 132, 199, 0.12);\n --srte-danger: #dc2626;\n --srte-primary: #2563eb;\n /* --srte-primary on --srte-accent-bg measures 4.43:1, under WCAG AA's\n 4.5:1 for normal text - used only for the pressed toolbar-button state\n (see .srte-tool-button[aria-pressed=\"true\"]), not a replacement for\n --srte-primary generally. */\n --srte-primary-pressed: #1d4ed8;\n --srte-on-primary: #ffffff;\n --srte-cancel-bg: #f3f4f6;\n --srte-code-bg: #f6f8fa;\n --srte-code-text: #24292f;\n}\n.srte-editor.srte-dark {\n --srte-background: var(--card, #1e293b);\n --srte-canvas: var(--background, #0f172a);\n --srte-foreground: var(--foreground, #f8fafc);\n --srte-muted: var(--muted, #334155);\n --srte-muted-foreground: var(--muted-foreground, #94a3b8);\n --srte-ring: var(--ring, #38bdf8);\n --srte-bg: var(--srte-canvas);\n --srte-text: var(--srte-foreground);\n --srte-text-muted: var(--srte-muted-foreground);\n --srte-border: var(--border, #334155);\n --srte-border-light: var(--srte-border);\n --srte-toolbar-bg: var(--srte-background);\n --srte-input-bg: var(--srte-background);\n --srte-input-text: var(--srte-foreground);\n --srte-input-border: var(--srte-border);\n --srte-modal-backdrop: rgba(0, 0, 0, 0.22);\n --srte-modal-backdrop-filter: blur(10px) saturate(0.9);\n --srte-modal-bg: var(--card, #1e293b);\n --srte-modal-text: var(--foreground, #e0e0e0);\n --srte-menu-bg: var(--srte-background);\n --srte-menu-text: var(--srte-foreground);\n --srte-menu-shadow: 0 8px 24px rgba(0, 0, 0, 0.4);\n --srte-accent: #38bdf8;\n --srte-accent-bg: rgba(56, 189, 248, 0.16);\n --srte-danger: #ef4444;\n --srte-primary: #3b82f6;\n --srte-primary-pressed: var(--srte-primary);\n --srte-on-primary: #ffffff;\n --srte-cancel-bg: #333333;\n --srte-code-bg: #111827;\n --srte-code-text: #e5e7eb;\n}\n.srte-editor {\n border-radius: var(--srte-radius);\n font-family: \"IBM Plex Sans\", ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, \"Segoe UI\", sans-serif;\n container: srte-editor / inline-size;\n}\n.srte-canonical-authority > .srte-editor[contenteditable] {\n width: 100%;\n padding: 16px 20px;\n box-sizing: border-box;\n border: 1px solid var(--srte-border);\n border-radius: 0 0 var(--srte-radius) var(--srte-radius);\n outline: none;\n background: var(--srte-canvas);\n color: var(--srte-foreground);\n caret-color: var(--srte-foreground);\n line-height: 1.6;\n}\n.srte-toolbar {\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n gap: 4px;\n width: 100%;\n min-height: 48px;\n padding: 8px;\n box-sizing: border-box;\n border-bottom: 1px solid var(--srte-border);\n background: var(--srte-background);\n color: var(--srte-foreground);\n position: sticky;\n top: 0;\n z-index: 10;\n}\n.srte-toolbar-group {\n display: inline-flex;\n align-items: center;\n gap: 2px;\n min-width: 0;\n}\n.srte-toolbar-group + .srte-toolbar-group::before {\n content: \"\";\n width: 1px;\n height: 20px;\n margin: 0 5px 0 3px;\n background: var(--srte-border);\n}\n.srte-tool-button,\n.srte-toolbar select {\n height: 32px;\n min-width: 32px;\n box-sizing: border-box;\n border: 1px solid transparent;\n border-radius: 8px;\n background: transparent;\n color: var(--srte-foreground);\n font: 500 13px/1 \"IBM Plex Sans\", ui-sans-serif, system-ui, sans-serif;\n}\n.srte-tool-button {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n gap: 3px;\n padding: 0 7px;\n cursor: pointer;\n}\n.srte-toolbar select {\n max-width: 132px;\n padding: 0 26px 0 9px;\n border-color: var(--srte-border);\n background: var(--srte-input-bg);\n cursor: pointer;\n}\n.srte-tool-button:hover,\n.srte-toolbar select:hover,\n.srte-toolbar-menu[open] > .srte-menu-trigger {\n background: var(--srte-muted);\n border-color: var(--srte-border);\n}\n.srte-tool-button.srte-active,\n.srte-tool-button[aria-pressed=\"true\"] {\n /* var(--srte-primary) (#2563eb) on var(--srte-accent-bg) measured 4.43:1,\n just under WCAG AA's 4.5:1 for normal text (axe-core, Phase 11 Tier 3) -\n a dedicated, darker pressed-state color keeps the same hue family\n while clearing the threshold, without changing --srte-primary's other,\n already-compliant usages (e.g. against solid backgrounds). */\n color: var(--srte-primary-pressed);\n background: var(--srte-accent-bg);\n border-color: color-mix(in srgb, var(--srte-primary) 35%, transparent);\n}\n.srte-tool-button:focus-visible,\n.srte-toolbar select:focus-visible,\n.srte-menu-item:focus-visible {\n outline: 2px solid var(--srte-ring);\n outline-offset: 1px;\n}\n.srte-tool-button:disabled,\n.srte-menu-item:disabled,\n.srte-toolbar select:disabled {\n cursor: not-allowed;\n opacity: .4;\n}\n.srte-toolbar-menu {\n position: relative;\n}\n.srte-toolbar-menu > summary {\n list-style: none;\n}\n.srte-toolbar-menu > summary::-webkit-details-marker {\n display: none;\n}\n.srte-menu {\n position: absolute;\n top: calc(100% + 6px);\n left: 0;\n z-index: 80;\n min-width: 210px;\n padding: 4px;\n overflow: hidden;\n border: 1px solid var(--srte-border);\n border-radius: 12px;\n background: var(--srte-menu-bg);\n color: var(--srte-menu-text);\n box-shadow: var(--srte-menu-shadow);\n}\n/*\n * ToolbarDropdown's own menu panel (docs/bugs/\n * toolbar-dropdown-clipped-by-host-overflow-hidden.md): position:fixed with\n * JS-measured left/top (ToolbarPrimitives.tsx) instead of the default\n * position:absolute above, so it always escapes an ancestor host container's\n * own overflow:hidden regardless of DOM depth - MobileMoreMenu's overflow\n * menu (no data-srte-menu-fixed attribute) is unaffected and keeps its\n * existing position:absolute/right-anchored behavior.\n */\n.srte-menu[data-srte-menu-fixed=\"true\"] {\n position: fixed;\n top: 0;\n left: 0;\n}\n.srte-menu-item {\n display: flex;\n align-items: center;\n gap: 8px;\n width: 100%;\n height: 36px;\n padding: 0 8px;\n border: 0;\n border-radius: 8px;\n background: transparent;\n color: inherit;\n cursor: pointer;\n font: 500 13px/1 \"IBM Plex Sans\", ui-sans-serif, system-ui, sans-serif;\n text-align: left;\n white-space: nowrap;\n}\n.srte-menu-item:hover {\n background: var(--srte-muted);\n}\n.srte-menu-check {\n margin-left: auto;\n color: var(--srte-primary);\n font-weight: 700;\n}\n.srte-menu-separator {\n height: 1px;\n margin: 4px;\n background: var(--srte-border);\n}\n.srte-mobile-more { display: none; }\n.srte-toolbar .srte-command-proxy { display: none; }\n.srte-split-control {\n display: inline-flex;\n}\n.srte-split-control > .srte-tool-button:first-child {\n border-radius: 8px 0 0 8px;\n}\n.srte-split-control > select {\n width: 27px;\n padding: 0;\n border-radius: 0 8px 8px 0;\n border-left: 0;\n appearance: none;\n -webkit-appearance: none;\n color: transparent;\n background-color: var(--srte-muted);\n background-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='14' height='14' viewBox='0 0 24 24' fill='none' stroke='%2364748b' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m7 10 5 5 5-5'/%3E%3C/svg%3E\");\n background-repeat: no-repeat;\n background-position: center;\n cursor: pointer;\n}\n.srte-editor [contenteditable] ul[data-srte-checklist=\"true\"] > li {\n display: grid;\n grid-template-columns: 1.1em minmax(0, 1fr);\n column-gap: .45em;\n align-items: start;\n padding-left: 0;\n}\n.srte-editor [contenteditable] ol[data-srte-list-preset=\"ordered-decimal-paren\"][data-srte-list-depth=\"0\"] > li::marker {\n content: counter(list-item, decimal) \") \";\n}\n.srte-editor [contenteditable] ol[data-srte-list-preset=\"ordered-decimal-paren\"][data-srte-list-depth=\"1\"] > li::marker {\n content: counter(list-item, lower-alpha) \") \";\n}\n.srte-editor [contenteditable] ol[data-srte-list-preset=\"ordered-decimal-paren\"][data-srte-list-depth=\"2\"] > li::marker,\n.srte-editor [contenteditable] ol[data-srte-list-preset=\"ordered-decimal-paren\"][data-srte-list-depth=\"3\"] > li::marker {\n content: counter(list-item, lower-roman) \") \";\n}\n.srte-editor [contenteditable] ol[data-srte-list-preset=\"ordered-decimal\"][data-srte-list-depth=\"0\"] > li::marker {\n content: counter(list-item, decimal) \". \";\n}\n.srte-editor [contenteditable] ol[data-srte-list-preset=\"ordered-decimal\"][data-srte-list-depth=\"1\"] > li::marker {\n content: counter(list-item, lower-alpha) \". \";\n}\n.srte-editor [contenteditable] ol[data-srte-list-preset=\"ordered-decimal\"][data-srte-list-depth=\"2\"] > li::marker,\n.srte-editor [contenteditable] ol[data-srte-list-preset=\"ordered-decimal\"][data-srte-list-depth=\"3\"] > li::marker {\n content: counter(list-item, lower-roman) \". \";\n}\n.srte-editor [contenteditable] ol[data-srte-list-preset=\"ordered-upper-alpha\"][data-srte-list-depth=\"0\"] > li::marker {\n content: counter(list-item, upper-alpha) \". \";\n}\n.srte-editor [contenteditable] ol[data-srte-list-preset=\"ordered-upper-alpha\"][data-srte-list-depth=\"1\"] > li::marker {\n content: counter(list-item, lower-alpha) \". \";\n}\n.srte-editor [contenteditable] ol[data-srte-list-preset=\"ordered-upper-alpha\"][data-srte-list-depth=\"2\"] > li::marker,\n.srte-editor [contenteditable] ol[data-srte-list-preset=\"ordered-upper-alpha\"][data-srte-list-depth=\"3\"] > li::marker {\n content: counter(list-item, lower-roman) \". \";\n}\n.srte-editor [contenteditable] ol[data-srte-list-preset=\"ordered-upper-roman\"][data-srte-list-depth=\"0\"] > li::marker {\n content: counter(list-item, upper-roman) \". \";\n}\n.srte-editor [contenteditable] ol[data-srte-list-preset=\"ordered-upper-roman\"][data-srte-list-depth=\"1\"] > li::marker {\n content: counter(list-item, upper-alpha) \". \";\n}\n.srte-editor [contenteditable] ol[data-srte-list-preset=\"ordered-upper-roman\"][data-srte-list-depth=\"2\"] > li::marker,\n.srte-editor [contenteditable] ol[data-srte-list-preset=\"ordered-upper-roman\"][data-srte-list-depth=\"3\"] > li::marker {\n content: counter(list-item, decimal) \". \";\n}\n.srte-editor [contenteditable] ol[data-srte-list-preset=\"ordered-leading-zero\"][data-srte-list-depth=\"0\"] > li::marker {\n content: counter(list-item, decimal-leading-zero) \". \";\n}\n.srte-editor [contenteditable] ol[data-srte-list-preset=\"ordered-leading-zero\"][data-srte-list-depth=\"1\"] > li::marker {\n content: counter(list-item, lower-alpha) \". \";\n}\n.srte-editor [contenteditable] ol[data-srte-list-preset=\"ordered-leading-zero\"][data-srte-list-depth=\"2\"] > li::marker,\n.srte-editor [contenteditable] ol[data-srte-list-preset=\"ordered-leading-zero\"][data-srte-list-depth=\"3\"] > li::marker {\n content: counter(list-item, lower-roman) \". \";\n}\n.srte-editor [contenteditable] ul[data-srte-list-preset=\"bullet-disc\"][data-srte-list-depth=\"0\"] > li::marker {\n content: \"\u25CF \";\n}\n.srte-editor [contenteditable] ul[data-srte-list-preset=\"bullet-disc\"][data-srte-list-depth=\"1\"] > li::marker {\n content: \"\u25CB \";\n}\n.srte-editor [contenteditable] ul[data-srte-list-preset=\"bullet-disc\"][data-srte-list-depth=\"2\"] > li::marker,\n.srte-editor [contenteditable] ul[data-srte-list-preset=\"bullet-disc\"][data-srte-list-depth=\"3\"] > li::marker {\n content: \"\u25A0 \";\n}\n.srte-editor [contenteditable] ol[data-srte-list-preset=\"ordered-outline\"] > li::marker {\n content: counters(list-item, \".\") \". \";\n}\n.srte-editor [contenteditable] ul[data-srte-list-preset=\"bullet-diamond\"][data-srte-list-depth=\"0\"] > li::marker { content: \"\u2756 \"; }\n.srte-editor [contenteditable] ul[data-srte-list-preset=\"bullet-diamond\"][data-srte-list-depth=\"1\"] > li::marker { content: \"\u27A2 \"; }\n.srte-editor [contenteditable] ul[data-srte-list-preset=\"bullet-diamond\"][data-srte-list-depth=\"2\"] > li::marker { content: \"\u25A0 \"; }\n.srte-editor [contenteditable] ul[data-srte-list-preset=\"bullet-square\"][data-srte-list-depth=\"0\"] > li::marker { content: \"\u25A1 \"; }\n.srte-editor [contenteditable] ul[data-srte-list-preset=\"bullet-square\"][data-srte-list-depth=\"1\"] > li::marker { content: \"\u25A3 \"; }\n.srte-editor [contenteditable] ul[data-srte-list-preset=\"bullet-square\"][data-srte-list-depth=\"2\"] > li::marker { content: \"\u25AA \"; }\n.srte-editor [contenteditable] ul[data-srte-list-preset=\"bullet-arrow\"][data-srte-list-depth=\"0\"] > li::marker { content: \"\u279C \"; }\n.srte-editor [contenteditable] ul[data-srte-list-preset=\"bullet-arrow\"][data-srte-list-depth=\"1\"] > li::marker { content: \"\u25C6 \"; }\n.srte-editor [contenteditable] ul[data-srte-list-preset=\"bullet-arrow\"][data-srte-list-depth=\"2\"] > li::marker { content: \"\u25CF \"; }\n.srte-editor [contenteditable] ul[data-srte-list-preset=\"bullet-star\"][data-srte-list-depth=\"0\"] > li::marker { content: \"\u2605 \"; }\n.srte-editor [contenteditable] ul[data-srte-list-preset=\"bullet-star\"][data-srte-list-depth=\"1\"] > li::marker { content: \"\u25CB \"; }\n.srte-editor [contenteditable] ul[data-srte-list-preset=\"bullet-star\"][data-srte-list-depth=\"2\"] > li::marker { content: \"\u25A0 \"; }\n.srte-editor [contenteditable] ul[data-srte-list-preset=\"bullet-arrow-circle\"][data-srte-list-depth=\"0\"] > li::marker { content: \"\u27A2 \"; }\n.srte-editor [contenteditable] ul[data-srte-list-preset=\"bullet-arrow-circle\"][data-srte-list-depth=\"1\"] > li::marker { content: \"\u25CB \"; }\n.srte-editor [contenteditable] ul[data-srte-list-preset=\"bullet-arrow-circle\"][data-srte-list-depth=\"2\"] > li::marker { content: \"\u25A0 \"; }\n.srte-editor [contenteditable] ul[data-srte-checklist=\"true\"] > li > [data-srte-check] {\n display: inline-flex;\n grid-column: 1;\n grid-row: 1;\n align-items: center;\n justify-content: center;\n width: 1.1em;\n height: 1.6em;\n min-width: 0;\n box-sizing: border-box;\n padding: 0;\n border: 0;\n border-radius: 0;\n background: transparent;\n color: inherit;\n font: inherit;\n cursor: pointer;\n appearance: none;\n outline: none;\n margin: 0;\n line-height: 1.6;\n vertical-align: top;\n position: relative;\n}\n.srte-editor [contenteditable] ul[data-srte-checklist=\"true\"] > li > [data-srte-check]:focus-visible {\n outline: 2px solid var(--srte-ring);\n outline-offset: 2px;\n}\n.srte-editor [contenteditable] ul[data-srte-checklist=\"true\"] > li > [data-srte-check]::before {\n content: \"\";\n width: .9em;\n height: .9em;\n box-sizing: border-box;\n border: 1.5px solid var(--srte-muted-foreground);\n border-radius: 3px;\n background: var(--srte-canvas);\n}\n.srte-editor [contenteditable] ul[data-srte-checklist=\"true\"] > li > [data-srte-check][data-checked=\"true\"]::before {\n border-color: var(--srte-primary);\n background: var(--srte-primary);\n box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--srte-on-primary) 25%, transparent);\n}\n.srte-editor [contenteditable] ul[data-srte-checklist=\"true\"] > li > [data-srte-check][data-checked=\"true\"]::after {\n content: \"\";\n position: absolute;\n width: .42em;\n height: .22em;\n border-left: 1.5px solid var(--srte-on-primary);\n border-bottom: 1.5px solid var(--srte-on-primary);\n transform: translateY(-.08em) rotate(-45deg);\n}\n.srte-editor [contenteditable] ul[data-srte-checklist=\"true\"] > li > :is(p,h1,h2,h3,h4,h5,h6,blockquote,pre) {\n grid-column: 2;\n min-width: 0;\n margin-top: 0;\n}\n.srte-editor [contenteditable] ul[data-srte-checklist=\"true\"] > li > :is(ul,ol) {\n grid-column: 2;\n}\n.srte-editor:focus-within {\n border-color: var(--srte-ring) !important;\n box-shadow: 0 0 0 2px color-mix(in srgb, var(--srte-ring) 18%, transparent);\n}\n/*\n * @container (element width), not @media (viewport width): a host can embed\n * the editor at any fraction of the browser window - a split pane next to a\n * preview panel is the case that surfaced this (docs/bugs/\n * toolbar-wide-promotion-fires-on-viewport-width-not-container-width.md). A\n * plain @media breakpoint here would fire based on the *window*, wrongly\n * switching tiers for an editor instance that's actually much narrower (or\n * wider) than the window - .srte-editor already establishes its own\n * container: srte-editor / inline-size (see the .srte-editor rule above),\n * so @container always reflects this specific instance's real rendered width regardless of host\n * layout, with no @media fallback needed (container queries have full\n * support across this project's three target engines).\n */\n@container srte-editor (max-width: 639px) {\n .srte-toolbar { gap: 3px; padding: 6px; }\n .srte-tool-button, .srte-toolbar select { height: 40px; min-width: 40px; }\n .srte-toolbar-group[data-srte-priority=\"3\"],\n .srte-toolbar-menu[data-srte-priority=\"2\"] { display: none; }\n .srte-mobile-more { display: block; }\n /* left/top are no longer set here - ToolbarPrimitives.tsx now computes\n them via inline style (JS-measured, position:fixed, viewport-clamped;\n see MobileMoreMenu's own doc comment), which always wins over any\n value set here regardless of specificity or source order. Sizing/\n scroll behavior stays defined here since inline positioning doesn't\n touch those properties. */\n .srte-mobile-more .srte-menu {\n width: min(280px, calc(100vw - 16px));\n min-width: 0;\n max-height: min(70dvh, 480px);\n overflow-x: hidden;\n overflow-y: auto;\n overscroll-behavior: contain;\n -webkit-overflow-scrolling: touch;\n }\n .srte-menu-item { height: 40px; }\n}\n/*\n * The Bulleted/Numbered list buttons' split-control style picker (a native\n * <select> chevron next to the button - see .srte-split-control) needs\n * ~16px more room than the absolute minimum toolbar width supports.\n * Empirically bisected (not guessed): the toolbar's own \"no button should\n * render narrower than its content\" test only starts failing (Checklist\n * squeezed) at exactly 340-349px container width, and is clean at 350px and\n * every width above it. An earlier version of this fix reused the much\n * wider 639px mobile-tier breakpoint above to hide the select, which \"fixed\"\n * the failing test but also hid the feature across the entire ~340-639px\n * range - it was never actually needed past ~349px, and hiding it that\n * broadly is what caused a live report (\"below 680px it's not showing\n * dropdown icons at list tools\"). Scoped tightly here instead (359px, a\n * small safety margin past the measured 349px floor) so the picker is only\n * absent in the sliver of widths that actually can't fit it - the presets\n * remain reachable via the mobile \"List preset\" control regardless, once\n * .srte-mobile-more takes over at 639px anyway.\n */\n@container srte-editor (max-width: 359px) {\n .srte-split-control > select { display: none; }\n .srte-split-control > .srte-tool-button:first-child { border-radius: 8px; }\n}\n/*\n * Below 480px, the block-structure controls (Paragraph/Heading style,\n * Bulleted list, Numbered list, Checklist) drop their text label and show\n * icon-only - \"visible enough to understand\" plus the existing title/\n * aria-label as a real tooltip, not a silent accessibility loss. Scoped to\n * just these four (via data-srte-narrow-icon-only on the buttons, and a\n * dedicated class on the block-type select) rather than every always-\n * visible button, since only these were reported as needing it - Bold/\n * Italic/etc. keep their labels at every width, unchanged.\n *\n * The block-type control is a native <select> (its shown text is the\n * selected option's value, not a separate label node that can be hidden on\n * its own), so it gets the same treatment .srte-split-control's own select\n * already uses: transparent text plus a background-image icon standing in\n * for it - here, the Pilcrow glyph (already in toolbarIcons as\n * \"paragraphStyle\" but otherwise unused). The native option list itself is\n * unaffected and still shows full text when opened.\n */\n@container srte-editor (max-width: 479px) {\n .srte-tool-button[data-srte-narrow-icon-only=\"true\"] span { display: none; }\n .srte-toolbar select.srte-block-type-select {\n appearance: none;\n -webkit-appearance: none;\n color: transparent;\n width: 40px;\n min-width: 40px;\n max-width: 40px;\n padding: 0;\n background-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='18' height='18' viewBox='0 0 24 24' fill='none' stroke='%2364748b' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M13 4v16'/%3E%3Cpath d='M17 4v16'/%3E%3Cpath d='M19 4H9.5a4.5 4.5 0 0 0 0 9H13'/%3E%3C/svg%3E\");\n background-repeat: no-repeat;\n background-position: center;\n }\n}\n/*\n * Wide-viewport promotion (docs/bugs/toolbar-priority-collapse-fixed-threshold-no-wide-promotion.md):\n * below this, the single 639px breakpoint above was this system's ONLY\n * threshold - every dropdown-grouped tool stayed hidden inside its dropdown\n * at every width from 640px up to and past a 2200px+ desktop, since nothing\n * ever measured or scaled with the extra room. A handful of tools frequent\n * enough to matter once there's genuinely spare room (Superscript,\n * Subscript, Text colour, Background colour, Font size, Font family, Remove\n * link, Insert formula, Special characters) get a standalone always-visible\n * ToolbarButton copy (data-srte-wide-promote, rendered directly in the\n * toolbar row) that appears past this breakpoint, while their existing\n * dropdown/mobile-menu ToolbarMenuItem copy (the exact same\n * data-srte-wide-promote attribute, on the .srte-menu-item element instead)\n * hides so the tool isn't offered twice. Below this breakpoint - including\n * all of mobile AND the 640-1439px tablet/typical-laptop band, which keeps\n * today's grouped-dropdown layout unchanged - the standalone copy stays\n * hidden and the dropdown copy is what's reachable.\n *\n * 1440px, not 1280px: Playwright's own default test viewport is exactly\n * 1280x720, and the vast majority of this suite's toolbar interactions run\n * at that default without ever calling page.setViewportSize - a 1280px\n * threshold would have flipped nearly every existing toolbar test's\n * dropdown-item locators (role=menuitem) over to CSS display:none out from\n * under them. 1440px clears that default with room to spare while still\n * comfortably covering \"wide desktop\" (the ~2264px width the original\n * report was filed against).\n *\n * @container only, no @media fallback - same reasoning as the mobile tier\n * above (docs/bugs/toolbar-wide-promotion-fires-on-viewport-width-not-container-width.md):\n * a plain @media(min-width:1440px) fired whenever the *browser window* was\n * wide, even when this editor instance was embedded in a much narrower\n * split pane - promoting 9 extra buttons into a group with no room for\n * them, which squeezed/overlapped the group's existing buttons instead of\n * wrapping cleanly.\n */\n.srte-tool-button[data-srte-wide-promote=\"true\"] { display: none; }\n@container srte-editor (min-width: 1440px) {\n .srte-tool-button[data-srte-wide-promote=\"true\"] { display: inline-flex; }\n .srte-menu-item[data-srte-wide-promote=\"true\"] { display: none; }\n}\n.srte-editor [contenteditable] blockquote {\n border-left: 4px solid var(--srte-accent);\n margin: 0.75em 0;\n padding: 0.5em 1em;\n /* --srte-muted, not the retired --srte-surface-subtle (docs/bugs/\n srte-surface-subtle-not-host-theme-aware.md): every other neutral\n surface/text token here (--srte-background, --srte-foreground,\n --srte-muted, --srte-border) is defined as var(--host-token, fallback)\n so it correctly follows a host's OWN light/dark state even if\n .srte-dark never gets applied for some host-side wiring reason -\n --srte-surface-subtle was the one exception, a bare hardcoded hex with\n no host integration, which is exactly what let a real host (dark mode,\n .srte-dark not reaching this editor instance) render this blockquote\n with a light-mode background and dark-mode text simultaneously. */\n background: var(--srte-muted);\n color: var(--srte-text);\n}\n.srte-editor [contenteditable] p,\n.srte-editor [contenteditable] h1,\n.srte-editor [contenteditable] h2,\n.srte-editor [contenteditable] h3 {\n color: inherit;\n}\n.srte-editor [contenteditable] p {\n display: block;\n margin: 0 0 0.75em;\n font-size: 1em;\n font-weight: 400;\n line-height: 1.6;\n}\n.srte-editor [contenteditable] p[data-srte-caret-boundary=\"true\"] {\n min-height: 1.6em;\n}\n.srte-editor [contenteditable] h1,\n.srte-editor [contenteditable] h2,\n.srte-editor [contenteditable] h3 {\n display: block;\n margin: 0.75em 0 0.4em;\n font-weight: 700;\n line-height: 1.25;\n}\n.srte-editor [contenteditable] h1 {\n font-size: 2em;\n}\n.srte-editor [contenteditable] h2 {\n font-size: 1.5em;\n}\n.srte-editor [contenteditable] h3 {\n font-size: 1.25em;\n}\n.srte-editor [contenteditable] > :first-child {\n margin-top: 0;\n}\n.srte-editor [contenteditable] ul {\n list-style-type: disc;\n list-style-position: outside;\n margin: 0.75em 0;\n padding-left: 1.75em;\n}\n.srte-editor [contenteditable] ol {\n list-style-type: decimal;\n list-style-position: outside;\n margin: 0.75em 0;\n padding-left: 1.75em;\n}\n.srte-editor [contenteditable] li {\n display: list-item;\n margin: 0.25em 0;\n padding-left: 0.25em;\n}\n.srte-editor [contenteditable] li::marker {\n color: currentColor;\n}\n.srte-editor [contenteditable] table {\n width: 100%;\n margin: 0.75em 0;\n border-collapse: collapse;\n}\n.srte-editor [contenteditable] th,\n.srte-editor [contenteditable] td {\n padding: 8px;\n border: 1px solid var(--srte-border);\n vertical-align: top;\n}\n.srte-editor [contenteditable] [data-smart-cell-selected=\"true\"] {\n background: color-mix(in srgb, var(--srte-primary) 12%, var(--srte-canvas));\n box-shadow: inset 0 0 0 2px color-mix(in srgb, var(--srte-primary) 65%, transparent);\n}\n.srte-editor [contenteditable][data-smart-cell-selection-active] ::selection {\n background: transparent;\n color: inherit;\n}\n.srte-editor [contenteditable] th {\n background: var(--srte-muted);\n font-weight: 600;\n text-align: left;\n}\n.srte-editor [contenteditable] td > h1,\n.srte-editor [contenteditable] td > h2,\n.srte-editor [contenteditable] td > h3,\n.srte-editor [contenteditable] th > h1,\n.srte-editor [contenteditable] th > h2,\n.srte-editor [contenteditable] th > h3 {\n margin: 0 0 0.4em;\n overflow-wrap: anywhere;\n}\n.srte-editor [contenteditable] pre {\n display: block;\n margin: 0.75em 0;\n padding: 12px 14px;\n overflow-x: auto;\n border: 1px solid var(--srte-border);\n border-radius: 6px;\n background: var(--srte-code-bg);\n color: var(--srte-code-text);\n font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, \"Liberation Mono\", monospace;\n font-size: 0.9em;\n line-height: 1.55;\n white-space: pre-wrap;\n}\n.srte-editor [contenteditable] pre code {\n padding: 0;\n border: 0;\n background: transparent;\n color: inherit;\n font: inherit;\n}\n.srte-editor [contenteditable] code:not(pre code) {\n padding: 0.1em 0.35em;\n border-radius: 3px;\n background: var(--srte-code-bg);\n color: var(--srte-code-text);\n font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, \"Liberation Mono\", monospace;\n font-size: 0.9em;\n}\n.srte-editor [contenteditable] a,\n.srte-editor [contenteditable] a:visited {\n color: var(--srte-primary) !important;\n text-decoration: underline !important;\n text-decoration-thickness: 1px !important;\n text-underline-offset: 2px !important;\n cursor: pointer;\n}\n.srte-editor [contenteditable] a:hover {\n color: var(--srte-accent) !important;\n}\n.srte-editor [contenteditable] a:focus-visible {\n outline: 2px solid var(--srte-accent);\n outline-offset: 2px;\n}\n.srte-editor [contenteditable] sub,\n.srte-editor [contenteditable] sup {\n line-height: 0;\n}\n/*\n * A page break has no native visual meaning in a continuously-scrolling\n * web editor - its entire purpose is what happens at export/print time\n * (see atom/formats.ts's atomToHtml doc comment). This gives it a clearly\n * distinct live-editor appearance from the plain solid <hr> a horizontal\n * line renders as by default, so the two are never confused: a dashed\n * line with an explicit \"Page break\" label, per the feature's own design\n * decision.\n */\n.srte-editor [contenteditable] [data-smart-type=\"page_break\"] {\n display: block;\n position: relative;\n height: 0;\n margin: 1.5em 0;\n border: none;\n border-top: 2px dashed var(--srte-border);\n cursor: default;\n}\n.srte-editor [contenteditable] [data-smart-type=\"page_break\"]::after {\n content: \"Page break\";\n position: absolute;\n top: -0.7em;\n left: 50%;\n transform: translateX(-50%);\n background: var(--srte-canvas);\n padding: 0 0.6em;\n font-size: 11px;\n font-weight: 600;\n text-transform: uppercase;\n letter-spacing: 0.05em;\n color: var(--srte-text-muted);\n white-space: nowrap;\n}\n";
3
3
  export declare function ensureStyleSheet(): void;
package/dist/theme.js CHANGED
@@ -443,6 +443,61 @@ export const SRTE_DEFAULT_CSS = `
443
443
  }
444
444
  .srte-menu-item { height: 40px; }
445
445
  }
446
+ /*
447
+ * The Bulleted/Numbered list buttons' split-control style picker (a native
448
+ * <select> chevron next to the button - see .srte-split-control) needs
449
+ * ~16px more room than the absolute minimum toolbar width supports.
450
+ * Empirically bisected (not guessed): the toolbar's own "no button should
451
+ * render narrower than its content" test only starts failing (Checklist
452
+ * squeezed) at exactly 340-349px container width, and is clean at 350px and
453
+ * every width above it. An earlier version of this fix reused the much
454
+ * wider 639px mobile-tier breakpoint above to hide the select, which "fixed"
455
+ * the failing test but also hid the feature across the entire ~340-639px
456
+ * range - it was never actually needed past ~349px, and hiding it that
457
+ * broadly is what caused a live report ("below 680px it's not showing
458
+ * dropdown icons at list tools"). Scoped tightly here instead (359px, a
459
+ * small safety margin past the measured 349px floor) so the picker is only
460
+ * absent in the sliver of widths that actually can't fit it - the presets
461
+ * remain reachable via the mobile "List preset" control regardless, once
462
+ * .srte-mobile-more takes over at 639px anyway.
463
+ */
464
+ @container srte-editor (max-width: 359px) {
465
+ .srte-split-control > select { display: none; }
466
+ .srte-split-control > .srte-tool-button:first-child { border-radius: 8px; }
467
+ }
468
+ /*
469
+ * Below 480px, the block-structure controls (Paragraph/Heading style,
470
+ * Bulleted list, Numbered list, Checklist) drop their text label and show
471
+ * icon-only - "visible enough to understand" plus the existing title/
472
+ * aria-label as a real tooltip, not a silent accessibility loss. Scoped to
473
+ * just these four (via data-srte-narrow-icon-only on the buttons, and a
474
+ * dedicated class on the block-type select) rather than every always-
475
+ * visible button, since only these were reported as needing it - Bold/
476
+ * Italic/etc. keep their labels at every width, unchanged.
477
+ *
478
+ * The block-type control is a native <select> (its shown text is the
479
+ * selected option's value, not a separate label node that can be hidden on
480
+ * its own), so it gets the same treatment .srte-split-control's own select
481
+ * already uses: transparent text plus a background-image icon standing in
482
+ * for it - here, the Pilcrow glyph (already in toolbarIcons as
483
+ * "paragraphStyle" but otherwise unused). The native option list itself is
484
+ * unaffected and still shows full text when opened.
485
+ */
486
+ @container srte-editor (max-width: 479px) {
487
+ .srte-tool-button[data-srte-narrow-icon-only="true"] span { display: none; }
488
+ .srte-toolbar select.srte-block-type-select {
489
+ appearance: none;
490
+ -webkit-appearance: none;
491
+ color: transparent;
492
+ width: 40px;
493
+ min-width: 40px;
494
+ max-width: 40px;
495
+ padding: 0;
496
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='18' height='18' viewBox='0 0 24 24' fill='none' stroke='%2364748b' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M13 4v16'/%3E%3Cpath d='M17 4v16'/%3E%3Cpath d='M19 4H9.5a4.5 4.5 0 0 0 0 9H13'/%3E%3C/svg%3E");
497
+ background-repeat: no-repeat;
498
+ background-position: center;
499
+ }
500
+ }
446
501
  /*
447
502
  * Wide-viewport promotion (docs/bugs/toolbar-priority-collapse-fixed-threshold-no-wide-promotion.md):
448
503
  * below this, the single 639px breakpoint above was this system's ONLY
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "smartrte-react",
3
- "version": "1.0.0-beta.11",
3
+ "version": "1.0.0-beta.12",
4
4
  "description": "A powerful, feature-rich Rich Text Editor for React with support for tables, mathematical formulas (LaTeX/KaTeX), and media management",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -84,7 +84,7 @@
84
84
  "lucide-react": "^1.35.0",
85
85
  "mammoth": "^1.11.0",
86
86
  "pdfjs-dist": "^5.4.530",
87
- "smartrte-core": "^1.0.0-beta.6"
87
+ "smartrte-core": "^1.0.0-beta.7"
88
88
  },
89
89
  "scripts": {
90
90
  "build": "rm -rf dist && tsc -p tsconfig.json",