@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
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Autosave-to-localStorage drafts (lexical-editor-spec.md §6.2), namespaced
3
+ * by `documentId` (§6.1). Every function is a no-op (never throws) when
4
+ * `localStorage` is unavailable (SSR, privacy mode) or the stored value is
5
+ * corrupt — autosave is best-effort, never a hard dependency.
6
+ */
7
+ const KEY_PREFIX = 'likhari:draft:';
8
+ export function draftKey(documentId) {
9
+ return `${KEY_PREFIX}${documentId}`;
10
+ }
11
+ function getLocalStorage() {
12
+ try {
13
+ return typeof localStorage === 'undefined' ? null : localStorage;
14
+ }
15
+ catch {
16
+ // Some browsers throw on accessing localStorage in certain privacy modes.
17
+ return null;
18
+ }
19
+ }
20
+ export function readDraft(documentId) {
21
+ const storage = getLocalStorage();
22
+ if (!storage)
23
+ return null;
24
+ try {
25
+ const raw = storage.getItem(draftKey(documentId));
26
+ if (!raw)
27
+ return null;
28
+ const parsed = JSON.parse(raw);
29
+ if (typeof parsed.json !== 'string' || typeof parsed.savedAt !== 'number' || parsed.version !== 1)
30
+ return null;
31
+ return parsed;
32
+ }
33
+ catch {
34
+ return null;
35
+ }
36
+ }
37
+ /**
38
+ * Best-effort debounced write. Silently skips (rather than throwing) when
39
+ * `json` exceeds `maxBytes` — localStorage has a shared ~5-10MB per-origin
40
+ * ceiling (§6.2), so a single large document (e.g. embedded base64 images)
41
+ * must not be allowed to autosave past a caller-configured limit — or when
42
+ * the write itself fails (quota exceeded, privacy mode).
43
+ */
44
+ export function writeDraft(documentId, json, maxBytes) {
45
+ const storage = getLocalStorage();
46
+ if (!storage)
47
+ return;
48
+ // .length (UTF-16 code units) over-estimates UTF-8 byte size for any
49
+ // non-Latin text, which only makes this check more conservative — fine
50
+ // for a soft cap that doesn't need to be exact.
51
+ if (json.length > maxBytes)
52
+ return;
53
+ const draft = { json, savedAt: Date.now(), version: 1 };
54
+ try {
55
+ storage.setItem(draftKey(documentId), JSON.stringify(draft));
56
+ }
57
+ catch {
58
+ // Quota exceeded or similar — autosave is best-effort.
59
+ }
60
+ }
61
+ export function clearDraft(documentId) {
62
+ const storage = getLocalStorage();
63
+ if (!storage)
64
+ return;
65
+ try {
66
+ storage.removeItem(draftKey(documentId));
67
+ }
68
+ catch {
69
+ // ignore
70
+ }
71
+ }
@@ -0,0 +1,16 @@
1
+ export interface AutosaveRestorePluginProps {
2
+ documentId?: string;
3
+ enabled: boolean;
4
+ /** The Lexical JSON the editor was constructed with (stringified), or
5
+ * undefined for a blank document — compared against a stored draft so an
6
+ * identical draft doesn't trigger a pointless prompt. */
7
+ initialJson?: string;
8
+ confirmMessage: string;
9
+ }
10
+ /**
11
+ * On mount, offers to restore a newer localStorage draft over the initial
12
+ * content passed in (lexical-editor-spec.md §6.2) — data-loss prevention is
13
+ * the explicit goal, so this never silently picks one over the other.
14
+ */
15
+ export declare function AutosaveRestorePlugin({ documentId, enabled, initialJson, confirmMessage }: AutosaveRestorePluginProps): null;
16
+ //# sourceMappingURL=AutosaveRestorePlugin.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"AutosaveRestorePlugin.d.ts","sourceRoot":"","sources":["../../src/plugins/AutosaveRestorePlugin.tsx"],"names":[],"mappings":"AAIA,MAAM,WAAW,0BAA0B;IACzC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,OAAO,CAAC;IACjB;;6DAEyD;IACzD,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,cAAc,EAAE,MAAM,CAAC;CACxB;AAED;;;;GAIG;AACH,wBAAgB,qBAAqB,CAAC,EAAE,UAAU,EAAE,OAAO,EAAE,WAAW,EAAE,cAAc,EAAE,EAAE,0BAA0B,QAwBrH"}
@@ -0,0 +1,33 @@
1
+ import { useEffect, useRef } from 'react';
2
+ import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
3
+ import { readDraft } from '../persistence/draftStorage.js';
4
+ /**
5
+ * On mount, offers to restore a newer localStorage draft over the initial
6
+ * content passed in (lexical-editor-spec.md §6.2) — data-loss prevention is
7
+ * the explicit goal, so this never silently picks one over the other.
8
+ */
9
+ export function AutosaveRestorePlugin({ documentId, enabled, initialJson, confirmMessage }) {
10
+ const [editor] = useLexicalComposerContext();
11
+ const checked = useRef(false);
12
+ useEffect(() => {
13
+ if (checked.current || !enabled || !documentId)
14
+ return;
15
+ checked.current = true;
16
+ const draft = readDraft(documentId);
17
+ if (!draft || draft.json === initialJson)
18
+ return;
19
+ if (typeof window === 'undefined' || !window.confirm(confirmMessage))
20
+ return;
21
+ try {
22
+ const parsed = editor.parseEditorState(draft.json);
23
+ editor.setEditorState(parsed);
24
+ }
25
+ catch {
26
+ // Corrupt or incompatible draft (e.g. from a much older node schema) —
27
+ // ignore it and keep the initial content rather than crashing.
28
+ }
29
+ // Deliberately runs once on mount only.
30
+ // eslint-disable-next-line react-hooks/exhaustive-deps
31
+ }, []);
32
+ return null;
33
+ }
@@ -4,7 +4,23 @@ import './editor.css';
4
4
  export interface EditorThemeProviderProps {
5
5
  theme?: MantineThemeOverride;
6
6
  colorScheme?: 'light' | 'dark';
7
+ /** Overrides both Mantine's primary color and the `--editor-accent` /
8
+ * `--editor-accent-soft` CSS variables — see EditorRootProps.accentColor. */
9
+ accentColor?: string;
10
+ /**
11
+ * The id of the portal-anchor element EditorRoot renders inside
12
+ * `.likhari-root` (see PortalTargetContext.tsx). Scopes Mantine's OWN
13
+ * `data-mantine-color-scheme` attribute and CSS variables to that single
14
+ * element instead of the default `document.documentElement` — without
15
+ * this, Mantine's Modal/Menu/Select/Tooltip surfaces (background, text,
16
+ * border colors) would always render in Mantine's global light scheme
17
+ * regardless of `colorScheme`, since nothing ever told MantineProvider
18
+ * which scheme to use; forcing it at `document.documentElement` instead
19
+ * would work but would leak dark mode onto the rest of the host page and
20
+ * collide between multiple EditorRoot instances with different schemes.
21
+ */
22
+ scopeElementId?: string;
7
23
  children: React.ReactNode;
8
24
  }
9
- export declare function EditorThemeProvider({ theme, colorScheme, children }: EditorThemeProviderProps): import("react").JSX.Element;
25
+ export declare function EditorThemeProvider({ theme, colorScheme, accentColor, scopeElementId, children }: EditorThemeProviderProps): import("react").JSX.Element;
10
26
  //# sourceMappingURL=EditorThemeProvider.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"EditorThemeProvider.d.ts","sourceRoot":"","sources":["../../src/theme/EditorThemeProvider.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAmB,KAAK,oBAAoB,EAAE,MAAM,eAAe,CAAC;AAC3E,OAAO,0BAA0B,CAAC;AAGlC,OAAO,cAAc,CAAC;AAsBtB,MAAM,WAAW,wBAAwB;IACvC,KAAK,CAAC,EAAE,oBAAoB,CAAC;IAC7B,WAAW,CAAC,EAAE,OAAO,GAAG,MAAM,CAAC;IAC/B,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAC;CAC3B;AAED,wBAAgB,mBAAmB,CAAC,EAAE,KAAK,EAAE,WAAW,EAAE,QAAQ,EAAE,EAAE,wBAAwB,+BAa7F"}
1
+ {"version":3,"file":"EditorThemeProvider.d.ts","sourceRoot":"","sources":["../../src/theme/EditorThemeProvider.tsx"],"names":[],"mappings":"AACA,OAAO,EAAmB,KAAK,oBAAoB,EAAE,MAAM,eAAe,CAAC;AAC3E,OAAO,0BAA0B,CAAC;AAIlC,OAAO,cAAc,CAAC;AAsBtB,MAAM,WAAW,wBAAwB;IACvC,KAAK,CAAC,EAAE,oBAAoB,CAAC;IAC7B,WAAW,CAAC,EAAE,OAAO,GAAG,MAAM,CAAC;IAC/B;iFAC6E;IAC7E,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;;;;;;;;;OAWG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAC;CAC3B;AAED,wBAAgB,mBAAmB,CAAC,EAAE,KAAK,EAAE,WAAW,EAAE,WAAW,EAAE,cAAc,EAAE,QAAQ,EAAE,EAAE,wBAAwB,+BA+C1H"}
@@ -1,8 +1,10 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { useMemo } from 'react';
2
3
  import { MantineProvider } from '@mantine/core';
3
4
  import '@mantine/core/styles.css';
4
5
  import { buildThemeCss } from '@inshapardaz/likhari-core';
5
6
  import { defaultMantineTheme } from './mantineTheme.js';
7
+ import { generateAccentCssVars, generateAccentShades } from './accentColor.js';
6
8
  import './editor.css';
7
9
  let cssInjected = false;
8
10
  // Scopes the --editor-* variables to the wrapper div below, which is what
@@ -21,8 +23,28 @@ function useThemeCssInjection() {
21
23
  cssInjected = true;
22
24
  }
23
25
  }
24
- export function EditorThemeProvider({ theme, colorScheme, children }) {
26
+ export function EditorThemeProvider({ theme, colorScheme, accentColor, scopeElementId, children }) {
25
27
  useThemeCssInjection();
28
+ // Mantine wants a 10-shade array; a host only supplies one color, so a
29
+ // small local helper (accentColor.ts) mixes it towards white/black for
30
+ // the rest of the ramp. If `theme` is ALSO supplied, its own `colors`
31
+ // still wins for anything it explicitly sets (shallow-merged per color
32
+ // key) — the accent override alone isn't worth a generic deep-merge here.
33
+ const resolvedTheme = useMemo(() => {
34
+ const shades = accentColor ? generateAccentShades(accentColor) : null;
35
+ const base = shades ? { ...defaultMantineTheme, colors: { ...defaultMantineTheme.colors, likhariAccent: shades } } : defaultMantineTheme;
36
+ if (!theme)
37
+ return base;
38
+ return { ...base, ...theme, colors: { ...base.colors, ...theme.colors } };
39
+ }, [accentColor, theme]);
40
+ // Inline style wins over the injected stylesheet rule (same element, same
41
+ // specificity otherwise) — only set when accentColor is actually given,
42
+ // so the default static tokens keep applying via the stylesheet the rest
43
+ // of the time.
44
+ const accentVars = accentColor ? generateAccentCssVars(accentColor, colorScheme === 'dark' ? 'dark' : 'light') : null;
45
+ const accentStyle = accentVars
46
+ ? { '--editor-accent': accentVars.accent, '--editor-accent-soft': accentVars.accentSoft }
47
+ : {};
26
48
  return (
27
49
  // display: contents keeps this wrapper out of the layout tree — CSS
28
50
  // custom properties still cascade to children through it (inheritance
@@ -30,5 +52,5 @@ export function EditorThemeProvider({ theme, colorScheme, children }) {
30
52
  // element and the host's actual parent, so EditorRoot's `height` prop
31
53
  // (e.g. '100%') resolves against the host's container as if this div
32
54
  // weren't there at all.
33
- _jsx("div", { className: "likhari-theme-scope", "data-editor-color-scheme": colorScheme, style: { display: 'contents' }, children: _jsx(MantineProvider, { theme: theme ?? defaultMantineTheme, children: children }) }));
55
+ _jsx("div", { className: "likhari-theme-scope", "data-editor-color-scheme": colorScheme, style: { display: 'contents', ...accentStyle }, children: _jsx(MantineProvider, { theme: resolvedTheme, forceColorScheme: colorScheme, defaultColorScheme: "auto", getRootElement: scopeElementId ? () => document.getElementById(scopeElementId) ?? undefined : undefined, cssVariablesSelector: scopeElementId ? `#${CSS.escape(scopeElementId)}` : undefined, children: children }) }));
34
56
  }
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Derives Mantine's 10-shade color array and the editor's own
3
+ * `--editor-accent` / `--editor-accent-soft` pair from a single
4
+ * host-supplied accent color (EditorRootProps.accentColor). No color
5
+ * library needed — a few lines of hex parsing plus RGB lerp is enough for a
6
+ * usable (not professionally calibrated) ramp.
7
+ */
8
+ interface Rgb {
9
+ r: number;
10
+ g: number;
11
+ b: number;
12
+ }
13
+ /** Parses a `#rgb` or `#rrggbb` hex color. Returns null for anything else —
14
+ * callers fall back to the static default tokens rather than throwing on a
15
+ * bad host-supplied value. */
16
+ export declare function hexToRgb(hex: string): Rgb | null;
17
+ export declare function rgbToHex({ r, g, b }: Rgb): string;
18
+ /** Linear-interpolates two colors; t=0 -> a, t=1 -> b. */
19
+ export declare function mix(a: Rgb, b: Rgb, t: number): Rgb;
20
+ /** Mantine's color-tuple shape: exactly 10 shades. */
21
+ export type AccentShades = readonly [string, string, string, string, string, string, string, string, string, string];
22
+ /**
23
+ * A 10-shade Mantine color array for `theme.colors.likhariAccent`, mixing
24
+ * the base color towards white for the lighter shades (0-4ish) and towards
25
+ * black for the darker ones (6-9ish) — shade 6 (Mantine's default "filled"
26
+ * shade) is the exact input color, matching what the static
27
+ * `likhariAccent` ramp already did for the default tokens.
28
+ */
29
+ export declare function generateAccentShades(hex: string): AccentShades | null;
30
+ /**
31
+ * The editor canvas/toolbar's own `--editor-accent` / `--editor-accent-soft`
32
+ * pair for a host-supplied accent color, mirroring the relationship between
33
+ * `LIGHT_TOKENS`/`DARK_TOKENS`'s `accent`/`accentSoft` (packages/core/src/theme/tokens.ts):
34
+ * light mode keeps the accent as given and derives a light tint for "soft";
35
+ * dark mode lightens the accent itself (for contrast against a dark canvas)
36
+ * and derives a dark tint for "soft".
37
+ */
38
+ export declare function generateAccentCssVars(hex: string, mode: 'light' | 'dark'): {
39
+ accent: string;
40
+ accentSoft: string;
41
+ } | null;
42
+ export {};
43
+ //# sourceMappingURL=accentColor.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"accentColor.d.ts","sourceRoot":"","sources":["../../src/theme/accentColor.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,UAAU,GAAG;IACX,CAAC,EAAE,MAAM,CAAC;IACV,CAAC,EAAE,MAAM,CAAC;IACV,CAAC,EAAE,MAAM,CAAC;CACX;AAED;;8BAE8B;AAC9B,wBAAgB,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,GAAG,IAAI,CAehD;AAQD,wBAAgB,QAAQ,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,EAAE,GAAG,GAAG,MAAM,CAEjD;AAED,0DAA0D;AAC1D,wBAAgB,GAAG,CAAC,CAAC,EAAE,GAAG,EAAE,CAAC,EAAE,GAAG,EAAE,CAAC,EAAE,MAAM,GAAG,GAAG,CAMlD;AAKD,sDAAsD;AACtD,MAAM,MAAM,YAAY,GAAG,SAAS,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC;AAErH;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,MAAM,GAAG,YAAY,GAAG,IAAI,CAQrE;AAED;;;;;;;GAOG;AACH,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,GAAG,MAAM,GAAG;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAaxH"}
@@ -0,0 +1,84 @@
1
+ /**
2
+ * Derives Mantine's 10-shade color array and the editor's own
3
+ * `--editor-accent` / `--editor-accent-soft` pair from a single
4
+ * host-supplied accent color (EditorRootProps.accentColor). No color
5
+ * library needed — a few lines of hex parsing plus RGB lerp is enough for a
6
+ * usable (not professionally calibrated) ramp.
7
+ */
8
+ /** Parses a `#rgb` or `#rrggbb` hex color. Returns null for anything else —
9
+ * callers fall back to the static default tokens rather than throwing on a
10
+ * bad host-supplied value. */
11
+ export function hexToRgb(hex) {
12
+ const cleaned = hex.trim().replace(/^#/, '');
13
+ const full = cleaned.length === 3
14
+ ? cleaned
15
+ .split('')
16
+ .map((c) => c + c)
17
+ .join('')
18
+ : cleaned;
19
+ if (!/^[0-9a-fA-F]{6}$/.test(full))
20
+ return null;
21
+ return {
22
+ r: parseInt(full.slice(0, 2), 16),
23
+ g: parseInt(full.slice(2, 4), 16),
24
+ b: parseInt(full.slice(4, 6), 16),
25
+ };
26
+ }
27
+ function toHex(n) {
28
+ return Math.round(Math.min(255, Math.max(0, n)))
29
+ .toString(16)
30
+ .padStart(2, '0');
31
+ }
32
+ export function rgbToHex({ r, g, b }) {
33
+ return `#${toHex(r)}${toHex(g)}${toHex(b)}`;
34
+ }
35
+ /** Linear-interpolates two colors; t=0 -> a, t=1 -> b. */
36
+ export function mix(a, b, t) {
37
+ return {
38
+ r: a.r + (b.r - a.r) * t,
39
+ g: a.g + (b.g - a.g) * t,
40
+ b: a.b + (b.b - a.b) * t,
41
+ };
42
+ }
43
+ const WHITE = { r: 255, g: 255, b: 255 };
44
+ const BLACK = { r: 0, g: 0, b: 0 };
45
+ /**
46
+ * A 10-shade Mantine color array for `theme.colors.likhariAccent`, mixing
47
+ * the base color towards white for the lighter shades (0-4ish) and towards
48
+ * black for the darker ones (6-9ish) — shade 6 (Mantine's default "filled"
49
+ * shade) is the exact input color, matching what the static
50
+ * `likhariAccent` ramp already did for the default tokens.
51
+ */
52
+ export function generateAccentShades(hex) {
53
+ const base = hexToRgb(hex);
54
+ if (!base)
55
+ return null;
56
+ // How far to mix towards white (positive) or black (negative) per shade,
57
+ // relative to shade 6 = the exact input color.
58
+ const mixAmount = [0.88, 0.72, 0.56, 0.4, 0.22, 0.1, 0, -0.14, -0.28, -0.42];
59
+ const shades = mixAmount.map((t) => rgbToHex(mix(base, t >= 0 ? WHITE : BLACK, Math.abs(t))));
60
+ return shades;
61
+ }
62
+ /**
63
+ * The editor canvas/toolbar's own `--editor-accent` / `--editor-accent-soft`
64
+ * pair for a host-supplied accent color, mirroring the relationship between
65
+ * `LIGHT_TOKENS`/`DARK_TOKENS`'s `accent`/`accentSoft` (packages/core/src/theme/tokens.ts):
66
+ * light mode keeps the accent as given and derives a light tint for "soft";
67
+ * dark mode lightens the accent itself (for contrast against a dark canvas)
68
+ * and derives a dark tint for "soft".
69
+ */
70
+ export function generateAccentCssVars(hex, mode) {
71
+ const base = hexToRgb(hex);
72
+ if (!base)
73
+ return null;
74
+ if (mode === 'dark') {
75
+ return {
76
+ accent: rgbToHex(mix(base, WHITE, 0.45)),
77
+ accentSoft: rgbToHex(mix(base, BLACK, 0.75)),
78
+ };
79
+ }
80
+ return {
81
+ accent: hex,
82
+ accentSoft: rgbToHex(mix(base, WHITE, 0.88)),
83
+ };
84
+ }
@@ -19,6 +19,12 @@
19
19
  its own height so only the canvas below scrolls. */
20
20
  .likhari-toolbar {
21
21
  display: flex;
22
+ /* Containing block for the hidden width-measuring clone (Toolbar.tsx):
23
+ without this, its position: absolute would resolve against the
24
+ viewport instead, and its intentionally-unwrapped (possibly very wide)
25
+ content could inflate the host page's scroll area instead of being
26
+ clipped by .likhari-root's overflow: hidden. */
27
+ position: relative;
22
28
  flex-shrink: 0;
23
29
  align-items: center;
24
30
  gap: 8px;
@@ -33,6 +39,18 @@
33
39
  row-gap: 4px;
34
40
  }
35
41
 
42
+ /* Empty portal-anchor (EditorRoot.tsx) — exists only so Mantine's portaled
43
+ Menu/Modal/Select-combobox/Tooltip content has a DOM ancestor inside
44
+ .likhari-root/.likhari-theme-scope to inherit --editor-* vars and `dir`
45
+ from; it must take up no space and never intercept pointer events. */
46
+ .likhari-portal-target {
47
+ position: absolute;
48
+ width: 0;
49
+ height: 0;
50
+ overflow: visible;
51
+ pointer-events: none;
52
+ }
53
+
36
54
  .likhari-toolbar-group {
37
55
  display: flex;
38
56
  align-items: center;
@@ -235,6 +253,113 @@
235
253
  margin: 1.5em 0;
236
254
  }
237
255
 
256
+ /* Images — block-level figure; the caption is static text (edited in the
257
+ image's dialog), and a selected image gets a corner resize handle. */
258
+ .likhari-image-block {
259
+ margin: 0 0 1em 0;
260
+ }
261
+
262
+ .likhari-image-figure {
263
+ display: block;
264
+ margin: 0;
265
+ text-align: center;
266
+ }
267
+
268
+ .likhari-image-wrap {
269
+ position: relative;
270
+ display: inline-block;
271
+ max-width: 100%;
272
+ line-height: 0;
273
+ }
274
+
275
+ .likhari-image {
276
+ display: block;
277
+ max-width: 100%;
278
+ height: auto;
279
+ border-radius: 4px;
280
+ cursor: default;
281
+ }
282
+
283
+ .likhari-image-figure[data-selected='true'] .likhari-image {
284
+ outline: 2px solid var(--editor-accent);
285
+ outline-offset: 2px;
286
+ }
287
+
288
+ .likhari-image-resize-handle {
289
+ position: absolute;
290
+ right: -6px;
291
+ bottom: -6px;
292
+ width: 12px;
293
+ height: 12px;
294
+ border: 2px solid var(--editor-paper);
295
+ border-radius: 3px;
296
+ background: var(--editor-accent);
297
+ cursor: nwse-resize;
298
+ touch-action: none;
299
+ }
300
+
301
+ .likhari-image-caption {
302
+ margin-top: 6px;
303
+ color: var(--editor-ink-soft);
304
+ font-size: 0.85em;
305
+ line-height: 1.4;
306
+ text-align: center;
307
+ }
308
+
309
+ /* Image dialog — crop stage. The selection dims everything outside it. */
310
+ .likhari-cropper-stage {
311
+ position: relative;
312
+ display: inline-block;
313
+ align-self: center;
314
+ max-width: 100%;
315
+ padding: 8px;
316
+ overflow: hidden;
317
+ line-height: 0;
318
+ cursor: crosshair;
319
+ touch-action: none;
320
+ user-select: none;
321
+ }
322
+
323
+ .likhari-cropper-frame {
324
+ position: relative;
325
+ display: inline-block;
326
+ max-width: 100%;
327
+ }
328
+
329
+ .likhari-cropper-image {
330
+ display: block;
331
+ max-width: 100%;
332
+ max-height: 320px;
333
+ }
334
+
335
+ .likhari-cropper-selection {
336
+ position: absolute;
337
+ border: 2px solid #fff;
338
+ outline: 1px solid rgba(0, 0, 0, 0.5);
339
+ box-shadow: 0 0 0 9999px rgba(0, 0, 0, 0.5);
340
+ cursor: move;
341
+ }
342
+
343
+ .likhari-cropper-handle {
344
+ position: absolute;
345
+ width: 12px;
346
+ height: 12px;
347
+ border: 1px solid rgba(0, 0, 0, 0.6);
348
+ border-radius: 2px;
349
+ background: #fff;
350
+ /* 12px handles on small selections would overlap; keep them on the border. */
351
+ transform: translate(-50%, -50%);
352
+ }
353
+
354
+ .likhari-cropper-handle[data-handle='nw'] { left: 0; top: 0; cursor: nwse-resize; }
355
+ .likhari-cropper-handle[data-handle='n'] { left: 50%; top: 0; cursor: ns-resize; }
356
+ .likhari-cropper-handle[data-handle='ne'] { left: 100%; top: 0; cursor: nesw-resize; }
357
+ .likhari-cropper-handle[data-handle='e'] { left: 100%; top: 50%; cursor: ew-resize; }
358
+ .likhari-cropper-handle[data-handle='se'] { left: 100%; top: 100%; cursor: nwse-resize; }
359
+ .likhari-cropper-handle[data-handle='s'] { left: 50%; top: 100%; cursor: ns-resize; }
360
+ .likhari-cropper-handle[data-handle='sw'] { left: 0; top: 100%; cursor: nesw-resize; }
361
+ .likhari-cropper-handle[data-handle='w'] { left: 0; top: 50%; cursor: ew-resize; }
362
+
238
363
  /* Link menu: the URL item shows one truncated line. */
239
364
  .likhari-link-menu-url {
240
365
  max-width: 100%;
@@ -1 +1 @@
1
- {"version":3,"file":"editorTheme.d.ts","sourceRoot":"","sources":["../../src/theme/editorTheme.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,SAAS,CAAC;AAElD;;;;GAIG;AACH,eAAO,MAAM,WAAW,EAAE,kBA6BzB,CAAC"}
1
+ {"version":3,"file":"editorTheme.d.ts","sourceRoot":"","sources":["../../src/theme/editorTheme.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,SAAS,CAAC;AAElD;;;;GAIG;AACH,eAAO,MAAM,WAAW,EAAE,kBA8BzB,CAAC"}
@@ -32,4 +32,5 @@ export const editorTheme = {
32
32
  superscript: 'likhari-text-superscript',
33
33
  },
34
34
  hr: 'likhari-hr',
35
+ image: 'likhari-image-block',
35
36
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inshapardaz/likhari-react",
3
- "version": "0.1.6",
3
+ "version": "0.1.7-dev.75",
4
4
  "description": "React implementation of the Likhari editor, built on @lexical/react.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -13,6 +13,7 @@
13
13
  "exports": {
14
14
  ".": {
15
15
  "types": "./dist/index.d.ts",
16
+ "development": "./src/index.ts",
16
17
  "import": "./dist/index.js"
17
18
  },
18
19
  "./package.json": "./package.json"