@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.
Files changed (107) hide show
  1. package/package.json +43 -2
  2. package/registry/api-table.json +21 -0
  3. package/registry/banner.json +1 -1
  4. package/registry/button.json +1 -1
  5. package/registry/callout.json +19 -0
  6. package/registry/checkbox.json +4 -1
  7. package/registry/code-block.json +26 -0
  8. package/registry/code-group.json +20 -0
  9. package/registry/combobox.json +1 -1
  10. package/registry/command.json +3 -2
  11. package/registry/component-viewer.json +25 -0
  12. package/registry/container.json +18 -0
  13. package/registry/context-menu.json +1 -1
  14. package/registry/create-app-updater.json +13 -0
  15. package/registry/create-document-tabs.json +13 -0
  16. package/registry/create-global-shortcut.json +13 -0
  17. package/registry/create-tauri-window.json +13 -0
  18. package/registry/create-tiptap-editor.json +34 -0
  19. package/registry/dialog.json +1 -1
  20. package/registry/dropdown-menu.json +3 -2
  21. package/registry/field.json +1 -1
  22. package/registry/file-tree.json +21 -0
  23. package/registry/footer.json +1 -1
  24. package/registry/form-message.json +3 -2
  25. package/registry/icon-button.json +1 -1
  26. package/registry/index.json +336 -5
  27. package/registry/navbar.json +1 -1
  28. package/registry/navigation-menu.json +1 -1
  29. package/registry/number-input.json +1 -1
  30. package/registry/package-manager-tabs.json +24 -0
  31. package/registry/pager.json +21 -0
  32. package/registry/popover.json +4 -1
  33. package/registry/resizable.json +1 -1
  34. package/registry/rich-text-editor.json +95 -0
  35. package/registry/scroll-area.json +1 -1
  36. package/registry/section-heading.json +21 -0
  37. package/registry/select.json +3 -2
  38. package/registry/sheet.json +1 -1
  39. package/registry/sidebar.json +1 -1
  40. package/registry/status.json +1 -1
  41. package/registry/steps.json +20 -0
  42. package/registry/switch.json +4 -1
  43. package/registry/table-of-contents.json +23 -0
  44. package/registry/tabs.json +4 -1
  45. package/registry/theme-manager.json +6 -5
  46. package/registry/titlebar-tabs.json +24 -0
  47. package/registry/titlebar.json +26 -0
  48. package/registry/toggle-group.json +1 -1
  49. package/registry/updater-modal.json +26 -0
  50. package/src/index.ts +48 -0
  51. package/src/registry/components/ui/api-table.tsx +117 -0
  52. package/src/registry/components/ui/banner.tsx +0 -2
  53. package/src/registry/components/ui/button.tsx +1 -1
  54. package/src/registry/components/ui/callout.tsx +137 -0
  55. package/src/registry/components/ui/checkbox.tsx +1 -1
  56. package/src/registry/components/ui/code-block.tsx +326 -0
  57. package/src/registry/components/ui/code-group.tsx +105 -0
  58. package/src/registry/components/ui/combobox.tsx +83 -18
  59. package/src/registry/components/ui/command.tsx +110 -106
  60. package/src/registry/components/ui/component-viewer.tsx +363 -0
  61. package/src/registry/components/ui/container.tsx +45 -0
  62. package/src/registry/components/ui/context-menu.tsx +67 -22
  63. package/src/registry/components/ui/dialog.tsx +42 -32
  64. package/src/registry/components/ui/dropdown-menu.tsx +69 -31
  65. package/src/registry/components/ui/field.tsx +7 -3
  66. package/src/registry/components/ui/file-tree.tsx +181 -0
  67. package/src/registry/components/ui/footer.tsx +1 -1
  68. package/src/registry/components/ui/form-message.tsx +2 -2
  69. package/src/registry/components/ui/icon-button.tsx +1 -1
  70. package/src/registry/components/ui/navbar.tsx +18 -11
  71. package/src/registry/components/ui/navigation-menu.tsx +2 -2
  72. package/src/registry/components/ui/number-input.tsx +3 -3
  73. package/src/registry/components/ui/package-manager-tabs.tsx +226 -0
  74. package/src/registry/components/ui/pager.tsx +102 -0
  75. package/src/registry/components/ui/popover.tsx +1 -1
  76. package/src/registry/components/ui/resizable.tsx +3 -1
  77. package/src/registry/components/ui/rich-text-editor/bubble-menu.tsx +236 -0
  78. package/src/registry/components/ui/rich-text-editor/editor.tsx +308 -0
  79. package/src/registry/components/ui/rich-text-editor/extensions.ts +116 -0
  80. package/src/registry/components/ui/rich-text-editor/footer.tsx +36 -0
  81. package/src/registry/components/ui/rich-text-editor/image-dialog.tsx +75 -0
  82. package/src/registry/components/ui/rich-text-editor/index.ts +11 -0
  83. package/src/registry/components/ui/rich-text-editor/link-dialog.tsx +48 -0
  84. package/src/registry/components/ui/rich-text-editor/markdown.ts +170 -0
  85. package/src/registry/components/ui/rich-text-editor/slash-command.tsx +301 -0
  86. package/src/registry/components/ui/rich-text-editor/table-controls.tsx +68 -0
  87. package/src/registry/components/ui/rich-text-editor/toolbar.tsx +465 -0
  88. package/src/registry/components/ui/rich-text-editor/use-editor.ts +231 -0
  89. package/src/registry/components/ui/scroll-area.tsx +12 -2
  90. package/src/registry/components/ui/section-heading.tsx +108 -0
  91. package/src/registry/components/ui/select.tsx +16 -19
  92. package/src/registry/components/ui/sheet.tsx +58 -53
  93. package/src/registry/components/ui/sidebar.tsx +4 -4
  94. package/src/registry/components/ui/status.tsx +7 -5
  95. package/src/registry/components/ui/steps.tsx +198 -0
  96. package/src/registry/components/ui/switch.tsx +1 -1
  97. package/src/registry/components/ui/table-of-contents.tsx +131 -0
  98. package/src/registry/components/ui/tabs.tsx +1 -1
  99. package/src/registry/components/ui/theme-toggle.tsx +175 -139
  100. package/src/registry/components/ui/titlebar-tabs.tsx +773 -0
  101. package/src/registry/components/ui/titlebar.tsx +468 -0
  102. package/src/registry/components/ui/toggle-group.tsx +1 -1
  103. package/src/registry/components/ui/updater-modal.tsx +254 -0
  104. package/src/registry/metadata.ts +167 -6
  105. package/src/registry/providers/theme-provider.tsx +16 -8
  106. package/src/registry/providers/theme-script.tsx +29 -9
  107. 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
+ };