@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,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
|
+
}
|