@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,245 @@
|
|
|
1
|
+
import { createSignal, onMount, onCleanup, For, type Component } from "solid-js"
|
|
2
|
+
import type { BaseBlock } from "../../../core/types/blocks"
|
|
3
|
+
import { initBlockSortable, cleanAllSortableClasses } from "../../utils/sortable"
|
|
4
|
+
import RLSInput from "@rimelight/ui/components/input/RLSInput.tsx"
|
|
5
|
+
import RLSTextarea from "@rimelight/ui/components/textarea/RLSTextarea.tsx"
|
|
6
|
+
import RLSBadge from "@rimelight/ui/components/badge/RLSBadge.tsx"
|
|
7
|
+
import RLSButton from "@rimelight/ui/components/button/RLSButton.tsx"
|
|
8
|
+
|
|
9
|
+
export interface ScriptEditorProps {
|
|
10
|
+
blockId?: string
|
|
11
|
+
props: Record<string, any>
|
|
12
|
+
children?: BaseBlock[] | (() => BaseBlock[])
|
|
13
|
+
sectionDepth?: number
|
|
14
|
+
onPropsChange: (newProps: Record<string, any>) => void
|
|
15
|
+
onRelocate?: ((id: string, newIndex: number, targetParentId?: string | null) => void) | undefined
|
|
16
|
+
onOpenPicker?: () => void
|
|
17
|
+
renderChildBlock?: (block: BaseBlock, depth: number) => any
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export const ScriptEditor: Component<ScriptEditorProps> = (componentProps) => {
|
|
21
|
+
const [charInputVal, setCharInputVal] = createSignal("")
|
|
22
|
+
let childrenContainer: HTMLDivElement | null = null as HTMLDivElement | null
|
|
23
|
+
let sortableInstance: any = null
|
|
24
|
+
|
|
25
|
+
const characters = (): string[] => componentProps.props.characters || []
|
|
26
|
+
const childrenBlocks = (): BaseBlock[] => {
|
|
27
|
+
if (typeof componentProps.children === "function") {
|
|
28
|
+
return componentProps.children() || []
|
|
29
|
+
}
|
|
30
|
+
return componentProps.children || componentProps.props.children || []
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
onMount(() => {
|
|
34
|
+
if (childrenContainer) {
|
|
35
|
+
sortableInstance = initBlockSortable(childrenContainer, {
|
|
36
|
+
filter: ".cms-empty-placeholder, .locked",
|
|
37
|
+
onReorder: (oldIdx: number, newIdx: number) => {
|
|
38
|
+
cleanAllSortableClasses()
|
|
39
|
+
const list = [...childrenBlocks()]
|
|
40
|
+
const item = list.splice(oldIdx, 1)[0]
|
|
41
|
+
if (item) {
|
|
42
|
+
list.splice(newIdx, 0, item)
|
|
43
|
+
componentProps.onPropsChange({ ...componentProps.props, children: list })
|
|
44
|
+
}
|
|
45
|
+
requestAnimationFrame(() => cleanAllSortableClasses())
|
|
46
|
+
setTimeout(cleanAllSortableClasses, 50)
|
|
47
|
+
setTimeout(cleanAllSortableClasses, 200)
|
|
48
|
+
},
|
|
49
|
+
onAdd: (blockId: string, newIdx: number) => {
|
|
50
|
+
cleanAllSortableClasses()
|
|
51
|
+
if (componentProps.onRelocate && componentProps.blockId) {
|
|
52
|
+
componentProps.onRelocate(blockId, newIdx, componentProps.blockId)
|
|
53
|
+
}
|
|
54
|
+
requestAnimationFrame(() => cleanAllSortableClasses())
|
|
55
|
+
setTimeout(cleanAllSortableClasses, 50)
|
|
56
|
+
setTimeout(cleanAllSortableClasses, 200)
|
|
57
|
+
},
|
|
58
|
+
onRemove: (_blockId: string, _oldIdx: number) => {
|
|
59
|
+
cleanAllSortableClasses()
|
|
60
|
+
}
|
|
61
|
+
})
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
onCleanup(() => {
|
|
65
|
+
if (sortableInstance) sortableInstance.destroy()
|
|
66
|
+
})
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
const saveProps = (partial: Record<string, any>) => {
|
|
70
|
+
componentProps.onPropsChange({
|
|
71
|
+
...componentProps.props,
|
|
72
|
+
...partial,
|
|
73
|
+
children: childrenBlocks()
|
|
74
|
+
})
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const addCharacter = (name: string) => {
|
|
78
|
+
const trimmed = name.trim()
|
|
79
|
+
if (!trimmed) return
|
|
80
|
+
const currentChars = characters()
|
|
81
|
+
if (!currentChars.includes(trimmed)) {
|
|
82
|
+
const updated = [...currentChars, trimmed]
|
|
83
|
+
setCharInputVal("")
|
|
84
|
+
componentProps.onPropsChange({ ...componentProps.props, characters: updated })
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const removeCharacter = (name: string) => {
|
|
89
|
+
const updated = characters().filter((c) => c !== name)
|
|
90
|
+
componentProps.onPropsChange({ ...componentProps.props, characters: updated })
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return (
|
|
94
|
+
<div class="rimelight-script-editor flex flex-col gap-4 p-3 border-l-2 border-primary/50 w-full">
|
|
95
|
+
<div class="flex flex-col gap-1">
|
|
96
|
+
<span class="text-xs font-bold text-muted-foreground uppercase tracking-widest">
|
|
97
|
+
Script Title
|
|
98
|
+
</span>
|
|
99
|
+
<RLSInput
|
|
100
|
+
data-field="title"
|
|
101
|
+
value={componentProps.props.title || ""}
|
|
102
|
+
onInput={(e) => saveProps({ title: e.currentTarget.value })}
|
|
103
|
+
placeholder="Script Title"
|
|
104
|
+
size="sm"
|
|
105
|
+
/>
|
|
106
|
+
</div>
|
|
107
|
+
|
|
108
|
+
<div class="flex flex-col gap-1">
|
|
109
|
+
<span class="text-xs font-bold text-muted-foreground uppercase tracking-widest">
|
|
110
|
+
Synopsis
|
|
111
|
+
</span>
|
|
112
|
+
<RLSTextarea
|
|
113
|
+
data-field="synopsis"
|
|
114
|
+
value={componentProps.props.synopsis || ""}
|
|
115
|
+
onInput={(e) => saveProps({ synopsis: e.currentTarget.value })}
|
|
116
|
+
placeholder="Synopsis..."
|
|
117
|
+
rows={2}
|
|
118
|
+
/>
|
|
119
|
+
</div>
|
|
120
|
+
|
|
121
|
+
<div class="flex flex-col gap-1">
|
|
122
|
+
<span class="text-xs font-bold text-muted-foreground uppercase tracking-widest">
|
|
123
|
+
Characters
|
|
124
|
+
</span>
|
|
125
|
+
<div class="chars-container flex flex-wrap gap-1.5 mb-1.5 min-h-[24px]">
|
|
126
|
+
{characters().length === 0 ? (
|
|
127
|
+
<div class="text-xs text-muted-foreground italic py-0.5">No characters added</div>
|
|
128
|
+
) : (
|
|
129
|
+
<For each={characters()}>
|
|
130
|
+
{(char) => (
|
|
131
|
+
<RLSBadge variant="soft" color="primary" size="sm">
|
|
132
|
+
<span>{char}</span>
|
|
133
|
+
<button
|
|
134
|
+
type="button"
|
|
135
|
+
onClick={() => removeCharacter(char)}
|
|
136
|
+
class="hover:text-red-500 cursor-pointer ml-1 font-bold bg-transparent border-none p-0 inline-flex items-center"
|
|
137
|
+
>
|
|
138
|
+
×
|
|
139
|
+
</button>
|
|
140
|
+
</RLSBadge>
|
|
141
|
+
)}
|
|
142
|
+
</For>
|
|
143
|
+
)}
|
|
144
|
+
</div>
|
|
145
|
+
<div class="flex gap-2">
|
|
146
|
+
<div class="flex-1">
|
|
147
|
+
<RLSInput
|
|
148
|
+
data-field="char-input"
|
|
149
|
+
value={charInputVal()}
|
|
150
|
+
onInput={(e) => setCharInputVal(e.currentTarget.value)}
|
|
151
|
+
onKeyDown={(e) => {
|
|
152
|
+
if (e.key === "Enter") {
|
|
153
|
+
e.preventDefault()
|
|
154
|
+
addCharacter(charInputVal())
|
|
155
|
+
}
|
|
156
|
+
}}
|
|
157
|
+
placeholder="New character name"
|
|
158
|
+
size="sm"
|
|
159
|
+
/>
|
|
160
|
+
</div>
|
|
161
|
+
<RLSButton
|
|
162
|
+
variant="outline"
|
|
163
|
+
color="neutral"
|
|
164
|
+
size="sm"
|
|
165
|
+
onClick={() => addCharacter(charInputVal())}
|
|
166
|
+
label="Add"
|
|
167
|
+
/>
|
|
168
|
+
</div>
|
|
169
|
+
</div>
|
|
170
|
+
|
|
171
|
+
<div class="flex flex-col gap-1">
|
|
172
|
+
<span class="text-xs font-bold text-muted-foreground uppercase tracking-widest">Plots</span>
|
|
173
|
+
<div class="grid grid-cols-3 gap-2">
|
|
174
|
+
<RLSInput
|
|
175
|
+
data-field="aPlot"
|
|
176
|
+
value={componentProps.props.aPlot || ""}
|
|
177
|
+
onInput={(e) => saveProps({ aPlot: e.currentTarget.value })}
|
|
178
|
+
placeholder="A-Plot"
|
|
179
|
+
size="sm"
|
|
180
|
+
/>
|
|
181
|
+
<RLSInput
|
|
182
|
+
data-field="bPlot"
|
|
183
|
+
value={componentProps.props.bPlot || ""}
|
|
184
|
+
onInput={(e) => saveProps({ bPlot: e.currentTarget.value })}
|
|
185
|
+
placeholder="B-Plot"
|
|
186
|
+
size="sm"
|
|
187
|
+
/>
|
|
188
|
+
<RLSInput
|
|
189
|
+
data-field="cPlot"
|
|
190
|
+
value={componentProps.props.cPlot || ""}
|
|
191
|
+
onInput={(e) => saveProps({ cPlot: e.currentTarget.value })}
|
|
192
|
+
placeholder="C-Plot"
|
|
193
|
+
size="sm"
|
|
194
|
+
/>
|
|
195
|
+
</div>
|
|
196
|
+
</div>
|
|
197
|
+
|
|
198
|
+
<div class="flex flex-col gap-1">
|
|
199
|
+
<span class="text-xs font-bold text-muted-foreground uppercase tracking-widest">
|
|
200
|
+
Stinger
|
|
201
|
+
</span>
|
|
202
|
+
<RLSInput
|
|
203
|
+
data-field="stinger"
|
|
204
|
+
value={componentProps.props.stinger || ""}
|
|
205
|
+
onInput={(e) => saveProps({ stinger: e.currentTarget.value })}
|
|
206
|
+
placeholder="Stinger (post-credits etc)"
|
|
207
|
+
size="sm"
|
|
208
|
+
/>
|
|
209
|
+
</div>
|
|
210
|
+
|
|
211
|
+
<div class="mt-2">
|
|
212
|
+
<span class="text-xs font-bold text-muted-foreground uppercase tracking-widest block mb-1">
|
|
213
|
+
Scenes & Content
|
|
214
|
+
</span>
|
|
215
|
+
<div
|
|
216
|
+
ref={(el) => (childrenContainer = el)}
|
|
217
|
+
class="children-container mt-1 pl-3 border-l-2 border-default flex flex-col gap-2 min-h-[60px]"
|
|
218
|
+
>
|
|
219
|
+
{childrenBlocks().length === 0 ? (
|
|
220
|
+
<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">
|
|
221
|
+
<div class="text-xs font-semibold text-muted-foreground">Empty script content</div>
|
|
222
|
+
<RLSButton
|
|
223
|
+
variant="outline"
|
|
224
|
+
color="neutral"
|
|
225
|
+
size="xs"
|
|
226
|
+
onClick={(e: MouseEvent) => {
|
|
227
|
+
e.stopPropagation()
|
|
228
|
+
componentProps.onOpenPicker?.()
|
|
229
|
+
}}
|
|
230
|
+
leadingIcon="plus"
|
|
231
|
+
label="Add Block"
|
|
232
|
+
/>
|
|
233
|
+
</div>
|
|
234
|
+
) : (
|
|
235
|
+
<For each={childrenBlocks()}>
|
|
236
|
+
{(childBlock) =>
|
|
237
|
+
componentProps.renderChildBlock?.(childBlock, componentProps.sectionDepth || 0)
|
|
238
|
+
}
|
|
239
|
+
</For>
|
|
240
|
+
)}
|
|
241
|
+
</div>
|
|
242
|
+
</div>
|
|
243
|
+
</div>
|
|
244
|
+
)
|
|
245
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { onMount, onCleanup, For, type Component } from "solid-js"
|
|
2
|
+
import { type BaseBlock, calculateHeadingLevel } from "../../../core/types/blocks"
|
|
3
|
+
import { initBlockSortable, cleanAllSortableClasses } from "../../utils/sortable"
|
|
4
|
+
import RLSButton from "@rimelight/ui/components/button/RLSButton.tsx"
|
|
5
|
+
|
|
6
|
+
export interface SectionEditorProps {
|
|
7
|
+
blockId?: string
|
|
8
|
+
props: Record<string, any>
|
|
9
|
+
children?: BaseBlock[] | (() => BaseBlock[])
|
|
10
|
+
sectionDepth?: number
|
|
11
|
+
onPropsChange: (newProps: Record<string, any>) => void
|
|
12
|
+
onRelocate?: ((id: string, newIndex: number, targetParentId?: string | null) => void) | undefined
|
|
13
|
+
onOpenPicker?: () => void
|
|
14
|
+
renderChildBlock?: (block: BaseBlock, depth: number) => any
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export const SectionEditor: Component<SectionEditorProps> = (componentProps) => {
|
|
18
|
+
let titleInput: HTMLInputElement | null = null as HTMLInputElement | null
|
|
19
|
+
let descInput: HTMLInputElement | null = null as HTMLInputElement | null
|
|
20
|
+
let childrenContainer: HTMLDivElement | null = null as HTMLDivElement | null
|
|
21
|
+
let sortableInstance: any = null
|
|
22
|
+
|
|
23
|
+
const headingLevel = () => calculateHeadingLevel(componentProps.sectionDepth || 0)
|
|
24
|
+
|
|
25
|
+
const childrenBlocks = (): BaseBlock[] => {
|
|
26
|
+
if (typeof componentProps.children === "function") {
|
|
27
|
+
return componentProps.children() || []
|
|
28
|
+
}
|
|
29
|
+
return componentProps.children || componentProps.props.children || []
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
onMount(() => {
|
|
33
|
+
if (childrenContainer) {
|
|
34
|
+
sortableInstance = initBlockSortable(childrenContainer, {
|
|
35
|
+
filter: ".cms-empty-placeholder, .locked",
|
|
36
|
+
onReorder: (oldIdx: number, newIdx: number) => {
|
|
37
|
+
cleanAllSortableClasses()
|
|
38
|
+
const list = [...childrenBlocks()]
|
|
39
|
+
const item = list.splice(oldIdx, 1)[0]
|
|
40
|
+
if (item) {
|
|
41
|
+
list.splice(newIdx, 0, item)
|
|
42
|
+
componentProps.onPropsChange({ ...componentProps.props, children: list })
|
|
43
|
+
}
|
|
44
|
+
requestAnimationFrame(() => cleanAllSortableClasses())
|
|
45
|
+
setTimeout(cleanAllSortableClasses, 50)
|
|
46
|
+
setTimeout(cleanAllSortableClasses, 200)
|
|
47
|
+
},
|
|
48
|
+
onAdd: (blockId: string, newIdx: number) => {
|
|
49
|
+
cleanAllSortableClasses()
|
|
50
|
+
if (componentProps.onRelocate && componentProps.blockId) {
|
|
51
|
+
componentProps.onRelocate(blockId, newIdx, componentProps.blockId)
|
|
52
|
+
}
|
|
53
|
+
requestAnimationFrame(() => cleanAllSortableClasses())
|
|
54
|
+
setTimeout(cleanAllSortableClasses, 50)
|
|
55
|
+
setTimeout(cleanAllSortableClasses, 200)
|
|
56
|
+
},
|
|
57
|
+
onRemove: (_blockId: string, _oldIdx: number) => {
|
|
58
|
+
cleanAllSortableClasses()
|
|
59
|
+
}
|
|
60
|
+
})
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
onCleanup(() => {
|
|
64
|
+
if (sortableInstance) sortableInstance.destroy()
|
|
65
|
+
})
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
const saveProps = () => {
|
|
69
|
+
componentProps.onPropsChange({
|
|
70
|
+
...componentProps.props,
|
|
71
|
+
title: titleInput?.value || "",
|
|
72
|
+
description: descInput?.value || "",
|
|
73
|
+
level: headingLevel(),
|
|
74
|
+
children: childrenBlocks()
|
|
75
|
+
})
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return (
|
|
79
|
+
<div class="w-full flex flex-col gap-2 py-0.5">
|
|
80
|
+
<div class="flex items-center gap-2.5">
|
|
81
|
+
<span class="text-xs font-bold text-muted-foreground select-none shrink-0 tracking-wider">
|
|
82
|
+
H{headingLevel()}
|
|
83
|
+
</span>
|
|
84
|
+
<input
|
|
85
|
+
ref={(el) => (titleInput = el)}
|
|
86
|
+
type="text"
|
|
87
|
+
value={componentProps.props.title || ""}
|
|
88
|
+
onInput={saveProps}
|
|
89
|
+
placeholder="Section Title"
|
|
90
|
+
class="flex-1 font-bold text-lg bg-transparent border-0 focus:outline-none text-foreground placeholder:text-muted-foreground/60 py-0.5"
|
|
91
|
+
/>
|
|
92
|
+
</div>
|
|
93
|
+
|
|
94
|
+
<input
|
|
95
|
+
ref={(el) => (descInput = el)}
|
|
96
|
+
type="text"
|
|
97
|
+
value={componentProps.props.description || ""}
|
|
98
|
+
onInput={saveProps}
|
|
99
|
+
placeholder="Section description (optional)..."
|
|
100
|
+
class="text-sm text-muted-foreground bg-transparent border-0 focus:outline-none placeholder:text-muted-foreground/50 py-0.5"
|
|
101
|
+
/>
|
|
102
|
+
|
|
103
|
+
<div
|
|
104
|
+
ref={(el) => (childrenContainer = el)}
|
|
105
|
+
class="ml-1 pl-8 border-l-2 border-primary/30 flex flex-col gap-2 mt-1 min-h-[32px]"
|
|
106
|
+
>
|
|
107
|
+
{childrenBlocks().length === 0 ? (
|
|
108
|
+
<div class="flex flex-col items-center justify-center p-3 border border-dashed border-default rounded-lg text-center">
|
|
109
|
+
<span class="text-xs text-muted-foreground">Empty section</span>
|
|
110
|
+
<RLSButton
|
|
111
|
+
variant="link"
|
|
112
|
+
color="primary"
|
|
113
|
+
size="xs"
|
|
114
|
+
onClick={() => componentProps.onOpenPicker?.()}
|
|
115
|
+
label="+ Add first block"
|
|
116
|
+
/>
|
|
117
|
+
</div>
|
|
118
|
+
) : (
|
|
119
|
+
<For each={childrenBlocks()}>
|
|
120
|
+
{(childBlock: BaseBlock) =>
|
|
121
|
+
componentProps.renderChildBlock?.(childBlock, (componentProps.sectionDepth || 0) + 1)
|
|
122
|
+
}
|
|
123
|
+
</For>
|
|
124
|
+
)}
|
|
125
|
+
</div>
|
|
126
|
+
|
|
127
|
+
<div class="flex items-center pt-1">
|
|
128
|
+
<RLSButton
|
|
129
|
+
variant="ghost"
|
|
130
|
+
color="neutral"
|
|
131
|
+
size="xs"
|
|
132
|
+
onClick={() => componentProps.onOpenPicker?.()}
|
|
133
|
+
leadingIcon="plus"
|
|
134
|
+
label="Add block"
|
|
135
|
+
/>
|
|
136
|
+
</div>
|
|
137
|
+
</div>
|
|
138
|
+
)
|
|
139
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { onMount, onCleanup, For, type Component } from "solid-js"
|
|
2
|
+
import type { BaseBlock } from "../../../core/types/blocks"
|
|
3
|
+
import { initBlockSortable, cleanAllSortableClasses } from "../../utils/sortable"
|
|
4
|
+
import RLSInput from "@rimelight/ui/components/input/RLSInput.tsx"
|
|
5
|
+
import RLSButton from "@rimelight/ui/components/button/RLSButton.tsx"
|
|
6
|
+
|
|
7
|
+
export interface StepItemEditorProps {
|
|
8
|
+
blockId?: string
|
|
9
|
+
props: Record<string, any>
|
|
10
|
+
children?: BaseBlock[] | (() => BaseBlock[])
|
|
11
|
+
sectionDepth?: number
|
|
12
|
+
onPropsChange: (newProps: Record<string, any>) => void
|
|
13
|
+
onRelocate?: ((id: string, newIndex: number, targetParentId?: string | null) => void) | undefined
|
|
14
|
+
onOpenPicker?: () => void
|
|
15
|
+
renderChildBlock?: (block: BaseBlock, depth: number) => any
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const StepItemEditor: Component<StepItemEditorProps> = (componentProps) => {
|
|
19
|
+
let childrenContainer: HTMLDivElement | undefined = undefined
|
|
20
|
+
let sortableInstance: any = null
|
|
21
|
+
|
|
22
|
+
const childrenBlocks = (): BaseBlock[] => {
|
|
23
|
+
if (typeof componentProps.children === "function") {
|
|
24
|
+
return componentProps.children() || []
|
|
25
|
+
}
|
|
26
|
+
return componentProps.children || componentProps.props.children || []
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
onMount(() => {
|
|
30
|
+
if (childrenContainer) {
|
|
31
|
+
sortableInstance = initBlockSortable(childrenContainer, {
|
|
32
|
+
filter: ".cms-empty-placeholder, .locked",
|
|
33
|
+
onReorder: (oldIdx: number, newIdx: number) => {
|
|
34
|
+
cleanAllSortableClasses()
|
|
35
|
+
const list = [...childrenBlocks()]
|
|
36
|
+
const item = list.splice(oldIdx, 1)[0]
|
|
37
|
+
if (item) {
|
|
38
|
+
list.splice(newIdx, 0, item)
|
|
39
|
+
componentProps.onPropsChange({ ...componentProps.props, children: list })
|
|
40
|
+
}
|
|
41
|
+
requestAnimationFrame(() => cleanAllSortableClasses())
|
|
42
|
+
setTimeout(cleanAllSortableClasses, 50)
|
|
43
|
+
setTimeout(cleanAllSortableClasses, 200)
|
|
44
|
+
},
|
|
45
|
+
onAdd: (blockId: string, newIdx: number) => {
|
|
46
|
+
cleanAllSortableClasses()
|
|
47
|
+
if (componentProps.onRelocate && componentProps.blockId) {
|
|
48
|
+
componentProps.onRelocate(blockId, newIdx, componentProps.blockId)
|
|
49
|
+
}
|
|
50
|
+
requestAnimationFrame(() => cleanAllSortableClasses())
|
|
51
|
+
setTimeout(cleanAllSortableClasses, 50)
|
|
52
|
+
setTimeout(cleanAllSortableClasses, 200)
|
|
53
|
+
},
|
|
54
|
+
onRemove: (_blockId: string, _oldIdx: number) => {
|
|
55
|
+
cleanAllSortableClasses()
|
|
56
|
+
}
|
|
57
|
+
})
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
onCleanup(() => {
|
|
61
|
+
if (sortableInstance) sortableInstance.destroy()
|
|
62
|
+
})
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
const saveMeta = (title?: string, description?: string) => {
|
|
66
|
+
componentProps.onPropsChange({
|
|
67
|
+
...componentProps.props,
|
|
68
|
+
title: title !== undefined ? title : componentProps.props.title || "",
|
|
69
|
+
description: description !== undefined ? description : componentProps.props.description || ""
|
|
70
|
+
})
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return (
|
|
74
|
+
<div class="rimelight-step-item-editor flex flex-col gap-2 p-3 rounded-lg border border-default bg-muted/10 w-full">
|
|
75
|
+
<div class="flex flex-col gap-1.5">
|
|
76
|
+
<RLSInput
|
|
77
|
+
type="text"
|
|
78
|
+
placeholder="Step title (e.g. Install Dependencies)"
|
|
79
|
+
value={componentProps.props.title || ""}
|
|
80
|
+
onBlur={(e) => saveMeta(e.currentTarget.value, undefined)}
|
|
81
|
+
size="sm"
|
|
82
|
+
/>
|
|
83
|
+
<RLSInput
|
|
84
|
+
type="text"
|
|
85
|
+
placeholder="Step description (optional)"
|
|
86
|
+
value={componentProps.props.description || ""}
|
|
87
|
+
onBlur={(e) => saveMeta(undefined, e.currentTarget.value)}
|
|
88
|
+
size="sm"
|
|
89
|
+
/>
|
|
90
|
+
</div>
|
|
91
|
+
|
|
92
|
+
<div
|
|
93
|
+
ref={(el) => (childrenContainer = el)}
|
|
94
|
+
class="p-2 rounded-lg border border-dashed border-default flex flex-col gap-2 min-h-[36px] bg-background"
|
|
95
|
+
>
|
|
96
|
+
{childrenBlocks().length === 0 ? (
|
|
97
|
+
<div class="text-center py-2">
|
|
98
|
+
<span class="text-xs text-muted-foreground block">Empty step content</span>
|
|
99
|
+
<RLSButton
|
|
100
|
+
variant="link"
|
|
101
|
+
color="primary"
|
|
102
|
+
size="xs"
|
|
103
|
+
onClick={() => componentProps.onOpenPicker?.()}
|
|
104
|
+
label="+ Add block"
|
|
105
|
+
/>
|
|
106
|
+
</div>
|
|
107
|
+
) : (
|
|
108
|
+
<For each={childrenBlocks()}>
|
|
109
|
+
{(childBlock) =>
|
|
110
|
+
componentProps.renderChildBlock?.(childBlock, componentProps.sectionDepth || 0)
|
|
111
|
+
}
|
|
112
|
+
</For>
|
|
113
|
+
)}
|
|
114
|
+
</div>
|
|
115
|
+
</div>
|
|
116
|
+
)
|
|
117
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { onMount, onCleanup, For, type Component } from "solid-js"
|
|
2
|
+
import type { BaseBlock } from "../../../core/types/blocks"
|
|
3
|
+
import { initBlockSortable, cleanAllSortableClasses } from "../../utils/sortable"
|
|
4
|
+
import RLSButton from "@rimelight/ui/components/button/RLSButton.tsx"
|
|
5
|
+
|
|
6
|
+
export interface StepsEditorProps {
|
|
7
|
+
blockId?: string
|
|
8
|
+
props: Record<string, any>
|
|
9
|
+
children?: BaseBlock[] | (() => BaseBlock[])
|
|
10
|
+
sectionDepth?: number
|
|
11
|
+
onPropsChange: (newProps: Record<string, any>) => void
|
|
12
|
+
onRelocate?: ((id: string, newIndex: number, targetParentId?: string | null) => void) | undefined
|
|
13
|
+
onOpenPicker?: () => void
|
|
14
|
+
renderChildBlock?: (block: BaseBlock, depth: number) => any
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export const StepsEditor: Component<StepsEditorProps> = (componentProps) => {
|
|
18
|
+
let childrenContainer: HTMLDivElement | undefined = undefined
|
|
19
|
+
let sortableInstance: any = null
|
|
20
|
+
|
|
21
|
+
const childrenBlocks = (): BaseBlock[] => {
|
|
22
|
+
if (typeof componentProps.children === "function") {
|
|
23
|
+
return componentProps.children() || []
|
|
24
|
+
}
|
|
25
|
+
return componentProps.children || componentProps.props.children || []
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
onMount(() => {
|
|
29
|
+
if (childrenContainer) {
|
|
30
|
+
sortableInstance = initBlockSortable(childrenContainer, {
|
|
31
|
+
filter: ".cms-empty-placeholder, .locked",
|
|
32
|
+
onReorder: (oldIdx: number, newIdx: number) => {
|
|
33
|
+
cleanAllSortableClasses()
|
|
34
|
+
const list = [...childrenBlocks()]
|
|
35
|
+
const item = list.splice(oldIdx, 1)[0]
|
|
36
|
+
if (item) {
|
|
37
|
+
list.splice(newIdx, 0, item)
|
|
38
|
+
componentProps.onPropsChange({ ...componentProps.props, children: list })
|
|
39
|
+
}
|
|
40
|
+
requestAnimationFrame(() => cleanAllSortableClasses())
|
|
41
|
+
setTimeout(cleanAllSortableClasses, 50)
|
|
42
|
+
setTimeout(cleanAllSortableClasses, 200)
|
|
43
|
+
},
|
|
44
|
+
onAdd: (blockId: string, newIdx: number) => {
|
|
45
|
+
cleanAllSortableClasses()
|
|
46
|
+
if (componentProps.onRelocate && componentProps.blockId) {
|
|
47
|
+
componentProps.onRelocate(blockId, newIdx, componentProps.blockId)
|
|
48
|
+
}
|
|
49
|
+
requestAnimationFrame(() => cleanAllSortableClasses())
|
|
50
|
+
setTimeout(cleanAllSortableClasses, 50)
|
|
51
|
+
setTimeout(cleanAllSortableClasses, 200)
|
|
52
|
+
},
|
|
53
|
+
onRemove: (_blockId: string, _oldIdx: number) => {
|
|
54
|
+
cleanAllSortableClasses()
|
|
55
|
+
}
|
|
56
|
+
})
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
onCleanup(() => {
|
|
60
|
+
if (sortableInstance) sortableInstance.destroy()
|
|
61
|
+
})
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
return (
|
|
65
|
+
<div class="rimelight-steps-editor flex flex-col gap-3 p-3 rounded-xl border border-default/70 w-full">
|
|
66
|
+
<div class="flex items-center justify-between">
|
|
67
|
+
<span class="text-xs font-bold uppercase tracking-wider text-muted-foreground">
|
|
68
|
+
Steps Walkthrough
|
|
69
|
+
</span>
|
|
70
|
+
<RLSButton
|
|
71
|
+
variant="outline"
|
|
72
|
+
color="neutral"
|
|
73
|
+
size="xs"
|
|
74
|
+
onClick={() => componentProps.onOpenPicker?.()}
|
|
75
|
+
leadingIcon="plus"
|
|
76
|
+
label="Add Step Item"
|
|
77
|
+
/>
|
|
78
|
+
</div>
|
|
79
|
+
|
|
80
|
+
<div
|
|
81
|
+
ref={(el) => (childrenContainer = el)}
|
|
82
|
+
class="p-2 rounded-lg border border-dashed border-default flex flex-col gap-2 min-h-[36px]"
|
|
83
|
+
>
|
|
84
|
+
{childrenBlocks().length === 0 ? (
|
|
85
|
+
<div class="text-center py-3">
|
|
86
|
+
<span class="text-xs text-muted-foreground block mb-2">No steps in walkthrough</span>
|
|
87
|
+
<RLSButton
|
|
88
|
+
variant="link"
|
|
89
|
+
color="primary"
|
|
90
|
+
size="xs"
|
|
91
|
+
onClick={() => componentProps.onOpenPicker?.()}
|
|
92
|
+
label="+ Add Step Item"
|
|
93
|
+
/>
|
|
94
|
+
</div>
|
|
95
|
+
) : (
|
|
96
|
+
<For each={childrenBlocks()}>
|
|
97
|
+
{(childBlock) =>
|
|
98
|
+
componentProps.renderChildBlock?.(childBlock, componentProps.sectionDepth || 0)
|
|
99
|
+
}
|
|
100
|
+
</For>
|
|
101
|
+
)}
|
|
102
|
+
</div>
|
|
103
|
+
</div>
|
|
104
|
+
)
|
|
105
|
+
}
|