@adminforge/core 0.3.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 (86) hide show
  1. package/.turbo/turbo-build.log +56 -0
  2. package/CHANGELOG.md +32 -0
  3. package/LICENSE +21 -0
  4. package/bin/adminforge.js +317 -0
  5. package/dist/auth-client.cjs +45 -0
  6. package/dist/auth-client.cjs.map +1 -0
  7. package/dist/auth-client.d.cts +17 -0
  8. package/dist/auth-client.d.ts +17 -0
  9. package/dist/auth-client.js +20 -0
  10. package/dist/auth-client.js.map +1 -0
  11. package/dist/auth.cjs +65 -0
  12. package/dist/auth.cjs.map +1 -0
  13. package/dist/auth.d.cts +21 -0
  14. package/dist/auth.d.ts +21 -0
  15. package/dist/auth.js +36 -0
  16. package/dist/auth.js.map +1 -0
  17. package/dist/client-D0cjJVsn.d.ts +20 -0
  18. package/dist/client-sRnmZ-Y9.d.cts +20 -0
  19. package/dist/index-CyzxaE7n.d.cts +124 -0
  20. package/dist/index-CyzxaE7n.d.ts +124 -0
  21. package/dist/index.cjs +453 -0
  22. package/dist/index.cjs.map +1 -0
  23. package/dist/index.d.cts +65 -0
  24. package/dist/index.d.ts +65 -0
  25. package/dist/index.js +410 -0
  26. package/dist/index.js.map +1 -0
  27. package/dist/next.cjs +839 -0
  28. package/dist/next.cjs.map +1 -0
  29. package/dist/next.d.cts +84 -0
  30. package/dist/next.d.ts +84 -0
  31. package/dist/next.js +800 -0
  32. package/dist/next.js.map +1 -0
  33. package/dist/styles.css +763 -0
  34. package/dist/styles.css.map +1 -0
  35. package/dist/styles.d.cts +2 -0
  36. package/dist/styles.d.ts +2 -0
  37. package/dist/ui.cjs +2500 -0
  38. package/dist/ui.cjs.map +1 -0
  39. package/dist/ui.d.cts +119 -0
  40. package/dist/ui.d.ts +119 -0
  41. package/dist/ui.js +2448 -0
  42. package/dist/ui.js.map +1 -0
  43. package/eslint.config.js +35 -0
  44. package/package.json +99 -0
  45. package/src/api/controller.ts +234 -0
  46. package/src/api/index.ts +4 -0
  47. package/src/api/next.ts +281 -0
  48. package/src/api/security/agent-auth.ts +134 -0
  49. package/src/auth/config.ts +20 -0
  50. package/src/auth/index.ts +3 -0
  51. package/src/auth/middleware.ts +15 -0
  52. package/src/auth/provider.tsx +28 -0
  53. package/src/core/fields/index.ts +119 -0
  54. package/src/core/hooks/index.ts +60 -0
  55. package/src/core/index.ts +43 -0
  56. package/src/core/registry/index.ts +22 -0
  57. package/src/core/schema/collection.ts +12 -0
  58. package/src/core/schema/config.ts +11 -0
  59. package/src/core/schema/normalize.ts +32 -0
  60. package/src/core/types/index.ts +114 -0
  61. package/src/db/client.ts +146 -0
  62. package/src/db/index.ts +3 -0
  63. package/src/db/schema-generator.ts +104 -0
  64. package/src/fields/index.ts +1 -0
  65. package/src/index.ts +4 -0
  66. package/src/next.ts +3 -0
  67. package/src/styles/adminforge.css +840 -0
  68. package/src/ui/AdminDashboard.tsx +176 -0
  69. package/src/ui/AdminForgeContext.tsx +64 -0
  70. package/src/ui/components/AdminLayout.tsx +107 -0
  71. package/src/ui/form-engine/FormEngine.tsx +250 -0
  72. package/src/ui/form-engine/ImageUpload.tsx +68 -0
  73. package/src/ui/form-engine/RelationInput.tsx +215 -0
  74. package/src/ui/form-engine/RichTextEditor.tsx +708 -0
  75. package/src/ui/index.ts +18 -0
  76. package/src/ui/screens/AdminPage.tsx +162 -0
  77. package/src/ui/screens/AgentTokenPage.tsx +232 -0
  78. package/src/ui/screens/CollectionFormPage.tsx +135 -0
  79. package/src/ui/screens/CollectionListPage.tsx +170 -0
  80. package/src/ui/screens/CollectionSchemaPage.tsx +180 -0
  81. package/src/ui/screens/RoleDetailPage.tsx +147 -0
  82. package/src/ui/screens/RolesListPage.tsx +57 -0
  83. package/src/ui/table-engine/TableEngine.tsx +157 -0
  84. package/src/ui.ts +3 -0
  85. package/tsconfig.json +10 -0
  86. package/tsup.config.ts +54 -0
@@ -0,0 +1,708 @@
1
+ "use client";
2
+
3
+ import { useEditor, EditorContent, Editor } from "@tiptap/react";
4
+ import { BubbleMenu } from "@tiptap/react/menus";
5
+ import StarterKit from "@tiptap/starter-kit";
6
+ import Link from "@tiptap/extension-link";
7
+ import Underline from "@tiptap/extension-underline";
8
+ import TextAlign from "@tiptap/extension-text-align";
9
+ import Placeholder from "@tiptap/extension-placeholder";
10
+ import Highlight from "@tiptap/extension-highlight";
11
+ import Subscript from "@tiptap/extension-subscript";
12
+ import Superscript from "@tiptap/extension-superscript";
13
+ import TaskList from "@tiptap/extension-task-list";
14
+ import TaskItem from "@tiptap/extension-task-item";
15
+ import HorizontalRule from "@tiptap/extension-horizontal-rule";
16
+ import Typography from "@tiptap/extension-typography";
17
+ import BubbleMenuExtension from "@tiptap/extension-bubble-menu";
18
+ import Image from "@tiptap/extension-image";
19
+ import { EditorView } from "@tiptap/pm/view";
20
+ import { Slice } from "@tiptap/pm/model";
21
+ import { useEffect, useState, useCallback } from "react";
22
+
23
+ interface RichTextEditorProps {
24
+ name: string;
25
+ value?: string;
26
+ onChange: (value: string) => void;
27
+ }
28
+
29
+ function MenuButton({
30
+ onClick,
31
+ isActive = false,
32
+ disabled = false,
33
+ icon,
34
+ title
35
+ }: {
36
+ onClick: () => void;
37
+ isActive?: boolean;
38
+ disabled?: boolean;
39
+ icon: string;
40
+ title: string;
41
+ }) {
42
+ return (
43
+ <button
44
+ type="button"
45
+ onClick={onClick}
46
+ disabled={disabled}
47
+ className={`adminforge-editor-btn ${isActive ? "active" : ""}`}
48
+ title={title}
49
+ >
50
+ <span className="material-symbols-outlined" style={{ fontSize: '18px' }}>{icon}</span>
51
+ </button>
52
+ );
53
+ }
54
+
55
+ function Toolbar({ editor }: { editor: Editor | null }) {
56
+ const setLink = useCallback(() => {
57
+ if (!editor) return;
58
+ const previousUrl = editor.getAttributes('link').href;
59
+ const url = window.prompt('URL', previousUrl);
60
+
61
+ if (url === null) return;
62
+ if (url === '') {
63
+ editor.chain().focus().extendMarkRange('link').unsetLink().run();
64
+ return;
65
+ }
66
+
67
+ editor.chain().focus().extendMarkRange('link').setLink({ href: url }).run();
68
+ }, [editor]);
69
+
70
+ const addImage = useCallback(() => {
71
+ if (!editor) return;
72
+
73
+ const input = document.createElement('input');
74
+ input.type = 'file';
75
+ input.accept = 'image/*';
76
+ input.onchange = async () => {
77
+ const file = input.files?.[0];
78
+ if (!file) return;
79
+
80
+ const formData = new FormData();
81
+ formData.append('file', file);
82
+
83
+ try {
84
+ const res = await fetch('/api/upload', {
85
+ method: 'POST',
86
+ body: formData,
87
+ });
88
+ if (!res.ok) throw new Error('Upload failed');
89
+ const data = await res.json();
90
+ editor.chain().focus().setImage({ src: data.url }).run();
91
+ } catch (err) {
92
+ alert('Failed to upload image');
93
+ }
94
+ };
95
+ input.click();
96
+ }, [editor]);
97
+
98
+ if (!editor) return null;
99
+
100
+ return (
101
+ <div className="adminforge-editor-toolbar">
102
+ <div className="adminforge-editor-toolbar-group">
103
+ <MenuButton
104
+ onClick={() => editor.chain().focus().undo().run()}
105
+ disabled={!editor.can().undo()}
106
+ icon="undo"
107
+ title="Undo"
108
+ />
109
+ <MenuButton
110
+ onClick={() => editor.chain().focus().redo().run()}
111
+ disabled={!editor.can().redo()}
112
+ icon="redo"
113
+ title="Redo"
114
+ />
115
+ </div>
116
+
117
+ <div className="adminforge-editor-toolbar-separator" />
118
+
119
+ <div className="adminforge-editor-toolbar-group">
120
+ <MenuButton
121
+ onClick={() => editor.chain().focus().toggleHeading({ level: 1 }).run()}
122
+ isActive={editor.isActive("heading", { level: 1 })}
123
+ icon="format_h1"
124
+ title="Heading 1"
125
+ />
126
+ <MenuButton
127
+ onClick={() => editor.chain().focus().toggleHeading({ level: 2 }).run()}
128
+ isActive={editor.isActive("heading", { level: 2 })}
129
+ icon="format_h2"
130
+ title="Heading 2"
131
+ />
132
+ <MenuButton
133
+ onClick={() => editor.chain().focus().toggleHeading({ level: 3 }).run()}
134
+ isActive={editor.isActive("heading", { level: 3 })}
135
+ icon="format_h3"
136
+ title="Heading 3"
137
+ />
138
+ </div>
139
+
140
+ <div className="adminforge-editor-toolbar-separator" />
141
+
142
+ <div className="adminforge-editor-toolbar-group">
143
+ <MenuButton
144
+ onClick={() => editor.chain().focus().toggleBold().run()}
145
+ isActive={editor.isActive("bold")}
146
+ icon="format_bold"
147
+ title="Bold"
148
+ />
149
+ <MenuButton
150
+ onClick={() => editor.chain().focus().toggleItalic().run()}
151
+ isActive={editor.isActive("italic")}
152
+ icon="format_italic"
153
+ title="Italic"
154
+ />
155
+ <MenuButton
156
+ onClick={() => editor.chain().focus().toggleUnderline().run()}
157
+ isActive={editor.isActive("underline")}
158
+ icon="format_underlined"
159
+ title="Underline"
160
+ />
161
+ <MenuButton
162
+ onClick={() => editor.chain().focus().toggleStrike().run()}
163
+ isActive={editor.isActive("strike")}
164
+ icon="format_strikethrough"
165
+ title="Strikethrough"
166
+ />
167
+ <MenuButton
168
+ onClick={() => editor.chain().focus().toggleCode().run()}
169
+ isActive={editor.isActive("code")}
170
+ icon="code"
171
+ title="Code"
172
+ />
173
+ <MenuButton
174
+ onClick={() => editor.chain().focus().toggleHighlight().run()}
175
+ isActive={editor.isActive("highlight")}
176
+ icon="format_ink_highlighter"
177
+ title="Highlight"
178
+ />
179
+ </div>
180
+
181
+ <div className="adminforge-editor-toolbar-separator" />
182
+
183
+ <div className="adminforge-editor-toolbar-group">
184
+ <MenuButton
185
+ onClick={setLink}
186
+ isActive={editor.isActive("link")}
187
+ icon="link"
188
+ title="Link"
189
+ />
190
+ <MenuButton
191
+ onClick={addImage}
192
+ icon="add_photo_alternate"
193
+ title="Add Image"
194
+ />
195
+ <MenuButton
196
+ onClick={() => editor.chain().focus().toggleSubscript().run()}
197
+ isActive={editor.isActive("subscript")}
198
+ icon="subscript"
199
+ title="Subscript"
200
+ />
201
+ <MenuButton
202
+ onClick={() => editor.chain().focus().toggleSuperscript().run()}
203
+ isActive={editor.isActive("superscript")}
204
+ icon="superscript"
205
+ title="Superscript"
206
+ />
207
+ </div>
208
+
209
+ <div className="adminforge-editor-toolbar-separator" />
210
+
211
+ <div className="adminforge-editor-toolbar-group">
212
+ <MenuButton
213
+ onClick={() => editor.chain().focus().setTextAlign('left').run()}
214
+ isActive={editor.isActive({ textAlign: 'left' })}
215
+ icon="format_align_left"
216
+ title="Align Left"
217
+ />
218
+ <MenuButton
219
+ onClick={() => editor.chain().focus().setTextAlign('center').run()}
220
+ isActive={editor.isActive({ textAlign: 'center' })}
221
+ icon="format_align_center"
222
+ title="Align Center"
223
+ />
224
+ <MenuButton
225
+ onClick={() => editor.chain().focus().setTextAlign('right').run()}
226
+ isActive={editor.isActive({ textAlign: 'right' })}
227
+ icon="format_align_right"
228
+ title="Align Right"
229
+ />
230
+ <MenuButton
231
+ onClick={() => editor.chain().focus().setTextAlign('justify').run()}
232
+ isActive={editor.isActive({ textAlign: 'justify' })}
233
+ icon="format_align_justify"
234
+ title="Justify"
235
+ />
236
+ </div>
237
+
238
+ <div className="adminforge-editor-toolbar-separator" />
239
+
240
+ <div className="adminforge-editor-toolbar-group">
241
+ <MenuButton
242
+ onClick={() => editor.chain().focus().toggleBulletList().run()}
243
+ isActive={editor.isActive("bulletList")}
244
+ icon="format_list_bulleted"
245
+ title="Bullet List"
246
+ />
247
+ <MenuButton
248
+ onClick={() => editor.chain().focus().toggleOrderedList().run()}
249
+ isActive={editor.isActive("orderedList")}
250
+ icon="format_list_numbered"
251
+ title="Ordered List"
252
+ />
253
+ <MenuButton
254
+ onClick={() => editor.chain().focus().toggleTaskList().run()}
255
+ isActive={editor.isActive("taskList")}
256
+ icon="checklist"
257
+ title="Task List"
258
+ />
259
+ </div>
260
+
261
+ <div className="adminforge-editor-toolbar-separator" />
262
+
263
+ <div className="adminforge-editor-toolbar-group">
264
+ <MenuButton
265
+ onClick={() => editor.chain().focus().toggleBlockquote().run()}
266
+ isActive={editor.isActive("blockquote")}
267
+ icon="format_quote"
268
+ title="Blockquote"
269
+ />
270
+ <MenuButton
271
+ onClick={() => editor.chain().focus().toggleCodeBlock().run()}
272
+ isActive={editor.isActive("codeBlock")}
273
+ icon="terminal"
274
+ title="Code Block"
275
+ />
276
+ <MenuButton
277
+ onClick={() => editor.chain().focus().setHorizontalRule().run()}
278
+ icon="horizontal_rule"
279
+ title="Horizontal Rule"
280
+ />
281
+ </div>
282
+ </div>
283
+ );
284
+ }
285
+
286
+ export function RichTextEditor({ name, value = "", onChange }: RichTextEditorProps) {
287
+ const editor = useEditor({
288
+ extensions: [
289
+ StarterKit.configure({
290
+ horizontalRule: false,
291
+ }),
292
+ Link.configure({
293
+ openOnClick: false,
294
+ HTMLAttributes: {
295
+ class: 'adminforge-editor-link',
296
+ },
297
+ }),
298
+ Underline,
299
+ TextAlign.configure({
300
+ types: ['heading', 'paragraph', 'image'],
301
+ }),
302
+ Placeholder.configure({
303
+ placeholder: 'Write something amazing...',
304
+ }),
305
+ Highlight,
306
+ Subscript,
307
+ Superscript,
308
+ TaskList,
309
+ TaskItem.configure({
310
+ nested: true,
311
+ }),
312
+ HorizontalRule,
313
+ Typography,
314
+ BubbleMenuExtension,
315
+ Image.extend({
316
+ addAttributes() {
317
+ return {
318
+ ...this.parent?.(),
319
+ width: {
320
+ default: '100%',
321
+ renderHTML: attributes => {
322
+ return {
323
+ style: `width: ${attributes.width}; height: auto;`,
324
+ }
325
+ }
326
+ },
327
+ textAlign: {
328
+ default: 'left',
329
+ parseHTML: element => element.style.textAlign || element.getAttribute('data-text-align'),
330
+ renderHTML: attributes => {
331
+ return {
332
+ 'data-align': attributes.textAlign,
333
+ }
334
+ }
335
+ }
336
+ }
337
+ }
338
+ }).configure({
339
+ allowBase64: true,
340
+ HTMLAttributes: {
341
+ class: 'adminforge-editor-image',
342
+ },
343
+ }),
344
+ ],
345
+ content: value,
346
+ immediatelyRender: false,
347
+ editorProps: {
348
+ handleDrop: (view: EditorView, event: DragEvent, slice: Slice, moved: boolean) => {
349
+ if (!moved && event.dataTransfer && event.dataTransfer.files && event.dataTransfer.files[0]) {
350
+ const file = event.dataTransfer.files[0];
351
+ if (file.type.startsWith('image/')) {
352
+ const formData = new FormData();
353
+ formData.append('file', file);
354
+ fetch('/api/upload', { method: 'POST', body: formData })
355
+ .then(res => res.json())
356
+ .then(data => {
357
+ if (data.url) {
358
+ const { schema } = view.state;
359
+ const coordinates = view.posAtCoords({ left: event.clientX, top: event.clientY });
360
+ const node = schema.nodes.image.create({ src: data.url });
361
+ const transaction = view.state.tr.insert(coordinates?.pos ?? 0, node);
362
+ view.dispatch(transaction);
363
+ }
364
+ })
365
+ .catch(() => alert('Upload failed'));
366
+ return true;
367
+ }
368
+ }
369
+ return false;
370
+ },
371
+ handlePaste: (view: EditorView, event: ClipboardEvent) => {
372
+ if (event.clipboardData && event.clipboardData.files && event.clipboardData.files[0]) {
373
+ const file = event.clipboardData.files[0];
374
+ if (file.type.startsWith('image/')) {
375
+ const formData = new FormData();
376
+ formData.append('file', file);
377
+ fetch('/api/upload', { method: 'POST', body: formData })
378
+ .then(res => res.json())
379
+ .then(data => {
380
+ if (data.url) {
381
+ const { schema } = view.state;
382
+ const node = schema.nodes.image.create({ src: data.url });
383
+ const transaction = view.state.tr.replaceSelectionWith(node);
384
+ view.dispatch(transaction);
385
+ }
386
+ })
387
+ .catch(() => alert('Upload failed'));
388
+ return true;
389
+ }
390
+ }
391
+ return false;
392
+ },
393
+ },
394
+ onUpdate: ({ editor }: { editor: Editor }) => {
395
+ onChange(editor.getHTML());
396
+ },
397
+ });
398
+
399
+ useEffect(() => {
400
+ if (editor && value !== editor.getHTML()) {
401
+ editor.commands.setContent(value);
402
+ }
403
+ }, [value, editor]);
404
+
405
+ if (!editor) {
406
+ return null;
407
+ }
408
+
409
+ return (
410
+ <div className="adminforge-editor-container">
411
+ <Toolbar editor={editor} />
412
+
413
+ {editor && (
414
+ <>
415
+ {editor && (
416
+ <BubbleMenu
417
+ editor={editor}
418
+ shouldShow={({ editor, state }) => {
419
+ // Show if image is active or if we have a node selection on an image
420
+ const isImage = editor.isActive('image') || (state.selection.content().content.firstChild?.type.name === 'image');
421
+ return isImage || !state.selection.empty;
422
+ }}
423
+ options={{ duration: 100, zIndex: 9999 } as any}
424
+ >
425
+ <div className="adminforge-editor-bubble-menu" style={{ zIndex: 10000 }}>
426
+ {editor.isActive('image') ? (
427
+ <>
428
+ <button
429
+ type="button"
430
+ onClick={() => editor.chain().focus().setTextAlign('left').run()}
431
+ className={editor.isActive({ textAlign: 'left' }) ? 'active' : ''}
432
+ >
433
+ <span className="material-symbols-outlined">format_align_left</span>
434
+ </button>
435
+ <button
436
+ type="button"
437
+ onClick={() => editor.chain().focus().setTextAlign('center').run()}
438
+ className={editor.isActive({ textAlign: 'center' }) ? 'active' : ''}
439
+ >
440
+ <span className="material-symbols-outlined">format_align_center</span>
441
+ </button>
442
+ <button
443
+ type="button"
444
+ onClick={() => editor.chain().focus().setTextAlign('right').run()}
445
+ className={editor.isActive({ textAlign: 'right' }) ? 'active' : ''}
446
+ >
447
+ <span className="material-symbols-outlined">format_align_right</span>
448
+ </button>
449
+ <div style={{ width: '1px', background: 'rgba(255,255,255,0.2)', margin: '4px 2px' }} />
450
+ <button
451
+ type="button"
452
+ onClick={() => editor.chain().focus().updateAttributes('image', { width: 'calc(25% - 1rem)' }).run()}
453
+ style={{ fontSize: '11px', fontWeight: 'bold' }}
454
+ >
455
+ 25%
456
+ </button>
457
+ <button
458
+ type="button"
459
+ onClick={() => editor.chain().focus().updateAttributes('image', { width: 'calc(50% - 1rem)' }).run()}
460
+ style={{ fontSize: '11px', fontWeight: 'bold' }}
461
+ >
462
+ 50%
463
+ </button>
464
+ <button
465
+ type="button"
466
+ onClick={() => editor.chain().focus().updateAttributes('image', { width: '100%' }).run()}
467
+ style={{ fontSize: '11px', fontWeight: 'bold' }}
468
+ >
469
+ 100%
470
+ </button>
471
+ <div style={{ width: '1px', background: 'rgba(255,255,255,0.2)', margin: '4px 2px' }} />
472
+ <button
473
+ type="button"
474
+ onClick={() => editor.chain().focus().deleteSelection().run()}
475
+ style={{ color: '#f87171' }}
476
+ >
477
+ <span className="material-symbols-outlined">delete</span>
478
+ </button>
479
+ </>
480
+ ) : (
481
+ <>
482
+ <button
483
+ type="button"
484
+ onClick={() => editor.chain().focus().toggleBold().run()}
485
+ className={editor.isActive('bold') ? 'active' : ''}
486
+ >
487
+ <span className="material-symbols-outlined">format_bold</span>
488
+ </button>
489
+ <button
490
+ type="button"
491
+ onClick={() => editor.chain().focus().toggleItalic().run()}
492
+ className={editor.isActive('italic') ? 'active' : ''}
493
+ >
494
+ <span className="material-symbols-outlined">format_italic</span>
495
+ </button>
496
+ <button
497
+ type="button"
498
+ onClick={() => editor.chain().focus().toggleStrike().run()}
499
+ className={editor.isActive('strike') ? 'active' : ''}
500
+ >
501
+ <span className="material-symbols-outlined">format_strikethrough</span>
502
+ </button>
503
+ <button
504
+ type="button"
505
+ onClick={() => editor.chain().focus().toggleHighlight().run()}
506
+ className={editor.isActive('highlight') ? 'active' : ''}
507
+ >
508
+ <span className="material-symbols-outlined">format_ink_highlighter</span>
509
+ </button>
510
+ </>
511
+ )}
512
+ </div>
513
+ </BubbleMenu>
514
+ )}
515
+ </>
516
+ )}
517
+
518
+ <div className="adminforge-editor-content">
519
+ <EditorContent editor={editor} />
520
+ </div>
521
+
522
+ <style dangerouslySetInnerHTML={{ __html: `
523
+ .adminforge-editor-container {
524
+ border: 1px solid #e5e7eb;
525
+ border-radius: 8px;
526
+ overflow: hidden;
527
+ background: white;
528
+ display: flex;
529
+ flex-direction: column;
530
+ min-height: 400px;
531
+ }
532
+
533
+ .adminforge-editor-toolbar {
534
+ display: flex;
535
+ flex-wrap: wrap;
536
+ padding: 8px;
537
+ background: #f9fafb;
538
+ border-bottom: 1px solid #e5e7eb;
539
+ gap: 4px;
540
+ }
541
+
542
+ .adminforge-editor-toolbar-group {
543
+ display: flex;
544
+ gap: 2px;
545
+ }
546
+
547
+ .adminforge-editor-toolbar-separator {
548
+ width: 1px;
549
+ background: #e5e7eb;
550
+ margin: 4px 2px;
551
+ }
552
+
553
+ .adminforge-editor-btn {
554
+ width: 32px;
555
+ height: 32px;
556
+ display: flex;
557
+ align-items: center;
558
+ justify-content: center;
559
+ border-radius: 4px;
560
+ border: none;
561
+ background: transparent;
562
+ color: #4b5563;
563
+ cursor: pointer;
564
+ transition: all 0.2s;
565
+ }
566
+
567
+ .adminforge-editor-btn:hover {
568
+ background: #f3f4f6;
569
+ color: #111827;
570
+ }
571
+
572
+ .adminforge-editor-btn:disabled {
573
+ opacity: 0.5;
574
+ cursor: not-allowed;
575
+ }
576
+
577
+ .adminforge-editor-btn.active {
578
+ background: #e5e7eb;
579
+ color: #2563eb;
580
+ }
581
+
582
+ .adminforge-editor-content {
583
+ padding: 16px;
584
+ flex: 1;
585
+ overflow-y: auto;
586
+ }
587
+
588
+ .ProseMirror {
589
+ min-height: 300px;
590
+ outline: none;
591
+ }
592
+
593
+ .ProseMirror p.is-editor-empty:first-child::before {
594
+ content: attr(data-placeholder);
595
+ float: left;
596
+ color: #9ca3af;
597
+ pointer-events: none;
598
+ height: 0;
599
+ }
600
+
601
+ .adminforge-editor-link {
602
+ color: #2563eb;
603
+ text-decoration: underline;
604
+ cursor: pointer;
605
+ }
606
+
607
+ .adminforge-editor-bubble-menu {
608
+ display: flex;
609
+ background: #1f2937;
610
+ padding: 4px;
611
+ border-radius: 6px;
612
+ box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
613
+ gap: 2px;
614
+ }
615
+
616
+ .adminforge-editor-bubble-menu button {
617
+ width: 28px;
618
+ height: 28px;
619
+ display: flex;
620
+ align-items: center;
621
+ justify-content: center;
622
+ border: none;
623
+ background: transparent;
624
+ color: white;
625
+ border-radius: 4px;
626
+ cursor: pointer;
627
+ }
628
+
629
+ .adminforge-editor-bubble-menu button:hover {
630
+ background: #374151;
631
+ }
632
+
633
+ .adminforge-editor-bubble-menu button.active {
634
+ color: #60a5fa;
635
+ background: #374151;
636
+ }
637
+
638
+ .adminforge-editor-bubble-menu .material-symbols-outlined {
639
+ font-size: 16px;
640
+ }
641
+
642
+ /* Task list styles */
643
+ ul[data-type="taskList"] {
644
+ list-style: none;
645
+ padding: 0;
646
+ }
647
+
648
+ ul[data-type="taskList"] li {
649
+ display: flex;
650
+ align-items: flex-start;
651
+ margin-bottom: 0.5rem;
652
+ }
653
+
654
+ ul[data-type="taskList"] li > label {
655
+ margin-right: 0.5rem;
656
+ user-select: none;
657
+ }
658
+
659
+ ul[data-type="taskList"] li > div {
660
+ flex: 1;
661
+ }
662
+
663
+ .adminforge-editor-image {
664
+ max-width: 100% !important;
665
+ height: auto !important;
666
+ border-radius: 8px;
667
+ margin: 0.5rem;
668
+ display: inline-block;
669
+ vertical-align: middle;
670
+ transition: all 0.2s;
671
+ }
672
+
673
+ /* Float logic for text wrapping and alignment */
674
+ .adminforge-editor-image[data-align="left"] {
675
+ float: left;
676
+ margin-left: 0;
677
+ margin-right: 1.5rem;
678
+ display: block; /* block + float is standard */
679
+ }
680
+
681
+ .adminforge-editor-image[data-align="right"] {
682
+ float: right;
683
+ margin-right: 0;
684
+ margin-left: 1.5rem;
685
+ display: block;
686
+ }
687
+
688
+ .adminforge-editor-image[data-align="center"] {
689
+ display: block;
690
+ margin-left: auto;
691
+ margin-right: auto;
692
+ float: none;
693
+ }
694
+
695
+ /* Clearfix for the parent paragraph to contain floats */
696
+ .ProseMirror p::after {
697
+ content: "";
698
+ display: table;
699
+ clear: both;
700
+ }
701
+
702
+ .adminforge-editor-image.ProseMirror-selectednode {
703
+ outline: 3px solid #2563eb;
704
+ }
705
+ ` }} />
706
+ </div>
707
+ );
708
+ }