@inshapardaz/likhari-react 0.1.48-dev.356 → 0.1.48-dev.371

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"file":"Toolbar.d.ts","sourceRoot":"","sources":["../../src/components/Toolbar.tsx"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"Toolbar.d.ts","sourceRoot":"","sources":["../../src/components/Toolbar.tsx"],"names":[],"mappings":"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"}
@@ -14,7 +14,7 @@ import { $replaceMatch } from '../find/findReplaceActions.js';
14
14
  import { getSpeller } from '../spellcheck/spellDictionaries.js';
15
15
  import { lookupSynonyms } from '../thesaurus/thesaurusStores.js';
16
16
  import { addUserWord, ignoreWord } from '../spellcheck/userWords.js';
17
- import { $wordAtDomPoint, textPointAt } from '../spellcheck/spellingMenu.js';
17
+ import { $selectionIsWithinWord, $wordAtDomPoint, $wordAtSelection, textPointAt } from '../spellcheck/spellingMenu.js';
18
18
  import { INSERT_HORIZONTAL_RULE_COMMAND } from '@lexical/react/LexicalHorizontalRuleNode';
19
19
  import { ImageDialog } from '../image/ImageDialog.js';
20
20
  import { $createImageNode } from '../image/ImageNode.js';
@@ -36,7 +36,8 @@ import { CANVAS_FONT_DEFAULTS, DEFAULT_FONT_OPTIONS, FONT_SIZES_PX, ZOOM_LEVELS_
36
36
  import { setStyleProperty } from '../utils/style.js';
37
37
  import { useStrings } from '../i18n/useStrings.js';
38
38
  import { usePortalTarget } from '../PortalTargetContext.js';
39
- import { IconAbc, IconAlignCenter, IconAlignJustified, IconAlignLeft, IconAlignRight, IconArrowBackUp, IconArrowForwardUp, IconArrowsJoin, IconBold, IconClearFormatting, IconDeviceFloppy, IconBook, IconChevronDown, IconChevronUp, IconDots, IconHelpCircle, IconHistory, IconExternalLink, IconFeather, IconH1, IconH2, IconH3, IconH4, IconH5, IconH6, IconIndentDecrease, IconIndentIncrease, IconItalic, IconLetterCase, IconLetterCaseLower, IconLetterCaseUpper, IconLink, IconLinkOff, IconList, IconListCheck, IconListNumbers, IconPageBreak, IconPencil, IconPhoto, IconPilcrow, IconQuote, IconSeparatorHorizontal, IconSparkles, IconStrikethrough, IconSubscript, IconSuperscript, IconArrowMergeBoth, IconArrowsSplit2, IconColumnInsertLeft, IconColumnInsertRight, IconColumnRemove, IconColumns, IconClipboard, IconCopy, IconCut, IconSearch, IconSelectAll, IconColumns1, IconColumns2, IconNumber1Small, IconRowInsertBottom, IconRowInsertTop, IconRowRemove, IconArrowsMaximize, IconArrowsMinimize, IconArrowsShuffle, IconTable, IconTableMinus, IconTrash, IconTableOptions, IconMinus, IconPlus, IconTextSize, IconTypography, IconUnderline, IconWand, } from '@tabler/icons-react';
39
+ import { IconAbc, IconAlignCenter, IconAlignJustified, IconAlignLeft, IconAlignRight, IconArrowBackUp, IconArrowForwardUp, IconArrowsJoin, IconBold, IconClearFormatting, IconDeviceFloppy, IconBook, IconChevronDown, IconChevronUp, IconDots, IconHelpCircle, IconHistory, IconExternalLink, IconFeather, IconH1, IconH2, IconH3, IconH4, IconH5, IconH6, IconIndentDecrease, IconIndentIncrease, IconItalic, IconLetterCase, IconLetterCaseLower, IconLetterCaseUpper, IconLink, IconLinkOff, IconList, IconListCheck, IconListNumbers, IconPageBreak, IconPencil, IconPhoto, IconPilcrow, IconQuote, IconSeparatorHorizontal, IconSparkles, IconStrikethrough, IconSubscript, IconSuperscript, IconArrowMergeBoth, IconArrowsSplit2, IconColumnInsertLeft, IconColumnInsertRight, IconColumnRemove, IconColumns, IconClipboard, IconCopy, IconCut, IconSearch, IconSelectAll, IconColumns1, IconColumns2, IconNumber1Small, IconRowInsertBottom, IconRowInsertTop, IconRowRemove, IconArrowsShuffle, IconTable, IconTableMinus, IconTableOptions, IconMinus, IconPlus, IconTextSize, IconTypography, IconUnderline, IconWand, } from '@tabler/icons-react';
40
+ import { IconCoupletCenterNarrow, IconCoupletCenterWide, IconCoupletDelete, IconCoupletGutterNarrow, IconCoupletGutterWide, IconCoupletSpacingLoose, IconCoupletSpacingTight, IconCoupletStaggerNarrow, IconCoupletStaggerWide, IconPoetryDelete, } from '../icons/poetryIcons.js';
40
41
  /** Sentinel for "no inline style": Lexical returns the default it is given when
41
42
  * the selection has none, so this tells "unstyled" (show the canvas default)
42
43
  * apart from "styled with a value the dropdown doesn't list" (show nothing). */
@@ -189,16 +190,25 @@ function ToolbarSelect({ icon: Icon, label, value, data, width, disabled, placeh
189
190
  function MoreMenuItem({ icon: Icon, label, active, onClick, }) {
190
191
  return (_jsx(Menu.Item, { leftSection: _jsx(Icon, { size: ICON_SIZE, stroke: ICON_STROKE }), "data-active": active ? 'true' : 'false', onMouseDown: (e) => e.preventDefault(), onClick: onClick, children: label }));
191
192
  }
192
- /** One command, as a menu item or as a ribbon button (the label becomes its tooltip and name). */
193
+ /** A menu item's keyboard shortcut, shown dimmed at its trailing edge. Hidden from
194
+ * assistive tech (the item's `aria-keyshortcuts` carries that instead), so it never
195
+ * changes the item's accessible name. */
196
+ function ShortcutHint({ keys }) {
197
+ return (_jsx("span", { "aria-hidden": "true", className: "likhari-context-shortcut", children: keys }));
198
+ }
199
+ /** One command, as a menu item, a ribbon button, or an icon-only menu item (`icons`) — in every case the label becomes its tooltip and accessible name except in a labelled menu. */
193
200
  function CommandItem({ mode, icon: Icon, label, onClick, disabled, color, }) {
194
201
  if (mode === 'ribbon') {
195
202
  return _jsx(ToolbarButton, { icon: Icon, title: label, disabled: disabled, "data-danger": color === 'red' ? 'true' : undefined, onClick: onClick });
196
203
  }
204
+ if (mode === 'icons') {
205
+ return (_jsx(Menu.Item, { "aria-label": label, title: label, color: color, disabled: disabled, onMouseDown: (e) => e.preventDefault(), onClick: onClick, children: _jsx(Icon, { size: ICON_SIZE, stroke: ICON_STROKE }) }));
206
+ }
197
207
  return (_jsx(Menu.Item, { color: color, disabled: disabled, leftSection: _jsx(Icon, { size: ICON_SIZE, stroke: ICON_STROKE }), onMouseDown: (e) => e.preventDefault(), onClick: onClick, children: label }));
198
208
  }
199
- /** A group heading in a menu; the ribbon's tab and a submenu's parent item already name the group, so they show nothing. */
209
+ /** A group heading in a menu; the ribbon's tab, a submenu's parent item, and an icon row's own aria-label already name the group, so they show nothing. */
200
210
  const ItemLabel = ({ mode, children }) => (mode === 'menu' ? _jsx(Menu.Label, { children: children }) : null);
201
- const ItemDivider = ({ mode }) => (mode === 'ribbon' ? _jsx(MantineToolbar.Divider, {}) : _jsx(Menu.Divider, {}));
211
+ const ItemDivider = ({ mode }) => mode === 'ribbon' ? _jsx(MantineToolbar.Divider, {}) : mode === 'icons' ? _jsx("span", { className: "likhari-context-icons-divider" }) : _jsx(Menu.Divider, {});
202
212
  function LinkMenuItems({ url, onEdit, onRemove, strings, mode = 'menu' }) {
203
213
  // Only offer the link as clickable if it's a URL the editor would have accepted.
204
214
  const safeUrl = normalizeLinkUrl(url);
@@ -216,7 +226,14 @@ function TableMenuItems({ strings, direction, rows, columns, canMergeCells, canU
216
226
  const manyRows = rows > 1;
217
227
  const manyColumns = columns > 1;
218
228
  const item = (icon, label, action, color) => (_jsx(CommandItem, { mode: mode, icon: icon, label: label, onClick: onAction(action), color: color }));
219
- return (_jsxs(_Fragment, { children: [_jsx(ItemLabel, { mode: mode, children: t.menuLabel }), item(IconRowInsertTop, manyRows ? t.insertRowsBefore : t.insertRowBefore, () => $insertTableRows(false)), item(IconRowInsertBottom, manyRows ? t.insertRowsAfter : t.insertRowAfter, () => $insertTableRows(true)), item(rtl ? IconColumnInsertRight : IconColumnInsertLeft, manyColumns ? t.insertColumnsBefore : t.insertColumnBefore, () => $insertTableColumns(false)), item(rtl ? IconColumnInsertLeft : IconColumnInsertRight, manyColumns ? t.insertColumnsAfter : t.insertColumnAfter, () => $insertTableColumns(true)), (canMergeCells || canUnmergeCell) && _jsx(ItemDivider, { mode: mode }), canMergeCells && item(IconArrowMergeBoth, t.mergeCells, $mergeTableCells), canUnmergeCell && item(IconArrowsSplit2, t.unmergeCell, $unmergeTableCell), _jsx(ItemDivider, { mode: mode }), item(IconRowRemove, manyRows ? t.deleteRows : t.deleteRow, $deleteTableRows), item(IconColumnRemove, manyColumns ? t.deleteColumns : t.deleteColumn, $deleteTableColumns), item(IconTableMinus, t.deleteTable, $deleteTable, 'red')] }));
229
+ // `icons` mode leaves "Delete table" out of the icon row — it comes back as a full
230
+ // labelled item below, since a single trash icon among several would be too easy to
231
+ // hit by mistake (same treatment as "Delete poetry" in PoetryMenuItems).
232
+ const rowItems = (_jsxs(_Fragment, { children: [_jsx(ItemLabel, { mode: mode, children: t.menuLabel }), item(IconRowInsertTop, manyRows ? t.insertRowsBefore : t.insertRowBefore, () => $insertTableRows(false)), item(IconRowInsertBottom, manyRows ? t.insertRowsAfter : t.insertRowAfter, () => $insertTableRows(true)), item(rtl ? IconColumnInsertRight : IconColumnInsertLeft, manyColumns ? t.insertColumnsBefore : t.insertColumnBefore, () => $insertTableColumns(false)), item(rtl ? IconColumnInsertLeft : IconColumnInsertRight, manyColumns ? t.insertColumnsAfter : t.insertColumnAfter, () => $insertTableColumns(true)), (canMergeCells || canUnmergeCell) && _jsx(ItemDivider, { mode: mode }), canMergeCells && item(IconArrowMergeBoth, t.mergeCells, $mergeTableCells), canUnmergeCell && item(IconArrowsSplit2, t.unmergeCell, $unmergeTableCell), _jsx(ItemDivider, { mode: mode }), item(IconRowRemove, manyRows ? t.deleteRows : t.deleteRow, $deleteTableRows), item(IconColumnRemove, manyColumns ? t.deleteColumns : t.deleteColumn, $deleteTableColumns), mode !== 'icons' && item(IconTableMinus, t.deleteTable, $deleteTable, 'red')] }));
233
+ if (mode === 'icons') {
234
+ return (_jsxs(_Fragment, { children: [_jsx("div", { className: "likhari-context-icons", role: "group", "aria-label": t.menuLabel, children: rowItems }), _jsx(Menu.Item, { color: "red", leftSection: _jsx(IconTableMinus, { size: ICON_SIZE, stroke: ICON_STROKE }), onMouseDown: (e) => e.preventDefault(), onClick: onAction($deleteTable), children: t.deleteTable })] }));
235
+ }
236
+ return rowItems;
220
237
  }
221
238
  /** Whether a one-notch step from `value` stays on the scale — a step that
222
239
  * would fall off either end is disabled rather than silently clamped. */
@@ -232,7 +249,20 @@ function PoetryMenuItems({ strings, layout, scale, centered, onAction, mode = 'm
232
249
  if (node)
233
250
  $setPoetryLayout(node, next);
234
251
  };
235
- return (_jsxs(_Fragment, { children: [_jsx(ItemLabel, { mode: mode, children: t.menuLabel }), item(IconColumns1, t.singleColumn, setLayout('single'), layout === 'single'), item(IconColumns2, t.twoColumn, setLayout('two-column'), layout === 'two-column'), item(IconArrowsShuffle, t.staggered, setLayout('staggered'), layout === 'staggered'), _jsx(ItemDivider, { mode: mode }), _jsx(ItemDivider, { mode: mode }), item(IconRowInsertTop, t.insertCoupletBefore, () => $insertCoupletRelativeToSelection('before')), item(IconRowInsertBottom, t.insertCoupletAfter, () => $insertCoupletRelativeToSelection('after')), item(IconArrowsMinimize, t.tighterSpacing, () => $adjustPoetrySpacing(-1), !canStep(POETRY_SPACINGS, scale.spacing, -1)), item(IconArrowsMaximize, t.looserSpacing, () => $adjustPoetrySpacing(1), !canStep(POETRY_SPACINGS, scale.spacing, 1)), layout === 'staggered' && (_jsxs(_Fragment, { children: [item(IconArrowsMinimize, t.narrowerCouplets, () => $adjustPoetryStagger(-1), !canStep(POETRY_STAGGERS, scale.stagger, -1)), item(IconArrowsMaximize, t.widerCouplets, () => $adjustPoetryStagger(1), !canStep(POETRY_STAGGERS, scale.stagger, 1))] })), layout === 'two-column' && (_jsxs(_Fragment, { children: [item(IconAlignCenter, centered ? t.uncenterCouplet : t.centerCouplet, () => $setCoupletCentered(!centered)), centered && (_jsxs(_Fragment, { children: [item(IconArrowsMinimize, t.narrowerCenter, () => $adjustPoetryCenterWidth(-1), !canStep(POETRY_CENTER_WIDTHS, scale.centerWidth, -1)), item(IconArrowsMaximize, t.widerCenter, () => $adjustPoetryCenterWidth(1), !canStep(POETRY_CENTER_WIDTHS, scale.centerWidth, 1))] })), item(IconColumns2, t.narrowerGutter, () => $adjustPoetryGutter(-1), !canStep(POETRY_GUTTERS, scale.gutter, -1)), item(IconColumns2, t.widerGutter, () => $adjustPoetryGutter(1), !canStep(POETRY_GUTTERS, scale.gutter, 1))] })), item(IconTrash, t.deleteCouplet, $deletePoetryCouplet, false, 'red'), item(IconTrash, t.deletePoetry, $deletePoetryBlock, false, 'red')] }));
252
+ // Which layout applies (single/two-column/staggered), kept apart from what that layout's
253
+ // selection then allows (insert, spacing, centering, delete) — two rows of icon buttons
254
+ // in `icons` mode, one heading-less list everywhere else.
255
+ const layoutItems = (_jsxs(_Fragment, { children: [item(IconColumns1, t.singleColumn, setLayout('single'), layout === 'single'), item(IconColumns2, t.twoColumn, setLayout('two-column'), layout === 'two-column'), item(IconArrowsShuffle, t.staggered, setLayout('staggered'), layout === 'staggered')] }));
256
+ // `icons` mode puts a divider between each group of similar actions (insert /
257
+ // spacing / this layout's own adjustments), and leaves "Delete poetry" out —
258
+ // it comes back as a full labelled item below, since a single trash icon in a
259
+ // row of several would be too easy to hit by mistake.
260
+ const groupDivider = mode === 'icons' ? _jsx(ItemDivider, { mode: mode }) : null;
261
+ const actionItems = (_jsxs(_Fragment, { children: [item(IconRowInsertTop, t.insertCoupletBefore, () => $insertCoupletRelativeToSelection('before')), item(IconRowInsertBottom, t.insertCoupletAfter, () => $insertCoupletRelativeToSelection('after')), groupDivider, item(IconCoupletSpacingTight, t.tighterSpacing, () => $adjustPoetrySpacing(-1), !canStep(POETRY_SPACINGS, scale.spacing, -1)), item(IconCoupletSpacingLoose, t.looserSpacing, () => $adjustPoetrySpacing(1), !canStep(POETRY_SPACINGS, scale.spacing, 1)), layout === 'staggered' && (_jsxs(_Fragment, { children: [groupDivider, item(IconCoupletStaggerNarrow, t.narrowerCouplets, () => $adjustPoetryStagger(-1), !canStep(POETRY_STAGGERS, scale.stagger, -1)), item(IconCoupletStaggerWide, t.widerCouplets, () => $adjustPoetryStagger(1), !canStep(POETRY_STAGGERS, scale.stagger, 1))] })), layout === 'two-column' && (_jsxs(_Fragment, { children: [groupDivider, item(IconAlignCenter, centered ? t.uncenterCouplet : t.centerCouplet, () => $setCoupletCentered(!centered)), centered && (_jsxs(_Fragment, { children: [item(IconCoupletCenterNarrow, t.narrowerCenter, () => $adjustPoetryCenterWidth(-1), !canStep(POETRY_CENTER_WIDTHS, scale.centerWidth, -1)), item(IconCoupletCenterWide, t.widerCenter, () => $adjustPoetryCenterWidth(1), !canStep(POETRY_CENTER_WIDTHS, scale.centerWidth, 1))] })), item(IconCoupletGutterNarrow, t.narrowerGutter, () => $adjustPoetryGutter(-1), !canStep(POETRY_GUTTERS, scale.gutter, -1)), item(IconCoupletGutterWide, t.widerGutter, () => $adjustPoetryGutter(1), !canStep(POETRY_GUTTERS, scale.gutter, 1))] })), groupDivider, item(IconCoupletDelete, t.deleteCouplet, $deletePoetryCouplet, false, 'red'), mode !== 'icons' && item(IconPoetryDelete, t.deletePoetry, $deletePoetryBlock, false, 'red')] }));
262
+ if (mode === 'icons') {
263
+ return (_jsxs(_Fragment, { children: [_jsxs("div", { className: "likhari-context-icon-rows", role: "group", "aria-label": t.menuLabel, children: [_jsx("div", { className: "likhari-context-icons", children: layoutItems }), _jsx("div", { className: "likhari-context-icon-rows-divider" }), _jsx("div", { className: "likhari-context-icons", children: actionItems })] }), _jsx(Menu.Item, { color: "red", leftSection: _jsx(IconPoetryDelete, { size: ICON_SIZE, stroke: ICON_STROKE }), onMouseDown: (e) => e.preventDefault(), onClick: onAction($deletePoetryBlock), children: t.deletePoetry })] }));
264
+ }
265
+ return (_jsxs(_Fragment, { children: [_jsx(ItemLabel, { mode: mode, children: t.menuLabel }), layoutItems, _jsx(ItemDivider, { mode: mode }), _jsx(ItemDivider, { mode: mode }), actionItems] }));
236
266
  }
237
267
  /** `docs/markdown-dialect.md` at the repository's default branch — the Markdown
238
268
  * format this editor reads and writes, including the poetry container syntax. */
@@ -259,6 +289,7 @@ export function Toolbar({ config, toolbarStyle, dictionaryStores = [], thesaurus
259
289
  // join lines) is lower priority still, so it is first to move.
260
290
  const toolbarContainerRef = useRef(null);
261
291
  const toolbarMeasureRef = useRef(null);
292
+ const toolbarOverflowMeasureRef = useRef(null);
262
293
  const [overflowGroupKeys, setOverflowGroupKeys] = useState(EMPTY_OVERFLOW);
263
294
  const updateToolbar = useCallback(() => {
264
295
  editor.getEditorState().read(() => {
@@ -652,8 +683,14 @@ export function Toolbar({ config, toolbarStyle, dictionaryStores = [], thesaurus
652
683
  block.selectStart();
653
684
  }
654
685
  }
655
- if (point)
656
- word = $wordAtDomPoint(point.node, point.offset);
686
+ if (point) {
687
+ const atPoint = $wordAtDomPoint(point.node, point.offset);
688
+ // A selection that reaches beyond this one word (several words, a whole line, several
689
+ // lines) means the user is after something else entirely: no spelling fixes or
690
+ // synonyms for a word that's merely under the pointer, not what's actually selected.
691
+ if (atPoint && $selectionIsWithinWord(atPoint))
692
+ word = atPoint;
693
+ }
657
694
  if (!cell && !couplet && config.joinLines) {
658
695
  menu.text = true;
659
696
  menu.canJoin = $canJoinLines();
@@ -733,12 +770,14 @@ export function Toolbar({ config, toolbarStyle, dictionaryStores = [], thesaurus
733
770
  }, { discrete: true });
734
771
  editor.focus();
735
772
  };
736
- // Ctrl/Cmd+K opens the link dialog.
773
+ // Ctrl/Cmd+K opens the link dialog. Matched on event.code (the physical key),
774
+ // not event.key, so it still fires while typing in Urdu or another non-Latin
775
+ // layout, where event.key would be whatever character that layout produces.
737
776
  useEffect(() => {
738
777
  if (!config.links)
739
778
  return;
740
779
  return editor.registerCommand(KEY_MODIFIER_COMMAND, (event) => {
741
- if (event.key.toLowerCase() !== 'k' || !(event.ctrlKey || event.metaKey))
780
+ if (event.code !== 'KeyK' || !(event.ctrlKey || event.metaKey))
742
781
  return false;
743
782
  event.preventDefault();
744
783
  openLinkDialog();
@@ -919,6 +958,80 @@ export function Toolbar({ config, toolbarStyle, dictionaryStores = [], thesaurus
919
958
  }, { discrete: true });
920
959
  editor.focus();
921
960
  };
961
+ /** Runs `action` on the misspelled word at the caret — the same check the right-click
962
+ * menu makes before offering Ignore / Add to dictionary / Add to auto-correct, so the
963
+ * matching keyboard shortcuts only fire where those menu items would actually appear. */
964
+ const wordActionAtCaret = async (action) => {
965
+ if (!config.language.spellCheck)
966
+ return;
967
+ const word = editor.getEditorState().read(() => $wordAtSelection());
968
+ if (!word)
969
+ return;
970
+ const language = spellCheckLanguage !== 'auto' ? spellCheckLanguage : word.direction === 'rtl' ? 'ur' : 'en';
971
+ const speller = getSpeller(language);
972
+ if (!speller)
973
+ return;
974
+ const checked = await speller;
975
+ if (checked.correct(word.word))
976
+ return;
977
+ await action(language, word.word);
978
+ };
979
+ const ignoreWordAtCaret = () => {
980
+ void wordActionAtCaret((language, word) => ignoreWord(language, word));
981
+ };
982
+ const addWordAtCaretToDictionary = () => {
983
+ void wordActionAtCaret(async (language, word) => {
984
+ const added = await addUserWord(dictionaryStores, language, word);
985
+ if (!added)
986
+ ignoreWord(language, word);
987
+ });
988
+ };
989
+ const addWordAtCaretToAutoCorrect = () => {
990
+ if (!onAddAutoCorrect || !config.language.autocorrect)
991
+ return;
992
+ void wordActionAtCaret((language, word) => onAddAutoCorrect(word, language));
993
+ };
994
+ // Keyboard shortcuts mirroring the right-click menu: Ctrl/Cmd+Alt+J joins the
995
+ // selected lines (plain Ctrl+J is already the browser's bookmarks-manager
996
+ // shortcut), and Ctrl/Cmd+Alt+I/D/A act on the misspelled word at the caret the
997
+ // way the menu's Ignore / Add to dictionary / Add to auto-correct items do.
998
+ // Matched on event.code (the physical key), not event.key, so they still fire
999
+ // while typing in Urdu or another non-Latin layout, where event.key would be
1000
+ // whatever character that layout produces rather than "j"/"i"/"d"/"a".
1001
+ useEffect(() => {
1002
+ return editor.registerCommand(KEY_MODIFIER_COMMAND, (event) => {
1003
+ if (!(event.ctrlKey || event.metaKey) || !event.altKey)
1004
+ return false;
1005
+ switch (event.code) {
1006
+ case 'KeyJ':
1007
+ if (!config.joinLines)
1008
+ return false;
1009
+ event.preventDefault();
1010
+ joinLines();
1011
+ return true;
1012
+ case 'KeyI':
1013
+ if (!config.language.spellCheck)
1014
+ return false;
1015
+ event.preventDefault();
1016
+ ignoreWordAtCaret();
1017
+ return true;
1018
+ case 'KeyD':
1019
+ if (!config.language.spellCheck)
1020
+ return false;
1021
+ event.preventDefault();
1022
+ addWordAtCaretToDictionary();
1023
+ return true;
1024
+ case 'KeyA':
1025
+ if (!config.language.spellCheck || !onAddAutoCorrect || !config.language.autocorrect)
1026
+ return false;
1027
+ event.preventDefault();
1028
+ addWordAtCaretToAutoCorrect();
1029
+ return true;
1030
+ default:
1031
+ return false;
1032
+ }
1033
+ }, COMMAND_PRIORITY_NORMAL);
1034
+ });
922
1035
  // The tools group is always built, even when every command in it is off, so the ribbon asks first.
923
1036
  const hasToolsGroup = config.language.autocorrect || config.language.textCleanup || config.language.spellCheck || config.joinLines;
924
1037
  const toolsSection = (_jsxs(MantineToolbar.Group, { className: "likhari-toolbar-cluster", children: [config.language.autocorrect && (_jsx(ToolbarButton, { icon: IconWand, title: strings.toolbar.autocorrect, active: autoCorrectOpen, onClick: onToggleAutoCorrect })), config.language.textCleanup && (_jsx(ToolbarButton, { icon: IconSparkles, title: strings.autoCorrect.correctDocument, onClick: () => editor.dispatchCommand(CORRECT_DOCUMENT_COMMAND, undefined) })), config.language.spellCheck && (_jsx(ToolbarButton, { icon: IconAbc, title: strings.toolbar.spellChecker, active: spellOpen, onClick: onToggleSpell })), config.joinLines && (_jsx(ToolbarButton, { icon: IconArrowsJoin, title: strings.toolbar.joinLines, disabled: !state.canJoinLines, onClick: joinLines }))] }, "tools"));
@@ -969,10 +1082,9 @@ export function Toolbar({ config, toolbarStyle, dictionaryStores = [], thesaurus
969
1082
  const measure = toolbarMeasureRef.current;
970
1083
  if (!container || !measure)
971
1084
  return;
972
- // Rough width of the "..." button itself (icon button + its gap) —
973
- // only charged against the budget once something has actually moved
974
- // into it, since it isn't rendered at all otherwise.
975
- const OVERFLOW_BUTTON_WIDTH = 38;
1085
+ // Fallback only, in case the overflow button's own hidden measurement
1086
+ // element (below) isn't mounted yet on the very first pass.
1087
+ const OVERFLOW_BUTTON_FALLBACK_WIDTH = 38;
976
1088
  const recompute = () => {
977
1089
  const available = container.clientWidth;
978
1090
  const total = measure.scrollWidth;
@@ -984,6 +1096,10 @@ export function Toolbar({ config, toolbarStyle, dictionaryStores = [], thesaurus
984
1096
  measure.querySelectorAll('[data-group-key]').forEach((el) => {
985
1097
  widths.set(el.dataset.groupKey, el.getBoundingClientRect().width);
986
1098
  });
1099
+ // Measured from a real (hidden) copy of the "..." button, in the toolbar's own
1100
+ // CSS, rather than guessed — only charged against the budget once something
1101
+ // has actually moved into it, since it isn't rendered at all otherwise.
1102
+ const overflowButtonWidth = toolbarOverflowMeasureRef.current?.scrollWidth ?? OVERFLOW_BUTTON_FALLBACK_WIDTH;
987
1103
  let remaining = total;
988
1104
  const next = new Set();
989
1105
  for (const key of movableGroupKeys.split(',').filter(Boolean)) {
@@ -996,13 +1112,15 @@ export function Toolbar({ config, toolbarStyle, dictionaryStores = [], thesaurus
996
1112
  next.add(key);
997
1113
  }
998
1114
  if (next.size > 0)
999
- remaining += OVERFLOW_BUTTON_WIDTH;
1115
+ remaining += overflowButtonWidth;
1000
1116
  setOverflowGroupKeys((prev) => (setsEqual(prev, next) ? prev : next));
1001
1117
  };
1002
1118
  recompute();
1003
1119
  const ro = new ResizeObserver(recompute);
1004
1120
  ro.observe(container);
1005
1121
  ro.observe(measure);
1122
+ if (toolbarOverflowMeasureRef.current)
1123
+ ro.observe(toolbarOverflowMeasureRef.current);
1006
1124
  return () => ro.disconnect();
1007
1125
  });
1008
1126
  const isVisible = (section) => !overflowGroupKeys.has(section.key);
@@ -1027,10 +1145,10 @@ export function Toolbar({ config, toolbarStyle, dictionaryStores = [], thesaurus
1027
1145
  const match = contextMenu.spelling.match;
1028
1146
  editor.update(() => $replaceMatch(match, suggestion), { discrete: true });
1029
1147
  editor.focus();
1030
- }, children: suggestion }, suggestion))), _jsx(Menu.Item, { onClick: () => {
1148
+ }, children: suggestion }, suggestion))), _jsx(Menu.Item, { rightSection: _jsx(ShortcutHint, { keys: "Ctrl+Alt+I" }), "aria-keyshortcuts": "Control+Alt+I", onClick: () => {
1031
1149
  ignoreWord(contextMenu.spelling.language, contextMenu.spelling.word);
1032
1150
  setContextMenu(null);
1033
- }, children: strings.contextMenu.ignore }), _jsx(Menu.Item, { onClick: () => {
1151
+ }, children: strings.contextMenu.ignore }), _jsx(Menu.Item, { rightSection: _jsx(ShortcutHint, { keys: "Ctrl+Alt+D" }), "aria-keyshortcuts": "Control+Alt+D", onClick: () => {
1034
1152
  const { language, word } = contextMenu.spelling;
1035
1153
  // Without a writable store, the word is ignored for this session instead.
1036
1154
  void addUserWord(dictionaryStores, language, word).then((added) => {
@@ -1038,21 +1156,25 @@ export function Toolbar({ config, toolbarStyle, dictionaryStores = [], thesaurus
1038
1156
  ignoreWord(language, word);
1039
1157
  });
1040
1158
  setContextMenu(null);
1041
- }, children: strings.contextMenu.addToDictionary }), onAddAutoCorrect && config.language.autocorrect && (_jsx(Menu.Item, { onClick: () => {
1159
+ }, children: strings.contextMenu.addToDictionary }), onAddAutoCorrect && config.language.autocorrect && (_jsx(Menu.Item, { rightSection: _jsx(ShortcutHint, { keys: "Ctrl+Alt+A" }), "aria-keyshortcuts": "Control+Alt+A", onClick: () => {
1042
1160
  onAddAutoCorrect(contextMenu.spelling.word, contextMenu.spelling.language);
1043
1161
  setContextMenu(null);
1044
1162
  }, children: strings.contextMenu.addToAutoCorrect })), _jsx(Menu.Divider, {})] })) : (
1045
1163
  // Everything that applies to what is under the pointer, each kind of content behind its own submenu.
1046
- _jsxs(_Fragment, { children: [contextMenu?.thesaurus && (_jsxs(Menu.Sub, { ...CONTEXT_SUBMENU_PROPS, children: [_jsx(Menu.Sub.Target, { children: _jsx(Menu.Sub.Item, { leftSection: _jsx(IconBook, { size: ICON_SIZE, stroke: ICON_STROKE }), children: strings.thesaurus.synonyms }) }), _jsxs(Menu.Sub.Dropdown, { children: [contextMenu.thesaurus.items === undefined && _jsx(Menu.Item, { disabled: true, children: strings.thesaurus.loading }), contextMenu.thesaurus.items?.length === 0 && _jsx(Menu.Item, { disabled: true, children: strings.thesaurus.none }), contextMenu.thesaurus.items?.map((synonym) => (_jsx(Menu.Item, { onClick: () => {
1164
+ _jsxs(_Fragment, { children: [contextMenu?.thesaurus && contextMenu.thesaurus.items?.length !== 0 && (_jsxs(Menu.Sub, { ...CONTEXT_SUBMENU_PROPS, children: [_jsx(Menu.Sub.Target, { children: _jsx(Menu.Sub.Item, { leftSection: _jsx(IconBook, { size: ICON_SIZE, stroke: ICON_STROKE }), children: strings.thesaurus.synonyms }) }), _jsxs(Menu.Sub.Dropdown, { children: [contextMenu.thesaurus.items === undefined && _jsx(Menu.Item, { disabled: true, children: strings.thesaurus.loading }), contextMenu.thesaurus.items?.map((synonym) => (_jsx(Menu.Item, { onClick: () => {
1047
1165
  const match = contextMenu.thesaurus.match;
1048
1166
  editor.update(() => $replaceMatch(match, synonym), { discrete: true });
1049
1167
  editor.focus();
1050
1168
  }, children: synonym }, synonym)))] })] })), contextMenu?.link !== undefined && (_jsxs(Menu.Sub, { ...CONTEXT_SUBMENU_PROPS, children: [_jsx(Menu.Sub.Target, { children: _jsx(Menu.Sub.Item, { leftSection: _jsx(IconLink, { size: ICON_SIZE, stroke: ICON_STROKE }), children: strings.link.menuLabel }) }), _jsx(Menu.Sub.Dropdown, { children: _jsx(LinkMenuItems, { mode: "submenu", url: contextMenu.link, onEdit: () => {
1051
1169
  restoreSelection();
1052
1170
  openLinkDialog();
1053
- }, onRemove: removeLink, strings: strings }) })] })), contextMenu?.table && (_jsxs(Menu.Sub, { ...CONTEXT_SUBMENU_PROPS, children: [_jsx(Menu.Sub.Target, { children: _jsx(Menu.Sub.Item, { leftSection: _jsx(IconTable, { size: ICON_SIZE, stroke: ICON_STROKE }), children: strings.tableMenu.menuLabel }) }), _jsx(Menu.Sub.Dropdown, { children: _jsx(TableMenuItems, { mode: "submenu", strings: strings, direction: direction, rows: contextMenu.table.rows, columns: contextMenu.table.columns, canMergeCells: state.canMergeCells, canUnmergeCell: state.canUnmergeCell, onAction: runMenuAction }) })] })), contextMenu?.poetry && (_jsxs(Menu.Sub, { ...CONTEXT_SUBMENU_PROPS, children: [_jsx(Menu.Sub.Target, { children: _jsx(Menu.Sub.Item, { leftSection: _jsx(IconFeather, { size: ICON_SIZE, stroke: ICON_STROKE }), children: strings.poetryMenu.menuLabel }) }), _jsx(Menu.Sub.Dropdown, { children: _jsx(PoetryMenuItems, { mode: "submenu", strings: strings, layout: state.poetryLayout, scale: state.poetryScale, centered: state.poetryCentered, onAction: runMenuAction }) })] })), contextMenu?.text && (_jsx(Menu.Item, { leftSection: _jsx(IconArrowsJoin, { size: ICON_SIZE, stroke: ICON_STROKE }), disabled: !contextMenu.canJoin, onClick: runMenuAction(() => {
1171
+ }, onRemove: removeLink, strings: strings }) })] })), contextMenu?.table && (_jsx(TableMenuItems, { mode: "icons", strings: strings, direction: direction, rows: contextMenu.table.rows, columns: contextMenu.table.columns, canMergeCells: state.canMergeCells, canUnmergeCell: state.canUnmergeCell, onAction: runMenuAction })), contextMenu?.poetry && (_jsx(PoetryMenuItems, { mode: "icons", strings: strings, layout: state.poetryLayout, scale: state.poetryScale, centered: state.poetryCentered, onAction: runMenuAction })), contextMenu?.text && (_jsx(Menu.Item, { leftSection: _jsx(IconArrowsJoin, { size: ICON_SIZE, stroke: ICON_STROKE }), rightSection: _jsx(ShortcutHint, { keys: "Ctrl+Alt+J" }), "aria-keyshortcuts": "Control+Alt+J", disabled: !contextMenu.canJoin, onClick: runMenuAction(() => {
1054
1172
  $joinSelectedLines();
1055
- }), children: strings.contextMenu.joinLines })), (contextMenu?.thesaurus || contextMenu?.link !== undefined || contextMenu?.table || contextMenu?.poetry || contextMenu?.text) && (_jsx(Menu.Divider, {}))] })), _jsxs("div", { className: "likhari-context-row", role: "group", children: [_jsx(Menu.Item, { "aria-label": strings.contextMenu.cut, title: strings.contextMenu.cut, onClick: () => void copyOrCut(true)(), children: _jsx(IconCut, { size: ICON_SIZE, stroke: ICON_STROKE }) }), _jsx(Menu.Item, { "aria-label": strings.contextMenu.copy, title: strings.contextMenu.copy, onClick: () => void copyOrCut(false)(), children: _jsx(IconCopy, { size: ICON_SIZE, stroke: ICON_STROKE }) }), _jsx(Menu.Item, { "aria-label": strings.contextMenu.paste, title: strings.contextMenu.paste, onClick: () => void paste(), children: _jsx(IconClipboard, { size: ICON_SIZE, stroke: ICON_STROKE }) })] }), !contextMenu?.spelling && (_jsxs(_Fragment, { children: [_jsx(Menu.Divider, {}), _jsx(Menu.Item, { leftSection: _jsx(IconSelectAll, { size: ICON_SIZE, stroke: ICON_STROKE }), onClick: runMenuAction(() => $selectAll()), children: strings.contextMenu.selectAll })] }))] })] }));
1173
+ }), children: strings.contextMenu.joinLines })), ((contextMenu?.thesaurus && contextMenu.thesaurus.items?.length !== 0) ||
1174
+ contextMenu?.link !== undefined ||
1175
+ contextMenu?.table ||
1176
+ contextMenu?.poetry ||
1177
+ contextMenu?.text) && _jsx(Menu.Divider, {})] })), _jsxs("div", { className: "likhari-context-row", role: "group", children: [_jsx(Menu.Item, { "aria-label": strings.contextMenu.cut, title: strings.contextMenu.cut, onClick: () => void copyOrCut(true)(), children: _jsx(IconCut, { size: ICON_SIZE, stroke: ICON_STROKE }) }), _jsx(Menu.Item, { "aria-label": strings.contextMenu.copy, title: strings.contextMenu.copy, onClick: () => void copyOrCut(false)(), children: _jsx(IconCopy, { size: ICON_SIZE, stroke: ICON_STROKE }) }), _jsx(Menu.Item, { "aria-label": strings.contextMenu.paste, title: strings.contextMenu.paste, onClick: () => void paste(), children: _jsx(IconClipboard, { size: ICON_SIZE, stroke: ICON_STROKE }) })] }), !contextMenu?.spelling && (_jsxs(_Fragment, { children: [_jsx(Menu.Divider, {}), _jsx(Menu.Item, { leftSection: _jsx(IconSelectAll, { size: ICON_SIZE, stroke: ICON_STROKE }), onClick: runMenuAction(() => $selectAll()), children: strings.contextMenu.selectAll })] }))] })] }));
1056
1178
  const dialogsEl = (_jsxs(_Fragment, { children: [(config.images.linked || config.images.embedded) && (_jsx(ImageDialog, { mode: "insert", opened: imageDialogOpen, onSubmit: insertImage, onClose: closeImageDialog })), config.tables && _jsx(TableDialog, { opened: tableDialogOpen, onSubmit: insertTable, onClose: closeTableDialog }), config.columns && _jsx(LayoutDialog, { opened: layoutDialogOpen, onSubmit: insertLayout, onClose: closeLayoutDialog }), config.links && (_jsx(LinkDialog, { opened: linkDialogOpen, initialUrl: state.linkUrl, showTextField: linkNeedsText, onSubmit: applyLink, onRemove: state.isLink ? removeLink : undefined, onClose: closeLinkDialog })), drafts && _jsx(DraftsDialog, { opened: draftsOpen, locale: locale, onClose: () => setDraftsOpen(false), ...drafts })] }));
1057
1179
  // The ribbon layout (UI spec §3.4): the same command groups as above, on tabs.
1058
1180
  if (ribbon) {
@@ -1141,5 +1263,5 @@ export function Toolbar({ config, toolbarStyle, dictionaryStores = [], thesaurus
1141
1263
  setRibbonPeek(false);
1142
1264
  }, children: _jsx(MantineToolbar, { "aria-label": strings.toolbar.ariaLabel, className: bordered ? 'likhari-toolbar likhari-ribbon-panel' : 'likhari-toolbar likhari-toolbar--plain likhari-ribbon-panel', withBorder: bordered, variant: variant, color: "var(--editor-accent)", children: withDividers(activeTab.groups) }) })), contextMenuEl, dialogsEl] }));
1143
1265
  }
1144
- return (_jsxs(MantineToolbar, { ref: toolbarContainerRef, "aria-label": strings.toolbar.ariaLabel, className: bordered ? 'likhari-toolbar' : 'likhari-toolbar likhari-toolbar--plain', withBorder: bordered, variant: variant, color: "var(--editor-accent)", children: [withDividers(startSections), visibleMovable.map((section) => (_jsxs(Fragment, { children: [_jsx(MantineToolbar.Divider, {}), section.node] }, section.key))), hiddenMovable.length > 0 && (_jsxs(_Fragment, { children: [_jsx(MantineToolbar.Divider, {}), _jsxs(Menu, { opened: overflowMenuOpen, onChange: handleOverflowMenuChange, position: "bottom-end", withinPortal: true, portalProps: { target: portalTarget }, shadow: "sm", width: "max-content", closeOnItemClick: false, children: [_jsx(Menu.Target, { children: _jsx(ToolbarButton, { icon: IconDots, title: strings.toolbar.moreFormatting }) }), _jsx(Menu.Dropdown, { p: 6, children: _jsx(MantineToolbar, { withBorder: false, variant: variant, color: "var(--editor-accent)", style: { maxWidth: 320 }, children: withDividers(hiddenMovable.map((section) => section.node)) }) })] })] })), _jsx("span", { "aria-hidden": "true", style: { flex: '1 1 auto' } }), visibleEnd.map((section) => section.node), contextMenuEl, overflowEnabled && (_jsxs(MantineToolbar, { "aria-hidden": "true", ref: toolbarMeasureRef, withBorder: bordered, variant: variant, color: "var(--editor-accent)", style: { position: 'absolute', visibility: 'hidden', pointerEvents: 'none', top: 0, left: 0, flexWrap: 'nowrap', width: 'max-content' }, children: [withDividers(startSections), movableSections.map((section) => (_jsxs(Fragment, { children: [_jsx(MantineToolbar.Divider, {}), _jsx("span", { "data-group-key": section.key, style: { display: 'inline-flex' }, children: section.node })] }, section.key)))] })), dialogsEl] }));
1266
+ return (_jsxs(MantineToolbar, { ref: toolbarContainerRef, "aria-label": strings.toolbar.ariaLabel, className: bordered ? 'likhari-toolbar' : 'likhari-toolbar likhari-toolbar--plain', withBorder: bordered, variant: variant, color: "var(--editor-accent)", children: [withDividers(startSections), visibleMovable.map((section) => (_jsxs(Fragment, { children: [_jsx(MantineToolbar.Divider, {}), section.node] }, section.key))), hiddenMovable.length > 0 && (_jsxs(_Fragment, { children: [_jsx(MantineToolbar.Divider, {}), _jsxs(Menu, { opened: overflowMenuOpen, onChange: handleOverflowMenuChange, position: "bottom-end", withinPortal: true, portalProps: { target: portalTarget }, shadow: "sm", width: "max-content", closeOnItemClick: false, children: [_jsx(Menu.Target, { children: _jsx(ToolbarButton, { icon: IconDots, title: strings.toolbar.moreFormatting }) }), _jsx(Menu.Dropdown, { p: 6, children: _jsx(MantineToolbar, { withBorder: false, variant: variant, color: "var(--editor-accent)", style: { maxWidth: 320 }, children: withDividers(hiddenMovable.map((section) => section.node)) }) })] })] })), _jsx("span", { "aria-hidden": "true", style: { flex: '1 1 auto' } }), visibleEnd.map((section) => section.node), contextMenuEl, overflowEnabled && (_jsxs(MantineToolbar, { "aria-hidden": "true", ref: toolbarMeasureRef, className: bordered ? 'likhari-toolbar' : 'likhari-toolbar likhari-toolbar--plain', withBorder: bordered, variant: variant, color: "var(--editor-accent)", style: { position: 'absolute', visibility: 'hidden', pointerEvents: 'none', top: 0, left: 0, flexWrap: 'nowrap', width: 'max-content' }, children: [withDividers(startSections), mainMovable.map((section) => (_jsxs(Fragment, { children: [_jsx(MantineToolbar.Divider, {}), _jsx("span", { "data-group-key": section.key, style: { display: 'inline-flex' }, children: section.node })] }, section.key))), endMovable.map((section) => (_jsx("span", { "data-group-key": section.key, style: { display: 'inline-flex' }, children: section.node }, section.key)))] })), overflowEnabled && (_jsxs(MantineToolbar, { "aria-hidden": "true", ref: toolbarOverflowMeasureRef, className: bordered ? 'likhari-toolbar' : 'likhari-toolbar likhari-toolbar--plain', withBorder: bordered, variant: variant, color: "var(--editor-accent)", style: { position: 'absolute', visibility: 'hidden', pointerEvents: 'none', top: 0, left: 0, width: 'max-content' }, children: [_jsx(MantineToolbar.Divider, {}), _jsx(ToolbarButton, { icon: IconDots, title: "Toolbar overflow measurement" })] })), dialogsEl] }));
1145
1267
  }
@@ -51,7 +51,7 @@ const en = {
51
51
  autocorrect: 'Auto-correct',
52
52
  textCleanup: 'Text cleanup',
53
53
  spellChecker: 'Spell-checker',
54
- joinLines: 'Join lines',
54
+ joinLines: 'Join lines (Ctrl+Alt+J)',
55
55
  moreFormatting: 'More formatting',
56
56
  strikethrough: 'Strikethrough',
57
57
  superscript: 'Superscript',
@@ -349,7 +349,7 @@ const ur = {
349
349
  autocorrect: 'خودکار تصحیح',
350
350
  textCleanup: 'متن کی صفائی',
351
351
  spellChecker: 'ہجے چیک کرنے والا',
352
- joinLines: 'سطریں ملائیں',
352
+ joinLines: 'سطریں ملائیں (Ctrl+Alt+J)',
353
353
  moreFormatting: 'مزید فارمیٹنگ',
354
354
  strikethrough: 'خط زدہ',
355
355
  superscript: 'بالا نویس',
@@ -647,7 +647,7 @@ const paShahmukhi = {
647
647
  autocorrect: 'خودکار درستی',
648
648
  textCleanup: 'متن دی صفائی',
649
649
  spellChecker: 'ہجے چیکر',
650
- joinLines: 'سطراں ملاؤ',
650
+ joinLines: 'سطراں ملاؤ (Ctrl+Alt+J)',
651
651
  moreFormatting: 'ہور فارمیٹنگ',
652
652
  strikethrough: 'کٹی لکیر',
653
653
  superscript: 'اُتلا لکھت',
@@ -0,0 +1,22 @@
1
+ import type { IconProps } from '@tabler/icons-react';
2
+ /** A trash can with a single line inside it: delete this one couplet. */
3
+ export declare const IconCoupletDelete: import("react").ForwardRefExoticComponent<IconProps & import("react").RefAttributes<SVGSVGElement>>;
4
+ /** A trash can with three lines inside it: delete every couplet in the block. */
5
+ export declare const IconPoetryDelete: import("react").ForwardRefExoticComponent<IconProps & import("react").RefAttributes<SVGSVGElement>>;
6
+ /** Two full-width rows pinched together: tighten the vertical gap between couplets. */
7
+ export declare const IconCoupletSpacingTight: import("react").ForwardRefExoticComponent<IconProps & import("react").RefAttributes<SVGSVGElement>>;
8
+ /** Two full-width rows pulled apart: loosen the vertical gap between couplets. */
9
+ export declare const IconCoupletSpacingLoose: import("react").ForwardRefExoticComponent<IconProps & import("react").RefAttributes<SVGSVGElement>>;
10
+ /** Two staggered (alternating-side) rows pinched together: narrow the stagger offset. */
11
+ export declare const IconCoupletStaggerNarrow: import("react").ForwardRefExoticComponent<IconProps & import("react").RefAttributes<SVGSVGElement>>;
12
+ /** Two staggered (alternating-side) rows pulled apart: widen the stagger offset. */
13
+ export declare const IconCoupletStaggerWide: import("react").ForwardRefExoticComponent<IconProps & import("react").RefAttributes<SVGSVGElement>>;
14
+ /** Four column rails, the inner pair pinched together: narrow the center column. */
15
+ export declare const IconCoupletCenterNarrow: import("react").ForwardRefExoticComponent<IconProps & import("react").RefAttributes<SVGSVGElement>>;
16
+ /** Four column rails, the inner pair pulled apart: widen the center column. */
17
+ export declare const IconCoupletCenterWide: import("react").ForwardRefExoticComponent<IconProps & import("react").RefAttributes<SVGSVGElement>>;
18
+ /** Two column rails pinched together: narrow the gutter between the columns. */
19
+ export declare const IconCoupletGutterNarrow: import("react").ForwardRefExoticComponent<IconProps & import("react").RefAttributes<SVGSVGElement>>;
20
+ /** Two column rails pulled apart: widen the gutter between the columns. */
21
+ export declare const IconCoupletGutterWide: import("react").ForwardRefExoticComponent<IconProps & import("react").RefAttributes<SVGSVGElement>>;
22
+ //# sourceMappingURL=poetryIcons.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"poetryIcons.d.ts","sourceRoot":"","sources":["../../src/icons/poetryIcons.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AA2CrD,yEAAyE;AACzE,eAAO,MAAM,iBAAiB,qGAK5B,CAAC;AAEH,iFAAiF;AACjF,eAAO,MAAM,gBAAgB,qGAO3B,CAAC;AAEH,uFAAuF;AACvF,eAAO,MAAM,uBAAuB,qGAKlC,CAAC;AAEH,kFAAkF;AAClF,eAAO,MAAM,uBAAuB,qGAKlC,CAAC;AAEH,yFAAyF;AACzF,eAAO,MAAM,wBAAwB,qGAKnC,CAAC;AAEH,oFAAoF;AACpF,eAAO,MAAM,sBAAsB,qGAKjC,CAAC;AAEH,oFAAoF;AACpF,eAAO,MAAM,uBAAuB,qGAOlC,CAAC;AAEH,+EAA+E;AAC/E,eAAO,MAAM,qBAAqB,qGAOhC,CAAC;AAEH,gFAAgF;AAChF,eAAO,MAAM,uBAAuB,qGAKlC,CAAC;AAEH,2EAA2E;AAC3E,eAAO,MAAM,qBAAqB,qGAKhC,CAAC"}
@@ -0,0 +1,94 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { forwardRef } from 'react';
3
+ /**
4
+ * A handful of icons for poetry-couplet actions that @tabler/icons-react has no
5
+ * distinct enough pair for: deleting one couplet vs. the whole block (both would
6
+ * otherwise be a plain `IconTrash`), the couplet's row spacing vs. its staggered
7
+ * horizontal offset, and the two-column layout's center width vs. its gutter
8
+ * (all four would otherwise reuse the same arrows-in/arrows-out icon). Built in
9
+ * the same shape as a Tabler icon — 24x24, outline, round caps and joins — so
10
+ * they sit in the toolbar and context menu without looking out of place.
11
+ */
12
+ function createPoetryIcon(name, paths) {
13
+ const Component = forwardRef(function PoetryIcon({ color = 'currentColor', size = 24, stroke = 2, title, className, ...rest }, ref) {
14
+ return (_jsxs("svg", { ref: ref, xmlns: "http://www.w3.org/2000/svg", width: size, height: size, viewBox: "0 0 24 24", fill: "none", stroke: color, strokeWidth: stroke, strokeLinecap: "round", strokeLinejoin: "round", className: ['likhari-icon', `likhari-icon-${name}`, className].filter(Boolean).join(' '), ...rest, children: [title && _jsx("title", { children: title }), _jsx("path", { stroke: "none", d: "M0 0h24v24H0z", fill: "none" }), paths.map((d, index) => (_jsx("path", { d: d }, index)))] }));
15
+ });
16
+ Component.displayName = name;
17
+ return Component;
18
+ }
19
+ /** A trash can with a single line inside it: delete this one couplet. */
20
+ export const IconCoupletDelete = createPoetryIcon('couplet-delete', [
21
+ 'M4 7h16',
22
+ 'M9 7v-3a1 1 0 0 1 1 -1h4a1 1 0 0 1 1 1v3',
23
+ 'M5 7l1 12a2 2 0 0 0 2 2h8a2 2 0 0 0 2 -2l1 -12',
24
+ 'M12 11v6',
25
+ ]);
26
+ /** A trash can with three lines inside it: delete every couplet in the block. */
27
+ export const IconPoetryDelete = createPoetryIcon('poetry-delete', [
28
+ 'M4 7h16',
29
+ 'M9 7v-3a1 1 0 0 1 1 -1h4a1 1 0 0 1 1 1v3',
30
+ 'M5 7l1 12a2 2 0 0 0 2 2h8a2 2 0 0 0 2 -2l1 -12',
31
+ 'M9 11v6',
32
+ 'M12 11v6',
33
+ 'M15 11v6',
34
+ ]);
35
+ /** Two full-width rows pinched together: tighten the vertical gap between couplets. */
36
+ export const IconCoupletSpacingTight = createPoetryIcon('couplet-spacing-tight', [
37
+ 'M4 5h16',
38
+ 'M4 19h16',
39
+ 'M9 9l3 3l3 -3',
40
+ 'M9 15l3 -3l3 3',
41
+ ]);
42
+ /** Two full-width rows pulled apart: loosen the vertical gap between couplets. */
43
+ export const IconCoupletSpacingLoose = createPoetryIcon('couplet-spacing-loose', [
44
+ 'M4 5h16',
45
+ 'M4 19h16',
46
+ 'M9 10l3 -3l3 3',
47
+ 'M9 14l3 3l3 -3',
48
+ ]);
49
+ /** Two staggered (alternating-side) rows pinched together: narrow the stagger offset. */
50
+ export const IconCoupletStaggerNarrow = createPoetryIcon('couplet-stagger-narrow', [
51
+ 'M4 7h7',
52
+ 'M13 17h7',
53
+ 'M9 9l3 3l-3 3',
54
+ 'M15 9l-3 3l3 3',
55
+ ]);
56
+ /** Two staggered (alternating-side) rows pulled apart: widen the stagger offset. */
57
+ export const IconCoupletStaggerWide = createPoetryIcon('couplet-stagger-wide', [
58
+ 'M4 7h7',
59
+ 'M13 17h7',
60
+ 'M10 9l-3 3l3 3',
61
+ 'M14 9l3 3l-3 3',
62
+ ]);
63
+ /** Four column rails, the inner pair pinched together: narrow the center column. */
64
+ export const IconCoupletCenterNarrow = createPoetryIcon('couplet-center-narrow', [
65
+ 'M4 4v16',
66
+ 'M10 4v16',
67
+ 'M14 4v16',
68
+ 'M20 4v16',
69
+ 'M9 9l3 3l-3 3',
70
+ 'M15 9l-3 3l3 3',
71
+ ]);
72
+ /** Four column rails, the inner pair pulled apart: widen the center column. */
73
+ export const IconCoupletCenterWide = createPoetryIcon('couplet-center-wide', [
74
+ 'M4 4v16',
75
+ 'M10 4v16',
76
+ 'M14 4v16',
77
+ 'M20 4v16',
78
+ 'M10 9l-3 3l3 3',
79
+ 'M14 9l3 3l-3 3',
80
+ ]);
81
+ /** Two column rails pinched together: narrow the gutter between the columns. */
82
+ export const IconCoupletGutterNarrow = createPoetryIcon('couplet-gutter-narrow', [
83
+ 'M10 4v16',
84
+ 'M14 4v16',
85
+ 'M9 9l3 3l-3 3',
86
+ 'M15 9l-3 3l3 3',
87
+ ]);
88
+ /** Two column rails pulled apart: widen the gutter between the columns. */
89
+ export const IconCoupletGutterWide = createPoetryIcon('couplet-gutter-wide', [
90
+ 'M10 4v16',
91
+ 'M14 4v16',
92
+ 'M10 9l-3 3l3 3',
93
+ 'M14 9l3 3l-3 3',
94
+ ]);
@@ -2,8 +2,10 @@ export declare function $canJoinLines(): boolean;
2
2
  /**
3
3
  * Joins the lines in the selection into one paragraph. Soft breaks and the
4
4
  * paragraph breaks between selected paragraphs become a single space each,
5
- * unless there is already a space at that point. A soft break right after the
6
- * selection becomes a paragraph break, so the joined text stands on its own.
5
+ * unless there is already a space at that point. Anything after the
6
+ * selection's end point — the unselected rest of a partly-selected line, or
7
+ * further soft-broken lines — splits off into its own paragraph, so it stands
8
+ * apart from the text that was actually joined.
7
9
  */
8
10
  export declare function $joinSelectedLines(): boolean;
9
11
  //# sourceMappingURL=joinLinesActions.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"joinLinesActions.d.ts","sourceRoot":"","sources":["../../src/join/joinLinesActions.ts"],"names":[],"mappings":"AAoCA,wBAAgB,aAAa,IAAI,OAAO,CAKvC;AAiDD;;;;;GAKG;AACH,wBAAgB,kBAAkB,IAAI,OAAO,CA2B5C"}
1
+ {"version":3,"file":"joinLinesActions.d.ts","sourceRoot":"","sources":["../../src/join/joinLinesActions.ts"],"names":[],"mappings":"AAqCA,wBAAgB,aAAa,IAAI,OAAO,CAKvC;AAiFD;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,IAAI,OAAO,CA+B5C"}
@@ -59,23 +59,62 @@ function $appendParagraph(target, paragraph) {
59
59
  }
60
60
  paragraph.remove();
61
61
  }
62
- /** Turns a soft break into a paragraph break: the content after it moves to a new paragraph. */
63
- function $splitParagraphAt(lineBreak, paragraph) {
64
- const rest = $createParagraphNode();
65
- let sibling = lineBreak.getNextSibling();
66
- while (sibling) {
67
- const next = sibling.getNextSibling();
68
- rest.append(sibling);
69
- sibling = next;
62
+ /** Moves `tail` into a new paragraph right after `paragraph`. A leading soft break is the
63
+ * split point itself, so it is dropped rather than carried over as the new paragraph's first child. */
64
+ function $moveTailToNewParagraph(paragraph, tail) {
65
+ if ($isLineBreakNode(tail[0])) {
66
+ tail[0].remove();
67
+ tail.shift();
70
68
  }
71
- lineBreak.remove();
69
+ if (tail.length === 0)
70
+ return;
71
+ const rest = $createParagraphNode();
72
+ for (const node of tail)
73
+ rest.append(node);
72
74
  paragraph.insertAfter(rest);
73
75
  }
76
+ /**
77
+ * Splits off whatever in `paragraph` comes after the selection's end point into a
78
+ * paragraph of its own, so content the user didn't select isn't swept into the join
79
+ * along with it — whether that's the rest of a partly-selected line, or the
80
+ * paragraph's remaining soft-broken lines.
81
+ */
82
+ function $splitUnselectedTail(paragraph, end) {
83
+ if (end.type === 'text') {
84
+ const endNode = end.getNode();
85
+ if (endNode.getTopLevelElement()?.getKey() !== paragraph.getKey())
86
+ return;
87
+ const text = endNode.getTextContent();
88
+ const tail = [];
89
+ if (end.offset === 0) {
90
+ tail.push(endNode);
91
+ }
92
+ else if (end.offset < text.length) {
93
+ endNode.setTextContent(text.slice(0, end.offset));
94
+ tail.push($createTextNode(text.slice(end.offset)));
95
+ }
96
+ let sibling = endNode.getNextSibling();
97
+ while (sibling) {
98
+ const next = sibling.getNextSibling();
99
+ tail.push(sibling);
100
+ sibling = next;
101
+ }
102
+ $moveTailToNewParagraph(paragraph, tail);
103
+ }
104
+ else {
105
+ const endNode = end.getNode();
106
+ if (endNode.getKey() !== paragraph.getKey())
107
+ return;
108
+ $moveTailToNewParagraph(paragraph, endNode.getChildren().slice(end.offset));
109
+ }
110
+ }
74
111
  /**
75
112
  * Joins the lines in the selection into one paragraph. Soft breaks and the
76
113
  * paragraph breaks between selected paragraphs become a single space each,
77
- * unless there is already a space at that point. A soft break right after the
78
- * selection becomes a paragraph break, so the joined text stands on its own.
114
+ * unless there is already a space at that point. Anything after the
115
+ * selection's end point — the unselected rest of a partly-selected line, or
116
+ * further soft-broken lines — splits off into its own paragraph, so it stands
117
+ * apart from the text that was actually joined.
79
118
  */
80
119
  export function $joinSelectedLines() {
81
120
  const selection = $getSelection();
@@ -87,9 +126,7 @@ export function $joinSelectedLines() {
87
126
  const textsWithNewlines = $textNodesWithNewlines(selection);
88
127
  if (paragraphs.length < 2 && lineBreaks.length === 0 && textsWithNewlines.length === 0)
89
128
  return false;
90
- const last = nodes[nodes.length - 1];
91
- const breakAfter = last?.getNextSibling();
92
- const trailingBreak = $isLineBreakNode(breakAfter) ? breakAfter : null;
129
+ const [start, end] = selection.getStartEndPoints() ?? [];
93
130
  for (const lineBreak of lineBreaks)
94
131
  $replaceBreakWithSpace(lineBreak);
95
132
  for (const textNode of textsWithNewlines)
@@ -97,13 +134,19 @@ export function $joinSelectedLines() {
97
134
  const [target, ...rest] = paragraphs;
98
135
  if (!target)
99
136
  return false;
137
+ const lastParagraph = paragraphs[paragraphs.length - 1];
138
+ if (end)
139
+ $splitUnselectedTail(lastParagraph, end);
100
140
  for (const paragraph of rest)
101
141
  $appendParagraph(target, paragraph);
102
- if (trailingBreak?.isAttached()) {
103
- const paragraph = trailingBreak.getParent();
104
- if ($isParagraphNode(paragraph))
105
- $splitParagraphAt(trailingBreak, paragraph);
142
+ // Keep the caret where the selection began, rather than at the end of everything
143
+ // just joined — useful on its own, and essential when many lines were joined at
144
+ // once, where "the end" could otherwise land far past where the user was working.
145
+ if (start) {
146
+ start.getNode().select(start.offset, start.offset);
147
+ }
148
+ else {
149
+ target.selectEnd();
106
150
  }
107
- target.selectEnd();
108
151
  return true;
109
152
  }
@@ -12,4 +12,13 @@ export declare function textPointAt(x: number, y: number): {
12
12
  } | null;
13
13
  /** The whole word around an offset in a Lexical text node. Must run inside an editor read or update. */
14
14
  export declare function $wordAtDomPoint(domText: Node, offset: number): WordAtPoint | null;
15
+ /** The whole word at the caret, for keyboard shortcuts that act on "the word here"
16
+ * without a mouse position. Must run inside an editor read or update. */
17
+ export declare function $wordAtSelection(): WordAtPoint | null;
18
+ /** Whether the current selection is no wider than `word` — a caret, or a selection that
19
+ * sits entirely inside that one word. A selection that reaches beyond it (more of that
20
+ * word's line, another word, another line) means the user selected something other than
21
+ * just this word, even though it happens to be the word under the pointer. Must run
22
+ * inside an editor read or update. */
23
+ export declare function $selectionIsWithinWord(word: WordAtPoint): boolean;
15
24
  //# sourceMappingURL=spellingMenu.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"spellingMenu.d.ts","sourceRoot":"","sources":["../../src/spellcheck/spellingMenu.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,4BAA4B,CAAC;AAG5D,qFAAqF;AACrF,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,KAAK,GAAG,KAAK,CAAC;IACzB,KAAK,EAAE,SAAS,CAAC;CAClB;AAED,gGAAgG;AAChG,wBAAgB,WAAW,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG;IAAE,IAAI,EAAE,IAAI,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAUvF;AAED,wGAAwG;AACxG,wBAAgB,eAAe,CAAC,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,GAAG,WAAW,GAAG,IAAI,CAWjF"}
1
+ {"version":3,"file":"spellingMenu.d.ts","sourceRoot":"","sources":["../../src/spellcheck/spellingMenu.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,4BAA4B,CAAC;AAG5D,qFAAqF;AACrF,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,KAAK,GAAG,KAAK,CAAC;IACzB,KAAK,EAAE,SAAS,CAAC;CAClB;AAED,gGAAgG;AAChG,wBAAgB,WAAW,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG;IAAE,IAAI,EAAE,IAAI,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAUvF;AAcD,wGAAwG;AACxG,wBAAgB,eAAe,CAAC,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,GAAG,WAAW,GAAG,IAAI,CAIjF;AAED;yEACyE;AACzE,wBAAgB,gBAAgB,IAAI,WAAW,GAAG,IAAI,CAMrD;AAED;;;;sCAIsC;AACtC,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,WAAW,GAAG,OAAO,CAMjE"}
@@ -1,4 +1,4 @@
1
- import { $getNearestNodeFromDOMNode, $isTextNode } from 'lexical';
1
+ import { $getNearestNodeFromDOMNode, $getSelection, $isRangeSelection, $isTextNode } from 'lexical';
2
2
  import { wordsIn } from './spellDictionaries.js';
3
3
  /** The DOM text node and offset under a viewport point, or null if the point is not on text. */
4
4
  export function textPointAt(x, y) {
@@ -10,11 +10,8 @@ export function textPointAt(x, y) {
10
10
  const range = doc.caretRangeFromPoint?.(x, y);
11
11
  return range && range.startContainer.nodeType === Node.TEXT_NODE ? { node: range.startContainer, offset: range.startOffset } : null;
12
12
  }
13
- /** The whole word around an offset in a Lexical text node. Must run inside an editor read or update. */
14
- export function $wordAtDomPoint(domText, offset) {
15
- const node = $getNearestNodeFromDOMNode(domText);
16
- if (!$isTextNode(node))
17
- return null;
13
+ /** The whole word around an offset in a Lexical text node. */
14
+ function $wordAtTextOffset(node, offset) {
18
15
  const text = node.getTextContent();
19
16
  const span = wordsIn(text).find((s) => offset >= s.start && offset <= s.end);
20
17
  if (!span)
@@ -25,3 +22,34 @@ export function $wordAtDomPoint(domText, offset) {
25
22
  match: { anchorKey: node.getKey(), anchorOffset: span.start, focusKey: node.getKey(), focusOffset: span.end },
26
23
  };
27
24
  }
25
+ /** The whole word around an offset in a Lexical text node. Must run inside an editor read or update. */
26
+ export function $wordAtDomPoint(domText, offset) {
27
+ const node = $getNearestNodeFromDOMNode(domText);
28
+ if (!$isTextNode(node))
29
+ return null;
30
+ return $wordAtTextOffset(node, offset);
31
+ }
32
+ /** The whole word at the caret, for keyboard shortcuts that act on "the word here"
33
+ * without a mouse position. Must run inside an editor read or update. */
34
+ export function $wordAtSelection() {
35
+ const selection = $getSelection();
36
+ if (!$isRangeSelection(selection) || !selection.isCollapsed())
37
+ return null;
38
+ const node = selection.anchor.getNode();
39
+ if (!$isTextNode(node))
40
+ return null;
41
+ return $wordAtTextOffset(node, selection.anchor.offset);
42
+ }
43
+ /** Whether the current selection is no wider than `word` — a caret, or a selection that
44
+ * sits entirely inside that one word. A selection that reaches beyond it (more of that
45
+ * word's line, another word, another line) means the user selected something other than
46
+ * just this word, even though it happens to be the word under the pointer. Must run
47
+ * inside an editor read or update. */
48
+ export function $selectionIsWithinWord(word) {
49
+ const selection = $getSelection();
50
+ if (!$isRangeSelection(selection) || selection.isCollapsed())
51
+ return true;
52
+ const { anchorKey, anchorOffset, focusKey, focusOffset } = word.match;
53
+ const withinSpan = (key, offset) => key === anchorKey && offset >= anchorOffset && offset <= focusOffset;
54
+ return withinSpan(selection.anchor.key, selection.anchor.offset) && withinSpan(selection.focus.key, selection.focus.offset);
55
+ }
@@ -1339,6 +1339,13 @@
1339
1339
  height: 14px;
1340
1340
  }
1341
1341
 
1342
+ /* A menu item's keyboard shortcut, dimmed at its trailing edge. */
1343
+ .likhari-context-shortcut {
1344
+ font-size: 11px;
1345
+ color: var(--editor-ink-soft);
1346
+ white-space: nowrap;
1347
+ }
1348
+
1342
1349
  /* Cut, copy and paste as one row of icon buttons in the right-click menu. */
1343
1350
  .likhari-context-row {
1344
1351
  display: flex;
@@ -1355,3 +1362,46 @@
1355
1362
  display: flex;
1356
1363
  justify-content: center;
1357
1364
  }
1365
+
1366
+ /* Table / poetry actions in the right-click menu: a wrapping grid of icon-only
1367
+ buttons instead of a submenu, so the common actions are one click away. */
1368
+ .likhari-context-icons {
1369
+ display: flex;
1370
+ flex-wrap: wrap;
1371
+ gap: 2px;
1372
+ padding: 2px 0;
1373
+ }
1374
+
1375
+ .likhari-context-icons [data-menu-item] {
1376
+ flex: 0 0 auto;
1377
+ width: 30px;
1378
+ justify-content: center;
1379
+ padding-block: 5px;
1380
+ }
1381
+
1382
+ .likhari-context-icons [data-menu-item] > * {
1383
+ display: flex;
1384
+ justify-content: center;
1385
+ }
1386
+
1387
+ /* A thin separator between groups of icon buttons (e.g. merge/unmerge from delete). */
1388
+ .likhari-context-icons-divider {
1389
+ align-self: stretch;
1390
+ width: 1px;
1391
+ margin-block: 4px;
1392
+ background: var(--mantine-color-default-border);
1393
+ }
1394
+
1395
+ /* Poetry: layout (single/two-column/staggered) on its own row, apart from what that
1396
+ layout's selection then allows (insert couplet, spacing, centering, delete). */
1397
+ .likhari-context-icon-rows {
1398
+ display: flex;
1399
+ flex-direction: column;
1400
+ gap: 2px;
1401
+ }
1402
+
1403
+ .likhari-context-icon-rows-divider {
1404
+ height: 1px;
1405
+ margin-inline: 4px;
1406
+ background: var(--mantine-color-default-border);
1407
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inshapardaz/likhari-react",
3
- "version": "0.1.48-dev.356",
3
+ "version": "0.1.48-dev.371",
4
4
  "description": "React implementation of the Likhari editor, built on @lexical/react.",
5
5
  "license": "MIT",
6
6
  "repository": {