@inshapardaz/likhari-react 0.1.3 → 0.1.4-dev.25
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/EditorRoot.d.ts +20 -0
- package/dist/EditorRoot.d.ts.map +1 -1
- package/dist/EditorRoot.js +10 -3
- package/dist/components/Toolbar.d.ts +4 -1
- package/dist/components/Toolbar.d.ts.map +1 -1
- package/dist/components/Toolbar.js +103 -31
- package/dist/fonts/index.d.ts +7 -0
- package/dist/fonts/index.d.ts.map +1 -0
- package/dist/fonts/index.js +11 -0
- package/dist/fonts/urduWebFonts.d.ts +14 -0
- package/dist/fonts/urduWebFonts.d.ts.map +1 -0
- package/dist/fonts/urduWebFonts.js +66 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/theme/editor.css +29 -58
- package/dist/utils/style.d.ts +4 -0
- package/dist/utils/style.d.ts.map +1 -0
- package/dist/utils/style.js +15 -0
- package/package.json +1 -1
package/dist/EditorRoot.d.ts
CHANGED
|
@@ -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 FontOption } from './fonts/index.js';
|
|
5
6
|
export interface EditorInitialContent {
|
|
6
7
|
format: Extract<FormatId, 'lexical-json' | 'plain-text'>;
|
|
7
8
|
value: string;
|
|
@@ -25,7 +26,26 @@ export interface EditorRootProps {
|
|
|
25
26
|
*/
|
|
26
27
|
height?: string | number;
|
|
27
28
|
onChange?: (state: SerializedEditorState) => void;
|
|
29
|
+
/**
|
|
30
|
+
* Handler for the toolbar's Save button. Receives the serialized content
|
|
31
|
+
* (plain text when that format is enabled, otherwise Lexical JSON).
|
|
32
|
+
*/
|
|
28
33
|
onSave?: (content: string, format: FormatId) => void;
|
|
34
|
+
/**
|
|
35
|
+
* Whether the Save button (icon-only) is shown. Defaults to `true` when
|
|
36
|
+
* `onSave` is provided and `false` otherwise, i.e. the previous behavior.
|
|
37
|
+
* Set explicitly to show the button without a handler (saving still clears
|
|
38
|
+
* the unsaved-changes state) or to hide it while keeping `onSave`.
|
|
39
|
+
*/
|
|
40
|
+
showSave?: boolean;
|
|
41
|
+
/**
|
|
42
|
+
* Entries for the toolbar's font-family dropdown. Defaults to
|
|
43
|
+
* `DEFAULT_FONT_OPTIONS` (generic Latin faces plus the Urdu/Arabic-script
|
|
44
|
+
* collection from inshapardaz/urdu-web-fonts, the same source qari uses).
|
|
45
|
+
* Spread the defaults to add your own, e.g.
|
|
46
|
+
* `[...DEFAULT_FONT_OPTIONS, { name: 'Mine', family: '"Mine", serif' }]`.
|
|
47
|
+
*/
|
|
48
|
+
fontOptions?: FontOption[];
|
|
29
49
|
}
|
|
30
50
|
export interface EditorRef {
|
|
31
51
|
getContent(format: FormatId): string;
|
package/dist/EditorRoot.d.ts.map
CHANGED
|
@@ -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;
|
|
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;AAKzH,OAAO,EAAyB,KAAK,UAAU,EAAE,MAAM,SAAS,CAAC;AAEjE,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;IACnB;;;;;;OAMG;IACH,WAAW,CAAC,EAAE,UAAU,EAAE,CAAC;CAC5B;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,uGA4HrB,CAAC"}
|
package/dist/EditorRoot.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { forwardRef, useImperativeHandle, useMemo, useRef, useState } from 'react';
|
|
2
|
+
import { forwardRef, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react';
|
|
3
3
|
import { LexicalComposer } from '@lexical/react/LexicalComposer';
|
|
4
4
|
import { RichTextPlugin } from '@lexical/react/LexicalRichTextPlugin';
|
|
5
5
|
import { ContentEditable } from '@lexical/react/LexicalContentEditable';
|
|
@@ -14,14 +14,21 @@ import { EDITOR_NODES } from './nodes.js';
|
|
|
14
14
|
import { editorTheme } from './theme/editorTheme.js';
|
|
15
15
|
import { EditorThemeProvider } from './theme/EditorThemeProvider.js';
|
|
16
16
|
import { Toolbar } from './components/Toolbar.js';
|
|
17
|
+
import { injectUrduWebFontsCss } from './fonts/index.js';
|
|
17
18
|
function initialEditorStateJson(initialContent) {
|
|
18
19
|
if (!initialContent)
|
|
19
20
|
return undefined;
|
|
20
21
|
const state = defaultFormatRegistry.parse(initialContent.format, initialContent.value);
|
|
21
22
|
return JSON.stringify(state);
|
|
22
23
|
}
|
|
23
|
-
export const EditorRoot = forwardRef(function EditorRoot({ documentId, initialContent, featureConfig, featurePreset, theme, colorScheme, locale = 'en', placeholder = 'Start writing…', height = '480px', onChange, onSave, }, ref) {
|
|
24
|
+
export const EditorRoot = forwardRef(function EditorRoot({ documentId, initialContent, featureConfig, featurePreset, theme, colorScheme, locale = 'en', placeholder = 'Start writing…', height = '480px', onChange, onSave, showSave = Boolean(onSave), fontOptions, }, ref) {
|
|
24
25
|
const config = useMemo(() => resolveFeatureConfig(featureConfig, featurePreset), [featureConfig, featurePreset]);
|
|
26
|
+
// The urdu-web-fonts stylesheets are needed for the font dropdown and for
|
|
27
|
+
// RTL content, whose canvas font (editor.css) is one of those families.
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
if (config.font.family || locale !== 'en')
|
|
30
|
+
injectUrduWebFontsCss();
|
|
31
|
+
}, [config.font.family, locale]);
|
|
25
32
|
const editorStateRef = useRef(null);
|
|
26
33
|
const lastSavedJsonRef = useRef(null);
|
|
27
34
|
const [isDirty, setIsDirty] = useState(false);
|
|
@@ -84,5 +91,5 @@ export const EditorRoot = forwardRef(function EditorRoot({ documentId, initialCo
|
|
|
84
91
|
onSave?.(content, format);
|
|
85
92
|
};
|
|
86
93
|
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:
|
|
94
|
+
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, fontOptions: fontOptions }), _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
95
|
});
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import type { ResolvedEditorFeatureConfig } from '@inshapardaz/likhari-core';
|
|
2
|
+
import { type FontOption } from '../fonts/index.js';
|
|
2
3
|
export interface ToolbarProps {
|
|
3
4
|
config: ResolvedEditorFeatureConfig;
|
|
4
5
|
onSave?: () => void;
|
|
5
6
|
isDirty?: boolean;
|
|
6
7
|
showSave?: boolean;
|
|
8
|
+
/** Font-family dropdown entries; defaults to DEFAULT_FONT_OPTIONS. */
|
|
9
|
+
fontOptions?: FontOption[];
|
|
7
10
|
}
|
|
8
|
-
export declare function Toolbar({ config, onSave, isDirty, showSave }: ToolbarProps): import("react").JSX.Element;
|
|
11
|
+
export declare function Toolbar({ config, onSave, isDirty, showSave, fontOptions }: ToolbarProps): import("react").JSX.Element;
|
|
9
12
|
//# sourceMappingURL=Toolbar.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Toolbar.d.ts","sourceRoot":"","sources":["../../src/components/Toolbar.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"Toolbar.d.ts","sourceRoot":"","sources":["../../src/components/Toolbar.tsx"],"names":[],"mappings":"AAmCA,OAAO,KAAK,EAAE,2BAA2B,EAAE,MAAM,2BAA2B,CAAC;AAC7E,OAAO,EAAuC,KAAK,UAAU,EAAE,MAAM,UAAU,CAAC;AAuLhF,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;IACnB,sEAAsE;IACtE,WAAW,CAAC,EAAE,UAAU,EAAE,CAAC;CAC5B;AAED,wBAAgB,OAAO,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,QAAQ,EAAE,WAAkC,EAAE,EAAE,YAAY,+BAwa9G"}
|
|
@@ -1,18 +1,23 @@
|
|
|
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';
|
|
3
4
|
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
|
|
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';
|
|
5
|
-
import { $setBlocksType } from '@lexical/selection';
|
|
5
|
+
import { $getSelection, $isRangeSelection, $isElementNode, $setSelection, $getRoot, 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
|
+
import { $getSelectionStyleValueForProperty, $patchStyleText, $setBlocksType } from '@lexical/selection';
|
|
6
7
|
import { $createParagraphNode } from 'lexical';
|
|
7
8
|
import { $createHeadingNode, $createQuoteNode, $isHeadingNode, $isQuoteNode } from '@lexical/rich-text';
|
|
8
9
|
import { $isListNode, INSERT_CHECK_LIST_COMMAND, INSERT_ORDERED_LIST_COMMAND, INSERT_UNORDERED_LIST_COMMAND, REMOVE_LIST_COMMAND, } from '@lexical/list';
|
|
9
10
|
import { $findMatchingParent } from '@lexical/utils';
|
|
11
|
+
import { DEFAULT_FONT_OPTIONS, FONT_SIZES_PX } from '../fonts/index.js';
|
|
12
|
+
import { setStyleProperty } from '../utils/style.js';
|
|
10
13
|
import { IconAbc, IconAlignCenter, IconAlignJustified, IconAlignLeft, IconAlignRight, IconArrowBackUp, IconArrowForwardUp, IconBold, IconClearFormatting, IconDeviceFloppy, IconDots, IconFeather, IconIndentDecrease, IconIndentIncrease, IconItalic, IconLetterCase, IconLetterCaseLower, IconLetterCaseUpper, IconLink, IconPhoto, IconPilcrow, IconSparkles, IconStrikethrough, IconSubscript, IconSuperscript, IconTextSize, IconTypography, IconUnderline, IconWand, } from '@tabler/icons-react';
|
|
11
14
|
const INITIAL_STATE = {
|
|
12
15
|
blockType: 'paragraph',
|
|
13
16
|
activeFormats: new Set(),
|
|
14
17
|
elementFormat: 'start',
|
|
15
18
|
listType: null,
|
|
19
|
+
fontFamily: '',
|
|
20
|
+
fontSize: '',
|
|
16
21
|
canUndo: false,
|
|
17
22
|
canRedo: false,
|
|
18
23
|
};
|
|
@@ -26,8 +31,8 @@ const ALIGN_ICONS = {
|
|
|
26
31
|
right: IconAlignRight,
|
|
27
32
|
justify: IconAlignJustified,
|
|
28
33
|
};
|
|
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 }) }));
|
|
34
|
+
function ToolbarButton({ icon: Icon, title, active, dirty, disabled, onClick, }) {
|
|
35
|
+
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 }) }));
|
|
31
36
|
}
|
|
32
37
|
/** A disabled placeholder for a feature whose config flag is on but that
|
|
33
38
|
* isn't implemented yet (link/image/poetry/font/language-tooling — see
|
|
@@ -36,16 +41,21 @@ function ToolbarButton({ icon: Icon, title, active, disabled, onClick, }) {
|
|
|
36
41
|
function StubButton({ icon, title }) {
|
|
37
42
|
return _jsx(ToolbarButton, { icon: icon, title: `${title} (coming soon)`, disabled: true, onClick: undefined });
|
|
38
43
|
}
|
|
39
|
-
/**
|
|
40
|
-
*
|
|
41
|
-
|
|
42
|
-
|
|
44
|
+
/** Mantine Select (combobox) for the toolbar's dropdown controls. The icon
|
|
45
|
+
* labels the control itself, since a dropdown can't show one per option in
|
|
46
|
+
* its closed state. The dropdown portals to <body>, so it is not clipped by
|
|
47
|
+
* the editor's `overflow: hidden` frame. */
|
|
48
|
+
function ToolbarSelect({ icon: Icon, label, value, data, width, disabled, placeholder, searchable, renderOption, onChange }) {
|
|
49
|
+
return (_jsx(Select, { size: "xs", w: width, "aria-label": label, title: disabled ? `${label} (coming soon)` : label, data: data, value: value, placeholder: placeholder, disabled: disabled, searchable: searchable, renderOption: renderOption, nothingFoundMessage: searchable ? 'No match' : undefined, 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) }));
|
|
43
50
|
}
|
|
44
|
-
|
|
51
|
+
function OverflowItem({ icon: Icon, label, active, onClick, }) {
|
|
52
|
+
return (_jsx(Menu.Item, { leftSection: _jsx(Icon, { size: ICON_SIZE, stroke: ICON_STROKE }), "data-active": active ? 'true' : 'false', className: "likhari-overflow-item",
|
|
53
|
+
// Keep the editor's selection: a mousedown here would blur the canvas.
|
|
54
|
+
onMouseDown: (e) => e.preventDefault(), onClick: onClick, children: label }));
|
|
55
|
+
}
|
|
56
|
+
export function Toolbar({ config, onSave, isDirty, showSave, fontOptions = DEFAULT_FONT_OPTIONS }) {
|
|
45
57
|
const [editor] = useLexicalComposerContext();
|
|
46
58
|
const [state, setState] = useState(INITIAL_STATE);
|
|
47
|
-
const [overflowOpen, setOverflowOpen] = useState(false);
|
|
48
|
-
const overflowRef = useRef(null);
|
|
49
59
|
const updateToolbar = useCallback(() => {
|
|
50
60
|
editor.getEditorState().read(() => {
|
|
51
61
|
const selection = $getSelection();
|
|
@@ -62,6 +72,8 @@ export function Toolbar({ config, onSave, isDirty, showSave }) {
|
|
|
62
72
|
else if ($isQuoteNode(element)) {
|
|
63
73
|
blockType = 'quote';
|
|
64
74
|
}
|
|
75
|
+
const fontFamily = $getSelectionStyleValueForProperty(selection, 'font-family', '');
|
|
76
|
+
const fontSize = $getSelectionStyleValueForProperty(selection, 'font-size', '');
|
|
65
77
|
const activeFormats = new Set();
|
|
66
78
|
['bold', 'italic', 'underline', 'strikethrough', 'subscript', 'superscript'].forEach((format) => {
|
|
67
79
|
if (selection.hasFormat(format))
|
|
@@ -78,6 +90,8 @@ export function Toolbar({ config, onSave, isDirty, showSave }) {
|
|
|
78
90
|
activeFormats,
|
|
79
91
|
elementFormat,
|
|
80
92
|
listType,
|
|
93
|
+
fontFamily,
|
|
94
|
+
fontSize,
|
|
81
95
|
}));
|
|
82
96
|
});
|
|
83
97
|
}, [editor]);
|
|
@@ -99,25 +113,6 @@ export function Toolbar({ config, onSave, isDirty, showSave }) {
|
|
|
99
113
|
return false;
|
|
100
114
|
}, COMMAND_PRIORITY_LOW);
|
|
101
115
|
}, [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]);
|
|
121
116
|
const setBlockType = (type) => {
|
|
122
117
|
editor.update(() => {
|
|
123
118
|
const selection = $getSelection();
|
|
@@ -151,6 +146,47 @@ export function Toolbar({ config, onSave, isDirty, showSave }) {
|
|
|
151
146
|
}
|
|
152
147
|
setBlockType(value);
|
|
153
148
|
};
|
|
149
|
+
/** Runs a toolbar-select action, then returns focus to the canvas — the
|
|
150
|
+
* combobox otherwise keeps it, so typing after picking would go nowhere. */
|
|
151
|
+
// Opening the overflow Menu moves focus into its dropdown, which makes
|
|
152
|
+
// Lexical drop the canvas selection — so snapshot it on open and restore it
|
|
153
|
+
// before an item's action runs.
|
|
154
|
+
const menuSelectionRef = useRef(null);
|
|
155
|
+
const snapshotSelection = () => {
|
|
156
|
+
editor.getEditorState().read(() => {
|
|
157
|
+
const selection = $getSelection();
|
|
158
|
+
menuSelectionRef.current = selection ? selection.clone() : null;
|
|
159
|
+
});
|
|
160
|
+
};
|
|
161
|
+
const runOverflowAction = (action) => () => {
|
|
162
|
+
const saved = menuSelectionRef.current;
|
|
163
|
+
if (saved)
|
|
164
|
+
editor.update(() => $setSelection(saved.clone()), { discrete: true });
|
|
165
|
+
action();
|
|
166
|
+
editor.focus();
|
|
167
|
+
};
|
|
168
|
+
/** Applies a font property per `config.font.scope`: to the selection, to
|
|
169
|
+
* every text node in the document, or (for 'both') to the selection when
|
|
170
|
+
* there is a range and to the whole document when there isn't. */
|
|
171
|
+
const applyFont = (property, value) => {
|
|
172
|
+
editor.update(() => {
|
|
173
|
+
const selection = $getSelection();
|
|
174
|
+
const scope = config.font.scope;
|
|
175
|
+
const wholeDocument = scope === 'document' || (scope === 'both' && (!$isRangeSelection(selection) || selection.isCollapsed()));
|
|
176
|
+
if (wholeDocument) {
|
|
177
|
+
for (const node of $getRoot().getAllTextNodes()) {
|
|
178
|
+
node.setStyle(setStyleProperty(node.getStyle(), property, value));
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
else if ($isRangeSelection(selection)) {
|
|
182
|
+
$patchStyleText(selection, { [property]: value });
|
|
183
|
+
}
|
|
184
|
+
});
|
|
185
|
+
};
|
|
186
|
+
const withRefocus = (fn) => (value) => {
|
|
187
|
+
fn(value);
|
|
188
|
+
editor.focus();
|
|
189
|
+
};
|
|
154
190
|
const formatText = (format) => editor.dispatchCommand(FORMAT_TEXT_COMMAND, format);
|
|
155
191
|
const formatElement = (format) => editor.dispatchCommand(FORMAT_ELEMENT_COMMAND, format);
|
|
156
192
|
const applyCaseTransform = (transform) => {
|
|
@@ -198,7 +234,43 @@ export function Toolbar({ config, onSave, isDirty, showSave }) {
|
|
|
198
234
|
const showInsertPoetryGroup = config.links || config.images.linked || config.images.embedded || config.poetry.enabled;
|
|
199
235
|
const showLanguageGroup = config.language.autocorrect || config.language.textCleanup || config.language.spellCheck;
|
|
200
236
|
const showOverflowMenu = fmt.strikethrough || fmt.superscript || fmt.subscript || fmt.caseTransforms || fmt.clearFormatting || config.indent;
|
|
237
|
+
// Grouped for the dropdown when options declare groups (Latin / Urdu).
|
|
238
|
+
const fontData = (() => {
|
|
239
|
+
const data = [];
|
|
240
|
+
const groups = new Map();
|
|
241
|
+
for (const f of fontOptions) {
|
|
242
|
+
const item = { value: f.family, label: f.name };
|
|
243
|
+
if (!f.group) {
|
|
244
|
+
data.push(item);
|
|
245
|
+
continue;
|
|
246
|
+
}
|
|
247
|
+
let group = groups.get(f.group);
|
|
248
|
+
if (!group) {
|
|
249
|
+
group = { group: f.group, items: [] };
|
|
250
|
+
groups.set(f.group, group);
|
|
251
|
+
data.push(group);
|
|
252
|
+
}
|
|
253
|
+
group.items.push(item);
|
|
254
|
+
}
|
|
255
|
+
return data;
|
|
256
|
+
})();
|
|
257
|
+
const formattingOptions = [
|
|
258
|
+
{ value: 'paragraph', label: 'Paragraph' },
|
|
259
|
+
...headingLevels.map((level) => ({ value: `h${level}`, label: `Heading ${level}` })),
|
|
260
|
+
...(config.lists.numbered ? [{ value: 'number', label: 'Numbered list' }] : []),
|
|
261
|
+
...(config.lists.bullet ? [{ value: 'bullet', label: 'Bullet list' }] : []),
|
|
262
|
+
...(config.lists.check ? [{ value: 'check', label: 'Task list' }] : []),
|
|
263
|
+
...(config.blocks.quote ? [{ value: 'quote', label: 'Quote' }] : []),
|
|
264
|
+
];
|
|
265
|
+
const alignOptions = [
|
|
266
|
+
...(config.alignment.start ? [{ value: 'start', label: 'Align start' }] : []),
|
|
267
|
+
...(config.alignment.center ? [{ value: 'center', label: 'Align center' }] : []),
|
|
268
|
+
...(config.alignment.start ? [{ value: 'end', label: 'Align end' }] : []),
|
|
269
|
+
...(config.alignment.justify ? [{ value: 'justify', label: 'Justify' }] : []),
|
|
270
|
+
...(config.alignment.left ? [{ value: 'left', label: 'Align left' }] : []),
|
|
271
|
+
...(config.alignment.right ? [{ value: 'right', label: 'Align right' }] : []),
|
|
272
|
+
];
|
|
201
273
|
const formattingValue = state.listType ?? state.blockType;
|
|
202
274
|
const AlignIcon = ALIGN_ICONS[state.elementFormat] ?? IconAlignLeft;
|
|
203
|
-
return (_jsxs("div", { className: "likhari-toolbar", role: "toolbar", "aria-label": "Formatting", children: [showSave && (
|
|
275
|
+
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", children: [config.font.family && (_jsx(ToolbarSelect, { icon: IconTypography, label: "Font family", width: 150, value: fontOptions.some((f) => f.family === state.fontFamily) ? state.fontFamily : null, placeholder: "Font", data: fontData, searchable: true, 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: "Font size", width: 84, value: FONT_SIZES_PX.some((px) => `${px}px` === state.fontSize) ? state.fontSize : null, placeholder: "Size", data: FONT_SIZES_PX.map((px) => ({ value: `${px}px`, label: String(px) })), onChange: withRefocus((v) => applyFont('font-size', v)) }))] })), 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)) })] }))] })] }))] }));
|
|
204
276
|
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { type FontOption } from './urduWebFonts.js';
|
|
2
|
+
export { URDU_WEB_FONT_OPTIONS, injectUrduWebFontsCss, type FontOption } from './urduWebFonts.js';
|
|
3
|
+
/** Three generic Latin faces plus the full urdu-web-fonts collection. */
|
|
4
|
+
export declare const DEFAULT_FONT_OPTIONS: FontOption[];
|
|
5
|
+
/** Sizes offered by the toolbar's font-size dropdown, in px. */
|
|
6
|
+
export declare const FONT_SIZES_PX: number[];
|
|
7
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/fonts/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAyB,KAAK,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAExE,OAAO,EAAE,qBAAqB,EAAE,qBAAqB,EAAE,KAAK,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAE/F,yEAAyE;AACzE,eAAO,MAAM,oBAAoB,EAAE,UAAU,EAK5C,CAAC;AAEF,gEAAgE;AAChE,eAAO,MAAM,aAAa,UAAmD,CAAC"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { URDU_WEB_FONT_OPTIONS } from './urduWebFonts.js';
|
|
2
|
+
export { URDU_WEB_FONT_OPTIONS, injectUrduWebFontsCss } from './urduWebFonts.js';
|
|
3
|
+
/** Three generic Latin faces plus the full urdu-web-fonts collection. */
|
|
4
|
+
export const DEFAULT_FONT_OPTIONS = [
|
|
5
|
+
{ name: 'Serif', family: `'IBM Plex Serif', Georgia, serif`, group: 'Latin' },
|
|
6
|
+
{ name: 'Sans', family: `'IBM Plex Sans', system-ui, sans-serif`, group: 'Latin' },
|
|
7
|
+
{ name: 'Mono', family: `ui-monospace, 'SFMono-Regular', Menlo, Consolas, monospace`, group: 'Latin' },
|
|
8
|
+
...URDU_WEB_FONT_OPTIONS,
|
|
9
|
+
];
|
|
10
|
+
/** Sizes offered by the toolbar's font-size dropdown, in px. */
|
|
11
|
+
export const FONT_SIZES_PX = [12, 13, 14, 15, 16, 18, 20, 24, 28, 32, 36, 48];
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/** One entry in the toolbar's font-family dropdown. */
|
|
2
|
+
export interface FontOption {
|
|
3
|
+
/** Label shown in the dropdown */
|
|
4
|
+
name: string;
|
|
5
|
+
/** CSS font-family value stored on the selected text, e.g. `"Amiri", serif` */
|
|
6
|
+
family: string;
|
|
7
|
+
/** Optional dropdown group heading */
|
|
8
|
+
group?: string;
|
|
9
|
+
}
|
|
10
|
+
export declare const URDU_WEB_FONT_OPTIONS: FontOption[];
|
|
11
|
+
/** Registers every urdu-web-fonts @font-face via a <link> per family. Safe to
|
|
12
|
+
* call repeatedly and from several editors: it injects once per page. */
|
|
13
|
+
export declare function injectUrduWebFontsCss(): void;
|
|
14
|
+
//# sourceMappingURL=urduWebFonts.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"urduWebFonts.d.ts","sourceRoot":"","sources":["../../src/fonts/urduWebFonts.ts"],"names":[],"mappings":"AAqDA,uDAAuD;AACvD,MAAM,WAAW,UAAU;IACzB,kCAAkC;IAClC,IAAI,EAAE,MAAM,CAAC;IACb,+EAA+E;IAC/E,MAAM,EAAE,MAAM,CAAC;IACf,sCAAsC;IACtC,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,eAAO,MAAM,qBAAqB,EAAE,UAAU,EAI3C,CAAC;AAIJ;yEACyE;AACzE,wBAAgB,qBAAqB,IAAI,IAAI,CAc5C"}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Loads the Urdu/Arabic-script font collection from the
|
|
3
|
+
* inshapardaz/urdu-web-fonts GitHub repository through jsDelivr's GitHub
|
|
4
|
+
* CDN — the same source, and the same pinned commit, that the qari EPUB
|
|
5
|
+
* reader uses, so both apps offer an identical font set. Nothing is bundled
|
|
6
|
+
* or added as an npm dependency.
|
|
7
|
+
*
|
|
8
|
+
* Pinned to a commit (not a branch) so the fonts served don't change under
|
|
9
|
+
* consumers unexpectedly; bump URDU_WEB_FONTS_COMMIT to pick up upstream
|
|
10
|
+
* additions. Each family's stylesheet only registers its @font-face rules —
|
|
11
|
+
* the browser downloads a font file when that font is actually used.
|
|
12
|
+
*/
|
|
13
|
+
const URDU_WEB_FONTS_COMMIT = '571e360d5c272252efb0b0b21af54d1f63457521';
|
|
14
|
+
const URDU_WEB_FONTS_BASE_URL = `https://cdn.jsdelivr.net/gh/inshapardaz/urdu-web-fonts@${URDU_WEB_FONTS_COMMIT}/src/fonts`;
|
|
15
|
+
const URDU_WEB_FONTS = [
|
|
16
|
+
{ dir: 'adobe-arabic', name: 'Adobe Arabic', family: 'AdobeArabic' },
|
|
17
|
+
{ dir: 'alvi-lahori-nastalique', name: 'Alvi Lahori Nastaleeq', family: 'AlviLahoriNastaleeq' },
|
|
18
|
+
{ dir: 'amiri', name: 'Amiri', family: 'Amiri' },
|
|
19
|
+
{ dir: 'aref-ruqqa', name: 'Aref Ruqaa', family: 'Aref Ruqaa' },
|
|
20
|
+
{ dir: 'dehalvi-khushkhat', name: 'Dehalvi Khush Khat', family: 'DehalviKhushKhat' },
|
|
21
|
+
{ dir: 'dubai', name: 'Dubai', family: 'Dubai' },
|
|
22
|
+
{ dir: 'emad-nastaleeq', name: 'Emad Nastaleeq', family: 'EmadNastaleeq' },
|
|
23
|
+
{ dir: 'fajer-noori-nastalique', name: 'Fajer Noori Nastalique', family: 'FajerNooriNastalique' },
|
|
24
|
+
{ dir: 'gulzar-nastalique', name: 'Gulzar Nastalique', family: 'gulzar-nastalique' },
|
|
25
|
+
{ dir: 'jameel-khushkhati', name: 'Jameel Khushkhati', family: 'jameel-khushkhati' },
|
|
26
|
+
{ dir: 'jameel-noori-kasheeda', name: 'Jameel Noori Nastaleeq Kasheeda', family: 'JameelNooriNastaleeqKasheeda' },
|
|
27
|
+
{ dir: 'jameel-noori-nastalique', name: 'Jameel Noori Nastaleeq', family: 'JameelNooriNastaleeq' },
|
|
28
|
+
{ dir: 'lalezar', name: 'Lalezar', family: 'Lalezar' },
|
|
29
|
+
{ dir: 'lateef', name: 'Lateef', family: 'Lateef' },
|
|
30
|
+
{ dir: 'mada', name: 'Mada', family: 'Mada' },
|
|
31
|
+
{ dir: 'mehr-nastalique', name: 'Mehr Nastaleeq', family: 'MehrNastaleeq' },
|
|
32
|
+
{ dir: 'mehr-nastalique-2', name: 'Mehr Nastaliq Web', family: 'Mehr Nastaliq Web' },
|
|
33
|
+
{ dir: 'nafees-nastaleeq', name: 'Nafees Nastaleeq', family: 'NafeesNastaleeq' },
|
|
34
|
+
{ dir: 'Nafees-web-naskh', name: 'Nafees Web Naskh', family: 'NafeesWebNaskh' },
|
|
35
|
+
{ dir: 'noto-naskh', name: 'Noto Naskh Arabic', family: 'Noto Naskh Arabic' },
|
|
36
|
+
{ dir: 'noto-nastalique', name: 'Noto Nastaliq Urdu', family: 'Noto Nastaliq Urdu' },
|
|
37
|
+
{ dir: 'pak-nastaleeq', name: 'Pak Nastaleeq', family: 'PakNastaleeq' },
|
|
38
|
+
{ dir: 'qahiri', name: 'Qahiri', family: 'Qahiri' },
|
|
39
|
+
{ dir: 'reem-kufi', name: 'Reem Kufi', family: 'Reem Kufi' },
|
|
40
|
+
{ dir: 'sameer-khashab-bold', name: 'Sameer Khashab Bold', family: 'sameer-khashab' },
|
|
41
|
+
{ dir: 'scheherazade', name: 'Scheherazade New', family: 'Scheherazade New' },
|
|
42
|
+
];
|
|
43
|
+
export const URDU_WEB_FONT_OPTIONS = URDU_WEB_FONTS.map((font) => ({
|
|
44
|
+
name: font.name,
|
|
45
|
+
family: `"${font.family}", serif`,
|
|
46
|
+
group: 'Urdu / Arabic script',
|
|
47
|
+
}));
|
|
48
|
+
let injected = false;
|
|
49
|
+
/** Registers every urdu-web-fonts @font-face via a <link> per family. Safe to
|
|
50
|
+
* call repeatedly and from several editors: it injects once per page. */
|
|
51
|
+
export function injectUrduWebFontsCss() {
|
|
52
|
+
if (injected || typeof document === 'undefined')
|
|
53
|
+
return;
|
|
54
|
+
if (document.querySelector('link[data-likhari-urdu-fonts]')) {
|
|
55
|
+
injected = true;
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
for (const font of URDU_WEB_FONTS) {
|
|
59
|
+
const link = document.createElement('link');
|
|
60
|
+
link.rel = 'stylesheet';
|
|
61
|
+
link.href = `${URDU_WEB_FONTS_BASE_URL}/${font.dir}/stylesheet.css`;
|
|
62
|
+
link.setAttribute('data-likhari-urdu-fonts', font.dir);
|
|
63
|
+
document.head.appendChild(link);
|
|
64
|
+
}
|
|
65
|
+
injected = true;
|
|
66
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
export { EditorRoot } from './EditorRoot.js';
|
|
2
2
|
export type { EditorRootProps, EditorRef, EditorInitialContent } from './EditorRoot.js';
|
|
3
3
|
export { defaultMantineTheme } from './theme/mantineTheme.js';
|
|
4
|
+
export { DEFAULT_FONT_OPTIONS, URDU_WEB_FONT_OPTIONS, FONT_SIZES_PX } from './fonts/index.js';
|
|
5
|
+
export type { FontOption } from './fonts/index.js';
|
|
4
6
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -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,MAAM,cAAc,CAAC;AACrF,OAAO,EAAE,mBAAmB,EAAE,MAAM,sBAAsB,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,MAAM,cAAc,CAAC;AACrF,OAAO,EAAE,mBAAmB,EAAE,MAAM,sBAAsB,CAAC;AAC3D,OAAO,EAAE,oBAAoB,EAAE,qBAAqB,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AACrF,YAAY,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC"}
|
package/dist/index.js
CHANGED
package/dist/theme/editor.css
CHANGED
|
@@ -23,11 +23,14 @@
|
|
|
23
23
|
align-items: center;
|
|
24
24
|
gap: 8px;
|
|
25
25
|
min-height: 46px;
|
|
26
|
-
padding:
|
|
26
|
+
padding: 4px 8px;
|
|
27
27
|
background: var(--editor-paper-dim);
|
|
28
28
|
border-bottom: 1px solid var(--editor-line);
|
|
29
|
-
|
|
30
|
-
|
|
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;
|
|
31
34
|
}
|
|
32
35
|
|
|
33
36
|
.likhari-toolbar-group {
|
|
@@ -35,11 +38,11 @@
|
|
|
35
38
|
align-items: center;
|
|
36
39
|
gap: 2px;
|
|
37
40
|
padding: 0 6px;
|
|
38
|
-
border-
|
|
41
|
+
border-inline-end: 0.5px solid var(--editor-line);
|
|
39
42
|
}
|
|
40
43
|
|
|
41
44
|
.likhari-toolbar-group:last-child {
|
|
42
|
-
border-
|
|
45
|
+
border-inline-end: none;
|
|
43
46
|
}
|
|
44
47
|
|
|
45
48
|
.likhari-toolbar-button {
|
|
@@ -79,86 +82,54 @@
|
|
|
79
82
|
cursor: not-allowed;
|
|
80
83
|
}
|
|
81
84
|
|
|
82
|
-
|
|
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 {
|
|
83
90
|
height: 30px;
|
|
84
|
-
|
|
91
|
+
min-height: 30px;
|
|
85
92
|
font-size: 12px;
|
|
86
93
|
border-radius: 6px;
|
|
87
94
|
border: 1px solid var(--editor-line-strong);
|
|
88
95
|
background: var(--editor-paper);
|
|
89
96
|
color: var(--editor-ink);
|
|
90
|
-
padding: 0 6px;
|
|
91
97
|
}
|
|
92
98
|
|
|
93
|
-
.likhari-toolbar-select:disabled {
|
|
99
|
+
.likhari-toolbar-select-input:disabled {
|
|
94
100
|
opacity: 0.4;
|
|
95
101
|
cursor: not-allowed;
|
|
96
102
|
}
|
|
97
103
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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;
|
|
104
|
+
.likhari-overflow-item[data-active='true'] {
|
|
105
|
+
background: var(--mantine-primary-color-light);
|
|
106
|
+
color: var(--mantine-primary-color-filled);
|
|
132
107
|
}
|
|
133
108
|
|
|
134
109
|
/* Responsive collapse — editor-ui-design-spec.md §3.3. These groups hold
|
|
135
110
|
only not-yet-implemented placeholders, so hiding them below the tablet
|
|
136
111
|
breakpoint costs nothing functional while keeping the primary row usable
|
|
137
|
-
on small screens; the toolbar
|
|
138
|
-
|
|
112
|
+
on small screens; the toolbar wraps onto a second row if the
|
|
113
|
+
still-visible controls alone overflow a narrow viewport. */
|
|
139
114
|
@media (max-width: 900px) {
|
|
140
115
|
.likhari-toolbar-group--collapse-tablet {
|
|
141
116
|
display: none;
|
|
142
117
|
}
|
|
143
118
|
}
|
|
144
119
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
gap: 6px;
|
|
149
|
-
height: 30px;
|
|
150
|
-
padding: 0 14px;
|
|
151
|
-
border-radius: 6px;
|
|
152
|
-
border: 1px solid var(--editor-accent);
|
|
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'] {
|
|
153
123
|
background: var(--editor-accent);
|
|
154
124
|
color: var(--editor-paper);
|
|
155
|
-
font-size: 12px;
|
|
156
|
-
font-weight: 500;
|
|
157
|
-
cursor: pointer;
|
|
158
125
|
}
|
|
159
126
|
|
|
160
|
-
.likhari-
|
|
161
|
-
background:
|
|
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'] {
|
|
162
133
|
color: var(--editor-accent);
|
|
163
134
|
}
|
|
164
135
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"style.d.ts","sourceRoot":"","sources":["../../src/utils/style.ts"],"names":[],"mappings":"AAAA;yDACyD;AACzD,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAYrF"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/** Sets (or replaces) one property in an inline CSS string like Lexical's
|
|
2
|
+
* TextNode style, keeping every other property as-is. */
|
|
3
|
+
export function setStyleProperty(css, property, value) {
|
|
4
|
+
const entries = css
|
|
5
|
+
.split(';')
|
|
6
|
+
.map((part) => part.trim())
|
|
7
|
+
.filter(Boolean)
|
|
8
|
+
.map((part) => {
|
|
9
|
+
const i = part.indexOf(':');
|
|
10
|
+
return [part.slice(0, i).trim(), part.slice(i + 1).trim()];
|
|
11
|
+
})
|
|
12
|
+
.filter(([name]) => name !== property);
|
|
13
|
+
entries.push([property, value]);
|
|
14
|
+
return entries.map(([name, val]) => `${name}: ${val};`).join(' ');
|
|
15
|
+
}
|