@kyro-cms/admin 0.9.0 → 0.9.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.
Files changed (100) hide show
  1. package/dist/index.cjs +11960 -11006
  2. package/dist/index.cjs.map +1 -1
  3. package/dist/index.css +67 -65
  4. package/dist/index.css.map +1 -1
  5. package/dist/index.d.cts +563 -0
  6. package/dist/index.d.ts +7 -7
  7. package/dist/index.js +12183 -11238
  8. package/dist/index.js.map +1 -1
  9. package/package.json +15 -11
  10. package/src/components/ActionBar.tsx +27 -14
  11. package/src/components/Admin.tsx +1 -1
  12. package/src/components/ApiKeysManager.tsx +5 -5
  13. package/src/components/AutoForm.tsx +585 -369
  14. package/src/components/BrandingHub.tsx +7 -4
  15. package/src/components/CreateView.tsx +2 -0
  16. package/src/components/DetailView.tsx +71 -56
  17. package/src/components/DeveloperCenter.tsx +8 -6
  18. package/src/components/FieldRenderer.tsx +94 -19
  19. package/src/components/ListView.tsx +33 -20
  20. package/src/components/MediaGallery.tsx +219 -194
  21. package/src/components/PluginsManager.tsx +197 -70
  22. package/src/components/RestPlayground.tsx +7 -7
  23. package/src/components/SessionsManager.tsx +1 -1
  24. package/src/components/SettingsPage.tsx +22 -0
  25. package/src/components/Sidebar.astro +13 -41
  26. package/src/components/UserManagement.tsx +153 -15
  27. package/src/components/UserMenu.tsx +30 -4
  28. package/src/components/VersionHistoryPanel.tsx +112 -119
  29. package/src/components/WebhookManager.tsx +6 -4
  30. package/src/components/blocks/ArrayBlock.tsx +6 -23
  31. package/src/components/blocks/BlockEditModal.tsx +82 -309
  32. package/src/components/blocks/CardBlock.tsx +35 -0
  33. package/src/components/blocks/ChildBlocksTree.tsx +57 -31
  34. package/src/components/blocks/GenericBlock.tsx +44 -0
  35. package/src/components/blocks/HeadingSubheadingBlock.tsx +32 -0
  36. package/src/components/blocks/HeroBlock.tsx +5 -14
  37. package/src/components/blocks/RichTextBlock.tsx +5 -5
  38. package/src/components/blocks/index.ts +5 -3
  39. package/src/components/fields/AccordionField.tsx +2 -2
  40. package/src/components/fields/ArrayField.tsx +1 -1
  41. package/src/components/fields/ArrayLayout.tsx +120 -29
  42. package/src/components/fields/BlocksField.tsx +430 -50
  43. package/src/components/fields/CardField.tsx +73 -0
  44. package/src/components/fields/CheckboxField.tsx +7 -3
  45. package/src/components/fields/DateField.tsx +4 -1
  46. package/src/components/fields/GroupLayout.tsx +2 -2
  47. package/src/components/fields/HeadingSubheadingField.tsx +43 -0
  48. package/src/components/fields/ListField.tsx +2 -2
  49. package/src/components/fields/NumberField.tsx +4 -1
  50. package/src/components/fields/RelationshipField.tsx +153 -87
  51. package/src/components/fields/RichTextField.tsx +781 -0
  52. package/src/components/fields/SecretField.tsx +102 -0
  53. package/src/components/fields/SelectField.tsx +19 -6
  54. package/src/components/fields/TabsLayout.tsx +19 -9
  55. package/src/components/fields/TextField.tsx +4 -1
  56. package/src/components/fields/UploadField.tsx +122 -56
  57. package/src/components/fields/extensions/blockComponents.tsx +103 -174
  58. package/src/components/fields/extensions/blocksStore.ts +8 -1
  59. package/src/components/fields/index.ts +4 -2
  60. package/src/components/ui/PageHeader.tsx +5 -5
  61. package/src/components/ui/SlidePanel.tsx +8 -3
  62. package/src/components/ui/icons.tsx +109 -109
  63. package/src/components/users/UserDetail.tsx +79 -16
  64. package/src/hooks/useAutoFormState.ts +125 -62
  65. package/src/integration.ts +148 -46
  66. package/src/kyro-cms.d.ts +7 -2
  67. package/src/layouts/AuthLayout.astro +14 -2
  68. package/src/lib/autoform-store.ts +85 -52
  69. package/src/lib/change-source.ts +9 -0
  70. package/src/lib/config.ts +104 -8
  71. package/src/lib/globals.ts +44 -9
  72. package/src/lib/normalize-upload-fields.ts +41 -0
  73. package/src/lib/paths.ts +2 -2
  74. package/src/lib/resolve-field-value.ts +110 -0
  75. package/src/lib/shim/use-sync-external-store-with-selector.js +30 -0
  76. package/src/lib/shim/use-sync-external-store.js +1 -0
  77. package/src/lib/stores/index.ts +1 -0
  78. package/src/lib/useResourceManager.ts +4 -4
  79. package/src/lib/vite-shim-plugin.ts +100 -0
  80. package/src/pages/[collection]/[id].astro +1 -1
  81. package/src/pages/preview/[collection]/[id].astro +4 -4
  82. package/src/pages/settings/[slug].astro +2 -2
  83. package/src/styles/main.css +60 -54
  84. package/README.md +0 -46
  85. package/dist/EditorClient-Q23UXR37.cjs +0 -468
  86. package/dist/EditorClient-Q23UXR37.cjs.map +0 -1
  87. package/dist/EditorClient-T5PASFNR.js +0 -466
  88. package/dist/EditorClient-T5PASFNR.js.map +0 -1
  89. package/dist/chunk-3BGDYKTD.cjs +0 -348
  90. package/dist/chunk-3BGDYKTD.cjs.map +0 -1
  91. package/dist/chunk-EEFXLQVT.js +0 -3
  92. package/dist/chunk-EEFXLQVT.js.map +0 -1
  93. package/src/components/blocks/ButtonBlock.tsx +0 -64
  94. package/src/components/blocks/ColumnsBlock.tsx +0 -55
  95. package/src/components/blocks/DividerBlock.tsx +0 -43
  96. package/src/components/blocks/LinkBlock.tsx +0 -65
  97. package/src/components/blocks/VStackBlock.tsx +0 -29
  98. package/src/components/fields/EditorClient.tsx +0 -535
  99. package/src/components/fields/PortableTextField.tsx +0 -155
  100. package/src/components/fields/PortableTextRenderer.tsx +0 -68
@@ -0,0 +1,781 @@
1
+ import React, { useEffect, useState, useRef } from "react";
2
+ import { createPortal } from "react-dom";
3
+ import { useEditor, EditorContent } from "@tiptap/react";
4
+ import StarterKit from "@tiptap/starter-kit";
5
+ import Link from "@tiptap/extension-link";
6
+ import Image from "@tiptap/extension-image";
7
+ import TextAlign from "@tiptap/extension-text-align";
8
+ import Underline from "@tiptap/extension-underline";
9
+ import Highlight from "@tiptap/extension-highlight";
10
+ import TaskList from "@tiptap/extension-task-list";
11
+ import TaskItem from "@tiptap/extension-task-item";
12
+ import { TextStyle } from "@tiptap/extension-text-style";
13
+ import Color from "@tiptap/extension-color";
14
+ import type { Field } from "@kyro-cms/core/client";
15
+ import FieldLayout from "./FieldLayout";
16
+ import { SlidePanel } from "../ui/SlidePanel";
17
+ import { MediaGallery } from "../MediaGallery";
18
+ import {
19
+ Bold,
20
+ Italic,
21
+ Strikethrough,
22
+ Code,
23
+ Heading1,
24
+ Heading2,
25
+ Heading3,
26
+ List,
27
+ ListOrdered,
28
+ Quote,
29
+ AlignLeft,
30
+ AlignCenter,
31
+ AlignRight,
32
+ Link as LinkIcon,
33
+ Image as ImageIcon,
34
+ Undo,
35
+ Redo,
36
+ Terminal,
37
+ Minus,
38
+ Underline as UnderlineIcon,
39
+ Highlighter,
40
+ Palette,
41
+ CheckSquare,
42
+ Eye,
43
+ Edit2,
44
+ Maximize2,
45
+ Minimize2,
46
+ ChevronDown,
47
+ } from "lucide-react";
48
+
49
+ interface RichTextFieldProps {
50
+ field: Field;
51
+ value: Record<string, any> | null;
52
+ onChange: (value: Record<string, any>) => void;
53
+ error?: string;
54
+ disabled?: boolean;
55
+ }
56
+
57
+ const PRESET_COLORS = [
58
+ { name: "Default", value: "inherit" },
59
+ { name: "Red", value: "#ef4444" },
60
+ { name: "Orange", value: "#f97316" },
61
+ { name: "Amber", value: "#f59e0b" },
62
+ { name: "Emerald", value: "#10b981" },
63
+ { name: "Blue", value: "#3b82f6" },
64
+ { name: "Indigo", value: "#6366f1" },
65
+ { name: "Violet", value: "#8b5cf6" },
66
+ { name: "Rose", value: "#f43f5e" },
67
+ { name: "Slate", value: "#64748b" },
68
+ ];
69
+
70
+ const MenuBar = ({
71
+ editor,
72
+ isExpanded,
73
+ setIsExpanded,
74
+ onOpenMediaPicker,
75
+ }: {
76
+ editor: any;
77
+ isExpanded: boolean;
78
+ setIsExpanded: (expanded: boolean) => void;
79
+ onOpenMediaPicker: () => void;
80
+ }) => {
81
+ const [activeDropdown, setActiveDropdown] = useState<string | null>(null);
82
+ const menuBarRef = useRef<HTMLDivElement>(null);
83
+
84
+ useEffect(() => {
85
+ function handleClickOutside(event: MouseEvent) {
86
+ if (menuBarRef.current && !menuBarRef.current.contains(event.target as Node)) {
87
+ setActiveDropdown(null);
88
+ }
89
+ }
90
+ document.addEventListener("mousedown", handleClickOutside);
91
+ return () => document.removeEventListener("mousedown", handleClickOutside);
92
+ }, []);
93
+
94
+ if (!editor) {
95
+ return null;
96
+ }
97
+
98
+ const addImage = () => {
99
+ onOpenMediaPicker();
100
+ };
101
+
102
+ const setLink = () => {
103
+ const previousUrl = editor.getAttributes("link").href;
104
+ const url = window.prompt("URL", previousUrl);
105
+
106
+ if (url === null) {
107
+ return;
108
+ }
109
+
110
+ if (url === "") {
111
+ editor.chain().focus().extendMarkRange("link").unsetLink().run();
112
+ return;
113
+ }
114
+
115
+ editor.chain().focus().extendMarkRange("link").setLink({ href: url }).run();
116
+ };
117
+
118
+ const selectColor = (colorVal: string) => {
119
+ if (colorVal === "inherit") {
120
+ editor.chain().focus().unsetColor().run();
121
+ } else {
122
+ editor.chain().focus().setColor(colorVal).run();
123
+ }
124
+ setActiveDropdown(null);
125
+ };
126
+
127
+ const toggleDropdown = (name: string) => {
128
+ setActiveDropdown(activeDropdown === name ? null : name);
129
+ };
130
+
131
+ const getHeadingLabel = () => {
132
+ if (editor.isActive("heading", { level: 1 })) return "Heading 1";
133
+ if (editor.isActive("heading", { level: 2 })) return "Heading 2";
134
+ if (editor.isActive("heading", { level: 3 })) return "Heading 3";
135
+ if (editor.isActive("heading", { level: 4 })) return "Heading 4";
136
+ return "Normal Text";
137
+ };
138
+
139
+ const getAlignIcon = () => {
140
+ if (editor.isActive({ textAlign: "center" })) return <AlignCenter size={12} />;
141
+ if (editor.isActive({ textAlign: "right" })) return <AlignRight size={12} />;
142
+ return <AlignLeft size={12} />;
143
+ };
144
+
145
+ const getListIcon = () => {
146
+ if (editor.isActive("orderedList")) return <ListOrdered size={12} />;
147
+ if (editor.isActive("taskList")) return <CheckSquare size={12} />;
148
+ return <List size={12} />;
149
+ };
150
+
151
+ const getBlockIcon = () => {
152
+ if (editor.isActive("codeBlock")) return <Terminal size={12} />;
153
+ return <Quote size={12} />;
154
+ };
155
+
156
+ const ToolbarButton = ({
157
+ onClick,
158
+ isActive = false,
159
+ disabled = false,
160
+ children,
161
+ title,
162
+ }: any) => (
163
+ <button
164
+ type="button"
165
+ onClick={onClick}
166
+ disabled={disabled}
167
+ title={title}
168
+ className={`p-1 rounded flex items-center justify-center transition-all duration-150 relative
169
+ ${isActive ? "bg-[var(--kyro-primary)] text-white shadow-xs scale-95" : "text-[var(--kyro-text)] hover:bg-[var(--kyro-bg-hover)]"}
170
+ ${disabled ? "opacity-35 cursor-not-allowed" : "cursor-pointer active:scale-90"}`}
171
+ >
172
+ {children}
173
+ </button>
174
+ );
175
+
176
+ const DropdownTrigger = ({ onClick, isActive, children, title }: any) => (
177
+ <button
178
+ type="button"
179
+ onClick={onClick}
180
+ title={title}
181
+ className={`px-2 py-1 rounded flex items-center gap-1 transition-all duration-150 text-xs border border-transparent hover:bg-[var(--kyro-bg-hover)] cursor-pointer text-[var(--kyro-text)] active:scale-98
182
+ ${isActive ? "bg-[var(--kyro-bg-hover)] border-[var(--kyro-border)]" : ""}`}
183
+ >
184
+ {children}
185
+ <ChevronDown size={10} className="opacity-60" />
186
+ </button>
187
+ );
188
+
189
+ return (
190
+ <div
191
+ ref={menuBarRef}
192
+ className="flex flex-wrap items-center gap-1.5 p-1.5 border-b border-[var(--kyro-border)] bg-[var(--kyro-bg-secondary)] rounded-t-lg select-none"
193
+ >
194
+ {/* Group 1: History Actions */}
195
+ <div className="flex items-center gap-0.5 p-0.5 bg-[var(--kyro-bg)] border border-[var(--kyro-border)] rounded-md shadow-xs">
196
+ <ToolbarButton
197
+ onClick={() => editor.chain().focus().undo().run()}
198
+ disabled={!editor.can().chain().focus().undo().run()}
199
+ title="Undo (Ctrl+Z)"
200
+ >
201
+ <Undo size={12} />
202
+ </ToolbarButton>
203
+ <ToolbarButton
204
+ onClick={() => editor.chain().focus().redo().run()}
205
+ disabled={!editor.can().chain().focus().redo().run()}
206
+ title="Redo (Ctrl+Y)"
207
+ >
208
+ <Redo size={12} />
209
+ </ToolbarButton>
210
+ </div>
211
+
212
+ {/* Group 2: Inline Styles */}
213
+ <div className="flex items-center gap-0.5 p-0.5 bg-[var(--kyro-bg)] border border-[var(--kyro-border)] rounded-md shadow-xs">
214
+ <ToolbarButton
215
+ onClick={() => editor.chain().focus().toggleBold().run()}
216
+ disabled={!editor.can().chain().focus().toggleBold().run()}
217
+ isActive={editor.isActive("bold")}
218
+ title="Bold (Ctrl+B)"
219
+ >
220
+ <Bold size={12} />
221
+ </ToolbarButton>
222
+ <ToolbarButton
223
+ onClick={() => editor.chain().focus().toggleItalic().run()}
224
+ disabled={!editor.can().chain().focus().toggleItalic().run()}
225
+ isActive={editor.isActive("italic")}
226
+ title="Italic (Ctrl+I)"
227
+ >
228
+ <Italic size={12} />
229
+ </ToolbarButton>
230
+ <ToolbarButton
231
+ onClick={() => editor.chain().focus().toggleUnderline().run()}
232
+ disabled={!editor.can().chain().focus().toggleUnderline().run()}
233
+ isActive={editor.isActive("underline")}
234
+ title="Underline (Ctrl+U)"
235
+ >
236
+ <UnderlineIcon size={12} />
237
+ </ToolbarButton>
238
+ <ToolbarButton
239
+ onClick={() => editor.chain().focus().toggleStrike().run()}
240
+ disabled={!editor.can().chain().focus().toggleStrike().run()}
241
+ isActive={editor.isActive("strike")}
242
+ title="Strikethrough"
243
+ >
244
+ <Strikethrough size={12} />
245
+ </ToolbarButton>
246
+ <ToolbarButton
247
+ onClick={() => editor.chain().focus().toggleCode().run()}
248
+ disabled={!editor.can().chain().focus().toggleCode().run()}
249
+ isActive={editor.isActive("code")}
250
+ title="Inline Code"
251
+ >
252
+ <Code size={12} />
253
+ </ToolbarButton>
254
+ <ToolbarButton
255
+ onClick={() => editor.chain().focus().toggleHighlight().run()}
256
+ isActive={editor.isActive("highlight")}
257
+ title="Highlight Text"
258
+ >
259
+ <Highlighter size={12} />
260
+ </ToolbarButton>
261
+
262
+ {/* Text Color Picker Dropdown */}
263
+ <div className="relative flex items-center justify-center">
264
+ <ToolbarButton
265
+ onClick={() => toggleDropdown("color")}
266
+ title="Text Color"
267
+ isActive={activeDropdown === "color" || editor.isActive("textStyle")}
268
+ >
269
+ <Palette size={12} />
270
+ </ToolbarButton>
271
+ {activeDropdown === "color" && (
272
+ <div className="absolute top-full left-0 mt-1.5 p-2 bg-[var(--kyro-bg)] border border-[var(--kyro-border)] rounded-lg shadow-xl z-50 flex flex-wrap gap-1 w-44 animate-in fade-in slide-in-from-top-1 duration-150">
273
+ {PRESET_COLORS.map((col) => (
274
+ <button
275
+ key={col.name}
276
+ type="button"
277
+ onClick={() => selectColor(col.value)}
278
+ title={col.name}
279
+ className="w-6 h-6 rounded-full border border-[var(--kyro-border)] transition-transform hover:scale-115 active:scale-95 cursor-pointer relative"
280
+ style={{
281
+ backgroundColor: col.value === "inherit" ? "transparent" : col.value,
282
+ }}
283
+ >
284
+ {col.value === "inherit" && (
285
+ <span className="absolute inset-0 flex items-center justify-center text-[10px] text-[var(--kyro-text)] font-semibold">
286
+
287
+ </span>
288
+ )}
289
+ </button>
290
+ ))}
291
+ </div>
292
+ )}
293
+ </div>
294
+ </div>
295
+
296
+ {/* Group 3: Headings hierarchy Dropdown */}
297
+ <div className="relative flex items-center p-0.5 bg-[var(--kyro-bg)] border border-[var(--kyro-border)] rounded-md shadow-xs">
298
+ <DropdownTrigger
299
+ onClick={() => toggleDropdown("heading")}
300
+ isActive={activeDropdown === "heading"}
301
+ title="Heading hierarchy"
302
+ >
303
+ <span className="font-medium text-[11px] leading-none min-w-[70px] text-left">
304
+ {getHeadingLabel()}
305
+ </span>
306
+ </DropdownTrigger>
307
+ {activeDropdown === "heading" && (
308
+ <div className="absolute top-full left-0 mt-1.5 p-1 bg-[var(--kyro-bg)] border border-[var(--kyro-border)] rounded-lg shadow-xl z-50 min-w-36 flex flex-col gap-0.5 animate-in fade-in slide-in-from-top-1 duration-150">
309
+ <button
310
+ type="button"
311
+ onClick={() => {
312
+ editor.chain().focus().setParagraph().run();
313
+ setActiveDropdown(null);
314
+ }}
315
+ className={`px-2.5 py-1.5 text-xs text-left rounded-md hover:bg-[var(--kyro-bg-hover)] cursor-pointer text-[var(--kyro-text)] transition-colors
316
+ ${!editor.isActive("heading") ? "font-semibold text-[var(--kyro-primary)] bg-[var(--kyro-bg-hover)]" : ""}`}
317
+ >
318
+ Normal Text
319
+ </button>
320
+ {[1, 2, 3, 4].map((level) => (
321
+ <button
322
+ key={level}
323
+ type="button"
324
+ onClick={() => {
325
+ editor.chain().focus().toggleHeading({ level }).run();
326
+ setActiveDropdown(null);
327
+ }}
328
+ className={`px-2.5 py-1.5 text-xs text-left rounded-md hover:bg-[var(--kyro-bg-hover)] cursor-pointer text-[var(--kyro-text)] transition-colors
329
+ ${editor.isActive("heading", { level }) ? "font-semibold text-[var(--kyro-primary)] bg-[var(--kyro-bg-hover)]" : ""}`}
330
+ >
331
+ Heading {level}
332
+ </button>
333
+ ))}
334
+ </div>
335
+ )}
336
+ </div>
337
+
338
+ {/* Group 4: List Types Dropdown */}
339
+ <div className="relative flex items-center p-0.5 bg-[var(--kyro-bg)] border border-[var(--kyro-border)] rounded-md shadow-xs">
340
+ <DropdownTrigger
341
+ onClick={() => toggleDropdown("lists")}
342
+ isActive={activeDropdown === "lists"}
343
+ title="List Types"
344
+ >
345
+ {getListIcon()}
346
+ </DropdownTrigger>
347
+ {activeDropdown === "lists" && (
348
+ <div className="absolute top-full left-0 mt-1.5 p-1 bg-[var(--kyro-bg)] border border-[var(--kyro-border)] rounded-lg shadow-xl z-50 min-w-36 flex flex-col gap-0.5 animate-in fade-in slide-in-from-top-1 duration-150">
349
+ <button
350
+ type="button"
351
+ onClick={() => {
352
+ editor.chain().focus().toggleBulletList().run();
353
+ setActiveDropdown(null);
354
+ }}
355
+ className={`px-2.5 py-1.5 text-xs text-left rounded-md hover:bg-[var(--kyro-bg-hover)] cursor-pointer text-[var(--kyro-text)] flex items-center gap-2 transition-colors
356
+ ${editor.isActive("bulletList") ? "font-semibold text-[var(--kyro-primary)] bg-[var(--kyro-bg-hover)]" : ""}`}
357
+ >
358
+ <List size={12} />
359
+ Bullet List
360
+ </button>
361
+ <button
362
+ type="button"
363
+ onClick={() => {
364
+ editor.chain().focus().toggleOrderedList().run();
365
+ setActiveDropdown(null);
366
+ }}
367
+ className={`px-2.5 py-1.5 text-xs text-left rounded-md hover:bg-[var(--kyro-bg-hover)] cursor-pointer text-[var(--kyro-text)] flex items-center gap-2 transition-colors
368
+ ${editor.isActive("orderedList") ? "font-semibold text-[var(--kyro-primary)] bg-[var(--kyro-bg-hover)]" : ""}`}
369
+ >
370
+ <ListOrdered size={12} />
371
+ Ordered List
372
+ </button>
373
+ <button
374
+ type="button"
375
+ onClick={() => {
376
+ editor.chain().focus().toggleTaskList().run();
377
+ setActiveDropdown(null);
378
+ }}
379
+ className={`px-2.5 py-1.5 text-xs text-left rounded-md hover:bg-[var(--kyro-bg-hover)] cursor-pointer text-[var(--kyro-text)] flex items-center gap-2 transition-colors
380
+ ${editor.isActive("taskList") ? "font-semibold text-[var(--kyro-primary)] bg-[var(--kyro-bg-hover)]" : ""}`}
381
+ >
382
+ <CheckSquare size={12} />
383
+ Task Checklist
384
+ </button>
385
+ </div>
386
+ )}
387
+ </div>
388
+
389
+ {/* Group 5: Blocks Dropdown */}
390
+ <div className="relative flex items-center p-0.5 bg-[var(--kyro-bg)] border border-[var(--kyro-border)] rounded-md shadow-xs">
391
+ <DropdownTrigger
392
+ onClick={() => toggleDropdown("blocks")}
393
+ isActive={activeDropdown === "blocks"}
394
+ title="Structural Blocks"
395
+ >
396
+ {getBlockIcon()}
397
+ </DropdownTrigger>
398
+ {activeDropdown === "blocks" && (
399
+ <div className="absolute top-full left-0 mt-1.5 p-1 bg-[var(--kyro-bg)] border border-[var(--kyro-border)] rounded-lg shadow-xl z-50 min-w-36 flex flex-col gap-0.5 animate-in fade-in slide-in-from-top-1 duration-150">
400
+ <button
401
+ type="button"
402
+ onClick={() => {
403
+ editor.chain().focus().toggleBlockquote().run();
404
+ setActiveDropdown(null);
405
+ }}
406
+ className={`px-2.5 py-1.5 text-xs text-left rounded-md hover:bg-[var(--kyro-bg-hover)] cursor-pointer text-[var(--kyro-text)] flex items-center gap-2 transition-colors
407
+ ${editor.isActive("blockquote") ? "font-semibold text-[var(--kyro-primary)] bg-[var(--kyro-bg-hover)]" : ""}`}
408
+ >
409
+ <Quote size={12} />
410
+ Blockquote
411
+ </button>
412
+ <button
413
+ type="button"
414
+ onClick={() => {
415
+ editor.chain().focus().toggleCodeBlock().run();
416
+ setActiveDropdown(null);
417
+ }}
418
+ className={`px-2.5 py-1.5 text-xs text-left rounded-md hover:bg-[var(--kyro-bg-hover)] cursor-pointer text-[var(--kyro-text)] flex items-center gap-2 transition-colors
419
+ ${editor.isActive("codeBlock") ? "font-semibold text-[var(--kyro-primary)] bg-[var(--kyro-bg-hover)]" : ""}`}
420
+ >
421
+ <Terminal size={12} />
422
+ Code Block
423
+ </button>
424
+ <button
425
+ type="button"
426
+ onClick={() => {
427
+ editor.chain().focus().setHorizontalRule().run();
428
+ setActiveDropdown(null);
429
+ }}
430
+ className="px-2.5 py-1.5 text-xs text-left rounded-md hover:bg-[var(--kyro-bg-hover)] cursor-pointer text-[var(--kyro-text)] flex items-center gap-2 transition-colors"
431
+ >
432
+ <Minus size={12} />
433
+ Horizontal Rule
434
+ </button>
435
+ </div>
436
+ )}
437
+ </div>
438
+
439
+ {/* Group 6: Text Alignments Dropdown */}
440
+ <div className="relative flex items-center p-0.5 bg-[var(--kyro-bg)] border border-[var(--kyro-border)] rounded-md shadow-xs">
441
+ <DropdownTrigger
442
+ onClick={() => toggleDropdown("align")}
443
+ isActive={activeDropdown === "align"}
444
+ title="Alignment"
445
+ >
446
+ {getAlignIcon()}
447
+ </DropdownTrigger>
448
+ {activeDropdown === "align" && (
449
+ <div className="absolute top-full left-0 mt-1.5 p-1 bg-[var(--kyro-bg)] border border-[var(--kyro-border)] rounded-lg shadow-xl z-50 min-w-32 flex flex-col gap-0.5 animate-in fade-in slide-in-from-top-1 duration-150">
450
+ <button
451
+ type="button"
452
+ onClick={() => {
453
+ editor.chain().focus().setTextAlign("left").run();
454
+ setActiveDropdown(null);
455
+ }}
456
+ className={`px-2.5 py-1.5 text-xs text-left rounded-md hover:bg-[var(--kyro-bg-hover)] cursor-pointer text-[var(--kyro-text)] flex items-center gap-2 transition-colors
457
+ ${editor.isActive({ textAlign: "left" }) ? "font-semibold text-[var(--kyro-primary)] bg-[var(--kyro-bg-hover)]" : ""}`}
458
+ >
459
+ <AlignLeft size={12} />
460
+ Align Left
461
+ </button>
462
+ <button
463
+ type="button"
464
+ onClick={() => {
465
+ editor.chain().focus().setTextAlign("center").run();
466
+ setActiveDropdown(null);
467
+ }}
468
+ className={`px-2.5 py-1.5 text-xs text-left rounded-md hover:bg-[var(--kyro-bg-hover)] cursor-pointer text-[var(--kyro-text)] flex items-center gap-2 transition-colors
469
+ ${editor.isActive({ textAlign: "center" }) ? "font-semibold text-[var(--kyro-primary)] bg-[var(--kyro-bg-hover)]" : ""}`}
470
+ >
471
+ <AlignCenter size={12} />
472
+ Align Center
473
+ </button>
474
+ <button
475
+ type="button"
476
+ onClick={() => {
477
+ editor.chain().focus().setTextAlign("right").run();
478
+ setActiveDropdown(null);
479
+ }}
480
+ className={`px-2.5 py-1.5 text-xs text-left rounded-md hover:bg-[var(--kyro-bg-hover)] cursor-pointer text-[var(--kyro-text)] flex items-center gap-2 transition-colors
481
+ ${editor.isActive({ textAlign: "right" }) ? "font-semibold text-[var(--kyro-primary)] bg-[var(--kyro-bg-hover)]" : ""}`}
482
+ >
483
+ <AlignRight size={12} />
484
+ Align Right
485
+ </button>
486
+ </div>
487
+ )}
488
+ </div>
489
+
490
+ {/* Group 7: Rich Media Embeds */}
491
+ <div className="flex items-center gap-0.5 p-0.5 bg-[var(--kyro-bg)] border border-[var(--kyro-border)] rounded-md shadow-xs">
492
+ <ToolbarButton
493
+ onClick={setLink}
494
+ isActive={editor.isActive("link")}
495
+ title="Link"
496
+ >
497
+ <LinkIcon size={12} />
498
+ </ToolbarButton>
499
+ <ToolbarButton onClick={addImage} title="Add Image">
500
+ <ImageIcon size={12} />
501
+ </ToolbarButton>
502
+ </div>
503
+
504
+ {/* Group 8: Workspace Controls */}
505
+ <div className="flex items-center gap-0.5 p-0.5 bg-[var(--kyro-bg)] border border-[var(--kyro-border)] rounded-md shadow-xs ml-auto">
506
+ <ToolbarButton
507
+ onClick={() => setIsExpanded(!isExpanded)}
508
+ title={isExpanded ? "Collapse Workspace" : "Enlarge Workspace"}
509
+ >
510
+ {isExpanded ? <Minimize2 size={12} /> : <Maximize2 size={12} />}
511
+ </ToolbarButton>
512
+ </div>
513
+ </div>
514
+ );
515
+ };
516
+
517
+ export default function RichTextField({
518
+ field,
519
+ value,
520
+ onChange,
521
+ error,
522
+ disabled,
523
+ }: RichTextFieldProps) {
524
+ const [isExpanded, setIsExpanded] = useState(false);
525
+ const [panelWidth, setPanelWidth] = useState(0);
526
+ const [isMediaPickerOpen, setIsMediaPickerOpen] = useState(false);
527
+ const [isMounted, setIsMounted] = useState(false);
528
+
529
+ useEffect(() => {
530
+ setIsMounted(true);
531
+ }, []);
532
+
533
+ useEffect(() => {
534
+ if (!isExpanded) {
535
+ setPanelWidth(0);
536
+ return;
537
+ }
538
+
539
+ const updateWidth = () => {
540
+ const panel = document.querySelector('[data-kyro-slide-panel="true"]');
541
+ if (panel) {
542
+ setPanelWidth(panel.getBoundingClientRect().width);
543
+ } else {
544
+ setPanelWidth(0);
545
+ }
546
+ };
547
+
548
+ updateWidth();
549
+
550
+ let observer: ResizeObserver | null = null;
551
+ const panel = document.querySelector('[data-kyro-slide-panel="true"]');
552
+ if (panel && typeof ResizeObserver !== "undefined") {
553
+ observer = new ResizeObserver(() => {
554
+ updateWidth();
555
+ });
556
+ observer.observe(panel);
557
+ }
558
+
559
+ window.addEventListener("resize", updateWidth);
560
+ const interval = setInterval(updateWidth, 100);
561
+
562
+ return () => {
563
+ if (observer) {
564
+ observer.disconnect();
565
+ }
566
+ window.removeEventListener("resize", updateWidth);
567
+ clearInterval(interval);
568
+ };
569
+ }, [isExpanded]);
570
+
571
+ const editor = useEditor({
572
+ extensions: [
573
+ StarterKit.configure({
574
+ codeBlock: true,
575
+ }),
576
+ Link.configure({
577
+ openOnClick: false,
578
+ }),
579
+ Image,
580
+ TextAlign.configure({
581
+ types: ["heading", "paragraph"],
582
+ }),
583
+ Underline,
584
+ Highlight.configure({
585
+ multicolor: true,
586
+ }),
587
+ TaskList,
588
+ TaskItem.configure({
589
+ nested: true,
590
+ }),
591
+ TextStyle,
592
+ Color,
593
+ ],
594
+ content: value || {},
595
+ editable: !disabled,
596
+ onUpdate: ({ editor }: { editor: any }) => {
597
+ onChange(editor.getJSON());
598
+ },
599
+ editorProps: {
600
+ attributes: {
601
+ class:
602
+ "prose prose-sm dark:prose-invert focus:outline-none min-h-[160px] p-4 max-w-none kyro-richtext text-[0.875rem] leading-relaxed",
603
+ },
604
+ },
605
+ });
606
+
607
+ useEffect(() => {
608
+ if (editor && value && JSON.stringify(value) !== JSON.stringify(editor.getJSON())) {
609
+ editor.commands.setContent(value);
610
+ }
611
+ }, [value, editor]);
612
+
613
+ if (!isMounted) {
614
+ return (
615
+ <FieldLayout field={field} error={error}>
616
+ <div
617
+ className={`border rounded-lg bg-[var(--kyro-bg)] overflow-hidden border-[var(--kyro-border)] flex flex-col shadow-sm transition-all duration-200
618
+ ${error ? "border-[var(--kyro-error)] shadow-[0_0_0_1px_var(--kyro-error)]" : "border-[var(--kyro-border)]"}
619
+ ${disabled ? "opacity-60 cursor-not-allowed" : ""}`}
620
+ >
621
+ <div className="flex flex-wrap items-center gap-1.5 p-1.5 border-b border-[var(--kyro-border)] bg-[var(--kyro-bg-secondary)] rounded-t-lg h-[40px]"></div>
622
+ <div className="overflow-y-auto min-h-[160px] max-h-[400px] p-4"></div>
623
+ </div>
624
+ </FieldLayout>
625
+ );
626
+ }
627
+
628
+ return (
629
+ <FieldLayout field={field} error={error}>
630
+ {isExpanded ? (
631
+ // Maximize mode placeholder inside form
632
+ <div className="border rounded-lg bg-[var(--kyro-bg-secondary)] border-[var(--kyro-border)] p-4 text-center text-xs text-[var(--kyro-text-muted)] flex items-center justify-center gap-3 h-20 transition-all duration-200 shadow-inner">
633
+ <span className="font-medium">
634
+ Rich text editor workspace is currently maximized.
635
+ </span>
636
+ <button
637
+ type="button"
638
+ onClick={() => setIsExpanded(false)}
639
+ className="px-2.5 py-1 bg-[var(--kyro-primary)] text-white text-xs rounded-md shadow-sm font-medium hover:scale-102 active:scale-98 transition-transform cursor-pointer"
640
+ >
641
+ Minimize Workspace
642
+ </button>
643
+ </div>
644
+ ) : (
645
+ // Standard inline editor
646
+ <div
647
+ className={`border rounded-lg bg-[var(--kyro-bg)] overflow-hidden border-[var(--kyro-border)] flex flex-col shadow-sm transition-all duration-200
648
+ ${error ? "border-[var(--kyro-error)] shadow-[0_0_0_1px_var(--kyro-error)]" : "border-[var(--kyro-border)] focus-within:border-[var(--kyro-primary)] focus-within:ring-1 focus-within:ring-[var(--kyro-primary)]"}
649
+ ${disabled ? "opacity-60 cursor-not-allowed" : ""}`}
650
+ >
651
+ <MenuBar
652
+ editor={editor}
653
+ isExpanded={isExpanded}
654
+ setIsExpanded={setIsExpanded}
655
+ onOpenMediaPicker={() => setIsMediaPickerOpen(true)}
656
+ />
657
+ <div className="overflow-y-auto min-h-[160px] max-h-[400px]">
658
+ <EditorContent editor={editor} />
659
+ </div>
660
+ </div>
661
+ )}
662
+
663
+ {/* Portal absolute fullscreen overlay directly inside document.body */}
664
+ {isExpanded &&
665
+ typeof document !== "undefined" &&
666
+ createPortal(
667
+ <div
668
+ style={{
669
+ position: "fixed",
670
+ top: "16px",
671
+ bottom: "16px",
672
+ left: "16px",
673
+ right: panelWidth > 0 ? `${panelWidth + 16}px` : "16px",
674
+ zIndex: 9999,
675
+ }}
676
+ className="flex flex-col bg-[var(--kyro-bg)] border border-[var(--kyro-border)] rounded-md shadow-2xl overflow-hidden animate-in fade-in zoom-in-95 duration-200 transition-all duration-300"
677
+ >
678
+ <MenuBar
679
+ editor={editor}
680
+ isExpanded={isExpanded}
681
+ setIsExpanded={setIsExpanded}
682
+ onOpenMediaPicker={() => setIsMediaPickerOpen(true)}
683
+ />
684
+ <div className="overflow-y-auto flex-1 h-[calc(100vh-140px)] bg-[var(--kyro-bg)]">
685
+ <EditorContent editor={editor} />
686
+ </div>
687
+ </div>,
688
+ document.body
689
+ )}
690
+
691
+ {/* Injected tasklist, and highlight custom styles for preview & editor */}
692
+ <style>{`
693
+ .kyro-richtext ul[data-type="taskList"] {
694
+ list-style: none !important;
695
+ padding: 0 !important;
696
+ margin: 0.5rem 0 !important;
697
+ }
698
+ .kyro-richtext li[data-type="taskItem"] {
699
+ display: flex !important;
700
+ align-items: flex-start !important;
701
+ gap: 0.5rem !important;
702
+ margin-bottom: 0.25rem !important;
703
+ }
704
+ .kyro-richtext li[data-type="taskItem"] > label {
705
+ margin-top: 0.25rem !important;
706
+ user-select: none !important;
707
+ cursor: pointer !important;
708
+ }
709
+ .kyro-richtext li[data-type="taskItem"] > div {
710
+ flex: 1 !important;
711
+ }
712
+ .kyro-richtext mark {
713
+ background-color: #fef08a !important;
714
+ border-radius: 0.25rem !important;
715
+ padding: 0.125rem 0.25rem !important;
716
+ color: #1e293b !important;
717
+ }
718
+ .kyro-richtext pre {
719
+ background-color: var(--kyro-bg-secondary, #1e1e3f) !important;
720
+ color: var(--kyro-text, #f8f8f2) !important;
721
+ padding: 1rem !important;
722
+ border-radius: 0.375rem !important;
723
+ border: 1px solid var(--kyro-border) !important;
724
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace !important;
725
+ overflow-x: auto !important;
726
+ margin: 1rem 0 !important;
727
+ }
728
+ .kyro-richtext pre code {
729
+ background: none !important;
730
+ padding: 0 !important;
731
+ border-radius: 0 !important;
732
+ font-size: 0.85rem !important;
733
+ }
734
+ .kyro-richtext h1 { font-size: 2em !important; font-weight: 700 !important; margin: 0 0 0.75rem !important; line-height: 1.2 !important; }
735
+ .kyro-richtext h2 { font-size: 1.5em !important; font-weight: 600 !important; margin: 0 0 0.75rem !important; line-height: 1.2 !important; }
736
+ .kyro-richtext h3 { font-size: 1.17em !important; font-weight: 600 !important; margin: 0 0 0.75rem !important; line-height: 1.2 !important; }
737
+ .kyro-richtext h4 { font-size: 1em !important; font-weight: 600 !important; margin: 0 0 0.75rem !important; line-height: 1.2 !important; }
738
+ .kyro-richtext ul, .kyro-richtext ol {
739
+ padding-left: 1.5rem !important;
740
+ }
741
+ .kyro-richtext blockquote {
742
+ border-left: 4px solid rgba(148, 163, 184, 0.5) !important;
743
+ margin-left: 0 !important;
744
+ padding-left: 1rem !important;
745
+ font-style: italic !important;
746
+ }
747
+
748
+ `}</style>
749
+
750
+ {/* Media Picker Modal */}
751
+ {isMediaPickerOpen && (
752
+ <SlidePanel
753
+ open={isMediaPickerOpen}
754
+ onClose={() => setIsMediaPickerOpen(false)}
755
+ title="Select Image"
756
+ width="xl"
757
+ >
758
+ <MediaGallery
759
+ pickerMode
760
+ multiple={false}
761
+ onSelect={(selectedItems) => {
762
+ if (selectedItems && selectedItems.length > 0) {
763
+ const selectedImage = selectedItems[0];
764
+ editor
765
+ .chain()
766
+ .focus()
767
+ .setImage({
768
+ src: selectedImage.url,
769
+ alt: selectedImage.alt || selectedImage.title || "",
770
+ title: selectedImage.title || "",
771
+ })
772
+ .run();
773
+ }
774
+ setIsMediaPickerOpen(false);
775
+ }}
776
+ />
777
+ </SlidePanel>
778
+ )}
779
+ </FieldLayout>
780
+ );
781
+ }