@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.
- package/README.md +19 -0
- package/package.json +69 -0
- package/src/astro/BlockRenderer.astro +111 -0
- package/src/astro/PageRenderer.astro +23 -0
- package/src/astro/blocks/CalloutBlock.astro +78 -0
- package/src/astro/blocks/CardBlock.astro +43 -0
- package/src/astro/blocks/CardsBlock.astro +24 -0
- package/src/astro/blocks/CodeBlock.astro +16 -0
- package/src/astro/blocks/DialogueBlock.astro +23 -0
- package/src/astro/blocks/FileTreeBlock.astro +45 -0
- package/src/astro/blocks/ImageBlock.astro +17 -0
- package/src/astro/blocks/OrderedListBlock.astro +55 -0
- package/src/astro/blocks/ParagraphBlock.astro +65 -0
- package/src/astro/blocks/SceneBlock.astro +65 -0
- package/src/astro/blocks/ScriptBlock.astro +59 -0
- package/src/astro/blocks/SectionBlock.astro +46 -0
- package/src/astro/blocks/StepItemBlock.astro +25 -0
- package/src/astro/blocks/StepsBlock.astro +14 -0
- package/src/astro/blocks/TabItemBlock.astro +19 -0
- package/src/astro/blocks/TableBlock.astro +49 -0
- package/src/astro/blocks/TabsBlock.astro +67 -0
- package/src/astro/blocks/UnorderedListBlock.astro +55 -0
- package/src/auth/editor-guards.ts +148 -0
- package/src/auth/filter-blocks.ts +44 -0
- package/src/auth/filter-templates.ts +81 -0
- package/src/auth/permissions.ts +40 -0
- package/src/cache/purge.ts +37 -0
- package/src/client/components/RimelightBlock.tsx +487 -0
- package/src/client/components/RimelightBlockPicker.tsx +305 -0
- package/src/client/components/RimelightEditor.tsx +131 -0
- package/src/client/components/RimelightEditorLayout.tsx +143 -0
- package/src/client/components/RimelightPreview.tsx +354 -0
- package/src/client/components/RimelightPropertiesPanel.tsx +408 -0
- package/src/client/components/RimelightTemplateEditor.tsx +245 -0
- package/src/client/components/editors/CalloutEditor.tsx +185 -0
- package/src/client/components/editors/CardEditor.tsx +60 -0
- package/src/client/components/editors/CardsEditor.tsx +132 -0
- package/src/client/components/editors/CodeEditor.tsx +101 -0
- package/src/client/components/editors/DialogueEditor.tsx +52 -0
- package/src/client/components/editors/FileTreeEditor.tsx +65 -0
- package/src/client/components/editors/ImageEditor.tsx +70 -0
- package/src/client/components/editors/ParagraphEditor.tsx +314 -0
- package/src/client/components/editors/SceneEditor.tsx +245 -0
- package/src/client/components/editors/ScriptEditor.tsx +245 -0
- package/src/client/components/editors/SectionEditor.tsx +139 -0
- package/src/client/components/editors/StepItemEditor.tsx +117 -0
- package/src/client/components/editors/StepsEditor.tsx +105 -0
- package/src/client/components/editors/TabItemEditor.tsx +113 -0
- package/src/client/components/editors/TableEditor.tsx +181 -0
- package/src/client/components/editors/TabsEditor.tsx +105 -0
- package/src/client/components/index.ts +7 -0
- package/src/client/loader.ts +19 -0
- package/src/client/state/autosave.ts +122 -0
- package/src/client/state/editor-store.ts +411 -0
- package/src/client/state/history.ts +251 -0
- package/src/client/state/lock-manager.ts +89 -0
- package/src/client/styles/cms-editor.css +653 -0
- package/src/client/toast.ts +12 -0
- package/src/client/utils/sortable.ts +184 -0
- package/src/client/utils/splitpane.ts +87 -0
- package/src/client/utils/tree-sanitizer.ts +33 -0
- package/src/core/page-definitions.ts +502 -0
- package/src/core/registry.ts +99 -0
- package/src/core/template-sync.ts +75 -0
- package/src/core/types/blocks.ts +367 -0
- package/src/core/types/inline.ts +23 -0
- package/src/core/types/pages.ts +36 -0
- package/src/core/types/templates.ts +32 -0
- package/src/core/types/versioning.ts +44 -0
- package/src/core/validator.ts +28 -0
- package/src/env.d.ts +4 -0
- package/src/index.ts +47 -0
- package/src/markdown/index.ts +2 -0
- package/src/markdown/parser.ts +347 -0
- package/src/markdown/serializer.ts +219 -0
- package/src/migration.ts +155 -0
- package/src/schema/index.ts +8 -0
- package/src/schema/page_draft_locks.ts +13 -0
- package/src/schema/page_drafts.ts +18 -0
- package/src/schema/page_templates.ts +27 -0
- package/src/schema/page_version_approvals.ts +12 -0
- package/src/schema/page_version_comments.ts +14 -0
- package/src/schema/page_versions.ts +34 -0
- package/src/schema/pages.ts +42 -0
- package/src/schema/search.ts +23 -0
- package/src/search/query.ts +10 -0
- 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
|
+
}
|