@spatika/editor 1.5.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.
- package/README.md +43 -0
- package/dist/composites/AiCommandDock.d.ts +13 -0
- package/dist/composites/AiCommandDock.d.ts.map +1 -0
- package/dist/composites/AiCommandDock.js +63 -0
- package/dist/composites/AiCommandMenu.d.ts +16 -0
- package/dist/composites/AiCommandMenu.d.ts.map +1 -0
- package/dist/composites/AiCommandMenu.js +51 -0
- package/dist/composites/AiCommandPanel.d.ts +12 -0
- package/dist/composites/AiCommandPanel.d.ts.map +1 -0
- package/dist/composites/AiCommandPanel.js +12 -0
- package/dist/composites/AiOverlay.d.ts +7 -0
- package/dist/composites/AiOverlay.d.ts.map +1 -0
- package/dist/composites/AiOverlay.js +8 -0
- package/dist/composites/EditorBubbleMenu.d.ts +10 -0
- package/dist/composites/EditorBubbleMenu.d.ts.map +1 -0
- package/dist/composites/EditorBubbleMenu.js +16 -0
- package/dist/composites/EditorToolbar.d.ts +20 -0
- package/dist/composites/EditorToolbar.d.ts.map +1 -0
- package/dist/composites/EditorToolbar.js +45 -0
- package/dist/composites/ImageBubbleMenu.d.ts +7 -0
- package/dist/composites/ImageBubbleMenu.d.ts.map +1 -0
- package/dist/composites/ImageBubbleMenu.js +15 -0
- package/dist/composites/MentionMenu.d.ts +11 -0
- package/dist/composites/MentionMenu.d.ts.map +1 -0
- package/dist/composites/MentionMenu.js +10 -0
- package/dist/composites/SlashCommandMenu.d.ts +13 -0
- package/dist/composites/SlashCommandMenu.d.ts.map +1 -0
- package/dist/composites/SlashCommandMenu.js +10 -0
- package/dist/composites/SpatikaEditor.d.ts +14 -0
- package/dist/composites/SpatikaEditor.d.ts.map +1 -0
- package/dist/composites/SpatikaEditor.js +159 -0
- package/dist/composites/TableGridPicker.d.ts +9 -0
- package/dist/composites/TableGridPicker.d.ts.map +1 -0
- package/dist/composites/TableGridPicker.js +54 -0
- package/dist/composites/ToolbarButton.d.ts +7 -0
- package/dist/composites/ToolbarButton.d.ts.map +1 -0
- package/dist/composites/ToolbarButton.js +7 -0
- package/dist/composites/ToolbarFormatPanels.d.ts +9 -0
- package/dist/composites/ToolbarFormatPanels.d.ts.map +1 -0
- package/dist/composites/ToolbarFormatPanels.js +35 -0
- package/dist/composites/ToolbarInsertPanels.d.ts +14 -0
- package/dist/composites/ToolbarInsertPanels.d.ts.map +1 -0
- package/dist/composites/ToolbarInsertPanels.js +96 -0
- package/dist/composites/ToolbarPopover.d.ts +14 -0
- package/dist/composites/ToolbarPopover.d.ts.map +1 -0
- package/dist/composites/ToolbarPopover.js +48 -0
- package/dist/extensions/ai-action.d.ts +24 -0
- package/dist/extensions/ai-action.d.ts.map +1 -0
- package/dist/extensions/ai-action.js +53 -0
- package/dist/extensions/mention.d.ts +17 -0
- package/dist/extensions/mention.d.ts.map +1 -0
- package/dist/extensions/mention.js +129 -0
- package/dist/extensions/resizable-image-view.d.ts +18 -0
- package/dist/extensions/resizable-image-view.d.ts.map +1 -0
- package/dist/extensions/resizable-image-view.js +70 -0
- package/dist/extensions/resizable-image.d.ts +4 -0
- package/dist/extensions/resizable-image.d.ts.map +1 -0
- package/dist/extensions/resizable-image.js +68 -0
- package/dist/extensions/slash-command.d.ts +18 -0
- package/dist/extensions/slash-command.d.ts.map +1 -0
- package/dist/extensions/slash-command.js +178 -0
- package/dist/index.d.ts +24 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +18 -0
- package/dist/lib/ai-command-menu.d.ts +9 -0
- package/dist/lib/ai-command-menu.d.ts.map +1 -0
- package/dist/lib/ai-command-menu.js +43 -0
- package/dist/lib/cn.d.ts +4 -0
- package/dist/lib/cn.d.ts.map +1 -0
- package/dist/lib/cn.js +6 -0
- package/dist/lib/editor-format-colors.d.ts +10 -0
- package/dist/lib/editor-format-colors.d.ts.map +1 -0
- package/dist/lib/editor-format-colors.js +24 -0
- package/dist/lib/image-upload.d.ts +14 -0
- package/dist/lib/image-upload.d.ts.map +1 -0
- package/dist/lib/image-upload.js +29 -0
- package/dist/lib/insert-editor-images.d.ts +5 -0
- package/dist/lib/insert-editor-images.d.ts.map +1 -0
- package/dist/lib/insert-editor-images.js +32 -0
- package/dist/lib/types.d.ts +131 -0
- package/dist/lib/types.d.ts.map +1 -0
- package/dist/lib/types.js +12 -0
- package/dist/lib/use-spatika-editor.d.ts +13 -0
- package/dist/lib/use-spatika-editor.d.ts.map +1 -0
- package/dist/lib/use-spatika-editor.js +171 -0
- package/dist/styles.css +1056 -0
- package/package.json +101 -0
- package/src/composites/AiCommandDock.test.tsx +48 -0
- package/src/composites/AiCommandDock.tsx +137 -0
- package/src/composites/AiCommandMenu.test.tsx +67 -0
- package/src/composites/AiCommandMenu.tsx +120 -0
- package/src/composites/AiCommandPanel.test.tsx +42 -0
- package/src/composites/AiCommandPanel.tsx +56 -0
- package/src/composites/AiOverlay.test.tsx +18 -0
- package/src/composites/AiOverlay.tsx +18 -0
- package/src/composites/EditorBubbleMenu.test.tsx +49 -0
- package/src/composites/EditorBubbleMenu.tsx +73 -0
- package/src/composites/EditorToolbar.test.tsx +146 -0
- package/src/composites/EditorToolbar.tsx +362 -0
- package/src/composites/ImageBubbleMenu.test.tsx +43 -0
- package/src/composites/ImageBubbleMenu.tsx +51 -0
- package/src/composites/MentionMenu.test.tsx +25 -0
- package/src/composites/MentionMenu.tsx +46 -0
- package/src/composites/SlashCommandMenu.test.tsx +33 -0
- package/src/composites/SlashCommandMenu.tsx +54 -0
- package/src/composites/SpatikaEditor.test.tsx +167 -0
- package/src/composites/SpatikaEditor.tsx +302 -0
- package/src/composites/TableGridPicker.test.tsx +26 -0
- package/src/composites/TableGridPicker.tsx +138 -0
- package/src/composites/ToolbarButton.test.tsx +21 -0
- package/src/composites/ToolbarButton.tsx +24 -0
- package/src/composites/ToolbarFormatPanels.test.tsx +46 -0
- package/src/composites/ToolbarFormatPanels.tsx +78 -0
- package/src/composites/ToolbarInsertPanels.test.tsx +60 -0
- package/src/composites/ToolbarInsertPanels.tsx +200 -0
- package/src/composites/ToolbarPopover.test.tsx +24 -0
- package/src/composites/ToolbarPopover.tsx +95 -0
- package/src/extensions/ai-action.test.ts +26 -0
- package/src/extensions/ai-action.ts +73 -0
- package/src/extensions/mention.test.ts +31 -0
- package/src/extensions/mention.ts +158 -0
- package/src/extensions/resizable-image-view.test.ts +26 -0
- package/src/extensions/resizable-image-view.ts +96 -0
- package/src/extensions/resizable-image.test.ts +28 -0
- package/src/extensions/resizable-image.ts +75 -0
- package/src/extensions/slash-command.test.ts +45 -0
- package/src/extensions/slash-command.ts +197 -0
- package/src/index.ts +49 -0
- package/src/lib/ai-command-menu.test.ts +48 -0
- package/src/lib/ai-command-menu.ts +57 -0
- package/src/lib/cn.ts +7 -0
- package/src/lib/editor-format-colors.test.ts +15 -0
- package/src/lib/editor-format-colors.ts +31 -0
- package/src/lib/image-upload.test.ts +19 -0
- package/src/lib/image-upload.ts +49 -0
- package/src/lib/insert-editor-images.test.ts +76 -0
- package/src/lib/insert-editor-images.ts +44 -0
- package/src/lib/types.ts +149 -0
- package/src/lib/use-spatika-editor.test.tsx +56 -0
- package/src/lib/use-spatika-editor.ts +191 -0
- package/src/styles/editor.css +1056 -0
- package/src/test/setup.ts +46 -0
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { forwardRef, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState, } from "react";
|
|
3
|
+
import { createPortal } from "react-dom";
|
|
4
|
+
import { EditorContent } from "@tiptap/react";
|
|
5
|
+
import { defaultSlashCommands } from "../extensions/slash-command";
|
|
6
|
+
import { cn } from "../lib/cn";
|
|
7
|
+
import { DEFAULT_TOOLBAR } from "../lib/types";
|
|
8
|
+
import { DEFAULT_AI_ACTIONS, resolveAiCommandActions, resolveAiCommandPlacement, usesAiCommandMenu, } from "../lib/ai-command-menu";
|
|
9
|
+
import { useSpatikaEditor } from "../lib/use-spatika-editor";
|
|
10
|
+
import { AiCommandDock } from "./AiCommandDock";
|
|
11
|
+
import { AiOverlay } from "./AiOverlay";
|
|
12
|
+
import { EditorBubbleMenu } from "./EditorBubbleMenu";
|
|
13
|
+
import { EditorToolbar } from "./EditorToolbar";
|
|
14
|
+
import { ImageBubbleMenu } from "./ImageBubbleMenu";
|
|
15
|
+
import { MentionMenu } from "./MentionMenu";
|
|
16
|
+
import { SlashCommandMenu } from "./SlashCommandMenu";
|
|
17
|
+
const DEFAULT_AI_ACTIONS_LEGACY = DEFAULT_AI_ACTIONS;
|
|
18
|
+
export const SpatikaEditor = forwardRef(function SpatikaEditor({ value, onChange, placeholder, disabled, className, heightClassName = "min-h-[400px]", toolbar, toolbarActions, extensions, starterKit, slashCommands, mentions, onAiSelectionAction, onAiCommand, isAiProcessing, aiActions = DEFAULT_AI_ACTIONS_LEGACY, aiCommandMenu, imageInsert, onInsertImage, resizableImages = true, mobileFullscreen = false, }, ref) {
|
|
19
|
+
const shellRef = useRef(null);
|
|
20
|
+
const [fullscreen, setFullscreen] = useState(false);
|
|
21
|
+
const [slashState, setSlashState] = useState(null);
|
|
22
|
+
const [mentionState, setMentionState] = useState(null);
|
|
23
|
+
const [aiBusyInternal, setAiBusyInternal] = useState(false);
|
|
24
|
+
const { editor, isReady } = useSpatikaEditor({
|
|
25
|
+
value,
|
|
26
|
+
onChange,
|
|
27
|
+
placeholder,
|
|
28
|
+
disabled,
|
|
29
|
+
extensions,
|
|
30
|
+
starterKit,
|
|
31
|
+
slashCommands,
|
|
32
|
+
mentions,
|
|
33
|
+
onAiSelectionAction,
|
|
34
|
+
onAiCommand,
|
|
35
|
+
resizableImages,
|
|
36
|
+
imageInsert,
|
|
37
|
+
onSlashUpdate: setSlashState,
|
|
38
|
+
onMentionUpdate: setMentionState,
|
|
39
|
+
});
|
|
40
|
+
useImperativeHandle(ref, () => ({
|
|
41
|
+
focus: () => editor?.commands.focus(),
|
|
42
|
+
setContent: (html) => editor?.commands.setContent(html),
|
|
43
|
+
getHTML: () => editor?.getHTML() ?? "",
|
|
44
|
+
getJSON: () => editor?.getJSON(),
|
|
45
|
+
expandFullscreen: () => setFullscreen(true),
|
|
46
|
+
}), [editor]);
|
|
47
|
+
const toolbarConfig = { ...DEFAULT_TOOLBAR, ...toolbar };
|
|
48
|
+
const allSlashCommands = useMemo(() => [...defaultSlashCommands(), ...(slashCommands ?? [])], [slashCommands]);
|
|
49
|
+
const filteredSlashItems = useMemo(() => {
|
|
50
|
+
const query = slashState?.query?.toLowerCase() ?? "";
|
|
51
|
+
return allSlashCommands.filter((item) => {
|
|
52
|
+
if (!query)
|
|
53
|
+
return true;
|
|
54
|
+
const haystack = [item.title, item.description, ...(item.keywords ?? [])]
|
|
55
|
+
.filter(Boolean)
|
|
56
|
+
.join(" ")
|
|
57
|
+
.toLowerCase();
|
|
58
|
+
return haystack.includes(query);
|
|
59
|
+
});
|
|
60
|
+
}, [allSlashCommands, slashState?.query]);
|
|
61
|
+
const filteredMentions = useMemo(() => {
|
|
62
|
+
const query = mentionState?.query?.toLowerCase() ?? "";
|
|
63
|
+
const contacts = mentions ?? [];
|
|
64
|
+
return contacts.filter((contact) => contact.label.toLowerCase().includes(query));
|
|
65
|
+
}, [mentions, mentionState?.query]);
|
|
66
|
+
const aiHandler = onAiCommand
|
|
67
|
+
? onAiCommand
|
|
68
|
+
: onAiSelectionAction
|
|
69
|
+
? async (context) => onAiSelectionAction(context.actionId, context.selectedText)
|
|
70
|
+
: undefined;
|
|
71
|
+
const commandMenuEnabled = usesAiCommandMenu(aiCommandMenu, Boolean(aiHandler));
|
|
72
|
+
const aiCommandPlacement = resolveAiCommandPlacement(aiCommandMenu);
|
|
73
|
+
const toolbarAiEnabled = commandMenuEnabled && aiCommandPlacement === "toolbar";
|
|
74
|
+
const dockAiEnabled = commandMenuEnabled && aiCommandPlacement === "dock";
|
|
75
|
+
const commandActions = resolveAiCommandActions(aiCommandMenu === false ? undefined : aiCommandMenu, aiActions);
|
|
76
|
+
const bubbleActionsEnabled = Boolean(aiHandler) &&
|
|
77
|
+
(commandMenuEnabled
|
|
78
|
+
? aiCommandMenu !== false && aiCommandMenu?.bubbleActions
|
|
79
|
+
: true);
|
|
80
|
+
const runAiCommand = useCallback(async (request) => {
|
|
81
|
+
if (!editor || !aiHandler)
|
|
82
|
+
return;
|
|
83
|
+
const { from, to } = editor.state.selection;
|
|
84
|
+
const selectedText = from !== to ? editor.state.doc.textBetween(from, to, " ") : "";
|
|
85
|
+
editor.commands.setAiSelection?.(from !== to ? { from, to } : null);
|
|
86
|
+
editor.commands.setAiProcessing?.(true);
|
|
87
|
+
setAiBusyInternal(true);
|
|
88
|
+
try {
|
|
89
|
+
const result = await aiHandler({
|
|
90
|
+
actionId: request.actionId,
|
|
91
|
+
prompt: request.prompt,
|
|
92
|
+
selectedText,
|
|
93
|
+
documentText: editor.state.doc.textContent,
|
|
94
|
+
html: editor.getHTML(),
|
|
95
|
+
selection: from !== to ? { from, to } : null,
|
|
96
|
+
});
|
|
97
|
+
if (result) {
|
|
98
|
+
if (from !== to) {
|
|
99
|
+
editor.chain().focus().insertContentAt({ from, to }, result).run();
|
|
100
|
+
}
|
|
101
|
+
else {
|
|
102
|
+
editor.chain().focus().insertContentAt(from, result).run();
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
finally {
|
|
107
|
+
editor.commands.setAiProcessing?.(false);
|
|
108
|
+
editor.commands.setAiSelection?.(null);
|
|
109
|
+
setAiBusyInternal(false);
|
|
110
|
+
}
|
|
111
|
+
}, [editor, aiHandler]);
|
|
112
|
+
const runLegacyAiAction = useCallback(async (actionId) => {
|
|
113
|
+
if (!editor)
|
|
114
|
+
return;
|
|
115
|
+
const { from, to } = editor.state.selection;
|
|
116
|
+
if (from === to)
|
|
117
|
+
return;
|
|
118
|
+
await runAiCommand({ actionId });
|
|
119
|
+
}, [editor, runAiCommand]);
|
|
120
|
+
useEffect(() => {
|
|
121
|
+
if (!mobileFullscreen || !editor)
|
|
122
|
+
return;
|
|
123
|
+
const onFocus = () => {
|
|
124
|
+
if (window.matchMedia("(max-width: 767px)").matches) {
|
|
125
|
+
setFullscreen(true);
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
editor.on("focus", onFocus);
|
|
129
|
+
return () => {
|
|
130
|
+
editor.off("focus", onFocus);
|
|
131
|
+
};
|
|
132
|
+
}, [editor, mobileFullscreen]);
|
|
133
|
+
const shell = (_jsxs("div", { ref: shellRef, "data-slot": "spatika-editor", className: cn("spk-editor-shell", disabled && "spk-editor-shell--disabled", fullscreen && "spk-editor-shell--fullscreen", dockAiEnabled && "spk-editor-shell--with-ai-dock", className), children: [_jsx(EditorToolbar, { editor: editor, config: toolbarConfig, toolbarActions: toolbarActions, aiActions: toolbarAiEnabled ? commandActions : !commandMenuEnabled ? aiActions : undefined, aiCommandMenu: aiCommandMenu === false ? false : aiCommandMenu, useAiCommandMenu: toolbarAiEnabled, onAiCommand: commandMenuEnabled ? runAiCommand : undefined, onAiAction: !commandMenuEnabled ? runLegacyAiAction : undefined, onInsertImage: onInsertImage, imageInsert: imageInsert }), _jsxs("div", { className: "spk-editor-body", children: [_jsx(EditorContent, { editor: editor, className: cn("spk-editor-surface", heightClassName) }), _jsx(EditorBubbleMenu, { editor: editor, aiActions: bubbleActionsEnabled ? (commandMenuEnabled ? commandActions : aiActions) : undefined, onAiAction: runLegacyAiAction }), _jsx(ImageBubbleMenu, { editor: editor }), _jsx(SlashCommandMenu, { editor: editor, items: filteredSlashItems, query: slashState?.query ?? "", activeIndex: slashState?.activeIndex ?? 0, open: Boolean(slashState?.range), onSelect: (item) => {
|
|
134
|
+
if (!editor || !slashState?.range)
|
|
135
|
+
return;
|
|
136
|
+
editor.chain().focus().deleteRange(slashState.range).run();
|
|
137
|
+
item.command(editor);
|
|
138
|
+
setSlashState(null);
|
|
139
|
+
} }), _jsx(MentionMenu, { contacts: filteredMentions, query: mentionState?.query ?? "", activeIndex: mentionState?.activeIndex ?? 0, open: Boolean(mentionState?.range), onSelect: (contact) => {
|
|
140
|
+
if (!editor || !mentionState?.range)
|
|
141
|
+
return;
|
|
142
|
+
editor
|
|
143
|
+
.chain()
|
|
144
|
+
.focus()
|
|
145
|
+
.insertContentAt(mentionState.range, [
|
|
146
|
+
{ type: "mention", attrs: { id: contact.id, label: contact.label } },
|
|
147
|
+
{ type: "text", text: " " },
|
|
148
|
+
])
|
|
149
|
+
.run();
|
|
150
|
+
setMentionState(null);
|
|
151
|
+
} }), _jsx(AiOverlay, { visible: Boolean(isAiProcessing || aiBusyInternal) })] }), dockAiEnabled ? (_jsx("div", { className: "spk-editor-ai-dock-footer", children: _jsx(AiCommandDock, { editor: editor, config: aiCommandMenu === false ? undefined : aiCommandMenu, actions: commandActions, onRun: runAiCommand }) })) : null, fullscreen ? (_jsx("div", { className: "spk-editor-fullscreen-bar", children: _jsx("button", { type: "button", className: "spk-editor-fullscreen-close", onClick: () => setFullscreen(false), children: "Done" }) })) : null] }));
|
|
152
|
+
if (fullscreen && typeof document !== "undefined") {
|
|
153
|
+
return createPortal(shell, document.body);
|
|
154
|
+
}
|
|
155
|
+
if (!isReady) {
|
|
156
|
+
return (_jsx("div", { className: cn("spk-editor-shell spk-editor-shell--loading", className), "aria-busy": "true", children: "Loading editor\u2026" }));
|
|
157
|
+
}
|
|
158
|
+
return shell;
|
|
159
|
+
});
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export declare const TABLE_GRID_SIZE = 10;
|
|
2
|
+
export declare const TABLE_ROWS_MAX = 20;
|
|
3
|
+
export declare const TABLE_COLS_MAX = 12;
|
|
4
|
+
type TableGridPickerProps = {
|
|
5
|
+
onInsert: (rows: number, cols: number, withHeaderRow: boolean) => void;
|
|
6
|
+
};
|
|
7
|
+
export declare function TableGridPicker({ onInsert }: TableGridPickerProps): import("react").JSX.Element;
|
|
8
|
+
export {};
|
|
9
|
+
//# sourceMappingURL=TableGridPicker.d.ts.map
|
|
@@ -0,0 +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"}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { Columns3, Rows3 } from "lucide-react";
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
import { cn } from "../lib/cn";
|
|
5
|
+
export const TABLE_GRID_SIZE = 10;
|
|
6
|
+
export const TABLE_ROWS_MAX = 20;
|
|
7
|
+
export const TABLE_COLS_MAX = 12;
|
|
8
|
+
function clampDimension(value, max) {
|
|
9
|
+
return Math.max(1, Math.min(max, value));
|
|
10
|
+
}
|
|
11
|
+
function pickCell(event) {
|
|
12
|
+
const cell = event.target.closest("[data-table-row]");
|
|
13
|
+
if (!cell)
|
|
14
|
+
return null;
|
|
15
|
+
return {
|
|
16
|
+
rows: clampDimension(Number(cell.dataset.tableRow), TABLE_ROWS_MAX),
|
|
17
|
+
cols: clampDimension(Number(cell.dataset.tableCol), TABLE_COLS_MAX),
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
export function TableGridPicker({ onInsert }) {
|
|
21
|
+
const [rows, setRows] = useState(3);
|
|
22
|
+
const [cols, setCols] = useState(3);
|
|
23
|
+
const [withHeaderRow, setWithHeaderRow] = useState(true);
|
|
24
|
+
const [hover, setHover] = useState(null);
|
|
25
|
+
const previewRows = hover?.rows ?? rows;
|
|
26
|
+
const previewCols = hover?.cols ?? cols;
|
|
27
|
+
const handleGridMove = (event) => {
|
|
28
|
+
const picked = pickCell(event);
|
|
29
|
+
if (picked)
|
|
30
|
+
setHover(picked);
|
|
31
|
+
};
|
|
32
|
+
const handleGridClick = (event) => {
|
|
33
|
+
const picked = pickCell(event);
|
|
34
|
+
if (picked) {
|
|
35
|
+
setRows(picked.rows);
|
|
36
|
+
setCols(picked.cols);
|
|
37
|
+
onInsert(picked.rows, picked.cols, withHeaderRow);
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
return (_jsxs("div", { className: "spk-editor-table-picker", children: [_jsxs("div", { className: "spk-editor-table-picker-label", "aria-live": "polite", children: [previewCols, " \u00D7 ", previewRows] }), _jsx("div", { className: "spk-editor-table-picker-grid", role: "grid", "aria-label": "Choose table size", onMouseMove: handleGridMove, onMouseLeave: () => setHover(null), onClick: handleGridClick, children: Array.from({ length: TABLE_GRID_SIZE * TABLE_GRID_SIZE }, (_, index) => {
|
|
41
|
+
const rowIndex = Math.floor(index / TABLE_GRID_SIZE);
|
|
42
|
+
const colIndex = index % TABLE_GRID_SIZE;
|
|
43
|
+
const cellRows = rowIndex + 1;
|
|
44
|
+
const cellCols = colIndex + 1;
|
|
45
|
+
const active = cellRows <= previewRows && cellCols <= previewCols;
|
|
46
|
+
return (_jsx("button", { type: "button", role: "gridcell", "data-table-row": cellRows, "data-table-col": cellCols, className: cn("spk-editor-table-picker-cell", active && "spk-editor-table-picker-cell--active"), "aria-label": `${cellCols} by ${cellRows} table`, onMouseDown: (event) => event.preventDefault() }, `${rowIndex}-${colIndex}`));
|
|
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
|
+
setHover(null);
|
|
49
|
+
setCols(clampDimension(Number.parseInt(event.target.value, 10) || 1, TABLE_COLS_MAX));
|
|
50
|
+
} })] }), _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
|
+
setHover(null);
|
|
52
|
+
setRows(clampDimension(Number.parseInt(event.target.value, 10) || 1, TABLE_ROWS_MAX));
|
|
53
|
+
} })] })] }), _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
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { type ButtonHTMLAttributes, type ReactNode } from "react";
|
|
2
|
+
/** Compact icon control for the editor toolbar. Avoids a @spatika/react dependency. */
|
|
3
|
+
export declare const ToolbarButton: import("react").ForwardRefExoticComponent<ButtonHTMLAttributes<HTMLButtonElement> & {
|
|
4
|
+
active?: boolean;
|
|
5
|
+
children: ReactNode;
|
|
6
|
+
} & import("react").RefAttributes<HTMLButtonElement>>;
|
|
7
|
+
//# sourceMappingURL=ToolbarButton.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ToolbarButton.d.ts","sourceRoot":"","sources":["../../src/composites/ToolbarButton.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAc,KAAK,oBAAoB,EAAE,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAG9E,uFAAuF;AACvF,eAAO,MAAM,aAAa;aAGb,OAAO;cACN,SAAS;qDAerB,CAAC"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { forwardRef } from "react";
|
|
3
|
+
import { cn } from "../lib/cn";
|
|
4
|
+
/** Compact icon control for the editor toolbar. Avoids a @spatika/react dependency. */
|
|
5
|
+
export const ToolbarButton = forwardRef(function ToolbarButton({ className, active, children, ...props }, ref) {
|
|
6
|
+
return (_jsx("button", { ref: ref, type: "button", "data-slot": "editor-toolbar-button", "data-active": active ? "true" : undefined, className: cn("spk-editor-toolbar-btn", active && "spk-editor-toolbar-btn--active", className), ...props, children: children }));
|
|
7
|
+
});
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { Editor } from "@tiptap/core";
|
|
2
|
+
type PanelProps = {
|
|
3
|
+
editor: Editor;
|
|
4
|
+
close: () => void;
|
|
5
|
+
};
|
|
6
|
+
export declare function TextColorPanel({ editor, close }: PanelProps): import("react").JSX.Element;
|
|
7
|
+
export declare function HighlightColorPanel({ editor, close }: PanelProps): import("react").JSX.Element;
|
|
8
|
+
export {};
|
|
9
|
+
//# sourceMappingURL=ToolbarFormatPanels.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ToolbarFormatPanels.d.ts","sourceRoot":"","sources":["../../src/composites/ToolbarFormatPanels.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AAK3C,KAAK,UAAU,GAAG;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,IAAI,CAAC;CACnB,CAAC;AAqCF,wBAAgB,cAAc,CAAC,EAAE,MAAM,EAAE,KAAK,EAAE,EAAE,UAAU,+BAe3D;AAED,wBAAgB,mBAAmB,CAAC,EAAE,MAAM,EAAE,KAAK,EAAE,EAAE,UAAU,+BAehE"}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { EDITOR_HIGHLIGHT_COLORS, EDITOR_TEXT_COLORS } from "../lib/editor-format-colors";
|
|
3
|
+
import { cn } from "../lib/cn";
|
|
4
|
+
function ColorSwatchGrid({ colors, activeValue, onSelect, }) {
|
|
5
|
+
return (_jsx("div", { className: "spk-editor-color-grid", role: "list", children: colors.map((color) => {
|
|
6
|
+
const isActive = color.value === null ? !activeValue : activeValue?.toLowerCase() === color.value.toLowerCase();
|
|
7
|
+
return (_jsx("button", { type: "button", role: "listitem", "aria-label": color.label, "aria-pressed": isActive, className: cn("spk-editor-color-swatch", color.value === null && "spk-editor-color-swatch--default", isActive && "spk-editor-color-swatch--active"), style: color.value ? { "--swatch-color": color.value } : undefined, onClick: () => onSelect(color.value) }, color.id));
|
|
8
|
+
}) }));
|
|
9
|
+
}
|
|
10
|
+
export function TextColorPanel({ editor, close }) {
|
|
11
|
+
const activeColor = editor.getAttributes("textStyle").color ?? null;
|
|
12
|
+
const apply = (value) => {
|
|
13
|
+
if (value === null) {
|
|
14
|
+
editor.chain().focus().unsetColor().run();
|
|
15
|
+
}
|
|
16
|
+
else {
|
|
17
|
+
editor.chain().focus().setColor(value).run();
|
|
18
|
+
}
|
|
19
|
+
close();
|
|
20
|
+
};
|
|
21
|
+
return (_jsx(ColorSwatchGrid, { colors: EDITOR_TEXT_COLORS, activeValue: activeColor, onSelect: apply }));
|
|
22
|
+
}
|
|
23
|
+
export function HighlightColorPanel({ editor, close }) {
|
|
24
|
+
const activeColor = editor.getAttributes("highlight").color ?? null;
|
|
25
|
+
const apply = (value) => {
|
|
26
|
+
if (value === null) {
|
|
27
|
+
editor.chain().focus().unsetHighlight().run();
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
editor.chain().focus().setHighlight({ color: value }).run();
|
|
31
|
+
}
|
|
32
|
+
close();
|
|
33
|
+
};
|
|
34
|
+
return (_jsx(ColorSwatchGrid, { colors: EDITOR_HIGHLIGHT_COLORS, activeValue: activeColor, onSelect: apply }));
|
|
35
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { Editor } from "@tiptap/core";
|
|
2
|
+
import type { ImageInsertConfig } from "../lib/types";
|
|
3
|
+
type PanelProps = {
|
|
4
|
+
editor: Editor;
|
|
5
|
+
close: () => void;
|
|
6
|
+
};
|
|
7
|
+
type ImagePanelProps = PanelProps & {
|
|
8
|
+
config?: ImageInsertConfig;
|
|
9
|
+
};
|
|
10
|
+
export declare function LinkInsertPanel({ editor, close }: PanelProps): import("react").JSX.Element;
|
|
11
|
+
export declare function ImageInsertPanel({ editor, close, config }: ImagePanelProps): import("react").JSX.Element;
|
|
12
|
+
export declare function TableInsertPanel({ editor, close }: PanelProps): import("react").JSX.Element;
|
|
13
|
+
export {};
|
|
14
|
+
//# sourceMappingURL=ToolbarInsertPanels.d.ts.map
|
|
@@ -0,0 +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"}
|
|
@@ -0,0 +1,96 @@
|
|
|
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";
|
|
4
|
+
import { cn } from "../lib/cn";
|
|
5
|
+
import { insertImageFilesIntoEditor } from "../lib/insert-editor-images";
|
|
6
|
+
import { TableGridPicker } from "./TableGridPicker";
|
|
7
|
+
export function LinkInsertPanel({ editor, close }) {
|
|
8
|
+
const previous = editor.getAttributes("link").href;
|
|
9
|
+
const [url, setUrl] = useState(previous ?? "https://");
|
|
10
|
+
const apply = () => {
|
|
11
|
+
const trimmed = url.trim();
|
|
12
|
+
if (!trimmed) {
|
|
13
|
+
editor.chain().focus().extendMarkRange("link").unsetLink().run();
|
|
14
|
+
}
|
|
15
|
+
else {
|
|
16
|
+
editor.chain().focus().extendMarkRange("link").setLink({ href: trimmed }).run();
|
|
17
|
+
}
|
|
18
|
+
close();
|
|
19
|
+
};
|
|
20
|
+
const remove = () => {
|
|
21
|
+
editor.chain().focus().extendMarkRange("link").unsetLink().run();
|
|
22
|
+
close();
|
|
23
|
+
};
|
|
24
|
+
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) => {
|
|
25
|
+
if (event.key === "Enter")
|
|
26
|
+
apply();
|
|
27
|
+
if (event.key === "Escape")
|
|
28
|
+
close();
|
|
29
|
+
}, autoFocus: true })] }), _jsxs("div", { className: "spk-editor-toolbar-popover-actions", children: [previous ? (_jsx("button", { type: "button", className: "spk-editor-toolbar-popover-btn spk-editor-toolbar-popover-btn--ghost", onClick: remove, children: "Remove" })) : null, _jsx("button", { type: "button", className: "spk-editor-toolbar-popover-btn spk-editor-toolbar-popover-btn--ghost", onClick: close, children: "Cancel" }), _jsx("button", { type: "button", className: "spk-editor-toolbar-popover-btn spk-editor-toolbar-popover-btn--primary", onClick: apply, children: "Apply" })] })] }));
|
|
30
|
+
}
|
|
31
|
+
function insertImageSrc(editor, src) {
|
|
32
|
+
editor.chain().focus().setImage({ src }).run();
|
|
33
|
+
}
|
|
34
|
+
export function ImageInsertPanel({ editor, close, config }) {
|
|
35
|
+
const inputRef = useRef(null);
|
|
36
|
+
const [url, setUrl] = useState("");
|
|
37
|
+
const [error, setError] = useState(null);
|
|
38
|
+
const [dragging, setDragging] = useState(false);
|
|
39
|
+
const maxFiles = config?.maxFiles ?? 3;
|
|
40
|
+
const maxSizeBytes = config?.maxSizeBytes ?? 5 * 1024 * 1024;
|
|
41
|
+
const accept = config?.accept ?? "image/*";
|
|
42
|
+
const limitMb = Math.round(maxSizeBytes / (1024 * 1024));
|
|
43
|
+
const handleFiles = async (files) => {
|
|
44
|
+
setError(null);
|
|
45
|
+
const inserted = await insertImageFilesIntoEditor(editor, files, config);
|
|
46
|
+
if (!inserted) {
|
|
47
|
+
setError("Could not insert image.");
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
close();
|
|
51
|
+
};
|
|
52
|
+
const insertFromUrl = () => {
|
|
53
|
+
const trimmed = url.trim();
|
|
54
|
+
if (!trimmed)
|
|
55
|
+
return;
|
|
56
|
+
insertImageSrc(editor, trimmed);
|
|
57
|
+
close();
|
|
58
|
+
};
|
|
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) => {
|
|
60
|
+
event.preventDefault();
|
|
61
|
+
setDragging(true);
|
|
62
|
+
}, onDragOver: (event) => {
|
|
63
|
+
event.preventDefault();
|
|
64
|
+
setDragging(true);
|
|
65
|
+
}, onDragLeave: () => setDragging(false), onDrop: (event) => {
|
|
66
|
+
event.preventDefault();
|
|
67
|
+
setDragging(false);
|
|
68
|
+
if (event.dataTransfer.files.length) {
|
|
69
|
+
void handleFiles(event.dataTransfer.files);
|
|
70
|
+
}
|
|
71
|
+
}, children: [_jsx("span", { className: "spk-editor-image-dropzone-icon", "aria-hidden": "true", children: _jsx(Upload, { className: "size-5" }) }), _jsx("span", { className: "spk-editor-image-dropzone-title", children: "Click to upload or drag and drop" }), _jsxs("span", { className: "spk-editor-image-dropzone-hint", children: ["Maximum ", maxFiles, " files, ", limitMb, "MB each"] })] }), _jsx("input", { ref: inputRef, type: "file", accept: accept, multiple: maxFiles > 1, className: "sr-only", onChange: (event) => {
|
|
72
|
+
if (event.target.files?.length) {
|
|
73
|
+
void handleFiles(event.target.files);
|
|
74
|
+
}
|
|
75
|
+
} }), 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) => {
|
|
76
|
+
if (event.key === "Enter")
|
|
77
|
+
insertFromUrl();
|
|
78
|
+
if (event.key === "Escape")
|
|
79
|
+
close();
|
|
80
|
+
} })] }), _jsxs("div", { className: "spk-editor-toolbar-popover-actions", children: [_jsx("button", { type: "button", className: "spk-editor-toolbar-popover-btn spk-editor-toolbar-popover-btn--ghost", onClick: close, children: "Cancel" }), _jsxs("button", { type: "button", className: "spk-editor-toolbar-popover-btn spk-editor-toolbar-popover-btn--primary", onClick: insertFromUrl, disabled: !url.trim(), children: [_jsx(ImagePlus, { className: "size-4", "aria-hidden": "true" }), "Insert"] })] })] }));
|
|
81
|
+
}
|
|
82
|
+
export function TableInsertPanel({ editor, close }) {
|
|
83
|
+
const insert = (nextRows, nextCols, withHeaderRow) => {
|
|
84
|
+
editor
|
|
85
|
+
.chain()
|
|
86
|
+
.focus()
|
|
87
|
+
.insertTable({
|
|
88
|
+
rows: nextRows,
|
|
89
|
+
cols: nextCols,
|
|
90
|
+
withHeaderRow,
|
|
91
|
+
})
|
|
92
|
+
.run();
|
|
93
|
+
close();
|
|
94
|
+
};
|
|
95
|
+
return _jsx(TableGridPicker, { onInsert: insert });
|
|
96
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
|
+
type ToolbarPopoverProps = {
|
|
3
|
+
label: string;
|
|
4
|
+
icon: ReactNode;
|
|
5
|
+
title: string;
|
|
6
|
+
className?: string;
|
|
7
|
+
active?: boolean;
|
|
8
|
+
children: (api: {
|
|
9
|
+
close: () => void;
|
|
10
|
+
}) => ReactNode;
|
|
11
|
+
};
|
|
12
|
+
export declare function ToolbarPopover({ label, icon, title, className, active, children, }: ToolbarPopoverProps): import("react").JSX.Element;
|
|
13
|
+
export {};
|
|
14
|
+
//# sourceMappingURL=ToolbarPopover.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ToolbarPopover.d.ts","sourceRoot":"","sources":["../../src/composites/ToolbarPopover.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAuD,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAK5F,KAAK,mBAAmB,GAAG;IACzB,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,SAAS,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,QAAQ,EAAE,CAAC,GAAG,EAAE;QAAE,KAAK,EAAE,MAAM,IAAI,CAAA;KAAE,KAAK,SAAS,CAAC;CACrD,CAAC;AAEF,wBAAgB,cAAc,CAAC,EAC7B,KAAK,EACL,IAAI,EACJ,KAAK,EACL,SAAS,EACT,MAAc,EACd,QAAQ,GACT,EAAE,mBAAmB,+BAyErB"}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useId, useLayoutEffect, useRef, useState } from "react";
|
|
3
|
+
import { createPortal } from "react-dom";
|
|
4
|
+
import { cn } from "../lib/cn";
|
|
5
|
+
import { ToolbarButton } from "./ToolbarButton";
|
|
6
|
+
export function ToolbarPopover({ label, icon, title, className, active = false, children, }) {
|
|
7
|
+
const [open, setOpen] = useState(false);
|
|
8
|
+
const [panelStyle, setPanelStyle] = useState(null);
|
|
9
|
+
const rootRef = useRef(null);
|
|
10
|
+
const triggerRef = useRef(null);
|
|
11
|
+
const panelRef = useRef(null);
|
|
12
|
+
const titleId = useId();
|
|
13
|
+
const close = () => setOpen(false);
|
|
14
|
+
useLayoutEffect(() => {
|
|
15
|
+
if (!open || !triggerRef.current)
|
|
16
|
+
return;
|
|
17
|
+
const updatePosition = () => {
|
|
18
|
+
const rect = triggerRef.current?.getBoundingClientRect();
|
|
19
|
+
if (!rect)
|
|
20
|
+
return;
|
|
21
|
+
setPanelStyle({
|
|
22
|
+
top: rect.bottom + 6,
|
|
23
|
+
left: rect.left,
|
|
24
|
+
});
|
|
25
|
+
};
|
|
26
|
+
updatePosition();
|
|
27
|
+
window.addEventListener("resize", updatePosition);
|
|
28
|
+
window.addEventListener("scroll", updatePosition, true);
|
|
29
|
+
return () => {
|
|
30
|
+
window.removeEventListener("resize", updatePosition);
|
|
31
|
+
window.removeEventListener("scroll", updatePosition, true);
|
|
32
|
+
};
|
|
33
|
+
}, [open]);
|
|
34
|
+
useEffect(() => {
|
|
35
|
+
if (!open)
|
|
36
|
+
return;
|
|
37
|
+
const onPointerDown = (event) => {
|
|
38
|
+
const target = event.target;
|
|
39
|
+
if (rootRef.current?.contains(target) || panelRef.current?.contains(target))
|
|
40
|
+
return;
|
|
41
|
+
setOpen(false);
|
|
42
|
+
};
|
|
43
|
+
document.addEventListener("mousedown", onPointerDown);
|
|
44
|
+
return () => document.removeEventListener("mousedown", onPointerDown);
|
|
45
|
+
}, [open]);
|
|
46
|
+
const panel = open && panelStyle ? (_jsxs("div", { ref: panelRef, id: titleId, className: "spk-editor-toolbar-popover-panel spk-editor-toolbar-popover-panel--portal", role: "dialog", "aria-label": title, style: { top: panelStyle.top, left: panelStyle.left }, onMouseDown: (event) => event.stopPropagation(), onPointerDown: (event) => event.stopPropagation(), children: [_jsx("div", { className: "spk-editor-toolbar-popover-title", children: title }), children({ close })] })) : null;
|
|
47
|
+
return (_jsxs("div", { ref: rootRef, className: cn("spk-editor-toolbar-popover", className), children: [_jsx(ToolbarButton, { ref: triggerRef, "aria-label": label, "aria-expanded": open, "aria-haspopup": "dialog", "aria-controls": open ? titleId : undefined, active: open || active, onClick: () => setOpen((value) => !value), children: icon }), typeof document !== "undefined" && panel ? createPortal(panel, document.body) : null] }));
|
|
48
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { Extension } from "@tiptap/core";
|
|
2
|
+
import { PluginKey } from "@tiptap/pm/state";
|
|
3
|
+
export declare const AiActionPluginKey: PluginKey<any>;
|
|
4
|
+
export type AiActionStorage = {
|
|
5
|
+
processing: boolean;
|
|
6
|
+
lastSelection: {
|
|
7
|
+
from: number;
|
|
8
|
+
to: number;
|
|
9
|
+
} | null;
|
|
10
|
+
};
|
|
11
|
+
/** Highlights the active selection while an AI transform runs. */
|
|
12
|
+
export declare function createAiActionExtension(): Extension<any, any>;
|
|
13
|
+
declare module "@tiptap/core" {
|
|
14
|
+
interface Commands<ReturnType> {
|
|
15
|
+
aiAction: {
|
|
16
|
+
setAiProcessing: (processing: boolean) => ReturnType;
|
|
17
|
+
setAiSelection: (range: {
|
|
18
|
+
from: number;
|
|
19
|
+
to: number;
|
|
20
|
+
} | null) => ReturnType;
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
//# sourceMappingURL=ai-action.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ai-action.d.ts","sourceRoot":"","sources":["../../src/extensions/ai-action.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AACzC,OAAO,EAAU,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAGrD,eAAO,MAAM,iBAAiB,gBAAmC,CAAC;AAElE,MAAM,MAAM,eAAe,GAAG;IAC5B,UAAU,EAAE,OAAO,CAAC;IACpB,aAAa,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,EAAE,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;CACpD,CAAC;AAEF,kEAAkE;AAClE,wBAAgB,uBAAuB,wBAmDtC;AAED,OAAO,QAAQ,cAAc,CAAC;IAC5B,UAAU,QAAQ,CAAC,UAAU;QAC3B,QAAQ,EAAE;YACR,eAAe,EAAE,CAAC,UAAU,EAAE,OAAO,KAAK,UAAU,CAAC;YACrD,cAAc,EAAE,CAAC,KAAK,EAAE;gBAAE,IAAI,EAAE,MAAM,CAAC;gBAAC,EAAE,EAAE,MAAM,CAAA;aAAE,GAAG,IAAI,KAAK,UAAU,CAAC;SAC5E,CAAC;KACH;CACF"}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { Extension } from "@tiptap/core";
|
|
2
|
+
import { Plugin, PluginKey } from "@tiptap/pm/state";
|
|
3
|
+
import { Decoration, DecorationSet } from "@tiptap/pm/view";
|
|
4
|
+
export const AiActionPluginKey = new PluginKey("spatikaAiAction");
|
|
5
|
+
/** Highlights the active selection while an AI transform runs. */
|
|
6
|
+
export function createAiActionExtension() {
|
|
7
|
+
return Extension.create({
|
|
8
|
+
name: "aiAction",
|
|
9
|
+
addStorage() {
|
|
10
|
+
return {
|
|
11
|
+
processing: false,
|
|
12
|
+
lastSelection: null,
|
|
13
|
+
};
|
|
14
|
+
},
|
|
15
|
+
addCommands() {
|
|
16
|
+
return {
|
|
17
|
+
setAiProcessing: (processing) => ({ tr, dispatch }) => {
|
|
18
|
+
this.storage.processing = processing;
|
|
19
|
+
if (dispatch)
|
|
20
|
+
dispatch(tr);
|
|
21
|
+
return true;
|
|
22
|
+
},
|
|
23
|
+
setAiSelection: (range) => ({ tr, dispatch }) => {
|
|
24
|
+
this.storage.lastSelection = range;
|
|
25
|
+
if (dispatch)
|
|
26
|
+
dispatch(tr);
|
|
27
|
+
return true;
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
},
|
|
31
|
+
addProseMirrorPlugins() {
|
|
32
|
+
const storage = this.storage;
|
|
33
|
+
return [
|
|
34
|
+
new Plugin({
|
|
35
|
+
key: AiActionPluginKey,
|
|
36
|
+
props: {
|
|
37
|
+
decorations: (state) => {
|
|
38
|
+
if (!storage.processing || !storage.lastSelection) {
|
|
39
|
+
return DecorationSet.empty;
|
|
40
|
+
}
|
|
41
|
+
const { from, to } = storage.lastSelection;
|
|
42
|
+
if (from === to)
|
|
43
|
+
return DecorationSet.empty;
|
|
44
|
+
return DecorationSet.create(state.doc, [
|
|
45
|
+
Decoration.inline(from, to, { class: "spk-editor-ai-selection" }),
|
|
46
|
+
]);
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
}),
|
|
50
|
+
];
|
|
51
|
+
},
|
|
52
|
+
});
|
|
53
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { Extension, Node } from "@tiptap/core";
|
|
2
|
+
import { PluginKey } from "@tiptap/pm/state";
|
|
3
|
+
import type { MentionContact } from "../lib/types";
|
|
4
|
+
export type MentionStorage = {
|
|
5
|
+
contacts: MentionContact[];
|
|
6
|
+
activeIndex: number;
|
|
7
|
+
query: string;
|
|
8
|
+
range: {
|
|
9
|
+
from: number;
|
|
10
|
+
to: number;
|
|
11
|
+
} | null;
|
|
12
|
+
filteredItems: MentionContact[];
|
|
13
|
+
runCommand: ((item: MentionContact) => void) | null;
|
|
14
|
+
};
|
|
15
|
+
export declare const MentionPluginKey: PluginKey<any>;
|
|
16
|
+
export declare function createMentionExtension(contacts: MentionContact[], onUpdate?: (state: MentionStorage) => void): (Extension<any, any> | Node<any, any>)[];
|
|
17
|
+
//# sourceMappingURL=mention.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mention.d.ts","sourceRoot":"","sources":["../../src/extensions/mention.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAmB,IAAI,EAAE,MAAM,cAAc,CAAC;AAChE,OAAO,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAE7C,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAEnD,MAAM,MAAM,cAAc,GAAG;IAC3B,QAAQ,EAAE,cAAc,EAAE,CAAC;IAC3B,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,EAAE,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;IAC3C,aAAa,EAAE,cAAc,EAAE,CAAC;IAChC,UAAU,EAAE,CAAC,CAAC,IAAI,EAAE,cAAc,KAAK,IAAI,CAAC,GAAG,IAAI,CAAC;CACrD,CAAC;AAEF,eAAO,MAAM,gBAAgB,gBAAkC,CAAC;AAEhE,wBAAgB,sBAAsB,CACpC,QAAQ,EAAE,cAAc,EAAE,EAC1B,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,cAAc,KAAK,IAAI,4CAsC3C"}
|