@inshapardaz/likhari-react 0.1.2-dev.20 → 0.1.3

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.
@@ -25,18 +25,7 @@ export interface EditorRootProps {
25
25
  */
26
26
  height?: string | number;
27
27
  onChange?: (state: SerializedEditorState) => void;
28
- /**
29
- * Handler for the toolbar's Save button. Receives the serialized content
30
- * (plain text when that format is enabled, otherwise Lexical JSON).
31
- */
32
28
  onSave?: (content: string, format: FormatId) => void;
33
- /**
34
- * Whether the Save button (icon-only) is shown. Defaults to `true` when
35
- * `onSave` is provided and `false` otherwise, i.e. the previous behavior.
36
- * Set explicitly to show the button without a handler (saving still clears
37
- * the unsaved-changes state) or to hide it while keeping `onSave`.
38
- */
39
- showSave?: boolean;
40
29
  }
41
30
  export interface EditorRef {
42
31
  getContent(format: FormatId): string;
@@ -1 +1 @@
1
- {"version":3,"file":"EditorRoot.d.ts","sourceRoot":"","sources":["../src/EditorRoot.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,eAAe,CAAC;AAS1D,OAAO,KAAK,EAAe,qBAAqB,EAAE,MAAM,SAAS,CAAC;AAClE,OAAO,EAAyB,KAAK,QAAQ,EAAE,MAAM,iCAAiC,CAAC;AACvF,OAAO,EAAwB,KAAK,mBAAmB,EAAE,KAAK,uBAAuB,EAAE,MAAM,2BAA2B,CAAC;AAMzH,MAAM,WAAW,oBAAoB;IACnC,MAAM,EAAE,OAAO,CAAC,QAAQ,EAAE,cAAc,GAAG,YAAY,CAAC,CAAC;IACzD,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,eAAe;IAC9B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,cAAc,CAAC,EAAE,oBAAoB,CAAC;IACtC,aAAa,CAAC,EAAE,mBAAmB,CAAC;IACpC,aAAa,CAAC,EAAE,uBAAuB,CAAC;IACxC,KAAK,CAAC,EAAE,oBAAoB,CAAC;IAC7B,WAAW,CAAC,EAAE,OAAO,GAAG,MAAM,CAAC;IAC/B,MAAM,CAAC,EAAE,IAAI,GAAG,IAAI,GAAG,cAAc,CAAC;IACtC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;;;;;OAOG;IACH,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IACzB,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,qBAAqB,KAAK,IAAI,CAAC;IAClD;;;OAGG;IACH,MAAM,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,KAAK,IAAI,CAAC;IACrD;;;;;OAKG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED,MAAM,WAAW,SAAS;IACxB,UAAU,CAAC,MAAM,EAAE,QAAQ,GAAG,MAAM,CAAC;IACrC,UAAU,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,GAAG,IAAI,CAAC;IAClD,iBAAiB,IAAI,OAAO,CAAC;IAC7B,cAAc,IAAI,OAAO,CAAC,OAAO,CAAC,CAAC;IACnC,KAAK,IAAI,IAAI,CAAC;CACf;AAQD,eAAO,MAAM,UAAU,uGAqHrB,CAAC"}
1
+ {"version":3,"file":"EditorRoot.d.ts","sourceRoot":"","sources":["../src/EditorRoot.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,eAAe,CAAC;AAS1D,OAAO,KAAK,EAAe,qBAAqB,EAAE,MAAM,SAAS,CAAC;AAClE,OAAO,EAAyB,KAAK,QAAQ,EAAE,MAAM,iCAAiC,CAAC;AACvF,OAAO,EAAwB,KAAK,mBAAmB,EAAE,KAAK,uBAAuB,EAAE,MAAM,2BAA2B,CAAC;AAMzH,MAAM,WAAW,oBAAoB;IACnC,MAAM,EAAE,OAAO,CAAC,QAAQ,EAAE,cAAc,GAAG,YAAY,CAAC,CAAC;IACzD,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,eAAe;IAC9B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,cAAc,CAAC,EAAE,oBAAoB,CAAC;IACtC,aAAa,CAAC,EAAE,mBAAmB,CAAC;IACpC,aAAa,CAAC,EAAE,uBAAuB,CAAC;IACxC,KAAK,CAAC,EAAE,oBAAoB,CAAC;IAC7B,WAAW,CAAC,EAAE,OAAO,GAAG,MAAM,CAAC;IAC/B,MAAM,CAAC,EAAE,IAAI,GAAG,IAAI,GAAG,cAAc,CAAC;IACtC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;;;;;OAOG;IACH,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IACzB,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,qBAAqB,KAAK,IAAI,CAAC;IAClD,MAAM,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,KAAK,IAAI,CAAC;CACtD;AAED,MAAM,WAAW,SAAS;IACxB,UAAU,CAAC,MAAM,EAAE,QAAQ,GAAG,MAAM,CAAC;IACrC,UAAU,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,GAAG,IAAI,CAAC;IAClD,iBAAiB,IAAI,OAAO,CAAC;IAC7B,cAAc,IAAI,OAAO,CAAC,OAAO,CAAC,CAAC;IACnC,KAAK,IAAI,IAAI,CAAC;CACf;AAQD,eAAO,MAAM,UAAU,uGAoHrB,CAAC"}
@@ -20,7 +20,7 @@ function initialEditorStateJson(initialContent) {
20
20
  const state = defaultFormatRegistry.parse(initialContent.format, initialContent.value);
21
21
  return JSON.stringify(state);
22
22
  }
23
- export const EditorRoot = forwardRef(function EditorRoot({ documentId, initialContent, featureConfig, featurePreset, theme, colorScheme, locale = 'en', placeholder = 'Start writing…', height = '480px', onChange, onSave, showSave = Boolean(onSave), }, ref) {
23
+ export const EditorRoot = forwardRef(function EditorRoot({ documentId, initialContent, featureConfig, featurePreset, theme, colorScheme, locale = 'en', placeholder = 'Start writing…', height = '480px', onChange, onSave, }, ref) {
24
24
  const config = useMemo(() => resolveFeatureConfig(featureConfig, featurePreset), [featureConfig, featurePreset]);
25
25
  const editorStateRef = useRef(null);
26
26
  const lastSavedJsonRef = useRef(null);
@@ -84,5 +84,5 @@ export const EditorRoot = forwardRef(function EditorRoot({ documentId, initialCo
84
84
  onSave?.(content, format);
85
85
  };
86
86
  const dir = locale === 'en' ? 'ltr' : 'rtl';
87
- return (_jsx(EditorThemeProvider, { theme: theme, colorScheme: colorScheme, children: _jsx("div", { className: "likhari-root", dir: dir, ref: rootElementRef, "data-document-id": documentId, style: { height: typeof height === 'number' ? `${height}px` : height }, children: _jsxs(LexicalComposer, { initialConfig: initialConfig, children: [_jsx(Toolbar, { config: config, onSave: handleSave, isDirty: isDirty, showSave: showSave }), _jsx("div", { className: "likhari-canvas", children: _jsx(RichTextPlugin, { contentEditable: _jsx(ContentEditable, { className: "likhari-content-editable", dir: dir, "aria-label": "Editor content" }), placeholder: _jsx("div", { className: "likhari-placeholder", children: placeholder }), ErrorBoundary: LexicalErrorBoundary }) }), config.history && _jsx(HistoryPlugin, {}), (config.lists.bullet || config.lists.numbered || config.lists.check) && _jsx(ListPlugin, {}), config.links && _jsx(LinkPlugin, {}), _jsx(OnChangePlugin, { onChange: handleChange })] }) }) }));
87
+ return (_jsx(EditorThemeProvider, { theme: theme, colorScheme: colorScheme, children: _jsx("div", { className: "likhari-root", dir: dir, ref: rootElementRef, "data-document-id": documentId, style: { height: typeof height === 'number' ? `${height}px` : height }, children: _jsxs(LexicalComposer, { initialConfig: initialConfig, children: [_jsx(Toolbar, { config: config, onSave: onSave ? handleSave : undefined, isDirty: isDirty, showSave: Boolean(onSave) }), _jsx("div", { className: "likhari-canvas", children: _jsx(RichTextPlugin, { contentEditable: _jsx(ContentEditable, { className: "likhari-content-editable", dir: dir, "aria-label": "Editor content" }), placeholder: _jsx("div", { className: "likhari-placeholder", children: placeholder }), ErrorBoundary: LexicalErrorBoundary }) }), config.history && _jsx(HistoryPlugin, {}), (config.lists.bullet || config.lists.numbered || config.lists.check) && _jsx(ListPlugin, {}), config.links && _jsx(LinkPlugin, {}), _jsx(OnChangePlugin, { onChange: handleChange })] }) }) }));
88
88
  });
@@ -1 +1 @@
1
- {"version":3,"file":"Toolbar.d.ts","sourceRoot":"","sources":["../../src/components/Toolbar.tsx"],"names":[],"mappings":"AAkCA,OAAO,KAAK,EAAE,2BAA2B,EAAE,MAAM,2BAA2B,CAAC;AA4K7E,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE,2BAA2B,CAAC;IACpC,MAAM,CAAC,EAAE,MAAM,IAAI,CAAC;IACpB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED,wBAAgB,OAAO,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,QAAQ,EAAE,EAAE,YAAY,+BA4W1E"}
1
+ {"version":3,"file":"Toolbar.d.ts","sourceRoot":"","sources":["../../src/components/Toolbar.tsx"],"names":[],"mappings":"AA+BA,OAAO,KAAK,EAAE,2BAA2B,EAAE,MAAM,2BAA2B,CAAC;AAsH7E,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE,2BAA2B,CAAC;IACpC,MAAM,CAAC,EAAE,MAAM,IAAI,CAAC;IACpB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED,wBAAgB,OAAO,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,QAAQ,EAAE,EAAE,YAAY,+BAgY1E"}
@@ -1,8 +1,7 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import { useCallback, useEffect, useRef, useState } from 'react';
3
- import { Menu, Select } from '@mantine/core';
4
3
  import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
5
- import { $getSelection, $isRangeSelection, $isElementNode, $setSelection, 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';
4
+ import { $getSelection, $isRangeSelection, $isElementNode, 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
5
  import { $setBlocksType } from '@lexical/selection';
7
6
  import { $createParagraphNode } from 'lexical';
8
7
  import { $createHeadingNode, $createQuoteNode, $isHeadingNode, $isQuoteNode } from '@lexical/rich-text';
@@ -27,8 +26,8 @@ const ALIGN_ICONS = {
27
26
  right: IconAlignRight,
28
27
  justify: IconAlignJustified,
29
28
  };
30
- function ToolbarButton({ icon: Icon, title, active, dirty, disabled, onClick, }) {
31
- 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 }) }));
29
+ function ToolbarButton({ icon: Icon, title, active, disabled, onClick, }) {
30
+ return (_jsx("button", { type: "button", className: "likhari-toolbar-button", "data-active": active ? '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 }) }));
32
31
  }
33
32
  /** A disabled placeholder for a feature whose config flag is on but that
34
33
  * isn't implemented yet (link/image/poetry/font/language-tooling — see
@@ -37,21 +36,16 @@ function ToolbarButton({ icon: Icon, title, active, dirty, disabled, onClick, })
37
36
  function StubButton({ icon, title }) {
38
37
  return _jsx(ToolbarButton, { icon: icon, title: `${title} (coming soon)`, disabled: true, onClick: undefined });
39
38
  }
40
- /** Mantine Select (combobox) for the toolbar's dropdown controls. The icon
41
- * labels the control itself, since a dropdown can't show one per option in
42
- * its closed state. The dropdown portals to <body>, so it is not clipped by
43
- * the editor's `overflow: hidden` frame. */
44
- function ToolbarSelect({ icon: Icon, label, value, data, width, disabled, placeholder, onChange }) {
45
- return (_jsx(Select, { size: "xs", w: width, "aria-label": label, title: disabled ? `${label} (coming soon)` : label, data: data, value: value, placeholder: placeholder, disabled: disabled, allowDeselect: false, leftSection: _jsx(Icon, { size: 15, stroke: ICON_STROKE }), comboboxProps: { withinPortal: true, position: 'bottom-start', middlewares: { flip: true, shift: true } }, classNames: { input: 'likhari-toolbar-select-input', dropdown: 'likhari-toolbar-select-dropdown' }, onChange: (v) => v && onChange?.(v) }));
46
- }
47
- function OverflowItem({ icon: Icon, label, active, onClick, }) {
48
- return (_jsx(Menu.Item, { leftSection: _jsx(Icon, { size: ICON_SIZE, stroke: ICON_STROKE }), "data-active": active ? 'true' : 'false', className: "likhari-overflow-item",
49
- // Keep the editor's selection: a mousedown here would blur the canvas.
50
- onMouseDown: (e) => e.preventDefault(), onClick: onClick, children: label }));
39
+ /** Icon shown to the left of a <select> — native selects can't render icons
40
+ * per-option, so this labels the control itself instead. */
41
+ function SelectIcon({ icon: Icon }) {
42
+ return (_jsx("span", { className: "likhari-toolbar-select-icon", "aria-hidden": "true", children: _jsx(Icon, { size: 15, stroke: ICON_STROKE }) }));
51
43
  }
52
44
  export function Toolbar({ config, onSave, isDirty, showSave }) {
53
45
  const [editor] = useLexicalComposerContext();
54
46
  const [state, setState] = useState(INITIAL_STATE);
47
+ const [overflowOpen, setOverflowOpen] = useState(false);
48
+ const overflowRef = useRef(null);
55
49
  const updateToolbar = useCallback(() => {
56
50
  editor.getEditorState().read(() => {
57
51
  const selection = $getSelection();
@@ -105,6 +99,25 @@ export function Toolbar({ config, onSave, isDirty, showSave }) {
105
99
  return false;
106
100
  }, COMMAND_PRIORITY_LOW);
107
101
  }, [editor]);
102
+ // Close the overflow panel on outside click or Escape.
103
+ useEffect(() => {
104
+ if (!overflowOpen)
105
+ return;
106
+ const handlePointerDown = (e) => {
107
+ if (overflowRef.current && !overflowRef.current.contains(e.target))
108
+ setOverflowOpen(false);
109
+ };
110
+ const handleKeyDown = (e) => {
111
+ if (e.key === 'Escape')
112
+ setOverflowOpen(false);
113
+ };
114
+ document.addEventListener('mousedown', handlePointerDown);
115
+ document.addEventListener('keydown', handleKeyDown);
116
+ return () => {
117
+ document.removeEventListener('mousedown', handlePointerDown);
118
+ document.removeEventListener('keydown', handleKeyDown);
119
+ };
120
+ }, [overflowOpen]);
108
121
  const setBlockType = (type) => {
109
122
  editor.update(() => {
110
123
  const selection = $getSelection();
@@ -138,29 +151,6 @@ export function Toolbar({ config, onSave, isDirty, showSave }) {
138
151
  }
139
152
  setBlockType(value);
140
153
  };
141
- /** Runs a toolbar-select action, then returns focus to the canvas — the
142
- * combobox otherwise keeps it, so typing after picking would go nowhere. */
143
- // Opening the overflow Menu moves focus into its dropdown, which makes
144
- // Lexical drop the canvas selection — so snapshot it on open and restore it
145
- // before an item's action runs.
146
- const menuSelectionRef = useRef(null);
147
- const snapshotSelection = () => {
148
- editor.getEditorState().read(() => {
149
- const selection = $getSelection();
150
- menuSelectionRef.current = selection ? selection.clone() : null;
151
- });
152
- };
153
- const runOverflowAction = (action) => () => {
154
- const saved = menuSelectionRef.current;
155
- if (saved)
156
- editor.update(() => $setSelection(saved.clone()), { discrete: true });
157
- action();
158
- editor.focus();
159
- };
160
- const withRefocus = (fn) => (value) => {
161
- fn(value);
162
- editor.focus();
163
- };
164
154
  const formatText = (format) => editor.dispatchCommand(FORMAT_TEXT_COMMAND, format);
165
155
  const formatElement = (format) => editor.dispatchCommand(FORMAT_ELEMENT_COMMAND, format);
166
156
  const applyCaseTransform = (transform) => {
@@ -208,23 +198,7 @@ export function Toolbar({ config, onSave, isDirty, showSave }) {
208
198
  const showInsertPoetryGroup = config.links || config.images.linked || config.images.embedded || config.poetry.enabled;
209
199
  const showLanguageGroup = config.language.autocorrect || config.language.textCleanup || config.language.spellCheck;
210
200
  const showOverflowMenu = fmt.strikethrough || fmt.superscript || fmt.subscript || fmt.caseTransforms || fmt.clearFormatting || config.indent;
211
- const formattingOptions = [
212
- { value: 'paragraph', label: 'Paragraph' },
213
- ...headingLevels.map((level) => ({ value: `h${level}`, label: `Heading ${level}` })),
214
- ...(config.lists.numbered ? [{ value: 'number', label: 'Numbered list' }] : []),
215
- ...(config.lists.bullet ? [{ value: 'bullet', label: 'Bullet list' }] : []),
216
- ...(config.lists.check ? [{ value: 'check', label: 'Task list' }] : []),
217
- ...(config.blocks.quote ? [{ value: 'quote', label: 'Quote' }] : []),
218
- ];
219
- const alignOptions = [
220
- ...(config.alignment.start ? [{ value: 'start', label: 'Align start' }] : []),
221
- ...(config.alignment.center ? [{ value: 'center', label: 'Align center' }] : []),
222
- ...(config.alignment.start ? [{ value: 'end', label: 'Align end' }] : []),
223
- ...(config.alignment.justify ? [{ value: 'justify', label: 'Justify' }] : []),
224
- ...(config.alignment.left ? [{ value: 'left', label: 'Align left' }] : []),
225
- ...(config.alignment.right ? [{ value: 'right', label: 'Align right' }] : []),
226
- ];
227
201
  const formattingValue = state.listType ?? state.blockType;
228
202
  const AlignIcon = ALIGN_ICONS[state.elementFormat] ?? IconAlignLeft;
229
- return (_jsxs("div", { className: "likhari-toolbar", role: "toolbar", "aria-label": "Formatting", children: [showSave && (_jsx("div", { className: "likhari-toolbar-group", children: _jsx(ToolbarButton, { icon: IconDeviceFloppy, title: "Save", dirty: Boolean(isDirty), onClick: onSave }) })), config.history && (_jsxs("div", { className: "likhari-toolbar-group", children: [_jsx(ToolbarButton, { icon: IconArrowBackUp, title: "Undo", disabled: !state.canUndo, onClick: () => editor.dispatchCommand(UNDO_COMMAND, undefined) }), _jsx(ToolbarButton, { icon: IconArrowForwardUp, title: "Redo", disabled: !state.canRedo, onClick: () => editor.dispatchCommand(REDO_COMMAND, undefined) })] })), showFormattingGroup && (_jsx("div", { className: "likhari-toolbar-group", children: _jsx(ToolbarSelect, { icon: IconPilcrow, label: "Formatting", width: 148, value: formattingValue, data: formattingOptions, onChange: withRefocus((v) => applyFormatting(v)) }) })), showInlineGroup && (_jsxs("div", { className: "likhari-toolbar-group", children: [fmt.bold && (_jsx(ToolbarButton, { icon: IconBold, title: "Bold", active: state.activeFormats.has('bold'), onClick: () => formatText('bold') })), fmt.italic && (_jsx(ToolbarButton, { icon: IconItalic, title: "Italic", active: state.activeFormats.has('italic'), onClick: () => formatText('italic') })), fmt.underline && (_jsx(ToolbarButton, { icon: IconUnderline, title: "Underline", active: state.activeFormats.has('underline'), onClick: () => formatText('underline') }))] })), (config.font.family || config.font.size) && (_jsxs("div", { className: "likhari-toolbar-group likhari-toolbar-group--collapse-tablet", children: [config.font.family && (_jsx(ToolbarSelect, { icon: IconTypography, label: "Font family", width: 110, value: null, placeholder: "Font", data: [], disabled: true })), config.font.size && (_jsx(ToolbarSelect, { icon: IconTextSize, label: "Font size", width: 90, value: null, placeholder: "Size", data: [], disabled: true }))] })), showAlignGroup && (_jsx("div", { className: "likhari-toolbar-group", children: _jsx(ToolbarSelect, { icon: AlignIcon, label: "Alignment", width: 138, value: state.elementFormat || 'start', data: alignOptions, onChange: withRefocus((v) => formatElement(v)) }) })), showInsertPoetryGroup && (_jsxs("div", { className: "likhari-toolbar-group likhari-toolbar-group--collapse-tablet", children: [config.links && _jsx(StubButton, { icon: IconLink, title: "Insert link" }), (config.images.linked || config.images.embedded) && _jsx(StubButton, { icon: IconPhoto, title: "Insert image" }), config.poetry.enabled && _jsx(StubButton, { icon: IconFeather, title: "Poetry blocks" })] })), showLanguageGroup && (_jsxs("div", { className: "likhari-toolbar-group likhari-toolbar-group--collapse-tablet", children: [config.language.autocorrect && _jsx(StubButton, { icon: IconWand, title: "Auto-correct" }), config.language.textCleanup && _jsx(StubButton, { icon: IconSparkles, title: "Text cleanup" }), config.language.spellCheck && _jsx(StubButton, { icon: IconAbc, title: "Spell-checker" })] })), showOverflowMenu && (_jsxs(Menu, { position: "bottom-end", withinPortal: true, shadow: "sm", width: 210, closeOnItemClick: true, onOpen: snapshotSelection, children: [_jsx(Menu.Target, { children: _jsx("button", { type: "button", className: "likhari-toolbar-button", "aria-label": "More formatting", title: "More formatting", onMouseDown: (e) => e.preventDefault(), children: _jsx(IconDots, { size: ICON_SIZE, stroke: ICON_STROKE }) }) }), _jsxs(Menu.Dropdown, { children: [fmt.strikethrough && (_jsx(OverflowItem, { icon: IconStrikethrough, label: "Strikethrough", active: state.activeFormats.has('strikethrough'), onClick: runOverflowAction(() => formatText('strikethrough')) })), fmt.superscript && (_jsx(OverflowItem, { icon: IconSuperscript, label: "Superscript", active: state.activeFormats.has('superscript'), onClick: runOverflowAction(() => formatText('superscript')) })), fmt.subscript && (_jsx(OverflowItem, { icon: IconSubscript, label: "Subscript", active: state.activeFormats.has('subscript'), onClick: runOverflowAction(() => formatText('subscript')) })), fmt.caseTransforms && (_jsxs(_Fragment, { children: [_jsx(OverflowItem, { icon: IconLetterCaseUpper, label: "UPPERCASE", onClick: runOverflowAction(() => applyCaseTransform('upper')) }), _jsx(OverflowItem, { icon: IconLetterCaseLower, label: "lowercase", onClick: runOverflowAction(() => applyCaseTransform('lower')) }), _jsx(OverflowItem, { icon: IconLetterCase, label: "Capitalize", onClick: runOverflowAction(() => applyCaseTransform('capitalize')) })] })), fmt.clearFormatting && _jsx(OverflowItem, { icon: IconClearFormatting, label: "Clear formatting", onClick: runOverflowAction(clearFormatting) }), config.indent && (_jsxs(_Fragment, { children: [_jsx(OverflowItem, { icon: IconIndentDecrease, label: "Outdent", onClick: runOverflowAction(() => editor.dispatchCommand(OUTDENT_CONTENT_COMMAND, undefined)) }), _jsx(OverflowItem, { icon: IconIndentIncrease, label: "Indent", onClick: runOverflowAction(() => editor.dispatchCommand(INDENT_CONTENT_COMMAND, undefined)) })] }))] })] }))] }));
203
+ return (_jsxs("div", { className: "likhari-toolbar", role: "toolbar", "aria-label": "Formatting", children: [showSave && (_jsxs("button", { type: "button", className: "likhari-save-button", "data-dirty": isDirty ? 'true' : 'false', onClick: onSave, children: [_jsx(IconDeviceFloppy, { size: 16, stroke: ICON_STROKE }), "Save"] })), config.history && (_jsxs("div", { className: "likhari-toolbar-group", children: [_jsx(ToolbarButton, { icon: IconArrowBackUp, title: "Undo", disabled: !state.canUndo, onClick: () => editor.dispatchCommand(UNDO_COMMAND, undefined) }), _jsx(ToolbarButton, { icon: IconArrowForwardUp, title: "Redo", disabled: !state.canRedo, onClick: () => editor.dispatchCommand(REDO_COMMAND, undefined) })] })), showFormattingGroup && (_jsxs("div", { className: "likhari-toolbar-group likhari-toolbar-select-group", children: [_jsx(SelectIcon, { icon: IconPilcrow }), _jsxs("select", { className: "likhari-toolbar-select", "aria-label": "Formatting", value: formattingValue, onChange: (e) => applyFormatting(e.target.value), children: [_jsx("option", { value: "paragraph", children: "Paragraph" }), headingLevels.map((level) => (_jsxs("option", { value: `h${level}`, children: ["Heading ", level] }, level))), config.lists.numbered && _jsx("option", { value: "number", children: "Numbered list" }), config.lists.bullet && _jsx("option", { value: "bullet", children: "Bullet list" }), config.lists.check && _jsx("option", { value: "check", children: "Task list" }), config.blocks.quote && _jsx("option", { value: "quote", children: "Quote" })] })] })), showInlineGroup && (_jsxs("div", { className: "likhari-toolbar-group", children: [fmt.bold && (_jsx(ToolbarButton, { icon: IconBold, title: "Bold", active: state.activeFormats.has('bold'), onClick: () => formatText('bold') })), fmt.italic && (_jsx(ToolbarButton, { icon: IconItalic, title: "Italic", active: state.activeFormats.has('italic'), onClick: () => formatText('italic') })), fmt.underline && (_jsx(ToolbarButton, { icon: IconUnderline, title: "Underline", active: state.activeFormats.has('underline'), onClick: () => formatText('underline') }))] })), (config.font.family || config.font.size) && (_jsxs("div", { className: "likhari-toolbar-group likhari-toolbar-group--collapse-tablet likhari-toolbar-select-group", children: [config.font.family && (_jsxs(_Fragment, { children: [_jsx(SelectIcon, { icon: IconTypography }), _jsx("select", { className: "likhari-toolbar-select", "aria-label": "Font family", disabled: true, title: "Font family (coming soon)", children: _jsx("option", { children: "Font" }) })] })), config.font.size && (_jsxs(_Fragment, { children: [_jsx(SelectIcon, { icon: IconTextSize }), _jsx("select", { className: "likhari-toolbar-select", "aria-label": "Font size", disabled: true, title: "Font size (coming soon)", children: _jsx("option", { children: "Size" }) })] }))] })), showAlignGroup && (_jsxs("div", { className: "likhari-toolbar-group likhari-toolbar-select-group", children: [_jsx(SelectIcon, { icon: AlignIcon }), _jsxs("select", { className: "likhari-toolbar-select", "aria-label": "Alignment", value: state.elementFormat || 'start', onChange: (e) => formatElement(e.target.value), children: [config.alignment.start && _jsx("option", { value: "start", children: "Align start" }), config.alignment.center && _jsx("option", { value: "center", children: "Align center" }), config.alignment.start && _jsx("option", { value: "end", children: "Align end" }), config.alignment.justify && _jsx("option", { value: "justify", children: "Justify" }), config.alignment.left && _jsx("option", { value: "left", children: "Align left" }), config.alignment.right && _jsx("option", { value: "right", children: "Align right" })] })] })), showInsertPoetryGroup && (_jsxs("div", { className: "likhari-toolbar-group likhari-toolbar-group--collapse-tablet", children: [config.links && _jsx(StubButton, { icon: IconLink, title: "Insert link" }), (config.images.linked || config.images.embedded) && _jsx(StubButton, { icon: IconPhoto, title: "Insert image" }), config.poetry.enabled && _jsx(StubButton, { icon: IconFeather, title: "Poetry blocks" })] })), showLanguageGroup && (_jsxs("div", { className: "likhari-toolbar-group likhari-toolbar-group--collapse-tablet", children: [config.language.autocorrect && _jsx(StubButton, { icon: IconWand, title: "Auto-correct" }), config.language.textCleanup && _jsx(StubButton, { icon: IconSparkles, title: "Text cleanup" }), config.language.spellCheck && _jsx(StubButton, { icon: IconAbc, title: "Spell-checker" })] })), showOverflowMenu && (_jsxs("div", { className: "likhari-toolbar-overflow", ref: overflowRef, children: [_jsx(ToolbarButton, { icon: IconDots, title: "More formatting", active: overflowOpen, onClick: () => setOverflowOpen((o) => !o) }), overflowOpen && (_jsxs("div", { className: "likhari-toolbar-overflow-panel", role: "menu", children: [fmt.strikethrough && (_jsx(ToolbarButton, { icon: IconStrikethrough, title: "Strikethrough", active: state.activeFormats.has('strikethrough'), onClick: () => formatText('strikethrough') })), fmt.superscript && (_jsx(ToolbarButton, { icon: IconSuperscript, title: "Superscript", active: state.activeFormats.has('superscript'), onClick: () => formatText('superscript') })), fmt.subscript && (_jsx(ToolbarButton, { icon: IconSubscript, title: "Subscript", active: state.activeFormats.has('subscript'), onClick: () => formatText('subscript') })), fmt.caseTransforms && (_jsxs(_Fragment, { children: [_jsx(ToolbarButton, { icon: IconLetterCaseUpper, title: "UPPERCASE", onClick: () => applyCaseTransform('upper') }), _jsx(ToolbarButton, { icon: IconLetterCaseLower, title: "lowercase", onClick: () => applyCaseTransform('lower') }), _jsx(ToolbarButton, { icon: IconLetterCase, title: "Capitalize", onClick: () => applyCaseTransform('capitalize') })] })), fmt.clearFormatting && _jsx(ToolbarButton, { icon: IconClearFormatting, title: "Clear formatting", onClick: clearFormatting }), config.indent && (_jsxs(_Fragment, { children: [_jsx(ToolbarButton, { icon: IconIndentDecrease, title: "Outdent", onClick: () => editor.dispatchCommand(OUTDENT_CONTENT_COMMAND, undefined) }), _jsx(ToolbarButton, { icon: IconIndentIncrease, title: "Indent", onClick: () => editor.dispatchCommand(INDENT_CONTENT_COMMAND, undefined) })] }))] }))] }))] }));
230
204
  }
@@ -23,14 +23,11 @@
23
23
  align-items: center;
24
24
  gap: 8px;
25
25
  min-height: 46px;
26
- padding: 4px 8px;
26
+ padding: 0 8px;
27
27
  background: var(--editor-paper-dim);
28
28
  border-bottom: 1px solid var(--editor-line);
29
- /* Wrap instead of scroll: an overflow container would clip menus and grow
30
- a scrollbar. Dropdowns (Select/Menu) portal out, so nothing needs to
31
- escape this box. */
32
- flex-wrap: wrap;
33
- row-gap: 4px;
29
+ flex-wrap: nowrap;
30
+ overflow-x: auto;
34
31
  }
35
32
 
36
33
  .likhari-toolbar-group {
@@ -38,11 +35,11 @@
38
35
  align-items: center;
39
36
  gap: 2px;
40
37
  padding: 0 6px;
41
- border-inline-end: 0.5px solid var(--editor-line);
38
+ border-right: 0.5px solid var(--editor-line);
42
39
  }
43
40
 
44
41
  .likhari-toolbar-group:last-child {
45
- border-inline-end: none;
42
+ border-right: none;
46
43
  }
47
44
 
48
45
  .likhari-toolbar-button {
@@ -82,54 +79,86 @@
82
79
  cursor: not-allowed;
83
80
  }
84
81
 
85
- /* Mantine Select inside the toolbar — sized to the 30px control height and
86
- themed from the --editor-* tokens (the closed input only; the dropdown is
87
- portaled to <body>, outside the token scope, so it uses Mantine's own
88
- variables, which the editor's MantineProvider already maps to the accent). */
89
- .likhari-toolbar-select-input {
82
+ .likhari-toolbar-select {
90
83
  height: 30px;
91
- min-height: 30px;
84
+ max-width: 116px;
92
85
  font-size: 12px;
93
86
  border-radius: 6px;
94
87
  border: 1px solid var(--editor-line-strong);
95
88
  background: var(--editor-paper);
96
89
  color: var(--editor-ink);
90
+ padding: 0 6px;
97
91
  }
98
92
 
99
- .likhari-toolbar-select-input:disabled {
93
+ .likhari-toolbar-select:disabled {
100
94
  opacity: 0.4;
101
95
  cursor: not-allowed;
102
96
  }
103
97
 
104
- .likhari-overflow-item[data-active='true'] {
105
- background: var(--mantine-primary-color-light);
106
- color: var(--mantine-primary-color-filled);
98
+ /* Icon shown to the left of a <select> — native selects can't render an
99
+ icon per-option, so the control itself is labeled this way instead. */
100
+ .likhari-toolbar-select-group {
101
+ gap: 4px;
102
+ }
103
+
104
+ .likhari-toolbar-select-icon {
105
+ display: inline-flex;
106
+ align-items: center;
107
+ color: var(--editor-ink-soft);
108
+ }
109
+
110
+ /* Overflow menu ("⋯") — houses less-frequent formatting (strikethrough,
111
+ super/subscript, case transforms, clear formatting, indent/outdent) so
112
+ the primary row stays compact on small/mobile viewports. */
113
+ .likhari-toolbar-overflow {
114
+ position: relative;
115
+ display: flex;
116
+ align-items: center;
117
+ }
118
+
119
+ .likhari-toolbar-overflow-panel {
120
+ position: absolute;
121
+ top: calc(100% + 4px);
122
+ inset-inline-end: 0;
123
+ z-index: 20;
124
+ display: flex;
125
+ flex-wrap: wrap;
126
+ gap: 2px;
127
+ width: 168px;
128
+ padding: 6px;
129
+ background: var(--editor-paper);
130
+ border: 0.5px solid var(--editor-line);
131
+ border-radius: 8px;
107
132
  }
108
133
 
109
134
  /* Responsive collapse — editor-ui-design-spec.md §3.3. These groups hold
110
135
  only not-yet-implemented placeholders, so hiding them below the tablet
111
136
  breakpoint costs nothing functional while keeping the primary row usable
112
- on small screens; the toolbar wraps onto a second row if the
113
- still-visible controls alone overflow a narrow viewport. */
137
+ on small screens; the toolbar's horizontal scroll (below) remains the
138
+ fallback if the still-visible controls alone overflow a narrow viewport. */
114
139
  @media (max-width: 900px) {
115
140
  .likhari-toolbar-group--collapse-tablet {
116
141
  display: none;
117
142
  }
118
143
  }
119
144
 
120
- /* Save (icon-only ToolbarButton): filled accent while there are unsaved
121
- changes, back to the plain outline once saved — editor-ui-design-spec.md §7. */
122
- .likhari-toolbar-button[data-dirty='true'] {
145
+ .likhari-save-button {
146
+ display: inline-flex;
147
+ align-items: center;
148
+ gap: 6px;
149
+ height: 30px;
150
+ padding: 0 14px;
151
+ border-radius: 6px;
152
+ border: 1px solid var(--editor-accent);
123
153
  background: var(--editor-accent);
124
154
  color: var(--editor-paper);
155
+ font-size: 12px;
156
+ font-weight: 500;
157
+ cursor: pointer;
125
158
  }
126
159
 
127
- .likhari-toolbar-button[data-dirty='true']:hover {
128
- background: var(--editor-accent);
129
- filter: brightness(1.1);
130
- }
131
-
132
- .likhari-toolbar-button[data-dirty='false'] {
160
+ .likhari-save-button[data-dirty='false'] {
161
+ background: transparent;
133
162
  color: var(--editor-accent);
134
163
  }
135
164
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inshapardaz/likhari-react",
3
- "version": "0.1.2-dev.20",
3
+ "version": "0.1.3",
4
4
  "description": "React implementation of the Likhari editor, built on @lexical/react.",
5
5
  "license": "MIT",
6
6
  "repository": {