@spatika/editor 1.5.0 → 1.6.1
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/composites/AiCommandDock.d.ts.map +1 -1
- package/dist/composites/AiCommandDock.js +3 -1
- package/dist/composites/SpatikaEditor.d.ts.map +1 -1
- package/dist/composites/SpatikaEditor.js +5 -0
- package/dist/composites/TableGridPicker.d.ts.map +1 -1
- package/dist/composites/TableGridPicker.js +6 -0
- package/dist/composites/ToolbarInsertPanels.d.ts.map +1 -1
- package/dist/composites/ToolbarInsertPanels.js +36 -3
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/lib/types.d.ts +17 -0
- package/dist/lib/types.d.ts.map +1 -1
- package/dist/styles.css +64 -0
- package/package.json +2 -2
- package/src/composites/AiCommandDock.tsx +7 -4
- package/src/composites/SpatikaEditor.tsx +4 -0
- package/src/composites/TableGridPicker.tsx +6 -0
- package/src/composites/ToolbarInsertPanels.test.tsx +80 -0
- package/src/composites/ToolbarInsertPanels.tsx +72 -2
- package/src/index.ts +1 -0
- package/src/lib/types.ts +18 -0
- package/src/styles/editor.css +64 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AiCommandDock.d.ts","sourceRoot":"","sources":["../../src/composites/AiCommandDock.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AAgB3C,OAAO,KAAK,EAAE,kBAAkB,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAC;AAI5E,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,iBAAiB,CAAC;AAoB3D,KAAK,kBAAkB,GAAG;IACxB,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,MAAM,CAAC,EAAE,mBAAmB,CAAC;IAC7B,OAAO,CAAC,EAAE,kBAAkB,EAAE,CAAC;IAC/B,KAAK,EAAE,CAAC,OAAO,EAAE,mBAAmB,KAAK,IAAI,CAAC;IAC9C,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,CAAC;AAEF,wBAAgB,aAAa,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,EAAE,kBAAkB,
|
|
1
|
+
{"version":3,"file":"AiCommandDock.d.ts","sourceRoot":"","sources":["../../src/composites/AiCommandDock.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AAgB3C,OAAO,KAAK,EAAE,kBAAkB,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAC;AAI5E,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,iBAAiB,CAAC;AAoB3D,KAAK,kBAAkB,GAAG;IACxB,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,MAAM,CAAC,EAAE,mBAAmB,CAAC;IAC7B,OAAO,CAAC,EAAE,kBAAkB,EAAE,CAAC;IAC/B,KAAK,EAAE,CAAC,OAAO,EAAE,mBAAmB,KAAK,IAAI,CAAC;IAC9C,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,CAAC;AAEF,wBAAgB,aAAa,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,EAAE,kBAAkB,sCA2F9F"}
|
|
@@ -55,8 +55,10 @@ export function AiCommandDock({ editor, config, actions, onRun, className }) {
|
|
|
55
55
|
if (!editor)
|
|
56
56
|
return null;
|
|
57
57
|
return (_jsxs("div", { ref: rootRef, className: cn("spk-editor-ai-dock", className), children: [open ? (_jsx(AiCommandPanel, { id: panelId, title: title, actions: commandActions, hasSelection: hasSelection, onSelect: (actionId) => runAction(actionId), className: "spk-editor-ai-dock-panel" })) : null, promptConfig.enabled ? (_jsxs("div", { className: "spk-editor-ai-dock-bar", role: "search", children: [_jsx("button", { type: "button", className: cn("spk-editor-ai-dock-trigger", open && "spk-editor-ai-dock-trigger--active"), "aria-label": "AI commands", "aria-expanded": open, "aria-controls": open ? panelId : undefined, onClick: () => setOpen((value) => !value), children: _jsx(Sparkles, { className: "size-4" }) }), _jsxs("label", { className: "spk-editor-ai-dock-field", children: [_jsx("span", { className: "sr-only", children: promptConfig.placeholder }), _jsx("input", { type: "text", className: "spk-editor-ai-dock-input", value: prompt, placeholder: promptConfig.placeholder, onFocus: () => setOpen(true), onChange: (event) => setPrompt(event.target.value), onKeyDown: (event) => {
|
|
58
|
-
if (event.key === "Enter")
|
|
58
|
+
if (event.key === "Enter") {
|
|
59
|
+
event.preventDefault();
|
|
59
60
|
submitPrompt();
|
|
61
|
+
}
|
|
60
62
|
if (event.key === "Escape")
|
|
61
63
|
setOpen(false);
|
|
62
64
|
} })] }), _jsx("button", { type: "button", className: "spk-editor-ai-dock-submit", "aria-label": promptConfig.submitLabel, disabled: !prompt.trim(), onClick: submitPrompt, children: _jsx(ArrowUp, { className: "size-4" }) })] })) : null] }));
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SpatikaEditor.d.ts","sourceRoot":"","sources":["../../src/composites/SpatikaEditor.tsx"],"names":[],"mappings":"AAeA,OAAO,KAAK,EAAE,mBAAmB,EAAsB,MAAM,cAAc,CAAC;AAmB5E,eAAO,MAAM,aAAa;;;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"SpatikaEditor.d.ts","sourceRoot":"","sources":["../../src/composites/SpatikaEditor.tsx"],"names":[],"mappings":"AAeA,OAAO,KAAK,EAAE,mBAAmB,EAAsB,MAAM,cAAc,CAAC;AAmB5E,eAAO,MAAM,aAAa;;;;;;;;;;;uDA+QzB,CAAC"}
|
|
@@ -43,6 +43,11 @@ export const SpatikaEditor = forwardRef(function SpatikaEditor({ value, onChange
|
|
|
43
43
|
getHTML: () => editor?.getHTML() ?? "",
|
|
44
44
|
getJSON: () => editor?.getJSON(),
|
|
45
45
|
expandFullscreen: () => setFullscreen(true),
|
|
46
|
+
insertImage: (url) => {
|
|
47
|
+
if (!url.trim() || !editor)
|
|
48
|
+
return;
|
|
49
|
+
editor.chain().focus().setImage({ src: url }).run();
|
|
50
|
+
},
|
|
46
51
|
}), [editor]);
|
|
47
52
|
const toolbarConfig = { ...DEFAULT_TOOLBAR, ...toolbar };
|
|
48
53
|
const allSlashCommands = useMemo(() => [...defaultSlashCommands(), ...(slashCommands ?? [])], [slashCommands]);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"TableGridPicker.d.ts","sourceRoot":"","sources":["../../src/composites/TableGridPicker.tsx"],"names":[],"mappings":"AAIA,eAAO,MAAM,eAAe,KAAK,CAAC;AAClC,eAAO,MAAM,cAAc,KAAK,CAAC;AACjC,eAAO,MAAM,cAAc,KAAK,CAAC;AAEjC,KAAK,oBAAoB,GAAG;IAC1B,QAAQ,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,aAAa,EAAE,OAAO,KAAK,IAAI,CAAC;CACxE,CAAC;AAeF,wBAAgB,eAAe,CAAC,EAAE,QAAQ,EAAE,EAAE,oBAAoB,+
|
|
1
|
+
{"version":3,"file":"TableGridPicker.d.ts","sourceRoot":"","sources":["../../src/composites/TableGridPicker.tsx"],"names":[],"mappings":"AAIA,eAAO,MAAM,eAAe,KAAK,CAAC;AAClC,eAAO,MAAM,cAAc,KAAK,CAAC;AACjC,eAAO,MAAM,cAAc,KAAK,CAAC;AAEjC,KAAK,oBAAoB,GAAG;IAC1B,QAAQ,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,aAAa,EAAE,OAAO,KAAK,IAAI,CAAC;CACxE,CAAC;AAeF,wBAAgB,eAAe,CAAC,EAAE,QAAQ,EAAE,EAAE,oBAAoB,+BAsHjE"}
|
|
@@ -47,8 +47,14 @@ export function TableGridPicker({ onInsert }) {
|
|
|
47
47
|
}) }), _jsxs("div", { className: "spk-editor-table-picker-size", children: [_jsxs("label", { className: "spk-editor-table-picker-size-field", children: [_jsx(Columns3, { className: "size-4", "aria-hidden": "true" }), _jsx("span", { className: "sr-only", children: "Columns" }), _jsx("input", { type: "number", min: 1, max: TABLE_COLS_MAX, className: "spk-editor-table-picker-size-input", value: previewCols, onChange: (event) => {
|
|
48
48
|
setHover(null);
|
|
49
49
|
setCols(clampDimension(Number.parseInt(event.target.value, 10) || 1, TABLE_COLS_MAX));
|
|
50
|
+
}, onKeyDown: (event) => {
|
|
51
|
+
if (event.key === "Enter")
|
|
52
|
+
event.preventDefault();
|
|
50
53
|
} })] }), _jsx("span", { className: "spk-editor-table-picker-size-sep", "aria-hidden": "true", children: "\u00D7" }), _jsxs("label", { className: "spk-editor-table-picker-size-field", children: [_jsx(Rows3, { className: "size-4", "aria-hidden": "true" }), _jsx("span", { className: "sr-only", children: "Rows" }), _jsx("input", { type: "number", min: 1, max: TABLE_ROWS_MAX, className: "spk-editor-table-picker-size-input", value: previewRows, onChange: (event) => {
|
|
51
54
|
setHover(null);
|
|
52
55
|
setRows(clampDimension(Number.parseInt(event.target.value, 10) || 1, TABLE_ROWS_MAX));
|
|
56
|
+
}, onKeyDown: (event) => {
|
|
57
|
+
if (event.key === "Enter")
|
|
58
|
+
event.preventDefault();
|
|
53
59
|
} })] })] }), _jsxs("label", { className: "spk-editor-toolbar-popover-check", children: [_jsx("input", { type: "checkbox", checked: withHeaderRow, onChange: (event) => setWithHeaderRow(event.target.checked) }), _jsx("span", { children: "Header row" })] }), _jsxs("button", { type: "button", className: "spk-editor-toolbar-popover-btn spk-editor-toolbar-popover-btn--primary spk-editor-table-picker-insert", onClick: () => onInsert(previewRows, previewCols, withHeaderRow), children: ["Insert ", previewCols, " \u00D7 ", previewRows, " table"] })] }));
|
|
54
60
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ToolbarInsertPanels.d.ts","sourceRoot":"","sources":["../../src/composites/ToolbarInsertPanels.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AAG3C,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AAKtD,KAAK,UAAU,GAAG;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,IAAI,CAAC;CACnB,CAAC;AAEF,KAAK,eAAe,GAAG,UAAU,GAAG;IAClC,MAAM,CAAC,EAAE,iBAAiB,CAAC;CAC5B,CAAC;
|
|
1
|
+
{"version":3,"file":"ToolbarInsertPanels.d.ts","sourceRoot":"","sources":["../../src/composites/ToolbarInsertPanels.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AAG3C,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AAKtD,KAAK,UAAU,GAAG;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,IAAI,CAAC;CACnB,CAAC;AAEF,KAAK,eAAe,GAAG,UAAU,GAAG;IAClC,MAAM,CAAC,EAAE,iBAAiB,CAAC;CAC5B,CAAC;AAQF,wBAAgB,eAAe,CAAC,EAAE,MAAM,EAAE,KAAK,EAAE,EAAE,UAAU,+BAoD5D;AAUD,wBAAgB,gBAAgB,CAAC,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,EAAE,eAAe,+BAuK1E;AAED,wBAAgB,gBAAgB,CAAC,EAAE,MAAM,EAAE,KAAK,EAAE,EAAE,UAAU,+BAe7D"}
|
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { ImagePlus, Upload } from "lucide-react";
|
|
3
|
-
import { useRef, useState } from "react";
|
|
2
|
+
import { ImagePlus, Images, Upload } from "lucide-react";
|
|
3
|
+
import { useEffect, useRef, useState } from "react";
|
|
4
4
|
import { cn } from "../lib/cn";
|
|
5
5
|
import { insertImageFilesIntoEditor } from "../lib/insert-editor-images";
|
|
6
6
|
import { TableGridPicker } from "./TableGridPicker";
|
|
7
|
+
function preventFormSubmitOnEnter(event) {
|
|
8
|
+
if (event.key === "Enter") {
|
|
9
|
+
event.preventDefault();
|
|
10
|
+
}
|
|
11
|
+
}
|
|
7
12
|
export function LinkInsertPanel({ editor, close }) {
|
|
8
13
|
const previous = editor.getAttributes("link").href;
|
|
9
14
|
const [url, setUrl] = useState(previous ?? "https://");
|
|
@@ -22,6 +27,7 @@ export function LinkInsertPanel({ editor, close }) {
|
|
|
22
27
|
close();
|
|
23
28
|
};
|
|
24
29
|
return (_jsxs("div", { className: "spk-editor-toolbar-popover-form", children: [_jsxs("label", { className: "spk-editor-toolbar-popover-field", children: [_jsx("span", { children: "URL" }), _jsx("input", { type: "url", className: "spk-editor-toolbar-popover-input", value: url, placeholder: "https://", onChange: (event) => setUrl(event.target.value), onKeyDown: (event) => {
|
|
30
|
+
preventFormSubmitOnEnter(event);
|
|
25
31
|
if (event.key === "Enter")
|
|
26
32
|
apply();
|
|
27
33
|
if (event.key === "Escape")
|
|
@@ -31,6 +37,9 @@ export function LinkInsertPanel({ editor, close }) {
|
|
|
31
37
|
function insertImageSrc(editor, src) {
|
|
32
38
|
editor.chain().focus().setImage({ src }).run();
|
|
33
39
|
}
|
|
40
|
+
function resolveGalleryPhotoSrc(photo) {
|
|
41
|
+
return photo.thumbnailUrl?.trim() || photo.url;
|
|
42
|
+
}
|
|
34
43
|
export function ImageInsertPanel({ editor, close, config }) {
|
|
35
44
|
const inputRef = useRef(null);
|
|
36
45
|
const [url, setUrl] = useState("");
|
|
@@ -40,6 +49,11 @@ export function ImageInsertPanel({ editor, close, config }) {
|
|
|
40
49
|
const maxSizeBytes = config?.maxSizeBytes ?? 5 * 1024 * 1024;
|
|
41
50
|
const accept = config?.accept ?? "image/*";
|
|
42
51
|
const limitMb = Math.round(maxSizeBytes / (1024 * 1024));
|
|
52
|
+
const galleryPhotos = config?.galleryPhotos ?? [];
|
|
53
|
+
const galleryEnabled = Boolean(config?.onGalleryOpen || galleryPhotos.length > 0);
|
|
54
|
+
useEffect(() => {
|
|
55
|
+
config?.onGalleryPrefetch?.();
|
|
56
|
+
}, [config]);
|
|
43
57
|
const handleFiles = async (files) => {
|
|
44
58
|
setError(null);
|
|
45
59
|
const inserted = await insertImageFilesIntoEditor(editor, files, config);
|
|
@@ -56,7 +70,25 @@ export function ImageInsertPanel({ editor, close, config }) {
|
|
|
56
70
|
insertImageSrc(editor, trimmed);
|
|
57
71
|
close();
|
|
58
72
|
};
|
|
59
|
-
|
|
73
|
+
const insertGalleryPhoto = (photoUrl) => {
|
|
74
|
+
if (!photoUrl.trim())
|
|
75
|
+
return;
|
|
76
|
+
insertImageSrc(editor, photoUrl);
|
|
77
|
+
close();
|
|
78
|
+
};
|
|
79
|
+
const openHostGallery = () => {
|
|
80
|
+
config?.onGalleryOpen?.();
|
|
81
|
+
close();
|
|
82
|
+
};
|
|
83
|
+
return (_jsxs("div", { className: "spk-editor-toolbar-popover-form spk-editor-image-insert", children: [galleryEnabled ? (_jsxs("div", { className: "spk-editor-image-gallery", children: [galleryPhotos.length > 0 ? (_jsx("div", { className: "spk-editor-image-gallery-grid", children: galleryPhotos.slice(0, 12).map((photo) => (_jsx("button", { type: "button", className: "spk-editor-image-gallery-item", onClick: (event) => {
|
|
84
|
+
event.preventDefault();
|
|
85
|
+
event.stopPropagation();
|
|
86
|
+
insertGalleryPhoto(photo.url);
|
|
87
|
+
}, children: _jsx("img", { src: resolveGalleryPhotoSrc(photo), alt: photo.alt ?? "Gallery photo" }) }, photo.id))) })) : (_jsx("p", { className: "spk-editor-image-gallery-empty", children: config?.galleryEmptyMessage ?? "No gallery photos available yet." })), config?.onGalleryOpen ? (_jsxs("button", { type: "button", className: "spk-editor-image-gallery-open", onClick: (event) => {
|
|
88
|
+
event.preventDefault();
|
|
89
|
+
event.stopPropagation();
|
|
90
|
+
openHostGallery();
|
|
91
|
+
}, children: [_jsx(Images, { className: "size-4", "aria-hidden": "true" }), config.galleryOpenLabel ?? "Open gallery"] })) : null] })) : null, galleryEnabled ? (_jsx("div", { className: "spk-editor-image-insert-divider", children: _jsx("span", { children: "or upload" }) })) : null, _jsxs("button", { type: "button", className: cn("spk-editor-image-dropzone", dragging && "spk-editor-image-dropzone--active"), onClick: () => inputRef.current?.click(), onDragEnter: (event) => {
|
|
60
92
|
event.preventDefault();
|
|
61
93
|
setDragging(true);
|
|
62
94
|
}, onDragOver: (event) => {
|
|
@@ -73,6 +105,7 @@ export function ImageInsertPanel({ editor, close, config }) {
|
|
|
73
105
|
void handleFiles(event.target.files);
|
|
74
106
|
}
|
|
75
107
|
} }), error ? _jsx("p", { className: "spk-editor-image-insert-error", children: error }) : null, _jsx("div", { className: "spk-editor-image-insert-divider", children: _jsx("span", { children: "or paste a URL" }) }), _jsxs("label", { className: "spk-editor-toolbar-popover-field", children: [_jsx("span", { children: "Image URL" }), _jsx("input", { type: "url", className: "spk-editor-toolbar-popover-input", value: url, placeholder: "https://example.com/photo.jpg", onChange: (event) => setUrl(event.target.value), onKeyDown: (event) => {
|
|
108
|
+
preventFormSubmitOnEnter(event);
|
|
76
109
|
if (event.key === "Enter")
|
|
77
110
|
insertFromUrl();
|
|
78
111
|
if (event.key === "Escape")
|
package/dist/index.d.ts
CHANGED
|
@@ -16,7 +16,7 @@ export { createSlashCommandExtension, defaultSlashCommands } from "./extensions/
|
|
|
16
16
|
export { createMentionExtension } from "./extensions/mention";
|
|
17
17
|
export { ResizableImage } from "./extensions/resizable-image";
|
|
18
18
|
export { createAiActionExtension } from "./extensions/ai-action";
|
|
19
|
-
export type { SpatikaEditorProps, SpatikaEditorHandle, SpatikaEditorToolbarConfig, SlashCommandItem, MentionContact, AiActionDefinition, AiCommandContext, AiCommandMenuConfig, AiCommandPromptConfig, ImageInsertConfig, PlaceholderConfig, UseSpatikaEditorOptions, } from "./lib/types";
|
|
19
|
+
export type { SpatikaEditorProps, SpatikaEditorHandle, SpatikaEditorToolbarConfig, SlashCommandItem, MentionContact, AiActionDefinition, AiCommandContext, AiCommandMenuConfig, AiCommandPromptConfig, ImageInsertConfig, ImageInsertGalleryPhoto, PlaceholderConfig, UseSpatikaEditorOptions, } from "./lib/types";
|
|
20
20
|
export { AI_PROMPT_ACTION_ID } from "./lib/types";
|
|
21
21
|
export type { SlashCommandStorage } from "./extensions/slash-command";
|
|
22
22
|
export type { MentionStorage } from "./extensions/mention";
|
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,aAAa,EAAE,MAAM,4BAA4B,CAAC;AAC3D,OAAO,EAAE,aAAa,EAAE,MAAM,4BAA4B,CAAC;AAC3D,OAAO,EAAE,gBAAgB,EAAE,MAAM,+BAA+B,CAAC;AACjE,OAAO,EAAE,eAAe,EAAE,MAAM,8BAA8B,CAAC;AAC/D,OAAO,EAAE,gBAAgB,EAAE,MAAM,+BAA+B,CAAC;AACjE,OAAO,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AACvD,OAAO,EAAE,aAAa,EAAE,MAAM,4BAA4B,CAAC;AAC3D,OAAO,EAAE,aAAa,EAAE,MAAM,4BAA4B,CAAC;AAC3D,OAAO,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAC;AAC7D,YAAY,EAAE,mBAAmB,EAAE,MAAM,4BAA4B,CAAC;AACtE,OAAO,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC;AACnD,OAAO,EAAE,aAAa,EAAE,MAAM,4BAA4B,CAAC;AAE3D,OAAO,EAAE,gBAAgB,EAAE,sBAAsB,EAAE,MAAM,0BAA0B,CAAC;AAEpF,OAAO,EACL,kBAAkB,EAClB,mBAAmB,EACnB,uBAAuB,EACvB,yBAAyB,EACzB,qBAAqB,EACrB,iBAAiB,GAClB,MAAM,uBAAuB,CAAC;AAE/B,OAAO,EAAE,2BAA2B,EAAE,oBAAoB,EAAE,MAAM,4BAA4B,CAAC;AAC/F,OAAO,EAAE,sBAAsB,EAAE,MAAM,sBAAsB,CAAC;AAC9D,OAAO,EAAE,cAAc,EAAE,MAAM,8BAA8B,CAAC;AAC9D,OAAO,EAAE,uBAAuB,EAAE,MAAM,wBAAwB,CAAC;AAEjE,YAAY,EACV,kBAAkB,EAClB,mBAAmB,EACnB,0BAA0B,EAC1B,gBAAgB,EAChB,cAAc,EACd,kBAAkB,EAClB,gBAAgB,EAChB,mBAAmB,EACnB,qBAAqB,EACrB,iBAAiB,EACjB,iBAAiB,EACjB,uBAAuB,GACxB,MAAM,aAAa,CAAC;AAErB,OAAO,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAC;AAElD,YAAY,EAAE,mBAAmB,EAAE,MAAM,4BAA4B,CAAC;AACtE,YAAY,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAC3D,YAAY,EAAE,UAAU,EAAE,MAAM,8BAA8B,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,4BAA4B,CAAC;AAC3D,OAAO,EAAE,aAAa,EAAE,MAAM,4BAA4B,CAAC;AAC3D,OAAO,EAAE,gBAAgB,EAAE,MAAM,+BAA+B,CAAC;AACjE,OAAO,EAAE,eAAe,EAAE,MAAM,8BAA8B,CAAC;AAC/D,OAAO,EAAE,gBAAgB,EAAE,MAAM,+BAA+B,CAAC;AACjE,OAAO,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AACvD,OAAO,EAAE,aAAa,EAAE,MAAM,4BAA4B,CAAC;AAC3D,OAAO,EAAE,aAAa,EAAE,MAAM,4BAA4B,CAAC;AAC3D,OAAO,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAC;AAC7D,YAAY,EAAE,mBAAmB,EAAE,MAAM,4BAA4B,CAAC;AACtE,OAAO,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC;AACnD,OAAO,EAAE,aAAa,EAAE,MAAM,4BAA4B,CAAC;AAE3D,OAAO,EAAE,gBAAgB,EAAE,sBAAsB,EAAE,MAAM,0BAA0B,CAAC;AAEpF,OAAO,EACL,kBAAkB,EAClB,mBAAmB,EACnB,uBAAuB,EACvB,yBAAyB,EACzB,qBAAqB,EACrB,iBAAiB,GAClB,MAAM,uBAAuB,CAAC;AAE/B,OAAO,EAAE,2BAA2B,EAAE,oBAAoB,EAAE,MAAM,4BAA4B,CAAC;AAC/F,OAAO,EAAE,sBAAsB,EAAE,MAAM,sBAAsB,CAAC;AAC9D,OAAO,EAAE,cAAc,EAAE,MAAM,8BAA8B,CAAC;AAC9D,OAAO,EAAE,uBAAuB,EAAE,MAAM,wBAAwB,CAAC;AAEjE,YAAY,EACV,kBAAkB,EAClB,mBAAmB,EACnB,0BAA0B,EAC1B,gBAAgB,EAChB,cAAc,EACd,kBAAkB,EAClB,gBAAgB,EAChB,mBAAmB,EACnB,qBAAqB,EACrB,iBAAiB,EACjB,uBAAuB,EACvB,iBAAiB,EACjB,uBAAuB,GACxB,MAAM,aAAa,CAAC;AAErB,OAAO,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAC;AAElD,YAAY,EAAE,mBAAmB,EAAE,MAAM,4BAA4B,CAAC;AACtE,YAAY,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAC3D,YAAY,EAAE,UAAU,EAAE,MAAM,8BAA8B,CAAC"}
|
package/dist/lib/types.d.ts
CHANGED
|
@@ -19,6 +19,12 @@ export type SpatikaEditorToolbarConfig = {
|
|
|
19
19
|
/** Toolbar layout. Default "ribbon". */
|
|
20
20
|
layout?: "ribbon" | "compact";
|
|
21
21
|
};
|
|
22
|
+
export type ImageInsertGalleryPhoto = {
|
|
23
|
+
id: string | number;
|
|
24
|
+
url: string;
|
|
25
|
+
thumbnailUrl?: string;
|
|
26
|
+
alt?: string;
|
|
27
|
+
};
|
|
22
28
|
export type ImageInsertConfig = {
|
|
23
29
|
/** Maximum files per drop or picker action. Default 3. */
|
|
24
30
|
maxFiles?: number;
|
|
@@ -28,6 +34,16 @@ export type ImageInsertConfig = {
|
|
|
28
34
|
accept?: string;
|
|
29
35
|
/** Host-owned upload handler. Defaults to inline base64 data URLs. */
|
|
30
36
|
onUpload?: (file: File) => Promise<string>;
|
|
37
|
+
/** Optional quick-pick gallery shown above upload/URL controls. */
|
|
38
|
+
galleryPhotos?: ImageInsertGalleryPhoto[];
|
|
39
|
+
/** Called when the insert-image panel opens (e.g. prefetch gallery assets). */
|
|
40
|
+
onGalleryPrefetch?: () => void;
|
|
41
|
+
/** Opens a host gallery picker (full library). */
|
|
42
|
+
onGalleryOpen?: () => void;
|
|
43
|
+
/** Empty-state copy when `galleryPhotos` is empty but gallery is enabled. */
|
|
44
|
+
galleryEmptyMessage?: string;
|
|
45
|
+
/** Label for the host gallery action button. Default "Open gallery". */
|
|
46
|
+
galleryOpenLabel?: string;
|
|
31
47
|
};
|
|
32
48
|
export type SlashCommandItem = {
|
|
33
49
|
id: string;
|
|
@@ -89,6 +105,7 @@ export type SpatikaEditorHandle = {
|
|
|
89
105
|
getHTML: () => string;
|
|
90
106
|
getJSON: () => unknown;
|
|
91
107
|
expandFullscreen: () => void;
|
|
108
|
+
insertImage: (url: string) => void;
|
|
92
109
|
};
|
|
93
110
|
export type PlaceholderConfig = string | ((node: {
|
|
94
111
|
type: string;
|
package/dist/lib/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/lib/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AACvD,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AAC7D,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAEvC,MAAM,MAAM,0BAA0B,GAAG;IACvC,iEAAiE;IACjE,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,iHAAiH;IACjH,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,iEAAiE;IACjE,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,wEAAwE;IACxE,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,wEAAwE;IACxE,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,qCAAqC;IACrC,EAAE,CAAC,EAAE,OAAO,CAAC;IACb,6CAA6C;IAC7C,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,wCAAwC;IACxC,MAAM,CAAC,EAAE,QAAQ,GAAG,SAAS,CAAC;CAC/B,CAAC;AAEF,MAAM,MAAM,iBAAiB,GAAG;IAC9B,0DAA0D;IAC1D,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,2CAA2C;IAC3C,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,0DAA0D;IAC1D,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,sEAAsE;IACtE,QAAQ,CAAC,EAAE,CAAC,IAAI,EAAE,IAAI,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/lib/types.ts"],"names":[],"mappings":"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"}
|
package/dist/styles.css
CHANGED
|
@@ -721,6 +721,70 @@
|
|
|
721
721
|
min-width: 16rem;
|
|
722
722
|
}
|
|
723
723
|
|
|
724
|
+
.spk-editor-image-gallery {
|
|
725
|
+
display: flex;
|
|
726
|
+
flex-direction: column;
|
|
727
|
+
gap: 0.5rem;
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
.spk-editor-image-gallery-grid {
|
|
731
|
+
display: grid;
|
|
732
|
+
grid-template-columns: repeat(4, minmax(0, 1fr));
|
|
733
|
+
gap: 0.4rem;
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
.spk-editor-image-gallery-item {
|
|
737
|
+
position: relative;
|
|
738
|
+
aspect-ratio: 1;
|
|
739
|
+
overflow: hidden;
|
|
740
|
+
border: 2px solid transparent;
|
|
741
|
+
border-radius: 0.65rem;
|
|
742
|
+
background: color-mix(in oklab, var(--muted) 40%, transparent);
|
|
743
|
+
padding: 0;
|
|
744
|
+
cursor: pointer;
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
.spk-editor-image-gallery-item:hover {
|
|
748
|
+
border-color: color-mix(in oklab, var(--primary) 55%, transparent);
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
.spk-editor-image-gallery-item img {
|
|
752
|
+
width: 100%;
|
|
753
|
+
height: 100%;
|
|
754
|
+
object-fit: cover;
|
|
755
|
+
display: block;
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
.spk-editor-image-gallery-empty {
|
|
759
|
+
margin: 0;
|
|
760
|
+
padding: 0.5rem 0.25rem;
|
|
761
|
+
font-size: 0.75rem;
|
|
762
|
+
line-height: 1.4;
|
|
763
|
+
color: var(--muted-foreground);
|
|
764
|
+
text-align: center;
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
.spk-editor-image-gallery-open {
|
|
768
|
+
display: inline-flex;
|
|
769
|
+
align-items: center;
|
|
770
|
+
justify-content: center;
|
|
771
|
+
gap: 0.4rem;
|
|
772
|
+
width: 100%;
|
|
773
|
+
min-height: 2rem;
|
|
774
|
+
border: 1px solid color-mix(in oklab, var(--border) 80%, transparent);
|
|
775
|
+
border-radius: 0.65rem;
|
|
776
|
+
background: color-mix(in oklab, var(--muted) 24%, transparent);
|
|
777
|
+
color: var(--foreground);
|
|
778
|
+
font-size: 0.8125rem;
|
|
779
|
+
font-weight: 600;
|
|
780
|
+
cursor: pointer;
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
.spk-editor-image-gallery-open:hover {
|
|
784
|
+
border-color: color-mix(in oklab, var(--primary) 35%, var(--border));
|
|
785
|
+
background: color-mix(in oklab, var(--primary) 8%, transparent);
|
|
786
|
+
}
|
|
787
|
+
|
|
724
788
|
.spk-editor-image-dropzone {
|
|
725
789
|
display: flex;
|
|
726
790
|
flex-direction: column;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@spatika/editor",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.6.1",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"description": "Spatika UI — Tiptap-powered rich text editor with glass theming, AI hooks, and extensible toolbar",
|
|
6
6
|
"homepage": "https://spatika.sweyam.com",
|
|
@@ -58,7 +58,7 @@
|
|
|
58
58
|
"react-dom": "^18.2.0 || ^19.0.0"
|
|
59
59
|
},
|
|
60
60
|
"dependencies": {
|
|
61
|
-
"@spatika/tokens": "1.5.0",
|
|
61
|
+
"@spatika/tokens": "^1.5.0",
|
|
62
62
|
"@tiptap/core": "^3.28.0",
|
|
63
63
|
"@tiptap/extension-bubble-menu": "^3.28.0",
|
|
64
64
|
"@tiptap/extension-color": "^3.30.5",
|
|
@@ -115,10 +115,13 @@ export function AiCommandDock({ editor, config, actions, onRun, className }: AiC
|
|
|
115
115
|
placeholder={promptConfig.placeholder}
|
|
116
116
|
onFocus={() => setOpen(true)}
|
|
117
117
|
onChange={(event) => setPrompt(event.target.value)}
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
118
|
+
onKeyDown={(event) => {
|
|
119
|
+
if (event.key === "Enter") {
|
|
120
|
+
event.preventDefault();
|
|
121
|
+
submitPrompt();
|
|
122
|
+
}
|
|
123
|
+
if (event.key === "Escape") setOpen(false);
|
|
124
|
+
}}
|
|
122
125
|
/>
|
|
123
126
|
</label>
|
|
124
127
|
<button
|
|
@@ -90,6 +90,10 @@ export const SpatikaEditor = forwardRef<SpatikaEditorHandle, SpatikaEditorProps>
|
|
|
90
90
|
getHTML: () => editor?.getHTML() ?? "",
|
|
91
91
|
getJSON: () => editor?.getJSON(),
|
|
92
92
|
expandFullscreen: () => setFullscreen(true),
|
|
93
|
+
insertImage: (url: string) => {
|
|
94
|
+
if (!url.trim() || !editor) return;
|
|
95
|
+
editor.chain().focus().setImage({ src: url }).run();
|
|
96
|
+
},
|
|
93
97
|
}),
|
|
94
98
|
[editor],
|
|
95
99
|
);
|
|
@@ -97,6 +97,9 @@ export function TableGridPicker({ onInsert }: TableGridPickerProps) {
|
|
|
97
97
|
setHover(null);
|
|
98
98
|
setCols(clampDimension(Number.parseInt(event.target.value, 10) || 1, TABLE_COLS_MAX));
|
|
99
99
|
}}
|
|
100
|
+
onKeyDown={(event) => {
|
|
101
|
+
if (event.key === "Enter") event.preventDefault();
|
|
102
|
+
}}
|
|
100
103
|
/>
|
|
101
104
|
</label>
|
|
102
105
|
<span className="spk-editor-table-picker-size-sep" aria-hidden="true">
|
|
@@ -115,6 +118,9 @@ export function TableGridPicker({ onInsert }: TableGridPickerProps) {
|
|
|
115
118
|
setHover(null);
|
|
116
119
|
setRows(clampDimension(Number.parseInt(event.target.value, 10) || 1, TABLE_ROWS_MAX));
|
|
117
120
|
}}
|
|
121
|
+
onKeyDown={(event) => {
|
|
122
|
+
if (event.key === "Enter") event.preventDefault();
|
|
123
|
+
}}
|
|
118
124
|
/>
|
|
119
125
|
</label>
|
|
120
126
|
</div>
|
|
@@ -57,4 +57,84 @@ describe("ToolbarInsertPanels", () => {
|
|
|
57
57
|
});
|
|
58
58
|
expect(close).toHaveBeenCalled();
|
|
59
59
|
});
|
|
60
|
+
|
|
61
|
+
it("inserts a gallery photo from the quick-pick grid", async () => {
|
|
62
|
+
const editor = createEditor();
|
|
63
|
+
const close = vi.fn();
|
|
64
|
+
const onGalleryPrefetch = vi.fn();
|
|
65
|
+
|
|
66
|
+
render(
|
|
67
|
+
<ImageInsertPanel
|
|
68
|
+
editor={editor}
|
|
69
|
+
close={close}
|
|
70
|
+
config={{
|
|
71
|
+
galleryPhotos: [{ id: 1, url: "https://example.com/gallery.png", alt: "Gallery photo" }],
|
|
72
|
+
onGalleryPrefetch,
|
|
73
|
+
galleryOpenLabel: "Open full gallery",
|
|
74
|
+
}}
|
|
75
|
+
/>,
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
expect(onGalleryPrefetch).toHaveBeenCalled();
|
|
79
|
+
await userEvent.click(screen.getByRole("button", { name: "Gallery photo" }));
|
|
80
|
+
expect(editor.chain).toHaveBeenCalled();
|
|
81
|
+
expect(close).toHaveBeenCalled();
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("opens the host gallery action from the insert panel", async () => {
|
|
85
|
+
const editor = createEditor();
|
|
86
|
+
const close = vi.fn();
|
|
87
|
+
const onGalleryOpen = vi.fn();
|
|
88
|
+
|
|
89
|
+
render(
|
|
90
|
+
<ImageInsertPanel
|
|
91
|
+
editor={editor}
|
|
92
|
+
close={close}
|
|
93
|
+
config={{ onGalleryOpen, galleryOpenLabel: "Open full gallery" }}
|
|
94
|
+
/>,
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
await userEvent.click(screen.getByRole("button", { name: "Open full gallery" }));
|
|
98
|
+
expect(onGalleryOpen).toHaveBeenCalled();
|
|
99
|
+
expect(close).toHaveBeenCalled();
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("does not submit surrounding forms when pressing Enter in the URL field", async () => {
|
|
103
|
+
const editor = createEditor();
|
|
104
|
+
const close = vi.fn();
|
|
105
|
+
const onSubmit = vi.fn((event: SubmitEvent) => event.preventDefault());
|
|
106
|
+
|
|
107
|
+
render(
|
|
108
|
+
<form onSubmit={onSubmit}>
|
|
109
|
+
<ImageInsertPanel editor={editor} close={close} />
|
|
110
|
+
</form>,
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
const input = screen.getByLabelText("Image URL");
|
|
114
|
+
await userEvent.type(input, "https://example.com/a.png{enter}");
|
|
115
|
+
expect(onSubmit).not.toHaveBeenCalled();
|
|
116
|
+
expect(editor.chain).toHaveBeenCalled();
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it("does not submit surrounding forms when clicking a gallery photo", async () => {
|
|
120
|
+
const editor = createEditor();
|
|
121
|
+
const close = vi.fn();
|
|
122
|
+
const onSubmit = vi.fn((event: SubmitEvent) => event.preventDefault());
|
|
123
|
+
|
|
124
|
+
render(
|
|
125
|
+
<form onSubmit={onSubmit}>
|
|
126
|
+
<ImageInsertPanel
|
|
127
|
+
editor={editor}
|
|
128
|
+
close={close}
|
|
129
|
+
config={{
|
|
130
|
+
galleryPhotos: [{ id: 1, url: "https://example.com/gallery.png", alt: "Gallery photo" }],
|
|
131
|
+
}}
|
|
132
|
+
/>
|
|
133
|
+
</form>,
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
await userEvent.click(screen.getByRole("button", { name: "Gallery photo" }));
|
|
137
|
+
expect(onSubmit).not.toHaveBeenCalled();
|
|
138
|
+
expect(editor.chain).toHaveBeenCalled();
|
|
139
|
+
});
|
|
60
140
|
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Editor } from "@tiptap/core";
|
|
2
|
-
import { ImagePlus, Upload } from "lucide-react";
|
|
3
|
-
import { useRef, useState } from "react";
|
|
2
|
+
import { ImagePlus, Images, Upload } from "lucide-react";
|
|
3
|
+
import { useEffect, useRef, useState } from "react";
|
|
4
4
|
import type { ImageInsertConfig } from "../lib/types";
|
|
5
5
|
import { cn } from "../lib/cn";
|
|
6
6
|
import { insertImageFilesIntoEditor } from "../lib/insert-editor-images";
|
|
@@ -15,6 +15,12 @@ type ImagePanelProps = PanelProps & {
|
|
|
15
15
|
config?: ImageInsertConfig;
|
|
16
16
|
};
|
|
17
17
|
|
|
18
|
+
function preventFormSubmitOnEnter(event: React.KeyboardEvent) {
|
|
19
|
+
if (event.key === "Enter") {
|
|
20
|
+
event.preventDefault();
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
18
24
|
export function LinkInsertPanel({ editor, close }: PanelProps) {
|
|
19
25
|
const previous = editor.getAttributes("link").href as string | undefined;
|
|
20
26
|
const [url, setUrl] = useState(previous ?? "https://");
|
|
@@ -45,6 +51,7 @@ export function LinkInsertPanel({ editor, close }: PanelProps) {
|
|
|
45
51
|
placeholder="https://"
|
|
46
52
|
onChange={(event) => setUrl(event.target.value)}
|
|
47
53
|
onKeyDown={(event) => {
|
|
54
|
+
preventFormSubmitOnEnter(event);
|
|
48
55
|
if (event.key === "Enter") apply();
|
|
49
56
|
if (event.key === "Escape") close();
|
|
50
57
|
}}
|
|
@@ -72,6 +79,10 @@ function insertImageSrc(editor: Editor, src: string) {
|
|
|
72
79
|
editor.chain().focus().setImage({ src }).run();
|
|
73
80
|
}
|
|
74
81
|
|
|
82
|
+
function resolveGalleryPhotoSrc(photo: NonNullable<ImageInsertConfig["galleryPhotos"]>[number]) {
|
|
83
|
+
return photo.thumbnailUrl?.trim() || photo.url;
|
|
84
|
+
}
|
|
85
|
+
|
|
75
86
|
export function ImageInsertPanel({ editor, close, config }: ImagePanelProps) {
|
|
76
87
|
const inputRef = useRef<HTMLInputElement>(null);
|
|
77
88
|
const [url, setUrl] = useState("");
|
|
@@ -82,6 +93,12 @@ export function ImageInsertPanel({ editor, close, config }: ImagePanelProps) {
|
|
|
82
93
|
const maxSizeBytes = config?.maxSizeBytes ?? 5 * 1024 * 1024;
|
|
83
94
|
const accept = config?.accept ?? "image/*";
|
|
84
95
|
const limitMb = Math.round(maxSizeBytes / (1024 * 1024));
|
|
96
|
+
const galleryPhotos = config?.galleryPhotos ?? [];
|
|
97
|
+
const galleryEnabled = Boolean(config?.onGalleryOpen || galleryPhotos.length > 0);
|
|
98
|
+
|
|
99
|
+
useEffect(() => {
|
|
100
|
+
config?.onGalleryPrefetch?.();
|
|
101
|
+
}, [config]);
|
|
85
102
|
|
|
86
103
|
const handleFiles = async (files: FileList | File[]) => {
|
|
87
104
|
setError(null);
|
|
@@ -100,8 +117,60 @@ export function ImageInsertPanel({ editor, close, config }: ImagePanelProps) {
|
|
|
100
117
|
close();
|
|
101
118
|
};
|
|
102
119
|
|
|
120
|
+
const insertGalleryPhoto = (photoUrl: string) => {
|
|
121
|
+
if (!photoUrl.trim()) return;
|
|
122
|
+
insertImageSrc(editor, photoUrl);
|
|
123
|
+
close();
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
const openHostGallery = () => {
|
|
127
|
+
config?.onGalleryOpen?.();
|
|
128
|
+
close();
|
|
129
|
+
};
|
|
130
|
+
|
|
103
131
|
return (
|
|
104
132
|
<div className="spk-editor-toolbar-popover-form spk-editor-image-insert">
|
|
133
|
+
{galleryEnabled ? (
|
|
134
|
+
<div className="spk-editor-image-gallery">
|
|
135
|
+
{galleryPhotos.length > 0 ? (
|
|
136
|
+
<div className="spk-editor-image-gallery-grid">
|
|
137
|
+
{galleryPhotos.slice(0, 12).map((photo) => (
|
|
138
|
+
<button
|
|
139
|
+
key={photo.id}
|
|
140
|
+
type="button"
|
|
141
|
+
className="spk-editor-image-gallery-item"
|
|
142
|
+
onClick={(event) => {
|
|
143
|
+
event.preventDefault();
|
|
144
|
+
event.stopPropagation();
|
|
145
|
+
insertGalleryPhoto(photo.url);
|
|
146
|
+
}}
|
|
147
|
+
>
|
|
148
|
+
<img src={resolveGalleryPhotoSrc(photo)} alt={photo.alt ?? "Gallery photo"} />
|
|
149
|
+
</button>
|
|
150
|
+
))}
|
|
151
|
+
</div>
|
|
152
|
+
) : (
|
|
153
|
+
<p className="spk-editor-image-gallery-empty">
|
|
154
|
+
{config?.galleryEmptyMessage ?? "No gallery photos available yet."}
|
|
155
|
+
</p>
|
|
156
|
+
)}
|
|
157
|
+
{config?.onGalleryOpen ? (
|
|
158
|
+
<button type="button" className="spk-editor-image-gallery-open" onClick={(event) => {
|
|
159
|
+
event.preventDefault();
|
|
160
|
+
event.stopPropagation();
|
|
161
|
+
openHostGallery();
|
|
162
|
+
}}>
|
|
163
|
+
<Images className="size-4" aria-hidden="true" />
|
|
164
|
+
{config.galleryOpenLabel ?? "Open gallery"}
|
|
165
|
+
</button>
|
|
166
|
+
) : null}
|
|
167
|
+
</div>
|
|
168
|
+
) : null}
|
|
169
|
+
{galleryEnabled ? (
|
|
170
|
+
<div className="spk-editor-image-insert-divider">
|
|
171
|
+
<span>or upload</span>
|
|
172
|
+
</div>
|
|
173
|
+
) : null}
|
|
105
174
|
<button
|
|
106
175
|
type="button"
|
|
107
176
|
className={cn(
|
|
@@ -159,6 +228,7 @@ export function ImageInsertPanel({ editor, close, config }: ImagePanelProps) {
|
|
|
159
228
|
placeholder="https://example.com/photo.jpg"
|
|
160
229
|
onChange={(event) => setUrl(event.target.value)}
|
|
161
230
|
onKeyDown={(event) => {
|
|
231
|
+
preventFormSubmitOnEnter(event);
|
|
162
232
|
if (event.key === "Enter") insertFromUrl();
|
|
163
233
|
if (event.key === "Escape") close();
|
|
164
234
|
}}
|
package/src/index.ts
CHANGED
package/src/lib/types.ts
CHANGED
|
@@ -21,6 +21,13 @@ export type SpatikaEditorToolbarConfig = {
|
|
|
21
21
|
layout?: "ribbon" | "compact";
|
|
22
22
|
};
|
|
23
23
|
|
|
24
|
+
export type ImageInsertGalleryPhoto = {
|
|
25
|
+
id: string | number;
|
|
26
|
+
url: string;
|
|
27
|
+
thumbnailUrl?: string;
|
|
28
|
+
alt?: string;
|
|
29
|
+
};
|
|
30
|
+
|
|
24
31
|
export type ImageInsertConfig = {
|
|
25
32
|
/** Maximum files per drop or picker action. Default 3. */
|
|
26
33
|
maxFiles?: number;
|
|
@@ -30,6 +37,16 @@ export type ImageInsertConfig = {
|
|
|
30
37
|
accept?: string;
|
|
31
38
|
/** Host-owned upload handler. Defaults to inline base64 data URLs. */
|
|
32
39
|
onUpload?: (file: File) => Promise<string>;
|
|
40
|
+
/** Optional quick-pick gallery shown above upload/URL controls. */
|
|
41
|
+
galleryPhotos?: ImageInsertGalleryPhoto[];
|
|
42
|
+
/** Called when the insert-image panel opens (e.g. prefetch gallery assets). */
|
|
43
|
+
onGalleryPrefetch?: () => void;
|
|
44
|
+
/** Opens a host gallery picker (full library). */
|
|
45
|
+
onGalleryOpen?: () => void;
|
|
46
|
+
/** Empty-state copy when `galleryPhotos` is empty but gallery is enabled. */
|
|
47
|
+
galleryEmptyMessage?: string;
|
|
48
|
+
/** Label for the host gallery action button. Default "Open gallery". */
|
|
49
|
+
galleryOpenLabel?: string;
|
|
33
50
|
};
|
|
34
51
|
|
|
35
52
|
export type SlashCommandItem = {
|
|
@@ -96,6 +113,7 @@ export type SpatikaEditorHandle = {
|
|
|
96
113
|
getHTML: () => string;
|
|
97
114
|
getJSON: () => unknown;
|
|
98
115
|
expandFullscreen: () => void;
|
|
116
|
+
insertImage: (url: string) => void;
|
|
99
117
|
};
|
|
100
118
|
|
|
101
119
|
export type PlaceholderConfig =
|
package/src/styles/editor.css
CHANGED
|
@@ -721,6 +721,70 @@
|
|
|
721
721
|
min-width: 16rem;
|
|
722
722
|
}
|
|
723
723
|
|
|
724
|
+
.spk-editor-image-gallery {
|
|
725
|
+
display: flex;
|
|
726
|
+
flex-direction: column;
|
|
727
|
+
gap: 0.5rem;
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
.spk-editor-image-gallery-grid {
|
|
731
|
+
display: grid;
|
|
732
|
+
grid-template-columns: repeat(4, minmax(0, 1fr));
|
|
733
|
+
gap: 0.4rem;
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
.spk-editor-image-gallery-item {
|
|
737
|
+
position: relative;
|
|
738
|
+
aspect-ratio: 1;
|
|
739
|
+
overflow: hidden;
|
|
740
|
+
border: 2px solid transparent;
|
|
741
|
+
border-radius: 0.65rem;
|
|
742
|
+
background: color-mix(in oklab, var(--muted) 40%, transparent);
|
|
743
|
+
padding: 0;
|
|
744
|
+
cursor: pointer;
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
.spk-editor-image-gallery-item:hover {
|
|
748
|
+
border-color: color-mix(in oklab, var(--primary) 55%, transparent);
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
.spk-editor-image-gallery-item img {
|
|
752
|
+
width: 100%;
|
|
753
|
+
height: 100%;
|
|
754
|
+
object-fit: cover;
|
|
755
|
+
display: block;
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
.spk-editor-image-gallery-empty {
|
|
759
|
+
margin: 0;
|
|
760
|
+
padding: 0.5rem 0.25rem;
|
|
761
|
+
font-size: 0.75rem;
|
|
762
|
+
line-height: 1.4;
|
|
763
|
+
color: var(--muted-foreground);
|
|
764
|
+
text-align: center;
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
.spk-editor-image-gallery-open {
|
|
768
|
+
display: inline-flex;
|
|
769
|
+
align-items: center;
|
|
770
|
+
justify-content: center;
|
|
771
|
+
gap: 0.4rem;
|
|
772
|
+
width: 100%;
|
|
773
|
+
min-height: 2rem;
|
|
774
|
+
border: 1px solid color-mix(in oklab, var(--border) 80%, transparent);
|
|
775
|
+
border-radius: 0.65rem;
|
|
776
|
+
background: color-mix(in oklab, var(--muted) 24%, transparent);
|
|
777
|
+
color: var(--foreground);
|
|
778
|
+
font-size: 0.8125rem;
|
|
779
|
+
font-weight: 600;
|
|
780
|
+
cursor: pointer;
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
.spk-editor-image-gallery-open:hover {
|
|
784
|
+
border-color: color-mix(in oklab, var(--primary) 35%, var(--border));
|
|
785
|
+
background: color-mix(in oklab, var(--primary) 8%, transparent);
|
|
786
|
+
}
|
|
787
|
+
|
|
724
788
|
.spk-editor-image-dropzone {
|
|
725
789
|
display: flex;
|
|
726
790
|
flex-direction: column;
|