@inshapardaz/likhari-react 0.1.28-dev.245 → 0.1.28-dev.257

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":"AA0CA,OAAO,EAAc,KAAK,aAAa,EAAE,MAAM,iCAAiC,CAAC;AACjF,OAAO,EAAkB,KAAK,cAAc,EAAE,MAAM,8BAA8B,CAAC;AACnF,OAAO,EAA2B,KAAK,aAAa,EAAE,MAAM,yBAAyB,CAAC;AAgDtF,OAAO,EAAgB,KAAK,oBAAoB,EAAE,MAAM,gBAAgB,CAAC;AAEzE,OAAO,KAAK,EAAE,2BAA2B,EAAE,MAAM,2BAA2B,CAAC;AAC7E,OAAO,EAA6D,KAAK,UAAU,EAAE,MAAM,UAAU,CAAC;AAItG,OAAO,KAAK,EAAE,MAAM,EAAW,MAAM,iBAAiB,CAAC;AA+hBvD,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE,2BAA2B,CAAC;IACpC,gCAAgC;IAChC,eAAe,CAAC,EAAE,cAAc,EAAE,CAAC;IACnC,qDAAqD;IACrD,gBAAgB,CAAC,EAAE,aAAa,EAAE,CAAC;IACnC,4DAA4D;IAC5D,gBAAgB,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,aAAa,KAAK,IAAI,CAAC;IACnE,+FAA+F;IAC/F,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,YAAY,CAAC,EAAE,MAAM,IAAI,CAAC;IAC1B,wFAAwF;IACxF,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,aAAa,CAAC,EAAE,MAAM,IAAI,CAAC;IAC3B,iGAAiG;IACjG,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,mBAAmB,CAAC,EAAE,MAAM,IAAI,CAAC;IACjC,MAAM,CAAC,EAAE,MAAM,IAAI,CAAC;IACpB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,sEAAsE;IACtE,WAAW,CAAC,EAAE,UAAU,EAAE,CAAC;IAC3B,wFAAwF;IACxF,SAAS,CAAC,EAAE,KAAK,GAAG,KAAK,CAAC;IAC1B,oFAAoF;IACpF,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,kGAAkG;IAClG,MAAM,CAAC,EAAE,oBAAoB,CAAC;CAC/B;AAED,wBAAgB,OAAO,CAAC,EAAE,MAAM,EAAE,gBAAqB,EAAE,eAAoB,EAAE,gBAAgB,EAAE,MAAM,EAAE,OAAO,EAAE,QAAQ,EAAE,QAAgB,EAAE,YAAY,EAAE,SAAiB,EAAE,aAAa,EAAE,eAAuB,EAAE,mBAAmB,EAAE,WAAkC,EAAE,SAAiB,EAAE,MAAa,EAAE,MAAM,EAAE,EAAE,YAAY,+BAspCvU"}
1
+ {"version":3,"file":"Toolbar.d.ts","sourceRoot":"","sources":["../../src/components/Toolbar.tsx"],"names":[],"mappings":"AA0CA,OAAO,EAAc,KAAK,aAAa,EAAE,MAAM,iCAAiC,CAAC;AACjF,OAAO,EAAkB,KAAK,cAAc,EAAE,MAAM,8BAA8B,CAAC;AACnF,OAAO,EAA2B,KAAK,aAAa,EAAE,MAAM,yBAAyB,CAAC;AAgDtF,OAAO,EAAgB,KAAK,oBAAoB,EAAE,MAAM,gBAAgB,CAAC;AAEzE,OAAO,KAAK,EAAE,2BAA2B,EAAE,MAAM,2BAA2B,CAAC;AAC7E,OAAO,EAA6D,KAAK,UAAU,EAAE,MAAM,UAAU,CAAC;AAItG,OAAO,KAAK,EAAE,MAAM,EAAW,MAAM,iBAAiB,CAAC;AAggBvD,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE,2BAA2B,CAAC;IACpC,gCAAgC;IAChC,eAAe,CAAC,EAAE,cAAc,EAAE,CAAC;IACnC,qDAAqD;IACrD,gBAAgB,CAAC,EAAE,aAAa,EAAE,CAAC;IACnC,4DAA4D;IAC5D,gBAAgB,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,aAAa,KAAK,IAAI,CAAC;IACnE,+FAA+F;IAC/F,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,YAAY,CAAC,EAAE,MAAM,IAAI,CAAC;IAC1B,wFAAwF;IACxF,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,aAAa,CAAC,EAAE,MAAM,IAAI,CAAC;IAC3B,iGAAiG;IACjG,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,mBAAmB,CAAC,EAAE,MAAM,IAAI,CAAC;IACjC,MAAM,CAAC,EAAE,MAAM,IAAI,CAAC;IACpB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,sEAAsE;IACtE,WAAW,CAAC,EAAE,UAAU,EAAE,CAAC;IAC3B,wFAAwF;IACxF,SAAS,CAAC,EAAE,KAAK,GAAG,KAAK,CAAC;IAC1B,oFAAoF;IACpF,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,kGAAkG;IAClG,MAAM,CAAC,EAAE,oBAAoB,CAAC;CAC/B;AAED,wBAAgB,OAAO,CAAC,EAAE,MAAM,EAAE,gBAAqB,EAAE,eAAoB,EAAE,gBAAgB,EAAE,MAAM,EAAE,OAAO,EAAE,QAAQ,EAAE,QAAgB,EAAE,YAAY,EAAE,SAAiB,EAAE,aAAa,EAAE,eAAuB,EAAE,mBAAmB,EAAE,WAAkC,EAAE,SAAiB,EAAE,MAAa,EAAE,MAAM,EAAE,EAAE,YAAY,+BA+/BvU"}
@@ -1,6 +1,6 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
- import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
3
- import { Menu, Select } from '@mantine/core';
2
+ import { Fragment, forwardRef, useCallback, useEffect, useRef, useState } from 'react';
3
+ import { Menu, Select, Toolbar as MantineToolbar } from '@mantine/core';
4
4
  import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
5
5
  import { $getSelection, $isRangeSelection, $isElementNode, $setSelection, $getRoot, $selectAll, $getNearestNodeFromDOMNode, $createTextNode, KEY_MODIFIER_COMMAND, COMMAND_PRIORITY_NORMAL, FORMAT_TEXT_COMMAND, FORMAT_ELEMENT_COMMAND, INDENT_CONTENT_COMMAND, OUTDENT_CONTENT_COMMAND, UNDO_COMMAND, REDO_COMMAND, SELECTION_CHANGE_COMMAND, CAN_UNDO_COMMAND, CAN_REDO_COMMAND, COMMAND_PRIORITY_LOW, } from 'lexical';
6
6
  import { $getSelectionStyleValueForProperty, $patchStyleText, $setBlocksType } from '@lexical/selection';
@@ -108,15 +108,18 @@ function iconOptionRenderer(icons, fallback) {
108
108
  return (_jsxs("span", { style: { display: 'flex', alignItems: 'center', gap: 8 }, children: [_jsx(Icon, { size: 16, stroke: ICON_STROKE }), option.label] }));
109
109
  };
110
110
  }
111
- function ToolbarButton({ icon: Icon, title, active, dirty, disabled, onClick, }) {
112
- return (_jsx("button", { type: "button", className: "likhari-toolbar-button", "data-active": active ? 'true' : 'false', "data-dirty": dirty === undefined ? undefined : dirty ? 'true' : 'false', disabled: disabled, "aria-pressed": active, "aria-label": title, title: title, onMouseDown: (e) => e.preventDefault(), onClick: onClick, children: _jsx(Icon, { size: ICON_SIZE, stroke: ICON_STROKE }) }));
113
- }
114
- /** A disabled placeholder for a feature whose config flag is on but that
115
- * isn't implemented yet (link/image/poetry/font/language-tooling — see
116
- * docs/lexical-editor-spec.md §13's phasing). Renders so the toolbar's
117
- * layout is final now and only needs its onClick wired up later. */
118
- function StubButton({ icon, title, comingSoon }) {
119
- return _jsx(ToolbarButton, { icon: icon, title: comingSoon(title), disabled: true, onClick: undefined });
111
+ const ToolbarButton = forwardRef(function ToolbarButton({ icon: Icon, title, active, dirty, onMouseDown, ...rest }, ref) {
112
+ return (_jsx(MantineToolbar.Toggle, { ref: ref, "aria-label": title, title: title, active: active, "data-dirty": dirty === undefined ? undefined : dirty ? 'true' : 'false',
113
+ // Keep the editor's selection: a mousedown here would blur the canvas.
114
+ onMouseDown: (e) => {
115
+ e.preventDefault();
116
+ onMouseDown?.(e);
117
+ }, ...rest, children: _jsx(Icon, { size: ICON_SIZE, stroke: ICON_STROKE }) }));
118
+ });
119
+ function withDividers(sections) {
120
+ return sections
121
+ .filter((section) => Boolean(section))
122
+ .flatMap((section, index) => (index === 0 ? [section] : [_jsx(MantineToolbar.Divider, {}, `divider-${index}`), section]));
120
123
  }
121
124
  /** Mantine Select (combobox) for the toolbar's dropdown controls. The icon
122
125
  * labels the control itself, since a dropdown can't show one per option in
@@ -126,24 +129,9 @@ function ToolbarSelect({ icon: Icon, label, value, data, width, disabled, placeh
126
129
  const portalTarget = usePortalTarget();
127
130
  return (_jsx(Select, { size: "xs", w: width, "aria-label": label, title: disabled && comingSoon ? comingSoon(label) : label, data: data, value: value, placeholder: placeholder, disabled: disabled, searchable: searchable, renderOption: renderOption, nothingFoundMessage: searchable ? noMatchMessage : undefined, allowDeselect: false, leftSection: _jsx(Icon, { size: 15, stroke: ICON_STROKE }), comboboxProps: { withinPortal: true, position: 'bottom-start', middlewares: { flip: true, shift: true }, portalProps: { target: portalTarget } }, classNames: { input: 'likhari-toolbar-select-input', dropdown: 'likhari-toolbar-select-dropdown' }, onChange: (v) => v && onChange?.(v) }));
128
131
  }
129
- const EMPTY_OVERFLOW = new Set();
130
- function setsEqual(a, b) {
131
- if (a.size !== b.size)
132
- return false;
133
- for (const v of a)
134
- if (!b.has(v))
135
- return false;
136
- return true;
137
- }
138
- /** Renders a movable group's items as normal inline toolbar buttons (used
139
- * both in the real toolbar and in the hidden width-measuring clone). */
140
- function MovableGroupButtons({ group }) {
141
- return (_jsx("div", { className: "likhari-toolbar-group", "data-group-key": group.key, children: group.items.map((item) => (_jsx(ToolbarButton, { icon: item.icon, title: item.label, active: item.active, onClick: item.onClick }, item.key))) }));
142
- }
143
- function OverflowItem({ icon: Icon, label, active, onClick, }) {
144
- return (_jsx(Menu.Item, { leftSection: _jsx(Icon, { size: ICON_SIZE, stroke: ICON_STROKE }), "data-active": active ? 'true' : 'false', className: "likhari-overflow-item",
145
- // Keep the editor's selection: a mousedown here would blur the canvas.
146
- onMouseDown: (e) => e.preventDefault(), onClick: onClick, children: label }));
132
+ /** An item in the "..." menu. A mousedown here would blur the canvas, so it is kept. */
133
+ function MoreMenuItem({ icon: Icon, label, active, onClick, }) {
134
+ 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 }));
147
135
  }
148
136
  function LinkMenuItems({ url, onEdit, onRemove, strings }) {
149
137
  // Only offer the link as clickable if it's a URL the editor would have accepted.
@@ -194,15 +182,6 @@ export function Toolbar({ config, dictionaryStores = [], thesaurusStores = [], o
194
182
  const [draftsOpen, setDraftsOpen] = useState(false);
195
183
  const rtl = direction === 'rtl';
196
184
  const ALIGN_ICONS = rtl ? ALIGN_ICONS_RTL : ALIGN_ICONS_LTR;
197
- // Responsive "priority+" overflow: the toolbar's outer container and a
198
- // hidden nowrap clone (rendered with every movable group forced inline)
199
- // are both measured; whenever the clone's natural width exceeds the
200
- // container's available width, the lowest-priority movable groups are
201
- // moved into the "..." menu, lowest-priority first, until what remains
202
- // inline fits (or there's nothing left to move).
203
- const toolbarContainerRef = useRef(null);
204
- const toolbarMeasureRef = useRef(null);
205
- const [overflowGroupKeys, setOverflowGroupKeys] = useState(EMPTY_OVERFLOW);
206
185
  const updateToolbar = useCallback(() => {
207
186
  editor.getEditorState().read(() => {
208
187
  const selection = $getSelection();
@@ -342,7 +321,7 @@ export function Toolbar({ config, dictionaryStores = [], thesaurusStores = [], o
342
321
  };
343
322
  const snapshotSelectionRef = useRef(snapshotSelection);
344
323
  snapshotSelectionRef.current = snapshotSelection;
345
- const runOverflowAction = (action) => () => {
324
+ const runMenuItem = (action) => () => {
346
325
  const saved = menuSelectionRef.current;
347
326
  if (saved)
348
327
  editor.update(() => $setSelection(saved.clone()), { discrete: true });
@@ -646,35 +625,22 @@ export function Toolbar({ config, dictionaryStores = [], thesaurusStores = [], o
646
625
  const headingLevels = config.blocks.headingLevels ?? [];
647
626
  const fmt = config.formatting;
648
627
  const showFormattingGroup = headingLevels.length > 0 || config.lists.bullet || config.lists.numbered || config.lists.check || config.blocks.quote;
649
- const showInlineGroup = fmt.bold || fmt.italic || fmt.underline;
650
628
  const showAlignGroup = config.alignment.start || config.alignment.center || config.alignment.justify || config.alignment.left || config.alignment.right;
651
- const showStubInsertGroup = config.images.linked ||
629
+ const showInsertGroup = config.links ||
630
+ config.images.linked ||
652
631
  config.images.embedded ||
653
632
  config.tables ||
654
633
  config.blocks.horizontalRule ||
655
634
  config.blocks.pageBreak ||
656
635
  config.columns ||
657
636
  config.footnotes ||
658
- config.poetry.enabled;
637
+ (config.poetry.enabled && locale !== 'en') ||
638
+ (config.tables && state.inTable) ||
639
+ (config.poetry.enabled && state.inPoetry);
659
640
  const showLanguageGroup = config.language.autocorrect || config.language.textCleanup || config.language.spellCheck;
660
- // The "script & cleanup" and "indent/outdent" groups (UI spec §3.3) render
661
- // as normal inline toolbar groups whenever there's room, and only move
662
- // into the "..." overflow menu when the measured toolbar content doesn't
663
- // fit the available width (see the ResizeObserver effect below) — this
664
- // replaces the old always-on overflow menu, which was driven purely by
665
- // these config flags regardless of actual space.
666
- const scriptCleanupItems = [];
667
- if (fmt.strikethrough) {
668
- scriptCleanupItems.push({
669
- key: 'strikethrough',
670
- icon: IconStrikethrough,
671
- label: strings.toolbar.strikethrough,
672
- active: state.activeFormats.has('strikethrough'),
673
- onClick: () => formatText('strikethrough'),
674
- });
675
- }
641
+ const moreItems = [];
676
642
  if (fmt.superscript) {
677
- scriptCleanupItems.push({
643
+ moreItems.push({
678
644
  key: 'superscript',
679
645
  icon: IconSuperscript,
680
646
  label: strings.toolbar.superscript,
@@ -683,7 +649,7 @@ export function Toolbar({ config, dictionaryStores = [], thesaurusStores = [], o
683
649
  });
684
650
  }
685
651
  if (fmt.subscript) {
686
- scriptCleanupItems.push({
652
+ moreItems.push({
687
653
  key: 'subscript',
688
654
  icon: IconSubscript,
689
655
  label: strings.toolbar.subscript,
@@ -692,10 +658,10 @@ export function Toolbar({ config, dictionaryStores = [], thesaurusStores = [], o
692
658
  });
693
659
  }
694
660
  if (fmt.caseTransforms) {
695
- scriptCleanupItems.push({ key: 'upper', icon: IconLetterCaseUpper, label: strings.toolbar.uppercase, onClick: () => applyCaseTransform('upper') }, { key: 'lower', icon: IconLetterCaseLower, label: strings.toolbar.lowercase, onClick: () => applyCaseTransform('lower') }, { key: 'capitalize', icon: IconLetterCase, label: strings.toolbar.capitalize, onClick: () => applyCaseTransform('capitalize') });
661
+ moreItems.push({ key: 'upper', icon: IconLetterCaseUpper, label: strings.toolbar.uppercase, onClick: () => applyCaseTransform('upper') }, { key: 'lower', icon: IconLetterCaseLower, label: strings.toolbar.lowercase, onClick: () => applyCaseTransform('lower') }, { key: 'capitalize', icon: IconLetterCase, label: strings.toolbar.capitalize, onClick: () => applyCaseTransform('capitalize') });
696
662
  }
697
663
  if (fmt.clearFormatting) {
698
- scriptCleanupItems.push({ key: 'clear', icon: IconClearFormatting, label: strings.toolbar.clearFormatting, onClick: clearFormatting });
664
+ moreItems.push({ key: 'clear', icon: IconClearFormatting, label: strings.toolbar.clearFormatting, onClick: clearFormatting });
699
665
  }
700
666
  // UI spec §5: indent/outdent arrows are direction-relative and swap in RTL
701
667
  // (outdent moves content toward the start edge, which is the right in RTL).
@@ -715,56 +681,6 @@ export function Toolbar({ config, dictionaryStores = [], thesaurusStores = [], o
715
681
  },
716
682
  ]
717
683
  : [];
718
- // Low to high priority: the first group here is the first one pushed into
719
- // overflow once content stops fitting (UI spec §3.3).
720
- const movableGroups = [
721
- { key: 'scriptCleanup', items: scriptCleanupItems },
722
- { key: 'indentOutdent', items: indentItems },
723
- ].filter((g) => g.items.length > 0);
724
- const movableGroupKeys = useMemo(() => movableGroups.map((g) => g.key).join(','), [movableGroups]);
725
- // Re-measure whenever the container is resized, or whenever the set of
726
- // movable groups (and so the hidden clone's content) changes.
727
- useLayoutEffect(() => {
728
- const container = toolbarContainerRef.current;
729
- const measure = toolbarMeasureRef.current;
730
- if (!container || !measure)
731
- return;
732
- // Rough width of the "..." button itself (icon button + its gap) —
733
- // only charged against the budget once something has actually moved
734
- // into it, since it isn't rendered at all otherwise.
735
- const OVERFLOW_BUTTON_WIDTH = 38;
736
- const recompute = () => {
737
- const available = container.clientWidth;
738
- const total = measure.scrollWidth;
739
- if (total <= available) {
740
- setOverflowGroupKeys((prev) => (prev.size === 0 ? prev : EMPTY_OVERFLOW));
741
- return;
742
- }
743
- const widths = new Map();
744
- measure.querySelectorAll('[data-group-key]').forEach((el) => {
745
- widths.set(el.dataset.groupKey, el.getBoundingClientRect().width);
746
- });
747
- let remaining = total;
748
- const next = new Set();
749
- for (const key of movableGroupKeys.split(',').filter(Boolean)) {
750
- if (remaining <= available)
751
- break;
752
- const w = widths.get(key);
753
- if (w === undefined)
754
- continue;
755
- remaining -= w;
756
- next.add(key);
757
- }
758
- if (next.size > 0)
759
- remaining += OVERFLOW_BUTTON_WIDTH;
760
- setOverflowGroupKeys((prev) => (setsEqual(prev, next) ? prev : next));
761
- };
762
- recompute();
763
- const ro = new ResizeObserver(recompute);
764
- ro.observe(container);
765
- ro.observe(measure);
766
- return () => ro.disconnect();
767
- });
768
684
  // Grouped for the dropdown when options declare groups (Latin / Urdu).
769
685
  const fontData = (() => {
770
686
  const data = [];
@@ -807,36 +723,26 @@ export function Toolbar({ config, dictionaryStores = [], thesaurusStores = [], o
807
723
  ];
808
724
  const formattingValue = state.listType ?? state.blockType;
809
725
  const AlignIcon = ALIGN_ICONS[state.elementFormat] ?? IconAlignLeft;
810
- // The toolbar's "always inline" groups (UI spec §3.3's Desktop tier) —
811
- // built as an array so the exact same elements can be rendered both in
812
- // the visible toolbar and in the hidden width-measuring clone below,
813
- // without duplicating the JSX. The link right-click context menu is kept
814
- // out of this list: it's a zero-size, fixed-position anchor, not part of
815
- // the toolbar's own layout width.
816
- const fixedGroups = [
817
- showSave && (_jsx("div", { className: "likhari-toolbar-group", children: _jsx(ToolbarButton, { icon: IconDeviceFloppy, title: strings.toolbar.save, dirty: Boolean(isDirty), onClick: onSave }) }, "save")),
818
- drafts && (_jsx("div", { className: "likhari-toolbar-group", children: _jsx(ToolbarButton, { icon: IconHistory, title: strings.toolbar.drafts, onClick: () => setDraftsOpen(true) }) }, "drafts")),
819
- config.history && (_jsxs("div", { className: "likhari-toolbar-group", children: [_jsx(ToolbarButton, { icon: IconArrowBackUp, title: strings.toolbar.undo, disabled: !state.canUndo, onClick: () => editor.dispatchCommand(UNDO_COMMAND, undefined) }), _jsx(ToolbarButton, { icon: IconArrowForwardUp, title: strings.toolbar.redo, disabled: !state.canRedo, onClick: () => editor.dispatchCommand(REDO_COMMAND, undefined) })] }, "history")),
820
- showFormattingGroup && (_jsx("div", { className: "likhari-toolbar-group", children: _jsx(ToolbarSelect, { icon: IconPilcrow, label: strings.toolbar.formattingLabel, width: 148, value: formattingValue, data: formattingOptions, comingSoon: strings.toolbar.comingSoon, renderOption: iconOptionRenderer(FORMATTING_ICONS, IconPilcrow), onChange: withRefocus((v) => applyFormatting(v)) }) }, "formatting")),
821
- showInlineGroup && (_jsxs("div", { className: "likhari-toolbar-group", children: [fmt.bold && (_jsx(ToolbarButton, { icon: IconBold, title: strings.toolbar.bold, active: state.activeFormats.has('bold'), onClick: () => formatText('bold') })), fmt.italic && (_jsx(ToolbarButton, { icon: IconItalic, title: strings.toolbar.italic, active: state.activeFormats.has('italic'), onClick: () => formatText('italic') })), fmt.underline && (_jsx(ToolbarButton, { icon: IconUnderline, title: strings.toolbar.underline, active: state.activeFormats.has('underline'), onClick: () => formatText('underline') }))] }, "inline")),
822
- (config.font.family || config.font.size) && (_jsxs("div", { className: "likhari-toolbar-group", children: [config.font.family && (_jsx(ToolbarSelect, { icon: IconTypography, label: strings.toolbar.fontFamily, width: 150, value: fontOptions.some((f) => f.family === shownFontFamily) ? shownFontFamily : null, placeholder: strings.toolbar.fontFamilyPlaceholder, data: fontData, searchable: true, noMatchMessage: strings.toolbar.noMatch, renderOption: ({ option }) => _jsx("span", { style: { fontFamily: option.value }, children: option.label }), onChange: withRefocus((v) => applyFont('font-family', v)) })), config.font.size && (_jsx(ToolbarSelect, { icon: IconTextSize, label: strings.toolbar.fontSize, width: 84, value: FONT_SIZES_PX.some((px) => `${px}px` === shownFontSize) ? shownFontSize : null, placeholder: strings.toolbar.fontSizePlaceholder, data: FONT_SIZES_PX.map((px) => ({ value: `${px}px`, label: String(px) })), onChange: withRefocus((v) => applyFont('font-size', v)) }))] }, "font")),
823
- showAlignGroup && (_jsx("div", { className: "likhari-toolbar-group", children: _jsx(ToolbarSelect, { icon: AlignIcon, label: strings.toolbar.alignment, width: 138, value: state.elementFormat || 'start', data: alignOptions, renderOption: iconOptionRenderer(ALIGN_ICONS, IconAlignLeft), onChange: withRefocus((v) => formatElement(v)) }) }, "align")),
824
- config.links && (_jsx("div", { className: "likhari-toolbar-group", children: state.isLink ? (
825
- // Caret is in a link: the button opens a menu to see, edit or remove it.
826
- _jsxs(Menu, { position: "bottom-start", withinPortal: true, portalProps: { target: portalTarget }, shadow: "sm", width: 240, onOpen: snapshotSelection, children: [_jsx(Menu.Target, { children: _jsx("button", { type: "button", className: "likhari-toolbar-button", "data-active": "true", "aria-pressed": "true", "aria-haspopup": "menu", "aria-label": strings.toolbar.linkOptions, title: strings.toolbar.linkOptions, onMouseDown: (e) => e.preventDefault(), children: _jsx(IconLink, { size: ICON_SIZE, stroke: ICON_STROKE }) }) }), _jsx(Menu.Dropdown, { children: _jsx(LinkMenuItems, { url: state.linkUrl, onEdit: () => {
827
- restoreSelection();
828
- openLinkDialog();
829
- }, onRemove: removeLink, strings: strings }) })] })) : (_jsx(ToolbarButton, { icon: IconLink, title: strings.toolbar.insertLink, onClick: openLinkDialog })) }, "link")),
830
- showStubInsertGroup && (_jsxs("div", { className: "likhari-toolbar-group likhari-toolbar-group--collapse-tablet", children: [(config.images.linked || config.images.embedded) && (_jsx(ToolbarButton, { icon: IconPhoto, title: strings.toolbar.insertImage, onClick: openImageDialog })), config.blocks.pageBreak && (_jsx(ToolbarButton, { icon: IconPageBreak, title: strings.toolbar.insertPageBreak, onClick: () => editor.dispatchCommand(INSERT_PAGE_BREAK_COMMAND, undefined) })), config.tables && _jsx(ToolbarButton, { icon: IconTable, title: strings.toolbar.insertTable, onClick: openTableDialog }), config.columns && _jsx(ToolbarButton, { icon: IconColumns, title: strings.toolbar.insertColumns, onClick: openLayoutDialog }), config.footnotes && (_jsx(ToolbarButton, { icon: IconNumber1Small, title: strings.toolbar.insertFootnote, onClick: () => editor.dispatchCommand(INSERT_FOOTNOTE_COMMAND, undefined) })), config.blocks.horizontalRule && (_jsx(ToolbarButton, { icon: IconSeparatorHorizontal, title: strings.toolbar.insertHorizontalRule, onClick: () => editor.dispatchCommand(INSERT_HORIZONTAL_RULE_COMMAND, undefined) })), config.findReplace && (_jsx(ToolbarButton, { icon: IconSearch, title: strings.findReplace.toggle, active: findOpen, onClick: onToggleFind })), config.poetry.enabled && locale !== 'en' && (_jsx(ToolbarButton, { icon: IconFeather, title: strings.toolbar.insertPoetryCouplet, onClick: () => editor.dispatchCommand(INSERT_POETRY_COUPLET_COMMAND, { layout: config.poetry.defaultLayout }) }))] }, "stubInsert")),
831
- config.tables && state.inTable && (_jsx("div", { className: "likhari-toolbar-group", children: _jsxs(Menu, { position: "bottom-start", withinPortal: true, portalProps: { target: portalTarget }, shadow: "sm", width: 240, onOpen: snapshotSelection, children: [_jsx(Menu.Target, { children: _jsx("button", { type: "button", className: "likhari-toolbar-button", "data-active": "true", "aria-pressed": "true", "aria-haspopup": "menu", "aria-label": strings.toolbar.tableOptions, title: strings.toolbar.tableOptions, onMouseDown: (e) => e.preventDefault(), children: _jsx(IconTableOptions, { size: ICON_SIZE, stroke: ICON_STROKE }) }) }), _jsx(Menu.Dropdown, { children: _jsx(TableMenuItems, { strings: strings, direction: direction, rows: state.tableRows, columns: state.tableColumns, canMergeCells: state.canMergeCells, canUnmergeCell: state.canUnmergeCell, onAction: runMenuAction }) })] }) }, "tableActions")),
832
- config.poetry.enabled && state.inPoetry && (_jsx("div", { className: "likhari-toolbar-group", children: _jsxs(Menu, { position: "bottom-start", withinPortal: true, portalProps: { target: portalTarget }, shadow: "sm", width: 200, onOpen: snapshotSelection, children: [_jsx(Menu.Target, { children: _jsx("button", { type: "button", className: "likhari-toolbar-button", "data-active": "true", "aria-pressed": "true", "aria-haspopup": "menu", "aria-label": strings.toolbar.poetryOptions, title: strings.toolbar.poetryOptions, onMouseDown: (e) => e.preventDefault(), children: _jsx(IconFeather, { size: ICON_SIZE, stroke: ICON_STROKE }) }) }), _jsx(Menu.Dropdown, { children: _jsx(PoetryMenuItems, { strings: strings, layout: state.poetryLayout, scale: state.poetryScale, centered: state.poetryCentered, onAction: runMenuAction }) })] }) }, "poetryActions")),
833
- showLanguageGroup && (_jsxs("div", { className: "likhari-toolbar-group likhari-toolbar-group--collapse-tablet", 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 }))] }, "language")),
726
+ const formatSection = (showFormattingGroup || fmt.bold || fmt.italic || fmt.underline || fmt.strikethrough || moreItems.length > 0) && (_jsxs(Fragment, { children: [showFormattingGroup && (_jsx(MantineToolbar.Group, { children: _jsx(ToolbarSelect, { icon: IconPilcrow, label: strings.toolbar.formattingLabel, width: 148, value: formattingValue, data: formattingOptions, comingSoon: strings.toolbar.comingSoon, renderOption: iconOptionRenderer(FORMATTING_ICONS, IconPilcrow), onChange: withRefocus((v) => applyFormatting(v)) }) })), (fmt.bold || fmt.italic || fmt.underline || fmt.strikethrough || moreItems.length > 0) && (_jsxs(MantineToolbar.Group, { className: "likhari-toolbar-cluster", children: [fmt.bold && _jsx(ToolbarButton, { icon: IconBold, title: strings.toolbar.bold, active: state.activeFormats.has('bold'), onClick: () => formatText('bold') }), fmt.italic && (_jsx(ToolbarButton, { icon: IconItalic, title: strings.toolbar.italic, active: state.activeFormats.has('italic'), onClick: () => formatText('italic') })), fmt.underline && (_jsx(ToolbarButton, { icon: IconUnderline, title: strings.toolbar.underline, active: state.activeFormats.has('underline'), onClick: () => formatText('underline') })), fmt.strikethrough && (_jsx(ToolbarButton, { icon: IconStrikethrough, title: strings.toolbar.strikethrough, active: state.activeFormats.has('strikethrough'), onClick: () => formatText('strikethrough') })), moreItems.length > 0 && (_jsxs(Menu, { position: "bottom-start", withinPortal: true, portalProps: { target: portalTarget }, shadow: "sm", width: 210, closeOnItemClick: true, onOpen: snapshotSelection, children: [_jsx(Menu.Target, { children: _jsx(ToolbarButton, { icon: IconDots, title: strings.toolbar.moreFormatting }) }), _jsx(Menu.Dropdown, { children: moreItems.map((item) => (_jsx(MoreMenuItem, { icon: item.icon, label: item.label, active: item.active, onClick: runMenuItem(item.onClick) }, item.key))) })] }))] }))] }, "format"));
727
+ const alignSection = (showAlignGroup || indentItems.length > 0) && (_jsxs(Fragment, { children: [showAlignGroup && (_jsx(MantineToolbar.Group, { children: _jsx(ToolbarSelect, { icon: AlignIcon, label: strings.toolbar.alignment, width: 138, value: state.elementFormat || 'start', data: alignOptions, renderOption: iconOptionRenderer(ALIGN_ICONS, IconAlignLeft), onChange: withRefocus((v) => formatElement(v)) }) })), indentItems.length > 0 && (_jsx(MantineToolbar.Group, { className: "likhari-toolbar-cluster", children: indentItems.map((item) => (_jsx(ToolbarButton, { icon: item.icon, title: item.label, onClick: item.onClick }, item.key))) }))] }, "align"));
728
+ const fontSection = (config.font.family || config.font.size) && (_jsxs(MantineToolbar.Group, { children: [config.font.family && (_jsx(ToolbarSelect, { icon: IconTypography, label: strings.toolbar.fontFamily, width: 150, value: fontOptions.some((f) => f.family === shownFontFamily) ? shownFontFamily : null, placeholder: strings.toolbar.fontFamilyPlaceholder, data: fontData, searchable: true, noMatchMessage: strings.toolbar.noMatch, renderOption: ({ option }) => _jsx("span", { style: { fontFamily: option.value }, children: option.label }), onChange: withRefocus((v) => applyFont('font-family', v)) })), config.font.size && (_jsx(ToolbarSelect, { icon: IconTextSize, label: strings.toolbar.fontSize, width: 84, value: FONT_SIZES_PX.some((px) => `${px}px` === shownFontSize) ? shownFontSize : null, placeholder: strings.toolbar.fontSizePlaceholder, data: FONT_SIZES_PX.map((px) => ({ value: `${px}px`, label: String(px) })), onChange: withRefocus((v) => applyFont('font-size', v)) }))] }, "font"));
729
+ const insertSection = showInsertGroup && (_jsxs(MantineToolbar.Group, { className: "likhari-toolbar-cluster", children: [config.links &&
730
+ (state.isLink ? (_jsxs(Menu, { position: "bottom-start", withinPortal: true, portalProps: { target: portalTarget }, shadow: "sm", width: 240, onOpen: snapshotSelection, children: [_jsx(Menu.Target, { children: _jsx(ToolbarButton, { icon: IconLink, title: strings.toolbar.linkOptions, active: true, "aria-haspopup": "menu" }) }), _jsx(Menu.Dropdown, { children: _jsx(LinkMenuItems, { url: state.linkUrl, onEdit: () => {
731
+ restoreSelection();
732
+ openLinkDialog();
733
+ }, onRemove: removeLink, strings: strings }) })] })) : (_jsx(ToolbarButton, { icon: IconLink, title: strings.toolbar.insertLink, onClick: openLinkDialog }))), (config.images.linked || config.images.embedded) && (_jsx(ToolbarButton, { icon: IconPhoto, title: strings.toolbar.insertImage, onClick: openImageDialog })), config.blocks.horizontalRule && (_jsx(ToolbarButton, { icon: IconSeparatorHorizontal, title: strings.toolbar.insertHorizontalRule, onClick: () => editor.dispatchCommand(INSERT_HORIZONTAL_RULE_COMMAND, undefined) })), config.tables && _jsx(ToolbarButton, { icon: IconTable, title: strings.toolbar.insertTable, onClick: openTableDialog }), config.tables && state.inTable && (_jsxs(Menu, { position: "bottom-start", withinPortal: true, portalProps: { target: portalTarget }, shadow: "sm", width: 240, onOpen: snapshotSelection, children: [_jsx(Menu.Target, { children: _jsx(ToolbarButton, { icon: IconTableOptions, title: strings.toolbar.tableOptions, active: true, "aria-haspopup": "menu" }) }), _jsx(Menu.Dropdown, { children: _jsx(TableMenuItems, { strings: strings, direction: direction, rows: state.tableRows, columns: state.tableColumns, canMergeCells: state.canMergeCells, canUnmergeCell: state.canUnmergeCell, onAction: runMenuAction }) })] })), config.columns && _jsx(ToolbarButton, { icon: IconColumns, title: strings.toolbar.insertColumns, onClick: openLayoutDialog }), config.footnotes && (_jsx(ToolbarButton, { icon: IconNumber1Small, title: strings.toolbar.insertFootnote, onClick: () => editor.dispatchCommand(INSERT_FOOTNOTE_COMMAND, undefined) })), config.blocks.pageBreak && (_jsx(ToolbarButton, { icon: IconPageBreak, title: strings.toolbar.insertPageBreak, onClick: () => editor.dispatchCommand(INSERT_PAGE_BREAK_COMMAND, undefined) })), config.poetry.enabled && locale !== 'en' && (_jsx(ToolbarButton, { icon: IconFeather, title: strings.toolbar.insertPoetryCouplet, onClick: () => editor.dispatchCommand(INSERT_POETRY_COUPLET_COMMAND, { layout: config.poetry.defaultLayout }) })), config.poetry.enabled && state.inPoetry && (_jsxs(Menu, { position: "bottom-start", withinPortal: true, portalProps: { target: portalTarget }, shadow: "sm", width: 200, onOpen: snapshotSelection, children: [_jsx(Menu.Target, { children: _jsx(ToolbarButton, { icon: IconFeather, title: strings.toolbar.poetryOptions, active: true, "aria-haspopup": "menu" }) }), _jsx(Menu.Dropdown, { children: _jsx(PoetryMenuItems, { strings: strings, layout: state.poetryLayout, scale: state.poetryScale, centered: state.poetryCentered, onAction: runMenuAction }) })] }))] }, "insert"));
734
+ const toolsSection = showLanguageGroup && (_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 }))] }, "tools"));
735
+ const endSection = (drafts || config.findReplace) && (_jsxs(MantineToolbar.Group, { className: "likhari-toolbar-cluster", style: { marginInlineStart: 'auto' }, children: [drafts && _jsx(ToolbarButton, { icon: IconHistory, title: strings.toolbar.drafts, onClick: () => setDraftsOpen(true) }), config.findReplace && (_jsx(ToolbarButton, { icon: IconSearch, title: strings.findReplace.toggle, active: findOpen, onClick: onToggleFind }))] }, "end"));
736
+ const startSections = [
737
+ showSave && (_jsx(MantineToolbar.Group, { className: "likhari-toolbar-cluster", children: _jsx(ToolbarButton, { icon: IconDeviceFloppy, title: strings.toolbar.save, dirty: Boolean(isDirty), onClick: onSave }) }, "save")),
738
+ config.history && (_jsxs(MantineToolbar.Group, { className: "likhari-toolbar-cluster", children: [_jsx(ToolbarButton, { icon: IconArrowBackUp, title: strings.toolbar.undo, disabled: !state.canUndo, onClick: () => editor.dispatchCommand(UNDO_COMMAND, undefined) }), _jsx(ToolbarButton, { icon: IconArrowForwardUp, title: strings.toolbar.redo, disabled: !state.canRedo, onClick: () => editor.dispatchCommand(REDO_COMMAND, undefined) })] }, "history")),
739
+ formatSection,
740
+ alignSection,
741
+ fontSection,
742
+ insertSection,
743
+ toolsSection,
834
744
  ];
835
- return (_jsxs("div", { className: "likhari-toolbar", role: "toolbar", "aria-label": strings.toolbar.ariaLabel, ref: toolbarContainerRef, children: [fixedGroups, movableGroups
836
- .filter((group) => !overflowGroupKeys.has(group.key))
837
- .map((group) => (_jsx(MovableGroupButtons, { group: group }, group.key))), overflowGroupKeys.size > 0 && (_jsxs(Menu, { position: "bottom-end", withinPortal: true, portalProps: { target: portalTarget }, shadow: "sm", width: 210, closeOnItemClick: true, onOpen: snapshotSelection, children: [_jsx(Menu.Target, { children: _jsx("button", { type: "button", className: "likhari-toolbar-button", "aria-label": strings.toolbar.moreFormatting, title: strings.toolbar.moreFormatting, onMouseDown: (e) => e.preventDefault(), children: _jsx(IconDots, { size: ICON_SIZE, stroke: ICON_STROKE }) }) }), _jsx(Menu.Dropdown, { children: movableGroups
838
- .filter((group) => overflowGroupKeys.has(group.key))
839
- .flatMap((group) => group.items.map((item) => (_jsx(OverflowItem, { icon: item.icon, label: item.label, active: item.active, onClick: runOverflowAction(item.onClick) }, item.key)))) })] })), _jsxs(Menu, { opened: contextMenu !== null, onChange: (opened) => {
745
+ return (_jsxs(MantineToolbar, { "aria-label": strings.toolbar.ariaLabel, className: "likhari-toolbar", children: [withDividers(startSections), endSection, _jsxs(Menu, { opened: contextMenu !== null, onChange: (opened) => {
840
746
  if (!opened)
841
747
  setContextMenu(null);
842
748
  }, position: "bottom-start", withinPortal: true, portalProps: { target: portalTarget }, shadow: "sm", width: 260, children: [_jsx(Menu.Target, { children: _jsx("span", { "aria-hidden": "true", style: { position: 'fixed', left: contextMenu?.x ?? -9999, top: contextMenu?.y ?? -9999, width: 1, height: 1, pointerEvents: 'none' } }) }), _jsxs(Menu.Dropdown, { children: [contextMenu?.spelling && (_jsxs(_Fragment, { children: [_jsx(Menu.Label, { children: strings.contextMenu.spelling }), contextMenu.spelling.suggestions.length === 0 && _jsx(Menu.Item, { disabled: true, children: strings.contextMenu.noSuggestions }), contextMenu.spelling.suggestions.map((suggestion) => (_jsx(Menu.Item, { onClick: () => {
@@ -864,14 +770,5 @@ export function Toolbar({ config, dictionaryStores = [], thesaurusStores = [], o
864
770
  }, children: synonym }, synonym))), _jsx(Menu.Divider, {})] })), contextMenu?.link !== undefined && (_jsxs(_Fragment, { children: [_jsx(LinkMenuItems, { url: contextMenu.link, onEdit: () => {
865
771
  restoreSelection();
866
772
  openLinkDialog();
867
- }, onRemove: removeLink, strings: strings }), _jsx(Menu.Divider, {})] })), contextMenu?.table && (_jsxs(_Fragment, { children: [_jsx(TableMenuItems, { strings: strings, direction: direction, rows: contextMenu.table.rows, columns: contextMenu.table.columns, canMergeCells: state.canMergeCells, canUnmergeCell: state.canUnmergeCell, onAction: runMenuAction }), _jsx(Menu.Divider, {})] })), contextMenu?.poetry && (_jsxs(_Fragment, { children: [_jsx(PoetryMenuItems, { strings: strings, layout: state.poetryLayout, scale: state.poetryScale, centered: state.poetryCentered, onAction: runMenuAction }), _jsx(Menu.Divider, {})] })), _jsx(Menu.Item, { leftSection: _jsx(IconCut, { size: ICON_SIZE, stroke: ICON_STROKE }), onClick: () => void copyOrCut(true)(), children: strings.contextMenu.cut }), _jsx(Menu.Item, { leftSection: _jsx(IconCopy, { size: ICON_SIZE, stroke: ICON_STROKE }), onClick: () => void copyOrCut(false)(), children: strings.contextMenu.copy }), _jsx(Menu.Item, { leftSection: _jsx(IconClipboard, { size: ICON_SIZE, stroke: ICON_STROKE }), onClick: () => void paste(), children: strings.contextMenu.paste }), _jsx(Menu.Item, { leftSection: _jsx(IconSelectAll, { size: ICON_SIZE, stroke: ICON_STROKE }), onClick: runMenuAction(() => $selectAll()), children: strings.contextMenu.selectAll })] })] }), _jsxs("div", { "aria-hidden": "true", ref: toolbarMeasureRef, style: {
868
- position: 'absolute',
869
- visibility: 'hidden',
870
- pointerEvents: 'none',
871
- top: 0,
872
- left: 0,
873
- display: 'flex',
874
- flexWrap: 'nowrap',
875
- gap: 8,
876
- }, children: [fixedGroups, movableGroups.map((group) => (_jsx(MovableGroupButtons, { group: group }, group.key)))] }), (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 })] }));
773
+ }, onRemove: removeLink, strings: strings }), _jsx(Menu.Divider, {})] })), contextMenu?.table && (_jsxs(_Fragment, { children: [_jsx(TableMenuItems, { strings: strings, direction: direction, rows: contextMenu.table.rows, columns: contextMenu.table.columns, canMergeCells: state.canMergeCells, canUnmergeCell: state.canUnmergeCell, onAction: runMenuAction }), _jsx(Menu.Divider, {})] })), contextMenu?.poetry && (_jsxs(_Fragment, { children: [_jsx(PoetryMenuItems, { strings: strings, layout: state.poetryLayout, scale: state.poetryScale, centered: state.poetryCentered, onAction: runMenuAction }), _jsx(Menu.Divider, {})] })), _jsx(Menu.Item, { leftSection: _jsx(IconCut, { size: ICON_SIZE, stroke: ICON_STROKE }), onClick: () => void copyOrCut(true)(), children: strings.contextMenu.cut }), _jsx(Menu.Item, { leftSection: _jsx(IconCopy, { size: ICON_SIZE, stroke: ICON_STROKE }), onClick: () => void copyOrCut(false)(), children: strings.contextMenu.copy }), _jsx(Menu.Item, { leftSection: _jsx(IconClipboard, { size: ICON_SIZE, stroke: ICON_STROKE }), onClick: () => void paste(), children: strings.contextMenu.paste }), _jsx(Menu.Item, { leftSection: _jsx(IconSelectAll, { size: ICON_SIZE, stroke: ICON_STROKE }), onClick: runMenuAction(() => $selectAll()), children: strings.contextMenu.selectAll })] })] }), (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 })] }));
877
774
  }
@@ -52,7 +52,7 @@ const en = {
52
52
  subscript: 'Subscript',
53
53
  uppercase: 'UPPERCASE',
54
54
  lowercase: 'lowercase',
55
- capitalize: 'Capitalize',
55
+ capitalize: 'InitCap',
56
56
  clearFormatting: 'Clear formatting',
57
57
  outdent: 'Outdent',
58
58
  indent: 'Indent',
@@ -1,8 +1,10 @@
1
1
  /**
2
2
  * Underlines misspelled words the way the browser does, without changing the
3
- * document: ranges are set on the rendered text through the CSS Custom
4
- * Highlight API. Checking is debounced so typing stays smooth. In browsers
5
- * without the API, nothing is underlined.
3
+ * document. Only blocks that are in view are checked, a block a few seconds after
4
+ * it scrolls into view, and an edit is rechecked after typing pauses. Results are
5
+ * kept per block, so a change re-checks only the block it is in. Underlines are
6
+ * ranges set through the CSS Custom Highlight API. In browsers without it, nothing
7
+ * is underlined.
6
8
  */
7
9
  export declare function SpellHighlightPlugin(): null;
8
10
  //# sourceMappingURL=SpellHighlightPlugin.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"SpellHighlightPlugin.d.ts","sourceRoot":"","sources":["../../src/spellcheck/SpellHighlightPlugin.tsx"],"names":[],"mappings":"AAUA;;;;;GAKG;AACH,wBAAgB,oBAAoB,SAkDnC"}
1
+ {"version":3,"file":"SpellHighlightPlugin.d.ts","sourceRoot":"","sources":["../../src/spellcheck/SpellHighlightPlugin.tsx"],"names":[],"mappings":"AAsBA;;;;;;;GAOG;AACH,wBAAgB,oBAAoB,SAoJnC"}
@@ -1,60 +1,196 @@
1
1
  import { useEffect } from 'react';
2
2
  import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
3
- import { $collectMisspellings } from './spellcheckActions.js';
3
+ import { $getNodeByKey, $getRoot, $isElementNode } from 'lexical';
4
+ import { $misspellingsInBlock } from './spellcheckActions.js';
4
5
  import { getSpeller } from './spellDictionaries.js';
5
6
  import { onSpellWordsChange } from './userWords.js';
6
7
  /** The CSS Custom Highlight name; editor.css styles it as a red wavy underline. */
7
8
  const HIGHLIGHT = 'likhari-spelling';
8
- const DEBOUNCE_MS = 300;
9
+ /** A block that has just come into view is checked after this long, so scrolling past it costs nothing. */
10
+ const VIEW_DELAY_MS = 2000;
11
+ /** An edit inside a visible block is checked after the typing pauses for this long. */
12
+ const TYPING_DELAY_MS = 300;
13
+ /** The top-level block (paragraph, table, list...) that holds a node, or null. */
14
+ function $topLevelKey(node) {
15
+ const root = $getRoot();
16
+ let current = node;
17
+ while (current.getParent() && !current.getParent().is(root))
18
+ current = current.getParent();
19
+ return current.getParent() ? current.getKey() : null;
20
+ }
9
21
  /**
10
22
  * Underlines misspelled words the way the browser does, without changing the
11
- * document: ranges are set on the rendered text through the CSS Custom
12
- * Highlight API. Checking is debounced so typing stays smooth. In browsers
13
- * without the API, nothing is underlined.
23
+ * document. Only blocks that are in view are checked, a block a few seconds after
24
+ * it scrolls into view, and an edit is rechecked after typing pauses. Results are
25
+ * kept per block, so a change re-checks only the block it is in. Underlines are
26
+ * ranges set through the CSS Custom Highlight API. In browsers without it, nothing
27
+ * is underlined.
14
28
  */
15
29
  export function SpellHighlightPlugin() {
16
30
  const [editor] = useLexicalComposerContext();
17
31
  useEffect(() => {
18
- if (typeof CSS === 'undefined' || !('highlights' in CSS))
32
+ if (typeof CSS === 'undefined' || !('highlights' in CSS) || typeof IntersectionObserver === 'undefined')
19
33
  return;
20
34
  let checkers = null;
21
- let timer;
22
35
  let active = true;
36
+ const cache = new Map();
37
+ const visible = new Set();
38
+ const elements = new Map();
39
+ const keyOf = new Map();
40
+ const timers = new Map();
23
41
  const paint = () => {
24
- if (!active || !checkers)
25
- return;
26
- const found = editor.getEditorState().read(() => $collectMisspellings(checkers));
27
42
  const ranges = [];
28
- for (const misspelling of found) {
29
- for (const segment of misspelling.segments) {
30
- const text = editor.getElementByKey(segment.key)?.firstChild;
31
- if (!text || text.nodeType !== Node.TEXT_NODE)
32
- continue;
33
- const range = document.createRange();
34
- range.setStart(text, segment.start);
35
- range.setEnd(text, segment.end);
36
- ranges.push(range);
43
+ for (const [key, words] of cache) {
44
+ const block = editor.getElementByKey(key);
45
+ if (!block || words.length === 0)
46
+ continue;
47
+ const pieces = textPieces(block);
48
+ for (const word of words) {
49
+ for (const piece of pieces) {
50
+ const start = Math.max(word.start, piece.start);
51
+ const end = Math.min(word.end, piece.end);
52
+ if (end <= start)
53
+ continue;
54
+ const range = document.createRange();
55
+ range.setStart(piece.node, start - piece.start);
56
+ range.setEnd(piece.node, end - piece.start);
57
+ ranges.push(range);
58
+ }
37
59
  }
38
60
  }
39
61
  CSS.highlights.set(HIGHLIGHT, new Highlight(...ranges));
40
62
  };
41
- const schedule = () => {
42
- clearTimeout(timer);
43
- timer = setTimeout(paint, DEBOUNCE_MS);
63
+ const checkBlock = (key) => {
64
+ if (!active || !checkers)
65
+ return;
66
+ const words = editor.getEditorState().read(() => {
67
+ const node = $getNodeByKey(key);
68
+ return $isElementNode(node) ? $misspellingsInBlock(node, checkers) : null;
69
+ });
70
+ if (words === null)
71
+ cache.delete(key);
72
+ else
73
+ cache.set(key, words);
74
+ paint();
75
+ };
76
+ const schedule = (key, delay) => {
77
+ clearTimeout(timers.get(key));
78
+ timers.set(key, setTimeout(() => {
79
+ timers.delete(key);
80
+ if (visible.has(key))
81
+ checkBlock(key);
82
+ }, delay));
83
+ };
84
+ const observer = new IntersectionObserver((entries) => {
85
+ for (const entry of entries) {
86
+ const key = keyOf.get(entry.target);
87
+ if (key === undefined)
88
+ continue;
89
+ if (entry.isIntersecting) {
90
+ visible.add(key);
91
+ if (!cache.has(key))
92
+ schedule(key, VIEW_DELAY_MS);
93
+ }
94
+ else {
95
+ visible.delete(key);
96
+ clearTimeout(timers.get(key));
97
+ timers.delete(key);
98
+ }
99
+ }
100
+ });
101
+ // Keeps the observer on exactly the current top-level blocks.
102
+ const syncBlocks = (editorState = editor.getEditorState()) => {
103
+ const current = new Map();
104
+ editorState.read(() => {
105
+ for (const block of $getRoot().getChildren()) {
106
+ const element = editor.getElementByKey(block.getKey());
107
+ if (element)
108
+ current.set(block.getKey(), element);
109
+ }
110
+ });
111
+ for (const [key, element] of elements) {
112
+ if (current.get(key) !== element) {
113
+ observer.unobserve(element);
114
+ keyOf.delete(element);
115
+ elements.delete(key);
116
+ visible.delete(key);
117
+ cache.delete(key);
118
+ clearTimeout(timers.get(key));
119
+ timers.delete(key);
120
+ }
121
+ }
122
+ for (const [key, element] of current) {
123
+ if (!elements.has(key)) {
124
+ elements.set(key, element);
125
+ keyOf.set(element, key);
126
+ observer.observe(element);
127
+ }
128
+ }
44
129
  };
45
- const unregister = editor.registerUpdateListener(schedule);
46
- const unsubscribe = onSpellWordsChange(schedule);
130
+ const unregisterUpdate = editor.registerUpdateListener(({ editorState, dirtyElements, dirtyLeaves }) => {
131
+ if (!checkers)
132
+ return;
133
+ const dirty = new Set();
134
+ editorState.read(() => {
135
+ for (const key of dirtyLeaves) {
136
+ const node = $getNodeByKey(key);
137
+ const top = node ? $topLevelKey(node) : null;
138
+ if (top)
139
+ dirty.add(top);
140
+ }
141
+ for (const key of dirtyElements.keys()) {
142
+ const node = $getNodeByKey(key);
143
+ const top = node ? $topLevelKey(node) : null;
144
+ if (top)
145
+ dirty.add(top);
146
+ }
147
+ });
148
+ syncBlocks(editorState);
149
+ for (const key of dirty) {
150
+ if (visible.has(key))
151
+ schedule(key, TYPING_DELAY_MS);
152
+ else
153
+ cache.delete(key);
154
+ }
155
+ if (dirty.size > 0)
156
+ paint();
157
+ });
158
+ const unsubscribeWords = onSpellWordsChange(() => {
159
+ cache.clear();
160
+ for (const key of visible)
161
+ schedule(key, 0);
162
+ paint();
163
+ });
47
164
  Promise.all([getSpeller('en'), getSpeller('ur')]).then(async ([en, ur]) => {
165
+ if (!active)
166
+ return;
48
167
  checkers = { ltr: en ? await en : undefined, rtl: ur ? await ur : undefined };
49
- schedule();
168
+ syncBlocks();
169
+ for (const key of visible)
170
+ if (!cache.has(key))
171
+ schedule(key, 0);
50
172
  });
51
173
  return () => {
52
174
  active = false;
53
- clearTimeout(timer);
54
- unregister();
55
- unsubscribe();
175
+ observer.disconnect();
176
+ unregisterUpdate();
177
+ unsubscribeWords();
178
+ for (const timer of timers.values())
179
+ clearTimeout(timer);
56
180
  CSS.highlights.delete(HIGHLIGHT);
57
181
  };
58
182
  }, [editor]);
59
183
  return null;
60
184
  }
185
+ /** The text nodes of a rendered block, with where each one starts in the block's text. */
186
+ function textPieces(block) {
187
+ const pieces = [];
188
+ const walker = document.createTreeWalker(block, NodeFilter.SHOW_TEXT);
189
+ let offset = 0;
190
+ for (let node = walker.nextNode(); node; node = walker.nextNode()) {
191
+ const text = node;
192
+ pieces.push({ node: text, start: offset, end: offset + text.length });
193
+ offset += text.length;
194
+ }
195
+ return pieces;
196
+ }
@@ -1,3 +1,4 @@
1
+ import type { ElementNode } from 'lexical';
1
2
  import { type FindMatch } from '../find/findReplaceActions.js';
2
3
  import { type Speller } from './spellDictionaries.js';
3
4
  /** The checker for each text direction: English for LTR blocks, Urdu for RTL. */
@@ -18,4 +19,16 @@ export interface Misspelling {
18
19
  }
19
20
  /** Every word in the document its block's checker does not recognise. */
20
21
  export declare function $collectMisspellings(checkers: Checkers): Misspelling[];
22
+ /** A misspelled word inside a block, as offsets into the block's text. */
23
+ export interface BlockMisspelling {
24
+ start: number;
25
+ end: number;
26
+ word: string;
27
+ }
28
+ /**
29
+ * The misspelled words inside one top-level block (a paragraph, a table, a list...),
30
+ * as offsets into the block's text. Words are found per parent (paragraph or cell),
31
+ * so text in different cells never forms one word. Must run inside an editor read.
32
+ */
33
+ export declare function $misspellingsInBlock(block: ElementNode, checkers: Checkers): BlockMisspelling[];
21
34
  //# sourceMappingURL=spellcheckActions.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"spellcheckActions.d.ts","sourceRoot":"","sources":["../../src/spellcheck/spellcheckActions.ts"],"names":[],"mappings":"AAAA,OAAO,EAAwB,KAAK,SAAS,EAAE,MAAM,4BAA4B,CAAC;AAClF,OAAO,EAAW,KAAK,OAAO,EAAE,MAAM,qBAAqB,CAAC;AAE5D,iFAAiF;AACjF,MAAM,WAAW,QAAQ;IACvB,GAAG,CAAC,EAAE,OAAO,CAAC;IACd,GAAG,CAAC,EAAE,OAAO,CAAC;CACf;AAED,qEAAqE;AACrE,MAAM,WAAW,OAAO;IACtB,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,MAAM,CAAC;CACb;AAED,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,SAAS,CAAC;IACjB,QAAQ,EAAE,OAAO,EAAE,CAAC;CACrB;AAED,yEAAyE;AACzE,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,QAAQ,GAAG,WAAW,EAAE,CAmBtE"}
1
+ {"version":3,"file":"spellcheckActions.d.ts","sourceRoot":"","sources":["../../src/spellcheck/spellcheckActions.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAC3C,OAAO,EAAwB,KAAK,SAAS,EAAE,MAAM,4BAA4B,CAAC;AAClF,OAAO,EAAW,KAAK,OAAO,EAAE,MAAM,qBAAqB,CAAC;AAE5D,iFAAiF;AACjF,MAAM,WAAW,QAAQ;IACvB,GAAG,CAAC,EAAE,OAAO,CAAC;IACd,GAAG,CAAC,EAAE,OAAO,CAAC;CACf;AAED,qEAAqE;AACrE,MAAM,WAAW,OAAO;IACtB,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,MAAM,CAAC;CACb;AAED,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,SAAS,CAAC;IACjB,QAAQ,EAAE,OAAO,EAAE,CAAC;CACrB;AAED,yEAAyE;AACzE,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,QAAQ,GAAG,WAAW,EAAE,CAmBtE;AAeD,0EAA0E;AAC1E,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;CACd;AAED;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,WAAW,EAAE,QAAQ,EAAE,QAAQ,GAAG,gBAAgB,EAAE,CA8B/F"}
@@ -36,3 +36,38 @@ function segmentsOf(group, start, end) {
36
36
  }
37
37
  return segments;
38
38
  }
39
+ /**
40
+ * The misspelled words inside one top-level block (a paragraph, a table, a list...),
41
+ * as offsets into the block's text. Words are found per parent (paragraph or cell),
42
+ * so text in different cells never forms one word. Must run inside an editor read.
43
+ */
44
+ export function $misspellingsInBlock(block, checkers) {
45
+ const nodes = block.getAllTextNodes();
46
+ const starts = [];
47
+ let offset = 0;
48
+ for (const node of nodes) {
49
+ starts.push(offset);
50
+ offset += node.getTextContent().length;
51
+ }
52
+ const found = [];
53
+ let i = 0;
54
+ while (i < nodes.length) {
55
+ const parent = nodes[i].getParentOrThrow();
56
+ let j = i;
57
+ let text = '';
58
+ while (j < nodes.length && nodes[j].getParentOrThrow().getKey() === parent.getKey()) {
59
+ text += nodes[j].getTextContent();
60
+ j += 1;
61
+ }
62
+ const speller = parent.getDirection() === 'rtl' ? checkers.rtl : checkers.ltr;
63
+ if (speller) {
64
+ for (const span of wordsIn(text)) {
65
+ if (!speller.correct(span.word)) {
66
+ found.push({ start: starts[i] + span.start, end: starts[i] + span.end, word: span.word });
67
+ }
68
+ }
69
+ }
70
+ i = j;
71
+ }
72
+ return found;
73
+ }
@@ -19,12 +19,6 @@
19
19
  its own height so only the canvas below scrolls. */
20
20
  .likhari-toolbar {
21
21
  display: flex;
22
- /* Containing block for the hidden width-measuring clone (Toolbar.tsx):
23
- without this, its position: absolute would resolve against the
24
- viewport instead, and its intentionally-unwrapped (possibly very wide)
25
- content could inflate the host page's scroll area instead of being
26
- clipped by .likhari-root's overflow: hidden. */
27
- position: relative;
28
22
  flex-shrink: 0;
29
23
  align-items: center;
30
24
  gap: 8px;
@@ -57,55 +51,6 @@
57
51
  pointer-events: auto;
58
52
  }
59
53
 
60
- .likhari-toolbar-group {
61
- display: flex;
62
- align-items: center;
63
- gap: 2px;
64
- padding: 0 6px;
65
- border-inline-end: 0.5px solid var(--editor-line);
66
- }
67
-
68
- .likhari-toolbar-group:last-child {
69
- border-inline-end: none;
70
- }
71
-
72
- .likhari-toolbar-button {
73
- display: inline-flex;
74
- align-items: center;
75
- justify-content: center;
76
- min-width: 30px;
77
- height: 30px;
78
- padding: 0 6px;
79
- border-radius: 6px;
80
- border: none;
81
- background: transparent;
82
- color: var(--editor-ink);
83
- cursor: pointer;
84
- }
85
-
86
- .likhari-toolbar-button svg {
87
- display: block;
88
- }
89
-
90
- .likhari-toolbar-button:hover {
91
- background: var(--editor-line);
92
- }
93
-
94
- .likhari-toolbar-button[data-active='true'] {
95
- background: var(--editor-accent-soft);
96
- color: var(--editor-accent);
97
- }
98
-
99
- .likhari-toolbar-button:focus-visible {
100
- outline: 2px solid var(--editor-accent);
101
- outline-offset: 1px;
102
- }
103
-
104
- .likhari-toolbar-button:disabled {
105
- opacity: 0.4;
106
- cursor: not-allowed;
107
- }
108
-
109
54
  /* Mantine Select inside the toolbar — sized to the 30px control height and
110
55
  themed from the --editor-* tokens (the closed input only; the dropdown is
111
56
  portaled to <body>, outside the token scope, so it uses Mantine's own
@@ -125,35 +70,35 @@
125
70
  cursor: not-allowed;
126
71
  }
127
72
 
128
- .likhari-overflow-item[data-active='true'] {
129
- background: var(--mantine-primary-color-light);
130
- color: var(--mantine-primary-color-filled);
73
+ /* A cluster of related buttons (undo/redo, bold/italic/underline/strikethrough, ...) reads as one control. */
74
+ .likhari-toolbar-cluster {
75
+ border: 1px solid var(--editor-line-strong);
76
+ border-radius: 6px;
77
+ background: var(--editor-paper);
78
+ overflow: hidden;
131
79
  }
132
80
 
133
- /* Responsive collapse — editor-ui-design-spec.md §3.3. These groups hold
134
- only not-yet-implemented placeholders, so hiding them below the tablet
135
- breakpoint costs nothing functional while keeping the primary row usable
136
- on small screens; the toolbar wraps onto a second row if the
137
- still-visible controls alone overflow a narrow viewport. */
138
- @media (max-width: 900px) {
139
- .likhari-toolbar-group--collapse-tablet {
140
- display: none;
141
- }
81
+ .likhari-toolbar-cluster [data-toolbar-toggle] {
82
+ border: none;
83
+ border-radius: 0;
84
+ }
85
+
86
+ .likhari-toolbar-cluster [data-toolbar-toggle] + [data-toolbar-toggle] {
87
+ border-inline-start: 1px solid var(--editor-line);
142
88
  }
143
89
 
144
- /* Save (icon-only ToolbarButton): filled accent while there are unsaved
145
- changes, back to the plain outline once saved — editor-ui-design-spec.md §7. */
146
- .likhari-toolbar-button[data-dirty='true'] {
90
+ /* Save: filled accent while there are unsaved changes, back to the plain outline once saved. */
91
+ .likhari-toolbar [data-dirty='true'] {
147
92
  background: var(--editor-accent);
148
93
  color: var(--editor-paper);
149
94
  }
150
95
 
151
- .likhari-toolbar-button[data-dirty='true']:hover {
96
+ .likhari-toolbar [data-dirty='true']:hover {
152
97
  background: var(--editor-accent);
153
98
  filter: brightness(1.1);
154
99
  }
155
100
 
156
- .likhari-toolbar-button[data-dirty='false'] {
101
+ .likhari-toolbar [data-dirty='false'] {
157
102
  color: var(--editor-accent);
158
103
  }
159
104
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inshapardaz/likhari-react",
3
- "version": "0.1.28-dev.245",
3
+ "version": "0.1.28-dev.257",
4
4
  "description": "React implementation of the Likhari editor, built on @lexical/react.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -47,14 +47,14 @@
47
47
  "nspell": "^2.1.5"
48
48
  },
49
49
  "peerDependencies": {
50
- "@mantine/core": "^9.6.3",
51
- "@mantine/hooks": "^9.6.3",
50
+ "@mantine/core": "^9.7.0",
51
+ "@mantine/hooks": "^9.7.0",
52
52
  "react": "^19.2.0",
53
53
  "react-dom": "^19.2.0"
54
54
  },
55
55
  "devDependencies": {
56
- "@mantine/core": "^9.6.3",
57
- "@mantine/hooks": "^9.6.3",
56
+ "@mantine/core": "^9.7.0",
57
+ "@mantine/hooks": "^9.7.0",
58
58
  "@types/nspell": "^2.1.6"
59
59
  }
60
60
  }