@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,424 @@
1
+ ---
2
+ import CMSDashboardLayout from "../layouts/CMSDashboardLayout.astro";
3
+ import RLABadge from "@rimelight/ui/components/badge/RLABadge.astro";
4
+ import RLAIcon from "@rimelight/ui/components/icon/RLAIcon.astro";
5
+ import RLAInput from "@rimelight/ui/components/input/RLAInput.astro";
6
+ import { getR2Bucket, listR2Files } from "../../storage/index";
7
+ import { storageConfig } from "virtual:rimelight-cms/storage";
8
+ import { getRelativeLocaleUrl } from "@rimelight/i18n";
9
+
10
+ export const prerender = false;
11
+
12
+ const currentLocale = Astro.currentLocale ?? "en";
13
+ const bindingName = storageConfig?.binding ?? "BLOB";
14
+ const bucket = await getR2Bucket(Astro.locals, bindingName);
15
+
16
+ let initialFiles: any[] = [];
17
+ let errorMsg: string | null = null;
18
+
19
+ if (bucket) {
20
+ try {
21
+ const apiBasePath = getRelativeLocaleUrl(currentLocale, "/cms/api/media/file");
22
+ const result = await listR2Files(bucket, {
23
+ prefix: storageConfig?.prefix ?? "media/",
24
+ publicUrl: storageConfig?.publicUrl,
25
+ apiBasePath
26
+ });
27
+ initialFiles = result.files;
28
+ } catch (err: any) {
29
+ errorMsg = err.message || "Failed to query R2 bucket";
30
+ }
31
+ } else {
32
+ errorMsg = `Storage binding "${bindingName}" is not available in runtime environment. Ensure R2 bucket is configured in wrangler.jsonc.`;
33
+ }
34
+
35
+ const apiEndpoint = getRelativeLocaleUrl(currentLocale, "/cms/api/media");
36
+
37
+ function formatFileSize(bytes?: number): string {
38
+ if (!bytes || bytes === 0) return "0 B";
39
+ const k = 1024;
40
+ const sizes = ["B", "KB", "MB", "GB"];
41
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
42
+ return (bytes / Math.pow(k, i)).toFixed(1) + " " + sizes[i];
43
+ }
44
+
45
+ function formatDate(dateStr?: string): string {
46
+ if (!dateStr) return "-";
47
+ try {
48
+ const d = new Date(dateStr);
49
+ return d.toLocaleDateString(undefined, { month: "short", day: "numeric", year: "numeric" });
50
+ } catch {
51
+ return dateStr;
52
+ }
53
+ }
54
+ ---
55
+
56
+ <CMSDashboardLayout title="Assets" description="Cloudflare R2 media library and file storage" activeSection="assets">
57
+ <Fragment slot="toolbar-right">
58
+ <label class="cursor-pointer inline-flex">
59
+ <input type="file" id="media-upload-input" class="hidden" multiple />
60
+ <span class="inline-flex items-center gap-2 px-4 py-2 text-sm font-medium text-white bg-primary hover:bg-primary/90 rounded-md transition-colors shadow-sm">
61
+ <RLAIcon name="i-lucide-upload" class="size-4" />
62
+ <span>Upload Asset</span>
63
+ </span>
64
+ </label>
65
+ </Fragment>
66
+
67
+ <div class="flex flex-col gap-6" id="assets-page-root" data-api-endpoint={apiEndpoint}>
68
+ <!-- Toolbar: Search & Filters -->
69
+ <div class="flex flex-col sm:flex-row items-stretch sm:items-center justify-between gap-4">
70
+ <div class="flex items-center gap-3 flex-1 max-w-md">
71
+ <RLAInput id="media-search-input" placeholder="Search assets by name or key..." leadingIcon="i-lucide-search" class="w-full" />
72
+ </div>
73
+
74
+ <div class="flex items-center gap-1.5 overflow-x-auto pb-1 sm:pb-0" id="media-type-filters">
75
+ <button type="button" data-filter="all" class="px-3 py-1.5 rounded-md text-xs font-medium bg-primary text-white transition-colors filter-btn">
76
+ All
77
+ </button>
78
+ <button type="button" data-filter="image" class="px-3 py-1.5 rounded-md text-xs font-medium bg-muted text-muted-foreground hover:text-highlighted transition-colors filter-btn">
79
+ Images
80
+ </button>
81
+ <button type="button" data-filter="video" class="px-3 py-1.5 rounded-md text-xs font-medium bg-muted text-muted-foreground hover:text-highlighted transition-colors filter-btn">
82
+ Videos
83
+ </button>
84
+ <button type="button" data-filter="audio" class="px-3 py-1.5 rounded-md text-xs font-medium bg-muted text-muted-foreground hover:text-highlighted transition-colors filter-btn">
85
+ Audio
86
+ </button>
87
+ <button type="button" data-filter="document" class="px-3 py-1.5 rounded-md text-xs font-medium bg-muted text-muted-foreground hover:text-highlighted transition-colors filter-btn">
88
+ Documents
89
+ </button>
90
+ </div>
91
+ </div>
92
+
93
+ <!-- Drag & Drop Zone -->
94
+ <div
95
+ id="media-dropzone"
96
+ class="border-2 border-dashed border-default hover:border-primary/60 rounded-xl p-8 flex flex-col items-center justify-center text-center gap-3 transition-colors bg-default/40 group cursor-pointer"
97
+ >
98
+ <div class="size-12 rounded-full bg-primary/10 flex items-center justify-center text-primary group-hover:scale-110 transition-transform">
99
+ <RLAIcon name="i-lucide-cloud-upload" class="size-6 text-primary" />
100
+ </div>
101
+ <div>
102
+ <p class="font-medium text-sm text-highlighted">Drag and drop files here, or click to browse</p>
103
+ <p class="text-xs text-muted mt-1">Supports images, videos, audio, and documents up to R2 bucket limit</p>
104
+ </div>
105
+ <div id="upload-status-indicator" class="hidden text-xs font-medium text-primary flex items-center gap-2 mt-2">
106
+ <span class="size-2 rounded-full bg-primary animate-ping"></span>
107
+ <span>Uploading files to R2...</span>
108
+ </div>
109
+ </div>
110
+
111
+ <!-- Storage Bucket Status Banner if not connected -->
112
+ {errorMsg && (
113
+ <div class="rounded-lg border border-yellow-500/30 bg-yellow-500/10 p-4 flex items-start gap-3 text-sm text-yellow-200">
114
+ <RLAIcon name="i-lucide-alert-triangle" class="size-5 text-yellow-400 shrink-0 mt-0.5" />
115
+ <div class="flex flex-col gap-1">
116
+ <span class="font-semibold text-yellow-300">R2 Storage Binding Notice</span>
117
+ <span class="text-xs text-yellow-200/80">{errorMsg}</span>
118
+ </div>
119
+ </div>
120
+ )}
121
+
122
+ <!-- Empty State -->
123
+ <div id="media-empty-state" class={initialFiles.length === 0 ? "flex flex-col items-center justify-center p-12 text-center" : "hidden flex-col items-center justify-center p-12 text-center"}>
124
+ <RLAIcon name="i-lucide-folder-open" class="size-12 text-muted mb-3" />
125
+ <h3 class="font-medium text-highlighted text-base">No assets found</h3>
126
+ <p class="text-xs text-muted mt-1 max-w-sm">Upload files using the button above or drag and drop files into the upload area.</p>
127
+ </div>
128
+
129
+ <!-- Assets Grid -->
130
+ <div id="media-grid" class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
131
+ {initialFiles.map((asset) => (
132
+ <div
133
+ class="border border-default rounded-lg bg-default overflow-hidden flex flex-col hover:border-primary/50 transition-all group asset-card"
134
+ data-key={asset.key}
135
+ data-name={asset.name}
136
+ data-type={asset.mimeType}
137
+ data-url={asset.url}
138
+ >
139
+ <div class="h-40 bg-muted/30 flex items-center justify-center relative overflow-hidden">
140
+ {asset.mimeType.startsWith("image/") ? (
141
+ <img src={asset.url} alt={asset.name} class="size-full object-cover group-hover:scale-105 transition-transform duration-300" loading="lazy" />
142
+ ) : asset.mimeType.startsWith("video/") ? (
143
+ <div class="flex flex-col items-center gap-2 text-primary">
144
+ <RLAIcon name="i-lucide-film" class="size-10 text-primary" />
145
+ <span class="text-[10px] uppercase font-mono tracking-wider">Video</span>
146
+ </div>
147
+ ) : asset.mimeType.startsWith("audio/") ? (
148
+ <div class="flex flex-col items-center gap-2 text-primary">
149
+ <RLAIcon name="i-lucide-music" class="size-10 text-primary" />
150
+ <span class="text-[10px] uppercase font-mono tracking-wider">Audio</span>
151
+ </div>
152
+ ) : (
153
+ <div class="flex flex-col items-center gap-2 text-muted">
154
+ <RLAIcon name="i-lucide-file-text" class="size-10 text-muted" />
155
+ <span class="text-[10px] uppercase font-mono tracking-wider">Document</span>
156
+ </div>
157
+ )}
158
+
159
+ <RLABadge variant="solid" color="neutral" size="xs" class="absolute top-2 right-2 uppercase font-mono text-[10px] backdrop-blur-md bg-default/80">
160
+ {asset.mimeType.split("/")[1] || "FILE"}
161
+ </RLABadge>
162
+ </div>
163
+
164
+ <div class="p-4 flex flex-col gap-1 flex-1">
165
+ <p class="font-medium text-sm text-highlighted truncate" title={asset.name}>{asset.name}</p>
166
+ <div class="flex items-center justify-between text-xs text-muted mt-1">
167
+ <span>{formatFileSize(asset.size)}</span>
168
+ <span>{formatDate(asset.uploadedAt)}</span>
169
+ </div>
170
+
171
+ <div class="flex items-center justify-between gap-2 mt-4 pt-3 border-t border-default/50">
172
+ <button
173
+ type="button"
174
+ class="copy-url-btn text-xs font-medium text-muted hover:text-highlighted flex items-center gap-1.5 transition-colors"
175
+ data-url={asset.url}
176
+ >
177
+ <RLAIcon name="i-lucide-copy" class="size-3.5" />
178
+ <span>Copy URL</span>
179
+ </button>
180
+
181
+ <button
182
+ type="button"
183
+ class="delete-asset-btn text-xs font-medium text-error hover:text-error/80 flex items-center gap-1 transition-colors"
184
+ data-key={asset.key}
185
+ >
186
+ <RLAIcon name="i-lucide-trash-2" class="size-3.5" />
187
+ <span>Delete</span>
188
+ </button>
189
+ </div>
190
+ </div>
191
+ </div>
192
+ ))}
193
+ </div>
194
+ </div>
195
+ </CMSDashboardLayout>
196
+
197
+ <script>
198
+ (function () {
199
+ const root = document.getElementById("assets-page-root");
200
+ if (!root) return;
201
+
202
+ const apiEndpoint = root.dataset.apiEndpoint || "/cms/api/media";
203
+ const uploadInput = document.getElementById("media-upload-input") as HTMLInputElement | null;
204
+ const dropzone = document.getElementById("media-dropzone");
205
+ const indicator = document.getElementById("upload-status-indicator");
206
+ const searchInput = document.getElementById("media-search-input") as HTMLInputElement | null;
207
+ const filterBtns = document.querySelectorAll(".filter-btn");
208
+ const mediaGrid = document.getElementById("media-grid");
209
+ const emptyState = document.getElementById("media-empty-state");
210
+
211
+ let currentFilter = "all";
212
+ let searchQuery = "";
213
+
214
+ // Trigger input on dropzone click
215
+ dropzone?.addEventListener("click", () => {
216
+ uploadInput?.click();
217
+ });
218
+
219
+ // Handle drag and drop
220
+ dropzone?.addEventListener("dragover", (e) => {
221
+ e.preventDefault();
222
+ dropzone.classList.add("border-primary", "bg-primary/5");
223
+ });
224
+
225
+ dropzone?.addEventListener("dragleave", () => {
226
+ dropzone.classList.remove("border-primary", "bg-primary/5");
227
+ });
228
+
229
+ dropzone?.addEventListener("drop", (e) => {
230
+ e.preventDefault();
231
+ dropzone.classList.remove("border-primary", "bg-primary/5");
232
+ if (e.dataTransfer?.files && e.dataTransfer.files.length > 0) {
233
+ uploadFiles(e.dataTransfer.files);
234
+ }
235
+ });
236
+
237
+ uploadInput?.addEventListener("change", (e) => {
238
+ const files = (e.target as HTMLInputElement).files;
239
+ if (files && files.length > 0) {
240
+ uploadFiles(files);
241
+ }
242
+ });
243
+
244
+ async function uploadFiles(fileList: FileList) {
245
+ if (!indicator) return;
246
+ indicator.classList.remove("hidden");
247
+
248
+ for (let i = 0; i < fileList.length; i++) {
249
+ const file = fileList[i];
250
+ if (!file) continue;
251
+ const formData = new FormData();
252
+ formData.append("file", file);
253
+
254
+ try {
255
+ const res = await fetch(apiEndpoint, {
256
+ method: "POST",
257
+ body: formData
258
+ });
259
+ const json = await res.json();
260
+ if (json.success && json.data?.file) {
261
+ appendAssetCard(json.data.file);
262
+ } else {
263
+ alert(json.error || "Failed to upload file");
264
+ }
265
+ } catch (err: any) {
266
+ alert(err.message || "Upload error");
267
+ }
268
+ }
269
+
270
+ indicator.classList.add("hidden");
271
+ if (uploadInput) uploadInput.value = "";
272
+ filterCards();
273
+ }
274
+
275
+ function appendAssetCard(asset: any) {
276
+ if (!mediaGrid) return;
277
+ const isImg = asset.mimeType?.startsWith("image/");
278
+ const isVid = asset.mimeType?.startsWith("video/");
279
+ const isAud = asset.mimeType?.startsWith("audio/");
280
+
281
+ const card = document.createElement("div");
282
+ card.className = "border border-default rounded-lg bg-default overflow-hidden flex flex-col hover:border-primary/50 transition-all group asset-card animate-in fade-in";
283
+ card.dataset.key = asset.key;
284
+ card.dataset.name = asset.name;
285
+ card.dataset.type = asset.mimeType;
286
+ card.dataset.url = asset.url;
287
+
288
+ card.innerHTML = `
289
+ <div class="h-40 bg-muted/30 flex items-center justify-center relative overflow-hidden">
290
+ ${isImg ? `<img src="${asset.url}" alt="${asset.name}" class="size-full object-cover group-hover:scale-105 transition-transform duration-300" loading="lazy" />` :
291
+ isVid ? `<div class="flex flex-col items-center gap-2 text-primary"><span class="size-10 text-primary">🎬</span><span class="text-[10px] uppercase font-mono tracking-wider">Video</span></div>` :
292
+ isAud ? `<div class="flex flex-col items-center gap-2 text-primary"><span class="size-10 text-primary">🎵</span><span class="text-[10px] uppercase font-mono tracking-wider">Audio</span></div>` :
293
+ `<div class="flex flex-col items-center gap-2 text-muted"><span class="size-10 text-muted">📄</span><span class="text-[10px] uppercase font-mono tracking-wider">Document</span></div>`}
294
+ <span class="absolute top-2 right-2 uppercase font-mono text-[10px] backdrop-blur-md bg-default/80 px-2 py-0.5 rounded text-highlighted">
295
+ ${asset.mimeType?.split("/")[1] || "FILE"}
296
+ </span>
297
+ </div>
298
+ <div class="p-4 flex flex-col gap-1 flex-1">
299
+ <p class="font-medium text-sm text-highlighted truncate" title="${asset.name}">${asset.name}</p>
300
+ <div class="flex items-center justify-between text-xs text-muted mt-1">
301
+ <span>${formatBytes(asset.size)}</span>
302
+ <span>Just now</span>
303
+ </div>
304
+ <div class="flex items-center justify-between gap-2 mt-4 pt-3 border-t border-default/50">
305
+ <button type="button" class="copy-url-btn text-xs font-medium text-muted hover:text-highlighted flex items-center gap-1.5 transition-colors" data-url="${asset.url}">
306
+ <span>Copy URL</span>
307
+ </button>
308
+ <button type="button" class="delete-asset-btn text-xs font-medium text-error hover:text-error/80 flex items-center gap-1 transition-colors" data-key="${asset.key}">
309
+ <span>Delete</span>
310
+ </button>
311
+ </div>
312
+ </div>
313
+ `;
314
+
315
+ mediaGrid.prepend(card);
316
+ bindCardEvents(card);
317
+ }
318
+
319
+ function bindCardEvents(parent: HTMLElement | Document) {
320
+ parent.querySelectorAll(".copy-url-btn").forEach((btn) => {
321
+ btn.addEventListener("click", async (e) => {
322
+ e.stopPropagation();
323
+ const url = (btn as HTMLElement).dataset.url;
324
+ if (url) {
325
+ const fullUrl = url.startsWith("http") ? url : window.location.origin + url;
326
+ await navigator.clipboard.writeText(fullUrl);
327
+ const originalText = btn.querySelector("span")?.textContent || "Copy URL";
328
+ const span = btn.querySelector("span");
329
+ if (span) span.textContent = "Copied!";
330
+ setTimeout(() => {
331
+ if (span) span.textContent = originalText;
332
+ }, 2000);
333
+ }
334
+ });
335
+ });
336
+
337
+ parent.querySelectorAll(".delete-asset-btn").forEach((btn) => {
338
+ btn.addEventListener("click", async (e) => {
339
+ e.stopPropagation();
340
+ const key = (btn as HTMLElement).dataset.key;
341
+ if (!key) return;
342
+ if (!confirm(`Are you sure you want to delete this file from R2 storage?\nKey: ${key}`)) return;
343
+
344
+ try {
345
+ const res = await fetch(`${apiEndpoint}?key=${encodeURIComponent(key)}`, {
346
+ method: "DELETE"
347
+ });
348
+ const json = await res.json();
349
+ if (json.success) {
350
+ const card = btn.closest(".asset-card");
351
+ card?.remove();
352
+ filterCards();
353
+ } else {
354
+ alert(json.error || "Failed to delete asset");
355
+ }
356
+ } catch (err: any) {
357
+ alert(err.message || "Failed to delete file");
358
+ }
359
+ });
360
+ });
361
+ }
362
+
363
+ bindCardEvents(document);
364
+
365
+ // Search and filter handling
366
+ function filterCards() {
367
+ const cards = document.querySelectorAll<HTMLElement>(".asset-card");
368
+ let visibleCount = 0;
369
+
370
+ cards.forEach((card) => {
371
+ const name = (card.dataset.name || "").toLowerCase();
372
+ const key = (card.dataset.key || "").toLowerCase();
373
+ const type = (card.dataset.type || "").toLowerCase();
374
+
375
+ const matchesSearch = !searchQuery || name.includes(searchQuery) || key.includes(searchQuery);
376
+ let matchesFilter = true;
377
+
378
+ if (currentFilter === "image") matchesFilter = type.startsWith("image/");
379
+ else if (currentFilter === "video") matchesFilter = type.startsWith("video/");
380
+ else if (currentFilter === "audio") matchesFilter = type.startsWith("audio/");
381
+ else if (currentFilter === "document") matchesFilter = type.includes("pdf") || type.includes("text") || type.includes("doc");
382
+
383
+ if (matchesSearch && matchesFilter) {
384
+ card.classList.remove("hidden");
385
+ visibleCount++;
386
+ } else {
387
+ card.classList.add("hidden");
388
+ }
389
+ });
390
+
391
+ if (emptyState) {
392
+ emptyState.classList.toggle("hidden", visibleCount > 0);
393
+ emptyState.classList.toggle("flex", visibleCount === 0);
394
+ }
395
+ }
396
+
397
+ searchInput?.addEventListener("input", (e) => {
398
+ searchQuery = (e.target as HTMLInputElement).value.toLowerCase().trim();
399
+ filterCards();
400
+ });
401
+
402
+ filterBtns.forEach((btn) => {
403
+ btn.addEventListener("click", () => {
404
+ filterBtns.forEach((b) => {
405
+ b.classList.remove("bg-primary", "text-white");
406
+ b.classList.add("bg-muted", "text-muted-foreground");
407
+ });
408
+ btn.classList.remove("bg-muted", "text-muted-foreground");
409
+ btn.classList.add("bg-primary", "text-white");
410
+
411
+ currentFilter = (btn as HTMLElement).dataset.filter || "all";
412
+ filterCards();
413
+ });
414
+ });
415
+
416
+ function formatBytes(bytes: number): string {
417
+ if (!bytes || bytes === 0) return "0 B";
418
+ const k = 1024;
419
+ const sizes = ["B", "KB", "MB", "GB"];
420
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
421
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + " " + sizes[i];
422
+ }
423
+ })();
424
+ </script>
@@ -0,0 +1,62 @@
1
+ ---
2
+ import CMSDashboardLayout from "../layouts/CMSDashboardLayout.astro";
3
+ import RLAButton from "@rimelight/ui/components/button/RLAButton.astro";
4
+ import RLABadge from "@rimelight/ui/components/badge/RLABadge.astro";
5
+ import RLAIcon from "@rimelight/ui/components/icon/RLAIcon.astro";
6
+
7
+ export const prerender = false;
8
+
9
+ const schemaFields = [
10
+ { key: "displayName", label: "Display Name", type: "string", required: true, description: "Primary author attribution name" },
11
+ { key: "avatarUrl", label: "Avatar Image", type: "image", required: false, description: "Author headshot or logo URL" },
12
+ { key: "role", label: "Role / Job Title", type: "string", required: false, description: "e.g. Lead Designer, Staff Writer" },
13
+ { key: "bio", label: "Biography", type: "rich_text", required: false, description: "Short author profile description" },
14
+ { key: "socialLinks", label: "Social Links", type: "array", required: false, description: "Twitter, GitHub, LinkedIn, Website URLs" },
15
+ { key: "organization", label: "Organization", type: "relation", required: false, description: "Associated company or team entity" },
16
+ ];
17
+ ---
18
+
19
+ <CMSDashboardLayout title="Byline Schema" description="Configure author attribution fields and metadata requirements" activeSection="byline-schema">
20
+ <Fragment slot="toolbar-right">
21
+ <RLAButton variant="solid" color="primary" leadingIcon="i-lucide-save">
22
+ Save Schema
23
+ </RLAButton>
24
+ </Fragment>
25
+
26
+ <div class="border border-default rounded-lg bg-default divide-y divide-default">
27
+ <div class="p-6 flex items-center justify-between">
28
+ <div>
29
+ <h3 class="font-semibold text-highlighted text-base">Attribution Fields</h3>
30
+ <p class="text-sm text-muted">Fields available across all author byline entities</p>
31
+ </div>
32
+ <RLAButton variant="outline" color="neutral" size="sm" leadingIcon="i-lucide-plus">
33
+ Add Custom Field
34
+ </RLAButton>
35
+ </div>
36
+
37
+ {schemaFields.map((field) => (
38
+ <div class="p-4 sm:p-6 flex items-center justify-between gap-4 hover:bg-muted/10 transition-colors">
39
+ <div class="flex items-center gap-4 min-w-0">
40
+ <RLAIcon name="i-lucide-grip-vertical" class="size-4 text-muted shrink-0 cursor-grab" />
41
+ <div class="flex flex-col truncate">
42
+ <div class="flex items-center gap-2">
43
+ <span class="font-medium text-highlighted text-sm">{field.label}</span>
44
+ <span class="font-mono text-xs text-muted">({field.key})</span>
45
+ {field.required && (
46
+ <RLABadge variant="soft" color="error" size="xs">Required</RLABadge>
47
+ )}
48
+ </div>
49
+ <span class="text-xs text-muted truncate mt-0.5">{field.description}</span>
50
+ </div>
51
+ </div>
52
+
53
+ <div class="flex items-center gap-3 shrink-0">
54
+ <RLABadge variant="soft" color="neutral" size="xs" class="font-mono">
55
+ {field.type}
56
+ </RLABadge>
57
+ <RLAButton variant="ghost" color="neutral" size="xs" leadingIcon="i-lucide-settings-2" aria-label="Configure field" />
58
+ </div>
59
+ </div>
60
+ ))}
61
+ </div>
62
+ </CMSDashboardLayout>
@@ -0,0 +1,16 @@
1
+ ---
2
+ import CMSDashboardLayout from "../layouts/CMSDashboardLayout.astro";
3
+ import { RimelightBylinesManager } from "../../client/components";
4
+
5
+ export const prerender = false;
6
+ ---
7
+
8
+ <CMSDashboardLayout
9
+ title="Bylines"
10
+ description="Manage author profiles and multi-author attributions"
11
+ activeSection="bylines"
12
+ >
13
+ <div class="w-full">
14
+ <RimelightBylinesManager client:only="solid" />
15
+ </div>
16
+ </CMSDashboardLayout>
@@ -0,0 +1,21 @@
1
+ ---
2
+ import CMSDashboardLayout from "../layouts/CMSDashboardLayout.astro";
3
+ import { RimelightTaxonomyMasterDetail } from "../../client/components";
4
+
5
+ export const prerender = false;
6
+ ---
7
+
8
+ <CMSDashboardLayout
9
+ title="Categories"
10
+ description="Manage hierarchical categories for content organization"
11
+ activeSection="categories"
12
+ >
13
+ <div class="w-full">
14
+ <RimelightTaxonomyMasterDetail
15
+ client:only="solid"
16
+ taxonomy="category"
17
+ title="Categories"
18
+ description="Organize your content into hierarchical topics and sections."
19
+ />
20
+ </div>
21
+ </CMSDashboardLayout>
@@ -0,0 +1,55 @@
1
+ ---
2
+ import CMSDashboardLayout from "../layouts/CMSDashboardLayout.astro";
3
+ import RLAButton from "@rimelight/ui/components/button/RLAButton.astro";
4
+ import RLABadge from "@rimelight/ui/components/badge/RLABadge.astro";
5
+ import RLAIcon from "@rimelight/ui/components/icon/RLAIcon.astro";
6
+
7
+ export const prerender = false;
8
+
9
+ const contentTypes = [
10
+ { id: "page", name: "Page", slug: "pages", icon: "i-lucide-file-text", description: "Standard standalone marketing and dynamic landing pages", count: 12, system: true },
11
+ { id: "blog", name: "Blog Post", slug: "blog", icon: "i-lucide-newspaper", description: "Articles and editorial news posts with bylines and dates", count: 8, system: false },
12
+ { id: "doc", name: "Documentation", slug: "docs", icon: "i-lucide-book-open", description: "Hierarchical product documentation and API references", count: 24, system: false },
13
+ { id: "legal", name: "Legal Document", slug: "legal", icon: "i-lucide-scale", description: "Version-controlled Terms of Service, Privacy Policies", count: 4, system: false },
14
+ ];
15
+ ---
16
+
17
+ <CMSDashboardLayout title="Content Types" description="Define schemas, block rules, and collection models" activeSection="content-types">
18
+ <Fragment slot="toolbar-right">
19
+ <RLAButton variant="solid" color="success" leadingIcon="i-lucide-plus">
20
+ Create Content Type
21
+ </RLAButton>
22
+ </Fragment>
23
+
24
+ <div class="grid grid-cols-1 md:grid-cols-2 gap-6">
25
+ {contentTypes.map((ct) => (
26
+ <div class="border border-default rounded-lg bg-default p-6 flex flex-col justify-between hover:border-primary/50 transition-colors">
27
+ <div class="flex flex-col gap-3">
28
+ <div class="flex items-center justify-between">
29
+ <div class="flex items-center gap-3">
30
+ <div class="size-10 rounded-lg bg-primary/10 flex items-center justify-center text-primary">
31
+ <RLAIcon name={ct.icon} class="size-5 text-primary" />
32
+ </div>
33
+ <div>
34
+ <h3 class="font-semibold text-highlighted text-base">{ct.name}</h3>
35
+ <span class="font-mono text-xs text-muted">/{ct.slug}</span>
36
+ </div>
37
+ </div>
38
+ <RLABadge variant="soft" color={ct.system ? "neutral" : "primary"} size="xs">
39
+ {ct.system ? "System" : "Custom"}
40
+ </RLABadge>
41
+ </div>
42
+ <p class="text-sm text-muted">{ct.description}</p>
43
+ </div>
44
+
45
+ <div class="flex items-center justify-between mt-6 pt-4 border-t border-default/50 text-sm">
46
+ <span class="text-xs text-muted">{ct.count} entries</span>
47
+ <div class="flex items-center gap-2">
48
+ <RLAButton variant="ghost" color="primary" size="xs">Edit Schema</RLAButton>
49
+ <RLAButton variant="ghost" color="neutral" size="xs">Permissions</RLAButton>
50
+ </div>
51
+ </div>
52
+ </div>
53
+ ))}
54
+ </div>
55
+ </CMSDashboardLayout>
@@ -0,0 +1,61 @@
1
+ ---
2
+ import CMSDashboardLayout from "../layouts/CMSDashboardLayout.astro";
3
+ import RLACard from "@rimelight/ui/components/card/RLACard.astro";
4
+ import RLAButton from "@rimelight/ui/components/button/RLAButton.astro";
5
+ import { getRelativeLocaleUrl } from "@rimelight/i18n";
6
+
7
+ export const prerender = false;
8
+
9
+ const currentLocale = Astro.currentLocale ?? "en";
10
+ ---
11
+
12
+ <CMSDashboardLayout title="CMS Admin" description="Content Management System administration" activeSection="dashboard">
13
+ <Fragment slot="toolbar-left">
14
+ <RLAButton variant="ghost" color="neutral" size="sm" leadingIcon="i-lucide-calendar">
15
+ Overview
16
+ </RLAButton>
17
+ </Fragment>
18
+ <Fragment slot="toolbar-right">
19
+ <RLAButton href={getRelativeLocaleUrl(currentLocale, "/cms/templates/new")} variant="solid" color="primary" size="sm" leadingIcon="i-lucide-plus">
20
+ Create Template
21
+ </RLAButton>
22
+ </Fragment>
23
+
24
+ <div class="grid grid-cols-1 md:grid-cols-3 gap-6 mb-8">
25
+ <RLACard title="Pages" description="Create and edit pages">
26
+ <div class="mt-4">
27
+ <RLAButton href={getRelativeLocaleUrl(currentLocale, "/cms/pages")} variant="ghost" color="primary" trailingIcon="i-lucide-arrow-right">
28
+ Manage Pages
29
+ </RLAButton>
30
+ </div>
31
+ </RLACard>
32
+ <RLACard title="Categories & Tags" description="Organize your content structure">
33
+ <div class="mt-4 flex gap-2">
34
+ <RLAButton href={getRelativeLocaleUrl(currentLocale, "/cms/categories")} variant="ghost" color="primary">
35
+ Categories
36
+ </RLAButton>
37
+ <RLAButton href={getRelativeLocaleUrl(currentLocale, "/cms/tags")} variant="ghost" color="primary">
38
+ Tags
39
+ </RLAButton>
40
+ </div>
41
+ </RLACard>
42
+ <RLACard title="Bylines" description="Manage author attribution">
43
+ <div class="mt-4">
44
+ <RLAButton href={getRelativeLocaleUrl(currentLocale, "/cms/bylines")} variant="ghost" color="primary" trailingIcon="i-lucide-arrow-right">
45
+ Manage Bylines
46
+ </RLAButton>
47
+ </div>
48
+ </RLACard>
49
+ </div>
50
+
51
+ <RLACard title="Quick Actions">
52
+ <div class="flex flex-wrap gap-4 mt-2">
53
+ <RLAButton href={getRelativeLocaleUrl(currentLocale, "/cms/pages/new")} variant="solid" color="primary" leadingIcon="i-lucide-plus">
54
+ Create Page
55
+ </RLAButton>
56
+ <RLAButton href={getRelativeLocaleUrl(currentLocale, "/cms/templates/new")} variant="outline" color="neutral" leadingIcon="i-lucide-plus">
57
+ Create Template
58
+ </RLAButton>
59
+ </div>
60
+ </RLACard>
61
+ </CMSDashboardLayout>