@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,185 @@
|
|
|
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 RLSSelect from "@rimelight/ui/components/select/RLSSelect.tsx"
|
|
5
|
+
import RLSInput from "@rimelight/ui/components/input/RLSInput.tsx"
|
|
6
|
+
import RLSButton from "@rimelight/ui/components/button/RLSButton.tsx"
|
|
7
|
+
|
|
8
|
+
export interface CalloutEditorProps {
|
|
9
|
+
blockId?: string
|
|
10
|
+
props: Record<string, any>
|
|
11
|
+
children?: BaseBlock[] | (() => BaseBlock[])
|
|
12
|
+
sectionDepth?: number
|
|
13
|
+
onPropsChange: (newProps: Record<string, any>) => void
|
|
14
|
+
onRelocate?: ((id: string, newIndex: number, targetParentId?: string | null) => void) | undefined
|
|
15
|
+
onOpenPicker?: () => void
|
|
16
|
+
renderChildBlock?: (block: BaseBlock, depth: number) => any
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const VARIANTS: Record<string, { icon: string; label: string }> = {
|
|
20
|
+
info: { icon: "info", label: "Info" },
|
|
21
|
+
success: { icon: "check", label: "Success" },
|
|
22
|
+
warning: { icon: "triangleAlert", label: "Warning" },
|
|
23
|
+
error: { icon: "circleAlert", label: "Error" },
|
|
24
|
+
commentary: { icon: "messageSquare", label: "Commentary" },
|
|
25
|
+
ideation: { icon: "lightbulb", label: "Ideation" },
|
|
26
|
+
source: { icon: "code", label: "Source" }
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const VARIANT_OPTIONS = Object.entries(VARIANTS).map(([key, item]) => ({
|
|
30
|
+
label: item.label,
|
|
31
|
+
value: key
|
|
32
|
+
}))
|
|
33
|
+
|
|
34
|
+
const TARGET_OPTIONS = [
|
|
35
|
+
{ label: "Same tab", value: "_self" },
|
|
36
|
+
{ label: "New tab (_blank)", value: "_blank" }
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
export const CalloutEditor: Component<CalloutEditorProps> = (componentProps) => {
|
|
40
|
+
const [linkSettingsOpen, setLinkSettingsOpen] = createSignal(!!componentProps.props.to)
|
|
41
|
+
let childrenContainer: HTMLDivElement | undefined = undefined
|
|
42
|
+
let sortableInstance: any = null
|
|
43
|
+
|
|
44
|
+
const variant = () => componentProps.props.variant || "info"
|
|
45
|
+
|
|
46
|
+
const childrenBlocks = (): BaseBlock[] => {
|
|
47
|
+
if (typeof componentProps.children === "function") {
|
|
48
|
+
return componentProps.children() || []
|
|
49
|
+
}
|
|
50
|
+
return componentProps.children || componentProps.props.children || []
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
onMount(() => {
|
|
54
|
+
if (childrenContainer) {
|
|
55
|
+
sortableInstance = initBlockSortable(childrenContainer, {
|
|
56
|
+
filter: ".cms-empty-placeholder, .locked",
|
|
57
|
+
onReorder: (oldIdx: number, newIdx: number) => {
|
|
58
|
+
cleanAllSortableClasses()
|
|
59
|
+
const list = [...childrenBlocks()]
|
|
60
|
+
const item = list.splice(oldIdx, 1)[0]
|
|
61
|
+
if (item) {
|
|
62
|
+
list.splice(newIdx, 0, item)
|
|
63
|
+
componentProps.onPropsChange({ ...componentProps.props, children: list })
|
|
64
|
+
}
|
|
65
|
+
requestAnimationFrame(() => cleanAllSortableClasses())
|
|
66
|
+
setTimeout(cleanAllSortableClasses, 50)
|
|
67
|
+
setTimeout(cleanAllSortableClasses, 200)
|
|
68
|
+
},
|
|
69
|
+
onAdd: (blockId: string, newIdx: number) => {
|
|
70
|
+
cleanAllSortableClasses()
|
|
71
|
+
if (componentProps.onRelocate && componentProps.blockId) {
|
|
72
|
+
componentProps.onRelocate(blockId, newIdx, componentProps.blockId)
|
|
73
|
+
}
|
|
74
|
+
requestAnimationFrame(() => cleanAllSortableClasses())
|
|
75
|
+
setTimeout(cleanAllSortableClasses, 50)
|
|
76
|
+
setTimeout(cleanAllSortableClasses, 200)
|
|
77
|
+
},
|
|
78
|
+
onRemove: (_blockId: string, _oldIdx: number) => {
|
|
79
|
+
cleanAllSortableClasses()
|
|
80
|
+
}
|
|
81
|
+
})
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
onCleanup(() => {
|
|
85
|
+
if (sortableInstance) sortableInstance.destroy()
|
|
86
|
+
})
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
const saveLinkProps = (to?: string, target?: string) => {
|
|
90
|
+
componentProps.onPropsChange({
|
|
91
|
+
...componentProps.props,
|
|
92
|
+
to: to !== undefined ? to : componentProps.props.to,
|
|
93
|
+
target: target !== undefined ? target : componentProps.props.target
|
|
94
|
+
})
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const setVariant = (v: string) => {
|
|
98
|
+
componentProps.onPropsChange({
|
|
99
|
+
...componentProps.props,
|
|
100
|
+
variant: v
|
|
101
|
+
})
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return (
|
|
105
|
+
<div class="w-full flex flex-col gap-3 p-3 rounded-xl border border-default/70">
|
|
106
|
+
<div class="flex items-center justify-between gap-2">
|
|
107
|
+
<div class="flex items-center gap-2">
|
|
108
|
+
<span class="text-xs font-bold uppercase tracking-wider text-muted-foreground">
|
|
109
|
+
Callout:
|
|
110
|
+
</span>
|
|
111
|
+
<div class="w-36">
|
|
112
|
+
<RLSSelect
|
|
113
|
+
items={VARIANT_OPTIONS}
|
|
114
|
+
value={variant()}
|
|
115
|
+
onChange={(e: Event & { currentTarget: HTMLSelectElement }) =>
|
|
116
|
+
setVariant(e.currentTarget.value)
|
|
117
|
+
}
|
|
118
|
+
size="xs"
|
|
119
|
+
/>
|
|
120
|
+
</div>
|
|
121
|
+
</div>
|
|
122
|
+
|
|
123
|
+
<RLSButton
|
|
124
|
+
variant="ghost"
|
|
125
|
+
color="neutral"
|
|
126
|
+
size="xs"
|
|
127
|
+
onClick={() => setLinkSettingsOpen(!linkSettingsOpen())}
|
|
128
|
+
title="Link settings"
|
|
129
|
+
leadingIcon="link"
|
|
130
|
+
label={componentProps.props.to ? "Linked" : "Add Link"}
|
|
131
|
+
/>
|
|
132
|
+
</div>
|
|
133
|
+
|
|
134
|
+
<Show when={linkSettingsOpen()}>
|
|
135
|
+
<div class="flex items-center gap-2 p-2 rounded-lg border border-default text-xs">
|
|
136
|
+
<div class="flex-1">
|
|
137
|
+
<RLSInput
|
|
138
|
+
type="text"
|
|
139
|
+
placeholder="Link URL (e.g. https://... or /slug)"
|
|
140
|
+
value={componentProps.props.to || ""}
|
|
141
|
+
onBlur={(e: FocusEvent & { currentTarget: HTMLInputElement }) =>
|
|
142
|
+
saveLinkProps(e.currentTarget.value, componentProps.props.target)
|
|
143
|
+
}
|
|
144
|
+
size="sm"
|
|
145
|
+
/>
|
|
146
|
+
</div>
|
|
147
|
+
<div class="w-32">
|
|
148
|
+
<RLSSelect
|
|
149
|
+
items={TARGET_OPTIONS}
|
|
150
|
+
value={componentProps.props.target || "_self"}
|
|
151
|
+
onChange={(e: Event & { currentTarget: HTMLSelectElement }) =>
|
|
152
|
+
saveLinkProps(componentProps.props.to, e.currentTarget.value)
|
|
153
|
+
}
|
|
154
|
+
size="xs"
|
|
155
|
+
/>
|
|
156
|
+
</div>
|
|
157
|
+
</div>
|
|
158
|
+
</Show>
|
|
159
|
+
|
|
160
|
+
<div
|
|
161
|
+
ref={(el) => (childrenContainer = el)}
|
|
162
|
+
class="p-2 rounded-lg border border-dashed border-default flex flex-col gap-2 min-h-[36px]"
|
|
163
|
+
>
|
|
164
|
+
{childrenBlocks().length === 0 ? (
|
|
165
|
+
<div class="text-center py-2">
|
|
166
|
+
<span class="text-xs text-muted-foreground block">Empty callout</span>
|
|
167
|
+
<RLSButton
|
|
168
|
+
variant="link"
|
|
169
|
+
color="primary"
|
|
170
|
+
size="xs"
|
|
171
|
+
onClick={() => componentProps.onOpenPicker?.()}
|
|
172
|
+
label="+ Add block"
|
|
173
|
+
/>
|
|
174
|
+
</div>
|
|
175
|
+
) : (
|
|
176
|
+
<For each={childrenBlocks()}>
|
|
177
|
+
{(childBlock) =>
|
|
178
|
+
componentProps.renderChildBlock?.(childBlock, componentProps.sectionDepth || 0)
|
|
179
|
+
}
|
|
180
|
+
</For>
|
|
181
|
+
)}
|
|
182
|
+
</div>
|
|
183
|
+
</div>
|
|
184
|
+
)
|
|
185
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { type Component } from "solid-js"
|
|
2
|
+
import RLSInput from "@rimelight/ui/components/input/RLSInput.tsx"
|
|
3
|
+
|
|
4
|
+
export interface CardEditorProps {
|
|
5
|
+
props: Record<string, any>
|
|
6
|
+
onPropsChange: (newProps: Record<string, any>) => void
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export const CardEditor: Component<CardEditorProps> = (componentProps) => {
|
|
10
|
+
const save = (field: string, value: string) => {
|
|
11
|
+
componentProps.onPropsChange({
|
|
12
|
+
...componentProps.props,
|
|
13
|
+
[field]: value
|
|
14
|
+
})
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
return (
|
|
18
|
+
<div class="rimelight-card-editor flex flex-col gap-2 p-3 rounded-xl border border-default bg-muted/10 w-full">
|
|
19
|
+
<div class="grid grid-cols-2 gap-2">
|
|
20
|
+
<RLSInput
|
|
21
|
+
type="text"
|
|
22
|
+
placeholder="Card Title"
|
|
23
|
+
value={componentProps.props.title || ""}
|
|
24
|
+
onBlur={(e) => save("title", e.currentTarget.value)}
|
|
25
|
+
size="sm"
|
|
26
|
+
/>
|
|
27
|
+
<RLSInput
|
|
28
|
+
type="text"
|
|
29
|
+
placeholder="Link (href e.g. /docs/getting-started)"
|
|
30
|
+
value={componentProps.props.href || ""}
|
|
31
|
+
onBlur={(e) => save("href", e.currentTarget.value)}
|
|
32
|
+
size="sm"
|
|
33
|
+
/>
|
|
34
|
+
</div>
|
|
35
|
+
<RLSInput
|
|
36
|
+
type="text"
|
|
37
|
+
placeholder="Card Description"
|
|
38
|
+
value={componentProps.props.description || ""}
|
|
39
|
+
onBlur={(e) => save("description", e.currentTarget.value)}
|
|
40
|
+
size="sm"
|
|
41
|
+
/>
|
|
42
|
+
<div class="grid grid-cols-2 gap-2">
|
|
43
|
+
<RLSInput
|
|
44
|
+
type="text"
|
|
45
|
+
placeholder="Icon (e.g. i-lucide-book-open)"
|
|
46
|
+
value={componentProps.props.icon || ""}
|
|
47
|
+
onBlur={(e) => save("icon", e.currentTarget.value)}
|
|
48
|
+
size="sm"
|
|
49
|
+
/>
|
|
50
|
+
<RLSInput
|
|
51
|
+
type="text"
|
|
52
|
+
placeholder="Badge (optional e.g. New, Beta)"
|
|
53
|
+
value={componentProps.props.badge || ""}
|
|
54
|
+
onBlur={(e) => save("badge", e.currentTarget.value)}
|
|
55
|
+
size="sm"
|
|
56
|
+
/>
|
|
57
|
+
</div>
|
|
58
|
+
</div>
|
|
59
|
+
)
|
|
60
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
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 RLSSelect from "@rimelight/ui/components/select/RLSSelect.tsx"
|
|
5
|
+
import RLSButton from "@rimelight/ui/components/button/RLSButton.tsx"
|
|
6
|
+
|
|
7
|
+
export interface CardsEditorProps {
|
|
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
|
+
const COLUMN_OPTIONS = [
|
|
19
|
+
{ label: "1 Column", value: "1" },
|
|
20
|
+
{ label: "2 Columns", value: "2" },
|
|
21
|
+
{ label: "3 Columns", value: "3" },
|
|
22
|
+
{ label: "4 Columns", value: "4" }
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
export const CardsEditor: Component<CardsEditorProps> = (componentProps) => {
|
|
26
|
+
let childrenContainer: HTMLDivElement | undefined = undefined
|
|
27
|
+
let sortableInstance: any = null
|
|
28
|
+
|
|
29
|
+
const childrenBlocks = (): BaseBlock[] => {
|
|
30
|
+
if (typeof componentProps.children === "function") {
|
|
31
|
+
return componentProps.children() || []
|
|
32
|
+
}
|
|
33
|
+
return componentProps.children || componentProps.props.children || []
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
onMount(() => {
|
|
37
|
+
if (childrenContainer) {
|
|
38
|
+
sortableInstance = initBlockSortable(childrenContainer, {
|
|
39
|
+
filter: ".cms-empty-placeholder, .locked",
|
|
40
|
+
onReorder: (oldIdx: number, newIdx: number) => {
|
|
41
|
+
cleanAllSortableClasses()
|
|
42
|
+
const list = [...childrenBlocks()]
|
|
43
|
+
const item = list.splice(oldIdx, 1)[0]
|
|
44
|
+
if (item) {
|
|
45
|
+
list.splice(newIdx, 0, item)
|
|
46
|
+
componentProps.onPropsChange({ ...componentProps.props, children: list })
|
|
47
|
+
}
|
|
48
|
+
requestAnimationFrame(() => cleanAllSortableClasses())
|
|
49
|
+
setTimeout(cleanAllSortableClasses, 50)
|
|
50
|
+
setTimeout(cleanAllSortableClasses, 200)
|
|
51
|
+
},
|
|
52
|
+
onAdd: (blockId: string, newIdx: number) => {
|
|
53
|
+
cleanAllSortableClasses()
|
|
54
|
+
if (componentProps.onRelocate && componentProps.blockId) {
|
|
55
|
+
componentProps.onRelocate(blockId, newIdx, componentProps.blockId)
|
|
56
|
+
}
|
|
57
|
+
requestAnimationFrame(() => cleanAllSortableClasses())
|
|
58
|
+
setTimeout(cleanAllSortableClasses, 50)
|
|
59
|
+
setTimeout(cleanAllSortableClasses, 200)
|
|
60
|
+
},
|
|
61
|
+
onRemove: (_blockId: string, _oldIdx: number) => {
|
|
62
|
+
cleanAllSortableClasses()
|
|
63
|
+
}
|
|
64
|
+
})
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
onCleanup(() => {
|
|
68
|
+
if (sortableInstance) sortableInstance.destroy()
|
|
69
|
+
})
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
const setColumns = (colStr: string) => {
|
|
73
|
+
componentProps.onPropsChange({
|
|
74
|
+
...componentProps.props,
|
|
75
|
+
columns: Number.parseInt(colStr, 10) || 2
|
|
76
|
+
})
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return (
|
|
80
|
+
<div class="rimelight-cards-editor flex flex-col gap-3 p-3 rounded-xl border border-default/70 w-full">
|
|
81
|
+
<div class="flex items-center justify-between">
|
|
82
|
+
<div class="flex items-center gap-2">
|
|
83
|
+
<span class="text-xs font-bold uppercase tracking-wider text-muted-foreground">
|
|
84
|
+
Cards Grid
|
|
85
|
+
</span>
|
|
86
|
+
<div class="w-32">
|
|
87
|
+
<RLSSelect
|
|
88
|
+
items={COLUMN_OPTIONS}
|
|
89
|
+
value={String(componentProps.props.columns || 2)}
|
|
90
|
+
onChange={(e: Event & { currentTarget: HTMLSelectElement }) =>
|
|
91
|
+
setColumns(e.currentTarget.value)
|
|
92
|
+
}
|
|
93
|
+
size="xs"
|
|
94
|
+
/>
|
|
95
|
+
</div>
|
|
96
|
+
</div>
|
|
97
|
+
<RLSButton
|
|
98
|
+
variant="outline"
|
|
99
|
+
color="neutral"
|
|
100
|
+
size="xs"
|
|
101
|
+
onClick={() => componentProps.onOpenPicker?.()}
|
|
102
|
+
leadingIcon="plus"
|
|
103
|
+
label="Add Card"
|
|
104
|
+
/>
|
|
105
|
+
</div>
|
|
106
|
+
|
|
107
|
+
<div
|
|
108
|
+
ref={(el) => (childrenContainer = el)}
|
|
109
|
+
class="p-2 rounded-lg border border-dashed border-default flex flex-col gap-2 min-h-[36px]"
|
|
110
|
+
>
|
|
111
|
+
{childrenBlocks().length === 0 ? (
|
|
112
|
+
<div class="text-center py-3">
|
|
113
|
+
<span class="text-xs text-muted-foreground block mb-2">No cards in grid</span>
|
|
114
|
+
<RLSButton
|
|
115
|
+
variant="link"
|
|
116
|
+
color="primary"
|
|
117
|
+
size="xs"
|
|
118
|
+
onClick={() => componentProps.onOpenPicker?.()}
|
|
119
|
+
label="+ Add Card"
|
|
120
|
+
/>
|
|
121
|
+
</div>
|
|
122
|
+
) : (
|
|
123
|
+
<For each={childrenBlocks()}>
|
|
124
|
+
{(childBlock) =>
|
|
125
|
+
componentProps.renderChildBlock?.(childBlock, componentProps.sectionDepth || 0)
|
|
126
|
+
}
|
|
127
|
+
</For>
|
|
128
|
+
)}
|
|
129
|
+
</div>
|
|
130
|
+
</div>
|
|
131
|
+
)
|
|
132
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { onMount, type Component } from "solid-js"
|
|
2
|
+
import RLSSelect from "@rimelight/ui/components/select/RLSSelect.tsx"
|
|
3
|
+
import RLSInput from "@rimelight/ui/components/input/RLSInput.tsx"
|
|
4
|
+
|
|
5
|
+
export interface CodeEditorProps {
|
|
6
|
+
props: Record<string, any>
|
|
7
|
+
onPropsChange: (newProps: Record<string, any>) => void
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const LANGUAGES = [
|
|
11
|
+
"typescript",
|
|
12
|
+
"javascript",
|
|
13
|
+
"python",
|
|
14
|
+
"bash",
|
|
15
|
+
"html",
|
|
16
|
+
"css",
|
|
17
|
+
"json",
|
|
18
|
+
"markdown",
|
|
19
|
+
"sql",
|
|
20
|
+
"rust",
|
|
21
|
+
"go",
|
|
22
|
+
"c",
|
|
23
|
+
"cpp",
|
|
24
|
+
"other"
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
export const CodeEditor: Component<CodeEditorProps> = (componentProps) => {
|
|
28
|
+
let codeTextarea: HTMLTextAreaElement | undefined
|
|
29
|
+
|
|
30
|
+
const autoResize = () => {
|
|
31
|
+
if (codeTextarea) {
|
|
32
|
+
codeTextarea.style.height = "auto"
|
|
33
|
+
codeTextarea.style.height = `${codeTextarea.scrollHeight + 2}px`
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const saveProps = (lang?: string, code?: string, caption?: string) => {
|
|
38
|
+
componentProps.onPropsChange({
|
|
39
|
+
...componentProps.props,
|
|
40
|
+
language: lang !== undefined ? lang : componentProps.props.language || "typescript",
|
|
41
|
+
code: code !== undefined ? code : codeTextarea?.value || "",
|
|
42
|
+
caption: caption !== undefined ? caption : componentProps.props.caption || ""
|
|
43
|
+
})
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
onMount(() => {
|
|
47
|
+
if (codeTextarea) {
|
|
48
|
+
codeTextarea.value = componentProps.props.code || ""
|
|
49
|
+
autoResize()
|
|
50
|
+
}
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
return (
|
|
54
|
+
<div class="rimelight-code-editor flex flex-col gap-2 p-2 w-full">
|
|
55
|
+
<div class="flex items-center justify-between mb-1">
|
|
56
|
+
<div class="flex items-center gap-2">
|
|
57
|
+
<span class="text-xs font-semibold text-muted-foreground uppercase tracking-wide">
|
|
58
|
+
Language
|
|
59
|
+
</span>
|
|
60
|
+
<div class="w-36">
|
|
61
|
+
<RLSSelect
|
|
62
|
+
items={LANGUAGES}
|
|
63
|
+
value={componentProps.props.language || "typescript"}
|
|
64
|
+
onChange={(e) => saveProps(e.currentTarget.value)}
|
|
65
|
+
size="xs"
|
|
66
|
+
/>
|
|
67
|
+
</div>
|
|
68
|
+
</div>
|
|
69
|
+
</div>
|
|
70
|
+
<div class="relative w-full">
|
|
71
|
+
<textarea
|
|
72
|
+
ref={(el) => (codeTextarea = el)}
|
|
73
|
+
data-field="code"
|
|
74
|
+
class="w-full bg-neutral-950/80 border border-default rounded-lg px-3 py-2 text-sm text-green-400 font-mono resize-none outline-none focus:border-primary min-h-[120px] leading-relaxed"
|
|
75
|
+
placeholder="// Write code here..."
|
|
76
|
+
spellcheck={false}
|
|
77
|
+
onInput={autoResize}
|
|
78
|
+
onBlur={() => saveProps()}
|
|
79
|
+
onKeyDown={(e) => {
|
|
80
|
+
if (e.key === "Tab" && codeTextarea) {
|
|
81
|
+
e.preventDefault()
|
|
82
|
+
const start = codeTextarea.selectionStart
|
|
83
|
+
const end = codeTextarea.selectionEnd
|
|
84
|
+
const val = codeTextarea.value
|
|
85
|
+
codeTextarea.value = val.substring(0, start) + " " + val.substring(end)
|
|
86
|
+
codeTextarea.selectionStart = codeTextarea.selectionEnd = start + 2
|
|
87
|
+
}
|
|
88
|
+
}}
|
|
89
|
+
/>
|
|
90
|
+
</div>
|
|
91
|
+
<RLSInput
|
|
92
|
+
type="text"
|
|
93
|
+
data-field="caption"
|
|
94
|
+
value={componentProps.props.caption || ""}
|
|
95
|
+
onBlur={(e) => saveProps(undefined, undefined, e.currentTarget.value)}
|
|
96
|
+
size="sm"
|
|
97
|
+
placeholder="Caption (optional)"
|
|
98
|
+
/>
|
|
99
|
+
</div>
|
|
100
|
+
)
|
|
101
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { type Component } from "solid-js"
|
|
2
|
+
import RLSInput from "@rimelight/ui/components/input/RLSInput.tsx"
|
|
3
|
+
import RLSTextarea from "@rimelight/ui/components/textarea/RLSTextarea.tsx"
|
|
4
|
+
|
|
5
|
+
export interface DialogueEditorProps {
|
|
6
|
+
props: Record<string, any>
|
|
7
|
+
onPropsChange: (newProps: Record<string, any>) => void
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export const DialogueEditor: Component<DialogueEditorProps> = (componentProps) => {
|
|
11
|
+
const saveProps = (character?: string, parenthetical?: string, line?: string) => {
|
|
12
|
+
componentProps.onPropsChange({
|
|
13
|
+
...componentProps.props,
|
|
14
|
+
character: character !== undefined ? character : componentProps.props.character || "",
|
|
15
|
+
parenthetical:
|
|
16
|
+
parenthetical !== undefined ? parenthetical : componentProps.props.parenthetical || "",
|
|
17
|
+
line: line !== undefined ? line : componentProps.props.line || ""
|
|
18
|
+
})
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return (
|
|
22
|
+
<div class="rimelight-dialogue-editor flex flex-col gap-2 p-2 border-l-2 border-primary/50 w-full">
|
|
23
|
+
<div class="w-48 mx-auto">
|
|
24
|
+
<RLSInput
|
|
25
|
+
data-field="character"
|
|
26
|
+
value={componentProps.props.character || ""}
|
|
27
|
+
onBlur={(e) => saveProps(e.currentTarget.value)}
|
|
28
|
+
class="text-center font-bold text-sm uppercase"
|
|
29
|
+
placeholder="CHARACTER"
|
|
30
|
+
size="sm"
|
|
31
|
+
/>
|
|
32
|
+
</div>
|
|
33
|
+
<div class="w-64 mx-auto">
|
|
34
|
+
<RLSInput
|
|
35
|
+
data-field="parenthetical"
|
|
36
|
+
value={componentProps.props.parenthetical || ""}
|
|
37
|
+
onBlur={(e) => saveProps(undefined, e.currentTarget.value)}
|
|
38
|
+
class="text-center italic text-sm"
|
|
39
|
+
placeholder="(parenthetical)"
|
|
40
|
+
size="sm"
|
|
41
|
+
/>
|
|
42
|
+
</div>
|
|
43
|
+
<RLSTextarea
|
|
44
|
+
data-field="line"
|
|
45
|
+
value={componentProps.props.line || ""}
|
|
46
|
+
onBlur={(e) => saveProps(undefined, undefined, e.currentTarget.value)}
|
|
47
|
+
placeholder="Dialogue line..."
|
|
48
|
+
rows={2}
|
|
49
|
+
/>
|
|
50
|
+
</div>
|
|
51
|
+
)
|
|
52
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { onMount, type Component } from "solid-js"
|
|
2
|
+
import type { FileTreeNode } from "../../../core/types/blocks"
|
|
3
|
+
|
|
4
|
+
export interface FileTreeEditorProps {
|
|
5
|
+
props: Record<string, any>
|
|
6
|
+
onPropsChange: (newProps: Record<string, any>) => void
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export const FileTreeEditor: Component<FileTreeEditorProps> = (componentProps) => {
|
|
10
|
+
let jsonTextarea: HTMLTextAreaElement | undefined
|
|
11
|
+
|
|
12
|
+
const tree = (): FileTreeNode[] =>
|
|
13
|
+
componentProps.props.tree || [
|
|
14
|
+
{ name: "src", isDir: true, children: [{ name: "index.ts", isDir: false }] },
|
|
15
|
+
{ name: "package.json", isDir: false }
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
const autoResize = () => {
|
|
19
|
+
if (jsonTextarea) {
|
|
20
|
+
jsonTextarea.style.height = "auto"
|
|
21
|
+
jsonTextarea.style.height = `${jsonTextarea.scrollHeight + 2}px`
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
onMount(() => {
|
|
26
|
+
if (jsonTextarea) {
|
|
27
|
+
jsonTextarea.value = JSON.stringify(tree(), null, 2)
|
|
28
|
+
autoResize()
|
|
29
|
+
}
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
const saveJson = (val: string) => {
|
|
33
|
+
try {
|
|
34
|
+
const parsed = JSON.parse(val)
|
|
35
|
+
if (Array.isArray(parsed)) {
|
|
36
|
+
componentProps.onPropsChange({
|
|
37
|
+
...componentProps.props,
|
|
38
|
+
tree: parsed
|
|
39
|
+
})
|
|
40
|
+
}
|
|
41
|
+
} catch {
|
|
42
|
+
// Ignore invalid JSON on intermediate typing
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return (
|
|
47
|
+
<div class="rimelight-file-tree-editor flex flex-col gap-2 p-3 rounded-xl border border-default w-full">
|
|
48
|
+
<div class="flex items-center justify-between">
|
|
49
|
+
<span class="text-xs font-bold uppercase tracking-wider text-muted-foreground">
|
|
50
|
+
File Tree JSON
|
|
51
|
+
</span>
|
|
52
|
+
<span class="text-xs text-muted-foreground italic">
|
|
53
|
+
Format: Array of {`{ name, isDir, comment, children }`}
|
|
54
|
+
</span>
|
|
55
|
+
</div>
|
|
56
|
+
<textarea
|
|
57
|
+
ref={(el) => (jsonTextarea = el)}
|
|
58
|
+
class="w-full bg-neutral-950/80 border border-default rounded-lg p-2.5 text-xs text-amber-400 font-mono resize-none outline-none focus:border-primary min-h-[100px] leading-relaxed"
|
|
59
|
+
spellcheck={false}
|
|
60
|
+
onInput={autoResize}
|
|
61
|
+
onBlur={(e) => saveJson(e.currentTarget.value)}
|
|
62
|
+
/>
|
|
63
|
+
</div>
|
|
64
|
+
)
|
|
65
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { type Component } from "solid-js"
|
|
2
|
+
import RLSInput from "@rimelight/ui/components/input/RLSInput.tsx"
|
|
3
|
+
|
|
4
|
+
export interface ImageEditorProps {
|
|
5
|
+
props: Record<string, any>
|
|
6
|
+
onPropsChange: (newProps: Record<string, any>) => void
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export const ImageEditor: Component<ImageEditorProps> = (componentProps) => {
|
|
10
|
+
const saveProps = (src?: string, alt?: string, caption?: string) => {
|
|
11
|
+
componentProps.onPropsChange({
|
|
12
|
+
...componentProps.props,
|
|
13
|
+
src: src !== undefined ? src : componentProps.props.src || "",
|
|
14
|
+
alt: alt !== undefined ? alt : componentProps.props.alt || "",
|
|
15
|
+
caption: caption !== undefined ? caption : componentProps.props.caption || ""
|
|
16
|
+
})
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return (
|
|
20
|
+
<div class="w-full flex flex-col gap-3 p-2">
|
|
21
|
+
<div class="flex flex-col gap-1">
|
|
22
|
+
<span class="text-xs font-bold text-muted-foreground uppercase">Image URL</span>
|
|
23
|
+
<RLSInput
|
|
24
|
+
id="image-src-input"
|
|
25
|
+
type="text"
|
|
26
|
+
value={componentProps.props.src || ""}
|
|
27
|
+
onBlur={(e) => saveProps(e.currentTarget.value)}
|
|
28
|
+
placeholder="https://..."
|
|
29
|
+
leadingIcon="image"
|
|
30
|
+
/>
|
|
31
|
+
</div>
|
|
32
|
+
|
|
33
|
+
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
|
34
|
+
<div class="flex flex-col gap-1">
|
|
35
|
+
<span class="text-xs font-bold text-muted-foreground uppercase">Alt Text</span>
|
|
36
|
+
<RLSInput
|
|
37
|
+
id="image-alt-input"
|
|
38
|
+
type="text"
|
|
39
|
+
value={componentProps.props.alt || ""}
|
|
40
|
+
onBlur={(e) => saveProps(undefined, e.currentTarget.value)}
|
|
41
|
+
placeholder="Alt description"
|
|
42
|
+
size="sm"
|
|
43
|
+
/>
|
|
44
|
+
</div>
|
|
45
|
+
|
|
46
|
+
<div class="flex flex-col gap-1">
|
|
47
|
+
<span class="text-xs font-bold text-muted-foreground uppercase">Caption</span>
|
|
48
|
+
<RLSInput
|
|
49
|
+
id="image-caption-input"
|
|
50
|
+
type="text"
|
|
51
|
+
value={componentProps.props.caption || ""}
|
|
52
|
+
onBlur={(e) => saveProps(undefined, undefined, e.currentTarget.value)}
|
|
53
|
+
placeholder="Image caption"
|
|
54
|
+
size="sm"
|
|
55
|
+
/>
|
|
56
|
+
</div>
|
|
57
|
+
</div>
|
|
58
|
+
|
|
59
|
+
{componentProps.props.src && (
|
|
60
|
+
<div class="mt-2 flex justify-center p-2 rounded-lg border border-default">
|
|
61
|
+
<img
|
|
62
|
+
src={componentProps.props.src}
|
|
63
|
+
alt={componentProps.props.alt || "Image preview"}
|
|
64
|
+
class="max-h-48 object-cover rounded shadow-sm"
|
|
65
|
+
/>
|
|
66
|
+
</div>
|
|
67
|
+
)}
|
|
68
|
+
</div>
|
|
69
|
+
)
|
|
70
|
+
}
|