@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,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, "&").replace(/</g, "<").replace(/>/g, ">")
|
|
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
|
+
}
|