@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.
Files changed (104) hide show
  1. package/LICENSE +21 -0
  2. package/package.json +16 -14
  3. package/src/admin/api/media-file.ts +39 -0
  4. package/src/admin/api/media.ts +199 -0
  5. package/src/admin/layouts/CMSDashboardLayout.astro +178 -0
  6. package/src/admin/layouts/DefaultParentLayout.astro +50 -0
  7. package/src/admin/pages/assets.astro +424 -0
  8. package/src/admin/pages/byline-schema.astro +62 -0
  9. package/src/admin/pages/bylines.astro +16 -0
  10. package/src/admin/pages/categories.astro +21 -0
  11. package/src/admin/pages/content-types.astro +55 -0
  12. package/src/admin/pages/dashboard.astro +61 -0
  13. package/src/admin/pages/pages-edit.astro +595 -0
  14. package/src/admin/pages/pages-index.astro +146 -0
  15. package/src/admin/pages/pages-new.astro +130 -0
  16. package/src/admin/pages/pages-preview.astro +111 -0
  17. package/src/admin/pages/pages-review.astro +236 -0
  18. package/src/admin/pages/settings.astro +203 -0
  19. package/src/admin/pages/tags.astro +21 -0
  20. package/src/admin/pages/templates-edit.astro +234 -0
  21. package/src/admin/pages/templates-index.astro +95 -0
  22. package/src/admin/pages/templates-new.astro +193 -0
  23. package/src/admin/pages/users.astro +74 -0
  24. package/src/admin/pages/versions.astro +74 -0
  25. package/src/astro/BlockRenderer.astro +44 -96
  26. package/src/astro/PageRenderer.astro +9 -2
  27. package/src/astro/blocks/CalloutBlock.astro +7 -7
  28. package/src/astro/blocks/CardBlock.astro +5 -5
  29. package/src/astro/blocks/CodeBlock.astro +2 -2
  30. package/src/astro/blocks/FileTreeBlock.astro +7 -7
  31. package/src/astro/blocks/ImageBlock.astro +2 -2
  32. package/src/astro/blocks/OrderedListBlock.astro +2 -2
  33. package/src/astro/blocks/ParagraphBlock.astro +3 -3
  34. package/src/astro/blocks/SectionBlock.astro +6 -6
  35. package/src/astro/blocks/StepItemBlock.astro +3 -3
  36. package/src/astro/blocks/StepsBlock.astro +1 -1
  37. package/src/astro/blocks/TableBlock.astro +6 -6
  38. package/src/astro/blocks/TabsBlock.astro +9 -9
  39. package/src/astro/blocks/UnorderedListBlock.astro +2 -2
  40. package/src/astro/blocks/index.ts +35 -0
  41. package/src/client/components/RimelightBlock.tsx +19 -19
  42. package/src/client/components/RimelightBlockPicker.tsx +9 -13
  43. package/src/client/components/RimelightBylinesManager.tsx +438 -0
  44. package/src/client/components/RimelightEditor.tsx +3 -3
  45. package/src/client/components/RimelightPreview.tsx +37 -45
  46. package/src/client/components/RimelightPropertiesPanel.tsx +368 -26
  47. package/src/client/components/RimelightTaxonomyManager.tsx +369 -0
  48. package/src/client/components/RimelightTaxonomyMasterDetail.tsx +412 -0
  49. package/src/client/components/RimelightTemplateEditor.tsx +6 -8
  50. package/src/client/components/editors/CalloutEditor.tsx +2 -4
  51. package/src/client/components/editors/CardsEditor.tsx +2 -4
  52. package/src/client/components/editors/CodeEditor.tsx +1 -3
  53. package/src/client/components/editors/FileTreeEditor.tsx +2 -4
  54. package/src/client/components/editors/ImageEditor.tsx +3 -3
  55. package/src/client/components/editors/ParagraphEditor.tsx +2 -2
  56. package/src/client/components/editors/SceneEditor.tsx +6 -12
  57. package/src/client/components/editors/ScriptEditor.tsx +8 -16
  58. package/src/client/components/editors/SectionEditor.tsx +4 -4
  59. package/src/client/components/editors/StepItemEditor.tsx +2 -2
  60. package/src/client/components/editors/StepsEditor.tsx +2 -4
  61. package/src/client/components/editors/TabItemEditor.tsx +3 -3
  62. package/src/client/components/editors/TableEditor.tsx +5 -5
  63. package/src/client/components/editors/TabsEditor.tsx +2 -4
  64. package/src/client/components/index.ts +3 -0
  65. package/src/client/loader.ts +3 -0
  66. package/src/core/diff.ts +145 -0
  67. package/src/core/excerpt.ts +159 -0
  68. package/src/core/preview.ts +87 -0
  69. package/src/core/registry.ts +0 -39
  70. package/src/core/types/blocks.ts +83 -6
  71. package/src/core/types/bylines.ts +14 -0
  72. package/src/core/types/media.ts +13 -0
  73. package/src/core/types/taxonomies.ts +20 -0
  74. package/src/cron/scheduled.ts +57 -0
  75. package/src/docs/agent.ts +116 -0
  76. package/src/docs/config.ts +25 -0
  77. package/src/docs/index.ts +5 -0
  78. package/src/docs/routing.ts +104 -0
  79. package/src/docs/sidebar.ts +251 -0
  80. package/src/docs/toc.ts +47 -0
  81. package/src/env.d.ts +13 -0
  82. package/src/index.ts +40 -6
  83. package/src/integration.ts +196 -0
  84. package/src/loader/live.ts +254 -0
  85. package/src/markdown/index.ts +0 -1
  86. package/src/mcp/index.ts +228 -0
  87. package/src/schema/bylines.ts +23 -0
  88. package/src/schema/index.ts +3 -0
  89. package/src/schema/page_versions.ts +2 -0
  90. package/src/schema/pages.ts +2 -0
  91. package/src/schema/site_settings.ts +41 -0
  92. package/src/schema/taxonomies.ts +49 -0
  93. package/src/services/bylines.ts +74 -0
  94. package/src/services/search-indexer.ts +61 -0
  95. package/src/services/site-settings.ts +108 -0
  96. package/src/services/taxonomies.ts +276 -0
  97. package/src/storage/index.ts +253 -0
  98. package/src/astro/blocks/DialogueBlock.astro +0 -23
  99. package/src/astro/blocks/SceneBlock.astro +0 -65
  100. package/src/astro/blocks/ScriptBlock.astro +0 -59
  101. package/src/core/template-sync.ts +0 -75
  102. package/src/markdown/parser.ts +0 -347
  103. package/src/migration.ts +0 -155
  104. package/src/search/sync.ts +0 -32
@@ -0,0 +1,369 @@
1
+ import { createSignal, onMount, For, Show, 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 RLSModal from "@rimelight/ui/components/modal/RLSModal.tsx"
6
+ import RLSIcon from "@rimelight/ui/components/icon/RLSIcon.tsx"
7
+
8
+ export interface TaxonomyTermRecord {
9
+ id: string
10
+ taxonomy: string
11
+ name: string
12
+ slug: string
13
+ description?: string
14
+ parentId?: string | null
15
+ postCount?: number
16
+ children?: TaxonomyTermRecord[]
17
+ }
18
+
19
+ export interface RimelightTaxonomyManagerProps {
20
+ initialTaxonomy?: string
21
+ }
22
+
23
+ export const RimelightTaxonomyManager: Component<RimelightTaxonomyManagerProps> = (props) => {
24
+ const [activeTaxonomy, setActiveTaxonomy] = createSignal<string>(
25
+ props.initialTaxonomy || "category"
26
+ )
27
+ const [terms, setTerms] = createSignal<TaxonomyTermRecord[]>([])
28
+ const [isLoading, setIsLoading] = createSignal(false)
29
+ const [error, setError] = createSignal<string | null>(null)
30
+
31
+ // Add / Edit Modal State
32
+ const [isModalOpen, setIsModalOpen] = createSignal(false)
33
+ const [editingTermId, setEditingTermId] = createSignal<string | null>(null)
34
+ const [termForm, setTermForm] = createSignal<{
35
+ name: string
36
+ slug: string
37
+ parentId: string
38
+ description: string
39
+ }>({
40
+ name: "",
41
+ slug: "",
42
+ parentId: "",
43
+ description: ""
44
+ })
45
+
46
+ const loadTerms = async () => {
47
+ setIsLoading(true)
48
+ setError(null)
49
+ try {
50
+ const res = await fetch(`/api/cms/taxonomies/${activeTaxonomy()}/terms?includeCounts=true`)
51
+ if (!res.ok) throw new Error("Failed to load taxonomy terms")
52
+ const data = await res.json()
53
+ setTerms(Array.isArray(data) ? data : [])
54
+ } catch (err: any) {
55
+ setError(err.message || "Failed to load terms")
56
+ } finally {
57
+ setIsLoading(false)
58
+ }
59
+ }
60
+
61
+ onMount(() => {
62
+ void loadTerms()
63
+ })
64
+
65
+ const handleTaxonomyTabChange = (tax: string) => {
66
+ setActiveTaxonomy(tax)
67
+ void loadTerms()
68
+ }
69
+
70
+ const openAddModal = (parentId: string = "") => {
71
+ setEditingTermId(null)
72
+ setTermForm({ name: "", slug: "", parentId, description: "" })
73
+ setIsModalOpen(true)
74
+ }
75
+
76
+ const openEditModal = (term: TaxonomyTermRecord) => {
77
+ setEditingTermId(term.id)
78
+ setTermForm({
79
+ name: term.name,
80
+ slug: term.slug,
81
+ parentId: term.parentId || "",
82
+ description: term.description || ""
83
+ })
84
+ setIsModalOpen(true)
85
+ }
86
+
87
+ const handleSaveTerm = async () => {
88
+ const form = termForm()
89
+ if (!form.name.trim()) return
90
+
91
+ const slug =
92
+ form.slug.trim() ||
93
+ form.name
94
+ .toLowerCase()
95
+ .replace(/[^a-z0-9]+/g, "-")
96
+ .replace(/(^-|-$)/g, "")
97
+
98
+ const payload = {
99
+ name: form.name.trim(),
100
+ slug,
101
+ parentId: form.parentId ? form.parentId : null,
102
+ description: form.description.trim()
103
+ }
104
+
105
+ try {
106
+ if (editingTermId()) {
107
+ const res = await fetch(
108
+ `/api/cms/taxonomies/${activeTaxonomy()}/terms/${editingTermId()}`,
109
+ {
110
+ method: "PUT",
111
+ headers: { "Content-Type": "application/json" },
112
+ body: JSON.stringify(payload)
113
+ }
114
+ )
115
+ if (!res.ok) throw new Error("Failed to update term")
116
+ } else {
117
+ const res = await fetch(`/api/cms/taxonomies/${activeTaxonomy()}/terms`, {
118
+ method: "POST",
119
+ headers: { "Content-Type": "application/json" },
120
+ body: JSON.stringify(payload)
121
+ })
122
+ if (!res.ok) throw new Error("Failed to create term")
123
+ }
124
+
125
+ setIsModalOpen(false)
126
+ void loadTerms()
127
+ } catch (err: any) {
128
+ alert(err.message || "Failed to save term")
129
+ }
130
+ }
131
+
132
+ const handleDeleteTerm = async (id: string, name: string) => {
133
+ if (!confirm(`Are you sure you want to delete term "${name}"?`)) return
134
+
135
+ try {
136
+ const res = await fetch(`/api/cms/taxonomies/${activeTaxonomy()}/terms/${id}`, {
137
+ method: "DELETE"
138
+ })
139
+ if (!res.ok) throw new Error("Failed to delete term")
140
+ void loadTerms()
141
+ } catch (err: any) {
142
+ alert(err.message || "Failed to delete term")
143
+ }
144
+ }
145
+
146
+ return (
147
+ <div class="flex flex-col gap-6 w-full">
148
+ {/* Header & Tabs */}
149
+ <div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4 border-b border-default pb-4">
150
+ <div class="flex flex-col">
151
+ <h2 class="text-xl font-bold text-highlighted">Taxonomy Manager</h2>
152
+ <p class="text-xs text-muted mt-0.5">
153
+ Organize posts and pages with hierarchical categories and flat tags.
154
+ </p>
155
+ </div>
156
+
157
+ <div class="flex items-center gap-2">
158
+ <div class="flex p-1 bg-elevated rounded-lg border border-default">
159
+ <button
160
+ type="button"
161
+ onClick={() => handleTaxonomyTabChange("category")}
162
+ class={`px-3 py-1.5 text-xs font-semibold rounded-md transition ${
163
+ activeTaxonomy() === "category"
164
+ ? "bg-background text-primary shadow-sm"
165
+ : "text-muted hover:text-highlighted bg-transparent"
166
+ }`}
167
+ >
168
+ Categories
169
+ </button>
170
+ <button
171
+ type="button"
172
+ onClick={() => handleTaxonomyTabChange("tag")}
173
+ class={`px-3 py-1.5 text-xs font-semibold rounded-md transition ${
174
+ activeTaxonomy() === "tag"
175
+ ? "bg-background text-primary shadow-sm"
176
+ : "text-muted hover:text-highlighted bg-transparent"
177
+ }`}
178
+ >
179
+ Tags
180
+ </button>
181
+ </div>
182
+
183
+ <RLSButton
184
+ variant="solid"
185
+ color="primary"
186
+ size="sm"
187
+ onClick={() => openAddModal()}
188
+ leadingIcon="plus"
189
+ label={`Add ${activeTaxonomy() === "category" ? "Category" : "Tag"}`}
190
+ />
191
+ </div>
192
+ </div>
193
+
194
+ {/* Content Table / Tree */}
195
+ <div class="bg-elevated/40 border border-default rounded-xl overflow-hidden">
196
+ <Show when={isLoading()}>
197
+ <div class="p-8 text-center text-sm text-muted animate-pulse">Loading terms...</div>
198
+ </Show>
199
+
200
+ <Show when={error()}>
201
+ <div class="p-4 text-center text-sm text-error-500">{error()}</div>
202
+ </Show>
203
+
204
+ <Show when={!isLoading() && terms().length === 0}>
205
+ <div class="p-12 text-center flex flex-col items-center justify-center gap-2">
206
+ <RLSIcon name="folder" size="lg" class="text-muted/50" />
207
+ <span class="text-sm font-semibold text-highlighted">
208
+ No {activeTaxonomy()} terms found
209
+ </span>
210
+ <p class="text-xs text-muted max-w-sm">
211
+ Create your first {activeTaxonomy()} term to classify articles and documentation.
212
+ </p>
213
+ <RLSButton
214
+ variant="outline"
215
+ color="primary"
216
+ size="xs"
217
+ onClick={() => openAddModal()}
218
+ label="Create Term"
219
+ class="mt-2"
220
+ />
221
+ </div>
222
+ </Show>
223
+
224
+ <Show when={!isLoading() && terms().length > 0}>
225
+ <div class="overflow-x-auto">
226
+ <table class="w-full text-left text-sm border-collapse">
227
+ <thead>
228
+ <tr class="border-b border-default bg-elevated/70 text-xs font-semibold text-muted uppercase tracking-wider">
229
+ <th class="py-3 px-4">Name</th>
230
+ <th class="py-3 px-4">Slug</th>
231
+ <th class="py-3 px-4">Description</th>
232
+ <th class="py-3 px-4 text-center">Posts</th>
233
+ <th class="py-3 px-4 text-right">Actions</th>
234
+ </tr>
235
+ </thead>
236
+ <tbody class="divide-y divide-default/50">
237
+ <For each={terms()}>
238
+ {(term) => (
239
+ <tr class="hover:bg-elevated/50 transition">
240
+ <td class="py-3 px-4 font-medium text-highlighted flex items-center gap-2">
241
+ {activeTaxonomy() === "tag" ? (
242
+ <RLSBadge variant="soft" color="neutral" size="sm">
243
+ #{term.name}
244
+ </RLSBadge>
245
+ ) : (
246
+ <span>{term.name}</span>
247
+ )}
248
+ </td>
249
+ <td class="py-3 px-4 font-mono text-xs text-muted">{term.slug}</td>
250
+ <td class="py-3 px-4 text-xs text-muted max-w-xs truncate">
251
+ {term.description || "—"}
252
+ </td>
253
+ <td class="py-3 px-4 text-center">
254
+ <span class="inline-flex items-center justify-center px-2 py-0.5 text-xs font-semibold rounded-full bg-primary/10 text-primary">
255
+ {term.postCount ?? 0}
256
+ </span>
257
+ </td>
258
+ <td class="py-3 px-4 text-right">
259
+ <div class="inline-flex items-center gap-1">
260
+ <RLSButton
261
+ variant="ghost"
262
+ color="neutral"
263
+ size="xs"
264
+ square={true}
265
+ onClick={() => openEditModal(term)}
266
+ title="Edit"
267
+ leading={<RLSIcon name="edit" size="xs" />}
268
+ />
269
+ <RLSButton
270
+ variant="ghost"
271
+ color="neutral"
272
+ size="xs"
273
+ square={true}
274
+ onClick={() => handleDeleteTerm(term.id, term.name)}
275
+ title="Delete"
276
+ class="!text-error-500 hover:!text-error-600"
277
+ leading={<RLSIcon name="trash" size="xs" />}
278
+ />
279
+ </div>
280
+ </td>
281
+ </tr>
282
+ )}
283
+ </For>
284
+ </tbody>
285
+ </table>
286
+ </div>
287
+ </Show>
288
+ </div>
289
+
290
+ {/* Add / Edit Modal */}
291
+ <RLSModal
292
+ open={isModalOpen()}
293
+ onClose={() => setIsModalOpen(false)}
294
+ title={editingTermId() ? `Edit ${activeTaxonomy()}` : `Add New ${activeTaxonomy()}`}
295
+ footerSlot={
296
+ <>
297
+ <RLSButton
298
+ variant="outline"
299
+ color="neutral"
300
+ size="sm"
301
+ onClick={() => setIsModalOpen(false)}
302
+ label="Cancel"
303
+ />
304
+ <RLSButton
305
+ variant="solid"
306
+ color="primary"
307
+ size="sm"
308
+ onClick={handleSaveTerm}
309
+ label={editingTermId() ? "Update Term" : "Create Term"}
310
+ />
311
+ </>
312
+ }
313
+ >
314
+ <div class="flex flex-col gap-4 py-2">
315
+ <div class="flex flex-col gap-1">
316
+ <span class="text-xs font-semibold text-muted">Name *</span>
317
+ <RLSInput
318
+ value={termForm().name}
319
+ onInput={(e: InputEvent & { currentTarget: HTMLInputElement }) =>
320
+ setTermForm({ ...termForm(), name: e.currentTarget.value })
321
+ }
322
+ placeholder="e.g. Engineering, Guides, Releases"
323
+ />
324
+ </div>
325
+
326
+ <div class="flex flex-col gap-1">
327
+ <span class="text-xs font-semibold text-muted">Slug (URL identifier)</span>
328
+ <RLSInput
329
+ value={termForm().slug}
330
+ onInput={(e: InputEvent & { currentTarget: HTMLInputElement }) =>
331
+ setTermForm({ ...termForm(), slug: e.currentTarget.value })
332
+ }
333
+ placeholder="Auto-generated if left empty"
334
+ />
335
+ </div>
336
+
337
+ <Show when={activeTaxonomy() === "category"}>
338
+ <div class="flex flex-col gap-1">
339
+ <span class="text-xs font-semibold text-muted">Parent Category</span>
340
+ <select
341
+ value={termForm().parentId}
342
+ onChange={(e) => setTermForm({ ...termForm(), parentId: e.currentTarget.value })}
343
+ 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"
344
+ >
345
+ <option value="">(None - Top Level)</option>
346
+ <For each={terms().filter((t) => t.id !== editingTermId())}>
347
+ {(cat) => <option value={cat.id}>{cat.name}</option>}
348
+ </For>
349
+ </select>
350
+ </div>
351
+ </Show>
352
+
353
+ <div class="flex flex-col gap-1">
354
+ <span class="text-xs font-semibold text-muted">Description</span>
355
+ <textarea
356
+ value={termForm().description}
357
+ onInput={(e: InputEvent & { currentTarget: HTMLTextAreaElement }) =>
358
+ setTermForm({ ...termForm(), description: e.currentTarget.value })
359
+ }
360
+ rows={3}
361
+ placeholder="Optional description of this taxonomy term..."
362
+ 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"
363
+ />
364
+ </div>
365
+ </div>
366
+ </RLSModal>
367
+ </div>
368
+ )
369
+ }