@rimelight/cms 0.0.9 → 0.0.11

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.
@@ -1,11 +1,13 @@
1
1
  ---
2
2
  import CMSDashboardLayout from "../layouts/CMSDashboardLayout.astro";
3
3
  import RLAButton from "@rimelight/ui/components/button/RLAButton.astro";
4
- import RLABadge from "@rimelight/ui/components/badge/RLABadge.astro";
4
+ import RLATable from "@rimelight/ui/components/table/RLATable.astro";
5
+ import type { TableColumn, TableRow } from "@rimelight/ui/components/table/table.ts";
5
6
  import { db } from "virtual:rimelight-cms/db";
6
7
  import { pageTemplates } from "../../schema/page_templates.ts";
7
8
  import { desc } from "drizzle-orm";
8
9
  import { getRelativeLocaleUrl } from "@rimelight/i18n";
10
+ import { ensureDefaultTemplates } from "../../services/template-seeder.ts";
9
11
 
10
12
  export const prerender = false;
11
13
 
@@ -14,6 +16,9 @@ const currentLocale = Astro.currentLocale ?? "en";
14
16
  let templates: any[] = [];
15
17
  if (db) {
16
18
  try {
19
+ // Auto-seed default starter templates if table is empty
20
+ await ensureDefaultTemplates(db, pageTemplates);
21
+
17
22
  templates = await db
18
23
  .select()
19
24
  .from(pageTemplates)
@@ -22,74 +27,239 @@ if (db) {
22
27
  console.warn("Failed to fetch templates:", err);
23
28
  }
24
29
  }
30
+
31
+ // Format template rows
32
+ const formattedTemplates = templates.map((tmpl) => {
33
+ let titleStr = "";
34
+ if (typeof tmpl.title === "object" && tmpl.title !== null) {
35
+ const titleObj = tmpl.title as Record<string, string>;
36
+ titleStr = titleObj[currentLocale] || titleObj["en"] || (Object.values(titleObj)[0] as string | undefined) || "";
37
+ } else {
38
+ titleStr = String(tmpl.title || "Untitled Template");
39
+ }
40
+
41
+ let descStr = "";
42
+ if (typeof tmpl.description === "object" && tmpl.description !== null) {
43
+ descStr = (tmpl.description as Record<string, string>)[currentLocale] || (tmpl.description as Record<string, string>)["en"] || "";
44
+ } else if (tmpl.description) {
45
+ descStr = String(tmpl.description);
46
+ }
47
+
48
+ const pageType = String(tmpl.pageType || "page").toLowerCase();
49
+ const initialBlocksCount = Array.isArray(tmpl.initialBlocks) ? tmpl.initialBlocks.length : 0;
50
+ const propertiesCount = tmpl.defaultProperties ? Object.keys(tmpl.defaultProperties).length : 0;
51
+
52
+ return {
53
+ id: tmpl.id,
54
+ slug: tmpl.slug || "",
55
+ title: titleStr,
56
+ description: descStr,
57
+ pageType,
58
+ version: tmpl.version || 1,
59
+ initialBlocksCount,
60
+ propertiesCount,
61
+ createdAt: tmpl.createdAt ? new Date(tmpl.createdAt) : new Date(),
62
+ updatedAt: tmpl.updatedAt ? new Date(tmpl.updatedAt) : new Date()
63
+ };
64
+ });
65
+
66
+ const typeCounts: Record<string, number> = {};
67
+ for (const t of formattedTemplates) {
68
+ typeCounts[t.pageType] = (typeCounts[t.pageType] || 0) + 1;
69
+ }
70
+
71
+ function getTypeBadgeColor(type: string): "primary" | "secondary" | "success" | "warning" | "error" | "neutral" {
72
+ switch (type) {
73
+ case "blog":
74
+ return "primary";
75
+ case "doc":
76
+ case "documentation":
77
+ return "secondary";
78
+ case "legal":
79
+ case "policy":
80
+ return "warning";
81
+ case "wiki":
82
+ case "species":
83
+ return "success";
84
+ case "character":
85
+ case "hero":
86
+ return "primary";
87
+ case "series":
88
+ return "secondary";
89
+ case "item":
90
+ case "location":
91
+ return "warning";
92
+ case "patchnote":
93
+ case "announcement":
94
+ return "error";
95
+ default:
96
+ return "neutral";
97
+ }
98
+ }
99
+
100
+ function getTypeIcon(type: string): string {
101
+ switch (type) {
102
+ case "blog":
103
+ return "i-lucide-newspaper";
104
+ case "doc":
105
+ case "documentation":
106
+ return "i-lucide-book-open";
107
+ case "character":
108
+ case "hero":
109
+ return "i-lucide-user";
110
+ case "series":
111
+ return "i-lucide-clapperboard";
112
+ case "location":
113
+ return "i-lucide-map-pin";
114
+ case "item":
115
+ return "i-lucide-package";
116
+ case "species":
117
+ return "i-lucide-dna";
118
+ case "skill":
119
+ return "i-lucide-sparkles";
120
+ case "patchnote":
121
+ return "i-lucide-file-code";
122
+ default:
123
+ return "i-lucide-layout-template";
124
+ }
125
+ }
126
+
127
+ const templateColumns: TableColumn<any>[] = [
128
+ {
129
+ accessorKey: "title",
130
+ header: "Template & Slug",
131
+ cell: ({ row }: { row: TableRow<any> }) => {
132
+ const tmpl = row.original;
133
+ return `<div class="flex items-start gap-3 py-1">
134
+ <div class="mt-0.5 size-8 rounded-lg bg-elevated border border-default text-muted flex items-center justify-center shrink-0">
135
+ <span class="${getTypeIcon(tmpl.pageType)} size-4"></span>
136
+ </div>
137
+ <div class="min-w-0 flex-1">
138
+ <a href="${getRelativeLocaleUrl(currentLocale, `/cms/templates/${tmpl.id}/edit`)}" class="font-semibold text-highlighted hover:text-primary transition-colors text-sm truncate block" title="${tmpl.title}">
139
+ ${tmpl.title}
140
+ </a>
141
+ <div class="flex items-center gap-2 mt-1">
142
+ <span class="font-mono text-xs text-muted bg-elevated px-1.5 py-0.5 rounded border border-default/60 truncate max-w-xs">
143
+ ${tmpl.slug}
144
+ </span>
145
+ </div>
146
+ ${tmpl.description ? `<p class="text-xs text-muted/80 mt-1 line-clamp-1 max-w-md">${tmpl.description}</p>` : ''}
147
+ </div>
148
+ </div>`;
149
+ }
150
+ },
151
+ {
152
+ accessorKey: "pageType",
153
+ header: "Page Type",
154
+ size: 140,
155
+ cell: ({ row }: { row: TableRow<any> }) => {
156
+ const tmpl = row.original;
157
+ const color = getTypeBadgeColor(tmpl.pageType);
158
+ return `<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-semibold uppercase tracking-wider bg-${color}-500/10 text-${color}-500 border border-${color}-500/20">${tmpl.pageType}</span>`;
159
+ }
160
+ },
161
+ {
162
+ id: "specs",
163
+ header: "Schema Specs",
164
+ size: 160,
165
+ cell: ({ row }: { row: TableRow<any> }) => {
166
+ const tmpl = row.original;
167
+ return `<div class="flex items-center gap-2 text-xs text-muted font-mono">
168
+ <span>${tmpl.initialBlocksCount} ${tmpl.initialBlocksCount === 1 ? "block" : "blocks"}</span>
169
+ <span class="opacity-40">•</span>
170
+ <span>${tmpl.propertiesCount} ${tmpl.propertiesCount === 1 ? "prop" : "props"}</span>
171
+ </div>`;
172
+ }
173
+ },
174
+ {
175
+ accessorKey: "version",
176
+ header: "Version",
177
+ size: 100,
178
+ cell: ({ row }: { row: TableRow<any> }) => `<span class="px-2 py-0.5 rounded bg-elevated border border-default text-highlighted font-semibold text-xs font-mono">v${row.getValue("version")}</span>`
179
+ },
180
+ {
181
+ id: "actions",
182
+ actions: (row: TableRow<any>) => [
183
+ {
184
+ label: "Create Page",
185
+ icon: "i-lucide-plus",
186
+ to: getRelativeLocaleUrl(currentLocale, `/cms/pages/new?templateId=${row.original.id}`)
187
+ },
188
+ {
189
+ label: "Edit Template",
190
+ icon: "i-lucide-pencil",
191
+ to: getRelativeLocaleUrl(currentLocale, `/cms/templates/${row.original.id}/edit`)
192
+ },
193
+ {
194
+ type: "separator" as const
195
+ },
196
+ {
197
+ label: "Delete Template",
198
+ icon: "i-lucide-trash-2",
199
+ color: "error" as const,
200
+ id: `delete-tmpl-${row.original.id}`
201
+ }
202
+ ]
203
+ }
204
+ ];
25
205
  ---
26
206
 
27
- <CMSDashboardLayout title="Templates" description="Manage CMS templates" activeSection="templates">
207
+ <CMSDashboardLayout title="Templates" description="Manage page templates, property schemas, and starter blocks" activeSection="templates">
28
208
  <Fragment slot="toolbar-right">
29
- <RLAButton href={getRelativeLocaleUrl(currentLocale, "/cms/templates/new")} variant="solid" color="primary" leadingIcon="i-lucide-plus">
30
- Create Template
31
- </RLAButton>
209
+ <div class="flex items-center gap-2">
210
+ <RLAButton
211
+ href={getRelativeLocaleUrl(currentLocale, "/cms/pages")}
212
+ variant="outline"
213
+ color="neutral"
214
+ size="sm"
215
+ leadingIcon="i-lucide-files"
216
+ >
217
+ Pages
218
+ </RLAButton>
219
+ <RLAButton
220
+ href={getRelativeLocaleUrl(currentLocale, "/cms/templates/new")}
221
+ variant="solid"
222
+ color="primary"
223
+ size="sm"
224
+ leadingIcon="i-lucide-plus"
225
+ >
226
+ Create Template
227
+ </RLAButton>
228
+ </div>
32
229
  </Fragment>
33
230
 
34
- <div class="bg-elevated/40 border border-default rounded-xl overflow-hidden">
35
- <table class="w-full text-left text-sm">
36
- <thead class="bg-elevated/70 border-b border-default text-xs font-semibold text-muted uppercase tracking-wider">
37
- <tr>
38
- <th class="px-6 py-3">Name</th>
39
- <th class="px-6 py-3">Type</th>
40
- <th class="px-6 py-3">Version</th>
41
- <th class="px-6 py-3 text-right">Actions</th>
42
- </tr>
43
- </thead>
44
- <tbody class="divide-y divide-default/50">
45
- {templates.map((template: any) => (
46
- <tr class="hover:bg-elevated/50 transition">
47
- <td class="px-6 py-4 whitespace-nowrap">
48
- <div class="text-sm font-medium text-highlighted">{template.title}</div>
49
- <div class="text-xs font-mono text-muted">{template.slug}</div>
50
- </td>
51
- <td class="px-6 py-4 whitespace-nowrap">
52
- <RLABadge variant="soft" color="primary" size="xs">{template.pageType}</RLABadge>
53
- </td>
54
- <td class="px-6 py-4 whitespace-nowrap text-xs font-mono text-muted">
55
- v{template.version}
56
- </td>
57
- <td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-right flex items-center justify-end gap-2">
58
- <RLAButton href={getRelativeLocaleUrl(currentLocale, `/cms/pages/new?templateId=${template.id}`)} variant="ghost" color="primary" size="xs">
59
- Create Page
60
- </RLAButton>
61
- <RLAButton href={getRelativeLocaleUrl(currentLocale, `/cms/templates/${template.id}/edit`)} variant="ghost" color="neutral" size="xs">
62
- Edit
63
- </RLAButton>
64
- <RLAButton variant="ghost" color="error" size="xs" class="delete-template-btn" data-id={template.id}>
65
- Delete
66
- </RLAButton>
67
- </td>
68
- </tr>
69
- ))}
70
- </tbody>
71
- </table>
231
+ <div class="flex flex-col gap-4">
232
+ <RLATable
233
+ data={formattedTemplates}
234
+ columns={templateColumns}
235
+ striped
236
+ hoverable
237
+ searchable
238
+ showColumnsToggle
239
+ exportable
240
+ exportFilename="rimelight-templates-export"
241
+ />
72
242
  </div>
73
243
 
74
244
  <script>
75
- document.querySelectorAll('.delete-template-btn').forEach((btn) => {
76
- btn.addEventListener('click', async () => {
77
- const id = (btn as HTMLElement).dataset["id"];
78
- if (!id) return;
79
-
80
- if (!confirm('Are you sure you want to delete this template?')) return;
81
-
245
+ document.addEventListener("click", async (e) => {
246
+ const target = e.target as HTMLElement;
247
+ const deleteItem = target.closest<HTMLElement>("[data-item-id^='delete-tmpl-']");
248
+ if (deleteItem) {
249
+ const templateId = deleteItem.getAttribute("data-item-id")?.replace("delete-tmpl-", "") || "";
250
+ if (!templateId) return;
251
+ if (!confirm("Are you sure you want to delete this template? This will not delete existing pages created with this template.")) return;
82
252
  try {
83
- const res = await fetch(`/api/cms/templates/${id}`, { method: 'DELETE' });
253
+ const res = await fetch(`/api/cms/templates/${templateId}`, { method: "DELETE" });
84
254
  if (res.ok) {
85
255
  window.location.reload();
86
256
  } else {
87
- alert('Failed to delete template');
257
+ alert("Failed to delete template");
88
258
  }
89
259
  } catch {
90
- alert('Network error deleting template');
260
+ alert("Network error deleting template");
91
261
  }
92
- });
262
+ }
93
263
  });
94
264
  </script>
95
265
  </CMSDashboardLayout>
@@ -1,10 +1,8 @@
1
1
  ---
2
2
  import CMSDashboardLayout from "../layouts/CMSDashboardLayout.astro";
3
3
  import RLAButton from "@rimelight/ui/components/button/RLAButton.astro";
4
- import RLABadge from "@rimelight/ui/components/badge/RLABadge.astro";
5
- import RLAAvatar from "@rimelight/ui/components/avatar/RLAAvatar.astro";
6
4
  import RLATable from "@rimelight/ui/components/table/RLATable.astro";
7
- import type { TableColumn } from "@rimelight/ui/components/table/table.ts";
5
+ import type { TableColumn, TableRow } from "@rimelight/ui/components/table/table.ts";
8
6
 
9
7
  export const prerender = false;
10
8
 
@@ -15,60 +13,108 @@ const users = [
15
13
  { id: "usr_4", name: "Elena Vance", email: "elena@rimelight.com", role: "Reviewer", status: "invited", lastActive: "Never" },
16
14
  ];
17
15
 
18
- const columns: TableColumn[] = [
19
- { id: "user", accessorKey: "name", header: "User", enableSorting: true },
20
- { id: "role", accessorKey: "role", header: "Role", enableSorting: true },
21
- { id: "status", accessorKey: "status", header: "Status", enableSorting: true },
22
- { id: "lastActive", accessorKey: "lastActive", header: "Last Active", enableSorting: true },
23
- { id: "actions", header: "Actions", enableSorting: false, meta: { class: { th: "text-right", td: "text-right" } } }
16
+ const userColumns: TableColumn<any>[] = [
17
+ {
18
+ accessorKey: "name",
19
+ header: "User",
20
+ cell: ({ row }: { row: TableRow<any> }) => {
21
+ const user = row.original;
22
+ return `<div class="flex items-center gap-3 py-1">
23
+ <div class="size-8 rounded-full bg-primary/10 text-primary font-bold text-xs flex items-center justify-center border border-primary/20 shrink-0">
24
+ ${user.name.slice(0, 2).toUpperCase()}
25
+ </div>
26
+ <div class="flex flex-col min-w-0">
27
+ <span class="font-medium text-highlighted text-sm truncate">${user.name}</span>
28
+ <span class="text-xs text-muted font-normal truncate">${user.email}</span>
29
+ </div>
30
+ </div>`;
31
+ }
32
+ },
33
+ {
34
+ accessorKey: "role",
35
+ header: "Role",
36
+ size: 150,
37
+ cell: ({ row }: { row: TableRow<any> }) => {
38
+ const user = row.original;
39
+ const roleColor = user.role === "Super Admin" ? "primary" : user.role === "Editor" ? "secondary" : "neutral";
40
+ return `<span class="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium border ${
41
+ roleColor === 'primary' ? 'bg-primary/10 text-primary border-primary/20' :
42
+ roleColor === 'secondary' ? 'bg-secondary/10 text-secondary border-secondary/20' :
43
+ 'bg-muted/10 text-muted border-default'
44
+ }">${user.role}</span>`;
45
+ }
46
+ },
47
+ {
48
+ accessorKey: "status",
49
+ header: "Status",
50
+ size: 130,
51
+ cell: ({ row }: { row: TableRow<any> }) => {
52
+ const user = row.original;
53
+ if (user.status === "active") {
54
+ return `<div class="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full text-xs font-medium bg-emerald-500/10 text-emerald-500 border border-emerald-500/20">
55
+ <span class="size-1.5 rounded-full bg-emerald-500"></span>
56
+ <span>Active</span>
57
+ </div>`;
58
+ }
59
+ return `<div class="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full text-xs font-medium bg-amber-500/10 text-amber-500 border border-amber-500/20">
60
+ <span class="size-1.5 rounded-full bg-amber-500"></span>
61
+ <span>Invited</span>
62
+ </div>`;
63
+ }
64
+ },
65
+ {
66
+ accessorKey: "lastActive",
67
+ header: "Last Active",
68
+ size: 140,
69
+ cell: ({ row }: { row: TableRow<any> }) => {
70
+ const user = row.original;
71
+ return `<span class="text-xs text-muted">${user.lastActive}</span>`;
72
+ }
73
+ },
74
+ {
75
+ id: "actions",
76
+ actions: (row: TableRow<any>) => [
77
+ {
78
+ label: "Edit Role & Permissions",
79
+ icon: "i-lucide-shield-check",
80
+ id: `edit-role-${row.original.id}`
81
+ },
82
+ {
83
+ label: "View Activity Log",
84
+ icon: "i-lucide-activity",
85
+ id: `activity-${row.original.id}`
86
+ },
87
+ {
88
+ type: "separator" as const
89
+ },
90
+ {
91
+ label: "Remove Member",
92
+ icon: "i-lucide-trash-2",
93
+ color: "error" as const,
94
+ id: `remove-${row.original.id}`
95
+ }
96
+ ]
97
+ }
24
98
  ];
25
99
  ---
26
100
 
27
101
  <CMSDashboardLayout title="CMS Users" description="Manage team permissions, editor roles, and workspace access" activeSection="users">
28
102
  <Fragment slot="toolbar-right">
29
- <RLAButton variant="solid" color="success" leadingIcon="i-lucide-user-plus">
103
+ <RLAButton variant="solid" color="primary" size="sm" leadingIcon="i-lucide-user-plus">
30
104
  Invite Member
31
105
  </RLAButton>
32
106
  </Fragment>
33
107
 
34
- <RLATable
35
- columns={columns}
36
- data={users}
37
- searchable
38
- searchPlaceholder="Search users..."
39
- class="w-full"
40
- >
41
- {users.map((user) => (
42
- <tr class="hover:bg-muted/20 transition-colors" data-slot="tr" data-row-id={user.id}>
43
- <td class="p-3.5 text-sm font-medium text-highlighted flex items-center gap-3" data-col-id="user" data-slot="td">
44
- <RLAAvatar name={user.name} size="sm" />
45
- <div class="flex flex-col">
46
- <span>{user.name}</span>
47
- <span class="text-xs text-muted font-normal">{user.email}</span>
48
- </div>
49
- </td>
50
- <td class="p-3.5 text-sm text-toned" data-col-id="role" data-slot="td">
51
- <RLABadge variant="soft" color={user.role === "Super Admin" ? "primary" : user.role === "Editor" ? "blue" : "neutral"} size="xs">
52
- {user.role}
53
- </RLABadge>
54
- </td>
55
- <td class="p-3.5" data-col-id="status" data-slot="td">
56
- {user.status === "active" ? (
57
- <RLABadge variant="soft" color="green" size="xs">Active</RLABadge>
58
- ) : (
59
- <RLABadge variant="soft" color="yellow" size="xs">Invited</RLABadge>
60
- )}
61
- </td>
62
- <td class="p-3.5 text-sm text-muted text-xs" data-col-id="lastActive" data-slot="td">
63
- {user.lastActive}
64
- </td>
65
- <td class="p-3.5 text-right whitespace-nowrap" data-col-id="actions" data-slot="td">
66
- <div class="flex items-center justify-end gap-2">
67
- <RLAButton variant="ghost" color="neutral" size="xs">Change Role</RLAButton>
68
- <RLAButton variant="ghost" color="error" size="xs" leadingIcon="i-lucide-trash-2" aria-label="Remove user" />
69
- </div>
70
- </td>
71
- </tr>
72
- ))}
73
- </RLATable>
108
+ <div class="flex flex-col gap-4">
109
+ <RLATable
110
+ data={users}
111
+ columns={userColumns}
112
+ striped
113
+ hoverable
114
+ searchable
115
+ showColumnsToggle
116
+ exportable
117
+ exportFilename="rimelight-users-export"
118
+ />
119
+ </div>
74
120
  </CMSDashboardLayout>
@@ -1,6 +1,7 @@
1
1
  ---
2
2
  import CMSDashboardLayout from "../layouts/CMSDashboardLayout.astro";
3
- import RLABadge from "@rimelight/ui/components/badge/RLABadge.astro";
3
+ import RLATable from "@rimelight/ui/components/table/RLATable.astro";
4
+ import type { TableColumn, TableRow } from "@rimelight/ui/components/table/table.ts";
4
5
  import { db } from "virtual:rimelight-cms/db";
5
6
  import { pageVersions, pages } from "../../schema";
6
7
  import { desc, eq } from "drizzle-orm";
@@ -31,44 +32,55 @@ if (db) {
31
32
  console.warn("Failed to fetch versions:", err);
32
33
  }
33
34
  }
35
+
36
+ const columns: TableColumn<any>[] = [
37
+ {
38
+ accessorKey: "pageTitle",
39
+ header: "Page",
40
+ cell: ({ row }: { row: TableRow<any> }) => {
41
+ const v = row.original;
42
+ return `<div>
43
+ <div class="text-sm font-semibold text-highlighted">${v.pageTitle}</div>
44
+ <div class="text-xs font-mono text-muted">/${v.slug}</div>
45
+ </div>`;
46
+ }
47
+ },
48
+ {
49
+ accessorKey: "versionNumber",
50
+ header: "Version",
51
+ size: 110,
52
+ cell: ({ row }: { row: TableRow<any> }) => `<span class="font-mono text-xs text-muted font-bold">v${row.getValue("versionNumber")}</span>`
53
+ },
54
+ {
55
+ accessorKey: "status",
56
+ header: "Status",
57
+ size: 140,
58
+ cell: ({ row }: { row: TableRow<any> }) => {
59
+ const st = row.getValue("status");
60
+ const color = st === "published" ? "success" : st === "pending" ? "warning" : "neutral";
61
+ return `<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-semibold capitalize bg-${color}-500/10 text-${color}-500 border border-${color}-500/20">${st}</span>`;
62
+ }
63
+ },
64
+ {
65
+ accessorKey: "createdAt",
66
+ header: "Created",
67
+ size: 150,
68
+ cell: ({ row }: { row: TableRow<any> }) => `<span class="text-xs text-muted font-mono">${new Date(row.getValue("createdAt")).toLocaleDateString(currentLocale)}</span>`
69
+ }
70
+ ];
34
71
  ---
35
72
 
36
73
  <CMSDashboardLayout title="Versions" description="Manage CMS page versions" activeSection="versions">
37
- <div class="bg-elevated/40 border border-default rounded-xl overflow-hidden">
38
- <table class="w-full text-left text-sm">
39
- <thead class="bg-elevated/70 border-b border-default text-xs font-semibold text-muted uppercase tracking-wider">
40
- <tr>
41
- <th class="px-6 py-3">Page</th>
42
- <th class="px-6 py-3">Version</th>
43
- <th class="px-6 py-3">Status</th>
44
- <th class="px-6 py-3">Created</th>
45
- </tr>
46
- </thead>
47
- <tbody class="divide-y divide-default/50">
48
- {versions.map((version: any) => (
49
- <tr class="hover:bg-elevated/50 transition">
50
- <td class="px-6 py-4 whitespace-nowrap">
51
- <div class="text-sm font-medium text-highlighted">{version.pageTitle}</div>
52
- <div class="text-xs font-mono text-muted">{version.slug}</div>
53
- </td>
54
- <td class="px-6 py-4 whitespace-nowrap text-xs font-mono text-muted">
55
- v{version.versionNumber}
56
- </td>
57
- <td class="px-6 py-4 whitespace-nowrap">
58
- {version.status === 'published' ? (
59
- <RLABadge variant="soft" color="primary" size="xs">Published</RLABadge>
60
- ) : version.status === 'pending' ? (
61
- <RLABadge variant="soft" color="warning" size="xs">Pending Review</RLABadge>
62
- ) : (
63
- <RLABadge variant="soft" color="neutral" size="xs">Draft</RLABadge>
64
- )}
65
- </td>
66
- <td class="px-6 py-4 whitespace-nowrap text-xs text-muted">
67
- {new Date(version.createdAt).toLocaleDateString(currentLocale)}
68
- </td>
69
- </tr>
70
- ))}
71
- </tbody>
72
- </table>
74
+ <div class="flex flex-col gap-4">
75
+ <RLATable
76
+ data={versions}
77
+ columns={columns}
78
+ striped
79
+ hoverable
80
+ searchable
81
+ showColumnsToggle
82
+ exportable
83
+ exportFilename="rimelight-versions-export"
84
+ />
73
85
  </div>
74
86
  </CMSDashboardLayout>
@@ -0,0 +1,25 @@
1
+ ---
2
+ import type { BaseBlock } from "../../core/types/blocks"
3
+
4
+ interface Props {
5
+ block: BaseBlock
6
+ locale?: string
7
+ }
8
+
9
+ const { block } = Astro.props
10
+ const { character = "CHARACTER", parenthetical, line = "" } = block.props || {}
11
+ ---
12
+
13
+ <div class="cms-dialogue-block max-w-xl mx-auto my-3 font-mono text-sm text-center" data-block-id={block.id}>
14
+ <div class="font-bold tracking-wider uppercase text-neutral-900 dark:text-neutral-100">
15
+ {character}
16
+ </div>
17
+ {parenthetical && (
18
+ <div class="text-xs italic text-neutral-500 dark:text-neutral-400">
19
+ ({parenthetical})
20
+ </div>
21
+ )}
22
+ <div class="text-left text-neutral-800 dark:text-neutral-200 mt-1 max-w-md mx-auto leading-relaxed">
23
+ {line}
24
+ </div>
25
+ </div>
@@ -0,0 +1,33 @@
1
+ ---
2
+ import type { BaseBlock } from "../../core/types/blocks"
3
+
4
+ interface Props {
5
+ block: BaseBlock
6
+ locale?: string
7
+ }
8
+
9
+ const { block } = Astro.props
10
+ const { componentCode = "", framework = "astro", height = "300px", width = "100%", editable = false } = block.props || {}
11
+ ---
12
+
13
+ <div
14
+ class="cms-live-preview-block my-6 rounded-2xl border border-neutral-200 dark:border-neutral-800 bg-neutral-900 overflow-hidden shadow-sm"
15
+ data-block-id={block.id}
16
+ >
17
+ <div class="flex items-center justify-between px-4 py-2 bg-neutral-950/80 border-b border-neutral-800 text-xs text-neutral-400">
18
+ <div class="flex items-center gap-2">
19
+ <span class="size-2 rounded-full bg-emerald-500 animate-pulse"></span>
20
+ <span class="font-mono uppercase font-semibold text-neutral-300">Preview ({framework})</span>
21
+ </div>
22
+ {editable && <span class="text-xs text-neutral-500">Interactive</span>}
23
+ </div>
24
+
25
+ <div
26
+ class="p-6 bg-neutral-900 flex items-center justify-center min-h-[160px] overflow-auto"
27
+ style={{ height, width }}
28
+ >
29
+ <div class="text-xs text-neutral-400 font-mono bg-neutral-950/60 p-4 rounded-xl border border-neutral-800 max-w-full overflow-auto">
30
+ <pre><code>{componentCode}</code></pre>
31
+ </div>
32
+ </div>
33
+ </div>