@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
|
@@ -8,6 +8,8 @@ import { $getNodeByKey, $getSelection, $isNodeSelection, CLICK_COMMAND, COMMAND_
|
|
|
8
8
|
import { ImageCropDialog } from './ImageCropDialog.js';
|
|
9
9
|
import { ImageDialog } from './ImageDialog.js';
|
|
10
10
|
import { useImageOptions } from './ImageOptionsContext.js';
|
|
11
|
+
import { useUiStrings } from '../i18n/useStrings.js';
|
|
12
|
+
import { usePortalTarget } from '../PortalTargetContext.js';
|
|
11
13
|
import { dataUrlBytes, fetchImageAsDataUrl } from './imageEdit.js';
|
|
12
14
|
// A type-only import of ImageNode (ImageNode itself imports this file), so the
|
|
13
15
|
// node is recognised by its type string rather than `instanceof`.
|
|
@@ -26,6 +28,8 @@ const MIN_WIDTH = 32;
|
|
|
26
28
|
export function ImageComponent({ nodeKey, src, altText, caption, linkType, width, height }) {
|
|
27
29
|
const [editor] = useLexicalComposerContext();
|
|
28
30
|
const options = useImageOptions();
|
|
31
|
+
const strings = useUiStrings();
|
|
32
|
+
const portalTarget = usePortalTarget();
|
|
29
33
|
const [isSelected, setSelected, clearSelection] = useLexicalNodeSelection(nodeKey);
|
|
30
34
|
const imageRef = useRef(null);
|
|
31
35
|
const figureRef = useRef(null);
|
|
@@ -82,7 +86,7 @@ export function ImageComponent({ nodeKey, src, altText, caption, linkType, width
|
|
|
82
86
|
try {
|
|
83
87
|
const dataUrl = await fetchImageAsDataUrl(src, options.fetchImage);
|
|
84
88
|
if (dataUrlBytes(dataUrl) > options.maxSizeMB * 1024 * 1024) {
|
|
85
|
-
throw new Error(
|
|
89
|
+
throw new Error(strings.imageDialog.errors.largerThan(options.maxSizeMB));
|
|
86
90
|
}
|
|
87
91
|
updateNode((node) => node.setSource(dataUrl, 'embedded'));
|
|
88
92
|
}
|
|
@@ -154,10 +158,10 @@ export function ImageComponent({ nodeKey, src, altText, caption, linkType, width
|
|
|
154
158
|
clearSelection();
|
|
155
159
|
setSelected(true);
|
|
156
160
|
setMenu({ x: e.clientX, y: e.clientY });
|
|
157
|
-
}, children: [_jsxs("span", { className: "likhari-image-wrap", children: [_jsx("img", { ref: imageRef, className: "likhari-image", src: src, alt: altText, width: shownWidth, draggable: false }), isSelected && linkType === 'embedded' && (_jsx("span", { className: "likhari-image-resize-handle", role: "separator", "aria-label":
|
|
161
|
+
}, children: [_jsxs("span", { className: "likhari-image-wrap", children: [_jsx("img", { ref: imageRef, className: "likhari-image", src: src, alt: altText, width: shownWidth, draggable: false }), isSelected && linkType === 'embedded' && (_jsx("span", { className: "likhari-image-resize-handle", role: "separator", "aria-label": strings.imageMenu.resizeImage, onPointerDown: onResizeDown, onPointerMove: onResizeMove, onPointerUp: onResizeUp, onPointerCancel: onResizeUp }))] }), caption && _jsx("figcaption", { className: "likhari-image-caption", children: caption }), _jsxs(Menu, { opened: menu !== null, onChange: (opened) => {
|
|
158
162
|
if (!opened)
|
|
159
163
|
setMenu(null);
|
|
160
|
-
}, position: "bottom-start", withinPortal: true, shadow: "sm", width: 200, children: [_jsx(Menu.Target, { children: _jsx("span", { "aria-hidden": "true", style: { position: 'fixed', left: menu?.x ?? -9999, top: menu?.y ?? -9999, width: 1, height: 1, pointerEvents: 'none' } }) }), _jsxs(Menu.Dropdown, { children: [_jsx(Menu.Item, { leftSection: _jsx(IconPhotoEdit, { size: 16, stroke: 1.75 }), onClick: () => setDialog({}), children:
|
|
164
|
+
}, position: "bottom-start", withinPortal: true, portalProps: { target: portalTarget }, shadow: "sm", width: 200, children: [_jsx(Menu.Target, { children: _jsx("span", { "aria-hidden": "true", style: { position: 'fixed', left: menu?.x ?? -9999, top: menu?.y ?? -9999, width: 1, height: 1, pointerEvents: 'none' } }) }), _jsxs(Menu.Dropdown, { children: [_jsx(Menu.Item, { leftSection: _jsx(IconPhotoEdit, { size: 16, stroke: 1.75 }), onClick: () => setDialog({}), children: strings.imageMenu.editImage }), linkType === 'linked' && options.allowEmbedded && (_jsx(Menu.Item, { leftSection: _jsx(IconPhotoDown, { size: 16, stroke: 1.75 }), onClick: () => void convertToEmbedded(), children: strings.imageMenu.convertToEmbedded })), linkType === 'embedded' && (_jsx(Menu.Item, { leftSection: _jsx(IconCrop, { size: 16, stroke: 1.75 }), onClick: () => setCropDialogOpen(true), children: strings.imageMenu.cropResize })), options.allowCaption && (_jsx(Menu.Item, { leftSection: _jsx(IconTextCaption, { size: 16, stroke: 1.75 }), onClick: () => setDialog({ intent: 'caption' }), children: caption ? strings.imageMenu.editCaption : strings.imageMenu.addCaption })), options.allowCaption && caption && (_jsx(Menu.Item, { leftSection: _jsx(IconX, { size: 16, stroke: 1.75 }), onClick: () => updateNode((node) => node.setCaption(null)), children: strings.imageMenu.removeCaption })), _jsx(Menu.Divider, {}), _jsx(Menu.Item, { color: "red", leftSection: _jsx(IconTrash, { size: 16, stroke: 1.75 }), onClick: removeImage, children: strings.imageMenu.deleteImage })] })] }), _jsx(ImageDialog, { mode: "edit", opened: dialog !== null, initial: { src, altText, caption, linkType }, intent: dialog?.intent, onSubmit: applyEdit, onClose: () => {
|
|
161
165
|
setDialog(null);
|
|
162
166
|
editor.focus();
|
|
163
167
|
} }), linkType === 'embedded' && (_jsx(ImageCropDialog, { opened: cropDialogOpen, initial: { src, width, height }, onSubmit: applyCrop, onClose: () => {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ImageCropDialog.d.ts","sourceRoot":"","sources":["../../src/image/ImageCropDialog.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"ImageCropDialog.d.ts","sourceRoot":"","sources":["../../src/image/ImageCropDialog.tsx"],"names":[],"mappings":"AASA,MAAM,WAAW,oBAAoB;IACnC,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;CACvB;AAED,MAAM,WAAW,oBAAoB;IACnC,MAAM,EAAE,OAAO,CAAC;IAChB;oEACgE;IAChE,OAAO,EAAE;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;QAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,CAAC;IACtE,QAAQ,EAAE,CAAC,KAAK,EAAE,oBAAoB,KAAK,IAAI,CAAC;IAChD,OAAO,EAAE,MAAM,IAAI,CAAC;CACrB;AAqBD;;qEAEqE;AACrE,wBAAgB,eAAe,CAAC,EAAE,MAAM,EAAE,OAAO,EAAE,QAAQ,EAAE,OAAO,EAAE,EAAE,oBAAoB,+BAqK3F"}
|
|
@@ -5,6 +5,8 @@ import { normalizeImageUrl } from './imageUrl.js';
|
|
|
5
5
|
import { dataUrlBytes, dataUrlToFile, fitDimension, mimeFromSrc } from './imageEdit.js';
|
|
6
6
|
import { ImageCropper } from './ImageCropper.js';
|
|
7
7
|
import { useImageOptions } from './ImageOptionsContext.js';
|
|
8
|
+
import { useUiStrings } from '../i18n/useStrings.js';
|
|
9
|
+
import { usePortalTarget } from '../PortalTargetContext.js';
|
|
8
10
|
/** Natural pixel size of an image URL (for the aspect ratio and size presets). */
|
|
9
11
|
function useNaturalSize(src) {
|
|
10
12
|
const [size, setSize] = useState(null);
|
|
@@ -29,6 +31,8 @@ function useNaturalSize(src) {
|
|
|
29
31
|
* its context menu. Edits that change pixels are re-embedded as a data URI,
|
|
30
32
|
* or stored through the host's `onImageUpload` when there is one. */
|
|
31
33
|
export function ImageCropDialog({ opened, initial, onSubmit, onClose }) {
|
|
34
|
+
const strings = useUiStrings();
|
|
35
|
+
const portalTarget = usePortalTarget();
|
|
32
36
|
const { maxSizeMB, onImageUpload } = useImageOptions();
|
|
33
37
|
const [pixelSrc, setPixelSrc] = useState(null);
|
|
34
38
|
const [width, setWidth] = useState(null);
|
|
@@ -75,7 +79,7 @@ export function ImageCropDialog({ opened, initial, onSubmit, onClose }) {
|
|
|
75
79
|
let src = effectiveSrc;
|
|
76
80
|
if (pixelSrc) {
|
|
77
81
|
if (dataUrlBytes(pixelSrc) > maxSizeMB * 1024 * 1024) {
|
|
78
|
-
setError(
|
|
82
|
+
setError(strings.imageCropDialog.errors.largerThan(maxSizeMB));
|
|
79
83
|
return;
|
|
80
84
|
}
|
|
81
85
|
if (onImageUpload) {
|
|
@@ -83,7 +87,7 @@ export function ImageCropDialog({ opened, initial, onSubmit, onClose }) {
|
|
|
83
87
|
const ext = mimeFromSrc(pixelSrc).split('/')[1];
|
|
84
88
|
src = await onImageUpload(dataUrlToFile(pixelSrc, `image.${ext}`));
|
|
85
89
|
if (!normalizeImageUrl(src)) {
|
|
86
|
-
setError(
|
|
90
|
+
setError(strings.imageCropDialog.errors.uploadUnusable);
|
|
87
91
|
return;
|
|
88
92
|
}
|
|
89
93
|
}
|
|
@@ -91,23 +95,26 @@ export function ImageCropDialog({ opened, initial, onSubmit, onClose }) {
|
|
|
91
95
|
onSubmit({ src, width, height });
|
|
92
96
|
}
|
|
93
97
|
catch (err) {
|
|
94
|
-
|
|
98
|
+
// ImageCropper's own pixel-edit error message (imageEdit.ts,
|
|
99
|
+
// applyPixelOp) is shown as-is when present — see the note in
|
|
100
|
+
// ImageDialog.tsx for why that pure utility's messages stay English.
|
|
101
|
+
setError(err instanceof Error ? err.message : strings.imageCropDialog.errors.couldNotSave);
|
|
95
102
|
}
|
|
96
103
|
finally {
|
|
97
104
|
setBusy(false);
|
|
98
105
|
}
|
|
99
106
|
};
|
|
100
|
-
return (_jsx(Modal, { opened: opened, onClose: onClose, title:
|
|
107
|
+
return (_jsx(Modal, { opened: opened, onClose: onClose, title: strings.imageCropDialog.title, centered: true, size: "lg", portalProps: { target: portalTarget }, children: _jsx("form", { onSubmit: handleSubmit, children: _jsxs(Stack, { gap: "md", children: [_jsx(ImageCropper, { src: effectiveSrc, canReset: pixelSrc !== null, onApply: (edited) => {
|
|
101
108
|
setPixelSrc(edited);
|
|
102
109
|
resetSize();
|
|
103
110
|
}, onReset: () => {
|
|
104
111
|
setPixelSrc(null);
|
|
105
112
|
setWidth(initial.width);
|
|
106
113
|
setHeight(initial.height);
|
|
107
|
-
} }), _jsx(Divider, { label:
|
|
114
|
+
} }), _jsx(Divider, { label: strings.imageCropDialog.sizeLabel, labelPosition: "left" }), _jsxs(Stack, { gap: "sm", children: [_jsxs(Group, { grow: true, align: "flex-end", children: [_jsx(NumberInput, { label: strings.imageCropDialog.widthLabel, min: 1, max: 10000, hideControls: true, placeholder: natural ? String(natural.width) : strings.imageCropDialog.autoPlaceholder, value: width ?? '', onChange: (v) => onSizeChange('width', v) }), _jsx(NumberInput, { label: strings.imageCropDialog.heightLabel, min: 1, max: 10000, hideControls: true, placeholder: natural ? String(natural.height) : strings.imageCropDialog.autoPlaceholder, value: height ?? '', onChange: (v) => onSizeChange('height', v) })] }), _jsx(Checkbox, { label: strings.imageCropDialog.keepProportions, checked: lockAspect, onChange: (e) => setLockAspect(e.currentTarget.checked) }), _jsxs(Group, { gap: "xs", children: [[25, 50, 75, 100].map((pct) => (_jsxs(Button, { size: "xs", variant: "default", disabled: !natural, onClick: () => {
|
|
108
115
|
if (!natural)
|
|
109
116
|
return;
|
|
110
117
|
setWidth(Math.round((natural.width * pct) / 100));
|
|
111
118
|
setHeight(Math.round((natural.height * pct) / 100));
|
|
112
|
-
}, children: [pct, "%"] }, pct))), _jsx(Button, { size: "xs", variant: "subtle", onClick: resetSize, children:
|
|
119
|
+
}, children: [pct, "%"] }, pct))), _jsx(Button, { size: "xs", variant: "subtle", onClick: resetSize, children: strings.imageCropDialog.originalSize })] }), _jsx(Text, { size: "xs", c: "dimmed", children: strings.imageCropDialog.dragHint(natural) })] }), error && (_jsx(Alert, { color: "red", variant: "light", p: "xs", children: error })), _jsxs(Group, { justify: "flex-end", gap: "xs", children: [_jsx(Button, { variant: "default", onClick: onClose, children: strings.common.cancel }), _jsx(Button, { type: "submit", loading: busy, children: strings.common.save })] })] }) }) }));
|
|
113
120
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ImageCropper.d.ts","sourceRoot":"","sources":["../../src/image/ImageCropper.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"ImageCropper.d.ts","sourceRoot":"","sources":["../../src/image/ImageCropper.tsx"],"names":[],"mappings":"AAgDA,MAAM,WAAW,iBAAiB;IAChC,4DAA4D;IAC5D,GAAG,EAAE,MAAM,CAAC;IACZ,kDAAkD;IAClD,OAAO,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;IAClC,uCAAuC;IACvC,QAAQ,EAAE,OAAO,CAAC;IAClB,OAAO,EAAE,MAAM,IAAI,CAAC;CACrB;AAED;;;0BAG0B;AAC1B,wBAAgB,YAAY,CAAC,EAC3B,GAAG,EACH,OAAO,EACP,QAAQ,EACR,OAAO,GACR,EAAE,iBAAiB,+BA0OnB"}
|
|
@@ -4,6 +4,7 @@ import { ActionIcon, Alert, Button, Group, SegmentedControl, Stack, Text, Toolti
|
|
|
4
4
|
import { IconCrop, IconFlipHorizontal, IconFlipVertical, IconRestore, IconRotate2, IconRotateClockwise2, } from "@tabler/icons-react";
|
|
5
5
|
import { applyPixelOp } from "./imageEdit.js";
|
|
6
6
|
import { MIN_CROP, moveRect, rectFromDrag, resizeRect, } from "./cropMath.js";
|
|
7
|
+
import { usePortalTarget } from "../PortalTargetContext.js";
|
|
7
8
|
const ASPECTS = {
|
|
8
9
|
free: null,
|
|
9
10
|
"1:1": 1,
|
|
@@ -17,6 +18,7 @@ const clamp = (v) => Math.min(Math.max(v, 0), 1);
|
|
|
17
18
|
* Each operation re-renders the pixels, so it needs an image the canvas may read
|
|
18
19
|
* (an embedded image). */
|
|
19
20
|
export function ImageCropper({ src, onApply, canReset, onReset, }) {
|
|
21
|
+
const portalTarget = usePortalTarget();
|
|
20
22
|
const imgRef = useRef(null);
|
|
21
23
|
const drag = useRef(null);
|
|
22
24
|
const [rect, setRect] = useState(null);
|
|
@@ -92,7 +94,7 @@ export function ImageCropper({ src, onApply, canReset, onReset, }) {
|
|
|
92
94
|
const selectionSize = rect && natural && rect.w > 0 && rect.h > 0
|
|
93
95
|
? `${Math.max(1, Math.round(rect.w * natural.width))} × ${Math.max(1, Math.round(rect.h * natural.height))} px`
|
|
94
96
|
: null;
|
|
95
|
-
return (_jsxs(Stack, { gap: "sm", children: [_jsxs(Group, { gap: "xs", children: [_jsx(Tooltip, { label: "Rotate left", children: _jsx(ActionIcon, { variant: "default", "aria-label": "Rotate left", disabled: busy, onClick: () => run({ type: "rotate", direction: "ccw" }), children: _jsx(IconRotate2, { size: 16 }) }) }), _jsx(Tooltip, { label: "Rotate right", children: _jsx(ActionIcon, { variant: "default", "aria-label": "Rotate right", disabled: busy, onClick: () => run({ type: "rotate", direction: "cw" }), children: _jsx(IconRotateClockwise2, { size: 16 }) }) }), _jsx(Tooltip, { label: "Flip horizontally", children: _jsx(ActionIcon, { variant: "default", "aria-label": "Flip horizontally", disabled: busy, onClick: () => run({ type: "flip", axis: "horizontal" }), children: _jsx(IconFlipHorizontal, { size: 16 }) }) }), _jsx(Tooltip, { label: "Flip vertically", children: _jsx(ActionIcon, { variant: "default", "aria-label": "Flip vertically", disabled: busy, onClick: () => run({ type: "flip", axis: "vertical" }), children: _jsx(IconFlipVertical, { size: 16 }) }) }), _jsx(Button, { ml: "auto", size: "xs", variant: "subtle", leftSection: _jsx(IconRestore, { size: 14 }), disabled: !canReset || busy, onClick: () => {
|
|
97
|
+
return (_jsxs(Stack, { gap: "sm", children: [_jsxs(Group, { gap: "xs", children: [_jsx(Tooltip, { label: "Rotate left", portalProps: { target: portalTarget }, children: _jsx(ActionIcon, { variant: "default", "aria-label": "Rotate left", disabled: busy, onClick: () => run({ type: "rotate", direction: "ccw" }), children: _jsx(IconRotate2, { size: 16 }) }) }), _jsx(Tooltip, { label: "Rotate right", portalProps: { target: portalTarget }, children: _jsx(ActionIcon, { variant: "default", "aria-label": "Rotate right", disabled: busy, onClick: () => run({ type: "rotate", direction: "cw" }), children: _jsx(IconRotateClockwise2, { size: 16 }) }) }), _jsx(Tooltip, { label: "Flip horizontally", portalProps: { target: portalTarget }, children: _jsx(ActionIcon, { variant: "default", "aria-label": "Flip horizontally", disabled: busy, onClick: () => run({ type: "flip", axis: "horizontal" }), children: _jsx(IconFlipHorizontal, { size: 16 }) }) }), _jsx(Tooltip, { label: "Flip vertically", portalProps: { target: portalTarget }, children: _jsx(ActionIcon, { variant: "default", "aria-label": "Flip vertically", disabled: busy, onClick: () => run({ type: "flip", axis: "vertical" }), children: _jsx(IconFlipVertical, { size: 16 }) }) }), _jsx(Button, { ml: "auto", size: "xs", variant: "subtle", leftSection: _jsx(IconRestore, { size: 14 }), disabled: !canReset || busy, onClick: () => {
|
|
96
98
|
setRect(null);
|
|
97
99
|
setError(null);
|
|
98
100
|
onReset();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ImageDialog.d.ts","sourceRoot":"","sources":["../../src/image/ImageDialog.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"ImageDialog.d.ts","sourceRoot":"","sources":["../../src/image/ImageDialog.tsx"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAEjD,MAAM,WAAW,gBAAgB;IAC/B,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,MAAM,CAAC;IAChB,wBAAwB;IACxB,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,QAAQ,EAAE,aAAa,CAAC;IACxB;gFAC4E;IAC5E,aAAa,EAAE,OAAO,CAAC;CACxB;AAED,MAAM,WAAW,gBAAgB;IAC/B,qFAAqF;IACrF,IAAI,EAAE,QAAQ,GAAG,MAAM,CAAC;IACxB,MAAM,EAAE,OAAO,CAAC;IAChB,0CAA0C;IAC1C,OAAO,CAAC,EAAE;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;QAAC,QAAQ,EAAE,aAAa,CAAA;KAAE,CAAC;IAC5F;4CACwC;IACxC,MAAM,CAAC,EAAE,SAAS,GAAG,SAAS,CAAC;IAC/B,QAAQ,EAAE,CAAC,KAAK,EAAE,gBAAgB,KAAK,IAAI,CAAC;IAC5C,OAAO,EAAE,MAAM,IAAI,CAAC;CACrB;AAaD;;;;;;GAMG;AACH,wBAAgB,WAAW,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,OAAO,EAAE,EAAE,gBAAgB,+BAwQjG"}
|
|
@@ -5,11 +5,13 @@ import { IconPhotoDown } from '@tabler/icons-react';
|
|
|
5
5
|
import { ACCEPTED_IMAGE_TYPES, normalizeImageUrl, validateImageFile } from './imageUrl.js';
|
|
6
6
|
import { dataUrlBytes, dataUrlToFile, fetchImageAsDataUrl, mimeFromSrc } from './imageEdit.js';
|
|
7
7
|
import { useImageOptions } from './ImageOptionsContext.js';
|
|
8
|
+
import { useUiStrings } from '../i18n/useStrings.js';
|
|
9
|
+
import { usePortalTarget } from '../PortalTargetContext.js';
|
|
8
10
|
function readAsDataUrl(file) {
|
|
9
11
|
return new Promise((resolve, reject) => {
|
|
10
12
|
const reader = new FileReader();
|
|
11
13
|
reader.onload = () => resolve(String(reader.result));
|
|
12
|
-
reader.onerror = () => reject(
|
|
14
|
+
reader.onerror = () => reject(new Error('READ_FAILED'));
|
|
13
15
|
reader.readAsDataURL(file);
|
|
14
16
|
});
|
|
15
17
|
}
|
|
@@ -21,6 +23,8 @@ function readAsDataUrl(file) {
|
|
|
21
23
|
* image's context menu once it has pixels of its own to edit.
|
|
22
24
|
*/
|
|
23
25
|
export function ImageDialog({ mode, opened, initial, intent, onSubmit, onClose }) {
|
|
26
|
+
const strings = useUiStrings();
|
|
27
|
+
const portalTarget = usePortalTarget();
|
|
24
28
|
const { allowLinked, allowEmbedded, allowCaption, maxSizeMB, onImageUpload, fetchImage } = useImageOptions();
|
|
25
29
|
const editing = mode === 'edit' && initial !== undefined;
|
|
26
30
|
const defaultSource = editing ? 'keep' : allowLinked ? 'linked' : 'embedded';
|
|
@@ -74,8 +78,8 @@ export function ImageDialog({ mode, opened, initial, intent, onSubmit, onClose }
|
|
|
74
78
|
try {
|
|
75
79
|
setFileSrc(await readAsDataUrl(next));
|
|
76
80
|
}
|
|
77
|
-
catch
|
|
78
|
-
setError(
|
|
81
|
+
catch {
|
|
82
|
+
setError(strings.imageDialog.errors.couldNotReadFile);
|
|
79
83
|
}
|
|
80
84
|
};
|
|
81
85
|
/** Downloads a linked image and switches the dialog to its embedded copy. */
|
|
@@ -87,13 +91,17 @@ export function ImageDialog({ mode, opened, initial, intent, onSubmit, onClose }
|
|
|
87
91
|
try {
|
|
88
92
|
const dataUrl = await fetchImageAsDataUrl(src, fetchImage);
|
|
89
93
|
if (dataUrlBytes(dataUrl) > maxSizeMB * 1024 * 1024)
|
|
90
|
-
throw new Error(
|
|
94
|
+
throw new Error(strings.imageDialog.errors.largerThan(maxSizeMB));
|
|
91
95
|
setSource('embedded');
|
|
92
96
|
setFile(null);
|
|
93
97
|
setFileSrc(dataUrl);
|
|
94
98
|
}
|
|
95
99
|
catch (err) {
|
|
96
|
-
|
|
100
|
+
// fetchImageAsDataUrl's own errors (network/CORS failures, unsupported
|
|
101
|
+
// type) are English (packages/react/src/image/imageEdit.ts) — shown
|
|
102
|
+
// as-is since threading locale into that pure utility isn't worth the
|
|
103
|
+
// refactor here; only the size-limit message above is ours to localize.
|
|
104
|
+
setError(err instanceof Error ? err.message : strings.imageDialog.errors.couldNotConvert);
|
|
97
105
|
}
|
|
98
106
|
finally {
|
|
99
107
|
setConverting(false);
|
|
@@ -103,7 +111,11 @@ export function ImageDialog({ mode, opened, initial, intent, onSubmit, onClose }
|
|
|
103
111
|
e.preventDefault();
|
|
104
112
|
setError(null);
|
|
105
113
|
if (!baseSrc) {
|
|
106
|
-
setError(source === 'linked'
|
|
114
|
+
setError(source === 'linked'
|
|
115
|
+
? strings.imageDialog.errors.enterUrl
|
|
116
|
+
: source === 'embedded'
|
|
117
|
+
? strings.imageDialog.errors.chooseFile
|
|
118
|
+
: strings.imageDialog.errors.noImage);
|
|
107
119
|
return;
|
|
108
120
|
}
|
|
109
121
|
try {
|
|
@@ -119,7 +131,7 @@ export function ImageDialog({ mode, opened, initial, intent, onSubmit, onClose }
|
|
|
119
131
|
}
|
|
120
132
|
if (newPixels) {
|
|
121
133
|
if (dataUrlBytes(newPixels) > maxSizeMB * 1024 * 1024) {
|
|
122
|
-
setError(
|
|
134
|
+
setError(strings.imageDialog.errors.largerThan(maxSizeMB));
|
|
123
135
|
return;
|
|
124
136
|
}
|
|
125
137
|
linkType = 'embedded';
|
|
@@ -129,7 +141,7 @@ export function ImageDialog({ mode, opened, initial, intent, onSubmit, onClose }
|
|
|
129
141
|
const upload = !file ? dataUrlToFile(newPixels, `image.${ext}`) : file;
|
|
130
142
|
src = await onImageUpload(upload);
|
|
131
143
|
if (!normalizeImageUrl(src)) {
|
|
132
|
-
setError(
|
|
144
|
+
setError(strings.imageDialog.errors.uploadUnusable);
|
|
133
145
|
return;
|
|
134
146
|
}
|
|
135
147
|
}
|
|
@@ -146,17 +158,19 @@ export function ImageDialog({ mode, opened, initial, intent, onSubmit, onClose }
|
|
|
146
158
|
});
|
|
147
159
|
}
|
|
148
160
|
catch (err) {
|
|
149
|
-
|
|
161
|
+
// fetchImageAsDataUrl's own network/CORS error message (imageEdit.ts)
|
|
162
|
+
// is shown as-is when present — see note in `convert` above.
|
|
163
|
+
setError(err instanceof Error ? err.message : strings.imageDialog.errors.couldNotSave);
|
|
150
164
|
}
|
|
151
165
|
finally {
|
|
152
166
|
setBusy(false);
|
|
153
167
|
}
|
|
154
168
|
};
|
|
155
|
-
const convertButton = (_jsx(Button, { size: "xs", variant: "light", leftSection: _jsx(IconPhotoDown, { size: 14 }), loading: converting, disabled: !baseSrc, onClick: () => convert(baseSrc), children:
|
|
156
|
-
return (_jsx(Modal, { opened: opened, onClose: onClose, title: editing ?
|
|
157
|
-
...(editing ? [{ value: 'keep', label:
|
|
158
|
-
...(allowLinked ? [{ value: 'linked', label:
|
|
159
|
-
...(allowEmbedded ? [{ value: 'embedded', label:
|
|
160
|
-
] })), source === 'keep' && (_jsxs(Stack, { gap: 6, children: [_jsx(Text, { size: "sm", c: "dimmed", lineClamp: 2, style: { wordBreak: 'break-all' }, children: initial?.linkType === 'embedded' ?
|
|
161
|
-
(convertedFromUrl ? (_jsx(Text, { size: "sm", c: "dimmed", children:
|
|
169
|
+
const convertButton = (_jsx(Button, { size: "xs", variant: "light", leftSection: _jsx(IconPhotoDown, { size: 14 }), loading: converting, disabled: !baseSrc, onClick: () => convert(baseSrc), children: strings.imageDialog.convertButton }));
|
|
170
|
+
return (_jsx(Modal, { opened: opened, onClose: onClose, title: editing ? strings.imageDialog.titleEdit : strings.imageDialog.titleInsert, centered: true, size: "sm", portalProps: { target: portalTarget }, children: _jsx("form", { onSubmit: handleSubmit, children: _jsxs(Stack, { gap: "sm", children: [(editing || (allowLinked && allowEmbedded)) && (_jsx(SegmentedControl, { fullWidth: true, value: source, onChange: (v) => changeSource(v), data: [
|
|
171
|
+
...(editing ? [{ value: 'keep', label: strings.imageDialog.currentImage }] : []),
|
|
172
|
+
...(allowLinked ? [{ value: 'linked', label: strings.imageDialog.fromUrl }] : []),
|
|
173
|
+
...(allowEmbedded ? [{ value: 'embedded', label: strings.imageDialog.upload }] : []),
|
|
174
|
+
] })), source === 'keep' && (_jsxs(Stack, { gap: 6, children: [_jsx(Text, { size: "sm", c: "dimmed", lineClamp: 2, style: { wordBreak: 'break-all' }, children: initial?.linkType === 'embedded' ? strings.imageDialog.embeddedInDocument : strings.imageDialog.linkedFrom(initial?.src ?? '') }), initial?.linkType === 'linked' && allowEmbedded && (_jsxs(Group, { gap: "xs", children: [convertButton, _jsx(Text, { size: "xs", c: "dimmed", children: strings.imageDialog.convertHint })] }))] })), source === 'linked' && (_jsxs(_Fragment, { children: [_jsx(TextInput, { label: strings.imageDialog.imageUrlLabel, placeholder: strings.imageDialog.imageUrlPlaceholder, value: url, onChange: (e) => setUrl(e.currentTarget.value), "data-autofocus": intent !== 'caption' ? true : undefined }), allowEmbedded && (_jsx(Checkbox, { label: strings.imageDialog.embedCopyLabel, description: strings.imageDialog.embedCopyDescription, checked: embedCopy, onChange: (e) => setEmbedCopy(e.currentTarget.checked) }))] })), source === 'embedded' &&
|
|
175
|
+
(convertedFromUrl ? (_jsx(Text, { size: "sm", c: "dimmed", children: strings.imageDialog.convertedFromUrl })) : (_jsx(FileInput, { label: strings.imageDialog.fileLabel, placeholder: strings.imageDialog.filePlaceholder, accept: ACCEPTED_IMAGE_TYPES.join(','), value: file, onChange: changeFile, description: strings.imageDialog.fileDescription(maxSizeMB, !onImageUpload) }))), _jsx(TextInput, { label: strings.imageDialog.altTextLabel, description: strings.imageDialog.altTextDescription, value: altText, onChange: (e) => setAltText(e.currentTarget.value) }), allowCaption && (_jsx(TextInput, { label: strings.imageDialog.captionLabel, description: strings.imageDialog.captionDescription, value: caption, onChange: (e) => setCaption(e.currentTarget.value), "data-autofocus": intent === 'caption' ? true : undefined })), error && (_jsx(Alert, { color: "red", variant: "light", p: "xs", children: error })), _jsxs(Group, { justify: "flex-end", gap: "xs", children: [_jsx(Button, { variant: "default", onClick: onClose, children: strings.common.cancel }), _jsx(Button, { type: "submit", loading: busy, children: editing ? strings.common.save : strings.common.insert })] })] }) }) }));
|
|
162
176
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -5,4 +5,6 @@ export { ImageNode, $createImageNode, $isImageNode } from './image/ImageNode.js'
|
|
|
5
5
|
export type { ImagePayload, ImageLinkType, SerializedImageNode } from './image/ImageNode.js';
|
|
6
6
|
export { DEFAULT_FONT_OPTIONS, URDU_WEB_FONT_OPTIONS, FONT_SIZES_PX } from './fonts/index.js';
|
|
7
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';
|
|
8
10
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAC1C,YAAY,EAAE,eAAe,EAAE,SAAS,EAAE,oBAAoB,EAAE,MAAM,cAAc,CAAC;AACrF,OAAO,EAAE,mBAAmB,EAAE,MAAM,sBAAsB,CAAC;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"}
|
|
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
|
@@ -2,3 +2,4 @@ export { EditorRoot } from './EditorRoot.js';
|
|
|
2
2
|
export { defaultMantineTheme } from './theme/mantineTheme.js';
|
|
3
3
|
export { ImageNode, $createImageNode, $isImageNode } from './image/ImageNode.js';
|
|
4
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';
|
|
@@ -4,7 +4,10 @@ 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;
|
|
7
10
|
children: React.ReactNode;
|
|
8
11
|
}
|
|
9
|
-
export declare function EditorThemeProvider({ theme, colorScheme, children }: EditorThemeProviderProps): import("react").JSX.Element;
|
|
12
|
+
export declare function EditorThemeProvider({ theme, colorScheme, accentColor, children }: EditorThemeProviderProps): import("react").JSX.Element;
|
|
10
13
|
//# sourceMappingURL=EditorThemeProvider.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"EditorThemeProvider.d.ts","sourceRoot":"","sources":["../../src/theme/EditorThemeProvider.tsx"],"names":[],"mappings":"
|
|
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,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAC;CAC3B;AAED,wBAAgB,mBAAmB,CAAC,EAAE,KAAK,EAAE,WAAW,EAAE,WAAW,EAAE,QAAQ,EAAE,EAAE,wBAAwB,+BAuC1G"}
|
|
@@ -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, 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:
|
|
55
|
+
_jsx("div", { className: "likhari-theme-scope", "data-editor-color-scheme": colorScheme, style: { display: 'contents', ...accentStyle }, children: _jsx(MantineProvider, { theme: resolvedTheme, 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
|
+
}
|
package/dist/theme/editor.css
CHANGED
|
@@ -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;
|