@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,113 @@
|
|
|
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 TabItemEditorProps {
|
|
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 TabItemEditor: Component<TabItemEditorProps> = (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 saveLabel = (label: string) => {
|
|
66
|
+
componentProps.onPropsChange({
|
|
67
|
+
...componentProps.props,
|
|
68
|
+
label,
|
|
69
|
+
value: componentProps.props.value || label.toLowerCase().replace(/\s+/g, "-")
|
|
70
|
+
})
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return (
|
|
74
|
+
<div class="rimelight-tab-item-editor flex flex-col gap-2 p-3 rounded-lg border border-default bg-muted/10 w-full">
|
|
75
|
+
<div class="flex items-center gap-2">
|
|
76
|
+
<span class="text-xs font-semibold text-muted-foreground uppercase">Tab Label:</span>
|
|
77
|
+
<div class="flex-1 max-w-xs">
|
|
78
|
+
<RLSInput
|
|
79
|
+
type="text"
|
|
80
|
+
placeholder="e.g. pnpm, Astro, TypeScript"
|
|
81
|
+
value={componentProps.props.label || ""}
|
|
82
|
+
onBlur={(e) => saveLabel(e.currentTarget.value)}
|
|
83
|
+
size="sm"
|
|
84
|
+
/>
|
|
85
|
+
</div>
|
|
86
|
+
</div>
|
|
87
|
+
|
|
88
|
+
<div
|
|
89
|
+
ref={(el) => (childrenContainer = el)}
|
|
90
|
+
class="p-2 rounded-lg border border-dashed border-default flex flex-col gap-2 min-h-[36px] bg-background"
|
|
91
|
+
>
|
|
92
|
+
{childrenBlocks().length === 0 ? (
|
|
93
|
+
<div class="text-center py-2">
|
|
94
|
+
<span class="text-xs text-muted-foreground block">Empty tab content</span>
|
|
95
|
+
<RLSButton
|
|
96
|
+
variant="link"
|
|
97
|
+
color="primary"
|
|
98
|
+
size="xs"
|
|
99
|
+
onClick={() => componentProps.onOpenPicker?.()}
|
|
100
|
+
label="+ Add block"
|
|
101
|
+
/>
|
|
102
|
+
</div>
|
|
103
|
+
) : (
|
|
104
|
+
<For each={childrenBlocks()}>
|
|
105
|
+
{(childBlock) =>
|
|
106
|
+
componentProps.renderChildBlock?.(childBlock, componentProps.sectionDepth || 0)
|
|
107
|
+
}
|
|
108
|
+
</For>
|
|
109
|
+
)}
|
|
110
|
+
</div>
|
|
111
|
+
</div>
|
|
112
|
+
)
|
|
113
|
+
}
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import { For, type Component } from "solid-js"
|
|
2
|
+
import type { TableColumnDef } from "../../../core/types/blocks"
|
|
3
|
+
import RLSInput from "@rimelight/ui/components/input/RLSInput.tsx"
|
|
4
|
+
import RLSButton from "@rimelight/ui/components/button/RLSButton.tsx"
|
|
5
|
+
|
|
6
|
+
export interface TableEditorProps {
|
|
7
|
+
props: Record<string, any>
|
|
8
|
+
onPropsChange: (newProps: Record<string, any>) => void
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const TableEditor: Component<TableEditorProps> = (componentProps) => {
|
|
12
|
+
const columns = (): TableColumnDef[] =>
|
|
13
|
+
componentProps.props.columns || [
|
|
14
|
+
{ key: "col1", header: "Header 1", align: "left" },
|
|
15
|
+
{ key: "col2", header: "Header 2", align: "left" }
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
const rows = (): Record<string, string>[] =>
|
|
19
|
+
componentProps.props.rows || [{ col1: "Cell 1", col2: "Cell 2" }]
|
|
20
|
+
|
|
21
|
+
const save = (newCols: TableColumnDef[], newRows: Record<string, string>[], caption?: string) => {
|
|
22
|
+
componentProps.onPropsChange({
|
|
23
|
+
...componentProps.props,
|
|
24
|
+
columns: newCols,
|
|
25
|
+
rows: newRows,
|
|
26
|
+
caption: caption !== undefined ? caption : componentProps.props.caption || ""
|
|
27
|
+
})
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const addColumn = () => {
|
|
31
|
+
const nextIdx = columns().length + 1
|
|
32
|
+
const key = `col${nextIdx}`
|
|
33
|
+
const newCols = [...columns(), { key, header: `Header ${nextIdx}`, align: "left" as const }]
|
|
34
|
+
const newRows = rows().map((r) => ({ ...r, [key]: "" }))
|
|
35
|
+
save(newCols, newRows)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const removeColumn = (colIdx: number) => {
|
|
39
|
+
if (columns().length <= 1) return
|
|
40
|
+
const targetKey = columns()[colIdx]!.key
|
|
41
|
+
const newCols = columns().filter((_, i) => i !== colIdx)
|
|
42
|
+
const newRows = rows().map((r) => {
|
|
43
|
+
const next = { ...r }
|
|
44
|
+
delete next[targetKey]
|
|
45
|
+
return next
|
|
46
|
+
})
|
|
47
|
+
save(newCols, newRows)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const addRow = () => {
|
|
51
|
+
const newRow: Record<string, string> = {}
|
|
52
|
+
for (const col of columns()) {
|
|
53
|
+
newRow[col.key] = ""
|
|
54
|
+
}
|
|
55
|
+
save(columns(), [...rows(), newRow])
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const removeRow = (rowIdx: number) => {
|
|
59
|
+
if (rows().length <= 1) return
|
|
60
|
+
save(
|
|
61
|
+
columns(),
|
|
62
|
+
rows().filter((_, i) => i !== rowIdx)
|
|
63
|
+
)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const updateHeader = (colIdx: number, val: string) => {
|
|
67
|
+
const newCols = [...columns()]
|
|
68
|
+
newCols[colIdx] = { ...newCols[colIdx]!, header: val }
|
|
69
|
+
save(newCols, rows())
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const updateCell = (rowIdx: number, colKey: string, val: string) => {
|
|
73
|
+
const newRows = [...rows()]
|
|
74
|
+
newRows[rowIdx] = { ...newRows[rowIdx], [colKey]: val }
|
|
75
|
+
save(columns(), newRows)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return (
|
|
79
|
+
<div class="rimelight-table-editor flex flex-col gap-3 p-3 rounded-xl border border-default/70 w-full overflow-x-auto">
|
|
80
|
+
<div class="flex items-center justify-between">
|
|
81
|
+
<span class="text-xs font-bold uppercase tracking-wider text-muted-foreground">Table</span>
|
|
82
|
+
<div class="flex items-center gap-2">
|
|
83
|
+
<RLSButton
|
|
84
|
+
variant="outline"
|
|
85
|
+
color="neutral"
|
|
86
|
+
size="xs"
|
|
87
|
+
onClick={addColumn}
|
|
88
|
+
leadingIcon="plus"
|
|
89
|
+
label="Add Column"
|
|
90
|
+
/>
|
|
91
|
+
<RLSButton
|
|
92
|
+
variant="outline"
|
|
93
|
+
color="neutral"
|
|
94
|
+
size="xs"
|
|
95
|
+
onClick={addRow}
|
|
96
|
+
leadingIcon="plus"
|
|
97
|
+
label="Add Row"
|
|
98
|
+
/>
|
|
99
|
+
</div>
|
|
100
|
+
</div>
|
|
101
|
+
|
|
102
|
+
<div class="overflow-x-auto border border-default rounded-lg">
|
|
103
|
+
<table class="w-full text-xs border-collapse">
|
|
104
|
+
<thead>
|
|
105
|
+
<tr class="bg-muted/30 border-b border-default">
|
|
106
|
+
<For each={columns()}>
|
|
107
|
+
{(col, colIdx) => (
|
|
108
|
+
<th class="p-1 min-w-[120px] font-medium text-left">
|
|
109
|
+
<div class="flex items-center gap-1">
|
|
110
|
+
<input
|
|
111
|
+
type="text"
|
|
112
|
+
aria-label={`Column ${colIdx() + 1} header`}
|
|
113
|
+
class="w-full bg-transparent border border-transparent hover:border-default focus:border-primary rounded px-2 py-1 font-semibold outline-none text-foreground"
|
|
114
|
+
value={col.header}
|
|
115
|
+
onBlur={(e) => updateHeader(colIdx(), e.currentTarget.value)}
|
|
116
|
+
/>
|
|
117
|
+
{columns().length > 1 && (
|
|
118
|
+
<button
|
|
119
|
+
type="button"
|
|
120
|
+
aria-label="Delete column"
|
|
121
|
+
class="text-muted-foreground hover:text-danger p-1 text-xs"
|
|
122
|
+
onClick={() => removeColumn(colIdx())}
|
|
123
|
+
title="Delete column"
|
|
124
|
+
>
|
|
125
|
+
✕
|
|
126
|
+
</button>
|
|
127
|
+
)}
|
|
128
|
+
</div>
|
|
129
|
+
</th>
|
|
130
|
+
)}
|
|
131
|
+
</For>
|
|
132
|
+
<th class="w-8 p-1" aria-label="Row actions"></th>
|
|
133
|
+
</tr>
|
|
134
|
+
</thead>
|
|
135
|
+
<tbody>
|
|
136
|
+
<For each={rows()}>
|
|
137
|
+
{(row, rowIdx) => (
|
|
138
|
+
<tr class="border-b border-default/50 hover:bg-muted/20">
|
|
139
|
+
<For each={columns()}>
|
|
140
|
+
{(col) => (
|
|
141
|
+
<td class="p-1">
|
|
142
|
+
<input
|
|
143
|
+
type="text"
|
|
144
|
+
aria-label={`${col.header} row ${rowIdx() + 1}`}
|
|
145
|
+
class="w-full bg-transparent border border-transparent hover:border-default focus:border-primary rounded px-2 py-1 outline-none text-foreground"
|
|
146
|
+
value={row[col.key] || ""}
|
|
147
|
+
onBlur={(e) => updateCell(rowIdx(), col.key, e.currentTarget.value)}
|
|
148
|
+
/>
|
|
149
|
+
</td>
|
|
150
|
+
)}
|
|
151
|
+
</For>
|
|
152
|
+
<td class="p-1 text-center">
|
|
153
|
+
{rows().length > 1 && (
|
|
154
|
+
<button
|
|
155
|
+
type="button"
|
|
156
|
+
aria-label="Delete row"
|
|
157
|
+
class="text-muted-foreground hover:text-danger p-1 text-xs"
|
|
158
|
+
onClick={() => removeRow(rowIdx())}
|
|
159
|
+
title="Delete row"
|
|
160
|
+
>
|
|
161
|
+
✕
|
|
162
|
+
</button>
|
|
163
|
+
)}
|
|
164
|
+
</td>
|
|
165
|
+
</tr>
|
|
166
|
+
)}
|
|
167
|
+
</For>
|
|
168
|
+
</tbody>
|
|
169
|
+
</table>
|
|
170
|
+
</div>
|
|
171
|
+
|
|
172
|
+
<RLSInput
|
|
173
|
+
type="text"
|
|
174
|
+
placeholder="Table caption (optional)"
|
|
175
|
+
value={componentProps.props.caption || ""}
|
|
176
|
+
onBlur={(e) => save(columns(), rows(), e.currentTarget.value)}
|
|
177
|
+
size="sm"
|
|
178
|
+
/>
|
|
179
|
+
</div>
|
|
180
|
+
)
|
|
181
|
+
}
|
|
@@ -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 TabsEditorProps {
|
|
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 TabsEditor: Component<TabsEditorProps> = (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-tabs-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
|
+
Tabs Container
|
|
69
|
+
</span>
|
|
70
|
+
<RLSButton
|
|
71
|
+
variant="outline"
|
|
72
|
+
color="neutral"
|
|
73
|
+
size="xs"
|
|
74
|
+
onClick={() => componentProps.onOpenPicker?.()}
|
|
75
|
+
leadingIcon="plus"
|
|
76
|
+
label="Add Tab 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 tabs in container</span>
|
|
87
|
+
<RLSButton
|
|
88
|
+
variant="link"
|
|
89
|
+
color="primary"
|
|
90
|
+
size="xs"
|
|
91
|
+
onClick={() => componentProps.onOpenPicker?.()}
|
|
92
|
+
label="+ Add Tab 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
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export * from "./RimelightBlock"
|
|
2
|
+
export * from "./RimelightBlockPicker"
|
|
3
|
+
export * from "./RimelightEditor"
|
|
4
|
+
export * from "./RimelightEditorLayout"
|
|
5
|
+
export * from "./RimelightPreview"
|
|
6
|
+
export * from "./RimelightPropertiesPanel"
|
|
7
|
+
export * from "./RimelightTemplateEditor"
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import "./styles/cms-editor.css"
|
|
2
|
+
|
|
3
|
+
// SolidJS components
|
|
4
|
+
export { RimelightEditorLayout } from "./components/RimelightEditorLayout"
|
|
5
|
+
export { RimelightEditor } from "./components/RimelightEditor"
|
|
6
|
+
export { RimelightBlock } from "./components/RimelightBlock"
|
|
7
|
+
export { RimelightPreview } from "./components/RimelightPreview"
|
|
8
|
+
export { RimelightPropertiesPanel } from "./components/RimelightPropertiesPanel"
|
|
9
|
+
export { RimelightBlockPicker } from "./components/RimelightBlockPicker"
|
|
10
|
+
export { RimelightTemplateEditor } from "./components/RimelightTemplateEditor"
|
|
11
|
+
|
|
12
|
+
export { ParagraphEditor } from "./components/editors/ParagraphEditor"
|
|
13
|
+
export { CalloutEditor } from "./components/editors/CalloutEditor"
|
|
14
|
+
export { CodeEditor } from "./components/editors/CodeEditor"
|
|
15
|
+
export { DialogueEditor } from "./components/editors/DialogueEditor"
|
|
16
|
+
export { ImageEditor } from "./components/editors/ImageEditor"
|
|
17
|
+
export { SceneEditor } from "./components/editors/SceneEditor"
|
|
18
|
+
export { ScriptEditor } from "./components/editors/ScriptEditor"
|
|
19
|
+
export { SectionEditor } from "./components/editors/SectionEditor"
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
export type AutosaveStatus = "saved" | "unsaved" | "saving" | "error"
|
|
2
|
+
|
|
3
|
+
export interface AutosaveOptions {
|
|
4
|
+
pageId: string
|
|
5
|
+
saveEndpoint?: string
|
|
6
|
+
debounceMs?: number
|
|
7
|
+
onStatusChange?: ((status: AutosaveStatus, errorMsg?: string) => void) | undefined
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export class AutosaveController {
|
|
11
|
+
private pageId: string
|
|
12
|
+
private saveEndpoint: string
|
|
13
|
+
private debounceMs: number
|
|
14
|
+
private timer: number | null = null
|
|
15
|
+
private status: AutosaveStatus = "saved"
|
|
16
|
+
private lastContent: any = null
|
|
17
|
+
private onStatusChange?: ((status: AutosaveStatus, errorMsg?: string) => void) | undefined
|
|
18
|
+
|
|
19
|
+
constructor(options: AutosaveOptions) {
|
|
20
|
+
this.pageId = options.pageId
|
|
21
|
+
this.saveEndpoint = options.saveEndpoint || `/api/cms/pages/${this.pageId}/draft`
|
|
22
|
+
this.debounceMs = options.debounceMs || 500
|
|
23
|
+
this.onStatusChange = options.onStatusChange
|
|
24
|
+
|
|
25
|
+
if (typeof window !== "undefined") {
|
|
26
|
+
window.addEventListener("beforeunload", () => this.flushSync())
|
|
27
|
+
document.addEventListener("visibilitychange", () => {
|
|
28
|
+
if (document.visibilityState === "hidden") {
|
|
29
|
+
this.flushSync()
|
|
30
|
+
}
|
|
31
|
+
})
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
getPageId(): string {
|
|
36
|
+
return this.pageId
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
notifyChange(content: any): void {
|
|
40
|
+
this.lastContent = content
|
|
41
|
+
this.saveToLocalStorage(content)
|
|
42
|
+
this.setStatus("unsaved")
|
|
43
|
+
|
|
44
|
+
if (this.timer !== null) {
|
|
45
|
+
clearTimeout(this.timer)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
this.timer = window.setTimeout(() => {
|
|
49
|
+
void this.save(content)
|
|
50
|
+
}, this.debounceMs)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async saveImmediate(content: any): Promise<boolean> {
|
|
54
|
+
this.lastContent = content
|
|
55
|
+
this.saveToLocalStorage(content)
|
|
56
|
+
if (this.timer !== null) {
|
|
57
|
+
clearTimeout(this.timer)
|
|
58
|
+
this.timer = null
|
|
59
|
+
}
|
|
60
|
+
return this.save(content)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async save(content: any): Promise<boolean> {
|
|
64
|
+
this.setStatus("saving")
|
|
65
|
+
try {
|
|
66
|
+
const res = await fetch(this.saveEndpoint, {
|
|
67
|
+
method: "POST",
|
|
68
|
+
headers: { "Content-Type": "application/json" },
|
|
69
|
+
body: JSON.stringify({ content })
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
if (res.ok) {
|
|
73
|
+
this.setStatus("saved")
|
|
74
|
+
return true
|
|
75
|
+
} else {
|
|
76
|
+
const data = await res.json().catch(() => ({}))
|
|
77
|
+
this.setStatus("error", data.message || "Failed to save draft")
|
|
78
|
+
return false
|
|
79
|
+
}
|
|
80
|
+
} catch (err: any) {
|
|
81
|
+
this.setStatus("error", err?.message || "Network error")
|
|
82
|
+
return false
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
private saveToLocalStorage(content: any): void {
|
|
87
|
+
if (typeof localStorage === "undefined" || !this.pageId) return
|
|
88
|
+
try {
|
|
89
|
+
localStorage.setItem(`cms_draft_backup_${this.pageId}`, JSON.stringify(content))
|
|
90
|
+
} catch (e) {
|
|
91
|
+
console.warn("Failed to write draft backup to localStorage", e)
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
private flushSync(): void {
|
|
96
|
+
if (this.status === "unsaved" && this.lastContent) {
|
|
97
|
+
if (this.timer !== null) {
|
|
98
|
+
clearTimeout(this.timer)
|
|
99
|
+
this.timer = null
|
|
100
|
+
}
|
|
101
|
+
try {
|
|
102
|
+
void fetch(this.saveEndpoint, {
|
|
103
|
+
method: "POST",
|
|
104
|
+
headers: { "Content-Type": "application/json" },
|
|
105
|
+
body: JSON.stringify({ content: this.lastContent }),
|
|
106
|
+
keepalive: true
|
|
107
|
+
})
|
|
108
|
+
} catch {}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
getStatus(): AutosaveStatus {
|
|
113
|
+
return this.status
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
private setStatus(status: AutosaveStatus, errorMsg?: string) {
|
|
117
|
+
this.status = status
|
|
118
|
+
if (this.onStatusChange) {
|
|
119
|
+
this.onStatusChange(status, errorMsg)
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|