@rimelight/cms 0.0.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 (87) hide show
  1. package/README.md +19 -0
  2. package/package.json +69 -0
  3. package/src/astro/BlockRenderer.astro +111 -0
  4. package/src/astro/PageRenderer.astro +23 -0
  5. package/src/astro/blocks/CalloutBlock.astro +78 -0
  6. package/src/astro/blocks/CardBlock.astro +43 -0
  7. package/src/astro/blocks/CardsBlock.astro +24 -0
  8. package/src/astro/blocks/CodeBlock.astro +16 -0
  9. package/src/astro/blocks/DialogueBlock.astro +23 -0
  10. package/src/astro/blocks/FileTreeBlock.astro +45 -0
  11. package/src/astro/blocks/ImageBlock.astro +17 -0
  12. package/src/astro/blocks/OrderedListBlock.astro +55 -0
  13. package/src/astro/blocks/ParagraphBlock.astro +65 -0
  14. package/src/astro/blocks/SceneBlock.astro +65 -0
  15. package/src/astro/blocks/ScriptBlock.astro +59 -0
  16. package/src/astro/blocks/SectionBlock.astro +46 -0
  17. package/src/astro/blocks/StepItemBlock.astro +25 -0
  18. package/src/astro/blocks/StepsBlock.astro +14 -0
  19. package/src/astro/blocks/TabItemBlock.astro +19 -0
  20. package/src/astro/blocks/TableBlock.astro +49 -0
  21. package/src/astro/blocks/TabsBlock.astro +67 -0
  22. package/src/astro/blocks/UnorderedListBlock.astro +55 -0
  23. package/src/auth/editor-guards.ts +148 -0
  24. package/src/auth/filter-blocks.ts +44 -0
  25. package/src/auth/filter-templates.ts +81 -0
  26. package/src/auth/permissions.ts +40 -0
  27. package/src/cache/purge.ts +37 -0
  28. package/src/client/components/RimelightBlock.tsx +487 -0
  29. package/src/client/components/RimelightBlockPicker.tsx +305 -0
  30. package/src/client/components/RimelightEditor.tsx +131 -0
  31. package/src/client/components/RimelightEditorLayout.tsx +143 -0
  32. package/src/client/components/RimelightPreview.tsx +354 -0
  33. package/src/client/components/RimelightPropertiesPanel.tsx +408 -0
  34. package/src/client/components/RimelightTemplateEditor.tsx +245 -0
  35. package/src/client/components/editors/CalloutEditor.tsx +185 -0
  36. package/src/client/components/editors/CardEditor.tsx +60 -0
  37. package/src/client/components/editors/CardsEditor.tsx +132 -0
  38. package/src/client/components/editors/CodeEditor.tsx +101 -0
  39. package/src/client/components/editors/DialogueEditor.tsx +52 -0
  40. package/src/client/components/editors/FileTreeEditor.tsx +65 -0
  41. package/src/client/components/editors/ImageEditor.tsx +70 -0
  42. package/src/client/components/editors/ParagraphEditor.tsx +314 -0
  43. package/src/client/components/editors/SceneEditor.tsx +245 -0
  44. package/src/client/components/editors/ScriptEditor.tsx +245 -0
  45. package/src/client/components/editors/SectionEditor.tsx +139 -0
  46. package/src/client/components/editors/StepItemEditor.tsx +117 -0
  47. package/src/client/components/editors/StepsEditor.tsx +105 -0
  48. package/src/client/components/editors/TabItemEditor.tsx +113 -0
  49. package/src/client/components/editors/TableEditor.tsx +181 -0
  50. package/src/client/components/editors/TabsEditor.tsx +105 -0
  51. package/src/client/components/index.ts +7 -0
  52. package/src/client/loader.ts +19 -0
  53. package/src/client/state/autosave.ts +122 -0
  54. package/src/client/state/editor-store.ts +411 -0
  55. package/src/client/state/history.ts +251 -0
  56. package/src/client/state/lock-manager.ts +89 -0
  57. package/src/client/styles/cms-editor.css +653 -0
  58. package/src/client/toast.ts +12 -0
  59. package/src/client/utils/sortable.ts +184 -0
  60. package/src/client/utils/splitpane.ts +87 -0
  61. package/src/client/utils/tree-sanitizer.ts +33 -0
  62. package/src/core/page-definitions.ts +502 -0
  63. package/src/core/registry.ts +99 -0
  64. package/src/core/template-sync.ts +75 -0
  65. package/src/core/types/blocks.ts +367 -0
  66. package/src/core/types/inline.ts +23 -0
  67. package/src/core/types/pages.ts +36 -0
  68. package/src/core/types/templates.ts +32 -0
  69. package/src/core/types/versioning.ts +44 -0
  70. package/src/core/validator.ts +28 -0
  71. package/src/env.d.ts +4 -0
  72. package/src/index.ts +47 -0
  73. package/src/markdown/index.ts +2 -0
  74. package/src/markdown/parser.ts +347 -0
  75. package/src/markdown/serializer.ts +219 -0
  76. package/src/migration.ts +155 -0
  77. package/src/schema/index.ts +8 -0
  78. package/src/schema/page_draft_locks.ts +13 -0
  79. package/src/schema/page_drafts.ts +18 -0
  80. package/src/schema/page_templates.ts +27 -0
  81. package/src/schema/page_version_approvals.ts +12 -0
  82. package/src/schema/page_version_comments.ts +14 -0
  83. package/src/schema/page_versions.ts +34 -0
  84. package/src/schema/pages.ts +42 -0
  85. package/src/schema/search.ts +23 -0
  86. package/src/search/query.ts +10 -0
  87. package/src/search/sync.ts +32 -0
@@ -0,0 +1,37 @@
1
+ export interface CachePurgeOptions {
2
+ slug: string
3
+ locales: string[]
4
+ domain?: string
5
+ basePath?: string
6
+ isEnterpriseCloudflare?: boolean
7
+ }
8
+
9
+ export async function invalidatePageCache(options: CachePurgeOptions): Promise<boolean> {
10
+ const { slug, locales, domain = "rimelight.com", basePath = "", isEnterpriseCloudflare } = options
11
+ const zoneId = process.env.CLOUDFLARE_ZONE_ID
12
+ const apiToken = process.env.CLOUDFLARE_API_TOKEN
13
+
14
+ if (!zoneId || !apiToken) {
15
+ return false
16
+ }
17
+
18
+ const cleanPath = basePath ? `/${basePath.replace(/^\/+|\/+$/g, "")}` : ""
19
+
20
+ const body = isEnterpriseCloudflare
21
+ ? { tags: [`page-${domain}-${slug}`] }
22
+ : { files: locales.map((lang) => `https://${domain}/${lang}${cleanPath}/${slug}`) }
23
+
24
+ try {
25
+ const res = await fetch(`https://api.cloudflare.com/client/v4/zones/${zoneId}/purge_cache`, {
26
+ method: "POST",
27
+ headers: {
28
+ "Authorization": `Bearer ${apiToken}`,
29
+ "Content-Type": "application/json"
30
+ },
31
+ body: JSON.stringify(body)
32
+ })
33
+ return res.ok
34
+ } catch {
35
+ return false
36
+ }
37
+ }
@@ -0,0 +1,487 @@
1
+ import { createSignal, onMount, onCleanup, Show, type Component } from "solid-js"
2
+ import type { BaseBlock } from "../../core/types/blocks"
3
+ import { ParagraphEditor } from "./editors/ParagraphEditor"
4
+ import { SectionEditor } from "./editors/SectionEditor"
5
+ import { CalloutEditor } from "./editors/CalloutEditor"
6
+ import { ImageEditor } from "./editors/ImageEditor"
7
+ import { CodeEditor } from "./editors/CodeEditor"
8
+ import { ScriptEditor } from "./editors/ScriptEditor"
9
+ import { SceneEditor } from "./editors/SceneEditor"
10
+ import { DialogueEditor } from "./editors/DialogueEditor"
11
+ import { TableEditor } from "./editors/TableEditor"
12
+ import { TabsEditor } from "./editors/TabsEditor"
13
+ import { TabItemEditor } from "./editors/TabItemEditor"
14
+ import { StepsEditor } from "./editors/StepsEditor"
15
+ import { StepItemEditor } from "./editors/StepItemEditor"
16
+ import { CardsEditor } from "./editors/CardsEditor"
17
+ import { CardEditor } from "./editors/CardEditor"
18
+ import { FileTreeEditor } from "./editors/FileTreeEditor"
19
+ import RLSButton from "@rimelight/ui/components/button/RLSButton.tsx"
20
+ import RLSIcon from "@rimelight/ui/components/icon/RLSIcon.tsx"
21
+ import RLSModal from "@rimelight/ui/components/modal/RLSModal.tsx"
22
+
23
+ export interface RimelightBlockProps {
24
+ block: BaseBlock
25
+ sectionDepth?: number | undefined
26
+ onPropsChange: (id: string, props: Record<string, any>) => void
27
+ onMove: (id: string, direction: -1 | 1) => void
28
+ onDelete: (id: string) => void
29
+ onDuplicate: (id: string) => void
30
+ onAddAdjacent: (targetId: string, position: "before" | "after") => void
31
+ onRelocate?: ((id: string, newIndex: number, targetParentId?: string | null) => void) | undefined
32
+ onOpenPicker?:
33
+ | ((
34
+ targetId: string | null,
35
+ position: "before" | "after",
36
+ sectionDepth: number,
37
+ parentId?: string | null,
38
+ containerType?: string | null
39
+ ) => void)
40
+ | undefined
41
+ }
42
+
43
+ export const RimelightBlock: Component<RimelightBlockProps> = (props) => {
44
+ const [menuOpen, setMenuOpen] = createSignal(false)
45
+ const [toolbarMenuOpen, setToolbarMenuOpen] = createSignal(false)
46
+ const [isDeleteModalOpen, setIsDeleteModalOpen] = createSignal(false)
47
+ let menuWrapEl: HTMLDivElement | undefined = undefined
48
+ let toolbarMenuWrapEl: HTMLDivElement | undefined = undefined
49
+
50
+ const isLocked = () => !!props.block.isTemplated || props.block.templatedProps === true
51
+
52
+ onMount(() => {
53
+ const handleOutside = (e: MouseEvent) => {
54
+ const target = e.target instanceof Node ? e.target : null
55
+ if (menuOpen() && menuWrapEl && target && !menuWrapEl.contains(target)) {
56
+ setMenuOpen(false)
57
+ }
58
+ if (toolbarMenuOpen() && toolbarMenuWrapEl && target && !toolbarMenuWrapEl.contains(target)) {
59
+ setToolbarMenuOpen(false)
60
+ }
61
+ }
62
+ document.addEventListener("click", handleOutside)
63
+ onCleanup(() => document.removeEventListener("click", handleOutside))
64
+ })
65
+
66
+ const renderEditorComponent = (blockItem: BaseBlock, depth: number) => {
67
+ const handleProps = (newProps: Record<string, any>) => {
68
+ props.onPropsChange(blockItem.id, newProps)
69
+ }
70
+
71
+ const renderChildBlock = (childBlock: BaseBlock, childDepth: number) => (
72
+ <RimelightBlock
73
+ block={childBlock}
74
+ sectionDepth={childDepth}
75
+ onPropsChange={props.onPropsChange}
76
+ onMove={props.onMove}
77
+ onDelete={props.onDelete}
78
+ onDuplicate={props.onDuplicate}
79
+ onAddAdjacent={props.onAddAdjacent}
80
+ onRelocate={props.onRelocate}
81
+ onOpenPicker={props.onOpenPicker}
82
+ />
83
+ )
84
+
85
+ const childrenList = () => {
86
+ if (Array.isArray(blockItem.children)) return blockItem.children
87
+ if (Array.isArray(blockItem.props?.children)) return blockItem.props.children
88
+ return []
89
+ }
90
+
91
+ switch (blockItem.type) {
92
+ case "ParagraphBlock":
93
+ return <ParagraphEditor props={blockItem.props} onPropsChange={handleProps} />
94
+ case "SectionBlock":
95
+ return (
96
+ <SectionEditor
97
+ blockId={blockItem.id}
98
+ props={blockItem.props}
99
+ children={childrenList}
100
+ sectionDepth={depth}
101
+ onPropsChange={handleProps}
102
+ onRelocate={props.onRelocate}
103
+ onOpenPicker={() =>
104
+ props.onOpenPicker?.(null, "after", depth + 1, blockItem.id, "SectionBlock")
105
+ }
106
+ renderChildBlock={renderChildBlock}
107
+ />
108
+ )
109
+ case "CalloutBlock":
110
+ return (
111
+ <CalloutEditor
112
+ blockId={blockItem.id}
113
+ props={blockItem.props}
114
+ children={childrenList}
115
+ sectionDepth={depth}
116
+ onPropsChange={handleProps}
117
+ onRelocate={props.onRelocate}
118
+ onOpenPicker={() =>
119
+ props.onOpenPicker?.(null, "after", depth, blockItem.id, "CalloutBlock")
120
+ }
121
+ renderChildBlock={renderChildBlock}
122
+ />
123
+ )
124
+ case "ImageBlock":
125
+ return <ImageEditor props={blockItem.props} onPropsChange={handleProps} />
126
+ case "CodeBlock":
127
+ return <CodeEditor props={blockItem.props} onPropsChange={handleProps} />
128
+ case "TableBlock":
129
+ return <TableEditor props={blockItem.props} onPropsChange={handleProps} />
130
+ case "TabsBlock":
131
+ return (
132
+ <TabsEditor
133
+ blockId={blockItem.id}
134
+ props={blockItem.props}
135
+ children={childrenList}
136
+ sectionDepth={depth}
137
+ onPropsChange={handleProps}
138
+ onRelocate={props.onRelocate}
139
+ onOpenPicker={() =>
140
+ props.onOpenPicker?.(null, "after", depth, blockItem.id, "TabsBlock")
141
+ }
142
+ renderChildBlock={renderChildBlock}
143
+ />
144
+ )
145
+ case "TabItemBlock":
146
+ return (
147
+ <TabItemEditor
148
+ blockId={blockItem.id}
149
+ props={blockItem.props}
150
+ children={childrenList}
151
+ sectionDepth={depth}
152
+ onPropsChange={handleProps}
153
+ onRelocate={props.onRelocate}
154
+ onOpenPicker={() =>
155
+ props.onOpenPicker?.(null, "after", depth, blockItem.id, "TabItemBlock")
156
+ }
157
+ renderChildBlock={renderChildBlock}
158
+ />
159
+ )
160
+ case "StepsBlock":
161
+ return (
162
+ <StepsEditor
163
+ blockId={blockItem.id}
164
+ props={blockItem.props}
165
+ children={childrenList}
166
+ sectionDepth={depth}
167
+ onPropsChange={handleProps}
168
+ onRelocate={props.onRelocate}
169
+ onOpenPicker={() =>
170
+ props.onOpenPicker?.(null, "after", depth, blockItem.id, "StepsBlock")
171
+ }
172
+ renderChildBlock={renderChildBlock}
173
+ />
174
+ )
175
+ case "StepItemBlock":
176
+ return (
177
+ <StepItemEditor
178
+ blockId={blockItem.id}
179
+ props={blockItem.props}
180
+ children={childrenList}
181
+ sectionDepth={depth}
182
+ onPropsChange={handleProps}
183
+ onRelocate={props.onRelocate}
184
+ onOpenPicker={() =>
185
+ props.onOpenPicker?.(null, "after", depth, blockItem.id, "StepItemBlock")
186
+ }
187
+ renderChildBlock={renderChildBlock}
188
+ />
189
+ )
190
+ case "CardsBlock":
191
+ return (
192
+ <CardsEditor
193
+ blockId={blockItem.id}
194
+ props={blockItem.props}
195
+ children={childrenList}
196
+ sectionDepth={depth}
197
+ onPropsChange={handleProps}
198
+ onRelocate={props.onRelocate}
199
+ onOpenPicker={() =>
200
+ props.onOpenPicker?.(null, "after", depth, blockItem.id, "CardsBlock")
201
+ }
202
+ renderChildBlock={renderChildBlock}
203
+ />
204
+ )
205
+ case "CardBlock":
206
+ return <CardEditor props={blockItem.props} onPropsChange={handleProps} />
207
+ case "FileTreeBlock":
208
+ return <FileTreeEditor props={blockItem.props} onPropsChange={handleProps} />
209
+ case "ScriptBlock":
210
+ return (
211
+ <ScriptEditor
212
+ blockId={blockItem.id}
213
+ props={blockItem.props}
214
+ children={childrenList}
215
+ sectionDepth={depth}
216
+ onPropsChange={handleProps}
217
+ onRelocate={props.onRelocate}
218
+ onOpenPicker={() =>
219
+ props.onOpenPicker?.(null, "after", depth, blockItem.id, "ScriptBlock")
220
+ }
221
+ renderChildBlock={renderChildBlock}
222
+ />
223
+ )
224
+ case "SceneBlock":
225
+ return (
226
+ <SceneEditor
227
+ blockId={blockItem.id}
228
+ props={blockItem.props}
229
+ children={childrenList}
230
+ sectionDepth={depth}
231
+ onPropsChange={handleProps}
232
+ onRelocate={props.onRelocate}
233
+ onOpenPicker={() =>
234
+ props.onOpenPicker?.(null, "after", depth, blockItem.id, "SceneBlock")
235
+ }
236
+ renderChildBlock={renderChildBlock}
237
+ />
238
+ )
239
+ case "DialogueBlock":
240
+ return <DialogueEditor props={blockItem.props} onPropsChange={handleProps} />
241
+ default:
242
+ return <ParagraphEditor props={blockItem.props} onPropsChange={handleProps} />
243
+ }
244
+ }
245
+
246
+ return (
247
+ <div
248
+ data-id={props.block.id}
249
+ data-block-id={props.block.id}
250
+ data-block-type={props.block.type}
251
+ class="cms-block-wrap group/block relative w-full my-1 flex items-start"
252
+ >
253
+ {/* Left Grip Handle & Context Menu */}
254
+ <div
255
+ ref={(el) => (menuWrapEl = el)}
256
+ class="cms-block-grip absolute -left-7 top-1 z-30 flex items-center justify-center opacity-0 group-hover/block:opacity-100 transition-opacity"
257
+ >
258
+ <RLSButton
259
+ variant="ghost"
260
+ color="neutral"
261
+ size="xs"
262
+ square={true}
263
+ onClick={(e: MouseEvent) => {
264
+ e.stopPropagation()
265
+ setMenuOpen(!menuOpen())
266
+ }}
267
+ aria-label="Drag handle and options menu"
268
+ class="cms-drag-handle !p-1 text-muted-foreground hover:text-foreground cursor-grab active:cursor-grabbing"
269
+ title="Drag to reorder / Click for options"
270
+ leading={<RLSIcon name="gripVertical" size="sm" />}
271
+ />
272
+
273
+ {/* Dropdown Options Menu */}
274
+ <Show when={menuOpen()}>
275
+ <div class="absolute left-0 top-7 z-50 w-48 bg-elevated border border-default rounded-xl shadow-xl py-1 text-xs">
276
+ {isLocked() ? (
277
+ <div class="px-3 py-2 text-muted-foreground italic">Locked — templated block</div>
278
+ ) : (
279
+ <>
280
+ <button
281
+ type="button"
282
+ onClick={() => {
283
+ props.onMove(props.block.id, -1)
284
+ setMenuOpen(false)
285
+ }}
286
+ class="flex items-center gap-2 w-full px-3 py-2 hover:bg-neutral-100 dark:hover:bg-neutral-800 cursor-pointer text-foreground transition-colors bg-transparent border-0 text-left"
287
+ >
288
+ <RLSIcon name="arrowUp" size="xs" class="text-muted-foreground" />
289
+ <span>Move Up</span>
290
+ </button>
291
+ <button
292
+ type="button"
293
+ onClick={() => {
294
+ props.onMove(props.block.id, 1)
295
+ setMenuOpen(false)
296
+ }}
297
+ class="flex items-center gap-2 w-full px-3 py-2 hover:bg-neutral-100 dark:hover:bg-neutral-800 cursor-pointer text-foreground transition-colors bg-transparent border-0 text-left"
298
+ >
299
+ <RLSIcon name="arrowDown" size="xs" class="text-muted-foreground" />
300
+ <span>Move Down</span>
301
+ </button>
302
+ <div class="my-1 border-t border-default" />
303
+ <button
304
+ type="button"
305
+ onClick={() => {
306
+ props.onAddAdjacent(props.block.id, "before")
307
+ setMenuOpen(false)
308
+ }}
309
+ class="flex items-center gap-2 w-full px-3 py-2 hover:bg-neutral-100 dark:hover:bg-neutral-800 cursor-pointer text-foreground transition-colors bg-transparent border-0 text-left"
310
+ >
311
+ <RLSIcon name="arrowUpFromLine" size="xs" class="text-muted-foreground" />
312
+ <span>Add Block Above</span>
313
+ </button>
314
+ <button
315
+ type="button"
316
+ onClick={() => {
317
+ props.onAddAdjacent(props.block.id, "after")
318
+ setMenuOpen(false)
319
+ }}
320
+ class="flex items-center gap-2 w-full px-3 py-2 hover:bg-neutral-100 dark:hover:bg-neutral-800 cursor-pointer text-foreground transition-colors bg-transparent border-0 text-left"
321
+ >
322
+ <RLSIcon name="arrowDownToLine" size="xs" class="text-muted-foreground" />
323
+ <span>Add Block Below</span>
324
+ </button>
325
+ <div class="my-1 border-t border-default" />
326
+ <button
327
+ type="button"
328
+ onClick={() => {
329
+ props.onDuplicate(props.block.id)
330
+ setMenuOpen(false)
331
+ }}
332
+ class="flex items-center gap-2 w-full px-3 py-2 hover:bg-neutral-100 dark:hover:bg-neutral-800 cursor-pointer text-foreground transition-colors bg-transparent border-0 text-left"
333
+ >
334
+ <RLSIcon name="copy" size="xs" class="text-muted-foreground" />
335
+ <span>Duplicate Block</span>
336
+ </button>
337
+ <button
338
+ type="button"
339
+ onClick={() => {
340
+ setMenuOpen(false)
341
+ setIsDeleteModalOpen(true)
342
+ }}
343
+ class="flex items-center gap-2 w-full px-3 py-2 hover:bg-red-50 dark:hover:bg-red-950/40 text-red-500 hover:text-red-600 cursor-pointer transition-colors bg-transparent border-0 text-left"
344
+ >
345
+ <RLSIcon name="trash" size="xs" class="text-red-500" />
346
+ <span>Delete Block...</span>
347
+ </button>
348
+ </>
349
+ )}
350
+ </div>
351
+ </Show>
352
+ </div>
353
+
354
+ <div class="flex-1 min-w-0 relative">
355
+ {/* Top Action Floating Toolbar (Shows on hover, inside block at top-right) */}
356
+ <div class="absolute top-0 right-0 z-20 opacity-0 group-hover/block:opacity-100 transition-opacity flex items-center gap-0.5 bg-elevated border border-default rounded-lg p-0.5 shadow-md">
357
+ <RLSButton
358
+ variant="ghost"
359
+ color="neutral"
360
+ size="xs"
361
+ square={true}
362
+ onClick={() => props.onAddAdjacent(props.block.id, "before")}
363
+ aria-label="Add block above"
364
+ title="Add block above"
365
+ leading={<RLSIcon name="arrowUpFromLine" size="xs" />}
366
+ />
367
+ <RLSButton
368
+ variant="ghost"
369
+ color="neutral"
370
+ size="xs"
371
+ square={true}
372
+ onClick={() => props.onAddAdjacent(props.block.id, "after")}
373
+ aria-label="Add block below"
374
+ title="Add block below"
375
+ leading={<RLSIcon name="arrowDownToLine" size="xs" />}
376
+ />
377
+ <RLSButton
378
+ variant="ghost"
379
+ color="neutral"
380
+ size="xs"
381
+ square={true}
382
+ onClick={() => props.onMove(props.block.id, -1)}
383
+ aria-label="Move up"
384
+ title="Move up"
385
+ leading={<RLSIcon name="arrowUp" size="xs" />}
386
+ />
387
+ <RLSButton
388
+ variant="ghost"
389
+ color="neutral"
390
+ size="xs"
391
+ square={true}
392
+ onClick={() => props.onMove(props.block.id, 1)}
393
+ aria-label="Move down"
394
+ title="Move down"
395
+ leading={<RLSIcon name="arrowDown" size="xs" />}
396
+ />
397
+ <RLSButton
398
+ variant="ghost"
399
+ color="neutral"
400
+ size="xs"
401
+ square={true}
402
+ onClick={() => props.onDuplicate(props.block.id)}
403
+ aria-label="Duplicate"
404
+ title="Duplicate"
405
+ leading={<RLSIcon name="copy" size="xs" />}
406
+ />
407
+
408
+ {/* More Actions Dropdown */}
409
+ <div ref={(el) => (toolbarMenuWrapEl = el)} class="relative inline-flex">
410
+ <RLSButton
411
+ variant="ghost"
412
+ color="neutral"
413
+ size="xs"
414
+ square={true}
415
+ onClick={(e: MouseEvent) => {
416
+ e.stopPropagation()
417
+ setToolbarMenuOpen(!toolbarMenuOpen())
418
+ }}
419
+ aria-label="More actions"
420
+ title="More actions"
421
+ leading={<RLSIcon name="ellipsisVertical" size="xs" />}
422
+ />
423
+ <Show when={toolbarMenuOpen()}>
424
+ <div class="absolute right-0 top-7 z-50 w-36 bg-elevated border border-default rounded-xl shadow-xl py-1 text-xs">
425
+ <button
426
+ type="button"
427
+ onClick={() => {
428
+ setToolbarMenuOpen(false)
429
+ setIsDeleteModalOpen(true)
430
+ }}
431
+ class="flex items-center gap-2 w-full px-3 py-2 hover:bg-red-50 dark:hover:bg-red-950/40 text-red-500 hover:text-red-600 cursor-pointer transition-colors bg-transparent border-0 text-left"
432
+ >
433
+ <RLSIcon name="trash" size="xs" class="text-red-500" />
434
+ <span>Delete Block...</span>
435
+ </button>
436
+ </div>
437
+ </Show>
438
+ </div>
439
+ </div>
440
+
441
+ {/* Block Editor Body (No border, no bg) */}
442
+ <div class="w-full relative">
443
+ {isLocked() && (
444
+ <div class="inline-flex items-center gap-1 px-2 py-0.5 bg-neutral-200/50 dark:bg-neutral-800/50 text-neutral-600 dark:text-neutral-300 rounded text-xs mb-2 select-none border border-default">
445
+ 🔒 Templated Block (Locked)
446
+ </div>
447
+ )}
448
+ <div class="w-full">{renderEditorComponent(props.block, props.sectionDepth || 0)}</div>
449
+ </div>
450
+ </div>
451
+
452
+ {/* Rimelight UI Delete Confirmation Modal */}
453
+ <RLSModal
454
+ open={isDeleteModalOpen()}
455
+ onClose={() => setIsDeleteModalOpen(false)}
456
+ title="Delete Block"
457
+ description="Are you sure you want to delete this block? This action cannot be undone."
458
+ size="sm"
459
+ footerSlot={
460
+ <>
461
+ <RLSButton
462
+ variant="outline"
463
+ color="neutral"
464
+ size="sm"
465
+ onClick={() => setIsDeleteModalOpen(false)}
466
+ label="Cancel"
467
+ />
468
+ <RLSButton
469
+ variant="solid"
470
+ color="error"
471
+ size="sm"
472
+ onClick={() => {
473
+ setIsDeleteModalOpen(false)
474
+ props.onDelete(props.block.id)
475
+ }}
476
+ label="Delete"
477
+ />
478
+ </>
479
+ }
480
+ >
481
+ <p class="text-xs text-muted-foreground mt-1">
482
+ Block type: <span class="font-semibold text-foreground">{props.block.type}</span>
483
+ </p>
484
+ </RLSModal>
485
+ </div>
486
+ )
487
+ }