@spatika/editor 1.5.0 → 1.6.0

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.
@@ -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,sCAwF9F"}
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;;;;;;;;;;;uDA2QzB,CAAC"}
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,+BAgHjE"}
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;AAEF,wBAAgB,eAAe,CAAC,EAAE,MAAM,EAAE,KAAK,EAAE,EAAE,UAAU,+BAmD5D;AAMD,wBAAgB,gBAAgB,CAAC,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,EAAE,eAAe,+BA4G1E;AAED,wBAAgB,gBAAgB,CAAC,EAAE,MAAM,EAAE,KAAK,EAAE,EAAE,UAAU,+BAe7D"}
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
- return (_jsxs("div", { className: "spk-editor-toolbar-popover-form spk-editor-image-insert", children: [_jsxs("button", { type: "button", className: cn("spk-editor-image-dropzone", dragging && "spk-editor-image-dropzone--active"), onClick: () => inputRef.current?.click(), onDragEnter: (event) => {
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";
@@ -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"}
@@ -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;
@@ -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;CAC5C,CAAC;AAEF,MAAM,MAAM,gBAAgB,GAAG;IAC7B,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,IAAI,CAAC,EAAE,SAAS,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;IACpB,OAAO,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;CACnC,CAAC;AAEF,MAAM,MAAM,cAAc,GAAG;IAC3B,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB,CAAC;AAEF,MAAM,MAAM,kBAAkB,GAAG;IAC/B,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,IAAI,CAAC,EAAE,SAAS,CAAC;IACjB,uEAAuE;IACvE,iBAAiB,CAAC,EAAE,OAAO,CAAC;CAC7B,CAAC;AAEF,MAAM,MAAM,qBAAqB,GAAG;IAClC,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB,CAAC;AAEF,MAAM,MAAM,mBAAmB,GAAG;IAChC,4CAA4C;IAC5C,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,iFAAiF;IACjF,OAAO,CAAC,EAAE,kBAAkB,EAAE,CAAC;IAC/B,wEAAwE;IACxE,MAAM,CAAC,EAAE,OAAO,GAAG,qBAAqB,CAAC;IACzC,yEAAyE;IACzE,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,iFAAiF;IACjF,SAAS,CAAC,EAAE,SAAS,GAAG,MAAM,CAAC;CAChC,CAAC;AAEF,oFAAoF;AACpF,MAAM,MAAM,gBAAgB,GAAG;IAC7B,+DAA+D;IAC/D,QAAQ,EAAE,MAAM,CAAC;IACjB,qEAAqE;IACrE,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,YAAY,EAAE,MAAM,CAAC;IACrB,YAAY,EAAE,MAAM,CAAC;IACrB,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,EAAE,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;CAChD,CAAC;AAEF,eAAO,MAAM,mBAAmB,WAAW,CAAC;AAE5C,MAAM,MAAM,mBAAmB,GAAG;IAChC,KAAK,EAAE,MAAM,IAAI,CAAC;IAClB,UAAU,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IACnC,OAAO,EAAE,MAAM,MAAM,CAAC;IACtB,OAAO,EAAE,MAAM,OAAO,CAAC;IACvB,gBAAgB,EAAE,MAAM,IAAI,CAAC;CAC9B,CAAC;AAEF,MAAM,MAAM,iBAAiB,GACzB,MAAM,GACN,CAAC,CAAC,IAAI,EAAE;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,KAAK,MAAM,CAAC,CAAC;AAEtE,MAAM,MAAM,uBAAuB,GAAG;IACpC,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IACjC,WAAW,CAAC,EAAE,iBAAiB,CAAC;IAChC,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,UAAU,CAAC,EAAE,UAAU,CAAC;IACxB,UAAU,CAAC,EAAE,OAAO,CAAC,iBAAiB,CAAC,CAAC;IACxC,aAAa,CAAC,EAAE,gBAAgB,EAAE,CAAC;IACnC,QAAQ,CAAC,EAAE,cAAc,EAAE,CAAC;IAC5B,iFAAiF;IACjF,mBAAmB,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IACvF,WAAW,CAAC,EAAE,CAAC,OAAO,EAAE,gBAAgB,KAAK,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IACpE,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,aAAa,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,6BAA6B,EAAE,mBAAmB,KAAK,IAAI,CAAC;IAC3F,eAAe,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,uBAAuB,EAAE,cAAc,KAAK,IAAI,CAAC;IAClF,WAAW,CAAC,EAAE,iBAAiB,CAAC;CACjC,CAAC;AAEF,MAAM,MAAM,kBAAkB,GAAG,uBAAuB,GAAG;IACzD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,OAAO,CAAC,EAAE,0BAA0B,CAAC;IACrC,cAAc,CAAC,EAAE,SAAS,CAAC;IAC3B,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,yFAAyF;IACzF,SAAS,CAAC,EAAE,kBAAkB,EAAE,CAAC;IACjC,gHAAgH;IAChH,aAAa,CAAC,EAAE,mBAAmB,GAAG,KAAK,CAAC;IAC5C,WAAW,CAAC,EAAE,iBAAiB,CAAC;IAChC,aAAa,CAAC,EAAE,MAAM,IAAI,CAAC;IAC3B,gBAAgB,CAAC,EAAE,OAAO,CAAC;CAC5B,CAAC;AAEF,eAAO,MAAM,eAAe,EAAE,QAAQ,CAAC,0BAA0B,CAShE,CAAC;AAEF,eAAO,MAAM,sBAAsB,EAAE,gBAAgB,EAAO,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.5.0",
3
+ "version": "1.6.0",
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.6.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
- onKeyDown={(event) => {
119
- if (event.key === "Enter") submitPrompt();
120
- if (event.key === "Escape") setOpen(false);
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
@@ -38,6 +38,7 @@ export type {
38
38
  AiCommandMenuConfig,
39
39
  AiCommandPromptConfig,
40
40
  ImageInsertConfig,
41
+ ImageInsertGalleryPhoto,
41
42
  PlaceholderConfig,
42
43
  UseSpatikaEditorOptions,
43
44
  } from "./lib/types";
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 =
@@ -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;