@nikala-ui/core 0.11.0 → 0.12.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/package.json +43 -2
- package/registry/api-table.json +21 -0
- package/registry/banner.json +1 -1
- package/registry/button.json +1 -1
- package/registry/callout.json +19 -0
- package/registry/checkbox.json +4 -1
- package/registry/code-block.json +26 -0
- package/registry/code-group.json +20 -0
- package/registry/combobox.json +1 -1
- package/registry/command.json +3 -2
- package/registry/component-viewer.json +25 -0
- package/registry/container.json +18 -0
- package/registry/context-menu.json +1 -1
- package/registry/create-app-updater.json +13 -0
- package/registry/create-document-tabs.json +13 -0
- package/registry/create-global-shortcut.json +13 -0
- package/registry/create-tauri-window.json +13 -0
- package/registry/create-tiptap-editor.json +34 -0
- package/registry/dialog.json +1 -1
- package/registry/dropdown-menu.json +3 -2
- package/registry/field.json +1 -1
- package/registry/file-tree.json +21 -0
- package/registry/footer.json +1 -1
- package/registry/form-message.json +3 -2
- package/registry/icon-button.json +1 -1
- package/registry/index.json +336 -5
- package/registry/navbar.json +1 -1
- package/registry/navigation-menu.json +1 -1
- package/registry/number-input.json +1 -1
- package/registry/package-manager-tabs.json +24 -0
- package/registry/pager.json +21 -0
- package/registry/popover.json +4 -1
- package/registry/resizable.json +1 -1
- package/registry/rich-text-editor.json +95 -0
- package/registry/scroll-area.json +1 -1
- package/registry/section-heading.json +21 -0
- package/registry/select.json +3 -2
- package/registry/sheet.json +1 -1
- package/registry/sidebar.json +1 -1
- package/registry/status.json +1 -1
- package/registry/steps.json +20 -0
- package/registry/switch.json +4 -1
- package/registry/table-of-contents.json +23 -0
- package/registry/tabs.json +4 -1
- package/registry/theme-manager.json +6 -5
- package/registry/titlebar-tabs.json +24 -0
- package/registry/titlebar.json +26 -0
- package/registry/toggle-group.json +1 -1
- package/registry/updater-modal.json +26 -0
- package/src/index.ts +48 -0
- package/src/registry/components/ui/api-table.tsx +117 -0
- package/src/registry/components/ui/banner.tsx +0 -2
- package/src/registry/components/ui/button.tsx +1 -1
- package/src/registry/components/ui/callout.tsx +137 -0
- package/src/registry/components/ui/checkbox.tsx +1 -1
- package/src/registry/components/ui/code-block.tsx +326 -0
- package/src/registry/components/ui/code-group.tsx +105 -0
- package/src/registry/components/ui/combobox.tsx +83 -18
- package/src/registry/components/ui/command.tsx +110 -106
- package/src/registry/components/ui/component-viewer.tsx +363 -0
- package/src/registry/components/ui/container.tsx +45 -0
- package/src/registry/components/ui/context-menu.tsx +67 -22
- package/src/registry/components/ui/dialog.tsx +42 -32
- package/src/registry/components/ui/dropdown-menu.tsx +69 -31
- package/src/registry/components/ui/field.tsx +7 -3
- package/src/registry/components/ui/file-tree.tsx +181 -0
- package/src/registry/components/ui/footer.tsx +1 -1
- package/src/registry/components/ui/form-message.tsx +2 -2
- package/src/registry/components/ui/icon-button.tsx +1 -1
- package/src/registry/components/ui/navbar.tsx +18 -11
- package/src/registry/components/ui/navigation-menu.tsx +2 -2
- package/src/registry/components/ui/number-input.tsx +3 -3
- package/src/registry/components/ui/package-manager-tabs.tsx +226 -0
- package/src/registry/components/ui/pager.tsx +102 -0
- package/src/registry/components/ui/popover.tsx +1 -1
- package/src/registry/components/ui/resizable.tsx +3 -1
- package/src/registry/components/ui/rich-text-editor/bubble-menu.tsx +236 -0
- package/src/registry/components/ui/rich-text-editor/editor.tsx +308 -0
- package/src/registry/components/ui/rich-text-editor/extensions.ts +116 -0
- package/src/registry/components/ui/rich-text-editor/footer.tsx +36 -0
- package/src/registry/components/ui/rich-text-editor/image-dialog.tsx +75 -0
- package/src/registry/components/ui/rich-text-editor/index.ts +11 -0
- package/src/registry/components/ui/rich-text-editor/link-dialog.tsx +48 -0
- package/src/registry/components/ui/rich-text-editor/markdown.ts +170 -0
- package/src/registry/components/ui/rich-text-editor/slash-command.tsx +301 -0
- package/src/registry/components/ui/rich-text-editor/table-controls.tsx +68 -0
- package/src/registry/components/ui/rich-text-editor/toolbar.tsx +465 -0
- package/src/registry/components/ui/rich-text-editor/use-editor.ts +231 -0
- package/src/registry/components/ui/scroll-area.tsx +12 -2
- package/src/registry/components/ui/section-heading.tsx +108 -0
- package/src/registry/components/ui/select.tsx +16 -19
- package/src/registry/components/ui/sheet.tsx +58 -53
- package/src/registry/components/ui/sidebar.tsx +4 -4
- package/src/registry/components/ui/status.tsx +7 -5
- package/src/registry/components/ui/steps.tsx +198 -0
- package/src/registry/components/ui/switch.tsx +1 -1
- package/src/registry/components/ui/table-of-contents.tsx +131 -0
- package/src/registry/components/ui/tabs.tsx +1 -1
- package/src/registry/components/ui/theme-toggle.tsx +175 -139
- package/src/registry/components/ui/titlebar-tabs.tsx +773 -0
- package/src/registry/components/ui/titlebar.tsx +468 -0
- package/src/registry/components/ui/toggle-group.tsx +1 -1
- package/src/registry/components/ui/updater-modal.tsx +254 -0
- package/src/registry/metadata.ts +167 -6
- package/src/registry/providers/theme-provider.tsx +16 -8
- package/src/registry/providers/theme-script.tsx +29 -9
- package/src/registry/providers/theme-transitions.ts +81 -50
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createContext,
|
|
3
|
+
createSignal,
|
|
4
|
+
createEffect,
|
|
5
|
+
useContext,
|
|
6
|
+
splitProps,
|
|
7
|
+
Show,
|
|
8
|
+
onMount,
|
|
9
|
+
onCleanup,
|
|
10
|
+
type Component,
|
|
11
|
+
type ComponentProps,
|
|
12
|
+
type Accessor,
|
|
13
|
+
} from "solid-js";
|
|
14
|
+
import { useEditor, type UseEditorReturn, type UseEditorOptions } from "./use-editor.js";
|
|
15
|
+
import { EditorToolbar } from "./toolbar.js";
|
|
16
|
+
import { EditorFooter } from "./footer.js";
|
|
17
|
+
import { EditorLinkDialog } from "./link-dialog.js";
|
|
18
|
+
import { EditorImageDialog } from "./image-dialog.js";
|
|
19
|
+
import { EditorBubbleMenu } from "./bubble-menu.js";
|
|
20
|
+
import { EditorSlashCommand } from "./slash-command.js";
|
|
21
|
+
import { cn } from "@/lib/cn";
|
|
22
|
+
|
|
23
|
+
export type EditorViewMode = "visual" | "markdown";
|
|
24
|
+
|
|
25
|
+
export interface RichTextEditorContextValue {
|
|
26
|
+
actions: UseEditorReturn;
|
|
27
|
+
container: Accessor<HTMLDivElement | undefined>;
|
|
28
|
+
isFullscreen: Accessor<boolean>;
|
|
29
|
+
toggleFullscreen: () => void;
|
|
30
|
+
viewMode: Accessor<EditorViewMode>;
|
|
31
|
+
toggleViewMode: () => void;
|
|
32
|
+
showLinkDialog: Accessor<boolean>;
|
|
33
|
+
setShowLinkDialog: (show: boolean) => void;
|
|
34
|
+
showImageDialog: Accessor<boolean>;
|
|
35
|
+
setShowImageDialog: (show: boolean) => void;
|
|
36
|
+
showTableMenu: Accessor<boolean>;
|
|
37
|
+
setShowTableMenu: (show: boolean | ((prev: boolean) => boolean)) => void;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const RichTextEditorContext = createContext<RichTextEditorContextValue>();
|
|
41
|
+
|
|
42
|
+
export function useRichTextEditor(): RichTextEditorContextValue {
|
|
43
|
+
const context = useContext(RichTextEditorContext);
|
|
44
|
+
if (!context) {
|
|
45
|
+
throw new Error("useRichTextEditor must be used within a <RichTextEditor /> component");
|
|
46
|
+
}
|
|
47
|
+
return context;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface RichTextEditorProps
|
|
51
|
+
extends Omit<ComponentProps<"div">, "autofocus" | "onChange"> {
|
|
52
|
+
value?: string;
|
|
53
|
+
onChange?: (html: string) => void;
|
|
54
|
+
placeholder?: string;
|
|
55
|
+
editable?: boolean;
|
|
56
|
+
autofocus?: boolean | "start" | "end" | "all" | number;
|
|
57
|
+
characterLimit?: number;
|
|
58
|
+
hideToolbar?: boolean;
|
|
59
|
+
hideFooter?: boolean;
|
|
60
|
+
hideBubbleMenu?: boolean;
|
|
61
|
+
editorOptions?: Omit<UseEditorOptions, "content" | "placeholder" | "editable">;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export const RichTextEditor: Component<RichTextEditorProps> = (props) => {
|
|
65
|
+
const [local, others] = splitProps(props, [
|
|
66
|
+
"class",
|
|
67
|
+
"value",
|
|
68
|
+
"onChange",
|
|
69
|
+
"placeholder",
|
|
70
|
+
"editable",
|
|
71
|
+
"autofocus",
|
|
72
|
+
"characterLimit",
|
|
73
|
+
"hideToolbar",
|
|
74
|
+
"hideFooter",
|
|
75
|
+
"hideBubbleMenu",
|
|
76
|
+
"editorOptions",
|
|
77
|
+
"children",
|
|
78
|
+
]);
|
|
79
|
+
|
|
80
|
+
let containerRef: HTMLDivElement | undefined;
|
|
81
|
+
let editorElementRef: HTMLDivElement | undefined;
|
|
82
|
+
const [container, setContainer] = createSignal<HTMLDivElement | undefined>();
|
|
83
|
+
const [isFullscreen, setIsFullscreen] = createSignal(false);
|
|
84
|
+
const [viewMode, setViewMode] = createSignal<EditorViewMode>("visual");
|
|
85
|
+
const [markdownSource, setMarkdownSource] = createSignal("");
|
|
86
|
+
const [showLinkDialog, setShowLinkDialog] = createSignal(false);
|
|
87
|
+
const [showImageDialog, setShowImageDialog] = createSignal(false);
|
|
88
|
+
const [showTableMenu, setShowTableMenu] = createSignal(false);
|
|
89
|
+
|
|
90
|
+
const actions = useEditor({
|
|
91
|
+
content: local.value,
|
|
92
|
+
placeholder: local.placeholder || "Write something rich or type '/' for commands...",
|
|
93
|
+
editable: local.editable ?? true,
|
|
94
|
+
autofocus: local.autofocus ?? false,
|
|
95
|
+
characterLimit: local.characterLimit,
|
|
96
|
+
onUpdate: ({ editor: ed }) => {
|
|
97
|
+
local.onChange?.(ed.getHTML());
|
|
98
|
+
},
|
|
99
|
+
...local.editorOptions,
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
onMount(() => {
|
|
103
|
+
if (editorElementRef) {
|
|
104
|
+
actions.mount(editorElementRef);
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
createEffect(() => {
|
|
109
|
+
const ed = actions.editor();
|
|
110
|
+
if (ed && local.value !== undefined && ed.getHTML() !== local.value) {
|
|
111
|
+
ed.commands.setContent(local.value, { emitUpdate: false });
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
createEffect(() => {
|
|
116
|
+
const ed = actions.editor();
|
|
117
|
+
if (ed && local.editable !== undefined) {
|
|
118
|
+
actions.setEditable(local.editable);
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
const toggleFullscreen = () => {
|
|
123
|
+
if (typeof document === "undefined") return;
|
|
124
|
+
if (!document.fullscreenElement) {
|
|
125
|
+
if (containerRef?.requestFullscreen) {
|
|
126
|
+
containerRef.requestFullscreen().catch(() => {
|
|
127
|
+
setIsFullscreen(true);
|
|
128
|
+
});
|
|
129
|
+
} else {
|
|
130
|
+
setIsFullscreen(true);
|
|
131
|
+
}
|
|
132
|
+
} else {
|
|
133
|
+
if (document.exitFullscreen) {
|
|
134
|
+
document.exitFullscreen().catch(() => {
|
|
135
|
+
setIsFullscreen(false);
|
|
136
|
+
});
|
|
137
|
+
} else {
|
|
138
|
+
setIsFullscreen(false);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
createEffect(() => {
|
|
144
|
+
if (typeof document === "undefined") return;
|
|
145
|
+
const handleFullscreenChange = () => {
|
|
146
|
+
const isNativeFs = document.fullscreenElement === containerRef;
|
|
147
|
+
setIsFullscreen(isNativeFs);
|
|
148
|
+
};
|
|
149
|
+
document.addEventListener("fullscreenchange", handleFullscreenChange);
|
|
150
|
+
document.addEventListener("webkitfullscreenchange", handleFullscreenChange);
|
|
151
|
+
onCleanup(() => {
|
|
152
|
+
document.removeEventListener("fullscreenchange", handleFullscreenChange);
|
|
153
|
+
document.removeEventListener("webkitfullscreenchange", handleFullscreenChange);
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
const toggleViewMode = () => {
|
|
158
|
+
const ed = actions.editor();
|
|
159
|
+
if (!ed) return;
|
|
160
|
+
|
|
161
|
+
if (viewMode() === "visual") {
|
|
162
|
+
// Switch to Markdown mode
|
|
163
|
+
const currentMd = (ed.storage as any).markdown?.getMarkdown?.() || "";
|
|
164
|
+
setMarkdownSource(currentMd);
|
|
165
|
+
setViewMode("markdown");
|
|
166
|
+
} else {
|
|
167
|
+
// Switch back to Visual mode
|
|
168
|
+
ed.commands.setContent(markdownSource(), { emitUpdate: true });
|
|
169
|
+
local.onChange?.(ed.getHTML());
|
|
170
|
+
setViewMode("visual");
|
|
171
|
+
}
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
const handleMarkdownInput = (val: string) => {
|
|
175
|
+
setMarkdownSource(val);
|
|
176
|
+
const ed = actions.editor();
|
|
177
|
+
if (ed) {
|
|
178
|
+
ed.commands.setContent(val, { emitUpdate: false });
|
|
179
|
+
local.onChange?.(ed.getHTML());
|
|
180
|
+
}
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
createEffect(() => {
|
|
184
|
+
if (typeof window === "undefined") return;
|
|
185
|
+
const handleEsc = (e: KeyboardEvent) => {
|
|
186
|
+
if (e.key === "Escape" && isFullscreen()) {
|
|
187
|
+
if (typeof document !== "undefined" && document.fullscreenElement) {
|
|
188
|
+
document.exitFullscreen?.().catch(() => {});
|
|
189
|
+
} else {
|
|
190
|
+
setIsFullscreen(false);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
};
|
|
194
|
+
window.addEventListener("keydown", handleEsc);
|
|
195
|
+
onCleanup(() => window.removeEventListener("keydown", handleEsc));
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
createEffect(() => {
|
|
199
|
+
if (typeof document === "undefined") return;
|
|
200
|
+
if (isFullscreen()) {
|
|
201
|
+
const original = document.body.style.overflow;
|
|
202
|
+
document.body.style.overflow = "hidden";
|
|
203
|
+
onCleanup(() => {
|
|
204
|
+
document.body.style.overflow = original;
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
const contextValue: RichTextEditorContextValue = {
|
|
210
|
+
actions,
|
|
211
|
+
container,
|
|
212
|
+
isFullscreen,
|
|
213
|
+
toggleFullscreen,
|
|
214
|
+
viewMode,
|
|
215
|
+
toggleViewMode,
|
|
216
|
+
showLinkDialog,
|
|
217
|
+
setShowLinkDialog,
|
|
218
|
+
showImageDialog,
|
|
219
|
+
setShowImageDialog,
|
|
220
|
+
showTableMenu,
|
|
221
|
+
setShowTableMenu: (val) => {
|
|
222
|
+
if (typeof val === "function") {
|
|
223
|
+
setShowTableMenu(val);
|
|
224
|
+
} else {
|
|
225
|
+
setShowTableMenu(val);
|
|
226
|
+
}
|
|
227
|
+
},
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
return (
|
|
231
|
+
<RichTextEditorContext.Provider value={contextValue}>
|
|
232
|
+
<div
|
|
233
|
+
ref={(el) => {
|
|
234
|
+
containerRef = el;
|
|
235
|
+
setContainer(el);
|
|
236
|
+
}}
|
|
237
|
+
class={cn(
|
|
238
|
+
"relative flex flex-col w-full rounded-lg border border-border bg-card text-card-foreground shadow-xs transition-all",
|
|
239
|
+
isFullscreen() &&
|
|
240
|
+
"fixed inset-0 z-[99999] rounded-none border-0 h-screen w-screen bg-background flex flex-col overflow-hidden m-0 p-0",
|
|
241
|
+
local.class
|
|
242
|
+
)}
|
|
243
|
+
{...others}
|
|
244
|
+
>
|
|
245
|
+
<Show when={!local.hideToolbar}>
|
|
246
|
+
<EditorToolbar />
|
|
247
|
+
</Show>
|
|
248
|
+
|
|
249
|
+
{/* Visual WYSIWYG View */}
|
|
250
|
+
<div
|
|
251
|
+
class={cn(
|
|
252
|
+
"relative flex-1 w-full overflow-y-auto min-h-[220px]",
|
|
253
|
+
viewMode() === "markdown" && "hidden"
|
|
254
|
+
)}
|
|
255
|
+
>
|
|
256
|
+
<div
|
|
257
|
+
class={cn(
|
|
258
|
+
"w-full px-4 py-4 min-h-full",
|
|
259
|
+
isFullscreen() && "max-w-4xl mx-auto px-8 py-8"
|
|
260
|
+
)}
|
|
261
|
+
>
|
|
262
|
+
<div ref={editorElementRef} class="tiptap-wrapper w-full h-full" />
|
|
263
|
+
<Show when={local.children}>{local.children}</Show>
|
|
264
|
+
</div>
|
|
265
|
+
</div>
|
|
266
|
+
|
|
267
|
+
{/* Raw Markdown Source View */}
|
|
268
|
+
<Show when={viewMode() === "markdown"}>
|
|
269
|
+
<div class="relative flex-1 w-full overflow-y-auto min-h-[220px] bg-muted/20">
|
|
270
|
+
<div
|
|
271
|
+
class={cn(
|
|
272
|
+
"w-full p-4 min-h-full",
|
|
273
|
+
isFullscreen() && "max-w-4xl mx-auto px-8 py-8"
|
|
274
|
+
)}
|
|
275
|
+
>
|
|
276
|
+
<textarea
|
|
277
|
+
class="w-full h-full min-h-[220px] bg-transparent text-foreground focus:outline-hidden resize-none leading-relaxed font-mono text-xs"
|
|
278
|
+
placeholder="Write or paste raw markdown here..."
|
|
279
|
+
value={markdownSource()}
|
|
280
|
+
onInput={(e) => handleMarkdownInput(e.currentTarget.value)}
|
|
281
|
+
/>
|
|
282
|
+
</div>
|
|
283
|
+
</div>
|
|
284
|
+
</Show>
|
|
285
|
+
|
|
286
|
+
<Show when={!local.hideFooter}>
|
|
287
|
+
<EditorFooter />
|
|
288
|
+
</Show>
|
|
289
|
+
|
|
290
|
+
<Show when={!local.hideBubbleMenu && viewMode() === "visual"}>
|
|
291
|
+
<EditorBubbleMenu />
|
|
292
|
+
</Show>
|
|
293
|
+
|
|
294
|
+
<Show when={viewMode() === "visual"}>
|
|
295
|
+
<EditorSlashCommand />
|
|
296
|
+
</Show>
|
|
297
|
+
|
|
298
|
+
<Show when={showLinkDialog()}>
|
|
299
|
+
<EditorLinkDialog />
|
|
300
|
+
</Show>
|
|
301
|
+
|
|
302
|
+
<Show when={showImageDialog()}>
|
|
303
|
+
<EditorImageDialog />
|
|
304
|
+
</Show>
|
|
305
|
+
</div>
|
|
306
|
+
</RichTextEditorContext.Provider>
|
|
307
|
+
);
|
|
308
|
+
};
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import StarterKit from "@tiptap/starter-kit";
|
|
2
|
+
import Placeholder from "@tiptap/extension-placeholder";
|
|
3
|
+
import Link from "@tiptap/extension-link";
|
|
4
|
+
import Image from "@tiptap/extension-image";
|
|
5
|
+
import TaskList from "@tiptap/extension-task-list";
|
|
6
|
+
import TaskItem from "@tiptap/extension-task-item";
|
|
7
|
+
import { Table, TableRow, TableCell, TableHeader } from "@tiptap/extension-table";
|
|
8
|
+
import Underline from "@tiptap/extension-underline";
|
|
9
|
+
import TextAlign from "@tiptap/extension-text-align";
|
|
10
|
+
import Highlight from "@tiptap/extension-highlight";
|
|
11
|
+
import CharacterCount from "@tiptap/extension-character-count";
|
|
12
|
+
import Subscript from "@tiptap/extension-subscript";
|
|
13
|
+
import Superscript from "@tiptap/extension-superscript";
|
|
14
|
+
import Typography from "@tiptap/extension-typography";
|
|
15
|
+
import { Markdown } from "tiptap-markdown";
|
|
16
|
+
import type { Extension } from "@tiptap/core";
|
|
17
|
+
|
|
18
|
+
export interface DefaultExtensionsOptions {
|
|
19
|
+
placeholder?: string;
|
|
20
|
+
characterLimit?: number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function getDefaultExtensions(options?: DefaultExtensionsOptions): Extension[] {
|
|
24
|
+
return [
|
|
25
|
+
StarterKit.configure({
|
|
26
|
+
heading: {
|
|
27
|
+
levels: [1, 2, 3, 4, 5, 6],
|
|
28
|
+
HTMLAttributes: { class: "font-heading tracking-tight" },
|
|
29
|
+
},
|
|
30
|
+
bulletList: {
|
|
31
|
+
HTMLAttributes: { class: "list-disc pl-6 my-3 space-y-1" },
|
|
32
|
+
},
|
|
33
|
+
orderedList: {
|
|
34
|
+
HTMLAttributes: { class: "list-decimal pl-6 my-3 space-y-1" },
|
|
35
|
+
},
|
|
36
|
+
listItem: {
|
|
37
|
+
HTMLAttributes: { class: "leading-normal my-0.5" },
|
|
38
|
+
},
|
|
39
|
+
codeBlock: {
|
|
40
|
+
HTMLAttributes: {
|
|
41
|
+
class: "rounded-lg bg-muted p-4 font-mono text-xs border border-border my-3 overflow-x-auto text-foreground",
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
code: {
|
|
45
|
+
HTMLAttributes: {
|
|
46
|
+
class: "rounded bg-muted px-1.5 py-0.5 font-mono text-xs text-foreground font-semibold border border-border/50",
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
blockquote: {
|
|
50
|
+
HTMLAttributes: {
|
|
51
|
+
class: "border-l-4 border-primary pl-4 italic text-muted-foreground my-3",
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
horizontalRule: {
|
|
55
|
+
HTMLAttributes: {
|
|
56
|
+
class: "border-border my-6",
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
}) as Extension,
|
|
60
|
+
Placeholder.configure({
|
|
61
|
+
placeholder: options?.placeholder || "Write something rich or type '/' for commands...",
|
|
62
|
+
emptyEditorClass: "is-editor-empty",
|
|
63
|
+
}) as Extension,
|
|
64
|
+
Underline as Extension,
|
|
65
|
+
Subscript as Extension,
|
|
66
|
+
Superscript as Extension,
|
|
67
|
+
Typography as Extension,
|
|
68
|
+
Highlight.configure({
|
|
69
|
+
multicolor: true,
|
|
70
|
+
HTMLAttributes: { class: "rounded px-1 py-0.5" },
|
|
71
|
+
}) as Extension,
|
|
72
|
+
TextAlign.configure({ types: ["heading", "paragraph"] }) as Extension,
|
|
73
|
+
Link.configure({
|
|
74
|
+
openOnClick: false,
|
|
75
|
+
HTMLAttributes: {
|
|
76
|
+
class: "text-primary font-medium underline underline-offset-4 hover:opacity-80 transition-opacity cursor-pointer",
|
|
77
|
+
},
|
|
78
|
+
}) as Extension,
|
|
79
|
+
Image.configure({
|
|
80
|
+
HTMLAttributes: {
|
|
81
|
+
class: "rounded-lg max-w-full h-auto my-4 border border-border shadow-xs",
|
|
82
|
+
},
|
|
83
|
+
}) as Extension,
|
|
84
|
+
TaskList.configure({
|
|
85
|
+
HTMLAttributes: { class: "list-none pl-0 my-3 space-y-2" },
|
|
86
|
+
}) as Extension,
|
|
87
|
+
TaskItem.configure({
|
|
88
|
+
nested: true,
|
|
89
|
+
HTMLAttributes: { class: "flex items-start gap-2.5 my-1" },
|
|
90
|
+
}) as Extension,
|
|
91
|
+
Table.configure({
|
|
92
|
+
resizable: true,
|
|
93
|
+
HTMLAttributes: {
|
|
94
|
+
class: "border-collapse table-auto w-full my-4 border border-border rounded-lg overflow-hidden text-sm",
|
|
95
|
+
},
|
|
96
|
+
}) as Extension,
|
|
97
|
+
TableRow as Extension,
|
|
98
|
+
TableHeader.configure({
|
|
99
|
+
HTMLAttributes: { class: "border border-border bg-muted/70 px-3 py-2 font-semibold text-left text-xs" },
|
|
100
|
+
}) as Extension,
|
|
101
|
+
TableCell.configure({
|
|
102
|
+
HTMLAttributes: { class: "border border-border px-3 py-2 text-xs" },
|
|
103
|
+
}) as Extension,
|
|
104
|
+
CharacterCount.configure({ limit: options?.characterLimit }) as Extension,
|
|
105
|
+
Markdown.configure({
|
|
106
|
+
html: true,
|
|
107
|
+
tightLists: true,
|
|
108
|
+
tightListClass: "tight",
|
|
109
|
+
bulletListMarker: "-",
|
|
110
|
+
linkify: true,
|
|
111
|
+
breaks: false,
|
|
112
|
+
transformPastedText: true,
|
|
113
|
+
transformCopiedText: true,
|
|
114
|
+
}) as Extension,
|
|
115
|
+
];
|
|
116
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { splitProps, type Component, type ComponentProps } from "solid-js";
|
|
2
|
+
import { useRichTextEditor } from "./editor.js";
|
|
3
|
+
import { cn } from "@/lib/cn";
|
|
4
|
+
|
|
5
|
+
export const EditorFooter: Component<ComponentProps<"div">> = (props) => {
|
|
6
|
+
const [local, others] = splitProps(props, ["class"]);
|
|
7
|
+
const { actions } = useRichTextEditor();
|
|
8
|
+
|
|
9
|
+
const readingTime = () => {
|
|
10
|
+
const words = actions.wordCount();
|
|
11
|
+
const minutes = Math.max(1, Math.ceil(words / 200));
|
|
12
|
+
return `${minutes} min read`;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
return (
|
|
16
|
+
<div
|
|
17
|
+
class={cn(
|
|
18
|
+
"flex items-center justify-between border-t border-border px-3 py-1.5 text-[11px] text-muted-foreground bg-muted/30 rounded-b-lg select-none",
|
|
19
|
+
local.class
|
|
20
|
+
)}
|
|
21
|
+
{...others}
|
|
22
|
+
>
|
|
23
|
+
<div class="flex items-center gap-3">
|
|
24
|
+
<span>
|
|
25
|
+
<strong class="text-foreground">{actions.wordCount()}</strong> words
|
|
26
|
+
</span>
|
|
27
|
+
<span>
|
|
28
|
+
<strong class="text-foreground">{actions.characterCount()}</strong> characters
|
|
29
|
+
</span>
|
|
30
|
+
</div>
|
|
31
|
+
<div class="flex items-center gap-2">
|
|
32
|
+
<span>{readingTime()}</span>
|
|
33
|
+
</div>
|
|
34
|
+
</div>
|
|
35
|
+
);
|
|
36
|
+
};
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { createSignal, type Component } from "solid-js";
|
|
2
|
+
import { useRichTextEditor } from "./editor.js";
|
|
3
|
+
|
|
4
|
+
export const EditorImageDialog: Component = () => {
|
|
5
|
+
const { actions, setShowImageDialog } = useRichTextEditor();
|
|
6
|
+
const [url, setUrl] = createSignal("");
|
|
7
|
+
const [alt, setAlt] = createSignal("");
|
|
8
|
+
|
|
9
|
+
const handleApply = (e: Event) => {
|
|
10
|
+
e.preventDefault();
|
|
11
|
+
if (url().trim()) {
|
|
12
|
+
actions.setImage({ src: url().trim(), alt: alt().trim() });
|
|
13
|
+
setShowImageDialog(false);
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const handleFileUpload = (e: Event & { currentTarget: HTMLInputElement }) => {
|
|
18
|
+
const file = e.currentTarget.files?.[0];
|
|
19
|
+
if (!file) return;
|
|
20
|
+
|
|
21
|
+
const reader = new FileReader();
|
|
22
|
+
reader.onload = () => {
|
|
23
|
+
if (typeof reader.result === "string") {
|
|
24
|
+
actions.setImage({ src: reader.result, alt: file.name });
|
|
25
|
+
setShowImageDialog(false);
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
reader.readAsDataURL(file);
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<div class="fixed inset-0 z-50 flex items-center justify-center bg-black/40 backdrop-blur-xs p-4">
|
|
33
|
+
<div class="w-full max-w-sm rounded-lg border border-border bg-card p-4 shadow-lg text-card-foreground">
|
|
34
|
+
<h3 class="text-sm font-semibold mb-3">Insert Image</h3>
|
|
35
|
+
<form onSubmit={handleApply} class="flex flex-col gap-3">
|
|
36
|
+
<div>
|
|
37
|
+
<label class="text-[11px] font-medium text-muted-foreground block mb-1">Image URL</label>
|
|
38
|
+
<input
|
|
39
|
+
type="url"
|
|
40
|
+
placeholder="https://images.unsplash.com/..."
|
|
41
|
+
value={url()}
|
|
42
|
+
onInput={(e) => setUrl(e.currentTarget.value)}
|
|
43
|
+
class="w-full h-8 px-2.5 text-xs rounded-md border border-border bg-background text-foreground focus:outline-hidden focus:ring-1 focus:ring-primary"
|
|
44
|
+
/>
|
|
45
|
+
</div>
|
|
46
|
+
<div>
|
|
47
|
+
<label class="text-[11px] font-medium text-muted-foreground block mb-1">Or Upload Local File</label>
|
|
48
|
+
<input
|
|
49
|
+
type="file"
|
|
50
|
+
accept="image/*"
|
|
51
|
+
onChange={handleFileUpload}
|
|
52
|
+
class="w-full text-xs text-muted-foreground file:mr-2 file:py-1 file:px-2.5 file:rounded-md file:border-0 file:text-xs file:font-semibold file:bg-muted file:text-foreground hover:file:bg-muted/80 cursor-pointer"
|
|
53
|
+
/>
|
|
54
|
+
</div>
|
|
55
|
+
<div class="flex items-center justify-end gap-2 pt-1">
|
|
56
|
+
<button
|
|
57
|
+
type="button"
|
|
58
|
+
class="px-2.5 py-1 text-xs rounded-md border border-border hover:bg-muted cursor-pointer"
|
|
59
|
+
onClick={() => setShowImageDialog(false)}
|
|
60
|
+
>
|
|
61
|
+
Cancel
|
|
62
|
+
</button>
|
|
63
|
+
<button
|
|
64
|
+
type="submit"
|
|
65
|
+
disabled={!url().trim()}
|
|
66
|
+
class="px-3 py-1 text-xs rounded-md bg-primary text-primary-foreground font-medium hover:bg-primary/90 disabled:opacity-50 cursor-pointer"
|
|
67
|
+
>
|
|
68
|
+
Insert Image
|
|
69
|
+
</button>
|
|
70
|
+
</div>
|
|
71
|
+
</form>
|
|
72
|
+
</div>
|
|
73
|
+
</div>
|
|
74
|
+
);
|
|
75
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export * from "./editor.js";
|
|
2
|
+
export * from "./use-editor.js";
|
|
3
|
+
export * from "./extensions.js";
|
|
4
|
+
export * from "./toolbar.js";
|
|
5
|
+
export * from "./bubble-menu.js";
|
|
6
|
+
export * from "./slash-command.js";
|
|
7
|
+
export * from "./link-dialog.js";
|
|
8
|
+
export * from "./image-dialog.js";
|
|
9
|
+
export * from "./table-controls.js";
|
|
10
|
+
export * from "./footer.js";
|
|
11
|
+
export * from "./markdown.js";
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { createSignal, type Component } from "solid-js";
|
|
2
|
+
import { useRichTextEditor } from "./editor.js";
|
|
3
|
+
|
|
4
|
+
export const EditorLinkDialog: Component = () => {
|
|
5
|
+
const { actions, setShowLinkDialog } = useRichTextEditor();
|
|
6
|
+
const [url, setUrl] = createSignal("");
|
|
7
|
+
|
|
8
|
+
const handleApply = (e: Event) => {
|
|
9
|
+
e.preventDefault();
|
|
10
|
+
if (url().trim()) {
|
|
11
|
+
actions.setLink({ href: url().trim() });
|
|
12
|
+
setShowLinkDialog(false);
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
return (
|
|
17
|
+
<div class="fixed inset-0 z-50 flex items-center justify-center bg-black/40 backdrop-blur-xs p-4">
|
|
18
|
+
<div class="w-full max-w-sm rounded-lg border border-border bg-card p-4 shadow-lg text-card-foreground">
|
|
19
|
+
<h3 class="text-sm font-semibold mb-3">Insert Hyperlink</h3>
|
|
20
|
+
<form onSubmit={handleApply} class="flex flex-col gap-3">
|
|
21
|
+
<input
|
|
22
|
+
type="url"
|
|
23
|
+
placeholder="https://example.com"
|
|
24
|
+
value={url()}
|
|
25
|
+
onInput={(e) => setUrl(e.currentTarget.value)}
|
|
26
|
+
autofocus
|
|
27
|
+
class="w-full h-8 px-2.5 text-xs rounded-md border border-border bg-background text-foreground focus:outline-hidden focus:ring-1 focus:ring-primary"
|
|
28
|
+
/>
|
|
29
|
+
<div class="flex items-center justify-end gap-2">
|
|
30
|
+
<button
|
|
31
|
+
type="button"
|
|
32
|
+
class="px-2.5 py-1 text-xs rounded-md border border-border hover:bg-muted cursor-pointer"
|
|
33
|
+
onClick={() => setShowLinkDialog(false)}
|
|
34
|
+
>
|
|
35
|
+
Cancel
|
|
36
|
+
</button>
|
|
37
|
+
<button
|
|
38
|
+
type="submit"
|
|
39
|
+
class="px-3 py-1 text-xs rounded-md bg-primary text-primary-foreground font-medium hover:bg-primary/90 cursor-pointer"
|
|
40
|
+
>
|
|
41
|
+
Apply Link
|
|
42
|
+
</button>
|
|
43
|
+
</div>
|
|
44
|
+
</form>
|
|
45
|
+
</div>
|
|
46
|
+
</div>
|
|
47
|
+
);
|
|
48
|
+
};
|