@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,305 @@
1
+ import { createSignal, onMount, onCleanup, For, type Component } from "solid-js"
2
+ import RLSInput from "@rimelight/ui/components/input/RLSInput.tsx"
3
+ import RLSIcon from "@rimelight/ui/components/icon/RLSIcon.tsx"
4
+ import RLSButton from "@rimelight/ui/components/button/RLSButton.tsx"
5
+
6
+ const BLOCK_DEFS = [
7
+ {
8
+ group: "Text",
9
+ type: "ParagraphBlock",
10
+ label: "Paragraph",
11
+ desc: "Rich text paragraph",
12
+ icon: "paragraph"
13
+ },
14
+ {
15
+ group: "Text",
16
+ type: "SectionBlock",
17
+ label: "Section",
18
+ desc: "Collapsible section with heading",
19
+ icon: "heading"
20
+ },
21
+ {
22
+ group: "Media",
23
+ type: "ImageBlock",
24
+ label: "Image",
25
+ desc: "Embed an image with caption",
26
+ icon: "image"
27
+ },
28
+ {
29
+ group: "Media",
30
+ type: "CodeBlock",
31
+ label: "Code",
32
+ desc: "Code block with syntax highlighting",
33
+ icon: "code"
34
+ },
35
+ {
36
+ group: "Callout",
37
+ type: "CalloutBlock",
38
+ label: "Callout",
39
+ desc: "Highlighted info/warning/note box",
40
+ icon: "info"
41
+ },
42
+ {
43
+ group: "Documentation",
44
+ type: "TableBlock",
45
+ label: "Table",
46
+ desc: "Structured data table with columns and rows",
47
+ icon: "table"
48
+ },
49
+ {
50
+ group: "Documentation",
51
+ type: "TabsBlock",
52
+ label: "Tabs",
53
+ desc: "Tabbed content container",
54
+ icon: "layers"
55
+ },
56
+ {
57
+ group: "Documentation",
58
+ type: "TabItemBlock",
59
+ label: "Tab Item",
60
+ desc: "Individual tab page",
61
+ icon: "fileText"
62
+ },
63
+ {
64
+ group: "Documentation",
65
+ type: "StepsBlock",
66
+ label: "Steps",
67
+ desc: "Step-by-step walkthrough sequence",
68
+ icon: "listOrdered"
69
+ },
70
+ {
71
+ group: "Documentation",
72
+ type: "StepItemBlock",
73
+ label: "Step Item",
74
+ desc: "Individual step",
75
+ icon: "check"
76
+ },
77
+ {
78
+ group: "Documentation",
79
+ type: "CardsBlock",
80
+ label: "Cards Grid",
81
+ desc: "Grid container for link cards",
82
+ icon: "grid"
83
+ },
84
+ {
85
+ group: "Documentation",
86
+ type: "CardBlock",
87
+ label: "Card",
88
+ desc: "Interactive doc/link card",
89
+ icon: "file"
90
+ },
91
+ {
92
+ group: "Documentation",
93
+ type: "FileTreeBlock",
94
+ label: "File Tree",
95
+ desc: "Directory and file structure visualizer",
96
+ icon: "folder"
97
+ },
98
+ {
99
+ group: "Screenplay",
100
+ type: "ScriptBlock",
101
+ label: "Script",
102
+ desc: "Script container with plot tracking",
103
+ icon: "fileText"
104
+ },
105
+ {
106
+ group: "Screenplay",
107
+ type: "SceneBlock",
108
+ label: "Scene",
109
+ desc: "Scene with location and time-of-day",
110
+ icon: "video"
111
+ },
112
+ {
113
+ group: "Screenplay",
114
+ type: "DialogueBlock",
115
+ label: "Dialogue",
116
+ desc: "Character dialogue line",
117
+ icon: "messageSquare"
118
+ }
119
+ ]
120
+
121
+ import { ALLOWED_CHILDREN_MAP, MAX_SECTION_DEPTH } from "../../core/types/blocks"
122
+ import { toast } from "../toast"
123
+ export { ALLOWED_CHILDREN_MAP }
124
+
125
+ export interface RimelightBlockPickerProps {
126
+ open: boolean
127
+ sectionDepth?: number | undefined
128
+ containerType?: string | null | undefined
129
+ onSelect: (type: string) => void
130
+ onClose: () => void
131
+ }
132
+
133
+ export const RimelightBlockPicker: Component<RimelightBlockPickerProps> = (props) => {
134
+ let searchInput: HTMLInputElement | undefined = undefined
135
+ const [query, setQuery] = createSignal("")
136
+ const [selectedIndex, setSelectedIndex] = createSignal(0)
137
+
138
+ const allowedTypes = (): string[] => {
139
+ const parent = props.containerType || "Root"
140
+ return ALLOWED_CHILDREN_MAP[parent] || ALLOWED_CHILDREN_MAP.Root || []
141
+ }
142
+
143
+ const filteredBlocks = () => {
144
+ const q = query().toLowerCase()
145
+ const depth = props.sectionDepth || 0
146
+ const allowed = allowedTypes()
147
+
148
+ return BLOCK_DEFS.filter((b) => {
149
+ if (!allowed.includes(b.type)) return false
150
+ if (b.type === "SectionBlock" && depth > MAX_SECTION_DEPTH) return false
151
+ return (
152
+ b.label.toLowerCase().includes(q) ||
153
+ b.type.toLowerCase().includes(q) ||
154
+ b.desc.toLowerCase().includes(q) ||
155
+ b.group.toLowerCase().includes(q)
156
+ )
157
+ })
158
+ }
159
+
160
+ const handleSelectBlock = (type: string) => {
161
+ const depth = props.sectionDepth || 0
162
+ if (type === "SectionBlock" && depth > MAX_SECTION_DEPTH) {
163
+ toast.error(
164
+ "Invalid Section Nesting",
165
+ "Sections cannot be nested inside an H6 section. H6 is the maximum heading level."
166
+ )
167
+ return
168
+ }
169
+ props.onSelect(type)
170
+ }
171
+
172
+ onMount(() => {
173
+ const handleGlobalKeydown = (e: KeyboardEvent) => {
174
+ if (props.open && e.key === "Escape") {
175
+ props.onClose()
176
+ }
177
+ }
178
+ document.addEventListener("keydown", handleGlobalKeydown)
179
+ onCleanup(() => document.removeEventListener("keydown", handleGlobalKeydown))
180
+ })
181
+
182
+ const handleKeydown = (e: KeyboardEvent) => {
183
+ const list = filteredBlocks()
184
+ if (e.key === "ArrowDown") {
185
+ e.preventDefault()
186
+ setSelectedIndex((prev) => Math.min(prev + 1, Math.max(0, list.length - 1)))
187
+ } else if (e.key === "ArrowUp") {
188
+ e.preventDefault()
189
+ setSelectedIndex((prev) => Math.max(prev - 1, 0))
190
+ } else if (e.key === "Enter") {
191
+ e.preventDefault()
192
+ const selected = list[selectedIndex()]
193
+ if (selected) {
194
+ handleSelectBlock(selected.type)
195
+ }
196
+ }
197
+ }
198
+
199
+ return (
200
+ <div classList={{ hidden: !props.open }}>
201
+ <div
202
+ role="presentation"
203
+ onClick={(e) => {
204
+ if (e.target === e.currentTarget) props.onClose()
205
+ }}
206
+ class="fixed inset-0 z-[9999] flex items-center justify-center p-4 bg-black/60 backdrop-blur-sm"
207
+ >
208
+ <div class="bg-elevated border border-default rounded-2xl w-full max-w-md shadow-2xl flex flex-col overflow-hidden">
209
+ <div class="flex items-center justify-between px-4 py-3 border-b border-default">
210
+ <div class="text-sm font-semibold text-foreground">
211
+ Add Block{" "}
212
+ {props.containerType ? `to ${props.containerType.replace("Block", "")}` : ""}
213
+ </div>
214
+ <RLSButton
215
+ variant="ghost"
216
+ color="neutral"
217
+ size="xs"
218
+ square={true}
219
+ onClick={() => props.onClose()}
220
+ aria-label="Close"
221
+ leading={<RLSIcon name="close" size="xs" />}
222
+ />
223
+ </div>
224
+
225
+ <div class="px-4 py-3 border-b border-default/50">
226
+ <RLSInput
227
+ ref={(el: HTMLInputElement) => {
228
+ searchInput = el
229
+ if (props.open) setTimeout(() => searchInput?.focus(), 10)
230
+ }}
231
+ value={query()}
232
+ onInput={(e: InputEvent & { currentTarget: HTMLInputElement }) => {
233
+ setQuery(e.currentTarget.value)
234
+ setSelectedIndex(0)
235
+ }}
236
+ onKeyDown={handleKeydown}
237
+ placeholder="Search blocks..."
238
+ leadingIcon="search"
239
+ size="sm"
240
+ />
241
+ </div>
242
+
243
+ <div class="overflow-y-auto max-h-[50vh] pb-2">
244
+ {filteredBlocks().length === 0 ? (
245
+ <div class="text-center py-4 text-muted-foreground text-sm">
246
+ No compatible blocks found
247
+ </div>
248
+ ) : (
249
+ (() => {
250
+ let currentGroup = ""
251
+ return (
252
+ <For each={filteredBlocks()}>
253
+ {(block, index) => {
254
+ const showHeader = block.group !== currentGroup
255
+ if (showHeader) currentGroup = block.group
256
+ const isSelected = index() === selectedIndex()
257
+
258
+ return (
259
+ <>
260
+ {showHeader && (
261
+ <div class="text-[10px] font-bold text-muted-foreground uppercase tracking-widest px-4 py-1.5 mt-2">
262
+ {block.group}
263
+ </div>
264
+ )}
265
+ <button
266
+ type="button"
267
+ onClick={(e) => {
268
+ e.preventDefault()
269
+ e.stopPropagation()
270
+ handleSelectBlock(block.type)
271
+ }}
272
+ onMouseEnter={() => setSelectedIndex(index())}
273
+ class="cms-picker-item flex items-center gap-3 px-4 py-2.5 cursor-pointer transition-colors hover:bg-neutral-100 dark:hover:bg-neutral-800/80 w-full text-left bg-transparent border-none"
274
+ classList={{ "bg-neutral-100 dark:bg-neutral-800/80": isSelected }}
275
+ >
276
+ <div class="text-base w-8 h-8 flex items-center justify-center shrink-0 rounded-lg bg-neutral-100 dark:bg-neutral-800 text-foreground border border-default">
277
+ <RLSIcon name={block.icon} size="sm" />
278
+ </div>
279
+ <div class="flex flex-col flex-1 min-w-0">
280
+ <div class="flex items-center justify-between">
281
+ <span class="text-sm font-medium text-foreground">
282
+ {block.label}
283
+ </span>
284
+ <span class="text-[10px] font-mono text-muted-foreground bg-neutral-100 dark:bg-neutral-800 px-1.5 py-0.5 rounded border border-default/50">
285
+ {block.type}
286
+ </span>
287
+ </div>
288
+ <span class="text-xs text-muted-foreground mt-0.5 truncate">
289
+ {block.desc}
290
+ </span>
291
+ </div>
292
+ </button>
293
+ </>
294
+ )
295
+ }}
296
+ </For>
297
+ )
298
+ })()
299
+ )}
300
+ </div>
301
+ </div>
302
+ </div>
303
+ </div>
304
+ )
305
+ }
@@ -0,0 +1,131 @@
1
+ import { onMount, onCleanup, For, type Component } from "solid-js"
2
+ import type { EditorState } from "../state/editor-store"
3
+ import { RimelightBlock } from "./RimelightBlock"
4
+ import { RimelightBlockPicker } from "./RimelightBlockPicker"
5
+ import { initBlockSortable, cleanAllSortableClasses } from "../utils/sortable"
6
+ import RLSButton from "@rimelight/ui/components/button/RLSButton.tsx"
7
+
8
+ export interface RimelightEditorProps {
9
+ state: EditorState
10
+ }
11
+
12
+ export const RimelightEditor: Component<RimelightEditorProps> = (props) => {
13
+ let canvasContainer: HTMLDivElement | undefined = undefined
14
+ let sortableInstance: any = null
15
+
16
+ onMount(() => {
17
+ if (canvasContainer) {
18
+ sortableInstance = initBlockSortable(canvasContainer, {
19
+ handle: ".cms-grip-btn",
20
+ filter: ".cms-empty-state, .locked",
21
+ onReorder: (oldIdx: number, newIdx: number) => {
22
+ cleanAllSortableClasses()
23
+ const list = [...props.state.blocks]
24
+ const item = list.splice(oldIdx, 1)[0]
25
+ if (item) {
26
+ list.splice(newIdx, 0, item)
27
+ props.state.setBlocks(list)
28
+ }
29
+ requestAnimationFrame(() => cleanAllSortableClasses())
30
+ setTimeout(cleanAllSortableClasses, 50)
31
+ setTimeout(cleanAllSortableClasses, 200)
32
+ },
33
+ onAdd: (blockId: string, newIdx: number) => {
34
+ cleanAllSortableClasses()
35
+ props.state.relocateBlock(blockId, newIdx, null)
36
+ requestAnimationFrame(() => cleanAllSortableClasses())
37
+ setTimeout(cleanAllSortableClasses, 50)
38
+ setTimeout(cleanAllSortableClasses, 200)
39
+ },
40
+ onRemove: (_blockId: string, _oldIdx: number) => {
41
+ cleanAllSortableClasses()
42
+ }
43
+ })
44
+ }
45
+
46
+ const handleKeydown = (e: KeyboardEvent) => {
47
+ if ((e.ctrlKey || e.metaKey) && e.key === "z" && !e.shiftKey) {
48
+ e.preventDefault()
49
+ props.state.undo()
50
+ } else if ((e.ctrlKey || e.metaKey) && (e.key === "y" || (e.key === "z" && e.shiftKey))) {
51
+ e.preventDefault()
52
+ props.state.redo()
53
+ }
54
+ }
55
+ window.addEventListener("keydown", handleKeydown)
56
+
57
+ onCleanup(() => {
58
+ window.removeEventListener("keydown", handleKeydown)
59
+ if (sortableInstance) sortableInstance.destroy()
60
+ })
61
+ })
62
+
63
+ return (
64
+ <div class="flex flex-col gap-6 w-full bg-transparent">
65
+ <div ref={canvasContainer} class="flex flex-col gap-3 w-full min-h-[200px] pl-6">
66
+ {props.state.blocks.length === 0 ? (
67
+ <div class="flex flex-col items-center justify-center p-8 sm:p-12 rounded-xl border border-dashed border-default text-center my-4">
68
+ <div class="text-3xl text-muted-foreground mb-2">📄</div>
69
+ <h4 class="text-base font-semibold text-foreground">No blocks yet</h4>
70
+ <p class="text-xs text-muted-foreground mt-1 max-w-xs">
71
+ Start building your page content by adding your first block below.
72
+ </p>
73
+ <div class="mt-4">
74
+ <RLSButton
75
+ variant="outline"
76
+ color="neutral"
77
+ size="sm"
78
+ onClick={() => props.state.openPicker(null, "after", 0)}
79
+ leadingIcon="plus"
80
+ label="Add Block"
81
+ />
82
+ </div>
83
+ </div>
84
+ ) : (
85
+ <For each={props.state.blocks}>
86
+ {(block) => (
87
+ <RimelightBlock
88
+ block={block}
89
+ sectionDepth={0}
90
+ onPropsChange={props.state.updateBlockProps}
91
+ onMove={props.state.moveBlock}
92
+ onDelete={props.state.deleteBlock}
93
+ onDuplicate={props.state.duplicateBlock}
94
+ onAddAdjacent={(targetId, pos) => props.state.openPicker(targetId, pos, 0)}
95
+ onRelocate={props.state.relocateBlock}
96
+ onOpenPicker={(targetId, pos, depth, parentId, containerType) =>
97
+ props.state.openPicker(targetId, pos, depth, parentId, containerType)
98
+ }
99
+ />
100
+ )}
101
+ </For>
102
+ )}
103
+ </div>
104
+
105
+ {props.state.blocks.length > 0 && (
106
+ <div class="flex flex-col items-center justify-center gap-4 p-4 mt-2">
107
+ <RLSButton
108
+ variant="outline"
109
+ color="neutral"
110
+ size="sm"
111
+ onClick={() => props.state.openPicker(null, "after", 0, null, "Root")}
112
+ leadingIcon="plus"
113
+ label="Add Block"
114
+ />
115
+ </div>
116
+ )}
117
+
118
+ <RimelightBlockPicker
119
+ open={props.state.pickerTarget().open}
120
+ sectionDepth={props.state.pickerTarget().sectionDepth}
121
+ containerType={props.state.pickerTarget().containerType}
122
+ onSelect={(type) => {
123
+ const { targetId, position, parentId } = props.state.pickerTarget()
124
+ props.state.insertBlock(type, targetId, position, parentId)
125
+ props.state.closePicker()
126
+ }}
127
+ onClose={props.state.closePicker}
128
+ />
129
+ </div>
130
+ )
131
+ }
@@ -0,0 +1,143 @@
1
+ import { onMount, createEffect, createMemo, Show, type Component } from "solid-js"
2
+ import type { BaseBlock } from "../../core/types/blocks"
3
+ import { createEditorState } from "../state/editor-store"
4
+ import { RimelightEditor } from "./RimelightEditor"
5
+ import { RimelightPreview } from "./RimelightPreview"
6
+ import { initSplitPane } from "../utils/splitpane"
7
+
8
+ export interface RimelightEditorLayoutProps {
9
+ pageId?: string
10
+ initialBlocks?: BaseBlock[] | string
11
+ showInternalPreview?: boolean
12
+ ref?: (el: any) => void
13
+ }
14
+
15
+ export const RimelightEditorLayout: Component<RimelightEditorLayoutProps> = (componentProps) => {
16
+ let containerRef: HTMLDivElement | undefined = undefined
17
+ let editorPaneRef: HTMLDivElement | undefined = undefined
18
+ let previewPaneRef: HTMLDivElement | undefined = undefined
19
+ let handleRef: HTMLDivElement | undefined = undefined
20
+
21
+ let parsedInitialBlocks: BaseBlock[] = []
22
+ if (typeof componentProps.initialBlocks === "string") {
23
+ try {
24
+ parsedInitialBlocks = JSON.parse(componentProps.initialBlocks)
25
+ } catch {
26
+ parsedInitialBlocks = []
27
+ }
28
+ } else if (Array.isArray(componentProps.initialBlocks)) {
29
+ parsedInitialBlocks = componentProps.initialBlocks
30
+ }
31
+
32
+ const state = createEditorState(parsedInitialBlocks, componentProps.pageId || "default-page")
33
+
34
+ // Snapshot of blocks as plain objects for the preview.
35
+ const previewBlocks = createMemo<BaseBlock[]>(() => {
36
+ const len = state.blocks.length
37
+ const result: BaseBlock[] = []
38
+ for (let i = 0; i < len; i++) {
39
+ const blockObj = JSON.parse(JSON.stringify(state.blocks[i]))
40
+ if (blockObj && typeof blockObj === "object") {
41
+ result.push(blockObj)
42
+ }
43
+ }
44
+ return result
45
+ })
46
+
47
+ createEffect(() => {
48
+ const currentStatus = state.status()
49
+ if (typeof document !== "undefined") {
50
+ document.dispatchEvent(
51
+ new CustomEvent("status-change", { detail: { status: currentStatus }, bubbles: true })
52
+ )
53
+ }
54
+ })
55
+
56
+ createEffect(() => {
57
+ const rawBlocks = previewBlocks()
58
+ if (typeof document !== "undefined") {
59
+ document.dispatchEvent(
60
+ new CustomEvent("blocks-change", { detail: { blocks: rawBlocks }, bubbles: true })
61
+ )
62
+ }
63
+ })
64
+
65
+ onMount(() => {
66
+ const publicMethods = {
67
+ undo: () => state.undo(),
68
+ redo: () => state.redo(),
69
+ addBlock: (_type?: string) => state.openPicker(null, "after", 0),
70
+ saveDraft: () => state.saveDraft(),
71
+ togglePreview: () => {
72
+ const next = !state.previewVisible()
73
+ state.setPreviewVisible(next)
74
+ if (typeof document !== "undefined") {
75
+ document.dispatchEvent(
76
+ new CustomEvent("preview-toggle", { detail: { visible: next }, bubbles: true })
77
+ )
78
+ }
79
+ return next
80
+ },
81
+ getBlocks: () => JSON.parse(JSON.stringify(state.blocks)),
82
+ setBlocks: (newBlocks: BaseBlock[]) => state.setBlocks(newBlocks),
83
+ getAttribute: (attr: string) => (attr === "page-id" ? componentProps.pageId : null)
84
+ }
85
+
86
+ if (componentProps.ref) {
87
+ componentProps.ref(publicMethods)
88
+ }
89
+
90
+ if (typeof window !== "undefined") {
91
+ const win = window as Window & { cmsEditorLayout?: Record<string, any> }
92
+ win.cmsEditorLayout = publicMethods
93
+ }
94
+
95
+ if (containerRef) {
96
+ Object.assign(containerRef, publicMethods)
97
+ }
98
+ })
99
+
100
+ createEffect(() => {
101
+ if (componentProps.showInternalPreview && state.previewVisible()) {
102
+ queueMicrotask(() => {
103
+ if (containerRef && editorPaneRef && previewPaneRef && handleRef) {
104
+ initSplitPane(containerRef, editorPaneRef, previewPaneRef, handleRef)
105
+ }
106
+ })
107
+ }
108
+ })
109
+
110
+ return (
111
+ <div class="rimelight-cms-editor-container flex-1 w-full h-full flex">
112
+ <div
113
+ ref={containerRef}
114
+ class="cms-split-layout w-full h-full flex flex-row overflow-hidden relative"
115
+ id="cms-layout"
116
+ >
117
+ <div
118
+ ref={editorPaneRef}
119
+ style={{
120
+ flex:
121
+ componentProps.showInternalPreview && state.previewVisible() ? "0 0 50%" : "1 1 100%"
122
+ }}
123
+ class="flex flex-col h-full overflow-hidden w-full"
124
+ >
125
+ <RimelightEditor state={state} />
126
+ </div>
127
+
128
+ <Show when={componentProps.showInternalPreview && state.previewVisible()}>
129
+ <div ref={handleRef} class="cms-resize-handle" id="cms-resize-handle">
130
+ <div class="cms-resize-pill" />
131
+ </div>
132
+ <div
133
+ ref={previewPaneRef}
134
+ style={{ flex: "0 0 50%" }}
135
+ class="flex flex-col h-full overflow-hidden border-l border-gray-800"
136
+ >
137
+ <RimelightPreview blocks={previewBlocks} />
138
+ </div>
139
+ </Show>
140
+ </div>
141
+ </div>
142
+ )
143
+ }