@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,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, "![$2]($1)");
59
+ md = md.replace(/<img[^>]*src="([^"]*)"[^>]*>/gi, "![]($1)");
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
+ };