@nikala-ui/core 0.11.0 → 0.12.1
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/LICENSE +20 -0
- package/README.md +97 -20
- 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,170 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Clean bidirectional HTML <-> Markdown serializer for Nikala Rich Text Editor.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export function htmlToMarkdown(html: string): string {
|
|
6
|
+
if (!html) return "";
|
|
7
|
+
|
|
8
|
+
let md = html;
|
|
9
|
+
|
|
10
|
+
// Replace headings
|
|
11
|
+
md = md.replace(/<h1[^>]*>(.*?)<\/h1>/gi, "# $1\n\n");
|
|
12
|
+
md = md.replace(/<h2[^>]*>(.*?)<\/h2>/gi, "## $1\n\n");
|
|
13
|
+
md = md.replace(/<h3[^>]*>(.*?)<\/h3>/gi, "### $1\n\n");
|
|
14
|
+
md = md.replace(/<h4[^>]*>(.*?)<\/h4>/gi, "#### $1\n\n");
|
|
15
|
+
|
|
16
|
+
// Replace blockquotes
|
|
17
|
+
md = md.replace(/<blockquote[^>]*>[\s\S]*?<p>(.*?)<\/p>[\s\S]*?<\/blockquote>/gi, "> $1\n\n");
|
|
18
|
+
md = md.replace(/<blockquote[^>]*>(.*?)<\/blockquote>/gi, "> $1\n\n");
|
|
19
|
+
|
|
20
|
+
// Replace code blocks
|
|
21
|
+
md = md.replace(/<pre[^>]*><code[^>]*>([\s\S]*?)<\/code><\/pre>/gi, "```\n$1\n```\n\n");
|
|
22
|
+
|
|
23
|
+
// Replace inline formatting
|
|
24
|
+
md = md.replace(/<strong>(.*?)<\/strong>/gi, "**$1**");
|
|
25
|
+
md = md.replace(/<b>(.*?)<\/b>/gi, "**$1**");
|
|
26
|
+
md = md.replace(/<em>(.*?)<\/em>/gi, "*$1*");
|
|
27
|
+
md = md.replace(/<i>(.*?)<\/i>/gi, "*$1*");
|
|
28
|
+
md = md.replace(/<s>(.*?)<\/s>/gi, "~~$1~~");
|
|
29
|
+
md = md.replace(/<del>(.*?)<\/del>/gi, "~~$1~~");
|
|
30
|
+
md = md.replace(/<code>(.*?)<\/code>/gi, "`$1`");
|
|
31
|
+
md = md.replace(/<mark[^>]*>(.*?)<\/mark>/gi, "==$1==");
|
|
32
|
+
|
|
33
|
+
// Replace task list items
|
|
34
|
+
md = md.replace(/<li[^>]*data-checked="true"[^>]*>[\s\S]*?<label>[\s\S]*?<\/label>[\s\S]*?<div>[\s\S]*?<p>(.*?)<\/p>[\s\S]*?<\/div><\/li>/gi, "- [x] $1\n");
|
|
35
|
+
md = md.replace(/<li[^>]*data-checked="false"[^>]*>[\s\S]*?<label>[\s\S]*?<\/label>[\s\S]*?<div>[\s\S]*?<p>(.*?)<\/p>[\s\S]*?<\/div><\/li>/gi, "- [ ] $1\n");
|
|
36
|
+
md = md.replace(/<li[^>]*data-checked="true"[^>]*>[\s\S]*?<label>[\s\S]*?<\/label>[\s\S]*?<div>(.*?)<\/div><\/li>/gi, "- [x] $1\n");
|
|
37
|
+
md = md.replace(/<li[^>]*data-checked="false"[^>]*>[\s\S]*?<label>[\s\S]*?<\/label>[\s\S]*?<div>(.*?)<\/div><\/li>/gi, "- [ ] $1\n");
|
|
38
|
+
md = md.replace(/<li[^>]*data-checked="true"[^>]*>([\s\S]*?)<\/li>/gi, "- [x] $1\n");
|
|
39
|
+
md = md.replace(/<li[^>]*data-checked="false"[^>]*>([\s\S]*?)<\/li>/gi, "- [ ] $1\n");
|
|
40
|
+
|
|
41
|
+
// Replace ordered list items inside <ol>
|
|
42
|
+
md = md.replace(/<ol[^>]*>([\s\S]*?)<\/ol>/gi, (match, listContent) => {
|
|
43
|
+
let index = 1;
|
|
44
|
+
return listContent.replace(/<li[^>]*>[\s\S]*?<p>(.*?)<\/p>[\s\S]*?<\/li>/gi, () => `${index++}. $1\n`)
|
|
45
|
+
.replace(/<li[^>]*>(.*?)<\/li>/gi, () => `${index++}. $1\n`) + "\n";
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
// Replace unordered list items inside <ul>
|
|
49
|
+
md = md.replace(/<ul(?:(?!data-type="taskList")[^>])*>([\s\S]*?)<\/ul>/gi, (match, listContent) => {
|
|
50
|
+
return listContent.replace(/<li[^>]*>[\s\S]*?<p>(.*?)<\/p>[\s\S]*?<\/li>/gi, "- $1\n")
|
|
51
|
+
.replace(/<li[^>]*>(.*?)<\/li>/gi, "- $1\n") + "\n";
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
// Replace links
|
|
55
|
+
md = md.replace(/<a[^>]*href="([^"]*)"[^>]*>(.*?)<\/a>/gi, "[$2]($1)");
|
|
56
|
+
|
|
57
|
+
// Replace images
|
|
58
|
+
md = md.replace(/<img[^>]*src="([^"]*)"[^>]*alt="([^"]*)"[^>]*>/gi, "");
|
|
59
|
+
md = md.replace(/<img[^>]*src="([^"]*)"[^>]*>/gi, "");
|
|
60
|
+
|
|
61
|
+
// Replace paragraphs and line breaks
|
|
62
|
+
md = md.replace(/<p[^>]*>(.*?)<\/p>/gi, "$1\n\n");
|
|
63
|
+
md = md.replace(/<br\s*[\/]?>/gi, "\n");
|
|
64
|
+
md = md.replace(/<hr\s*[\/]?>/gi, "---\n\n");
|
|
65
|
+
|
|
66
|
+
// Strip remaining HTML tags securely (iterative loop prevents bypasses)
|
|
67
|
+
let prev: string;
|
|
68
|
+
do {
|
|
69
|
+
prev = md;
|
|
70
|
+
md = md.replace(/<[^>]*>/g, "");
|
|
71
|
+
} while (md !== prev);
|
|
72
|
+
|
|
73
|
+
// Clean multiple extra line breaks
|
|
74
|
+
md = md.replace(/\n{3,}/g, "\n\n").trim();
|
|
75
|
+
|
|
76
|
+
return md;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function markdownToHtml(md: string): string {
|
|
80
|
+
if (!md) return "";
|
|
81
|
+
|
|
82
|
+
let processed = md;
|
|
83
|
+
|
|
84
|
+
// Headings
|
|
85
|
+
processed = processed.replace(/^### (.*$)/gim, "<h3>$1</h3>");
|
|
86
|
+
processed = processed.replace(/^## (.*$)/gim, "<h2>$1</h2>");
|
|
87
|
+
processed = processed.replace(/^# (.*$)/gim, "<h1>$1</h1>");
|
|
88
|
+
|
|
89
|
+
// Code blocks
|
|
90
|
+
processed = processed.replace(/```([\s\S]*?)```/gim, "<pre><code>$1</code></pre>");
|
|
91
|
+
|
|
92
|
+
// Blockquotes
|
|
93
|
+
processed = processed.replace(/^\> (.*$)/gim, "<blockquote><p>$1</p></blockquote>");
|
|
94
|
+
|
|
95
|
+
// Bold, Italic, Strikethrough, Inline Code, Highlight
|
|
96
|
+
processed = processed.replace(/\*\*(.*?)\*\*/gim, "<strong>$1</strong>");
|
|
97
|
+
processed = processed.replace(/\*(.*?)\*/gim, "<em>$1</em>");
|
|
98
|
+
processed = processed.replace(/~~(.*?)~~/gim, "<s>$1</s>");
|
|
99
|
+
processed = processed.replace(/`([^`]+)`/gim, "<code>$1</code>");
|
|
100
|
+
processed = processed.replace(/==([^=]+)==/gim, "<mark>$1</mark>");
|
|
101
|
+
|
|
102
|
+
// Links & Images
|
|
103
|
+
processed = processed.replace(/!\[(.*?)\]\((.*?)\)/gim, '<img src="$2" alt="$1" />');
|
|
104
|
+
processed = processed.replace(/\[(.*?)\]\((.*?)\)/gim, '<a href="$2" target="_blank">$1</a>');
|
|
105
|
+
|
|
106
|
+
// Group consecutive Task Checklist items (- [x] or - [ ])
|
|
107
|
+
processed = processed.replace(/(?:^|\n)((?:- \[[ xX]\] .*(?:\n|$))+)/g, (match, block) => {
|
|
108
|
+
const items = block
|
|
109
|
+
.trim()
|
|
110
|
+
.split("\n")
|
|
111
|
+
.filter(Boolean)
|
|
112
|
+
.map((line: string) => {
|
|
113
|
+
const checked = /^- \[[xX]\]/.test(line.trim());
|
|
114
|
+
const text = line.trim().replace(/^- \[[ xX]\]\s*/, "");
|
|
115
|
+
return `<li data-checked="${checked}" data-type="taskItem"><label><input type="checkbox"${checked ? ' checked="checked"' : ""}><span></span></label><div><p>${text}</p></div></li>`;
|
|
116
|
+
})
|
|
117
|
+
.join("");
|
|
118
|
+
return `\n<ul data-type="taskList">${items}</ul>\n`;
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
// Group consecutive Bullet List items (- or *)
|
|
122
|
+
processed = processed.replace(/(?:^|\n)((?:(?:-|\*) (?!\[[ xX]\]).*(?:\n|$))+)/g, (match, block) => {
|
|
123
|
+
const items = block
|
|
124
|
+
.trim()
|
|
125
|
+
.split("\n")
|
|
126
|
+
.filter(Boolean)
|
|
127
|
+
.map((line: string) => {
|
|
128
|
+
const text = line.trim().replace(/^(?:-|\*)\s*/, "");
|
|
129
|
+
return `<li><p>${text}</p></li>`;
|
|
130
|
+
})
|
|
131
|
+
.join("");
|
|
132
|
+
return `\n<ul>${items}</ul>\n`;
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
// Group consecutive Numbered List items (1. 2. 3.)
|
|
136
|
+
processed = processed.replace(/(?:^|\n)((?:^\d+\. .*(?:\n|$))+)/gm, (match, block) => {
|
|
137
|
+
const items = block
|
|
138
|
+
.trim()
|
|
139
|
+
.split("\n")
|
|
140
|
+
.filter(Boolean)
|
|
141
|
+
.map((line: string) => {
|
|
142
|
+
const text = line.trim().replace(/^\d+\.\s*/, "");
|
|
143
|
+
return `<li><p>${text}</p></li>`;
|
|
144
|
+
})
|
|
145
|
+
.join("");
|
|
146
|
+
return `\n<ol>${items}</ol>\n`;
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
// Paragraphs for remaining text blocks
|
|
150
|
+
const finalHtml = processed
|
|
151
|
+
.split(/\n\n+/)
|
|
152
|
+
.map((block) => {
|
|
153
|
+
const trimmed = block.trim();
|
|
154
|
+
if (!trimmed) return "";
|
|
155
|
+
if (
|
|
156
|
+
trimmed.startsWith("<h") ||
|
|
157
|
+
trimmed.startsWith("<pre") ||
|
|
158
|
+
trimmed.startsWith("<blockquote") ||
|
|
159
|
+
trimmed.startsWith("<ul") ||
|
|
160
|
+
trimmed.startsWith("<ol") ||
|
|
161
|
+
trimmed.startsWith("<table")
|
|
162
|
+
) {
|
|
163
|
+
return trimmed;
|
|
164
|
+
}
|
|
165
|
+
return `<p>${trimmed.replace(/\n/g, "<br />")}</p>`;
|
|
166
|
+
})
|
|
167
|
+
.join("\n");
|
|
168
|
+
|
|
169
|
+
return finalHtml;
|
|
170
|
+
}
|
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createSignal,
|
|
3
|
+
createEffect,
|
|
4
|
+
onCleanup,
|
|
5
|
+
Show,
|
|
6
|
+
For,
|
|
7
|
+
type Component,
|
|
8
|
+
} from "solid-js";
|
|
9
|
+
import { Portal } from "solid-js/web";
|
|
10
|
+
import { useRichTextEditor } from "./editor.js";
|
|
11
|
+
import {
|
|
12
|
+
Heading1,
|
|
13
|
+
Heading2,
|
|
14
|
+
Heading3,
|
|
15
|
+
List,
|
|
16
|
+
ListOrdered,
|
|
17
|
+
ListTodo,
|
|
18
|
+
Quote,
|
|
19
|
+
Table as TableIcon,
|
|
20
|
+
Sparkles,
|
|
21
|
+
Minus,
|
|
22
|
+
} from "lucide-solid";
|
|
23
|
+
|
|
24
|
+
export interface SlashCommandItem {
|
|
25
|
+
title: string;
|
|
26
|
+
description: string;
|
|
27
|
+
icon: Component<{ class?: string }>;
|
|
28
|
+
action: () => void;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export const EditorSlashCommand: Component = () => {
|
|
32
|
+
const { actions, container, isFullscreen } = useRichTextEditor();
|
|
33
|
+
const [isOpen, setIsOpen] = createSignal(false);
|
|
34
|
+
const [selectedIndex, setSelectedIndex] = createSignal(0);
|
|
35
|
+
const [query, setQuery] = createSignal("");
|
|
36
|
+
const [position, setPosition] = createSignal({ top: 0, left: 0 });
|
|
37
|
+
|
|
38
|
+
const itemRefs: HTMLElement[] = [];
|
|
39
|
+
|
|
40
|
+
const items: SlashCommandItem[] = [
|
|
41
|
+
{
|
|
42
|
+
title: "Heading 1",
|
|
43
|
+
description: "Big section heading",
|
|
44
|
+
icon: Heading1,
|
|
45
|
+
action: () => actions.setHeading(1),
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
title: "Heading 2",
|
|
49
|
+
description: "Medium section heading",
|
|
50
|
+
icon: Heading2,
|
|
51
|
+
action: () => actions.setHeading(2),
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
title: "Heading 3",
|
|
55
|
+
description: "Small section heading",
|
|
56
|
+
icon: Heading3,
|
|
57
|
+
action: () => actions.setHeading(3),
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
title: "Bullet List",
|
|
61
|
+
description: "Create a simple bulleted list",
|
|
62
|
+
icon: List,
|
|
63
|
+
action: actions.toggleBulletList,
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
title: "Numbered List",
|
|
67
|
+
description: "Create a numbered sequence",
|
|
68
|
+
icon: ListOrdered,
|
|
69
|
+
action: actions.toggleOrderedList,
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
title: "Task Checklist",
|
|
73
|
+
description: "Track tasks with interactive checkboxes",
|
|
74
|
+
icon: ListTodo,
|
|
75
|
+
action: actions.toggleTaskList,
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
title: "Blockquote",
|
|
79
|
+
description: "Capture a notable quote or callout",
|
|
80
|
+
icon: Quote,
|
|
81
|
+
action: actions.toggleBlockquote,
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
title: "Code Block",
|
|
85
|
+
description: "Display code with monospace font",
|
|
86
|
+
icon: Sparkles,
|
|
87
|
+
action: actions.toggleCodeBlock,
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
title: "Table",
|
|
91
|
+
description: "Insert a 3x3 editable data table",
|
|
92
|
+
icon: TableIcon,
|
|
93
|
+
action: () => actions.insertTable({ rows: 3, cols: 3, withHeaderRow: true }),
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
title: "Divider",
|
|
97
|
+
description: "Separate content with a horizontal line",
|
|
98
|
+
icon: Minus,
|
|
99
|
+
action: actions.insertHorizontalRule,
|
|
100
|
+
},
|
|
101
|
+
];
|
|
102
|
+
|
|
103
|
+
const filteredItems = () =>
|
|
104
|
+
items.filter(
|
|
105
|
+
(item) =>
|
|
106
|
+
item.title.toLowerCase().includes(query().toLowerCase()) ||
|
|
107
|
+
item.description.toLowerCase().includes(query().toLowerCase())
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
// Auto-scroll active item into view
|
|
111
|
+
createEffect(() => {
|
|
112
|
+
if (!isOpen()) return;
|
|
113
|
+
const idx = selectedIndex();
|
|
114
|
+
const el = itemRefs[idx];
|
|
115
|
+
if (el) {
|
|
116
|
+
el.scrollIntoView({ block: "nearest", behavior: "smooth" });
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
const executeItem = (item: SlashCommandItem) => {
|
|
121
|
+
const ed = actions.editor();
|
|
122
|
+
if (ed) {
|
|
123
|
+
const { from } = ed.state.selection;
|
|
124
|
+
const textBefore = ed.state.doc.textBetween(Math.max(0, from - 20), from, "\n");
|
|
125
|
+
const slashIndex = textBefore.lastIndexOf("/");
|
|
126
|
+
if (slashIndex !== -1) {
|
|
127
|
+
const deleteCount = textBefore.length - slashIndex;
|
|
128
|
+
ed.commands.deleteRange({ from: from - deleteCount, to: from });
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
item.action();
|
|
132
|
+
setIsOpen(false);
|
|
133
|
+
setQuery("");
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
createEffect(() => {
|
|
137
|
+
if (typeof window === "undefined") return;
|
|
138
|
+
|
|
139
|
+
const handleEditorUpdate = () => {
|
|
140
|
+
const ed = actions.editor();
|
|
141
|
+
const containerEl = container();
|
|
142
|
+
if (!ed || !ed.isFocused || !ed.isEditable) {
|
|
143
|
+
setIsOpen(false);
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const { from, empty } = ed.state.selection;
|
|
148
|
+
if (!empty) {
|
|
149
|
+
setIsOpen(false);
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const textBefore = ed.state.doc.textBetween(Math.max(0, from - 25), from, "\n");
|
|
154
|
+
const match = textBefore.match(/(?:^|\s)\/([a-zA-Z0-9]*)$/);
|
|
155
|
+
|
|
156
|
+
if (match) {
|
|
157
|
+
const matchedQuery = match[1] || "";
|
|
158
|
+
setQuery(matchedQuery);
|
|
159
|
+
setSelectedIndex(0);
|
|
160
|
+
|
|
161
|
+
try {
|
|
162
|
+
const coords = ed.view.coordsAtPos(from);
|
|
163
|
+
const menuHeight = 280;
|
|
164
|
+
const menuWidth = 288;
|
|
165
|
+
|
|
166
|
+
if (isFullscreen() && containerEl) {
|
|
167
|
+
// Fullscreen mode: position relative to container
|
|
168
|
+
const containerRect = containerEl.getBoundingClientRect();
|
|
169
|
+
const spaceBelow = containerRect.bottom - coords.bottom;
|
|
170
|
+
let top: number;
|
|
171
|
+
|
|
172
|
+
if (spaceBelow < menuHeight && coords.top - containerRect.top > menuHeight) {
|
|
173
|
+
// Flip upwards if not enough room below
|
|
174
|
+
top = coords.top - containerRect.top - menuHeight - 6;
|
|
175
|
+
} else {
|
|
176
|
+
// Open downwards
|
|
177
|
+
top = coords.bottom - containerRect.top + 6;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const left = Math.max(12, Math.min(containerRect.width - menuWidth - 12, coords.left - containerRect.left));
|
|
181
|
+
|
|
182
|
+
setPosition({ top, left });
|
|
183
|
+
setIsOpen(true);
|
|
184
|
+
} else {
|
|
185
|
+
// Normal mode: viewport fixed coordinates to prevent any container overflow clipping
|
|
186
|
+
const spaceBelow = window.innerHeight - coords.bottom;
|
|
187
|
+
let top: number;
|
|
188
|
+
|
|
189
|
+
if (spaceBelow < menuHeight && coords.top > menuHeight) {
|
|
190
|
+
// Flip upwards
|
|
191
|
+
top = coords.top - menuHeight - 6;
|
|
192
|
+
} else {
|
|
193
|
+
// Open downwards
|
|
194
|
+
top = coords.bottom + 6;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const left = Math.max(12, Math.min(window.innerWidth - menuWidth - 12, coords.left));
|
|
198
|
+
|
|
199
|
+
setPosition({ top, left });
|
|
200
|
+
setIsOpen(true);
|
|
201
|
+
}
|
|
202
|
+
} catch {
|
|
203
|
+
setIsOpen(false);
|
|
204
|
+
}
|
|
205
|
+
} else {
|
|
206
|
+
setIsOpen(false);
|
|
207
|
+
}
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
const handleKeyDown = (e: KeyboardEvent) => {
|
|
211
|
+
if (!isOpen()) return;
|
|
212
|
+
|
|
213
|
+
if (e.key === "ArrowDown") {
|
|
214
|
+
e.preventDefault();
|
|
215
|
+
setSelectedIndex((prev) => (prev + 1) % filteredItems().length);
|
|
216
|
+
} else if (e.key === "ArrowUp") {
|
|
217
|
+
e.preventDefault();
|
|
218
|
+
setSelectedIndex((prev) => (prev - 1 + filteredItems().length) % filteredItems().length);
|
|
219
|
+
} else if (e.key === "Enter") {
|
|
220
|
+
e.preventDefault();
|
|
221
|
+
const current = filteredItems()[selectedIndex()];
|
|
222
|
+
if (current) executeItem(current);
|
|
223
|
+
} else if (e.key === "Escape") {
|
|
224
|
+
setIsOpen(false);
|
|
225
|
+
}
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
window.addEventListener("keydown", handleKeyDown, true);
|
|
229
|
+
document.addEventListener("selectionchange", handleEditorUpdate);
|
|
230
|
+
document.addEventListener("input", handleEditorUpdate);
|
|
231
|
+
|
|
232
|
+
onCleanup(() => {
|
|
233
|
+
window.removeEventListener("keydown", handleKeyDown, true);
|
|
234
|
+
document.removeEventListener("selectionchange", handleEditorUpdate);
|
|
235
|
+
document.removeEventListener("input", handleEditorUpdate);
|
|
236
|
+
});
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
const portalTarget = () => {
|
|
240
|
+
if (typeof document === "undefined") return undefined;
|
|
241
|
+
return isFullscreen() ? container() || document.body : document.body;
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
return (
|
|
245
|
+
<Show when={isOpen() && filteredItems().length > 0}>
|
|
246
|
+
<Portal mount={portalTarget()}>
|
|
247
|
+
<div
|
|
248
|
+
class={`${
|
|
249
|
+
isFullscreen() ? "absolute" : "fixed"
|
|
250
|
+
} z-[999999] w-72 rounded-lg border border-border bg-popover text-popover-foreground shadow-2xl p-1.5 max-h-72 overflow-y-auto animate-in fade-in zoom-in-95 pointer-events-auto`}
|
|
251
|
+
style={{
|
|
252
|
+
top: `${position().top}px`,
|
|
253
|
+
left: `${position().left}px`,
|
|
254
|
+
}}
|
|
255
|
+
>
|
|
256
|
+
<div class="px-2 py-1 text-[10px] font-semibold text-muted-foreground uppercase tracking-wider border-b border-border/60 mb-1">
|
|
257
|
+
Slash Commands
|
|
258
|
+
</div>
|
|
259
|
+
<For each={filteredItems()}>
|
|
260
|
+
{(item, index) => {
|
|
261
|
+
const Icon = item.icon;
|
|
262
|
+
const isSelected = () => index() === selectedIndex();
|
|
263
|
+
return (
|
|
264
|
+
<button
|
|
265
|
+
ref={(el) => (itemRefs[index()] = el)}
|
|
266
|
+
type="button"
|
|
267
|
+
class={`flex items-center justify-between w-full px-2 py-2 rounded-md text-left text-xs transition-colors cursor-pointer ${
|
|
268
|
+
isSelected()
|
|
269
|
+
? "bg-accent/80 text-accent-foreground font-semibold ring-1 ring-border shadow-xs"
|
|
270
|
+
: "text-muted-foreground hover:bg-muted/60 hover:text-foreground"
|
|
271
|
+
}`}
|
|
272
|
+
onClick={() => executeItem(item)}
|
|
273
|
+
onMouseEnter={() => setSelectedIndex(index())}
|
|
274
|
+
>
|
|
275
|
+
<div class="flex items-center gap-2.5 truncate">
|
|
276
|
+
<div
|
|
277
|
+
class={`flex items-center justify-center h-6 w-6 rounded-md shrink-0 border ${
|
|
278
|
+
isSelected()
|
|
279
|
+
? "bg-background text-foreground border-border shadow-2xs"
|
|
280
|
+
: "bg-muted text-muted-foreground border-transparent"
|
|
281
|
+
}`}
|
|
282
|
+
>
|
|
283
|
+
<Icon class="h-3.5 w-3.5" />
|
|
284
|
+
</div>
|
|
285
|
+
<div class="flex flex-col truncate">
|
|
286
|
+
<span class="text-foreground text-xs">{item.title}</span>
|
|
287
|
+
<span class="text-[10px] text-muted-foreground truncate">{item.description}</span>
|
|
288
|
+
</div>
|
|
289
|
+
</div>
|
|
290
|
+
<Show when={isSelected()}>
|
|
291
|
+
<span class="text-[10px] text-muted-foreground font-mono ml-1">↵</span>
|
|
292
|
+
</Show>
|
|
293
|
+
</button>
|
|
294
|
+
);
|
|
295
|
+
}}
|
|
296
|
+
</For>
|
|
297
|
+
</div>
|
|
298
|
+
</Portal>
|
|
299
|
+
</Show>
|
|
300
|
+
);
|
|
301
|
+
};
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { Show, type Component } from "solid-js";
|
|
2
|
+
import { useRichTextEditor } from "./editor.js";
|
|
3
|
+
import { Plus, Minus, Trash2 } from "lucide-solid";
|
|
4
|
+
|
|
5
|
+
export const EditorTableControls: Component = () => {
|
|
6
|
+
const { actions, showTableMenu, setShowTableMenu } = useRichTextEditor();
|
|
7
|
+
|
|
8
|
+
return (
|
|
9
|
+
<Show when={showTableMenu()}>
|
|
10
|
+
<div class="absolute top-full left-0 mt-1 z-20 flex flex-col gap-1 p-2 bg-popover text-popover-foreground border border-border rounded-md shadow-md min-w-[160px]">
|
|
11
|
+
<Show
|
|
12
|
+
when={actions.isActive("table")}
|
|
13
|
+
fallback={
|
|
14
|
+
<button
|
|
15
|
+
type="button"
|
|
16
|
+
class="flex items-center gap-2 px-2 py-1.5 text-xs text-left rounded hover:bg-muted cursor-pointer transition-colors"
|
|
17
|
+
onClick={() => {
|
|
18
|
+
actions.insertTable({ rows: 3, cols: 3, withHeaderRow: true });
|
|
19
|
+
setShowTableMenu(false);
|
|
20
|
+
}}
|
|
21
|
+
>
|
|
22
|
+
<Plus class="h-3.5 w-3.5" /> Insert 3x3 Table
|
|
23
|
+
</button>
|
|
24
|
+
}
|
|
25
|
+
>
|
|
26
|
+
<button
|
|
27
|
+
type="button"
|
|
28
|
+
class="flex items-center gap-2 px-2 py-1 text-xs text-left rounded hover:bg-muted cursor-pointer transition-colors"
|
|
29
|
+
onClick={() => actions.editor()?.chain().focus().addRowAfter().run()}
|
|
30
|
+
>
|
|
31
|
+
<Plus class="h-3.5 w-3.5" /> Add Row Below
|
|
32
|
+
</button>
|
|
33
|
+
<button
|
|
34
|
+
type="button"
|
|
35
|
+
class="flex items-center gap-2 px-2 py-1 text-xs text-left rounded hover:bg-muted cursor-pointer transition-colors"
|
|
36
|
+
onClick={() => actions.editor()?.chain().focus().addColumnAfter().run()}
|
|
37
|
+
>
|
|
38
|
+
<Plus class="h-3.5 w-3.5" /> Add Column Right
|
|
39
|
+
</button>
|
|
40
|
+
<button
|
|
41
|
+
type="button"
|
|
42
|
+
class="flex items-center gap-2 px-2 py-1 text-xs text-left rounded hover:bg-muted cursor-pointer text-destructive transition-colors"
|
|
43
|
+
onClick={() => actions.editor()?.chain().focus().deleteRow().run()}
|
|
44
|
+
>
|
|
45
|
+
<Minus class="h-3.5 w-3.5" /> Delete Row
|
|
46
|
+
</button>
|
|
47
|
+
<button
|
|
48
|
+
type="button"
|
|
49
|
+
class="flex items-center gap-2 px-2 py-1 text-xs text-left rounded hover:bg-muted cursor-pointer text-destructive transition-colors"
|
|
50
|
+
onClick={() => actions.editor()?.chain().focus().deleteColumn().run()}
|
|
51
|
+
>
|
|
52
|
+
<Minus class="h-3.5 w-3.5" /> Delete Column
|
|
53
|
+
</button>
|
|
54
|
+
<button
|
|
55
|
+
type="button"
|
|
56
|
+
class="flex items-center gap-2 px-2 py-1 text-xs text-left rounded hover:bg-muted cursor-pointer text-destructive font-medium border-t border-border mt-1 pt-1.5 transition-colors"
|
|
57
|
+
onClick={() => {
|
|
58
|
+
actions.editor()?.chain().focus().deleteTable().run();
|
|
59
|
+
setShowTableMenu(false);
|
|
60
|
+
}}
|
|
61
|
+
>
|
|
62
|
+
<Trash2 class="h-3.5 w-3.5" /> Delete Table
|
|
63
|
+
</button>
|
|
64
|
+
</Show>
|
|
65
|
+
</div>
|
|
66
|
+
</Show>
|
|
67
|
+
);
|
|
68
|
+
};
|