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

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 (62) hide show
  1. package/dist/EditorRoot.d.ts +25 -1
  2. package/dist/EditorRoot.d.ts.map +1 -1
  3. package/dist/EditorRoot.js +24 -5
  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 +158 -0
  16. package/dist/i18n/strings.d.ts.map +1 -0
  17. package/dist/i18n/strings.js +425 -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/theme/EditorThemeProvider.d.ts +17 -2
  54. package/dist/theme/EditorThemeProvider.d.ts.map +1 -1
  55. package/dist/theme/EditorThemeProvider.js +27 -3
  56. package/dist/theme/accentColor.d.ts +43 -0
  57. package/dist/theme/accentColor.d.ts.map +1 -0
  58. package/dist/theme/accentColor.js +84 -0
  59. package/dist/theme/editor.css +125 -0
  60. package/dist/theme/editorTheme.d.ts.map +1 -1
  61. package/dist/theme/editorTheme.js +1 -0
  62. package/package.json +9 -3
@@ -0,0 +1,133 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { DecoratorNode, } from 'lexical';
3
+ import { ImageComponent } from './ImageComponent.js';
4
+ /**
5
+ * Block-level image (editor-architecture-design.md §3.5). The caption is a
6
+ * plain string (null = no caption) edited through the image's right-click menu
7
+ * / edit dialog — not the nested sub-editor the architecture doc leaves open —
8
+ * which keeps every edit in the parent editor's undo history.
9
+ */
10
+ export class ImageNode extends DecoratorNode {
11
+ static getType() {
12
+ return 'image';
13
+ }
14
+ static clone(node) {
15
+ return new ImageNode(node.__src, node.__altText, node.__caption, node.__linkType, node.__width, node.__height, node.__key);
16
+ }
17
+ static importJSON(json) {
18
+ return $createImageNode({
19
+ src: json.src,
20
+ altText: json.altText,
21
+ caption: json.caption,
22
+ linkType: json.linkType,
23
+ width: json.width,
24
+ height: json.height,
25
+ });
26
+ }
27
+ constructor(src, altText = '', caption = null, linkType = 'linked', width = null, height = null, key) {
28
+ super(key);
29
+ this.__src = src;
30
+ this.__altText = altText;
31
+ this.__caption = caption;
32
+ this.__linkType = linkType;
33
+ this.__width = width;
34
+ this.__height = height;
35
+ }
36
+ exportJSON() {
37
+ return {
38
+ type: 'image',
39
+ version: 1,
40
+ src: this.__src,
41
+ altText: this.__altText,
42
+ caption: this.__caption,
43
+ linkType: this.__linkType,
44
+ width: this.__width,
45
+ height: this.__height,
46
+ };
47
+ }
48
+ /** `<figure><img><figcaption>` — the figure/figcaption semantics from spec §4.7. */
49
+ exportDOM() {
50
+ const figure = document.createElement('figure');
51
+ const img = document.createElement('img');
52
+ img.setAttribute('src', this.__src);
53
+ img.setAttribute('alt', this.__altText);
54
+ if (this.__width)
55
+ img.setAttribute('width', String(this.__width));
56
+ if (this.__height)
57
+ img.setAttribute('height', String(this.__height));
58
+ figure.appendChild(img);
59
+ if (this.__caption) {
60
+ const caption = document.createElement('figcaption');
61
+ caption.textContent = this.__caption;
62
+ figure.appendChild(caption);
63
+ }
64
+ return { element: figure };
65
+ }
66
+ createDOM(config) {
67
+ const div = document.createElement('div');
68
+ const className = config.theme.image;
69
+ if (typeof className === 'string')
70
+ div.className = className;
71
+ return div;
72
+ }
73
+ updateDOM() {
74
+ return false;
75
+ }
76
+ isInline() {
77
+ return false;
78
+ }
79
+ getSrc() {
80
+ return this.__src;
81
+ }
82
+ getAltText() {
83
+ return this.__altText;
84
+ }
85
+ getCaption() {
86
+ return this.__caption;
87
+ }
88
+ getLinkType() {
89
+ return this.__linkType;
90
+ }
91
+ getWidth() {
92
+ return this.__width;
93
+ }
94
+ getHeight() {
95
+ return this.__height;
96
+ }
97
+ setCaption(caption) {
98
+ const writable = this.getWritable();
99
+ writable.__caption = caption || null;
100
+ return writable;
101
+ }
102
+ setAltText(altText) {
103
+ const writable = this.getWritable();
104
+ writable.__altText = altText;
105
+ return writable;
106
+ }
107
+ /** Points the node at new image data (a replaced URL/upload, or edited pixels). */
108
+ setSource(src, linkType) {
109
+ const writable = this.getWritable();
110
+ writable.__src = src;
111
+ writable.__linkType = linkType;
112
+ return writable;
113
+ }
114
+ /** Display size in px; null = the image's natural size. */
115
+ setDimensions(width, height) {
116
+ const writable = this.getWritable();
117
+ writable.__width = width;
118
+ writable.__height = height;
119
+ return writable;
120
+ }
121
+ getTextContent() {
122
+ return this.__altText || this.__caption || '';
123
+ }
124
+ decorate() {
125
+ return (_jsx(ImageComponent, { nodeKey: this.getKey(), src: this.__src, altText: this.__altText, caption: this.__caption, linkType: this.__linkType, width: this.__width, height: this.__height }));
126
+ }
127
+ }
128
+ export function $createImageNode({ src, altText, caption, linkType, width, height, key }) {
129
+ return new ImageNode(src, altText, caption ?? null, linkType, width ?? null, height ?? null, key);
130
+ }
131
+ export function $isImageNode(node) {
132
+ return node instanceof ImageNode;
133
+ }
@@ -0,0 +1,24 @@
1
+ /** Image settings from `EditorRoot` (feature config + upload handler), shared
2
+ * with the toolbar's insert dialog and with each image's own edit menu/dialog. */
3
+ export interface ImageOptions {
4
+ /** `images.linked` — images may reference an external URL. */
5
+ allowLinked: boolean;
6
+ /** `images.embedded` — images may be uploaded. */
7
+ allowEmbedded: boolean;
8
+ /** `images.caption` — images may have a caption. */
9
+ allowCaption: boolean;
10
+ /** `images.maxSizeMB` */
11
+ maxSizeMB: number;
12
+ /** Host upload handler; without it, uploads are embedded as base64 data URIs. */
13
+ onImageUpload?: (file: File) => Promise<string>;
14
+ /** Host override for downloading a linked image's bytes, used when converting
15
+ * a link to an embedded image or applying a crop/rotate/flip. Without it, the
16
+ * browser fetches the URL directly, which fails for any server that doesn't
17
+ * send CORS headers. A host can provide this to route the request through
18
+ * its own backend (or a CORS proxy) instead, since that restriction can only
19
+ * be worked around server-side, never from the browser alone. */
20
+ fetchImage?: (url: string) => Promise<Blob>;
21
+ }
22
+ export declare const ImageOptionsContext: import("react").Context<ImageOptions>;
23
+ export declare function useImageOptions(): ImageOptions;
24
+ //# sourceMappingURL=ImageOptionsContext.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ImageOptionsContext.d.ts","sourceRoot":"","sources":["../../src/image/ImageOptionsContext.tsx"],"names":[],"mappings":"AAEA;kFACkF;AAClF,MAAM,WAAW,YAAY;IAC3B,8DAA8D;IAC9D,WAAW,EAAE,OAAO,CAAC;IACrB,kDAAkD;IAClD,aAAa,EAAE,OAAO,CAAC;IACvB,oDAAoD;IACpD,YAAY,EAAE,OAAO,CAAC;IACtB,yBAAyB;IACzB,SAAS,EAAE,MAAM,CAAC;IAClB,iFAAiF;IACjF,aAAa,CAAC,EAAE,CAAC,IAAI,EAAE,IAAI,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;IAChD;;;;;qEAKiE;IACjE,UAAU,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CAC7C;AAED,eAAO,MAAM,mBAAmB,uCAK9B,CAAC;AAEH,wBAAgB,eAAe,IAAI,YAAY,CAE9C"}
@@ -0,0 +1,10 @@
1
+ import { createContext, useContext } from 'react';
2
+ export const ImageOptionsContext = createContext({
3
+ allowLinked: true,
4
+ allowEmbedded: true,
5
+ allowCaption: true,
6
+ maxSizeMB: 5,
7
+ });
8
+ export function useImageOptions() {
9
+ return useContext(ImageOptionsContext);
10
+ }
@@ -0,0 +1,35 @@
1
+ import type { CropRect } from './imageEdit.js';
2
+ export type CropHandle = 'n' | 's' | 'e' | 'w' | 'ne' | 'nw' | 'se' | 'sw';
3
+ /** Smallest selection, as a fraction of the image. */
4
+ export declare const MIN_CROP = 0.02;
5
+ /** Size of the image's on-screen box in px; needed to keep an aspect ratio,
6
+ * since rectangle coordinates are fractions. */
7
+ export interface Box {
8
+ width: number;
9
+ height: number;
10
+ }
11
+ /**
12
+ * A new selection dragged from `anchor` to `point` (both fractions). With a
13
+ * `ratio` (width / height, in pixels) the selection keeps it, shrinking to stay
14
+ * inside the image.
15
+ */
16
+ export declare function rectFromDrag(anchor: {
17
+ x: number;
18
+ y: number;
19
+ }, point: {
20
+ x: number;
21
+ y: number;
22
+ }, ratio: number | null, box: Box): CropRect;
23
+ /** Moves a selection by (dx, dy) fractions, keeping it inside the image. */
24
+ export declare function moveRect(rect: CropRect, dx: number, dy: number): CropRect;
25
+ /**
26
+ * Resizes a selection by dragging one of its handles to `point` (a fraction).
27
+ * Corners anchor the opposite corner; edges move one side. With a `ratio` the
28
+ * aspect is kept (an edge handle then grows the other dimension around the
29
+ * selection's centre). Never smaller than MIN_CROP, never outside the image.
30
+ */
31
+ export declare function resizeRect(rect: CropRect, handle: CropHandle, point: {
32
+ x: number;
33
+ y: number;
34
+ }, ratio: number | null, box: Box): CropRect;
35
+ //# sourceMappingURL=cropMath.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cropMath.d.ts","sourceRoot":"","sources":["../../src/image/cropMath.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAE5C,MAAM,MAAM,UAAU,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,CAAC;AAE3E,sDAAsD;AACtD,eAAO,MAAM,QAAQ,OAAO,CAAC;AAE7B;gDACgD;AAChD,MAAM,WAAW,GAAG;IAClB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;CAChB;AAID;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE;IAAE,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAA;CAAE,EAAE,KAAK,EAAE;IAAE,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAA;CAAE,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,EAAE,GAAG,EAAE,GAAG,GAAG,QAAQ,CAexI;AAED,4EAA4E;AAC5E,wBAAgB,QAAQ,CAAC,IAAI,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,QAAQ,CAEzE;AAED;;;;;GAKG;AACH,wBAAgB,UAAU,CAAC,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,UAAU,EAAE,KAAK,EAAE;IAAE,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAA;CAAE,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,EAAE,GAAG,EAAE,GAAG,GAAG,QAAQ,CAwDxI"}
@@ -0,0 +1,92 @@
1
+ /** Smallest selection, as a fraction of the image. */
2
+ export const MIN_CROP = 0.02;
3
+ const clamp = (v, min = 0, max = 1) => Math.min(Math.max(v, min), max);
4
+ /**
5
+ * A new selection dragged from `anchor` to `point` (both fractions). With a
6
+ * `ratio` (width / height, in pixels) the selection keeps it, shrinking to stay
7
+ * inside the image.
8
+ */
9
+ export function rectFromDrag(anchor, point, ratio, box) {
10
+ let dx = clamp(point.x) - anchor.x;
11
+ let dy = clamp(point.y) - anchor.y;
12
+ if (ratio) {
13
+ let widthPx = Math.abs(dx) * box.width;
14
+ let heightPx = widthPx / ratio;
15
+ const maxHeightPx = (dy >= 0 ? 1 - anchor.y : anchor.y) * box.height;
16
+ if (heightPx > maxHeightPx) {
17
+ heightPx = maxHeightPx;
18
+ widthPx = heightPx * ratio;
19
+ }
20
+ dx = (dx >= 0 ? 1 : -1) * (widthPx / box.width);
21
+ dy = (dy >= 0 ? 1 : -1) * (heightPx / box.height);
22
+ }
23
+ return { x: Math.min(anchor.x, anchor.x + dx), y: Math.min(anchor.y, anchor.y + dy), w: Math.abs(dx), h: Math.abs(dy) };
24
+ }
25
+ /** Moves a selection by (dx, dy) fractions, keeping it inside the image. */
26
+ export function moveRect(rect, dx, dy) {
27
+ return { ...rect, x: clamp(rect.x + dx, 0, 1 - rect.w), y: clamp(rect.y + dy, 0, 1 - rect.h) };
28
+ }
29
+ /**
30
+ * Resizes a selection by dragging one of its handles to `point` (a fraction).
31
+ * Corners anchor the opposite corner; edges move one side. With a `ratio` the
32
+ * aspect is kept (an edge handle then grows the other dimension around the
33
+ * selection's centre). Never smaller than MIN_CROP, never outside the image.
34
+ */
35
+ export function resizeRect(rect, handle, point, ratio, box) {
36
+ const left = rect.x;
37
+ const right = rect.x + rect.w;
38
+ const top = rect.y;
39
+ const bottom = rect.y + rect.h;
40
+ const px = clamp(point.x);
41
+ const py = clamp(point.y);
42
+ if (handle.length === 2) {
43
+ // Corner: the opposite corner stays put.
44
+ const anchor = { x: handle.includes('w') ? right : left, y: handle.includes('n') ? bottom : top };
45
+ return rectFromDrag(anchor, { x: px, y: py }, ratio, box);
46
+ }
47
+ const horizontal = handle === 'e' || handle === 'w';
48
+ if (!ratio) {
49
+ const next = { left, right, top, bottom };
50
+ if (handle === 'w')
51
+ next.left = Math.min(px, right - MIN_CROP);
52
+ if (handle === 'e')
53
+ next.right = Math.max(px, left + MIN_CROP);
54
+ if (handle === 'n')
55
+ next.top = Math.min(py, bottom - MIN_CROP);
56
+ if (handle === 's')
57
+ next.bottom = Math.max(py, top + MIN_CROP);
58
+ return { x: next.left, y: next.top, w: next.right - next.left, h: next.bottom - next.top };
59
+ }
60
+ // Edge with a locked ratio: the dragged side sets one dimension, the other follows.
61
+ const cx = left + rect.w / 2;
62
+ const cy = top + rect.h / 2;
63
+ if (horizontal) {
64
+ const fixed = handle === 'e' ? left : right;
65
+ let w = Math.max(Math.abs(px - fixed), MIN_CROP);
66
+ let h = w * box.width / ratio / box.height;
67
+ if (h > 1) {
68
+ h = 1;
69
+ w = (h * box.height * ratio) / box.width;
70
+ }
71
+ // Stay inside the image horizontally.
72
+ const room = handle === 'e' ? 1 - left : right;
73
+ if (w > room) {
74
+ w = room;
75
+ h = (w * box.width) / ratio / box.height;
76
+ }
77
+ return { x: handle === 'e' ? left : right - w, y: clamp(cy - h / 2, 0, 1 - h), w, h };
78
+ }
79
+ const fixed = handle === 's' ? top : bottom;
80
+ let h = Math.max(Math.abs(py - fixed), MIN_CROP);
81
+ let w = (h * box.height * ratio) / box.width;
82
+ if (w > 1) {
83
+ w = 1;
84
+ h = (w * box.width) / ratio / box.height;
85
+ }
86
+ const room = handle === 's' ? 1 - top : bottom;
87
+ if (h > room) {
88
+ h = room;
89
+ w = (h * box.height * ratio) / box.width;
90
+ }
91
+ return { x: clamp(cx - w / 2, 0, 1 - w), y: handle === 's' ? top : bottom - h, w, h };
92
+ }
@@ -0,0 +1,49 @@
1
+ /** A crop rectangle as fractions (0–1) of the image's width and height. */
2
+ export interface CropRect {
3
+ x: number;
4
+ y: number;
5
+ w: number;
6
+ h: number;
7
+ }
8
+ /** Converts a fractional crop rectangle to whole pixels inside a width x height
9
+ * image, clamped so it never leaves the image and is at least 1px. */
10
+ export declare function toPixelRect(rect: CropRect, width: number, height: number): {
11
+ sx: number;
12
+ sy: number;
13
+ sw: number;
14
+ sh: number;
15
+ };
16
+ /** When one dimension changes with the aspect ratio locked, returns both. */
17
+ export declare function fitDimension(changed: 'width' | 'height', value: number, naturalWidth: number, naturalHeight: number): {
18
+ width: number;
19
+ height: number;
20
+ };
21
+ /** MIME type to re-encode an edited image as: its own if it is a lossy/lossless
22
+ * format canvas can write (JPEG, WebP, PNG), otherwise PNG (GIF, SVG, unknown). */
23
+ export declare function mimeFromSrc(src: string): string;
24
+ /** Approximate decoded size, in bytes, of a base64 data URI. */
25
+ export declare function dataUrlBytes(dataUrl: string): number;
26
+ export declare function dataUrlToFile(dataUrl: string, name: string): File;
27
+ export declare const CANNOT_DOWNLOAD = "Couldn't download this image (its server doesn't allow it). Download the file and upload it instead.";
28
+ /** Downloads an image and returns it as a base64 data URI, to embed it in the
29
+ * document. Without a `fetchBlob` override, needs the image's server to allow
30
+ * cross-origin reads (CORS) — a host can pass one to route the download
31
+ * through its own backend or a CORS proxy instead. */
32
+ export declare function fetchImageAsDataUrl(url: string, fetchBlob?: (url: string) => Promise<Blob>): Promise<string>;
33
+ export type PixelOp = {
34
+ type: 'rotate';
35
+ direction: 'cw' | 'ccw';
36
+ } | {
37
+ type: 'flip';
38
+ axis: 'horizontal' | 'vertical';
39
+ } | {
40
+ type: 'crop';
41
+ rect: CropRect;
42
+ };
43
+ /** Loads an image for canvas use. External URLs are requested with CORS so the
44
+ * canvas isn't tainted; a server that doesn't allow that makes this reject. */
45
+ export declare function loadImage(src: string): Promise<HTMLImageElement>;
46
+ export declare const CANNOT_EDIT_PIXELS = "This image can't be edited here (its server doesn't allow it). Upload the file instead to crop or rotate it.";
47
+ /** Applies a rotate / flip / crop to an image and returns the result as a data URI. */
48
+ export declare function applyPixelOp(src: string, op: PixelOp): Promise<string>;
49
+ //# sourceMappingURL=imageEdit.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"imageEdit.d.ts","sourceRoot":"","sources":["../../src/image/imageEdit.ts"],"names":[],"mappings":"AAEA,2EAA2E;AAC3E,MAAM,WAAW,QAAQ;IACvB,CAAC,EAAE,MAAM,CAAC;IACV,CAAC,EAAE,MAAM,CAAC;IACV,CAAC,EAAE,MAAM,CAAC;IACV,CAAC,EAAE,MAAM,CAAC;CACX;AAED;sEACsE;AACtE,wBAAgB,WAAW,CAAC,IAAI,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM;;;;;EAMxE;AAED,6EAA6E;AAC7E,wBAAgB,YAAY,CAC1B,OAAO,EAAE,OAAO,GAAG,QAAQ,EAC3B,KAAK,EAAE,MAAM,EACb,YAAY,EAAE,MAAM,EACpB,aAAa,EAAE,MAAM,GACpB;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAKnC;AAED;mFACmF;AACnF,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAI/C;AASD,gEAAgE;AAChE,wBAAgB,YAAY,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAKpD;AAED,wBAAgB,aAAa,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI,CAOjE;AAED,eAAO,MAAM,eAAe,yGAC4E,CAAC;AAEzG;;;sDAGsD;AACtD,wBAAsB,mBAAmB,CACvC,GAAG,EAAE,MAAM,EACX,SAAS,GAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAIpC,GACH,OAAO,CAAC,MAAM,CAAC,CAejB;AAED,MAAM,MAAM,OAAO,GACf;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,SAAS,EAAE,IAAI,GAAG,KAAK,CAAA;CAAE,GAC3C;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,YAAY,GAAG,UAAU,CAAA;CAAE,GACjD;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,QAAQ,CAAA;CAAE,CAAC;AAErC;+EAC+E;AAC/E,wBAAgB,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAQhE;AAED,eAAO,MAAM,kBAAkB,iHACiF,CAAC;AAEjH,uFAAuF;AACvF,wBAAsB,YAAY,CAAC,GAAG,EAAE,MAAM,EAAE,EAAE,EAAE,OAAO,GAAG,OAAO,CAAC,MAAM,CAAC,CAuC5E"}
@@ -0,0 +1,132 @@
1
+ import { ACCEPTED_IMAGE_TYPES } from './imageUrl.js';
2
+ /** Converts a fractional crop rectangle to whole pixels inside a width x height
3
+ * image, clamped so it never leaves the image and is at least 1px. */
4
+ export function toPixelRect(rect, width, height) {
5
+ const sx = Math.min(Math.max(Math.round(rect.x * width), 0), width - 1);
6
+ const sy = Math.min(Math.max(Math.round(rect.y * height), 0), height - 1);
7
+ const sw = Math.min(Math.max(Math.round(rect.w * width), 1), width - sx);
8
+ const sh = Math.min(Math.max(Math.round(rect.h * height), 1), height - sy);
9
+ return { sx, sy, sw, sh };
10
+ }
11
+ /** When one dimension changes with the aspect ratio locked, returns both. */
12
+ export function fitDimension(changed, value, naturalWidth, naturalHeight) {
13
+ const v = Math.max(1, Math.round(value));
14
+ return changed === 'width'
15
+ ? { width: v, height: Math.max(1, Math.round((v * naturalHeight) / naturalWidth)) }
16
+ : { width: Math.max(1, Math.round((v * naturalWidth) / naturalHeight)), height: v };
17
+ }
18
+ /** MIME type to re-encode an edited image as: its own if it is a lossy/lossless
19
+ * format canvas can write (JPEG, WebP, PNG), otherwise PNG (GIF, SVG, unknown). */
20
+ export function mimeFromSrc(src) {
21
+ const data = /^data:(image\/[a-z0-9.+-]+)[;,]/i.exec(src);
22
+ const type = data ? data[1].toLowerCase() : extensionMime(src);
23
+ return type === 'image/jpeg' || type === 'image/webp' ? type : 'image/png';
24
+ }
25
+ function extensionMime(src) {
26
+ const match = /\.(jpe?g|webp|png|gif|svg)(?:[?#].*)?$/i.exec(src);
27
+ if (!match)
28
+ return 'image/png';
29
+ const ext = match[1].toLowerCase();
30
+ return ext === 'jpg' || ext === 'jpeg' ? 'image/jpeg' : ext === 'webp' ? 'image/webp' : 'image/png';
31
+ }
32
+ /** Approximate decoded size, in bytes, of a base64 data URI. */
33
+ export function dataUrlBytes(dataUrl) {
34
+ const comma = dataUrl.indexOf(',');
35
+ const base64 = comma === -1 ? '' : dataUrl.slice(comma + 1);
36
+ const padding = base64.endsWith('==') ? 2 : base64.endsWith('=') ? 1 : 0;
37
+ return Math.floor((base64.length * 3) / 4) - padding;
38
+ }
39
+ export function dataUrlToFile(dataUrl, name) {
40
+ const comma = dataUrl.indexOf(',');
41
+ const mime = /^data:([^;,]+)/.exec(dataUrl)?.[1] ?? 'image/png';
42
+ const binary = atob(dataUrl.slice(comma + 1));
43
+ const bytes = new Uint8Array(binary.length);
44
+ for (let i = 0; i < binary.length; i++)
45
+ bytes[i] = binary.charCodeAt(i);
46
+ return new File([bytes], name, { type: mime });
47
+ }
48
+ export const CANNOT_DOWNLOAD = "Couldn't download this image (its server doesn't allow it). Download the file and upload it instead.";
49
+ /** Downloads an image and returns it as a base64 data URI, to embed it in the
50
+ * document. Without a `fetchBlob` override, needs the image's server to allow
51
+ * cross-origin reads (CORS) — a host can pass one to route the download
52
+ * through its own backend or a CORS proxy instead. */
53
+ export async function fetchImageAsDataUrl(url, fetchBlob = (u) => fetch(u, { mode: 'cors' }).then((response) => {
54
+ if (!response.ok)
55
+ throw new Error('bad status');
56
+ return response.blob();
57
+ })) {
58
+ if (/^data:image\//i.test(url))
59
+ return url;
60
+ let blob;
61
+ try {
62
+ blob = await fetchBlob(url);
63
+ }
64
+ catch {
65
+ throw new Error(CANNOT_DOWNLOAD);
66
+ }
67
+ if (!ACCEPTED_IMAGE_TYPES.includes(blob.type))
68
+ throw new Error("That URL doesn't point to a PNG, JPEG, GIF, WebP or SVG image");
69
+ return new Promise((resolve, reject) => {
70
+ const reader = new FileReader();
71
+ reader.onload = () => resolve(String(reader.result));
72
+ reader.onerror = () => reject(new Error(CANNOT_DOWNLOAD));
73
+ reader.readAsDataURL(blob);
74
+ });
75
+ }
76
+ /** Loads an image for canvas use. External URLs are requested with CORS so the
77
+ * canvas isn't tainted; a server that doesn't allow that makes this reject. */
78
+ export function loadImage(src) {
79
+ return new Promise((resolve, reject) => {
80
+ const img = new Image();
81
+ if (/^https?:/i.test(src))
82
+ img.crossOrigin = 'anonymous';
83
+ img.onload = () => resolve(img);
84
+ img.onerror = () => reject(new Error('IMAGE_LOAD_FAILED'));
85
+ img.src = src;
86
+ });
87
+ }
88
+ export const CANNOT_EDIT_PIXELS = "This image can't be edited here (its server doesn't allow it). Upload the file instead to crop or rotate it.";
89
+ /** Applies a rotate / flip / crop to an image and returns the result as a data URI. */
90
+ export async function applyPixelOp(src, op) {
91
+ let img;
92
+ try {
93
+ img = await loadImage(src);
94
+ }
95
+ catch {
96
+ throw new Error(CANNOT_EDIT_PIXELS);
97
+ }
98
+ const width = img.naturalWidth;
99
+ const height = img.naturalHeight;
100
+ if (!width || !height)
101
+ throw new Error('This image has no pixel size to edit (some SVGs). Upload a PNG or JPEG instead.');
102
+ const canvas = document.createElement('canvas');
103
+ const ctx = canvas.getContext('2d');
104
+ if (!ctx)
105
+ throw new Error('Image editing is not available in this browser');
106
+ if (op.type === 'rotate') {
107
+ canvas.width = height;
108
+ canvas.height = width;
109
+ ctx.translate(height / 2, width / 2);
110
+ ctx.rotate(op.direction === 'cw' ? Math.PI / 2 : -Math.PI / 2);
111
+ ctx.drawImage(img, -width / 2, -height / 2);
112
+ }
113
+ else if (op.type === 'flip') {
114
+ canvas.width = width;
115
+ canvas.height = height;
116
+ ctx.translate(op.axis === 'horizontal' ? width : 0, op.axis === 'vertical' ? height : 0);
117
+ ctx.scale(op.axis === 'horizontal' ? -1 : 1, op.axis === 'vertical' ? -1 : 1);
118
+ ctx.drawImage(img, 0, 0);
119
+ }
120
+ else {
121
+ const { sx, sy, sw, sh } = toPixelRect(op.rect, width, height);
122
+ canvas.width = sw;
123
+ canvas.height = sh;
124
+ ctx.drawImage(img, sx, sy, sw, sh, 0, 0, sw, sh);
125
+ }
126
+ try {
127
+ return canvas.toDataURL(mimeFromSrc(src), 0.92);
128
+ }
129
+ catch {
130
+ throw new Error(CANNOT_EDIT_PIXELS);
131
+ }
132
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Validates the source of a linked image. Accepts http(s) URLs, relative
3
+ * paths, and base64 raster/SVG data URIs (what an embedded image stores);
4
+ * anything else (javascript:, other data: types, file:, ...) is rejected.
5
+ * Returns the trimmed URL, or null when it should be refused.
6
+ */
7
+ export declare function normalizeImageUrl(input: string): string | null;
8
+ export declare const ACCEPTED_IMAGE_TYPES: string[];
9
+ /** Returns an error message if the file can't be inserted, otherwise null. */
10
+ export declare function validateImageFile(file: {
11
+ type: string;
12
+ size: number;
13
+ }, maxSizeMB: number): string | null;
14
+ //# sourceMappingURL=imageUrl.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"imageUrl.d.ts","sourceRoot":"","sources":["../../src/image/imageUrl.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAO9D;AAED,eAAO,MAAM,oBAAoB,UAA0E,CAAC;AAE5G,8EAA8E;AAC9E,wBAAgB,iBAAiB,CAAC,IAAI,EAAE;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAIxG"}
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Validates the source of a linked image. Accepts http(s) URLs, relative
3
+ * paths, and base64 raster/SVG data URIs (what an embedded image stores);
4
+ * anything else (javascript:, other data: types, file:, ...) is rejected.
5
+ * Returns the trimmed URL, or null when it should be refused.
6
+ */
7
+ export function normalizeImageUrl(input) {
8
+ const url = input.trim();
9
+ if (!url)
10
+ return null;
11
+ if (/^https?:\/\/\S+$/i.test(url))
12
+ return url;
13
+ if (/^(\/|\.\.?\/)\S*$/.test(url))
14
+ return url;
15
+ if (/^data:image\/(png|jpe?g|gif|webp|svg\+xml);base64,[a-z0-9+/=]+$/i.test(url))
16
+ return url;
17
+ return null;
18
+ }
19
+ export const ACCEPTED_IMAGE_TYPES = ['image/png', 'image/jpeg', 'image/gif', 'image/webp', 'image/svg+xml'];
20
+ /** Returns an error message if the file can't be inserted, otherwise null. */
21
+ export function validateImageFile(file, maxSizeMB) {
22
+ if (!ACCEPTED_IMAGE_TYPES.includes(file.type))
23
+ return 'Choose a PNG, JPEG, GIF, WebP or SVG image';
24
+ if (file.size > maxSizeMB * 1024 * 1024)
25
+ return `Image is larger than ${maxSizeMB} MB`;
26
+ return null;
27
+ }
package/dist/index.d.ts CHANGED
@@ -1,6 +1,10 @@
1
1
  export { EditorRoot } from './EditorRoot.js';
2
2
  export type { EditorRootProps, EditorRef, EditorInitialContent } from './EditorRoot.js';
3
3
  export { defaultMantineTheme } from './theme/mantineTheme.js';
4
+ export { ImageNode, $createImageNode, $isImageNode } from './image/ImageNode.js';
5
+ export type { ImagePayload, ImageLinkType, SerializedImageNode } from './image/ImageNode.js';
4
6
  export { DEFAULT_FONT_OPTIONS, URDU_WEB_FONT_OPTIONS, FONT_SIZES_PX } from './fonts/index.js';
5
7
  export type { FontOption } from './fonts/index.js';
8
+ export { STRINGS, getStrings, useStrings, useUiStrings, UiStringsContext } from './i18n/index.js';
9
+ export type { Locale, Strings } from './i18n/index.js';
6
10
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAC1C,YAAY,EAAE,eAAe,EAAE,SAAS,EAAE,oBAAoB,EAAE,MAAM,cAAc,CAAC;AACrF,OAAO,EAAE,mBAAmB,EAAE,MAAM,sBAAsB,CAAC;AAC3D,OAAO,EAAE,oBAAoB,EAAE,qBAAqB,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AACrF,YAAY,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAC1C,YAAY,EAAE,eAAe,EAAE,SAAS,EAAE,oBAAoB,EAAE,MAAM,cAAc,CAAC;AACrF,OAAO,EAAE,mBAAmB,EAAE,MAAM,sBAAsB,CAAC;AAC3D,OAAO,EAAE,SAAS,EAAE,gBAAgB,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAC9E,YAAY,EAAE,YAAY,EAAE,aAAa,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AAC1F,OAAO,EAAE,oBAAoB,EAAE,qBAAqB,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AACrF,YAAY,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AAC1C,OAAO,EAAE,OAAO,EAAE,UAAU,EAAE,UAAU,EAAE,YAAY,EAAE,gBAAgB,EAAE,MAAM,QAAQ,CAAC;AACzF,YAAY,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC"}
package/dist/index.js CHANGED
@@ -1,3 +1,5 @@
1
1
  export { EditorRoot } from './EditorRoot.js';
2
2
  export { defaultMantineTheme } from './theme/mantineTheme.js';
3
+ export { ImageNode, $createImageNode, $isImageNode } from './image/ImageNode.js';
3
4
  export { DEFAULT_FONT_OPTIONS, URDU_WEB_FONT_OPTIONS, FONT_SIZES_PX } from './fonts/index.js';
5
+ export { STRINGS, getStrings, useStrings, useUiStrings, UiStringsContext } from './i18n/index.js';
@@ -1 +1 @@
1
- {"version":3,"file":"nodes.d.ts","sourceRoot":"","sources":["../src/nodes.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAMlD;;;;;;;;GAQG;AACH,eAAO,MAAM,YAAY,EAAE,KAAK,CAAC,WAAW,CAAC,EAO5C,CAAC"}
1
+ {"version":3,"file":"nodes.d.ts","sourceRoot":"","sources":["../src/nodes.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAOlD;;;;;;;;GAQG;AACH,eAAO,MAAM,YAAY,EAAE,KAAK,CAAC,WAAW,CAAC,EAQ5C,CAAC"}
package/dist/nodes.js CHANGED
@@ -2,6 +2,7 @@ import { HeadingNode, QuoteNode } from '@lexical/rich-text';
2
2
  import { ListItemNode, ListNode } from '@lexical/list';
3
3
  import { LinkNode } from '@lexical/link';
4
4
  import { HorizontalRuleNode } from '@lexical/react/LexicalHorizontalRuleNode';
5
+ import { ImageNode } from './image/ImageNode.js';
5
6
  /**
6
7
  * Node types registered on the editor at construction time. Per
7
8
  * editor-architecture-design.md §2, node registration cannot be conditional
@@ -18,4 +19,5 @@ export const EDITOR_NODES = [
18
19
  ListItemNode,
19
20
  LinkNode,
20
21
  HorizontalRuleNode,
22
+ ImageNode,
21
23
  ];
@@ -1,10 +1,25 @@
1
1
  import { type MantineThemeOverride } from '@mantine/core';
2
- import '@mantine/core/styles.css';
3
2
  import './editor.css';
4
3
  export interface EditorThemeProviderProps {
5
4
  theme?: MantineThemeOverride;
6
5
  colorScheme?: 'light' | 'dark';
6
+ /** Overrides both Mantine's primary color and the `--editor-accent` /
7
+ * `--editor-accent-soft` CSS variables — see EditorRootProps.accentColor. */
8
+ accentColor?: string;
9
+ /**
10
+ * The id of the portal-anchor element EditorRoot renders inside
11
+ * `.likhari-root` (see PortalTargetContext.tsx). Scopes Mantine's OWN
12
+ * `data-mantine-color-scheme` attribute and CSS variables to that single
13
+ * element instead of the default `document.documentElement` — without
14
+ * this, Mantine's Modal/Menu/Select/Tooltip surfaces (background, text,
15
+ * border colors) would always render in Mantine's global light scheme
16
+ * regardless of `colorScheme`, since nothing ever told MantineProvider
17
+ * which scheme to use; forcing it at `document.documentElement` instead
18
+ * would work but would leak dark mode onto the rest of the host page and
19
+ * collide between multiple EditorRoot instances with different schemes.
20
+ */
21
+ scopeElementId?: string;
7
22
  children: React.ReactNode;
8
23
  }
9
- export declare function EditorThemeProvider({ theme, colorScheme, children }: EditorThemeProviderProps): import("react").JSX.Element;
24
+ export declare function EditorThemeProvider({ theme, colorScheme, accentColor, scopeElementId, children }: EditorThemeProviderProps): import("react").JSX.Element;
10
25
  //# 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;AAO3E,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"}