@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,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
+ }