@inshapardaz/likhari-react 0.1.6-dev.60 → 0.1.6-dev.64
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 +10 -1
- package/dist/EditorRoot.d.ts.map +1 -1
- package/dist/EditorRoot.js +14 -5
- package/dist/PortalTargetContext.d.ts +16 -0
- package/dist/PortalTargetContext.d.ts.map +1 -0
- package/dist/PortalTargetContext.js +18 -0
- package/dist/components/LinkDialog.d.ts.map +1 -1
- package/dist/components/LinkDialog.js +7 -3
- package/dist/components/Toolbar.d.ts +4 -1
- package/dist/components/Toolbar.d.ts.map +1 -1
- package/dist/components/Toolbar.js +193 -30
- package/dist/i18n/index.d.ts +4 -0
- package/dist/i18n/index.d.ts.map +1 -0
- package/dist/i18n/index.js +2 -0
- package/dist/i18n/strings.d.ts +158 -0
- package/dist/i18n/strings.d.ts.map +1 -0
- package/dist/i18n/strings.js +425 -0
- package/dist/i18n/useStrings.d.ts +10 -0
- package/dist/i18n/useStrings.d.ts.map +1 -0
- package/dist/i18n/useStrings.js +14 -0
- package/dist/image/ImageComponent.d.ts.map +1 -1
- package/dist/image/ImageComponent.js +7 -3
- package/dist/image/ImageCropDialog.d.ts.map +1 -1
- package/dist/image/ImageCropDialog.js +13 -6
- package/dist/image/ImageCropper.d.ts.map +1 -1
- package/dist/image/ImageCropper.js +3 -1
- package/dist/image/ImageDialog.d.ts.map +1 -1
- package/dist/image/ImageDialog.js +30 -16
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/theme/EditorThemeProvider.d.ts +4 -1
- package/dist/theme/EditorThemeProvider.d.ts.map +1 -1
- package/dist/theme/EditorThemeProvider.js +24 -2
- package/dist/theme/accentColor.d.ts +43 -0
- package/dist/theme/accentColor.d.ts.map +1 -0
- package/dist/theme/accentColor.js +84 -0
- package/dist/theme/editor.css +18 -0
- package/package.json +1 -1
package/dist/EditorRoot.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ 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
5
|
import { type FontOption } from './fonts/index.js';
|
|
6
|
+
import { type Locale } from './i18n/index.js';
|
|
6
7
|
export interface EditorInitialContent {
|
|
7
8
|
format: Extract<FormatId, 'lexical-json' | 'plain-text'>;
|
|
8
9
|
value: string;
|
|
@@ -14,7 +15,15 @@ export interface EditorRootProps {
|
|
|
14
15
|
featurePreset?: FeatureConfigPresetName;
|
|
15
16
|
theme?: MantineThemeOverride;
|
|
16
17
|
colorScheme?: 'light' | 'dark';
|
|
17
|
-
|
|
18
|
+
/**
|
|
19
|
+
* A CSS color (e.g. `'#2B6E6E'`) overriding the editor's default accent —
|
|
20
|
+
* both Mantine's own primary color (buttons, portaled dropdowns' active-item
|
|
21
|
+
* highlight, etc.) and the `--editor-accent` / `--editor-accent-soft` CSS
|
|
22
|
+
* variables the toolbar/canvas use directly. Omit to use the built-in
|
|
23
|
+
* default tokens (packages/core/src/theme/tokens.ts).
|
|
24
|
+
*/
|
|
25
|
+
accentColor?: string;
|
|
26
|
+
locale?: Locale;
|
|
18
27
|
placeholder?: string;
|
|
19
28
|
/**
|
|
20
29
|
* CSS height for the editor box (toolbar + canvas together) — accepts any
|
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;AAU1D,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;AAIjE,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,
|
|
1
|
+
{"version":3,"file":"EditorRoot.d.ts","sourceRoot":"","sources":["../src/EditorRoot.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,eAAe,CAAC;AAU1D,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;AAIjE,OAAO,EAAgC,KAAK,MAAM,EAAE,MAAM,QAAQ,CAAC;AAEnE,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;;;;;;OAMG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,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;;;;OAIG;IACH,aAAa,CAAC,EAAE,CAAC,IAAI,EAAE,IAAI,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;IAChD;;;;;;;OAOG;IACH,UAAU,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC5C;;;;;;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,uGAkKrB,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, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react';
|
|
2
|
+
import { forwardRef, useEffect, useId, 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';
|
|
@@ -18,14 +18,18 @@ import { Toolbar } from './components/Toolbar.js';
|
|
|
18
18
|
import { injectUrduWebFontsCss } from './fonts/index.js';
|
|
19
19
|
import { LinkPastePlugin } from './plugins/LinkPastePlugin.js';
|
|
20
20
|
import { ImageOptionsContext } from './image/ImageOptionsContext.js';
|
|
21
|
+
import { PortalTargetContext } from './PortalTargetContext.js';
|
|
22
|
+
import { UiStringsContext, getStrings } from './i18n/index.js';
|
|
21
23
|
function initialEditorStateJson(initialContent) {
|
|
22
24
|
if (!initialContent)
|
|
23
25
|
return undefined;
|
|
24
26
|
const state = defaultFormatRegistry.parse(initialContent.format, initialContent.value);
|
|
25
27
|
return JSON.stringify(state);
|
|
26
28
|
}
|
|
27
|
-
export const EditorRoot = forwardRef(function EditorRoot({ documentId, initialContent, featureConfig, featurePreset, theme, colorScheme, locale = 'en', placeholder
|
|
29
|
+
export const EditorRoot = forwardRef(function EditorRoot({ documentId, initialContent, featureConfig, featurePreset, theme, colorScheme, accentColor, locale = 'en', placeholder, height = '480px', onChange, onSave, showSave = Boolean(onSave), onImageUpload, fetchImage, fontOptions, }, ref) {
|
|
28
30
|
const config = useMemo(() => resolveFeatureConfig(featureConfig, featurePreset), [featureConfig, featurePreset]);
|
|
31
|
+
const strings = useMemo(() => getStrings(locale), [locale]);
|
|
32
|
+
const resolvedPlaceholder = placeholder ?? strings.editor.placeholder;
|
|
29
33
|
// The urdu-web-fonts stylesheets are needed for the font dropdown and for
|
|
30
34
|
// RTL content, whose canvas font (editor.css) is one of those families.
|
|
31
35
|
useEffect(() => {
|
|
@@ -78,12 +82,12 @@ export const EditorRoot = forwardRef(function EditorRoot({ documentId, initialCo
|
|
|
78
82
|
return true;
|
|
79
83
|
if (typeof window === 'undefined')
|
|
80
84
|
return true;
|
|
81
|
-
return window.confirm(
|
|
85
|
+
return window.confirm(strings.editor.confirmDiscard);
|
|
82
86
|
},
|
|
83
87
|
focus() {
|
|
84
88
|
rootElementRef.current?.focus();
|
|
85
89
|
},
|
|
86
|
-
}), [isDirty]);
|
|
90
|
+
}), [isDirty, strings]);
|
|
87
91
|
const rootElementRef = useRef(null);
|
|
88
92
|
const handleChange = (state) => {
|
|
89
93
|
editorStateRef.current = state;
|
|
@@ -102,5 +106,10 @@ export const EditorRoot = forwardRef(function EditorRoot({ documentId, initialCo
|
|
|
102
106
|
onSave?.(content, format);
|
|
103
107
|
};
|
|
104
108
|
const dir = locale === 'en' ? 'ltr' : 'rtl';
|
|
105
|
-
|
|
109
|
+
// A stable per-instance id for the portal-anchor div below, so multiple
|
|
110
|
+
// EditorRoot instances on one page each get their own portal target
|
|
111
|
+
// rather than colliding on a shared one.
|
|
112
|
+
const portalTargetId = useId();
|
|
113
|
+
const portalTargetSelector = `#${CSS.escape(portalTargetId)}`;
|
|
114
|
+
return (_jsx(EditorThemeProvider, { theme: theme, colorScheme: colorScheme, accentColor: accentColor, 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: _jsx(UiStringsContext.Provider, { value: strings, children: _jsx(ImageOptionsContext.Provider, { value: imageOptions, children: _jsxs(LexicalComposer, { initialConfig: initialConfig, children: [_jsx(Toolbar, { config: config, onSave: handleSave, isDirty: isDirty, showSave: showSave, fontOptions: fontOptions, direction: dir, locale: locale }), _jsx("div", { className: "likhari-canvas", children: _jsx(RichTextPlugin, { contentEditable: _jsx(ContentEditable, { className: "likhari-content-editable", dir: dir, "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, {}), _jsx(OnChangePlugin, { onChange: handleChange })] }) }) }) })] }) }));
|
|
106
115
|
});
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CSS selector for the portal-anchor `<div>` `EditorRoot` renders as the
|
|
3
|
+
* first child of `.likhari-root` (see EditorRoot.tsx). Mantine's `Menu`,
|
|
4
|
+
* `Modal`, `Select`'s combobox and `Tooltip` all default to portaling their
|
|
5
|
+
* content to `document.body` — outside the DOM subtree that carries the
|
|
6
|
+
* `--editor-*` CSS custom properties (`.likhari-theme-scope`) and the `dir`
|
|
7
|
+
* attribute (`.likhari-root`), so none of that styling/direction reaches
|
|
8
|
+
* them. Passing this selector as the target fixes that by portaling into a
|
|
9
|
+
* genuine descendant of both instead.
|
|
10
|
+
*
|
|
11
|
+
* `undefined` (the default, outside any `EditorRoot`, or before it has
|
|
12
|
+
* mounted) falls back to Mantine's own default portal behavior.
|
|
13
|
+
*/
|
|
14
|
+
export declare const PortalTargetContext: import("react").Context<string | undefined>;
|
|
15
|
+
export declare function usePortalTarget(): string | undefined;
|
|
16
|
+
//# sourceMappingURL=PortalTargetContext.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"PortalTargetContext.d.ts","sourceRoot":"","sources":["../src/PortalTargetContext.tsx"],"names":[],"mappings":"AAEA;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,mBAAmB,6CAA+C,CAAC;AAEhF,wBAAgB,eAAe,IAAI,MAAM,GAAG,SAAS,CAEpD"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { createContext, useContext } from 'react';
|
|
2
|
+
/**
|
|
3
|
+
* CSS selector for the portal-anchor `<div>` `EditorRoot` renders as the
|
|
4
|
+
* first child of `.likhari-root` (see EditorRoot.tsx). Mantine's `Menu`,
|
|
5
|
+
* `Modal`, `Select`'s combobox and `Tooltip` all default to portaling their
|
|
6
|
+
* content to `document.body` — outside the DOM subtree that carries the
|
|
7
|
+
* `--editor-*` CSS custom properties (`.likhari-theme-scope`) and the `dir`
|
|
8
|
+
* attribute (`.likhari-root`), so none of that styling/direction reaches
|
|
9
|
+
* them. Passing this selector as the target fixes that by portaling into a
|
|
10
|
+
* genuine descendant of both instead.
|
|
11
|
+
*
|
|
12
|
+
* `undefined` (the default, outside any `EditorRoot`, or before it has
|
|
13
|
+
* mounted) falls back to Mantine's own default portal behavior.
|
|
14
|
+
*/
|
|
15
|
+
export const PortalTargetContext = createContext(undefined);
|
|
16
|
+
export function usePortalTarget() {
|
|
17
|
+
return useContext(PortalTargetContext);
|
|
18
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"LinkDialog.d.ts","sourceRoot":"","sources":["../../src/components/LinkDialog.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"LinkDialog.d.ts","sourceRoot":"","sources":["../../src/components/LinkDialog.tsx"],"names":[],"mappings":"AAMA,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,OAAO,CAAC;IAChB,8DAA8D;IAC9D,UAAU,EAAE,MAAM,CAAC;IACnB,wFAAwF;IACxF,aAAa,EAAE,OAAO,CAAC;IACvB,QAAQ,EAAE,CAAC,IAAI,EAAE;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,CAAC;IACxD,kDAAkD;IAClD,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAC;IACtB,OAAO,EAAE,MAAM,IAAI,CAAC;CACrB;AAED;gDACgD;AAChD,wBAAgB,UAAU,CAAC,EAAE,MAAM,EAAE,UAAU,EAAE,aAAa,EAAE,QAAQ,EAAE,QAAQ,EAAE,OAAO,EAAE,EAAE,eAAe,+BAyE7G"}
|
|
@@ -2,9 +2,13 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
import { useEffect, useState } from 'react';
|
|
3
3
|
import { Button, Group, Modal, TextInput } from '@mantine/core';
|
|
4
4
|
import { normalizeLinkUrl } from '../utils/linkUrl.js';
|
|
5
|
+
import { useUiStrings } from '../i18n/useStrings.js';
|
|
6
|
+
import { usePortalTarget } from '../PortalTargetContext.js';
|
|
5
7
|
/** Insert/edit-link dialog (UI spec §6). Mantine Modal supplies focus
|
|
6
8
|
* trapping, Escape-to-close and the overlay. */
|
|
7
9
|
export function LinkDialog({ opened, initialUrl, showTextField, onSubmit, onRemove, onClose }) {
|
|
10
|
+
const strings = useUiStrings();
|
|
11
|
+
const portalTarget = usePortalTarget();
|
|
8
12
|
const [url, setUrl] = useState(initialUrl);
|
|
9
13
|
const [text, setText] = useState('');
|
|
10
14
|
const [error, setError] = useState(null);
|
|
@@ -19,13 +23,13 @@ export function LinkDialog({ opened, initialUrl, showTextField, onSubmit, onRemo
|
|
|
19
23
|
e.preventDefault();
|
|
20
24
|
const normalized = normalizeLinkUrl(url);
|
|
21
25
|
if (!normalized) {
|
|
22
|
-
setError(
|
|
26
|
+
setError(strings.linkDialog.invalidUrl);
|
|
23
27
|
return;
|
|
24
28
|
}
|
|
25
29
|
onSubmit({ url: normalized, text: text.trim() || normalized });
|
|
26
30
|
};
|
|
27
|
-
return (_jsx(Modal, { opened: opened, onClose: onClose, title: onRemove ?
|
|
31
|
+
return (_jsx(Modal, { opened: opened, onClose: onClose, title: onRemove ? strings.linkDialog.titleEdit : strings.linkDialog.titleInsert, centered: true, size: "sm", portalProps: { target: portalTarget }, children: _jsxs("form", { onSubmit: handleSubmit, children: [_jsx(TextInput, { label: strings.linkDialog.urlLabel, placeholder: strings.linkDialog.urlPlaceholder, value: url, onChange: (e) => {
|
|
28
32
|
setUrl(e.currentTarget.value);
|
|
29
33
|
setError(null);
|
|
30
|
-
}, error: error, "data-autofocus": true }), showTextField && (_jsx(TextInput, { mt: "sm", label:
|
|
34
|
+
}, error: error, "data-autofocus": true }), showTextField && (_jsx(TextInput, { mt: "sm", label: strings.linkDialog.textLabel, placeholder: strings.linkDialog.textPlaceholder, value: text, onChange: (e) => setText(e.currentTarget.value) })), _jsxs(Group, { justify: "space-between", mt: "md", children: [onRemove ? (_jsx(Button, { variant: "subtle", color: "red", onClick: onRemove, children: strings.link.removeLink })) : (_jsx("span", {})), _jsxs(Group, { gap: "xs", children: [_jsx(Button, { variant: "default", onClick: onClose, children: strings.common.cancel }), _jsx(Button, { type: "submit", children: strings.linkDialog.apply })] })] })] }) }));
|
|
31
35
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { ResolvedEditorFeatureConfig } from '@inshapardaz/likhari-core';
|
|
2
2
|
import { type FontOption } from '../fonts/index.js';
|
|
3
|
+
import type { Locale } from '../i18n/strings.js';
|
|
3
4
|
export interface ToolbarProps {
|
|
4
5
|
config: ResolvedEditorFeatureConfig;
|
|
5
6
|
onSave?: () => void;
|
|
@@ -9,6 +10,8 @@ export interface ToolbarProps {
|
|
|
9
10
|
fontOptions?: FontOption[];
|
|
10
11
|
/** Text direction of the canvas; picks which default font and size are pre-selected. */
|
|
11
12
|
direction?: 'ltr' | 'rtl';
|
|
13
|
+
/** UI locale — picks the translated strings for labels, tooltips and menu items. */
|
|
14
|
+
locale?: Locale;
|
|
12
15
|
}
|
|
13
|
-
export declare function Toolbar({ config, onSave, isDirty, showSave, fontOptions, direction }: ToolbarProps): import("react").JSX.Element;
|
|
16
|
+
export declare function Toolbar({ config, onSave, isDirty, showSave, fontOptions, direction, locale }: ToolbarProps): import("react").JSX.Element;
|
|
14
17
|
//# sourceMappingURL=Toolbar.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Toolbar.d.ts","sourceRoot":"","sources":["../../src/components/Toolbar.tsx"],"names":[],"mappings":"AA4CA,OAAO,KAAK,EAAE,2BAA2B,EAAE,MAAM,2BAA2B,CAAC;AAC7E,OAAO,EAA6D,KAAK,UAAU,EAAE,MAAM,UAAU,CAAC;
|
|
1
|
+
{"version":3,"file":"Toolbar.d.ts","sourceRoot":"","sources":["../../src/components/Toolbar.tsx"],"names":[],"mappings":"AA4CA,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;AAsRvD,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;IAC3B,wFAAwF;IACxF,SAAS,CAAC,EAAE,KAAK,GAAG,KAAK,CAAC;IAC1B,oFAAoF;IACpF,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,wBAAgB,OAAO,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,QAAQ,EAAE,WAAkC,EAAE,SAAiB,EAAE,MAAa,EAAE,EAAE,YAAY,+BA4wBhJ"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
2
|
+
import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
|
|
3
3
|
import { Menu, Select } from '@mantine/core';
|
|
4
4
|
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
|
|
5
5
|
import { $getSelection, $isRangeSelection, $isElementNode, $setSelection, $getRoot, $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';
|
|
@@ -15,6 +15,8 @@ import { LinkDialog } from './LinkDialog.js';
|
|
|
15
15
|
import { normalizeLinkUrl } from '../utils/linkUrl.js';
|
|
16
16
|
import { CANVAS_FONT_DEFAULTS, DEFAULT_FONT_OPTIONS, FONT_SIZES_PX } from '../fonts/index.js';
|
|
17
17
|
import { setStyleProperty } from '../utils/style.js';
|
|
18
|
+
import { useStrings } from '../i18n/useStrings.js';
|
|
19
|
+
import { usePortalTarget } from '../PortalTargetContext.js';
|
|
18
20
|
import { IconAbc, IconAlignCenter, IconAlignJustified, IconAlignLeft, IconAlignRight, IconArrowBackUp, IconArrowForwardUp, IconBold, IconClearFormatting, IconDeviceFloppy, IconDots, IconExternalLink, IconFeather, IconIndentDecrease, IconIndentIncrease, IconItalic, IconLetterCase, IconLetterCaseLower, IconLetterCaseUpper, IconLink, IconLinkOff, IconPencil, IconPhoto, IconPilcrow, IconSparkles, IconStrikethrough, IconSubscript, IconSuperscript, IconTextSize, IconTypography, IconUnderline, IconWand, } from '@tabler/icons-react';
|
|
19
21
|
/** Sentinel for "no inline style": Lexical returns the default it is given when
|
|
20
22
|
* the selection has none, so this tells "unstyled" (show the canvas default)
|
|
@@ -49,15 +51,30 @@ function ToolbarButton({ icon: Icon, title, active, dirty, disabled, onClick, })
|
|
|
49
51
|
* isn't implemented yet (link/image/poetry/font/language-tooling — see
|
|
50
52
|
* docs/lexical-editor-spec.md §13's phasing). Renders so the toolbar's
|
|
51
53
|
* layout is final now and only needs its onClick wired up later. */
|
|
52
|
-
function StubButton({ icon, title }) {
|
|
53
|
-
return _jsx(ToolbarButton, { icon: icon, title:
|
|
54
|
+
function StubButton({ icon, title, comingSoon }) {
|
|
55
|
+
return _jsx(ToolbarButton, { icon: icon, title: comingSoon(title), disabled: true, onClick: undefined });
|
|
54
56
|
}
|
|
55
57
|
/** Mantine Select (combobox) for the toolbar's dropdown controls. The icon
|
|
56
58
|
* labels the control itself, since a dropdown can't show one per option in
|
|
57
59
|
* its closed state. The dropdown portals to <body>, so it is not clipped by
|
|
58
60
|
* the editor's `overflow: hidden` frame. */
|
|
59
|
-
function ToolbarSelect({ icon: Icon, label, value, data, width, disabled, placeholder, searchable, renderOption, onChange }) {
|
|
60
|
-
|
|
61
|
+
function ToolbarSelect({ icon: Icon, label, value, data, width, disabled, placeholder, searchable, renderOption, onChange, comingSoon, noMatchMessage }) {
|
|
62
|
+
const portalTarget = usePortalTarget();
|
|
63
|
+
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) }));
|
|
64
|
+
}
|
|
65
|
+
const EMPTY_OVERFLOW = new Set();
|
|
66
|
+
function setsEqual(a, b) {
|
|
67
|
+
if (a.size !== b.size)
|
|
68
|
+
return false;
|
|
69
|
+
for (const v of a)
|
|
70
|
+
if (!b.has(v))
|
|
71
|
+
return false;
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
74
|
+
/** Renders a movable group's items as normal inline toolbar buttons (used
|
|
75
|
+
* both in the real toolbar and in the hidden width-measuring clone). */
|
|
76
|
+
function MovableGroupButtons({ group }) {
|
|
77
|
+
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))) }));
|
|
61
78
|
}
|
|
62
79
|
function OverflowItem({ icon: Icon, label, active, onClick, }) {
|
|
63
80
|
return (_jsx(Menu.Item, { leftSection: _jsx(Icon, { size: ICON_SIZE, stroke: ICON_STROKE }), "data-active": active ? 'true' : 'false', className: "likhari-overflow-item",
|
|
@@ -66,16 +83,27 @@ function OverflowItem({ icon: Icon, label, active, onClick, }) {
|
|
|
66
83
|
}
|
|
67
84
|
/** The link menu's contents — shared by the toolbar button and the right-click
|
|
68
85
|
* menu: the URL (opens in a new tab), Edit link, Remove link. */
|
|
69
|
-
function LinkMenuItems({ url, onEdit, onRemove }) {
|
|
86
|
+
function LinkMenuItems({ url, onEdit, onRemove, strings }) {
|
|
70
87
|
// Only offer the link as clickable if it's a URL the editor would have accepted.
|
|
71
88
|
const safeUrl = normalizeLinkUrl(url);
|
|
72
|
-
return (_jsxs(_Fragment, { children: [_jsx(Menu.Label, { children:
|
|
89
|
+
return (_jsxs(_Fragment, { children: [_jsx(Menu.Label, { children: strings.link.menuLabel }), safeUrl ? (_jsx(Menu.Item, { component: "a", href: safeUrl, target: "_blank", rel: "noopener noreferrer", leftSection: _jsx(IconExternalLink, { size: ICON_SIZE, stroke: ICON_STROKE }), title: url, className: "likhari-link-menu-url", children: url })) : (
|
|
73
90
|
// Not a URL the editor would accept (e.g. loaded from a document): show it, don't make it clickable.
|
|
74
|
-
_jsx(Menu.Item, { disabled: true, title: url, className: "likhari-link-menu-url", children: url ||
|
|
91
|
+
_jsx(Menu.Item, { disabled: true, title: url, className: "likhari-link-menu-url", children: url || strings.link.noUrl })), _jsx(Menu.Item, { leftSection: _jsx(IconPencil, { size: ICON_SIZE, stroke: ICON_STROKE }), onMouseDown: (e) => e.preventDefault(), onClick: onEdit, children: strings.link.editLink }), _jsx(Menu.Item, { color: "red", leftSection: _jsx(IconLinkOff, { size: ICON_SIZE, stroke: ICON_STROKE }), onMouseDown: (e) => e.preventDefault(), onClick: onRemove, children: strings.link.removeLink })] }));
|
|
75
92
|
}
|
|
76
|
-
export function Toolbar({ config, onSave, isDirty, showSave, fontOptions = DEFAULT_FONT_OPTIONS, direction = 'ltr' }) {
|
|
93
|
+
export function Toolbar({ config, onSave, isDirty, showSave, fontOptions = DEFAULT_FONT_OPTIONS, direction = 'ltr', locale = 'en' }) {
|
|
77
94
|
const [editor] = useLexicalComposerContext();
|
|
78
95
|
const [state, setState] = useState(INITIAL_STATE);
|
|
96
|
+
const strings = useStrings(locale);
|
|
97
|
+
const portalTarget = usePortalTarget();
|
|
98
|
+
// Responsive "priority+" overflow: the toolbar's outer container and a
|
|
99
|
+
// hidden nowrap clone (rendered with every movable group forced inline)
|
|
100
|
+
// are both measured; whenever the clone's natural width exceeds the
|
|
101
|
+
// container's available width, the lowest-priority movable groups are
|
|
102
|
+
// moved into the "..." menu, lowest-priority first, until what remains
|
|
103
|
+
// inline fits (or there's nothing left to move).
|
|
104
|
+
const toolbarContainerRef = useRef(null);
|
|
105
|
+
const toolbarMeasureRef = useRef(null);
|
|
106
|
+
const [overflowGroupKeys, setOverflowGroupKeys] = useState(EMPTY_OVERFLOW);
|
|
79
107
|
const updateToolbar = useCallback(() => {
|
|
80
108
|
editor.getEditorState().read(() => {
|
|
81
109
|
const selection = $getSelection();
|
|
@@ -371,7 +399,112 @@ export function Toolbar({ config, onSave, isDirty, showSave, fontOptions = DEFAU
|
|
|
371
399
|
const showAlignGroup = config.alignment.start || config.alignment.center || config.alignment.justify || config.alignment.left || config.alignment.right;
|
|
372
400
|
const showStubInsertGroup = config.images.linked || config.images.embedded || config.poetry.enabled;
|
|
373
401
|
const showLanguageGroup = config.language.autocorrect || config.language.textCleanup || config.language.spellCheck;
|
|
374
|
-
|
|
402
|
+
// The "script & cleanup" and "indent/outdent" groups (UI spec §3.3) render
|
|
403
|
+
// as normal inline toolbar groups whenever there's room, and only move
|
|
404
|
+
// into the "..." overflow menu when the measured toolbar content doesn't
|
|
405
|
+
// fit the available width (see the ResizeObserver effect below) — this
|
|
406
|
+
// replaces the old always-on overflow menu, which was driven purely by
|
|
407
|
+
// these config flags regardless of actual space.
|
|
408
|
+
const scriptCleanupItems = [];
|
|
409
|
+
if (fmt.strikethrough) {
|
|
410
|
+
scriptCleanupItems.push({
|
|
411
|
+
key: 'strikethrough',
|
|
412
|
+
icon: IconStrikethrough,
|
|
413
|
+
label: strings.toolbar.strikethrough,
|
|
414
|
+
active: state.activeFormats.has('strikethrough'),
|
|
415
|
+
onClick: () => formatText('strikethrough'),
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
if (fmt.superscript) {
|
|
419
|
+
scriptCleanupItems.push({
|
|
420
|
+
key: 'superscript',
|
|
421
|
+
icon: IconSuperscript,
|
|
422
|
+
label: strings.toolbar.superscript,
|
|
423
|
+
active: state.activeFormats.has('superscript'),
|
|
424
|
+
onClick: () => formatText('superscript'),
|
|
425
|
+
});
|
|
426
|
+
}
|
|
427
|
+
if (fmt.subscript) {
|
|
428
|
+
scriptCleanupItems.push({
|
|
429
|
+
key: 'subscript',
|
|
430
|
+
icon: IconSubscript,
|
|
431
|
+
label: strings.toolbar.subscript,
|
|
432
|
+
active: state.activeFormats.has('subscript'),
|
|
433
|
+
onClick: () => formatText('subscript'),
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
if (fmt.caseTransforms) {
|
|
437
|
+
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') });
|
|
438
|
+
}
|
|
439
|
+
if (fmt.clearFormatting) {
|
|
440
|
+
scriptCleanupItems.push({ key: 'clear', icon: IconClearFormatting, label: strings.toolbar.clearFormatting, onClick: clearFormatting });
|
|
441
|
+
}
|
|
442
|
+
const indentItems = config.indent
|
|
443
|
+
? [
|
|
444
|
+
{
|
|
445
|
+
key: 'outdent',
|
|
446
|
+
icon: IconIndentDecrease,
|
|
447
|
+
label: strings.toolbar.outdent,
|
|
448
|
+
onClick: () => editor.dispatchCommand(OUTDENT_CONTENT_COMMAND, undefined),
|
|
449
|
+
},
|
|
450
|
+
{
|
|
451
|
+
key: 'indent',
|
|
452
|
+
icon: IconIndentIncrease,
|
|
453
|
+
label: strings.toolbar.indent,
|
|
454
|
+
onClick: () => editor.dispatchCommand(INDENT_CONTENT_COMMAND, undefined),
|
|
455
|
+
},
|
|
456
|
+
]
|
|
457
|
+
: [];
|
|
458
|
+
// Low to high priority: the first group here is the first one pushed into
|
|
459
|
+
// overflow once content stops fitting (UI spec §3.3).
|
|
460
|
+
const movableGroups = [
|
|
461
|
+
{ key: 'scriptCleanup', items: scriptCleanupItems },
|
|
462
|
+
{ key: 'indentOutdent', items: indentItems },
|
|
463
|
+
].filter((g) => g.items.length > 0);
|
|
464
|
+
const movableGroupKeys = useMemo(() => movableGroups.map((g) => g.key).join(','), [movableGroups]);
|
|
465
|
+
// Re-measure whenever the container is resized, or whenever the set of
|
|
466
|
+
// movable groups (and so the hidden clone's content) changes.
|
|
467
|
+
useLayoutEffect(() => {
|
|
468
|
+
const container = toolbarContainerRef.current;
|
|
469
|
+
const measure = toolbarMeasureRef.current;
|
|
470
|
+
if (!container || !measure)
|
|
471
|
+
return;
|
|
472
|
+
// Rough width of the "..." button itself (icon button + its gap) —
|
|
473
|
+
// only charged against the budget once something has actually moved
|
|
474
|
+
// into it, since it isn't rendered at all otherwise.
|
|
475
|
+
const OVERFLOW_BUTTON_WIDTH = 38;
|
|
476
|
+
const recompute = () => {
|
|
477
|
+
const available = container.clientWidth;
|
|
478
|
+
const total = measure.scrollWidth;
|
|
479
|
+
if (total <= available) {
|
|
480
|
+
setOverflowGroupKeys((prev) => (prev.size === 0 ? prev : EMPTY_OVERFLOW));
|
|
481
|
+
return;
|
|
482
|
+
}
|
|
483
|
+
const widths = new Map();
|
|
484
|
+
measure.querySelectorAll('[data-group-key]').forEach((el) => {
|
|
485
|
+
widths.set(el.dataset.groupKey, el.getBoundingClientRect().width);
|
|
486
|
+
});
|
|
487
|
+
let remaining = total;
|
|
488
|
+
const next = new Set();
|
|
489
|
+
for (const key of movableGroupKeys.split(',').filter(Boolean)) {
|
|
490
|
+
if (remaining <= available)
|
|
491
|
+
break;
|
|
492
|
+
const w = widths.get(key);
|
|
493
|
+
if (w === undefined)
|
|
494
|
+
continue;
|
|
495
|
+
remaining -= w;
|
|
496
|
+
next.add(key);
|
|
497
|
+
}
|
|
498
|
+
if (next.size > 0)
|
|
499
|
+
remaining += OVERFLOW_BUTTON_WIDTH;
|
|
500
|
+
setOverflowGroupKeys((prev) => (setsEqual(prev, next) ? prev : next));
|
|
501
|
+
};
|
|
502
|
+
recompute();
|
|
503
|
+
const ro = new ResizeObserver(recompute);
|
|
504
|
+
ro.observe(container);
|
|
505
|
+
ro.observe(measure);
|
|
506
|
+
return () => ro.disconnect();
|
|
507
|
+
});
|
|
375
508
|
// Grouped for the dropdown when options declare groups (Latin / Urdu).
|
|
376
509
|
const fontData = (() => {
|
|
377
510
|
const data = [];
|
|
@@ -397,33 +530,63 @@ export function Toolbar({ config, onSave, isDirty, showSave, fontOptions = DEFAU
|
|
|
397
530
|
const shownFontFamily = state.fontFamily === UNSET_STYLE ? canvasDefaults.family : state.fontFamily;
|
|
398
531
|
const shownFontSize = state.fontSize === UNSET_STYLE ? canvasDefaults.size : state.fontSize;
|
|
399
532
|
const formattingOptions = [
|
|
400
|
-
{ value: 'paragraph', label:
|
|
401
|
-
...headingLevels.map((level) => ({ value: `h${level}`, label:
|
|
402
|
-
...(config.lists.numbered ? [{ value: 'number', label:
|
|
403
|
-
...(config.lists.bullet ? [{ value: 'bullet', label:
|
|
404
|
-
...(config.lists.check ? [{ value: 'check', label:
|
|
405
|
-
...(config.blocks.quote ? [{ value: 'quote', label:
|
|
533
|
+
{ value: 'paragraph', label: strings.toolbar.formattingOptions.paragraph },
|
|
534
|
+
...headingLevels.map((level) => ({ value: `h${level}`, label: strings.toolbar.formattingOptions.heading(level) })),
|
|
535
|
+
...(config.lists.numbered ? [{ value: 'number', label: strings.toolbar.formattingOptions.numberedList }] : []),
|
|
536
|
+
...(config.lists.bullet ? [{ value: 'bullet', label: strings.toolbar.formattingOptions.bulletList }] : []),
|
|
537
|
+
...(config.lists.check ? [{ value: 'check', label: strings.toolbar.formattingOptions.taskList }] : []),
|
|
538
|
+
...(config.blocks.quote ? [{ value: 'quote', label: strings.toolbar.formattingOptions.quote }] : []),
|
|
406
539
|
];
|
|
407
540
|
const alignOptions = [
|
|
408
|
-
...(config.alignment.start ? [{ value: 'start', label:
|
|
409
|
-
...(config.alignment.center ? [{ value: 'center', label:
|
|
410
|
-
...(config.alignment.start ? [{ value: 'end', label:
|
|
411
|
-
...(config.alignment.justify ? [{ value: 'justify', label:
|
|
412
|
-
...(config.alignment.left ? [{ value: 'left', label:
|
|
413
|
-
...(config.alignment.right ? [{ value: 'right', label:
|
|
541
|
+
...(config.alignment.start ? [{ value: 'start', label: strings.toolbar.alignOptions.start }] : []),
|
|
542
|
+
...(config.alignment.center ? [{ value: 'center', label: strings.toolbar.alignOptions.center }] : []),
|
|
543
|
+
...(config.alignment.start ? [{ value: 'end', label: strings.toolbar.alignOptions.end }] : []),
|
|
544
|
+
...(config.alignment.justify ? [{ value: 'justify', label: strings.toolbar.alignOptions.justify }] : []),
|
|
545
|
+
...(config.alignment.left ? [{ value: 'left', label: strings.toolbar.alignOptions.left }] : []),
|
|
546
|
+
...(config.alignment.right ? [{ value: 'right', label: strings.toolbar.alignOptions.right }] : []),
|
|
414
547
|
];
|
|
415
548
|
const formattingValue = state.listType ?? state.blockType;
|
|
416
549
|
const AlignIcon = ALIGN_ICONS[state.elementFormat] ?? IconAlignLeft;
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
550
|
+
// The toolbar's "always inline" groups (UI spec §3.3's Desktop tier) —
|
|
551
|
+
// built as an array so the exact same elements can be rendered both in
|
|
552
|
+
// the visible toolbar and in the hidden width-measuring clone below,
|
|
553
|
+
// without duplicating the JSX. The link right-click context menu is kept
|
|
554
|
+
// out of this list: it's a zero-size, fixed-position anchor, not part of
|
|
555
|
+
// the toolbar's own layout width.
|
|
556
|
+
const fixedGroups = [
|
|
557
|
+
showSave && (_jsx("div", { className: "likhari-toolbar-group", children: _jsx(ToolbarButton, { icon: IconDeviceFloppy, title: strings.toolbar.save, dirty: Boolean(isDirty), onClick: onSave }) }, "save")),
|
|
558
|
+
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")),
|
|
559
|
+
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, onChange: withRefocus((v) => applyFormatting(v)) }) }, "formatting")),
|
|
560
|
+
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")),
|
|
561
|
+
(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")),
|
|
562
|
+
showAlignGroup && (_jsx("div", { className: "likhari-toolbar-group", children: _jsx(ToolbarSelect, { icon: AlignIcon, label: strings.toolbar.alignment, width: 138, value: state.elementFormat || 'start', data: alignOptions, onChange: withRefocus((v) => formatElement(v)) }) }, "align")),
|
|
563
|
+
config.links && (_jsx("div", { className: "likhari-toolbar-group", children: state.isLink ? (
|
|
564
|
+
// Caret is in a link: the button opens a menu to see, edit or remove it.
|
|
565
|
+
_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: () => {
|
|
566
|
+
restoreSelection();
|
|
567
|
+
openLinkDialog();
|
|
568
|
+
}, onRemove: removeLink, strings: strings }) })] })) : (_jsx(ToolbarButton, { icon: IconLink, title: strings.toolbar.insertLink, onClick: openLinkDialog })) }, "link")),
|
|
569
|
+
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.poetry.enabled && _jsx(StubButton, { icon: IconFeather, title: strings.toolbar.poetryBlocks, comingSoon: strings.toolbar.comingSoon })] }, "stubInsert")),
|
|
570
|
+
showLanguageGroup && (_jsxs("div", { className: "likhari-toolbar-group likhari-toolbar-group--collapse-tablet", children: [config.language.autocorrect && _jsx(StubButton, { icon: IconWand, title: strings.toolbar.autocorrect, comingSoon: strings.toolbar.comingSoon }), config.language.textCleanup && _jsx(StubButton, { icon: IconSparkles, title: strings.toolbar.textCleanup, comingSoon: strings.toolbar.comingSoon }), config.language.spellCheck && _jsx(StubButton, { icon: IconAbc, title: strings.toolbar.spellChecker, comingSoon: strings.toolbar.comingSoon })] }, "language")),
|
|
571
|
+
];
|
|
572
|
+
return (_jsxs("div", { className: "likhari-toolbar", role: "toolbar", "aria-label": strings.toolbar.ariaLabel, ref: toolbarContainerRef, children: [fixedGroups, movableGroups
|
|
573
|
+
.filter((group) => !overflowGroupKeys.has(group.key))
|
|
574
|
+
.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
|
|
575
|
+
.filter((group) => overflowGroupKeys.has(group.key))
|
|
576
|
+
.flatMap((group) => group.items.map((item) => (_jsx(OverflowItem, { icon: item.icon, label: item.label, active: item.active, onClick: runOverflowAction(item.onClick) }, item.key)))) })] })), config.links && (_jsxs(Menu, { opened: linkContext !== null, onChange: (opened) => {
|
|
423
577
|
if (!opened)
|
|
424
578
|
setLinkContext(null);
|
|
425
|
-
}, position: "bottom-start", withinPortal: true, shadow: "sm", width: 240, children: [_jsx(Menu.Target, { children: _jsx("span", { "aria-hidden": "true", style: { position: 'fixed', left: linkContext?.x ?? -9999, top: linkContext?.y ?? -9999, width: 1, height: 1, pointerEvents: 'none' } }) }), _jsx(Menu.Dropdown, { children: _jsx(LinkMenuItems, { url: linkContext?.url ?? '', onEdit: () => {
|
|
579
|
+
}, position: "bottom-start", withinPortal: true, portalProps: { target: portalTarget }, shadow: "sm", width: 240, children: [_jsx(Menu.Target, { children: _jsx("span", { "aria-hidden": "true", style: { position: 'fixed', left: linkContext?.x ?? -9999, top: linkContext?.y ?? -9999, width: 1, height: 1, pointerEvents: 'none' } }) }), _jsx(Menu.Dropdown, { children: _jsx(LinkMenuItems, { url: linkContext?.url ?? '', onEdit: () => {
|
|
426
580
|
restoreSelection();
|
|
427
581
|
openLinkDialog();
|
|
428
|
-
}, onRemove: removeLink
|
|
582
|
+
}, onRemove: removeLink, strings: strings }) })] })), _jsxs("div", { "aria-hidden": "true", ref: toolbarMeasureRef, style: {
|
|
583
|
+
position: 'absolute',
|
|
584
|
+
visibility: 'hidden',
|
|
585
|
+
pointerEvents: 'none',
|
|
586
|
+
top: 0,
|
|
587
|
+
left: 0,
|
|
588
|
+
display: 'flex',
|
|
589
|
+
flexWrap: 'nowrap',
|
|
590
|
+
gap: 8,
|
|
591
|
+
}, 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.links && (_jsx(LinkDialog, { opened: linkDialogOpen, initialUrl: state.linkUrl, showTextField: linkNeedsText, onSubmit: applyLink, onRemove: state.isLink ? removeLink : undefined, onClose: closeLinkDialog }))] }));
|
|
429
592
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/i18n/index.ts"],"names":[],"mappings":"AAAA,YAAY,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACjD,OAAO,EAAE,OAAO,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AAChD,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC"}
|