@inshapardaz/likhari-react 0.1.28 → 0.1.29

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.
@@ -2,6 +2,7 @@ import type { MantineThemeOverride } from '@mantine/core';
2
2
  import type { SerializedEditorState } from 'lexical';
3
3
  import { type FormatId } from '@inshapardaz/likhari-converters';
4
4
  import { type EditorFeatureConfig, type FeatureConfigPresetName } from '@inshapardaz/likhari-core';
5
+ import { type ToolbarStyle } from './components/Toolbar.js';
5
6
  import { type FontOption } from './fonts/index.js';
6
7
  import { type DraftRestoreMode } from './components/DraftRestore.js';
7
8
  import { type AutoCorrectStore } from './autocorrect/autoCorrectStores.js';
@@ -47,6 +48,8 @@ export interface EditorRootProps {
47
48
  * default tokens (packages/core/src/theme/tokens.ts).
48
49
  */
49
50
  accentColor?: string;
51
+ /** Toolbar appearance: whether it is outlined, and the button variant. */
52
+ toolbarStyle?: ToolbarStyle;
50
53
  locale?: Locale;
51
54
  placeholder?: string;
52
55
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"EditorRoot.d.ts","sourceRoot":"","sources":["../src/EditorRoot.tsx"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"EditorRoot.d.ts","sourceRoot":"","sources":["../src/EditorRoot.tsx"],"names":[],"mappings":"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"}
@@ -58,7 +58,7 @@ function initialEditorStateJson(initialContent) {
58
58
  const state = defaultFormatRegistry.parse(initialContent.format, initialContent.value);
59
59
  return JSON.stringify(state);
60
60
  }
61
- export const EditorRoot = forwardRef(function EditorRoot({ documentId, autoCorrectStores, dictionaryStores, completionStores, thesaurusStores, autoCompleteLanguage = 'auto', urduNormalization, punctuation, initialContent, featureConfig, featurePreset, theme, colorScheme, accentColor, locale = 'en', placeholder, height = '480px', onChange, onSave, showSave = Boolean(onSave), onImageUpload, fetchImage, fontOptions, autosave = true, restoreDraft = 'prompt', navigationGuard = 'confirm', onDraftRestored, autosaveDelayMs = DEFAULT_AUTOSAVE_DELAY_MS, autosaveMaxBytes = DEFAULT_AUTOSAVE_MAX_BYTES, autosaveMaxDrafts = DEFAULT_AUTOSAVE_MAX_DRAFTS, }, ref) {
61
+ export const EditorRoot = forwardRef(function EditorRoot({ documentId, autoCorrectStores, dictionaryStores, completionStores, thesaurusStores, autoCompleteLanguage = 'auto', urduNormalization, punctuation, initialContent, featureConfig, featurePreset, theme, colorScheme, accentColor, toolbarStyle, locale = 'en', placeholder, height = '480px', onChange, onSave, showSave = Boolean(onSave), onImageUpload, fetchImage, fontOptions, autosave = true, restoreDraft = 'prompt', navigationGuard = 'confirm', onDraftRestored, autosaveDelayMs = DEFAULT_AUTOSAVE_DELAY_MS, autosaveMaxBytes = DEFAULT_AUTOSAVE_MAX_BYTES, autosaveMaxDrafts = DEFAULT_AUTOSAVE_MAX_DRAFTS, }, ref) {
62
62
  const config = useMemo(() => resolveFeatureConfig(featureConfig, featurePreset), [featureConfig, featurePreset]);
63
63
  const strings = useMemo(() => getStrings(locale), [locale]);
64
64
  const resolvedPlaceholder = placeholder ?? strings.editor.placeholder;
@@ -311,7 +311,7 @@ export const EditorRoot = forwardRef(function EditorRoot({ documentId, autoCorre
311
311
  // rather than colliding on a shared one.
312
312
  const portalTargetId = useId();
313
313
  const portalTargetSelector = `#${CSS.escape(portalTargetId)}`;
314
- return (_jsx(EditorThemeProvider, { theme: theme, colorScheme: colorScheme, accentColor: accentColor, scopeElementId: portalTargetId, children: _jsxs("div", { className: "likhari-root", dir: dir, ref: rootElementRef, "data-document-id": documentId, style: { height: typeof height === 'number' ? `${height}px` : height }, children: [_jsx("div", { id: portalTargetId, className: "likhari-portal-target" }), _jsx(PortalTargetContext.Provider, { value: portalTargetSelector, children: _jsxs(UiStringsContext.Provider, { value: strings, children: [_jsx(ImageOptionsContext.Provider, { value: imageOptions, children: _jsxs(LexicalComposer, { initialConfig: initialConfig, children: [_jsx(Toolbar, { config: config, dictionaryStores: wordStores, thesaurusStores: thesaurusStores ?? DEFAULT_THESAURUS_STORES, onAddAutoCorrect: config.language.autocorrect ? openAutoCorrectFor : undefined, onSave: handleSave, findOpen: openPanel === 'find', onToggleFind: () => togglePanel('find'), spellOpen: openPanel === 'spell', onToggleSpell: () => togglePanel('spell'), autoCorrectOpen: openPanel === 'autocorrect', onToggleAutoCorrect: () => togglePanel('autocorrect'), isDirty: isDirty, showSave: showSave, fontOptions: fontOptions, direction: dir, locale: locale, drafts: autosave
314
+ return (_jsx(EditorThemeProvider, { theme: theme, colorScheme: colorScheme, accentColor: accentColor, scopeElementId: portalTargetId, children: _jsxs("div", { className: "likhari-root", dir: dir, ref: rootElementRef, "data-document-id": documentId, style: { height: typeof height === 'number' ? `${height}px` : height }, children: [_jsx("div", { id: portalTargetId, className: "likhari-portal-target" }), _jsx(PortalTargetContext.Provider, { value: portalTargetSelector, children: _jsxs(UiStringsContext.Provider, { value: strings, children: [_jsx(ImageOptionsContext.Provider, { value: imageOptions, children: _jsxs(LexicalComposer, { initialConfig: initialConfig, children: [_jsx(Toolbar, { config: config, toolbarStyle: toolbarStyle, dictionaryStores: wordStores, thesaurusStores: thesaurusStores ?? DEFAULT_THESAURUS_STORES, onAddAutoCorrect: config.language.autocorrect ? openAutoCorrectFor : undefined, onSave: handleSave, findOpen: openPanel === 'find', onToggleFind: () => togglePanel('find'), spellOpen: openPanel === 'spell', onToggleSpell: () => togglePanel('spell'), autoCorrectOpen: openPanel === 'autocorrect', onToggleAutoCorrect: () => togglePanel('autocorrect'), isDirty: isDirty, showSave: showSave, fontOptions: fontOptions, direction: dir, locale: locale, drafts: autosave
315
315
  ? { currentId: draftId, maxBytes: autosaveMaxBytes, adoptOnRestore: !documentId, onRestored: handleDraftRestored }
316
316
  : undefined }), documentId && (_jsx(DraftRestore, { draftId: documentId, mode: restoreDraft, initialJson: initialContentJsonRef.current, locale: locale, onRestored: handleDraftRestored })), _jsxs("div", { className: "likhari-canvas-frame", children: [config.findReplace && openPanel === 'find' && _jsx(FindReplaceBar, { strings: strings, dir: dir, onClose: () => setOpenPanel(null) }), config.language.spellCheck && openPanel === 'spell' && (_jsx(SpellcheckPanel, { strings: strings, dir: dir, dictionaryStores: wordStores, onAddAutoCorrect: config.language.autocorrect ? openAutoCorrectFor : undefined, onClose: () => setOpenPanel(null) })), config.language.autocorrect && openPanel === 'autocorrect' && (_jsx(AutoCorrectPanel, { strings: strings, dir: dir, stores: stores, initialFrom: autoCorrectPrefill?.from, initialLanguage: autoCorrectPrefill?.language, onSaved: () => setAutoCorrectVersion((v) => v + 1), onClose: () => setOpenPanel(null) }, autoCorrectPrefill?.id ?? 0)), _jsx("div", { className: "likhari-canvas", children: _jsx(RichTextPlugin, { contentEditable: _jsx(ContentEditable, { className: "likhari-content-editable", dir: dir, spellCheck: false, "aria-label": strings.editor.contentLabel }), placeholder: _jsx("div", { className: "likhari-placeholder", children: resolvedPlaceholder }), ErrorBoundary: LexicalErrorBoundary }) })] }), config.history && _jsx(HistoryPlugin, {}), (config.lists.bullet || config.lists.numbered || config.lists.check) && _jsx(ListPlugin, {}), config.lists.check && _jsx(CheckListPlugin, {}), config.links && _jsx(LinkPlugin, {}), config.links && _jsx(LinkPastePlugin, {}), config.blocks.pageBreak && _jsx(PageBreakPlugin, {}), config.language.spellCheck && _jsx(SpellHighlightPlugin, {}), config.language.autocomplete && (_jsx(AutocompletePlugin, { label: strings.autoComplete.label, stores: resolvedCompletionStores, language: autoCompleteLanguage })), (config.language.autocorrect || config.language.textCleanup) && _jsx(AutoCorrectPlugin, { stores: stores, version: autoCorrectVersion, enabled: true, typing: config.language.autocorrect, urduNormalization: urduNormalization, punctuation: punctuation }), config.columns && _jsx(LayoutPlugin, {}), config.footnotes && _jsx(FootnotePlugin, {}), config.poetry.enabled && _jsx(PoetryPlugin, {}), config.poetry.enabled && _jsx(PoetryResizer, {}), config.tables && _jsx(TablePlugin, { hasCellMerge: true, hasTabHandler: true }), config.blocks.horizontalRule && _jsx(HorizontalRulePlugin, {}), _jsx(EditorInstancePlugin, { instanceRef: editorInstanceRef }), _jsx(OnChangePlugin, { onChange: handleChange })] }) }), _jsx(LeaveDialog, { opened: leavePrompt !== null, canSave: Boolean(onSave), canSaveDraft: autosave || navigationGuard === 'save-draft', draftFailed: leavePrompt?.draftFailed ?? false, onSave: () => {
317
317
  handleSave();
@@ -5,8 +5,16 @@ import { type DraftsToolbarOptions } from './DraftsDialog.js';
5
5
  import type { ResolvedEditorFeatureConfig } from '@inshapardaz/likhari-core';
6
6
  import { type FontOption } from '../fonts/index.js';
7
7
  import type { Locale } from '../i18n/strings.js';
8
+ /** Appearance of the toolbar. The accent colour comes from the editor's `accentColor`. */
9
+ export interface ToolbarStyle {
10
+ /** Outline the toolbar and each button cluster. Default true. */
11
+ bordered?: boolean;
12
+ /** `light` tints the active button, `filled` fills it with the accent. Default `light`. */
13
+ variant?: 'light' | 'filled';
14
+ }
8
15
  export interface ToolbarProps {
9
16
  config: ResolvedEditorFeatureConfig;
17
+ toolbarStyle?: ToolbarStyle;
10
18
  /** Where synonyms come from. */
11
19
  thesaurusStores?: ThesaurusStore[];
12
20
  /** Where words added to the dictionary are saved. */
@@ -34,5 +42,5 @@ export interface ToolbarProps {
34
42
  /** Autosave drafts: when set, the toolbar offers a Drafts button that lists and restores them. */
35
43
  drafts?: DraftsToolbarOptions;
36
44
  }
37
- export declare function Toolbar({ config, dictionaryStores, thesaurusStores, onAddAutoCorrect, onSave, isDirty, showSave, findOpen, onToggleFind, spellOpen, onToggleSpell, autoCorrectOpen, onToggleAutoCorrect, fontOptions, direction, locale, drafts }: ToolbarProps): import("react").JSX.Element;
45
+ export declare function Toolbar({ config, toolbarStyle, dictionaryStores, thesaurusStores, onAddAutoCorrect, onSave, isDirty, showSave, findOpen, onToggleFind, spellOpen, onToggleSpell, autoCorrectOpen, onToggleAutoCorrect, fontOptions, direction, locale, drafts }: ToolbarProps): import("react").JSX.Element;
38
46
  //# sourceMappingURL=Toolbar.d.ts.map
@@ -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,0FAA0F;AAC1F,MAAM,WAAW,YAAY;IAC3B,iEAAiE;IACjE,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,2FAA2F;IAC3F,OAAO,CAAC,EAAE,OAAO,GAAG,QAAQ,CAAC;CAC9B;AAED,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE,2BAA2B,CAAC;IACpC,YAAY,CAAC,EAAE,YAAY,CAAC;IAC5B,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,YAAY,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,+BAugCrV"}
@@ -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.
@@ -186,7 +174,9 @@ function PoetryMenuItems({ strings, layout, scale, centered, onAction, }) {
186
174
  };
187
175
  return (_jsxs(_Fragment, { children: [_jsx(Menu.Label, { 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(Menu.Divider, {}), _jsx(Menu.Divider, {}), 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)), 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')] }));
188
176
  }
189
- export function Toolbar({ config, dictionaryStores = [], thesaurusStores = [], onAddAutoCorrect, onSave, isDirty, showSave, findOpen = false, onToggleFind, spellOpen = false, onToggleSpell, autoCorrectOpen = false, onToggleAutoCorrect, fontOptions = DEFAULT_FONT_OPTIONS, direction = 'ltr', locale = 'en', drafts }) {
177
+ export function Toolbar({ config, toolbarStyle, dictionaryStores = [], thesaurusStores = [], onAddAutoCorrect, onSave, isDirty, showSave, findOpen = false, onToggleFind, spellOpen = false, onToggleSpell, autoCorrectOpen = false, onToggleAutoCorrect, fontOptions = DEFAULT_FONT_OPTIONS, direction = 'ltr', locale = 'en', drafts }) {
178
+ const bordered = toolbarStyle?.bordered ?? true;
179
+ const variant = toolbarStyle?.variant ?? 'light';
190
180
  const [editor] = useLexicalComposerContext();
191
181
  const [state, setState] = useState(INITIAL_STATE);
192
182
  const strings = useStrings(locale);
@@ -194,15 +184,6 @@ export function Toolbar({ config, dictionaryStores = [], thesaurusStores = [], o
194
184
  const [draftsOpen, setDraftsOpen] = useState(false);
195
185
  const rtl = direction === 'rtl';
196
186
  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
187
  const updateToolbar = useCallback(() => {
207
188
  editor.getEditorState().read(() => {
208
189
  const selection = $getSelection();
@@ -342,7 +323,7 @@ export function Toolbar({ config, dictionaryStores = [], thesaurusStores = [], o
342
323
  };
343
324
  const snapshotSelectionRef = useRef(snapshotSelection);
344
325
  snapshotSelectionRef.current = snapshotSelection;
345
- const runOverflowAction = (action) => () => {
326
+ const runMenuItem = (action) => () => {
346
327
  const saved = menuSelectionRef.current;
347
328
  if (saved)
348
329
  editor.update(() => $setSelection(saved.clone()), { discrete: true });
@@ -646,35 +627,22 @@ export function Toolbar({ config, dictionaryStores = [], thesaurusStores = [], o
646
627
  const headingLevels = config.blocks.headingLevels ?? [];
647
628
  const fmt = config.formatting;
648
629
  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
630
  const showAlignGroup = config.alignment.start || config.alignment.center || config.alignment.justify || config.alignment.left || config.alignment.right;
651
- const showStubInsertGroup = config.images.linked ||
631
+ const showInsertGroup = config.links ||
632
+ config.images.linked ||
652
633
  config.images.embedded ||
653
634
  config.tables ||
654
635
  config.blocks.horizontalRule ||
655
636
  config.blocks.pageBreak ||
656
637
  config.columns ||
657
638
  config.footnotes ||
658
- config.poetry.enabled;
639
+ (config.poetry.enabled && locale !== 'en') ||
640
+ (config.tables && state.inTable) ||
641
+ (config.poetry.enabled && state.inPoetry);
659
642
  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
- }
643
+ const moreItems = [];
676
644
  if (fmt.superscript) {
677
- scriptCleanupItems.push({
645
+ moreItems.push({
678
646
  key: 'superscript',
679
647
  icon: IconSuperscript,
680
648
  label: strings.toolbar.superscript,
@@ -683,7 +651,7 @@ export function Toolbar({ config, dictionaryStores = [], thesaurusStores = [], o
683
651
  });
684
652
  }
685
653
  if (fmt.subscript) {
686
- scriptCleanupItems.push({
654
+ moreItems.push({
687
655
  key: 'subscript',
688
656
  icon: IconSubscript,
689
657
  label: strings.toolbar.subscript,
@@ -692,10 +660,10 @@ export function Toolbar({ config, dictionaryStores = [], thesaurusStores = [], o
692
660
  });
693
661
  }
694
662
  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') });
663
+ 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
664
  }
697
665
  if (fmt.clearFormatting) {
698
- scriptCleanupItems.push({ key: 'clear', icon: IconClearFormatting, label: strings.toolbar.clearFormatting, onClick: clearFormatting });
666
+ moreItems.push({ key: 'clear', icon: IconClearFormatting, label: strings.toolbar.clearFormatting, onClick: clearFormatting });
699
667
  }
700
668
  // UI spec §5: indent/outdent arrows are direction-relative and swap in RTL
701
669
  // (outdent moves content toward the start edge, which is the right in RTL).
@@ -715,56 +683,6 @@ export function Toolbar({ config, dictionaryStores = [], thesaurusStores = [], o
715
683
  },
716
684
  ]
717
685
  : [];
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
686
  // Grouped for the dropdown when options declare groups (Latin / Urdu).
769
687
  const fontData = (() => {
770
688
  const data = [];
@@ -807,36 +725,26 @@ export function Toolbar({ config, dictionaryStores = [], thesaurusStores = [], o
807
725
  ];
808
726
  const formattingValue = state.listType ?? state.blockType;
809
727
  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")),
728
+ 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"));
729
+ 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"));
730
+ 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"));
731
+ const insertSection = showInsertGroup && (_jsxs(MantineToolbar.Group, { className: "likhari-toolbar-cluster", children: [config.links &&
732
+ (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: () => {
733
+ restoreSelection();
734
+ openLinkDialog();
735
+ }, 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"));
736
+ 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"));
737
+ 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"));
738
+ const startSections = [
739
+ showSave && (_jsx(MantineToolbar.Group, { className: "likhari-toolbar-cluster", children: _jsx(ToolbarButton, { icon: IconDeviceFloppy, title: strings.toolbar.save, dirty: Boolean(isDirty), onClick: onSave }) }, "save")),
740
+ 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")),
741
+ formatSection,
742
+ alignSection,
743
+ fontSection,
744
+ insertSection,
745
+ toolsSection,
834
746
  ];
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) => {
747
+ return (_jsxs(MantineToolbar, { "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), endSection, _jsxs(Menu, { opened: contextMenu !== null, onChange: (opened) => {
840
748
  if (!opened)
841
749
  setContextMenu(null);
842
750
  }, 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 +772,5 @@ export function Toolbar({ config, dictionaryStores = [], thesaurusStores = [], o
864
772
  }, children: synonym }, synonym))), _jsx(Menu.Divider, {})] })), contextMenu?.link !== undefined && (_jsxs(_Fragment, { children: [_jsx(LinkMenuItems, { url: contextMenu.link, onEdit: () => {
865
773
  restoreSelection();
866
774
  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 })] }));
775
+ }, 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
776
  }
@@ -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',
package/dist/index.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  export { EditorRoot } from './EditorRoot.js';
2
2
  export type { EditorRootProps, EditorRef, EditorInitialContent, NavigationGuardMode } from './EditorRoot.js';
3
+ export type { ToolbarStyle } from './components/Toolbar.js';
3
4
  export type { DraftRestoreMode } from './components/DraftRestore.js';
4
5
  export { defaultMantineTheme } from './theme/mantineTheme.js';
5
6
  export { ImageNode, $createImageNode, $isImageNode } from './image/ImageNode.js';
@@ -8,7 +9,7 @@ export { DEFAULT_FONT_OPTIONS, URDU_WEB_FONT_OPTIONS, FONT_SIZES_PX } from './fo
8
9
  export type { FontOption } from './fonts/index.js';
9
10
  export { STRINGS, getStrings, useStrings, useUiStrings, UiStringsContext } from './i18n/index.js';
10
11
  export type { Locale, Strings } from './i18n/index.js';
11
- export { registerSpellDictionary, hasSpellDictionary, type HunspellFiles, type SpellLanguage } from './spellcheck/spellDictionaries.js';
12
+ export { registerSpellDictionary, hasSpellDictionary, setEnglishDictionaryBaseUrl, type HunspellFiles, type SpellLanguage } from './spellcheck/spellDictionaries.js';
12
13
  export { localStorageAutoCorrectStore, apiAutoCorrectStore, fileAutoCorrectStore } from './autocorrect/autoCorrectStores.js';
13
14
  export type { AutoCorrectStore, AutoCorrectEntry } from './autocorrect/autoCorrectStores.js';
14
15
  export { normalizeUrdu, normalizeUrduCharacters, removeUrduDiacritics, replaceUrduDigits } from './normalization/urduNormalize.js';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAC1C,YAAY,EAAE,eAAe,EAAE,SAAS,EAAE,oBAAoB,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAC;AAC1G,YAAY,EAAE,gBAAgB,EAAE,MAAM,2BAA2B,CAAC;AAClE,OAAO,EAAE,mBAAmB,EAAE,MAAM,sBAAsB,CAAC;AAC3D,OAAO,EAAE,SAAS,EAAE,gBAAgB,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAC9E,YAAY,EAAE,YAAY,EAAE,aAAa,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AAC1F,OAAO,EAAE,oBAAoB,EAAE,qBAAqB,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AACrF,YAAY,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AAC1C,OAAO,EAAE,OAAO,EAAE,UAAU,EAAE,UAAU,EAAE,YAAY,EAAE,gBAAgB,EAAE,MAAM,QAAQ,CAAC;AACzF,YAAY,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AAE9C,OAAO,EAAE,uBAAuB,EAAE,kBAAkB,EAAE,KAAK,aAAa,EAAE,KAAK,aAAa,EAAE,MAAM,gCAAgC,CAAC;AACrI,OAAO,EAAE,4BAA4B,EAAE,mBAAmB,EAAE,oBAAoB,EAAE,MAAM,iCAAiC,CAAC;AAC1H,YAAY,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,MAAM,iCAAiC,CAAC;AAC1F,OAAO,EAAE,aAAa,EAAE,uBAAuB,EAAE,oBAAoB,EAAE,iBAAiB,EAAE,MAAM,+BAA+B,CAAC;AAChI,YAAY,EAAE,wBAAwB,EAAE,MAAM,+BAA+B,CAAC;AAC9E,YAAY,EAAE,kBAAkB,EAAE,eAAe,EAAE,MAAM,gCAAgC,CAAC;AAC1F,OAAO,EAAE,yBAAyB,EAAE,gBAAgB,EAAE,UAAU,EAAE,WAAW,EAAE,kBAAkB,EAAE,MAAM,wBAAwB,CAAC;AAClI,YAAY,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAC;AAC5D,OAAO,EAAE,yBAAyB,EAAE,kBAAkB,EAAE,MAAM,iCAAiC,CAAC;AAChG,YAAY,EAAE,eAAe,EAAE,MAAM,iCAAiC,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAC1C,YAAY,EAAE,eAAe,EAAE,SAAS,EAAE,oBAAoB,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAC;AAC1G,YAAY,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC;AACzD,YAAY,EAAE,gBAAgB,EAAE,MAAM,2BAA2B,CAAC;AAClE,OAAO,EAAE,mBAAmB,EAAE,MAAM,sBAAsB,CAAC;AAC3D,OAAO,EAAE,SAAS,EAAE,gBAAgB,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAC9E,YAAY,EAAE,YAAY,EAAE,aAAa,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AAC1F,OAAO,EAAE,oBAAoB,EAAE,qBAAqB,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AACrF,YAAY,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AAC1C,OAAO,EAAE,OAAO,EAAE,UAAU,EAAE,UAAU,EAAE,YAAY,EAAE,gBAAgB,EAAE,MAAM,QAAQ,CAAC;AACzF,YAAY,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AAE9C,OAAO,EAAE,uBAAuB,EAAE,kBAAkB,EAAE,2BAA2B,EAAE,KAAK,aAAa,EAAE,KAAK,aAAa,EAAE,MAAM,gCAAgC,CAAC;AAClK,OAAO,EAAE,4BAA4B,EAAE,mBAAmB,EAAE,oBAAoB,EAAE,MAAM,iCAAiC,CAAC;AAC1H,YAAY,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,MAAM,iCAAiC,CAAC;AAC1F,OAAO,EAAE,aAAa,EAAE,uBAAuB,EAAE,oBAAoB,EAAE,iBAAiB,EAAE,MAAM,+BAA+B,CAAC;AAChI,YAAY,EAAE,wBAAwB,EAAE,MAAM,+BAA+B,CAAC;AAC9E,YAAY,EAAE,kBAAkB,EAAE,eAAe,EAAE,MAAM,gCAAgC,CAAC;AAC1F,OAAO,EAAE,yBAAyB,EAAE,gBAAgB,EAAE,UAAU,EAAE,WAAW,EAAE,kBAAkB,EAAE,MAAM,wBAAwB,CAAC;AAClI,YAAY,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAC;AAC5D,OAAO,EAAE,yBAAyB,EAAE,kBAAkB,EAAE,MAAM,iCAAiC,CAAC;AAChG,YAAY,EAAE,eAAe,EAAE,MAAM,iCAAiC,CAAC"}
package/dist/index.js CHANGED
@@ -3,7 +3,7 @@ export { defaultMantineTheme } from './theme/mantineTheme.js';
3
3
  export { ImageNode, $createImageNode, $isImageNode } from './image/ImageNode.js';
4
4
  export { DEFAULT_FONT_OPTIONS, URDU_WEB_FONT_OPTIONS, FONT_SIZES_PX } from './fonts/index.js';
5
5
  export { STRINGS, getStrings, useStrings, useUiStrings, UiStringsContext } from './i18n/index.js';
6
- export { registerSpellDictionary, hasSpellDictionary } from './spellcheck/spellDictionaries.js';
6
+ export { registerSpellDictionary, hasSpellDictionary, setEnglishDictionaryBaseUrl } from './spellcheck/spellDictionaries.js';
7
7
  export { localStorageAutoCorrectStore, apiAutoCorrectStore, fileAutoCorrectStore } from './autocorrect/autoCorrectStores.js';
8
8
  export { normalizeUrdu, normalizeUrduCharacters, removeUrduDiacritics, replaceUrduDigits } from './normalization/urduNormalize.js';
9
9
  export { localStorageUserWordStore, apiUserWordStore, ignoreWord, addUserWord, onSpellWordsChange } from './spellcheck/userWords.js';
@@ -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
+ }
@@ -12,6 +12,8 @@ export interface Speller {
12
12
  suggest(word: string): string[];
13
13
  }
14
14
  type Loader = () => Promise<HunspellFiles>;
15
+ /** Sets where the English dictionary files are served from. Takes effect on the next load. */
16
+ export declare function setEnglishDictionaryBaseUrl(url: string): void;
15
17
  /**
16
18
  * Registers a dictionary for a language. Call once at startup with files you
17
19
  * have licence to ship — e.g. the Urdu `.aff`/`.dic` pair — either as strings
@@ -1 +1 @@
1
- {"version":3,"file":"spellDictionaries.d.ts","sourceRoot":"","sources":["../../src/spellcheck/spellDictionaries.ts"],"names":[],"mappings":"AAGA,4DAA4D;AAC5D,MAAM,MAAM,aAAa,GAAG,IAAI,GAAG,IAAI,GAAG,cAAc,CAAC;AAEzD,wFAAwF;AACxF,MAAM,WAAW,aAAa;IAC5B,uDAAuD;IACvD,GAAG,EAAE,MAAM,CAAC;IACZ,qEAAqE;IACrE,GAAG,EAAE,MAAM,CAAC;CACb;AAED,MAAM,WAAW,OAAO;IACtB,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC;IAC/B,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;CACjC;AAED,KAAK,MAAM,GAAG,MAAM,OAAO,CAAC,aAAa,CAAC,CAAC;AAoB3C;;;;;GAKG;AACH,wBAAgB,uBAAuB,CAAC,QAAQ,EAAE,aAAa,EAAE,KAAK,EAAE,aAAa,GAAG,MAAM,GAAG,IAAI,CAIpG;AAED,2DAA2D;AAC3D,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,aAAa,GAAG,OAAO,CAEnE;AAED;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,QAAQ,EAAE,aAAa,GAAG,OAAO,CAAC,OAAO,CAAC,GAAG,IAAI,CAc3E;AAYD,0FAA0F;AAC1F,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,EAAE,CAOrD;AAED,iFAAiF;AACjF,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,aAAa,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,GAAG,IAAI,CAGpF;AAED,gEAAgE;AAChE,MAAM,WAAW,QAAQ;IACvB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,MAAM,CAAC;CACb;AASD,wBAAgB,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,QAAQ,EAAE,CAMhD;AAED,0DAA0D;AAC1D,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,GAAG,QAAQ,EAAE,CAEzE"}
1
+ {"version":3,"file":"spellDictionaries.d.ts","sourceRoot":"","sources":["../../src/spellcheck/spellDictionaries.ts"],"names":[],"mappings":"AAGA,4DAA4D;AAC5D,MAAM,MAAM,aAAa,GAAG,IAAI,GAAG,IAAI,GAAG,cAAc,CAAC;AAEzD,wFAAwF;AACxF,MAAM,WAAW,aAAa;IAC5B,uDAAuD;IACvD,GAAG,EAAE,MAAM,CAAC;IACZ,qEAAqE;IACrE,GAAG,EAAE,MAAM,CAAC;CACb;AAED,MAAM,WAAW,OAAO;IACtB,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC;IAC/B,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;CACjC;AAED,KAAK,MAAM,GAAG,MAAM,OAAO,CAAC,aAAa,CAAC,CAAC;AAiB3C,8FAA8F;AAC9F,wBAAgB,2BAA2B,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAI7D;AAUD;;;;;GAKG;AACH,wBAAgB,uBAAuB,CAAC,QAAQ,EAAE,aAAa,EAAE,KAAK,EAAE,aAAa,GAAG,MAAM,GAAG,IAAI,CAIpG;AAED,2DAA2D;AAC3D,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,aAAa,GAAG,OAAO,CAEnE;AAED;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,QAAQ,EAAE,aAAa,GAAG,OAAO,CAAC,OAAO,CAAC,GAAG,IAAI,CAc3E;AAYD,0FAA0F;AAC1F,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,EAAE,CAOrD;AAED,iFAAiF;AACjF,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,aAAa,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,GAAG,IAAI,CAGpF;AAED,gEAAgE;AAChE,MAAM,WAAW,QAAQ;IACvB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,MAAM,CAAC;CACb;AASD,wBAAgB,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,QAAQ,EAAE,CAMhD;AAED,0DAA0D;AAC1D,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,GAAG,QAAQ,EAAE,CAEzE"}
@@ -9,10 +9,20 @@ async function fetchText(url) {
9
9
  throw new Error(`Spellcheck dictionary failed to load: ${url}`);
10
10
  return response.text();
11
11
  }
12
+ // Where the English dictionary files are. Built from this module's own URL, so the
13
+ // files shipped next to it are found; a host that bundles the editor can point this
14
+ // elsewhere with setEnglishDictionaryBaseUrl.
15
+ let englishBaseUrl = import.meta.url.replace(/[^/]*$/, 'dictionaries/en/');
16
+ /** Sets where the English dictionary files are served from. Takes effect on the next load. */
17
+ export function setEnglishDictionaryBaseUrl(url) {
18
+ englishBaseUrl = url.endsWith('/') ? url : `${url}/`;
19
+ loadedFiles.delete('en');
20
+ spellers.delete('en');
21
+ }
12
22
  /** English ships with the editor: its files are fetched on first use. */
13
23
  const englishLoader = async () => ({
14
- aff: await fetchText(new URL('./dictionaries/en/index.aff', import.meta.url).href),
15
- dic: await fetchText(new URL('./dictionaries/en/index.dic', import.meta.url).href),
24
+ aff: await fetchText(`${englishBaseUrl}index.aff`),
25
+ dic: await fetchText(`${englishBaseUrl}index.dic`),
16
26
  });
17
27
  loaders.set('en', englishLoader);
18
28
  /**
@@ -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,19 +19,12 @@
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;
31
25
  min-height: 46px;
32
26
  padding: 4px 8px;
33
27
  background: var(--editor-paper-dim);
34
- border-bottom: 1px solid var(--editor-line);
35
28
  /* Wrap instead of scroll: an overflow container would clip menus and grow
36
29
  a scrollbar. Dropdowns (Select/Menu) portal out, so nothing needs to
37
30
  escape this box. */
@@ -57,55 +50,6 @@
57
50
  pointer-events: auto;
58
51
  }
59
52
 
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
53
  /* Mantine Select inside the toolbar — sized to the 30px control height and
110
54
  themed from the --editor-* tokens (the closed input only; the dropdown is
111
55
  portaled to <body>, outside the token scope, so it uses Mantine's own
@@ -125,35 +69,44 @@
125
69
  cursor: not-allowed;
126
70
  }
127
71
 
128
- .likhari-overflow-item[data-active='true'] {
129
- background: var(--mantine-primary-color-light);
130
- color: var(--mantine-primary-color-filled);
72
+ /* A cluster of related buttons (undo/redo, bold/italic/underline/strikethrough, ...) reads as one control. */
73
+ .likhari-toolbar--plain .likhari-toolbar-cluster {
74
+ border: none;
75
+ background: transparent;
131
76
  }
132
77
 
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
- }
78
+ .likhari-toolbar--plain .likhari-toolbar-cluster [data-toolbar-toggle] + [data-toolbar-toggle] {
79
+ border-inline-start: none;
80
+ }
81
+
82
+ .likhari-toolbar-cluster {
83
+ border: 1px solid var(--editor-line-strong);
84
+ border-radius: 6px;
85
+ background: var(--editor-paper);
86
+ overflow: hidden;
87
+ }
88
+
89
+ .likhari-toolbar-cluster [data-toolbar-toggle] {
90
+ border: none;
91
+ border-radius: 0;
92
+ }
93
+
94
+ .likhari-toolbar-cluster [data-toolbar-toggle] + [data-toolbar-toggle] {
95
+ border-inline-start: 1px solid var(--editor-line);
142
96
  }
143
97
 
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'] {
98
+ /* Save: filled accent while there are unsaved changes, back to the plain outline once saved. */
99
+ .likhari-toolbar [data-dirty='true'] {
147
100
  background: var(--editor-accent);
148
101
  color: var(--editor-paper);
149
102
  }
150
103
 
151
- .likhari-toolbar-button[data-dirty='true']:hover {
104
+ .likhari-toolbar [data-dirty='true']:hover {
152
105
  background: var(--editor-accent);
153
106
  filter: brightness(1.1);
154
107
  }
155
108
 
156
- .likhari-toolbar-button[data-dirty='false'] {
109
+ .likhari-toolbar [data-dirty='false'] {
157
110
  color: var(--editor-accent);
158
111
  }
159
112
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inshapardaz/likhari-react",
3
- "version": "0.1.28",
3
+ "version": "0.1.29",
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
  }