@rimelight/cms 0.0.1 → 0.0.3
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/LICENSE +21 -0
- package/package.json +16 -14
- package/src/admin/api/media-file.ts +39 -0
- package/src/admin/api/media.ts +199 -0
- package/src/admin/layouts/CMSDashboardLayout.astro +178 -0
- package/src/admin/layouts/DefaultParentLayout.astro +50 -0
- package/src/admin/pages/assets.astro +424 -0
- package/src/admin/pages/byline-schema.astro +62 -0
- package/src/admin/pages/bylines.astro +16 -0
- package/src/admin/pages/categories.astro +21 -0
- package/src/admin/pages/content-types.astro +55 -0
- package/src/admin/pages/dashboard.astro +61 -0
- package/src/admin/pages/pages-edit.astro +595 -0
- package/src/admin/pages/pages-index.astro +146 -0
- package/src/admin/pages/pages-new.astro +130 -0
- package/src/admin/pages/pages-preview.astro +111 -0
- package/src/admin/pages/pages-review.astro +236 -0
- package/src/admin/pages/settings.astro +203 -0
- package/src/admin/pages/tags.astro +21 -0
- package/src/admin/pages/templates-edit.astro +234 -0
- package/src/admin/pages/templates-index.astro +95 -0
- package/src/admin/pages/templates-new.astro +193 -0
- package/src/admin/pages/users.astro +74 -0
- package/src/admin/pages/versions.astro +74 -0
- package/src/astro/BlockRenderer.astro +44 -96
- package/src/astro/PageRenderer.astro +9 -2
- package/src/astro/blocks/CalloutBlock.astro +7 -7
- package/src/astro/blocks/CardBlock.astro +5 -5
- package/src/astro/blocks/CodeBlock.astro +2 -2
- package/src/astro/blocks/FileTreeBlock.astro +7 -7
- package/src/astro/blocks/ImageBlock.astro +2 -2
- package/src/astro/blocks/OrderedListBlock.astro +2 -2
- package/src/astro/blocks/ParagraphBlock.astro +3 -3
- package/src/astro/blocks/SectionBlock.astro +6 -6
- package/src/astro/blocks/StepItemBlock.astro +3 -3
- package/src/astro/blocks/StepsBlock.astro +1 -1
- package/src/astro/blocks/TableBlock.astro +6 -6
- package/src/astro/blocks/TabsBlock.astro +9 -9
- package/src/astro/blocks/UnorderedListBlock.astro +2 -2
- package/src/astro/blocks/index.ts +35 -0
- package/src/client/components/RimelightBlock.tsx +19 -19
- package/src/client/components/RimelightBlockPicker.tsx +9 -13
- package/src/client/components/RimelightBylinesManager.tsx +438 -0
- package/src/client/components/RimelightEditor.tsx +3 -3
- package/src/client/components/RimelightPreview.tsx +37 -45
- package/src/client/components/RimelightPropertiesPanel.tsx +368 -26
- package/src/client/components/RimelightTaxonomyManager.tsx +369 -0
- package/src/client/components/RimelightTaxonomyMasterDetail.tsx +412 -0
- package/src/client/components/RimelightTemplateEditor.tsx +6 -8
- package/src/client/components/editors/CalloutEditor.tsx +2 -4
- package/src/client/components/editors/CardsEditor.tsx +2 -4
- package/src/client/components/editors/CodeEditor.tsx +1 -3
- package/src/client/components/editors/FileTreeEditor.tsx +2 -4
- package/src/client/components/editors/ImageEditor.tsx +3 -3
- package/src/client/components/editors/ParagraphEditor.tsx +2 -2
- package/src/client/components/editors/SceneEditor.tsx +6 -12
- package/src/client/components/editors/ScriptEditor.tsx +8 -16
- package/src/client/components/editors/SectionEditor.tsx +4 -4
- package/src/client/components/editors/StepItemEditor.tsx +2 -2
- package/src/client/components/editors/StepsEditor.tsx +2 -4
- package/src/client/components/editors/TabItemEditor.tsx +3 -3
- package/src/client/components/editors/TableEditor.tsx +5 -5
- package/src/client/components/editors/TabsEditor.tsx +2 -4
- package/src/client/components/index.ts +3 -0
- package/src/client/loader.ts +3 -0
- package/src/core/diff.ts +145 -0
- package/src/core/excerpt.ts +159 -0
- package/src/core/preview.ts +87 -0
- package/src/core/registry.ts +0 -39
- package/src/core/types/blocks.ts +83 -6
- package/src/core/types/bylines.ts +14 -0
- package/src/core/types/media.ts +13 -0
- package/src/core/types/taxonomies.ts +20 -0
- package/src/cron/scheduled.ts +57 -0
- package/src/docs/agent.ts +116 -0
- package/src/docs/config.ts +25 -0
- package/src/docs/index.ts +5 -0
- package/src/docs/routing.ts +104 -0
- package/src/docs/sidebar.ts +251 -0
- package/src/docs/toc.ts +47 -0
- package/src/env.d.ts +13 -0
- package/src/index.ts +40 -6
- package/src/integration.ts +196 -0
- package/src/loader/live.ts +254 -0
- package/src/markdown/index.ts +0 -1
- package/src/mcp/index.ts +228 -0
- package/src/schema/bylines.ts +23 -0
- package/src/schema/index.ts +3 -0
- package/src/schema/page_versions.ts +2 -0
- package/src/schema/pages.ts +2 -0
- package/src/schema/site_settings.ts +41 -0
- package/src/schema/taxonomies.ts +49 -0
- package/src/services/bylines.ts +74 -0
- package/src/services/search-indexer.ts +61 -0
- package/src/services/site-settings.ts +108 -0
- package/src/services/taxonomies.ts +276 -0
- package/src/storage/index.ts +253 -0
- package/src/astro/blocks/DialogueBlock.astro +0 -23
- package/src/astro/blocks/SceneBlock.astro +0 -65
- package/src/astro/blocks/ScriptBlock.astro +0 -59
- package/src/core/template-sync.ts +0 -75
- package/src/markdown/parser.ts +0 -347
- package/src/migration.ts +0 -155
- package/src/search/sync.ts +0 -32
|
@@ -0,0 +1,438 @@
|
|
|
1
|
+
import { createSignal, onMount, For, Show, createMemo, type Component } from "solid-js"
|
|
2
|
+
import RLSInput from "@rimelight/ui/components/input/RLSInput.tsx"
|
|
3
|
+
import RLSButton from "@rimelight/ui/components/button/RLSButton.tsx"
|
|
4
|
+
import RLSBadge from "@rimelight/ui/components/badge/RLSBadge.tsx"
|
|
5
|
+
import RLSIcon from "@rimelight/ui/components/icon/RLSIcon.tsx"
|
|
6
|
+
|
|
7
|
+
export interface BylineEntity {
|
|
8
|
+
id: string
|
|
9
|
+
name: string
|
|
10
|
+
slug: string
|
|
11
|
+
websiteUrl?: string | null
|
|
12
|
+
bio?: string | null
|
|
13
|
+
avatar?: string | null
|
|
14
|
+
userId?: string | null
|
|
15
|
+
socials?: Record<string, string>
|
|
16
|
+
metadata?: Record<string, any>
|
|
17
|
+
createdAt?: string
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export const RimelightBylinesManager: Component = () => {
|
|
21
|
+
const [bylines, setBylines] = createSignal<BylineEntity[]>([])
|
|
22
|
+
const [selectedId, setSelectedId] = createSignal<string | null>(null)
|
|
23
|
+
const [searchQuery, setSearchQuery] = createSignal("")
|
|
24
|
+
const [isLoading, setIsLoading] = createSignal(false)
|
|
25
|
+
const [isSaving, setIsSaving] = createSignal(false)
|
|
26
|
+
const [error, setError] = createSignal<string | null>(null)
|
|
27
|
+
|
|
28
|
+
// Form State
|
|
29
|
+
const [form, setForm] = createSignal<{
|
|
30
|
+
name: string
|
|
31
|
+
slug: string
|
|
32
|
+
websiteUrl: string
|
|
33
|
+
bio: string
|
|
34
|
+
avatar: string
|
|
35
|
+
userId: string
|
|
36
|
+
}>({
|
|
37
|
+
name: "",
|
|
38
|
+
slug: "",
|
|
39
|
+
websiteUrl: "",
|
|
40
|
+
bio: "",
|
|
41
|
+
avatar: "",
|
|
42
|
+
userId: ""
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
const loadBylines = async () => {
|
|
46
|
+
setIsLoading(true)
|
|
47
|
+
setError(null)
|
|
48
|
+
try {
|
|
49
|
+
const res = await fetch("/api/cms/bylines")
|
|
50
|
+
if (!res.ok) throw new Error("Failed to load bylines")
|
|
51
|
+
const data = await res.json()
|
|
52
|
+
const list: BylineEntity[] = Array.isArray(data) ? data : []
|
|
53
|
+
setBylines(list)
|
|
54
|
+
|
|
55
|
+
if (list.length > 0 && !selectedId()) {
|
|
56
|
+
selectByline(list[0]!)
|
|
57
|
+
} else if (list.length === 0) {
|
|
58
|
+
startNewByline()
|
|
59
|
+
}
|
|
60
|
+
} catch (err: any) {
|
|
61
|
+
setError(err.message || "Failed to load bylines")
|
|
62
|
+
} finally {
|
|
63
|
+
setIsLoading(false)
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
onMount(() => {
|
|
68
|
+
void loadBylines()
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
const filteredBylines = createMemo(() => {
|
|
72
|
+
const q = searchQuery().toLowerCase().trim()
|
|
73
|
+
if (!q) return bylines()
|
|
74
|
+
return bylines().filter(
|
|
75
|
+
(b) =>
|
|
76
|
+
b.name.toLowerCase().includes(q) ||
|
|
77
|
+
b.slug.toLowerCase().includes(q) ||
|
|
78
|
+
(b.bio && b.bio.toLowerCase().includes(q))
|
|
79
|
+
)
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
const selectByline = (byline: BylineEntity) => {
|
|
83
|
+
setSelectedId(byline.id)
|
|
84
|
+
setForm({
|
|
85
|
+
name: byline.name || "",
|
|
86
|
+
slug: byline.slug || "",
|
|
87
|
+
websiteUrl: byline.websiteUrl || "",
|
|
88
|
+
bio: byline.bio || "",
|
|
89
|
+
avatar: byline.avatar || "",
|
|
90
|
+
userId: byline.userId || ""
|
|
91
|
+
})
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const startNewByline = () => {
|
|
95
|
+
setSelectedId(null)
|
|
96
|
+
setForm({
|
|
97
|
+
name: "",
|
|
98
|
+
slug: "",
|
|
99
|
+
websiteUrl: "",
|
|
100
|
+
bio: "",
|
|
101
|
+
avatar: "",
|
|
102
|
+
userId: ""
|
|
103
|
+
})
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const handleNameChange = (name: string) => {
|
|
107
|
+
const isNew = !selectedId()
|
|
108
|
+
const autoSlug = isNew
|
|
109
|
+
? name
|
|
110
|
+
.toLowerCase()
|
|
111
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
112
|
+
.replace(/(^-|-$)/g, "")
|
|
113
|
+
: form().slug
|
|
114
|
+
|
|
115
|
+
setForm((prev) => ({
|
|
116
|
+
...prev,
|
|
117
|
+
name,
|
|
118
|
+
slug: isNew ? autoSlug : prev.slug
|
|
119
|
+
}))
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const handleSave = async (e?: Event) => {
|
|
123
|
+
e?.preventDefault()
|
|
124
|
+
const f = form()
|
|
125
|
+
if (!f.name.trim()) {
|
|
126
|
+
alert("Display name is required")
|
|
127
|
+
return
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const slug =
|
|
131
|
+
f.slug.trim() ||
|
|
132
|
+
f.name
|
|
133
|
+
.toLowerCase()
|
|
134
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
135
|
+
.replace(/(^-|-$)/g, "")
|
|
136
|
+
|
|
137
|
+
const payload = {
|
|
138
|
+
name: f.name.trim(),
|
|
139
|
+
slug,
|
|
140
|
+
websiteUrl: f.websiteUrl.trim() || null,
|
|
141
|
+
bio: f.bio.trim() || null,
|
|
142
|
+
avatar: f.avatar.trim() || null,
|
|
143
|
+
userId: f.userId.trim() || null
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
setIsSaving(true)
|
|
147
|
+
try {
|
|
148
|
+
if (selectedId()) {
|
|
149
|
+
const res = await fetch(`/api/cms/bylines/${selectedId()}`, {
|
|
150
|
+
method: "PUT",
|
|
151
|
+
headers: { "Content-Type": "application/json" },
|
|
152
|
+
body: JSON.stringify(payload)
|
|
153
|
+
})
|
|
154
|
+
if (!res.ok) throw new Error("Failed to update byline")
|
|
155
|
+
const updated = await res.json()
|
|
156
|
+
setBylines((prev) => prev.map((b) => (b.id === updated.id ? updated : b)))
|
|
157
|
+
} else {
|
|
158
|
+
const res = await fetch("/api/cms/bylines", {
|
|
159
|
+
method: "POST",
|
|
160
|
+
headers: { "Content-Type": "application/json" },
|
|
161
|
+
body: JSON.stringify(payload)
|
|
162
|
+
})
|
|
163
|
+
if (!res.ok) throw new Error("Failed to create byline")
|
|
164
|
+
const created = await res.json()
|
|
165
|
+
setBylines((prev) => [created, ...prev])
|
|
166
|
+
setSelectedId(created.id)
|
|
167
|
+
}
|
|
168
|
+
} catch (err: any) {
|
|
169
|
+
alert(err.message || "Failed to save byline")
|
|
170
|
+
} finally {
|
|
171
|
+
setIsSaving(false)
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const handleDelete = async () => {
|
|
176
|
+
const id = selectedId()
|
|
177
|
+
if (!id) return
|
|
178
|
+
const current = bylines().find((b) => b.id === id)
|
|
179
|
+
if (!confirm(`Are you sure you want to delete byline "${current?.name || id}"?`)) return
|
|
180
|
+
|
|
181
|
+
try {
|
|
182
|
+
const res = await fetch(`/api/cms/bylines/${id}`, { method: "DELETE" })
|
|
183
|
+
if (!res.ok) throw new Error("Failed to delete byline")
|
|
184
|
+
setBylines((prev) => prev.filter((b) => b.id !== id))
|
|
185
|
+
const remaining = bylines().filter((b) => b.id !== id)
|
|
186
|
+
if (remaining.length > 0) {
|
|
187
|
+
selectByline(remaining[0]!)
|
|
188
|
+
} else {
|
|
189
|
+
startNewByline()
|
|
190
|
+
}
|
|
191
|
+
} catch (err: any) {
|
|
192
|
+
alert(err.message || "Failed to delete byline")
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return (
|
|
197
|
+
<div class="flex flex-col gap-6 w-full max-w-7xl mx-auto py-2">
|
|
198
|
+
{/* Top Header */}
|
|
199
|
+
<div class="flex items-center justify-between border-b border-default pb-4">
|
|
200
|
+
<div>
|
|
201
|
+
<h1 class="text-2xl font-bold text-highlighted tracking-tight">Bylines</h1>
|
|
202
|
+
<p class="text-xs text-muted mt-0.5">
|
|
203
|
+
Manage author profiles and multi-author attributions across articles and pages.
|
|
204
|
+
</p>
|
|
205
|
+
</div>
|
|
206
|
+
</div>
|
|
207
|
+
|
|
208
|
+
{/* 2-Column Master-Detail Layout */}
|
|
209
|
+
<div class="grid grid-cols-1 lg:grid-cols-12 gap-8 items-start">
|
|
210
|
+
{/* Left Column: Search & Byline Cards List (5 Cols) */}
|
|
211
|
+
<div class="lg:col-span-5 flex flex-col gap-3">
|
|
212
|
+
<div class="flex items-center gap-2">
|
|
213
|
+
<div class="flex-1">
|
|
214
|
+
<RLSInput
|
|
215
|
+
value={searchQuery()}
|
|
216
|
+
onInput={(e: InputEvent & { currentTarget: HTMLInputElement }) =>
|
|
217
|
+
setSearchQuery(e.currentTarget.value)
|
|
218
|
+
}
|
|
219
|
+
placeholder="Search bylines..."
|
|
220
|
+
size="sm"
|
|
221
|
+
/>
|
|
222
|
+
</div>
|
|
223
|
+
<RLSButton
|
|
224
|
+
variant="solid"
|
|
225
|
+
color="primary"
|
|
226
|
+
size="sm"
|
|
227
|
+
onClick={startNewByline}
|
|
228
|
+
leadingIcon="plus"
|
|
229
|
+
label="New"
|
|
230
|
+
/>
|
|
231
|
+
</div>
|
|
232
|
+
|
|
233
|
+
<Show when={isLoading()}>
|
|
234
|
+
<div class="p-8 text-center text-xs text-muted animate-pulse">Loading bylines...</div>
|
|
235
|
+
</Show>
|
|
236
|
+
|
|
237
|
+
<Show when={error()}>
|
|
238
|
+
<div class="p-4 text-center text-xs text-error-500">{error()}</div>
|
|
239
|
+
</Show>
|
|
240
|
+
|
|
241
|
+
<Show when={!isLoading() && filteredBylines().length === 0}>
|
|
242
|
+
<div class="p-8 text-center border border-dashed border-default rounded-xl flex flex-col items-center gap-2">
|
|
243
|
+
<RLSIcon name="user" size="md" class="text-muted/50" />
|
|
244
|
+
<span class="text-xs font-semibold text-highlighted">No bylines found</span>
|
|
245
|
+
<p class="text-[11px] text-muted">
|
|
246
|
+
Create a byline to attribute authors to your posts.
|
|
247
|
+
</p>
|
|
248
|
+
</div>
|
|
249
|
+
</Show>
|
|
250
|
+
|
|
251
|
+
<div class="flex flex-col gap-2 max-h-[calc(100vh-280px)] overflow-y-auto pr-1">
|
|
252
|
+
<For each={filteredBylines()}>
|
|
253
|
+
{(item) => {
|
|
254
|
+
const isSelected = () => selectedId() === item.id
|
|
255
|
+
return (
|
|
256
|
+
<button
|
|
257
|
+
type="button"
|
|
258
|
+
onClick={() => selectByline(item)}
|
|
259
|
+
class={`flex items-center gap-3 p-3 rounded-xl border text-left transition cursor-pointer ${
|
|
260
|
+
isSelected()
|
|
261
|
+
? "bg-elevated/90 border-primary shadow-sm"
|
|
262
|
+
: "bg-background/40 hover:bg-elevated/40 border-default"
|
|
263
|
+
}`}
|
|
264
|
+
>
|
|
265
|
+
{item.avatar ? (
|
|
266
|
+
<img
|
|
267
|
+
src={item.avatar}
|
|
268
|
+
alt={item.name}
|
|
269
|
+
class="w-10 h-10 rounded-full object-cover shrink-0 border border-default"
|
|
270
|
+
/>
|
|
271
|
+
) : (
|
|
272
|
+
<div class="w-10 h-10 rounded-full bg-primary/10 text-primary font-bold text-sm flex items-center justify-center shrink-0 border border-primary/20">
|
|
273
|
+
{item.name.charAt(0).toUpperCase()}
|
|
274
|
+
</div>
|
|
275
|
+
)}
|
|
276
|
+
<div class="flex flex-col min-w-0 flex-1">
|
|
277
|
+
<div class="flex items-center justify-between gap-1">
|
|
278
|
+
<span class="text-sm font-semibold text-highlighted truncate">
|
|
279
|
+
{item.name}
|
|
280
|
+
</span>
|
|
281
|
+
<Show when={isSelected()}>
|
|
282
|
+
<RLSBadge variant="solid" color="primary" size="sm">
|
|
283
|
+
Active
|
|
284
|
+
</RLSBadge>
|
|
285
|
+
</Show>
|
|
286
|
+
</div>
|
|
287
|
+
<span class="text-xs font-mono text-muted truncate">{item.slug}</span>
|
|
288
|
+
</div>
|
|
289
|
+
</button>
|
|
290
|
+
)
|
|
291
|
+
}}
|
|
292
|
+
</For>
|
|
293
|
+
</div>
|
|
294
|
+
</div>
|
|
295
|
+
|
|
296
|
+
{/* Right Column: Edit / Create Form (7 Cols) */}
|
|
297
|
+
<div class="lg:col-span-7 bg-elevated/30 border border-default rounded-2xl p-6 flex flex-col gap-5">
|
|
298
|
+
<div class="flex items-center justify-between border-b border-default pb-3">
|
|
299
|
+
<h3 class="text-lg font-bold text-highlighted">
|
|
300
|
+
{selectedId() ? "Edit byline" : "Create byline"}
|
|
301
|
+
</h3>
|
|
302
|
+
<Show when={selectedId()}>
|
|
303
|
+
<span class="text-xs font-mono text-muted">ID: {selectedId()}</span>
|
|
304
|
+
</Show>
|
|
305
|
+
</div>
|
|
306
|
+
|
|
307
|
+
<form onSubmit={handleSave} class="flex flex-col gap-4">
|
|
308
|
+
{/* Display Name */}
|
|
309
|
+
<div class="flex flex-col gap-1.5">
|
|
310
|
+
<label class="text-xs font-semibold text-neutral-700">Display name *</label>
|
|
311
|
+
<RLSInput
|
|
312
|
+
value={form().name}
|
|
313
|
+
onInput={(e: InputEvent & { currentTarget: HTMLInputElement }) =>
|
|
314
|
+
handleNameChange(e.currentTarget.value)
|
|
315
|
+
}
|
|
316
|
+
placeholder="e.g. Guest Contributor, EmDash Editorial"
|
|
317
|
+
/>
|
|
318
|
+
</div>
|
|
319
|
+
|
|
320
|
+
{/* Slug */}
|
|
321
|
+
<div class="flex flex-col gap-1.5">
|
|
322
|
+
<label class="text-xs font-semibold text-neutral-700">Slug</label>
|
|
323
|
+
<RLSInput
|
|
324
|
+
value={form().slug}
|
|
325
|
+
onInput={(e: InputEvent & { currentTarget: HTMLInputElement }) =>
|
|
326
|
+
setForm((prev) => ({ ...prev, slug: e.currentTarget.value }))
|
|
327
|
+
}
|
|
328
|
+
placeholder="guest-contributor"
|
|
329
|
+
/>
|
|
330
|
+
</div>
|
|
331
|
+
|
|
332
|
+
{/* Website URL */}
|
|
333
|
+
<div class="flex flex-col gap-1.5">
|
|
334
|
+
<label class="text-xs font-semibold text-neutral-700">Website URL</label>
|
|
335
|
+
<RLSInput
|
|
336
|
+
value={form().websiteUrl}
|
|
337
|
+
onInput={(e: InputEvent & { currentTarget: HTMLInputElement }) =>
|
|
338
|
+
setForm((prev) => ({ ...prev, websiteUrl: e.currentTarget.value }))
|
|
339
|
+
}
|
|
340
|
+
placeholder="https://..."
|
|
341
|
+
/>
|
|
342
|
+
</div>
|
|
343
|
+
|
|
344
|
+
{/* Bio */}
|
|
345
|
+
<div class="flex flex-col gap-1.5">
|
|
346
|
+
<label class="text-xs font-semibold text-neutral-700">Bio</label>
|
|
347
|
+
<textarea
|
|
348
|
+
value={form().bio}
|
|
349
|
+
onInput={(e: InputEvent & { currentTarget: HTMLTextAreaElement }) =>
|
|
350
|
+
setForm((prev) => ({ ...prev, bio: e.currentTarget.value }))
|
|
351
|
+
}
|
|
352
|
+
rows={3}
|
|
353
|
+
placeholder="Short author bio..."
|
|
354
|
+
class="w-full text-sm py-2 px-3 rounded-lg border border-default bg-background text-highlighted focus:outline-none focus:ring-1 focus:ring-primary resize-none"
|
|
355
|
+
/>
|
|
356
|
+
</div>
|
|
357
|
+
|
|
358
|
+
{/* Avatar URL & Preview */}
|
|
359
|
+
<div class="flex flex-col gap-1.5">
|
|
360
|
+
<label class="text-xs font-semibold text-neutral-700">Avatar Image URL</label>
|
|
361
|
+
<div class="flex items-center gap-3">
|
|
362
|
+
<Show
|
|
363
|
+
when={form().avatar}
|
|
364
|
+
fallback={
|
|
365
|
+
<div class="w-12 h-12 rounded-xl border border-dashed border-default flex items-center justify-center text-muted/50 shrink-0">
|
|
366
|
+
<RLSIcon name="image" size="md" />
|
|
367
|
+
</div>
|
|
368
|
+
}
|
|
369
|
+
>
|
|
370
|
+
<img
|
|
371
|
+
src={form().avatar}
|
|
372
|
+
alt="Preview"
|
|
373
|
+
class="w-12 h-12 rounded-xl object-cover border border-default shrink-0"
|
|
374
|
+
/>
|
|
375
|
+
</Show>
|
|
376
|
+
<div class="flex-1">
|
|
377
|
+
<RLSInput
|
|
378
|
+
value={form().avatar}
|
|
379
|
+
onInput={(e: InputEvent & { currentTarget: HTMLInputElement }) =>
|
|
380
|
+
setForm((prev) => ({ ...prev, avatar: e.currentTarget.value }))
|
|
381
|
+
}
|
|
382
|
+
placeholder="https://... / avatar.png"
|
|
383
|
+
/>
|
|
384
|
+
</div>
|
|
385
|
+
</div>
|
|
386
|
+
</div>
|
|
387
|
+
|
|
388
|
+
{/* Linked User ID */}
|
|
389
|
+
<div class="flex flex-col gap-1.5">
|
|
390
|
+
<label class="text-xs font-semibold text-neutral-700">Linked user</label>
|
|
391
|
+
<RLSInput
|
|
392
|
+
value={form().userId}
|
|
393
|
+
onInput={(e: InputEvent & { currentTarget: HTMLInputElement }) =>
|
|
394
|
+
setForm((prev) => ({ ...prev, userId: e.currentTarget.value }))
|
|
395
|
+
}
|
|
396
|
+
placeholder="Optional system User ID or email"
|
|
397
|
+
/>
|
|
398
|
+
</div>
|
|
399
|
+
|
|
400
|
+
{/* Form Actions */}
|
|
401
|
+
<div class="flex items-center justify-between border-t border-default pt-4 mt-2">
|
|
402
|
+
<Show when={selectedId()} fallback={<div />}>
|
|
403
|
+
<RLSButton
|
|
404
|
+
type="button"
|
|
405
|
+
variant="outline"
|
|
406
|
+
color="neutral"
|
|
407
|
+
size="sm"
|
|
408
|
+
onClick={handleDelete}
|
|
409
|
+
label="Delete"
|
|
410
|
+
class="!text-error-500 hover:!text-error-600 !border-error-500/30"
|
|
411
|
+
/>
|
|
412
|
+
</Show>
|
|
413
|
+
|
|
414
|
+
<div class="flex items-center gap-2">
|
|
415
|
+
<RLSButton
|
|
416
|
+
type="button"
|
|
417
|
+
variant="outline"
|
|
418
|
+
color="neutral"
|
|
419
|
+
size="sm"
|
|
420
|
+
onClick={startNewByline}
|
|
421
|
+
label="Cancel"
|
|
422
|
+
/>
|
|
423
|
+
<RLSButton
|
|
424
|
+
type="submit"
|
|
425
|
+
variant="solid"
|
|
426
|
+
color="primary"
|
|
427
|
+
size="sm"
|
|
428
|
+
disabled={isSaving()}
|
|
429
|
+
label={isSaving() ? "Saving..." : selectedId() ? "Save Changes" : "Create Byline"}
|
|
430
|
+
/>
|
|
431
|
+
</div>
|
|
432
|
+
</div>
|
|
433
|
+
</form>
|
|
434
|
+
</div>
|
|
435
|
+
</div>
|
|
436
|
+
</div>
|
|
437
|
+
)
|
|
438
|
+
}
|
|
@@ -65,9 +65,9 @@ export const RimelightEditor: Component<RimelightEditorProps> = (props) => {
|
|
|
65
65
|
<div ref={canvasContainer} class="flex flex-col gap-3 w-full min-h-[200px] pl-6">
|
|
66
66
|
{props.state.blocks.length === 0 ? (
|
|
67
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
|
|
69
|
-
<h4 class="text-base font-semibold text-
|
|
70
|
-
<p class="text-xs text-muted
|
|
68
|
+
<div class="text-3xl text-muted mb-2">📄</div>
|
|
69
|
+
<h4 class="text-base font-semibold text-highlighted">No blocks yet</h4>
|
|
70
|
+
<p class="text-xs text-muted mt-1 max-w-xs">
|
|
71
71
|
Start building your page content by adding your first block below.
|
|
72
72
|
</p>
|
|
73
73
|
<div class="mt-4">
|
|
@@ -85,7 +85,7 @@ export const RimelightPreview: Component<RimelightPreviewProps> = (props) => {
|
|
|
85
85
|
</span>
|
|
86
86
|
)}
|
|
87
87
|
{title() && (
|
|
88
|
-
<h1 class="text-3xl sm:text-4xl font-extrabold text-
|
|
88
|
+
<h1 class="text-3xl sm:text-4xl font-extrabold text-highlighted tracking-tight leading-tight">
|
|
89
89
|
{title()}
|
|
90
90
|
</h1>
|
|
91
91
|
)}
|
|
@@ -150,11 +150,11 @@ function renderBlockPreviewHtml(b: BaseBlock, sectionDepth: number = 0): string
|
|
|
150
150
|
if (node.type === "link") {
|
|
151
151
|
const inner = node.children ? renderNodes(node.children) : escapeHtml(node.text || "")
|
|
152
152
|
const target = node.openInNewTab ? ' target="_blank" rel="noopener noreferrer"' : ""
|
|
153
|
-
return `<a href="${escapeHtml(node.url || "#")}"${target} class="text-
|
|
153
|
+
return `<a href="${escapeHtml(node.url || "#")}"${target} class="text-info-500 hover:text-info-400 underline underline-offset-2">${inner}</a>`
|
|
154
154
|
}
|
|
155
155
|
if (node.type === "page_mention") {
|
|
156
156
|
const title = escapeHtml(node.displayTitle || node.pageSlug || node.pageId || "Page")
|
|
157
|
-
return `<span class="inline-flex items-center gap-1 px-1.5 py-0.5 rounded bg-
|
|
157
|
+
return `<span class="inline-flex items-center gap-1 px-1.5 py-0.5 rounded bg-info-500/10 text-info-400 text-sm font-medium">📄 ${title}</span>`
|
|
158
158
|
}
|
|
159
159
|
return ""
|
|
160
160
|
})
|
|
@@ -167,7 +167,7 @@ function renderBlockPreviewHtml(b: BaseBlock, sectionDepth: number = 0): string
|
|
|
167
167
|
} else {
|
|
168
168
|
html = escapeHtml(textVal)
|
|
169
169
|
}
|
|
170
|
-
return `<p class="text-neutral-800
|
|
170
|
+
return `<p class="text-neutral-800 text-base leading-relaxed my-3 whitespace-pre-wrap break-words">${html || '<span class="text-neutral-500 italic">Empty paragraph</span>'}</p>`
|
|
171
171
|
}
|
|
172
172
|
|
|
173
173
|
if (b.type === "SectionBlock") {
|
|
@@ -175,9 +175,8 @@ function renderBlockPreviewHtml(b: BaseBlock, sectionDepth: number = 0): string
|
|
|
175
175
|
const title = props.title || "Untitled Section"
|
|
176
176
|
const slug = title.toLowerCase().replace(/\W+/g, "-")
|
|
177
177
|
const description = props.description
|
|
178
|
-
let headingClasses = "font-bold text-neutral-900
|
|
179
|
-
if (level === 2)
|
|
180
|
-
headingClasses += " text-3xl mt-6 mb-3 border-b border-gray-200 dark:border-gray-800 pb-2"
|
|
178
|
+
let headingClasses = "font-bold text-neutral-900"
|
|
179
|
+
if (level === 2) headingClasses += " text-3xl mt-6 mb-3 border-b border-neutral-200 pb-2"
|
|
181
180
|
else if (level === 3) headingClasses += " text-2xl font-semibold mt-5 mb-2.5"
|
|
182
181
|
else if (level === 4) headingClasses += " text-xl font-medium mt-4 mb-2"
|
|
183
182
|
else if (level === 5) headingClasses += " text-lg font-medium mt-3 mb-1.5"
|
|
@@ -185,8 +184,8 @@ function renderBlockPreviewHtml(b: BaseBlock, sectionDepth: number = 0): string
|
|
|
185
184
|
|
|
186
185
|
return `<div class="section-block my-6">
|
|
187
186
|
<h${level} id="${slug}" class="${headingClasses}">${escapeHtml(title)}</h${level}>
|
|
188
|
-
${description ? `<p class="text-
|
|
189
|
-
<div class="mt-4 pl-4 border-l-2 border-
|
|
187
|
+
${description ? `<p class="text-neutral-500 text-sm mt-1 mb-4 italic">${escapeHtml(description)}</p>` : ""}
|
|
188
|
+
<div class="mt-4 pl-4 border-l-2 border-neutral-200">
|
|
190
189
|
${getChildren(b)
|
|
191
190
|
.map((c) => renderBlockPreviewHtml(c, sectionDepth + 1))
|
|
192
191
|
.join("")}
|
|
@@ -209,26 +208,19 @@ function renderBlockPreviewHtml(b: BaseBlock, sectionDepth: number = 0): string
|
|
|
209
208
|
}
|
|
210
209
|
const icon = icons[variant] || icons.info
|
|
211
210
|
|
|
212
|
-
let variantClasses =
|
|
213
|
-
"bg-blue-50/50 dark:bg-blue-950/10 border-blue-200 dark:border-blue-900/50 text-blue-800 dark:text-blue-200"
|
|
211
|
+
let variantClasses = "bg-info-50/50 border-info-200 text-info-800"
|
|
214
212
|
if (variant === "success") {
|
|
215
|
-
variantClasses =
|
|
216
|
-
"bg-green-50/50 dark:bg-green-950/10 border-green-200 dark:border-green-900/50 text-green-800 dark:text-green-200"
|
|
213
|
+
variantClasses = "bg-success-50/50 border-success-200 text-success-800"
|
|
217
214
|
} else if (variant === "warning") {
|
|
218
|
-
variantClasses =
|
|
219
|
-
"bg-yellow-50/50 dark:bg-yellow-950/10 border-yellow-200 dark:border-yellow-900/50 text-yellow-800 dark:text-yellow-200"
|
|
215
|
+
variantClasses = "bg-warning-50/50 border-warning-200 text-warning-800"
|
|
220
216
|
} else if (variant === "error") {
|
|
221
|
-
variantClasses =
|
|
222
|
-
"bg-red-50/50 dark:bg-red-950/10 border-red-200 dark:border-red-900/50 text-red-800 dark:text-red-200"
|
|
217
|
+
variantClasses = "bg-error-50/50 border-error-200 text-error-800"
|
|
223
218
|
} else if (variant === "commentary") {
|
|
224
|
-
variantClasses =
|
|
225
|
-
"bg-purple-50/50 dark:bg-purple-950/10 border-purple-200 dark:border-purple-900/50 text-purple-800 dark:text-purple-200"
|
|
219
|
+
variantClasses = "bg-ideation-50/50 border-ideation-200 text-ideation-800"
|
|
226
220
|
} else if (variant === "ideation") {
|
|
227
|
-
variantClasses =
|
|
228
|
-
"bg-amber-50/50 dark:bg-amber-950/10 border-amber-200 dark:border-amber-900/50 text-amber-800 dark:text-amber-200"
|
|
221
|
+
variantClasses = "bg-warning-50/50 border-warning-200 text-warning-800"
|
|
229
222
|
} else if (variant === "source") {
|
|
230
|
-
variantClasses =
|
|
231
|
-
"bg-indigo-50/50 dark:bg-indigo-950/10 border-indigo-200 dark:border-indigo-900/50 text-indigo-800 dark:text-indigo-200"
|
|
223
|
+
variantClasses = "bg-info-50/50 border-info-200 text-info-800"
|
|
232
224
|
}
|
|
233
225
|
|
|
234
226
|
const tag = to ? `a href="${escapeHtml(to)}" target="${escapeHtml(target)}"` : "div"
|
|
@@ -249,8 +241,8 @@ function renderBlockPreviewHtml(b: BaseBlock, sectionDepth: number = 0): string
|
|
|
249
241
|
const alt = props.alt || "Image"
|
|
250
242
|
const caption = props.caption
|
|
251
243
|
return `<figure class="mx-auto my-6 flex flex-col items-center max-w-full">
|
|
252
|
-
<img src="${escapeHtml(src)}" alt="${escapeHtml(alt)}" class="h-auto max-w-full rounded-xl object-cover border border-neutral-200
|
|
253
|
-
${caption ? `<figcaption class="mt-2 text-center text-xs text-neutral-500
|
|
244
|
+
<img src="${escapeHtml(src)}" alt="${escapeHtml(alt)}" class="h-auto max-w-full rounded-xl object-cover border border-neutral-200 shadow-sm" />
|
|
245
|
+
${caption ? `<figcaption class="mt-2 text-center text-xs text-neutral-500 italic">${escapeHtml(caption)}</figcaption>` : ""}
|
|
254
246
|
</figure>`
|
|
255
247
|
}
|
|
256
248
|
|
|
@@ -258,10 +250,10 @@ function renderBlockPreviewHtml(b: BaseBlock, sectionDepth: number = 0): string
|
|
|
258
250
|
const code = props.code || textVal
|
|
259
251
|
const language = props.language || "plaintext"
|
|
260
252
|
const caption = props.caption
|
|
261
|
-
return `<div class="cms-code my-4 rounded-lg bg-
|
|
262
|
-
<div class="text-xs text-
|
|
263
|
-
<pre class="m-0"><code class="text-
|
|
264
|
-
${caption ? `<div class="mt-2 text-center text-xs text-
|
|
253
|
+
return `<div class="cms-code my-4 rounded-lg bg-neutral-950 p-4 font-mono text-sm border border-neutral-800 overflow-x-auto">
|
|
254
|
+
<div class="text-xs text-neutral-500 uppercase mb-2 select-none">${escapeHtml(language)}</div>
|
|
255
|
+
<pre class="m-0"><code class="text-success-300 font-mono">${escapeHtml(code)}</code></pre>
|
|
256
|
+
${caption ? `<div class="mt-2 text-center text-xs text-neutral-500 italic">${escapeHtml(caption)}</div>` : ""}
|
|
265
257
|
</div>`
|
|
266
258
|
}
|
|
267
259
|
|
|
@@ -270,22 +262,22 @@ function renderBlockPreviewHtml(b: BaseBlock, sectionDepth: number = 0): string
|
|
|
270
262
|
const synopsis = props.synopsis
|
|
271
263
|
const chars = props.characters || []
|
|
272
264
|
const { aPlot, bPlot, cPlot, stinger } = props
|
|
273
|
-
return `<div class="my-6 p-4 border-l-4 border-
|
|
265
|
+
return `<div class="my-6 p-4 border-l-4 border-warning-400 bg-warning-50/20 rounded-r-lg">
|
|
274
266
|
<div class="mb-4">
|
|
275
|
-
<h3 class="text-xl font-bold text-
|
|
276
|
-
${synopsis ? `<p class="text-sm text-
|
|
277
|
-
${chars.length ? `<div class="flex flex-wrap gap-3 mt-3 text-xs">${chars.map((c: string) => `<span class="px-2 py-0.5 rounded-full bg-
|
|
267
|
+
<h3 class="text-xl font-bold text-warning-900">${escapeHtml(title)}</h3>
|
|
268
|
+
${synopsis ? `<p class="text-sm text-neutral-500 mt-1 italic">${escapeHtml(synopsis)}</p>` : ""}
|
|
269
|
+
${chars.length ? `<div class="flex flex-wrap gap-3 mt-3 text-xs">${chars.map((c: string) => `<span class="px-2 py-0.5 rounded-full bg-warning-100 text-warning-800">${escapeHtml(c)}</span>`).join("")}</div>` : ""}
|
|
278
270
|
</div>
|
|
279
271
|
${
|
|
280
272
|
aPlot || bPlot || cPlot
|
|
281
273
|
? `<div class="grid grid-cols-1 md:grid-cols-3 gap-3 my-3">
|
|
282
|
-
${aPlot ? `<div class="bg-
|
|
283
|
-
${bPlot ? `<div class="bg-
|
|
284
|
-
${cPlot ? `<div class="bg-
|
|
274
|
+
${aPlot ? `<div class="bg-neutral-50/50 rounded p-2 border border-neutral-200"><div class="text-[10px] font-bold uppercase tracking-wider text-neutral-500">A Plot</div><div class="text-sm mt-0.5">${escapeHtml(aPlot)}</div></div>` : ""}
|
|
275
|
+
${bPlot ? `<div class="bg-neutral-50/50 rounded p-2 border border-neutral-200"><div class="text-[10px] font-bold uppercase tracking-wider text-neutral-500">B Plot</div><div class="text-sm mt-0.5">${escapeHtml(bPlot)}</div></div>` : ""}
|
|
276
|
+
${cPlot ? `<div class="bg-neutral-50/50 rounded p-2 border border-neutral-200"><div class="text-[10px] font-bold uppercase tracking-wider text-neutral-500">C Plot</div><div class="text-sm mt-0.5">${escapeHtml(cPlot)}</div></div>` : ""}
|
|
285
277
|
</div>`
|
|
286
278
|
: ""
|
|
287
279
|
}
|
|
288
|
-
${stinger ? `<p class="text-sm text-
|
|
280
|
+
${stinger ? `<p class="text-sm text-neutral-500 italic mt-2 border-t border-neutral-200 pt-2">Stinger: ${escapeHtml(stinger)}</p>` : ""}
|
|
289
281
|
<div class="mt-4">
|
|
290
282
|
${getChildren(b)
|
|
291
283
|
.map((c) => renderBlockPreviewHtml(c, sectionDepth))
|
|
@@ -325,12 +317,12 @@ function renderBlockPreviewHtml(b: BaseBlock, sectionDepth: number = 0): string
|
|
|
325
317
|
if (location) parts.push(location)
|
|
326
318
|
if (tod && tod !== "Other") parts.push(`- ${tod}`)
|
|
327
319
|
|
|
328
|
-
return `<div class="my-4 p-3 border-l-4 border-
|
|
329
|
-
<div class="flex flex-wrap items-center gap-2 text-sm font-semibold text-
|
|
320
|
+
return `<div class="my-4 p-3 border-l-4 border-info-400 bg-info-50/20 rounded-r-lg">
|
|
321
|
+
<div class="flex flex-wrap items-center gap-2 text-sm font-semibold text-info-800 mb-2">
|
|
330
322
|
${parts.map((p) => `<span>${escapeHtml(p)}</span>`).join("")}
|
|
331
|
-
${transition ? `<span class="ml-auto text-[10px] italic text-
|
|
323
|
+
${transition ? `<span class="ml-auto text-[10px] italic text-neutral-500">${escapeHtml(transition)}</span>` : ""}
|
|
332
324
|
</div>
|
|
333
|
-
${props.description ? `<p class="text-sm text-
|
|
325
|
+
${props.description ? `<p class="text-sm text-neutral-700 mb-2">${escapeHtml(props.description)}</p>` : ""}
|
|
334
326
|
<div class="mt-3">
|
|
335
327
|
${getChildren(b)
|
|
336
328
|
.map((c) => renderBlockPreviewHtml(c, sectionDepth))
|
|
@@ -344,11 +336,11 @@ function renderBlockPreviewHtml(b: BaseBlock, sectionDepth: number = 0): string
|
|
|
344
336
|
const parenthetical = props.parenthetical
|
|
345
337
|
const line = props.line || ""
|
|
346
338
|
return `<div class="my-3 mx-8 max-w-md">
|
|
347
|
-
<div class="text-sm font-bold text-center text-
|
|
348
|
-
${parenthetical ? `<div class="text-xs italic text-center text-
|
|
349
|
-
<div class="text-sm text-center text-
|
|
339
|
+
<div class="text-sm font-bold text-center text-commentary-800">${escapeHtml(character)}</div>
|
|
340
|
+
${parenthetical ? `<div class="text-xs italic text-center text-neutral-500 mt-0.5">(${escapeHtml(parenthetical)})</div>` : ""}
|
|
341
|
+
<div class="text-sm text-center text-neutral-700 mt-1">${escapeHtml(line)}</div>
|
|
350
342
|
</div>`
|
|
351
343
|
}
|
|
352
344
|
|
|
353
|
-
return `<div class="p-3 bg-
|
|
345
|
+
return `<div class="p-3 bg-neutral-900 border border-neutral-800 rounded text-sm text-neutral-400 my-2">${escapeHtml(b.type)}: ${escapeHtml(textVal)}</div>`
|
|
354
346
|
}
|