@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,314 @@
1
+ import { createSignal, onMount, Show, type Component } from "solid-js"
2
+ import RLSButton from "@rimelight/ui/components/button/RLSButton.tsx"
3
+ import RLSModal from "@rimelight/ui/components/modal/RLSModal.tsx"
4
+ import RLSInput from "@rimelight/ui/components/input/RLSInput.tsx"
5
+
6
+ export interface InlineNode {
7
+ type: "text" | "link"
8
+ text?: string
9
+ marks?: string[]
10
+ url?: string
11
+ children?: InlineNode[]
12
+ openInNewTab?: boolean
13
+ }
14
+
15
+ export interface ParagraphEditorProps {
16
+ props: Record<string, any>
17
+ onPropsChange: (newProps: Record<string, any>) => void
18
+ }
19
+
20
+ const renderNodesToHtml = (nodes: InlineNode[]): string => {
21
+ return nodes
22
+ .map((node) => {
23
+ if (node.type === "text") {
24
+ let text = node.text || ""
25
+ text = text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;")
26
+ if (node.marks) {
27
+ if (node.marks.includes("bold")) text = `<strong>${text}</strong>`
28
+ if (node.marks.includes("italic")) text = `<em>${text}</em>`
29
+ if (node.marks.includes("underline")) text = `<u>${text}</u>`
30
+ if (node.marks.includes("strikethrough")) text = `<s>${text}</s>`
31
+ if (node.marks.includes("code")) text = `<code>${text}</code>`
32
+ }
33
+ return text
34
+ } else if (node.type === "link") {
35
+ const target = node.openInNewTab ? ' target="_blank"' : ""
36
+ return `<a href="${node.url || "#"}"${target}>${renderNodesToHtml(node.children || [])}</a>`
37
+ }
38
+ return ""
39
+ })
40
+ .join("")
41
+ }
42
+
43
+ export const ParagraphEditor: Component<ParagraphEditorProps> = (componentProps) => {
44
+ let editableDiv: HTMLDivElement | null = null as HTMLDivElement | null
45
+ let containerDiv: HTMLDivElement | null = null as HTMLDivElement | null
46
+
47
+ const [toolbarVisible, setToolbarVisible] = createSignal(false)
48
+ const [linkModalOpen, setLinkModalOpen] = createSignal(false)
49
+ const [linkUrl, setLinkUrl] = createSignal("")
50
+
51
+ const parseDOM = (root: Node): InlineNode[] => {
52
+ const nodes: InlineNode[] = []
53
+ root.childNodes.forEach((child) => {
54
+ if (child.nodeType === Node.TEXT_NODE) {
55
+ if (child.textContent) {
56
+ nodes.push({ type: "text", text: child.textContent })
57
+ }
58
+ } else if (child instanceof HTMLElement) {
59
+ const el = child
60
+ const tagName = el.tagName.toLowerCase()
61
+
62
+ if (el instanceof HTMLAnchorElement) {
63
+ const a = el
64
+ nodes.push({
65
+ type: "link",
66
+ url: a.href,
67
+ children: parseDOM(el)
68
+ })
69
+ } else {
70
+ const childNodes = parseDOM(el)
71
+ let mark = ""
72
+ if (["strong", "b"].includes(tagName)) mark = "bold"
73
+ else if (["em", "i"].includes(tagName)) mark = "italic"
74
+ else if (["u"].includes(tagName)) mark = "underline"
75
+ else if (["s", "strike"].includes(tagName)) mark = "strikethrough"
76
+ else if (["code"].includes(tagName)) mark = "code"
77
+
78
+ if (mark) {
79
+ childNodes.forEach((n) => {
80
+ if (n.type === "text") {
81
+ n.marks = n.marks || []
82
+ if (!n.marks.includes(mark)) n.marks.push(mark)
83
+ }
84
+ })
85
+ }
86
+ nodes.push(...childNodes)
87
+ }
88
+ }
89
+ })
90
+ return nodes
91
+ }
92
+
93
+ const saveProps = () => {
94
+ if (!editableDiv) return
95
+ const parsedNodes = parseDOM(editableDiv)
96
+ componentProps.onPropsChange({
97
+ ...componentProps.props,
98
+ text: parsedNodes
99
+ })
100
+ }
101
+
102
+ const execCommand = (cmd: string) => {
103
+ if (cmd === "createLink") {
104
+ // Open our modal instead of using browser prompt()
105
+ setLinkUrl("")
106
+ setLinkModalOpen(true)
107
+ } else {
108
+ // Use Selection API instead of deprecated execCommand
109
+ const selection = window.getSelection()
110
+ if (selection && selection.rangeCount > 0) {
111
+ const range = selection.getRangeAt(0)
112
+ const selectedText = range.toString()
113
+
114
+ if (selectedText) {
115
+ if (cmd === "bold") {
116
+ const strong = document.createElement("strong")
117
+ range.surroundContents(strong)
118
+ } else if (cmd === "italic") {
119
+ const em = document.createElement("em")
120
+ range.surroundContents(em)
121
+ } else if (cmd === "underline") {
122
+ const u = document.createElement("u")
123
+ range.surroundContents(u)
124
+ }
125
+ selection.removeAllRanges()
126
+ }
127
+ }
128
+ editableDiv?.focus()
129
+ }
130
+ }
131
+
132
+ const applyLink = () => {
133
+ const url = linkUrl().trim()
134
+ if (url) {
135
+ editableDiv?.focus()
136
+ // Use Selection API instead of deprecated execCommand
137
+ const selection = window.getSelection()
138
+ if (selection && selection.rangeCount > 0) {
139
+ const range = selection.getRangeAt(0)
140
+ const selectedText = range.toString()
141
+
142
+ if (selectedText) {
143
+ const a = document.createElement("a")
144
+ a.href = url
145
+ a.textContent = selectedText
146
+ range.deleteContents()
147
+ range.insertNode(a)
148
+ selection.removeAllRanges()
149
+ }
150
+ }
151
+ saveProps()
152
+ }
153
+ setLinkModalOpen(false)
154
+ }
155
+
156
+ onMount(() => {
157
+ if (!editableDiv) return
158
+ const text = componentProps.props.text
159
+ if (!text) {
160
+ editableDiv.innerHTML = ""
161
+ } else if (Array.isArray(text)) {
162
+ editableDiv.innerHTML = renderNodesToHtml(text)
163
+ } else if (typeof text === "object" && text.en) {
164
+ editableDiv.innerText = text.en
165
+ } else {
166
+ editableDiv.innerText = String(text)
167
+ }
168
+
169
+ editableDiv.addEventListener("input", () => {
170
+ saveProps()
171
+ })
172
+ editableDiv.addEventListener("focus", () => setToolbarVisible(true))
173
+ editableDiv.addEventListener("blur", () => {
174
+ setTimeout(() => {
175
+ if (containerDiv && !containerDiv.contains(document.activeElement)) {
176
+ setToolbarVisible(false)
177
+ saveProps()
178
+ }
179
+ }, 200)
180
+ })
181
+ editableDiv.addEventListener("keydown", (e: KeyboardEvent) => {
182
+ if (e.ctrlKey || e.metaKey) {
183
+ if (e.key === "b") {
184
+ e.preventDefault()
185
+ execCommand("bold")
186
+ } else if (e.key === "i") {
187
+ e.preventDefault()
188
+ execCommand("italic")
189
+ } else if (e.key === "u") {
190
+ e.preventDefault()
191
+ execCommand("underline")
192
+ } else if (e.key === "k") {
193
+ e.preventDefault()
194
+ execCommand("createLink")
195
+ }
196
+ }
197
+ })
198
+ editableDiv.addEventListener("paste", (e: ClipboardEvent) => {
199
+ e.preventDefault()
200
+ const clipboardText = e.clipboardData?.getData("text/plain") || ""
201
+ // Use Selection API instead of deprecated execCommand
202
+ const selection = window.getSelection()
203
+ if (selection && selection.rangeCount > 0) {
204
+ const range = selection.getRangeAt(0)
205
+ range.deleteContents()
206
+ const textNode = document.createTextNode(clipboardText)
207
+ range.insertNode(textNode)
208
+ selection.removeAllRanges()
209
+ }
210
+ saveProps()
211
+ })
212
+ })
213
+
214
+ return (
215
+ <div
216
+ ref={(el) => (containerDiv = el)}
217
+ class="rimelight-paragraph-editor flex flex-col w-full relative"
218
+ >
219
+ <div
220
+ class="toolbar-div flex items-center gap-0.5 px-1.5 py-1 bg-elevated border border-default rounded-lg shadow-sm mb-1 self-start"
221
+ classList={{ hidden: !toolbarVisible() }}
222
+ >
223
+ <RLSButton
224
+ variant="ghost"
225
+ color="neutral"
226
+ size="xs"
227
+ square={true}
228
+ onClick={() => execCommand("bold")}
229
+ class="font-bold !px-2"
230
+ label="B"
231
+ />
232
+ <RLSButton
233
+ variant="ghost"
234
+ color="neutral"
235
+ size="xs"
236
+ square={true}
237
+ onClick={() => execCommand("italic")}
238
+ class="italic font-serif !px-2"
239
+ label="I"
240
+ />
241
+ <RLSButton
242
+ variant="ghost"
243
+ color="neutral"
244
+ size="xs"
245
+ square={true}
246
+ onClick={() => execCommand("underline")}
247
+ class="underline !px-2"
248
+ label="U"
249
+ />
250
+ <div class="w-px h-3.5 bg-default mx-1" />
251
+ <RLSButton
252
+ variant="ghost"
253
+ color="neutral"
254
+ size="xs"
255
+ onClick={() => execCommand("createLink")}
256
+ leadingIcon="link"
257
+ label="Link"
258
+ />
259
+ </div>
260
+ <div
261
+ ref={(el) => (editableDiv = el)}
262
+ aria-label="Paragraph text content"
263
+ contentEditable={true}
264
+ class="editable-div py-1.5 px-2 outline-none min-h-[2.5rem] text-sm text-foreground bg-transparent leading-relaxed focus:ring-1 focus:ring-primary/40 rounded-lg"
265
+ />
266
+
267
+ {/* Link URL Modal — replaces browser prompt() */}
268
+ <Show when={linkModalOpen()}>
269
+ <RLSModal
270
+ open={linkModalOpen()}
271
+ onClose={() => setLinkModalOpen(false)}
272
+ title="Insert Link"
273
+ footerSlot={
274
+ <>
275
+ <RLSButton
276
+ variant="outline"
277
+ color="neutral"
278
+ size="sm"
279
+ onClick={() => setLinkModalOpen(false)}
280
+ label="Cancel"
281
+ />
282
+ <RLSButton
283
+ variant="solid"
284
+ color="primary"
285
+ size="sm"
286
+ onClick={applyLink}
287
+ label="Insert"
288
+ />
289
+ </>
290
+ }
291
+ >
292
+ <div class="flex flex-col gap-2 py-2">
293
+ <span class="text-xs font-semibold text-muted-foreground">URL</span>
294
+ <RLSInput
295
+ value={linkUrl()}
296
+ onInput={(e: InputEvent & { currentTarget: HTMLInputElement }) =>
297
+ setLinkUrl(e.currentTarget.value)
298
+ }
299
+ onKeyDown={(e: KeyboardEvent) => {
300
+ if (e.key === "Enter") {
301
+ e.preventDefault()
302
+ applyLink()
303
+ }
304
+ }}
305
+ placeholder="https://..."
306
+ leadingIcon="link"
307
+ autofocus
308
+ />
309
+ </div>
310
+ </RLSModal>
311
+ </Show>
312
+ </div>
313
+ )
314
+ }
@@ -0,0 +1,245 @@
1
+ import { createSignal, onMount, onCleanup, For, Show, type Component } from "solid-js"
2
+ import type { BaseBlock } from "../../../core/types/blocks"
3
+ import { initBlockSortable, cleanAllSortableClasses } from "../../utils/sortable"
4
+ import { createDefaultBlock } from "../../state/history"
5
+ import RLSSelect from "@rimelight/ui/components/select/RLSSelect.tsx"
6
+ import RLSInput from "@rimelight/ui/components/input/RLSInput.tsx"
7
+ import RLSTextarea from "@rimelight/ui/components/textarea/RLSTextarea.tsx"
8
+ import RLSButton from "@rimelight/ui/components/button/RLSButton.tsx"
9
+
10
+ export interface SceneEditorProps {
11
+ blockId?: string
12
+ props: Record<string, any>
13
+ children?: BaseBlock[] | (() => BaseBlock[])
14
+ sectionDepth?: number
15
+ onPropsChange: (newProps: Record<string, any>) => void
16
+ onRelocate?: ((id: string, newIndex: number, targetParentId?: string | null) => void) | undefined
17
+ onOpenPicker?: () => void
18
+ renderChildBlock?: (block: BaseBlock, depth: number) => any
19
+ }
20
+
21
+ const SETTINGS = ["INT.", "EXT.", "INT./EXT.", "OTHER"]
22
+ const TIMES_OF_DAY = [
23
+ "MORNING",
24
+ "DAY",
25
+ "AFTERNOON",
26
+ "EVENING",
27
+ "NIGHT",
28
+ "CONTINUOUS",
29
+ "LATER",
30
+ "OTHER"
31
+ ]
32
+ const TRANSITIONS = ["NONE", "CUT_TO", "FADE_TO", "DISSOLVE_TO"]
33
+
34
+ export const SceneEditor: Component<SceneEditorProps> = (componentProps) => {
35
+ let childrenCanvas: HTMLDivElement | undefined
36
+ let sortableInstance: any = null
37
+
38
+ const [addMenuOpen, setAddMenuOpen] = createSignal(false)
39
+
40
+ const childrenBlocks = (): BaseBlock[] => {
41
+ if (typeof componentProps.children === "function") {
42
+ return componentProps.children() || []
43
+ }
44
+ return componentProps.children || componentProps.props.children || []
45
+ }
46
+
47
+ const slugText = () => {
48
+ const setting = componentProps.props.setting || "INT."
49
+ const loc = (componentProps.props.location || "LOCATION").toUpperCase()
50
+ const time = componentProps.props.timeOfDay || "MORNING"
51
+ return `${setting} ${loc} - ${time}`
52
+ }
53
+
54
+ const saveProps = (partial: Record<string, any>) => {
55
+ componentProps.onPropsChange({
56
+ ...componentProps.props,
57
+ ...partial,
58
+ children: childrenBlocks()
59
+ })
60
+ }
61
+
62
+ onMount(() => {
63
+ if (childrenCanvas) {
64
+ sortableInstance = initBlockSortable(childrenCanvas, {
65
+ filter: ".cms-empty-placeholder, .locked",
66
+ onReorder: (oldIdx: number, newIdx: number) => {
67
+ cleanAllSortableClasses()
68
+ const list = [...childrenBlocks()]
69
+ const item = list.splice(oldIdx, 1)[0]
70
+ if (item) {
71
+ list.splice(newIdx, 0, item)
72
+ componentProps.onPropsChange({ ...componentProps.props, children: list })
73
+ }
74
+ requestAnimationFrame(() => cleanAllSortableClasses())
75
+ setTimeout(cleanAllSortableClasses, 50)
76
+ setTimeout(cleanAllSortableClasses, 200)
77
+ },
78
+ onAdd: (blockId: string, newIdx: number) => {
79
+ cleanAllSortableClasses()
80
+ if (componentProps.onRelocate && componentProps.blockId) {
81
+ componentProps.onRelocate(blockId, newIdx, componentProps.blockId)
82
+ }
83
+ requestAnimationFrame(() => cleanAllSortableClasses())
84
+ setTimeout(cleanAllSortableClasses, 50)
85
+ setTimeout(cleanAllSortableClasses, 200)
86
+ },
87
+ onRemove: (_blockId: string, _oldIdx: number) => {
88
+ cleanAllSortableClasses()
89
+ }
90
+ })
91
+ }
92
+
93
+ onCleanup(() => {
94
+ if (sortableInstance) sortableInstance.destroy()
95
+ })
96
+ })
97
+
98
+ const addChildBlock = (type: string) => {
99
+ const newBlock = createDefaultBlock(type)
100
+ const list = [...childrenBlocks(), newBlock]
101
+ componentProps.onPropsChange({ ...componentProps.props, children: list })
102
+ setAddMenuOpen(false)
103
+ }
104
+
105
+ return (
106
+ <div class="rimelight-scene-editor flex flex-col gap-3 p-3 border-l-2 border-primary/50 w-full">
107
+ <div class="slug-preview text-xs font-mono text-primary uppercase px-2 py-1 rounded border border-default/50 self-start">
108
+ {slugText()}
109
+ </div>
110
+
111
+ <div class="grid grid-cols-2 gap-3">
112
+ <div class="col-span-2 flex flex-col gap-1">
113
+ <span class="text-xs font-semibold text-muted-foreground uppercase tracking-wide">
114
+ Location
115
+ </span>
116
+ <RLSInput
117
+ id="scene-location-input"
118
+ data-field="location"
119
+ value={componentProps.props.location || ""}
120
+ onBlur={(e) => saveProps({ location: e.currentTarget.value })}
121
+ placeholder="E.g. LIVING ROOM"
122
+ size="sm"
123
+ />
124
+ </div>
125
+
126
+ <div class="flex flex-col gap-1">
127
+ <span class="text-xs font-semibold text-muted-foreground uppercase tracking-wide">
128
+ Setting
129
+ </span>
130
+ <RLSSelect
131
+ items={SETTINGS}
132
+ value={componentProps.props.setting || "INT."}
133
+ onChange={(e) => saveProps({ setting: e.currentTarget.value })}
134
+ size="sm"
135
+ />
136
+ </div>
137
+
138
+ <div class="flex flex-col gap-1">
139
+ <span class="text-xs font-semibold text-muted-foreground uppercase tracking-wide">
140
+ Time of Day
141
+ </span>
142
+ <RLSSelect
143
+ items={TIMES_OF_DAY}
144
+ value={componentProps.props.timeOfDay || "MORNING"}
145
+ onChange={(e) => saveProps({ timeOfDay: e.currentTarget.value })}
146
+ size="sm"
147
+ />
148
+ </div>
149
+ </div>
150
+
151
+ <div class="flex flex-col gap-1">
152
+ <span class="text-xs font-semibold text-muted-foreground uppercase tracking-wide">
153
+ Action / Description
154
+ </span>
155
+ <RLSTextarea
156
+ id="scene-description-textarea"
157
+ data-field="description"
158
+ value={componentProps.props.description || ""}
159
+ onBlur={(e) => saveProps({ description: e.currentTarget.value })}
160
+ placeholder="Action lines..."
161
+ rows={3}
162
+ />
163
+ </div>
164
+
165
+ <div class="flex flex-col gap-1">
166
+ <span class="text-xs font-semibold text-muted-foreground uppercase tracking-wide">
167
+ Transition (End of Scene)
168
+ </span>
169
+ <div class="w-48">
170
+ <RLSSelect
171
+ items={TRANSITIONS}
172
+ value={componentProps.props.transition || "NONE"}
173
+ onChange={(e) => saveProps({ transition: e.currentTarget.value })}
174
+ size="sm"
175
+ />
176
+ </div>
177
+ </div>
178
+
179
+ <div
180
+ ref={(el) => (childrenCanvas = el)}
181
+ class="children-canvas mt-2 pl-3 border-l-2 border-default flex flex-col gap-2 min-h-[60px]"
182
+ data-container-type="scene"
183
+ >
184
+ {childrenBlocks().length === 0 ? (
185
+ <div class="cms-empty-placeholder relative flex flex-col items-center justify-center gap-2 p-3 rounded-lg border border-dashed border-default text-center my-1">
186
+ <div class="text-xs font-semibold text-muted-foreground">Empty scene</div>
187
+ <RLSButton
188
+ variant="outline"
189
+ color="neutral"
190
+ size="xs"
191
+ onClick={() => setAddMenuOpen(true)}
192
+ leadingIcon="plus"
193
+ label="Add Content"
194
+ />
195
+ </div>
196
+ ) : (
197
+ <For each={childrenBlocks()}>
198
+ {(childBlock) =>
199
+ componentProps.renderChildBlock?.(childBlock, (componentProps.sectionDepth || 0) + 1)
200
+ }
201
+ </For>
202
+ )}
203
+ </div>
204
+
205
+ <Show when={addMenuOpen()}>
206
+ <div class="add-block-menu mt-2 p-2 rounded-lg border border-default flex items-center gap-2">
207
+ <RLSButton
208
+ variant="soft"
209
+ color="neutral"
210
+ size="xs"
211
+ onClick={() => addChildBlock("ParagraphBlock")}
212
+ label="Text"
213
+ />
214
+ <RLSButton
215
+ variant="soft"
216
+ color="primary"
217
+ size="xs"
218
+ onClick={() => addChildBlock("DialogueBlock")}
219
+ label="Dialogue"
220
+ />
221
+ <div class="ml-auto">
222
+ <RLSButton
223
+ variant="ghost"
224
+ color="neutral"
225
+ size="xs"
226
+ onClick={() => setAddMenuOpen(false)}
227
+ label="Cancel"
228
+ />
229
+ </div>
230
+ </div>
231
+ </Show>
232
+
233
+ <div class="pt-1">
234
+ <RLSButton
235
+ variant="outline"
236
+ color="neutral"
237
+ size="xs"
238
+ onClick={() => setAddMenuOpen(true)}
239
+ leadingIcon="plus"
240
+ label="Add Content"
241
+ />
242
+ </div>
243
+ </div>
244
+ )
245
+ }