@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,465 @@
1
+ import {
2
+ createSignal,
3
+ splitProps,
4
+ Show,
5
+ For,
6
+ onCleanup,
7
+ createEffect,
8
+ type Component,
9
+ type ComponentProps,
10
+ } from "solid-js";
11
+ import { useRichTextEditor } from "./editor.js";
12
+ import { EditorTableControls } from "./table-controls.js";
13
+ import { cn } from "@/lib/cn";
14
+ import {
15
+ Bold,
16
+ Italic,
17
+ Underline as UnderlineIcon,
18
+ Strikethrough,
19
+ Code,
20
+ Type,
21
+ Heading1,
22
+ Heading2,
23
+ Heading3,
24
+ List,
25
+ ListOrdered,
26
+ ListTodo,
27
+ Quote,
28
+ Table as TableIcon,
29
+ Link as LinkIcon,
30
+ Unlink,
31
+ Image as ImageIcon,
32
+ Undo,
33
+ Redo,
34
+ Maximize2,
35
+ Minimize2,
36
+ AlignLeft,
37
+ AlignCenter,
38
+ AlignRight,
39
+ Highlighter,
40
+ Sparkles,
41
+ RemoveFormatting,
42
+ FileCode2,
43
+ Eye,
44
+ ChevronDown,
45
+ } from "lucide-solid";
46
+
47
+ export const HIGHLIGHT_PALETTE = [
48
+ { name: "Yellow", value: "#eab308" },
49
+ { name: "Amber", value: "#f59e0b" },
50
+ { name: "Orange", value: "#f97316" },
51
+ { name: "Red", value: "#ef4444" },
52
+ { name: "Rose", value: "#f43f5e" },
53
+ { name: "Pink", value: "#ec4899" },
54
+ { name: "Purple", value: "#a855f7" },
55
+ { name: "Violet", value: "#8b5cf6" },
56
+ { name: "Indigo", value: "#6366f1" },
57
+ { name: "Blue", value: "#3b82f6" },
58
+ { name: "Cyan", value: "#06b6d4" },
59
+ { name: "Teal", value: "#14b8a6" },
60
+ { name: "Emerald", value: "#10b981" },
61
+ { name: "Green", value: "#22c55e" },
62
+ { name: "Lime", value: "#84cc16" },
63
+ ];
64
+
65
+ export interface ToolbarButtonProps extends ComponentProps<"button"> {
66
+ isActive?: boolean;
67
+ tooltip?: string;
68
+ }
69
+
70
+ export const ToolbarButton: Component<ToolbarButtonProps> = (props) => {
71
+ const [local, others] = splitProps(props, ["class", "isActive", "tooltip", "children"]);
72
+
73
+ return (
74
+ <button
75
+ type="button"
76
+ title={local.tooltip}
77
+ class={cn(
78
+ "inline-flex items-center justify-center h-8 w-8 rounded-md text-xs font-medium transition-colors cursor-pointer shrink-0 select-none",
79
+ "text-muted-foreground hover:bg-muted hover:text-foreground",
80
+ "focus-visible:outline-hidden focus-visible:ring-1 focus-visible:ring-ring",
81
+ local.isActive &&
82
+ "bg-primary text-primary-foreground font-semibold shadow-xs hover:bg-primary/90 hover:text-primary-foreground",
83
+ local.class
84
+ )}
85
+ {...others}
86
+ >
87
+ {local.children}
88
+ </button>
89
+ );
90
+ };
91
+
92
+ export const EditorToolbar: Component<ComponentProps<"div">> = (props) => {
93
+ const [local, others] = splitProps(props, ["class"]);
94
+ const {
95
+ actions,
96
+ isFullscreen,
97
+ toggleFullscreen,
98
+ viewMode,
99
+ toggleViewMode,
100
+ setShowLinkDialog,
101
+ setShowImageDialog,
102
+ setShowTableMenu,
103
+ } = useRichTextEditor();
104
+
105
+ const [showTypography, setShowTypography] = createSignal(false);
106
+ const [showAlign, setShowAlign] = createSignal(false);
107
+ const [showHighlightPalette, setShowHighlightPalette] = createSignal(false);
108
+
109
+ // Close menus on outside click
110
+ createEffect(() => {
111
+ if (typeof window === "undefined") return;
112
+ const handleClickOutside = (e: MouseEvent) => {
113
+ const target = e.target as HTMLElement;
114
+ if (!target.closest(".editor-popover-trigger") && !target.closest(".editor-popover-content")) {
115
+ setShowTypography(false);
116
+ setShowAlign(false);
117
+ setShowHighlightPalette(false);
118
+ }
119
+ };
120
+ document.addEventListener("click", handleClickOutside);
121
+ onCleanup(() => document.removeEventListener("click", handleClickOutside));
122
+ });
123
+
124
+ const currentTypographyLabel = () => {
125
+ if (actions.isActive("heading", { level: 1 })) return "H1";
126
+ if (actions.isActive("heading", { level: 2 })) return "H2";
127
+ if (actions.isActive("heading", { level: 3 })) return "H3";
128
+ return "Normal";
129
+ };
130
+
131
+ const CurrentAlignIcon = () => {
132
+ if (actions.isActive({ textAlign: "center" })) return AlignCenter;
133
+ if (actions.isActive({ textAlign: "right" })) return AlignRight;
134
+ return AlignLeft;
135
+ };
136
+
137
+ const isEditable = () => actions.editor()?.isEditable ?? true;
138
+
139
+ return (
140
+ <div
141
+ class={cn(
142
+ "sticky top-0 z-10 flex flex-wrap items-center gap-1 border-b border-border bg-card/95 backdrop-blur-xs p-1.5 rounded-t-lg",
143
+ local.class
144
+ )}
145
+ {...others}
146
+ >
147
+ {/* Editing Controls Group (Disabled in Read-Only Mode) */}
148
+ <div
149
+ class="flex flex-wrap items-center gap-1"
150
+ classList={{
151
+ "opacity-40 pointer-events-none select-none": !isEditable(),
152
+ }}
153
+ >
154
+ {/* 1. History (Undo / Redo) */}
155
+ <div class="flex items-center gap-0.5 pr-1 border-r border-border shrink-0">
156
+ <ToolbarButton onClick={actions.undo} disabled={!isEditable() || !actions.canUndo()} tooltip="Undo (Ctrl+Z)">
157
+ <Undo class="h-4 w-4" />
158
+ </ToolbarButton>
159
+ <ToolbarButton onClick={actions.redo} disabled={!isEditable() || !actions.canRedo()} tooltip="Redo (Ctrl+Y)">
160
+ <Redo class="h-4 w-4" />
161
+ </ToolbarButton>
162
+ </div>
163
+
164
+ {/* 2. Compact Typography Dropdown */}
165
+ <div class="relative shrink-0 editor-popover-trigger">
166
+ <button
167
+ type="button"
168
+ onClick={() => setShowTypography((p) => !p)}
169
+ class="inline-flex items-center gap-1 h-8 px-2 rounded-md text-xs font-medium text-foreground bg-muted/50 hover:bg-muted transition-colors cursor-pointer border border-border/40"
170
+ title="Typography"
171
+ >
172
+ <Type class="h-3.5 w-3.5 text-muted-foreground" />
173
+ <span class="font-semibold text-xs min-w-[44px] text-left">{currentTypographyLabel()}</span>
174
+ <ChevronDown class="h-3 w-3 text-muted-foreground" />
175
+ </button>
176
+
177
+ <Show when={showTypography()}>
178
+ <div class="absolute top-full left-0 mt-1 z-30 flex flex-col gap-0.5 p-1 bg-popover text-popover-foreground border border-border rounded-lg shadow-xl min-w-[140px] editor-popover-content animate-in fade-in zoom-in-95">
179
+ <button
180
+ type="button"
181
+ class={cn(
182
+ "flex items-center gap-2 px-2.5 py-1.5 rounded-md text-xs text-left cursor-pointer hover:bg-muted transition-colors",
183
+ !actions.isActive("heading") && "bg-primary text-primary-foreground font-semibold hover:bg-primary"
184
+ )}
185
+ onClick={() => {
186
+ actions.setParagraph();
187
+ setShowTypography(false);
188
+ }}
189
+ >
190
+ <Type class="h-3.5 w-3.5" />
191
+ <span>Normal text</span>
192
+ </button>
193
+ <button
194
+ type="button"
195
+ class={cn(
196
+ "flex items-center gap-2 px-2.5 py-1.5 rounded-md text-xs text-left cursor-pointer hover:bg-muted transition-colors",
197
+ actions.isActive("heading", { level: 1 }) && "bg-primary text-primary-foreground font-semibold hover:bg-primary"
198
+ )}
199
+ onClick={() => {
200
+ actions.setHeading(1);
201
+ setShowTypography(false);
202
+ }}
203
+ >
204
+ <Heading1 class="h-3.5 w-3.5" />
205
+ <span class="font-bold">Heading 1</span>
206
+ </button>
207
+ <button
208
+ type="button"
209
+ class={cn(
210
+ "flex items-center gap-2 px-2.5 py-1.5 rounded-md text-xs text-left cursor-pointer hover:bg-muted transition-colors",
211
+ actions.isActive("heading", { level: 2 }) && "bg-primary text-primary-foreground font-semibold hover:bg-primary"
212
+ )}
213
+ onClick={() => {
214
+ actions.setHeading(2);
215
+ setShowTypography(false);
216
+ }}
217
+ >
218
+ <Heading2 class="h-3.5 w-3.5" />
219
+ <span class="font-semibold">Heading 2</span>
220
+ </button>
221
+ <button
222
+ type="button"
223
+ class={cn(
224
+ "flex items-center gap-2 px-2.5 py-1.5 rounded-md text-xs text-left cursor-pointer hover:bg-muted transition-colors",
225
+ actions.isActive("heading", { level: 3 }) && "bg-primary text-primary-foreground font-semibold hover:bg-primary"
226
+ )}
227
+ onClick={() => {
228
+ actions.setHeading(3);
229
+ setShowTypography(false);
230
+ }}
231
+ >
232
+ <Heading3 class="h-3.5 w-3.5" />
233
+ <span class="font-medium">Heading 3</span>
234
+ </button>
235
+ </div>
236
+ </Show>
237
+ </div>
238
+
239
+ {/* 3. Inline Marks (Bold, Italic, Underline, Strike, Code, Highlight) */}
240
+ <div class="flex items-center gap-0.5 px-1 border-r border-border shrink-0">
241
+ <ToolbarButton onClick={actions.toggleBold} isActive={actions.isActive("bold")} tooltip="Bold (Ctrl+B)">
242
+ <Bold class="h-4 w-4" />
243
+ </ToolbarButton>
244
+ <ToolbarButton onClick={actions.toggleItalic} isActive={actions.isActive("italic")} tooltip="Italic (Ctrl+I)">
245
+ <Italic class="h-4 w-4" />
246
+ </ToolbarButton>
247
+ <ToolbarButton
248
+ onClick={actions.toggleUnderline}
249
+ isActive={actions.isActive("underline")}
250
+ tooltip="Underline (Ctrl+U)"
251
+ >
252
+ <UnderlineIcon class="h-4 w-4" />
253
+ </ToolbarButton>
254
+ <ToolbarButton onClick={actions.toggleStrike} isActive={actions.isActive("strike")} tooltip="Strikethrough">
255
+ <Strikethrough class="h-4 w-4" />
256
+ </ToolbarButton>
257
+ <ToolbarButton onClick={actions.toggleCode} isActive={actions.isActive("code")} tooltip="Inline Code">
258
+ <Code class="h-4 w-4" />
259
+ </ToolbarButton>
260
+
261
+ {/* Highlighter Color Palette */}
262
+ <div class="relative shrink-0 editor-popover-trigger">
263
+ <ToolbarButton
264
+ onClick={() => setShowHighlightPalette((p) => !p)}
265
+ isActive={actions.isActive("highlight")}
266
+ tooltip="Highlight Color"
267
+ >
268
+ <Highlighter class="h-4 w-4" />
269
+ </ToolbarButton>
270
+
271
+ <Show when={showHighlightPalette()}>
272
+ <div class="absolute top-full left-0 mt-1 z-30 flex flex-col gap-1.5 p-2 bg-popover text-popover-foreground border border-border rounded-lg shadow-xl min-w-[200px] editor-popover-content animate-in fade-in zoom-in-95">
273
+ <div class="text-[10px] font-semibold text-muted-foreground uppercase tracking-wider">
274
+ Tailwind Highlight Colors
275
+ </div>
276
+ <div class="grid grid-cols-5 gap-1.5">
277
+ <For each={HIGHLIGHT_PALETTE}>
278
+ {(color) => (
279
+ <button
280
+ type="button"
281
+ title={color.name}
282
+ class="h-6 w-6 rounded-md border border-border/60 hover:scale-110 transition-transform cursor-pointer shadow-2xs"
283
+ style={{ "background-color": color.value }}
284
+ onClick={() => {
285
+ actions.toggleHighlight(color.value);
286
+ setShowHighlightPalette(false);
287
+ }}
288
+ />
289
+ )}
290
+ </For>
291
+ </div>
292
+ <button
293
+ type="button"
294
+ class="w-full text-center py-1 mt-1 text-xs rounded-md bg-muted hover:bg-muted/80 text-foreground transition-colors cursor-pointer font-medium"
295
+ onClick={() => {
296
+ actions.toggleHighlight();
297
+ setShowHighlightPalette(false);
298
+ }}
299
+ >
300
+ Clear Highlight
301
+ </button>
302
+ </div>
303
+ </Show>
304
+ </div>
305
+ </div>
306
+
307
+ {/* 4. Compact Alignment Dropdown */}
308
+ <div class="relative shrink-0 editor-popover-trigger">
309
+ <button
310
+ type="button"
311
+ onClick={() => setShowAlign((p) => !p)}
312
+ class="inline-flex items-center justify-center h-8 w-8 rounded-md text-xs font-medium text-muted-foreground hover:bg-muted hover:text-foreground transition-colors cursor-pointer"
313
+ title="Text Alignment"
314
+ >
315
+ {(() => {
316
+ const Icon = CurrentAlignIcon();
317
+ return <Icon class="h-4 w-4" />;
318
+ })()}
319
+ </button>
320
+
321
+ <Show when={showAlign()}>
322
+ <div class="absolute top-full left-0 mt-1 z-30 flex items-center gap-0.5 p-1 bg-popover text-popover-foreground border border-border rounded-lg shadow-xl editor-popover-content animate-in fade-in zoom-in-95">
323
+ <ToolbarButton
324
+ onClick={() => {
325
+ actions.setTextAlign("left");
326
+ setShowAlign(false);
327
+ }}
328
+ isActive={actions.isActive({ textAlign: "left" })}
329
+ tooltip="Align Left"
330
+ >
331
+ <AlignLeft class="h-4 w-4" />
332
+ </ToolbarButton>
333
+ <ToolbarButton
334
+ onClick={() => {
335
+ actions.setTextAlign("center");
336
+ setShowAlign(false);
337
+ }}
338
+ isActive={actions.isActive({ textAlign: "center" })}
339
+ tooltip="Align Center"
340
+ >
341
+ <AlignCenter class="h-4 w-4" />
342
+ </ToolbarButton>
343
+ <ToolbarButton
344
+ onClick={() => {
345
+ actions.setTextAlign("right");
346
+ setShowAlign(false);
347
+ }}
348
+ isActive={actions.isActive({ textAlign: "right" })}
349
+ tooltip="Align Right"
350
+ >
351
+ <AlignRight class="h-4 w-4" />
352
+ </ToolbarButton>
353
+ </div>
354
+ </Show>
355
+ </div>
356
+
357
+ {/* 5. Lists & Blocks */}
358
+ <div class="flex items-center gap-0.5 px-1 border-r border-border shrink-0">
359
+ <ToolbarButton
360
+ onClick={actions.toggleBulletList}
361
+ isActive={actions.isActive("bulletList")}
362
+ tooltip="Bullet List"
363
+ >
364
+ <List class="h-4 w-4" />
365
+ </ToolbarButton>
366
+ <ToolbarButton
367
+ onClick={actions.toggleOrderedList}
368
+ isActive={actions.isActive("orderedList")}
369
+ tooltip="Numbered List"
370
+ >
371
+ <ListOrdered class="h-4 w-4" />
372
+ </ToolbarButton>
373
+ <ToolbarButton onClick={actions.toggleTaskList} isActive={actions.isActive("taskList")} tooltip="Task Checklist">
374
+ <ListTodo class="h-4 w-4" />
375
+ </ToolbarButton>
376
+ <ToolbarButton
377
+ onClick={actions.toggleBlockquote}
378
+ isActive={actions.isActive("blockquote")}
379
+ tooltip="Blockquote"
380
+ >
381
+ <Quote class="h-4 w-4" />
382
+ </ToolbarButton>
383
+ <ToolbarButton
384
+ onClick={actions.toggleCodeBlock}
385
+ isActive={actions.isActive("codeBlock")}
386
+ tooltip="Code Block"
387
+ >
388
+ <Sparkles class="h-4 w-4" />
389
+ </ToolbarButton>
390
+ </div>
391
+
392
+ {/* 6. Inserts: Link, Image, Table */}
393
+ <div class="flex items-center gap-0.5 px-1 shrink-0">
394
+ <ToolbarButton
395
+ onClick={() => {
396
+ if (actions.isActive("link")) {
397
+ actions.unsetLink();
398
+ } else {
399
+ setShowLinkDialog(true);
400
+ }
401
+ }}
402
+ isActive={actions.isActive("link")}
403
+ tooltip={actions.isActive("link") ? "Remove Link" : "Insert Link"}
404
+ >
405
+ <Show when={actions.isActive("link")} fallback={<LinkIcon class="h-4 w-4" />}>
406
+ <Unlink class="h-4 w-4" />
407
+ </Show>
408
+ </ToolbarButton>
409
+
410
+ <ToolbarButton onClick={() => setShowImageDialog(true)} tooltip="Insert Image">
411
+ <ImageIcon class="h-4 w-4" />
412
+ </ToolbarButton>
413
+
414
+ <div class="relative shrink-0">
415
+ <ToolbarButton
416
+ onClick={() => setShowTableMenu((prev) => !prev)}
417
+ isActive={actions.isActive("table")}
418
+ tooltip="Table Controls"
419
+ >
420
+ <TableIcon class="h-4 w-4" />
421
+ </ToolbarButton>
422
+
423
+ <EditorTableControls />
424
+ </div>
425
+ </div>
426
+ </div>
427
+
428
+ {/* 7. Markdown Toggle, Clear Formatting, Fullscreen */}
429
+ <div class="flex items-center gap-1 border-l border-border pl-1.5 shrink-0 ml-auto">
430
+ <ToolbarButton
431
+ onClick={toggleViewMode}
432
+ isActive={viewMode() === "markdown"}
433
+ tooltip={viewMode() === "markdown" ? "Switch to Visual Editor" : "Switch to Markdown Mode"}
434
+ >
435
+ <Show when={viewMode() === "markdown"} fallback={<FileCode2 class="h-4 w-4" />}>
436
+ <Eye class="h-4 w-4" />
437
+ </Show>
438
+ </ToolbarButton>
439
+
440
+ <ToolbarButton onClick={actions.clearContent} disabled={!isEditable()} tooltip="Clear All Content">
441
+ <RemoveFormatting class="h-4 w-4" />
442
+ </ToolbarButton>
443
+
444
+ <Show
445
+ when={isFullscreen()}
446
+ fallback={
447
+ <ToolbarButton onClick={toggleFullscreen} tooltip="Fullscreen Mode">
448
+ <Maximize2 class="h-4 w-4" />
449
+ </ToolbarButton>
450
+ }
451
+ >
452
+ <button
453
+ type="button"
454
+ onClick={toggleFullscreen}
455
+ class="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-md text-xs font-semibold bg-primary text-primary-foreground hover:bg-primary/90 transition-colors cursor-pointer shadow-xs ml-0.5 shrink-0"
456
+ title="Exit Fullscreen (Esc)"
457
+ >
458
+ <Minimize2 class="h-3.5 w-3.5" />
459
+ <span>Exit Fullscreen</span>
460
+ </button>
461
+ </Show>
462
+ </div>
463
+ </div>
464
+ );
465
+ };
@@ -0,0 +1,231 @@
1
+ import {
2
+ createSignal,
3
+ onMount,
4
+ onCleanup,
5
+ type Accessor,
6
+ } from "solid-js";
7
+ import { Editor, type EditorOptions, type Extension } from "@tiptap/core";
8
+ import { getDefaultExtensions } from "./extensions.js";
9
+
10
+ export interface UseEditorOptions extends Partial<Omit<EditorOptions, "element">> {
11
+ placeholder?: string;
12
+ characterLimit?: number;
13
+ }
14
+
15
+ export interface UseEditorReturn {
16
+ editor: Accessor<Editor | null>;
17
+ html: Accessor<string>;
18
+ text: Accessor<string>;
19
+ markdown: Accessor<string>;
20
+ isEmpty: Accessor<boolean>;
21
+ characterCount: Accessor<number>;
22
+ wordCount: Accessor<number>;
23
+ canUndo: Accessor<boolean>;
24
+ canRedo: Accessor<boolean>;
25
+ mount: (element: HTMLElement) => void;
26
+ destroy: () => void;
27
+ isActive: (name: string | Record<string, any>, attributes?: Record<string, any>) => boolean;
28
+ toggleBold: () => void;
29
+ toggleItalic: () => void;
30
+ toggleUnderline: () => void;
31
+ toggleStrike: () => void;
32
+ toggleCode: () => void;
33
+ toggleHighlight: (color?: string) => void;
34
+ setHeading: (level: 1 | 2 | 3 | 4 | 5 | 6) => void;
35
+ setParagraph: () => void;
36
+ toggleBulletList: () => void;
37
+ toggleOrderedList: () => void;
38
+ toggleTaskList: () => void;
39
+ toggleBlockquote: () => void;
40
+ toggleCodeBlock: () => void;
41
+ setTextAlign: (alignment: "left" | "center" | "right" | "justify") => void;
42
+ setLink: (options: string | { href: string; target?: string }) => void;
43
+ unsetLink: () => void;
44
+ setImage: (options: string | { src: string; alt?: string; title?: string }) => void;
45
+ insertTable: (options?: { rows?: number; cols?: number; withHeaderRow?: boolean }) => void;
46
+ insertHorizontalRule: () => void;
47
+ clearContent: () => void;
48
+ setContent: (content: string) => void;
49
+ setEditable: (editable: boolean) => void;
50
+ undo: () => void;
51
+ redo: () => void;
52
+ }
53
+
54
+ export function useEditor(options: UseEditorOptions = {}): UseEditorReturn {
55
+ const [editorInstance, setEditorInstance] = createSignal<Editor | null>(null);
56
+ const [html, setHtml] = createSignal<string>("");
57
+ const [text, setText] = createSignal<string>("");
58
+ const [markdown, setMarkdown] = createSignal<string>("");
59
+ const [isEmpty, setIsEmpty] = createSignal<boolean>(true);
60
+ const [characterCount, setCharacterCount] = createSignal<number>(0);
61
+ const [wordCount, setWordCount] = createSignal<number>(0);
62
+ const [canUndo, setCanUndo] = createSignal<boolean>(false);
63
+ const [canRedo, setCanRedo] = createSignal<boolean>(false);
64
+ const [transactionVersion, setTransactionVersion] = createSignal<number>(0);
65
+
66
+ let targetElement: HTMLElement | null = null;
67
+
68
+ const updateStats = (ed: Editor) => {
69
+ const currentHtml = ed.getHTML();
70
+ const currentText = ed.getText();
71
+ const currentMd = (ed.storage as any).markdown?.getMarkdown?.() || "";
72
+ setHtml(currentHtml);
73
+ setText(currentText);
74
+ setMarkdown(currentMd);
75
+ setIsEmpty(ed.isEmpty);
76
+ setCharacterCount(ed.storage.characterCount?.characters?.() ?? currentText.length);
77
+ setWordCount(ed.storage.characterCount?.words?.() ?? currentText.split(/\s+/).filter(Boolean).length);
78
+ setCanUndo(ed.can().undo());
79
+ setCanRedo(ed.can().redo());
80
+ setTransactionVersion((v) => v + 1);
81
+ };
82
+
83
+ const initEditor = (el: HTMLElement) => {
84
+ if (typeof window === "undefined") return;
85
+
86
+ const extensions = [
87
+ ...getDefaultExtensions({
88
+ placeholder: options.placeholder,
89
+ characterLimit: options.characterLimit,
90
+ }),
91
+ ...(options.extensions || []),
92
+ ];
93
+
94
+ const editor = new Editor({
95
+ element: el,
96
+ content: options.content,
97
+ editable: options.editable ?? true,
98
+ autofocus: options.autofocus ?? false,
99
+ extensions,
100
+ editorProps: {
101
+ attributes: {
102
+ class:
103
+ "focus:outline-hidden min-h-[220px] w-full max-w-none text-foreground leading-relaxed text-sm " +
104
+ "[&_h1]:text-3xl [&_h1]:font-bold [&_h1]:tracking-tight [&_h1]:mt-6 [&_h1]:mb-3 " +
105
+ "[&_h2]:text-2xl [&_h2]:font-semibold [&_h2]:tracking-tight [&_h2]:mt-5 [&_h2]:mb-2.5 " +
106
+ "[&_h3]:text-xl [&_h3]:font-semibold [&_h3]:mt-4 [&_h3]:mb-2 " +
107
+ "[&_h4]:text-lg [&_h4]:font-semibold [&_h4]:mt-3 [&_h4]:mb-1.5 " +
108
+ "[&_p]:my-2 [&_p]:leading-7 " +
109
+ "[&_ul]:list-disc [&_ul]:pl-6 [&_ul]:my-3 [&_ul]:space-y-1 " +
110
+ "[&_ol]:list-decimal [&_ol]:pl-6 [&_ol]:my-3 [&_ol]:space-y-1 " +
111
+ "[&_li]:my-0.5 " +
112
+ "[&_ul[data-type=taskList]]:list-none [&_ul[data-type=taskList]]:pl-0 [&_ul[data-type=taskList]]:my-3 [&_ul[data-type=taskList]]:space-y-2 " +
113
+ "[&_ul[data-type=taskList]_li]:flex [&_ul[data-type=taskList]_li]:items-start [&_ul[data-type=taskList]_li]:gap-2.5 " +
114
+ "[&_ul[data-type=taskList]_input[type=checkbox]]:mt-1 [&_ul[data-type=taskList]_input[type=checkbox]]:h-4 [&_ul[data-type=taskList]_input[type=checkbox]]:w-4 [&_ul[data-type=taskList]_input[type=checkbox]]:rounded [&_ul[data-type=taskList]_input[type=checkbox]]:border-border [&_ul[data-type=taskList]_input[type=checkbox]]:accent-primary [&_ul[data-type=taskList]_input[type=checkbox]]:cursor-pointer " +
115
+ "[&_mark]:text-foreground [&_mark]:dark:text-background [&_mark]:font-medium [&_mark]:rounded-xs [&_mark]:px-1 [&_mark]:py-0.5 " +
116
+ "[&_.is-editor-empty:first-child::before]:content-[attr(data-placeholder)] [&_.is-editor-empty:first-child::before]:text-muted-foreground [&_.is-editor-empty:first-child::before]:float-left [&_.is-editor-empty:first-child::before]:pointer-events-none [&_.is-editor-empty:first-child::before]:h-0",
117
+ },
118
+ ...options.editorProps,
119
+ },
120
+ onUpdate: (props) => {
121
+ updateStats(props.editor);
122
+ options.onUpdate?.(props);
123
+ },
124
+ onSelectionUpdate: (props) => {
125
+ updateStats(props.editor);
126
+ options.onSelectionUpdate?.(props);
127
+ },
128
+ onTransaction: (props) => {
129
+ updateStats(props.editor);
130
+ options.onTransaction?.(props);
131
+ },
132
+ onFocus: (args) => options.onFocus?.(args),
133
+ onBlur: (args) => options.onBlur?.(args),
134
+ });
135
+
136
+ setEditorInstance(editor);
137
+ updateStats(editor);
138
+ };
139
+
140
+ const mount = (element: HTMLElement) => {
141
+ targetElement = element;
142
+ initEditor(element);
143
+ };
144
+
145
+ const destroy = () => {
146
+ editorInstance()?.destroy();
147
+ setEditorInstance(null);
148
+ };
149
+
150
+ onCleanup(() => {
151
+ destroy();
152
+ });
153
+
154
+ const isActive = (name: string | Record<string, any>, attributes?: Record<string, any>) => {
155
+ transactionVersion();
156
+ const ed = editorInstance();
157
+ if (!ed) return false;
158
+ if (typeof name === "string") {
159
+ return ed.isActive(name, attributes);
160
+ }
161
+ return ed.isActive(name);
162
+ };
163
+
164
+ return {
165
+ editor: editorInstance,
166
+ html,
167
+ text,
168
+ markdown,
169
+ isEmpty,
170
+ characterCount,
171
+ wordCount,
172
+ canUndo,
173
+ canRedo,
174
+ mount,
175
+ destroy,
176
+ isActive,
177
+ toggleBold: () => editorInstance()?.chain().focus().toggleBold().run(),
178
+ toggleItalic: () => editorInstance()?.chain().focus().toggleItalic().run(),
179
+ toggleUnderline: () => editorInstance()?.chain().focus().toggleUnderline().run(),
180
+ toggleStrike: () => editorInstance()?.chain().focus().toggleStrike().run(),
181
+ toggleCode: () => editorInstance()?.chain().focus().toggleCode().run(),
182
+ toggleHighlight: (color) => {
183
+ const ed = editorInstance();
184
+ if (!ed) return;
185
+ if (color) {
186
+ ed.chain().focus().toggleHighlight({ color }).run();
187
+ } else {
188
+ ed.chain().focus().toggleHighlight().run();
189
+ }
190
+ },
191
+ setHeading: (level) => editorInstance()?.chain().focus().toggleHeading({ level }).run(),
192
+ setParagraph: () => editorInstance()?.chain().focus().setParagraph().run(),
193
+ toggleBulletList: () => editorInstance()?.chain().focus().toggleBulletList().run(),
194
+ toggleOrderedList: () => editorInstance()?.chain().focus().toggleOrderedList().run(),
195
+ toggleTaskList: () => editorInstance()?.chain().focus().toggleTaskList().run(),
196
+ toggleBlockquote: () => editorInstance()?.chain().focus().toggleBlockquote().run(),
197
+ toggleCodeBlock: () => editorInstance()?.chain().focus().toggleCodeBlock().run(),
198
+ setTextAlign: (alignment) => editorInstance()?.chain().focus().setTextAlign(alignment).run(),
199
+ setLink: (opts) => {
200
+ const ed = editorInstance();
201
+ if (!ed) return;
202
+ if (typeof opts === "string") {
203
+ ed.chain().focus().setLink({ href: opts, target: "_blank" }).run();
204
+ } else {
205
+ ed.chain().focus().setLink({ href: opts.href, target: opts.target ?? "_blank" }).run();
206
+ }
207
+ },
208
+ unsetLink: () => editorInstance()?.chain().focus().unsetLink().run(),
209
+ setImage: (opts) => {
210
+ const ed = editorInstance();
211
+ if (!ed) return;
212
+ if (typeof opts === "string") {
213
+ ed.chain().focus().setImage({ src: opts }).run();
214
+ } else {
215
+ ed.chain().focus().setImage(opts).run();
216
+ }
217
+ },
218
+ insertTable: (opts) =>
219
+ editorInstance()?.chain().focus().insertTable({
220
+ rows: opts?.rows ?? 3,
221
+ cols: opts?.cols ?? 3,
222
+ withHeaderRow: opts?.withHeaderRow ?? true,
223
+ }).run(),
224
+ insertHorizontalRule: () => editorInstance()?.chain().focus().setHorizontalRule().run(),
225
+ clearContent: () => editorInstance()?.chain().focus().clearContent().run(),
226
+ setContent: (c) => editorInstance()?.commands.setContent(c),
227
+ setEditable: (editable: boolean) => editorInstance()?.setEditable(editable),
228
+ undo: () => editorInstance()?.chain().focus().undo().run(),
229
+ redo: () => editorInstance()?.chain().focus().redo().run(),
230
+ };
231
+ }