@inshapardaz/likhari-react 0.1.6 → 0.1.7-dev.75

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.
Files changed (68) hide show
  1. package/dist/EditorRoot.d.ts +43 -1
  2. package/dist/EditorRoot.d.ts.map +1 -1
  3. package/dist/EditorRoot.js +77 -6
  4. package/dist/PortalTargetContext.d.ts +16 -0
  5. package/dist/PortalTargetContext.d.ts.map +1 -0
  6. package/dist/PortalTargetContext.js +18 -0
  7. package/dist/components/LinkDialog.d.ts.map +1 -1
  8. package/dist/components/LinkDialog.js +7 -3
  9. package/dist/components/Toolbar.d.ts +4 -1
  10. package/dist/components/Toolbar.d.ts.map +1 -1
  11. package/dist/components/Toolbar.js +246 -33
  12. package/dist/i18n/index.d.ts +4 -0
  13. package/dist/i18n/index.d.ts.map +1 -0
  14. package/dist/i18n/index.js +2 -0
  15. package/dist/i18n/strings.d.ts +160 -0
  16. package/dist/i18n/strings.d.ts.map +1 -0
  17. package/dist/i18n/strings.js +428 -0
  18. package/dist/i18n/useStrings.d.ts +10 -0
  19. package/dist/i18n/useStrings.d.ts.map +1 -0
  20. package/dist/i18n/useStrings.js +14 -0
  21. package/dist/image/ImageComponent.d.ts +23 -0
  22. package/dist/image/ImageComponent.d.ts.map +1 -0
  23. package/dist/image/ImageComponent.js +171 -0
  24. package/dist/image/ImageCropDialog.d.ts +22 -0
  25. package/dist/image/ImageCropDialog.d.ts.map +1 -0
  26. package/dist/image/ImageCropDialog.js +120 -0
  27. package/dist/image/ImageCropper.d.ts +15 -0
  28. package/dist/image/ImageCropper.d.ts.map +1 -0
  29. package/dist/image/ImageCropper.js +120 -0
  30. package/dist/image/ImageDialog.d.ts +37 -0
  31. package/dist/image/ImageDialog.d.ts.map +1 -0
  32. package/dist/image/ImageDialog.js +176 -0
  33. package/dist/image/ImageNode.d.ts +64 -0
  34. package/dist/image/ImageNode.d.ts.map +1 -0
  35. package/dist/image/ImageNode.js +133 -0
  36. package/dist/image/ImageOptionsContext.d.ts +24 -0
  37. package/dist/image/ImageOptionsContext.d.ts.map +1 -0
  38. package/dist/image/ImageOptionsContext.js +10 -0
  39. package/dist/image/cropMath.d.ts +35 -0
  40. package/dist/image/cropMath.d.ts.map +1 -0
  41. package/dist/image/cropMath.js +92 -0
  42. package/dist/image/imageEdit.d.ts +49 -0
  43. package/dist/image/imageEdit.d.ts.map +1 -0
  44. package/dist/image/imageEdit.js +132 -0
  45. package/dist/image/imageUrl.d.ts +14 -0
  46. package/dist/image/imageUrl.d.ts.map +1 -0
  47. package/dist/image/imageUrl.js +27 -0
  48. package/dist/index.d.ts +4 -0
  49. package/dist/index.d.ts.map +1 -1
  50. package/dist/index.js +2 -0
  51. package/dist/nodes.d.ts.map +1 -1
  52. package/dist/nodes.js +2 -0
  53. package/dist/persistence/draftStorage.d.ts +26 -0
  54. package/dist/persistence/draftStorage.d.ts.map +1 -0
  55. package/dist/persistence/draftStorage.js +71 -0
  56. package/dist/plugins/AutosaveRestorePlugin.d.ts +16 -0
  57. package/dist/plugins/AutosaveRestorePlugin.d.ts.map +1 -0
  58. package/dist/plugins/AutosaveRestorePlugin.js +33 -0
  59. package/dist/theme/EditorThemeProvider.d.ts +17 -1
  60. package/dist/theme/EditorThemeProvider.d.ts.map +1 -1
  61. package/dist/theme/EditorThemeProvider.js +24 -2
  62. package/dist/theme/accentColor.d.ts +43 -0
  63. package/dist/theme/accentColor.d.ts.map +1 -0
  64. package/dist/theme/accentColor.js +84 -0
  65. package/dist/theme/editor.css +125 -0
  66. package/dist/theme/editorTheme.d.ts.map +1 -1
  67. package/dist/theme/editorTheme.js +1 -0
  68. package/package.json +2 -1
@@ -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
- locale?: 'en' | 'ur' | 'pa-shahmukhi';
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
@@ -38,6 +47,21 @@ export interface EditorRootProps {
38
47
  * the unsaved-changes state) or to hide it while keeping `onSave`.
39
48
  */
40
49
  showSave?: boolean;
50
+ /**
51
+ * Stores an uploaded image and returns the URL to reference it by. Without
52
+ * it, uploaded images are embedded in the document as base64 data URIs —
53
+ * simple, but they bloat the document (see `images.maxSizeMB`).
54
+ */
55
+ onImageUpload?: (file: File) => Promise<string>;
56
+ /**
57
+ * Downloads a linked image's bytes when converting it to an embedded copy
58
+ * or applying a crop/rotate/flip. Without it, the browser fetches the URL
59
+ * directly, which fails for any image server that doesn't send CORS
60
+ * headers — that restriction can't be worked around from the browser
61
+ * alone. Provide this to route the request through your own backend (or a
62
+ * CORS proxy) instead.
63
+ */
64
+ fetchImage?: (url: string) => Promise<Blob>;
41
65
  /**
42
66
  * Entries for the toolbar's font-family dropdown. Defaults to
43
67
  * `DEFAULT_FONT_OPTIONS` (generic Latin faces plus the Urdu/Arabic-script
@@ -46,6 +70,24 @@ export interface EditorRootProps {
46
70
  * `[...DEFAULT_FONT_OPTIONS, { name: 'Mine', family: '"Mine", serif' }]`.
47
71
  */
48
72
  fontOptions?: FontOption[];
73
+ /**
74
+ * Debounced autosave of the current content to `localStorage`, namespaced
75
+ * by `documentId` (lexical-editor-spec.md §6.1–§6.2) — a resilience net
76
+ * against an accidental tab close, independent of `onSave`/the Save
77
+ * button. Requires `documentId` to be set; a draft newer than the initial
78
+ * content prompts (via `window.confirm`) to restore it on mount, and is
79
+ * cleared on a successful explicit save. Defaults to `true`.
80
+ */
81
+ autosave?: boolean;
82
+ /** Debounce delay before writing an autosave draft, in ms. Default `750`. */
83
+ autosaveDelayMs?: number;
84
+ /**
85
+ * Skips (rather than throwing) an autosave write whose JSON would exceed
86
+ * this size — `localStorage` has a shared ~5-10MB per-origin ceiling, so a
87
+ * single document with embedded base64 images must not be allowed to
88
+ * autosave past a sane limit. Default `2_000_000` (~2MB).
89
+ */
90
+ autosaveMaxBytes?: number;
49
91
  }
50
92
  export interface EditorRef {
51
93
  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;AAKzH,OAAO,EAAyB,KAAK,UAAU,EAAE,MAAM,SAAS,CAAC;AAGjE,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,uGA6HrB,CAAC"}
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;AAMjE,OAAO,EAAgC,KAAK,MAAM,EAAE,MAAM,QAAQ,CAAC;AAKnE,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;IAC3B;;;;;;;OAOG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,6EAA6E;IAC7E,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB;;;;;OAKG;IACH,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B;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,uGA6NrB,CAAC"}
@@ -1,10 +1,11 @@
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';
6
6
  import { HistoryPlugin } from '@lexical/react/LexicalHistoryPlugin';
7
7
  import { ListPlugin } from '@lexical/react/LexicalListPlugin';
8
+ import { CheckListPlugin } from '@lexical/react/LexicalCheckListPlugin';
8
9
  import { LinkPlugin } from '@lexical/react/LexicalLinkPlugin';
9
10
  import { OnChangePlugin } from '@lexical/react/LexicalOnChangePlugin';
10
11
  import { LexicalErrorBoundary } from '@lexical/react/LexicalErrorBoundary';
@@ -16,34 +17,83 @@ import { EditorThemeProvider } from './theme/EditorThemeProvider.js';
16
17
  import { Toolbar } from './components/Toolbar.js';
17
18
  import { injectUrduWebFontsCss } from './fonts/index.js';
18
19
  import { LinkPastePlugin } from './plugins/LinkPastePlugin.js';
20
+ import { AutosaveRestorePlugin } from './plugins/AutosaveRestorePlugin.js';
21
+ import { clearDraft, writeDraft } from './persistence/draftStorage.js';
22
+ import { ImageOptionsContext } from './image/ImageOptionsContext.js';
23
+ import { PortalTargetContext } from './PortalTargetContext.js';
24
+ import { UiStringsContext, getStrings } from './i18n/index.js';
25
+ const DEFAULT_AUTOSAVE_DELAY_MS = 750;
26
+ const DEFAULT_AUTOSAVE_MAX_BYTES = 2000000;
19
27
  function initialEditorStateJson(initialContent) {
20
28
  if (!initialContent)
21
29
  return undefined;
22
30
  const state = defaultFormatRegistry.parse(initialContent.format, initialContent.value);
23
31
  return JSON.stringify(state);
24
32
  }
25
- 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) {
33
+ 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, autosave = true, autosaveDelayMs = DEFAULT_AUTOSAVE_DELAY_MS, autosaveMaxBytes = DEFAULT_AUTOSAVE_MAX_BYTES, }, ref) {
26
34
  const config = useMemo(() => resolveFeatureConfig(featureConfig, featurePreset), [featureConfig, featurePreset]);
35
+ const strings = useMemo(() => getStrings(locale), [locale]);
36
+ const resolvedPlaceholder = placeholder ?? strings.editor.placeholder;
37
+ const autosaveEnabled = autosave && Boolean(documentId);
27
38
  // The urdu-web-fonts stylesheets are needed for the font dropdown and for
28
39
  // RTL content, whose canvas font (editor.css) is one of those families.
29
40
  useEffect(() => {
30
41
  if (config.font.family || locale !== 'en')
31
42
  injectUrduWebFontsCss();
32
43
  }, [config.font.family, locale]);
44
+ const imageOptions = useMemo(() => ({
45
+ allowLinked: config.images.linked,
46
+ allowEmbedded: config.images.embedded,
47
+ allowCaption: config.images.caption,
48
+ maxSizeMB: config.images.maxSizeMB,
49
+ onImageUpload,
50
+ fetchImage,
51
+ }), [config.images.linked, config.images.embedded, config.images.caption, config.images.maxSizeMB, onImageUpload, fetchImage]);
33
52
  const editorStateRef = useRef(null);
34
53
  const lastSavedJsonRef = useRef(null);
35
54
  const [isDirty, setIsDirty] = useState(false);
55
+ const isDirtyRef = useRef(isDirty);
56
+ isDirtyRef.current = isDirty;
57
+ const autosaveTimerRef = useRef(null);
58
+ // Computed once, before the first autosave write could possibly run, so
59
+ // the restore-prompt plugin below can tell a stored draft that's merely
60
+ // identical to the initial content apart (no prompt needed) from one that
61
+ // actually represents newer, unsaved work.
62
+ const initialContentJsonRef = useRef(undefined);
63
+ if (initialContentJsonRef.current === undefined) {
64
+ initialContentJsonRef.current = initialEditorStateJson(initialContent);
65
+ }
36
66
  const initialConfig = useMemo(() => ({
37
67
  namespace: `likhari-editor-${documentId ?? 'anonymous'}`,
38
68
  nodes: EDITOR_NODES,
39
69
  theme: editorTheme,
40
- editorState: initialEditorStateJson(initialContent),
70
+ editorState: initialContentJsonRef.current,
41
71
  onError(error) {
42
72
  throw error;
43
73
  },
44
74
  }),
45
75
  // eslint-disable-next-line react-hooks/exhaustive-deps
46
76
  []);
77
+ // beforeunload (lexical-editor-spec.md §6.4): the automatic half of the
78
+ // navigation guard — hasUnsavedChanges()/confirmDiscard() below are the
79
+ // imperative half a host's own router guard calls for in-app navigation,
80
+ // which the editor can't intercept on its own.
81
+ useEffect(() => {
82
+ if (typeof window === 'undefined')
83
+ return;
84
+ const handleBeforeUnload = (event) => {
85
+ if (!isDirtyRef.current)
86
+ return;
87
+ event.preventDefault();
88
+ event.returnValue = '';
89
+ };
90
+ window.addEventListener('beforeunload', handleBeforeUnload);
91
+ return () => window.removeEventListener('beforeunload', handleBeforeUnload);
92
+ }, []);
93
+ useEffect(() => () => {
94
+ if (autosaveTimerRef.current)
95
+ clearTimeout(autosaveTimerRef.current);
96
+ }, []);
47
97
  useImperativeHandle(ref, () => ({
48
98
  getContent(format) {
49
99
  if (!editorStateRef.current)
@@ -68,18 +118,25 @@ export const EditorRoot = forwardRef(function EditorRoot({ documentId, initialCo
68
118
  return true;
69
119
  if (typeof window === 'undefined')
70
120
  return true;
71
- return window.confirm('You have unsaved changes. Discard them?');
121
+ return window.confirm(strings.editor.confirmDiscard);
72
122
  },
73
123
  focus() {
74
124
  rootElementRef.current?.focus();
75
125
  },
76
- }), [isDirty]);
126
+ }), [isDirty, strings]);
77
127
  const rootElementRef = useRef(null);
78
128
  const handleChange = (state) => {
79
129
  editorStateRef.current = state;
80
130
  const json = JSON.stringify(state.toJSON());
81
131
  setIsDirty(json !== lastSavedJsonRef.current);
82
132
  onChange?.(state.toJSON());
133
+ if (autosaveEnabled && documentId) {
134
+ if (autosaveTimerRef.current)
135
+ clearTimeout(autosaveTimerRef.current);
136
+ autosaveTimerRef.current = setTimeout(() => {
137
+ writeDraft(documentId, json, autosaveMaxBytes);
138
+ }, autosaveDelayMs);
139
+ }
83
140
  };
84
141
  const handleSave = () => {
85
142
  if (!editorStateRef.current)
@@ -89,8 +146,22 @@ export const EditorRoot = forwardRef(function EditorRoot({ documentId, initialCo
89
146
  const content = defaultFormatRegistry.serialize(format, json);
90
147
  lastSavedJsonRef.current = JSON.stringify(json);
91
148
  setIsDirty(false);
149
+ // A successful explicit save supersedes any in-progress/queued autosave
150
+ // draft — don't let a stale one linger and falsely prompt to "restore"
151
+ // older content next time this document is opened (§6.2).
152
+ if (autosaveTimerRef.current) {
153
+ clearTimeout(autosaveTimerRef.current);
154
+ autosaveTimerRef.current = null;
155
+ }
156
+ if (documentId)
157
+ clearDraft(documentId);
92
158
  onSave?.(content, format);
93
159
  };
94
160
  const dir = locale === 'en' ? 'ltr' : 'rtl';
95
- 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, direction: dir }), _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, {}), config.links && _jsx(LinkPastePlugin, {}), _jsx(OnChangePlugin, { onChange: handleChange })] }) }) }));
161
+ // A stable per-instance id for the portal-anchor div below, so multiple
162
+ // EditorRoot instances on one page each get their own portal target
163
+ // rather than colliding on a shared one.
164
+ const portalTargetId = useId();
165
+ const portalTargetSelector = `#${CSS.escape(portalTargetId)}`;
166
+ return (_jsx(EditorThemeProvider, { theme: theme, colorScheme: colorScheme, accentColor: accentColor, scopeElementId: portalTargetId, children: _jsxs("div", { className: "likhari-root", dir: dir, ref: rootElementRef, "data-document-id": documentId, style: { height: typeof height === 'number' ? `${height}px` : height }, children: [_jsx("div", { id: portalTargetId, className: "likhari-portal-target" }), _jsx(PortalTargetContext.Provider, { value: portalTargetSelector, children: _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 }), _jsx(AutosaveRestorePlugin, { documentId: documentId, enabled: autosaveEnabled, initialJson: initialContentJsonRef.current, confirmMessage: strings.editor.restoreDraftConfirm })] }) }) }) })] }) }));
96
167
  });
@@ -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":"AAIA,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,+BAgE7G"}
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('Enter a valid http(s), mailto, tel or relative URL');
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 ? 'Edit link' : 'Insert link', centered: true, size: "sm", children: _jsxs("form", { onSubmit: handleSubmit, children: [_jsx(TextInput, { label: "URL", placeholder: "https://example.com", value: url, onChange: (e) => {
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: "Text", placeholder: "Link text (defaults to the URL)", 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: "Remove link" })) : (_jsx("span", {})), _jsxs(Group, { gap: "xs", children: [_jsx(Button, { variant: "default", onClick: onClose, children: "Cancel" }), _jsx(Button, { type: "submit", children: "Apply" })] })] })] }) }));
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":"AA0CA,OAAO,KAAK,EAAE,2BAA2B,EAAE,MAAM,2BAA2B,CAAC;AAC7E,OAAO,EAA6D,KAAK,UAAU,EAAE,MAAM,UAAU,CAAC;AAmPtG,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;CAC3B;AAED,wBAAgB,OAAO,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,QAAQ,EAAE,WAAkC,EAAE,SAAiB,EAAE,EAAE,YAAY,+BAqmBjI"}
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;AAqUvD,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,+BA8wBhJ"}