@rimelight/cms 0.0.2 → 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 (112) 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 -102
  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 -86
  70. package/src/core/types/blocks.ts +6 -1
  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 -8
  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 -4
  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/ApiDocumentationBlock.astro +0 -350
  99. package/src/astro/blocks/DialogueBlock.astro +0 -23
  100. package/src/astro/blocks/LivePreviewBlock.astro +0 -27
  101. package/src/astro/blocks/SceneBlock.astro +0 -65
  102. package/src/astro/blocks/ScriptBlock.astro +0 -59
  103. package/src/core/template-sync.ts +0 -75
  104. package/src/markdown/parser.ts +0 -347
  105. package/src/migration-tools.ts +0 -334
  106. package/src/migration.ts +0 -155
  107. package/src/schema/component_dependencies.ts +0 -21
  108. package/src/schema/component_metadata.ts +0 -31
  109. package/src/schema/component_metadata_cache.ts +0 -15
  110. package/src/schema/component_versions.ts +0 -19
  111. package/src/search/sync.ts +0 -32
  112. package/src/services/component-metadata.ts +0 -134
@@ -0,0 +1,253 @@
1
+ export interface StorageFile {
2
+ key: string
3
+ name: string
4
+ size: number
5
+ mimeType: string
6
+ uploadedAt: string
7
+ url: string
8
+ etag?: string
9
+ customMetadata?: Record<string, string>
10
+ }
11
+
12
+ export interface StorageListResult {
13
+ files: StorageFile[]
14
+ truncated: boolean
15
+ cursor?: string
16
+ }
17
+
18
+ export interface R2StorageOptions {
19
+ binding?: string | undefined
20
+ publicUrl?: string | undefined
21
+ prefix?: string | undefined
22
+ }
23
+
24
+ export interface StorageConfig {
25
+ provider: "r2" | "local" | "s3"
26
+ binding?: string | undefined
27
+ publicUrl?: string | undefined
28
+ prefix?: string | undefined
29
+ }
30
+
31
+ export interface StorageListOptions {
32
+ prefix?: string | undefined
33
+ cursor?: string | undefined
34
+ limit?: number | undefined
35
+ publicUrl?: string | undefined
36
+ apiBasePath?: string | undefined
37
+ }
38
+
39
+ export interface StorageUploadOptions {
40
+ key?: string | undefined
41
+ prefix?: string | undefined
42
+ publicUrl?: string | undefined
43
+ apiBasePath?: string | undefined
44
+ }
45
+
46
+ export function r2(options: R2StorageOptions = {}): StorageConfig {
47
+ return {
48
+ provider: "r2",
49
+ binding: options.binding ?? "BLOB",
50
+ publicUrl: options.publicUrl,
51
+ prefix: options.prefix ?? "media/"
52
+ }
53
+ }
54
+
55
+ /**
56
+ * Resolves Cloudflare R2 bucket instance from Cloudflare env or Astro locals.
57
+ */
58
+ export async function getR2Bucket(locals?: any, bindingName = "BLOB"): Promise<any> {
59
+ // 1. Try cloudflare:workers standard export (Astro v6+)
60
+ try {
61
+ // @ts-ignore
62
+ const cf = await import("cloudflare:workers")
63
+ if (cf?.env && cf.env[bindingName]) {
64
+ return cf.env[bindingName]
65
+ }
66
+ } catch {}
67
+
68
+ // 2. Direct locals property check (locals.env or locals[bindingName])
69
+ if (locals) {
70
+ if (locals.env && locals.env[bindingName]) {
71
+ return locals.env[bindingName]
72
+ }
73
+ if (locals[bindingName]) {
74
+ return locals[bindingName]
75
+ }
76
+ }
77
+
78
+ // 3. Global process.env fallback
79
+ if (typeof process !== "undefined" && process.env && (process.env as any)[bindingName]) {
80
+ return (process.env as any)[bindingName]
81
+ }
82
+
83
+ return null
84
+ }
85
+
86
+ /**
87
+ * Lists files from R2 Bucket under configured prefix.
88
+ */
89
+ export async function listR2Files(
90
+ bucket: any,
91
+ options: StorageListOptions = {}
92
+ ): Promise<StorageListResult> {
93
+ if (!bucket || typeof bucket.list !== "function") {
94
+ return { files: [], truncated: false }
95
+ }
96
+
97
+ const prefix = options.prefix ?? "media/"
98
+ const limit = options.limit ?? 50
99
+
100
+ const result = await bucket.list({
101
+ prefix,
102
+ cursor: options.cursor,
103
+ limit
104
+ })
105
+
106
+ const files: StorageFile[] = (result.objects || []).map((obj: any) => {
107
+ const rawKey = obj.key as string
108
+ const name = rawKey.startsWith(prefix) ? rawKey.slice(prefix.length) : rawKey
109
+ const cleanedName = name.includes("/") ? name.split("/").pop()! : name
110
+ const mimeType = obj.httpMetadata?.contentType || guessMimeType(rawKey)
111
+
112
+ // Resolve public URL: custom domain, or proxy endpoint
113
+ let url = ""
114
+ if (options.publicUrl) {
115
+ const baseUrl = options.publicUrl.endsWith("/")
116
+ ? options.publicUrl.slice(0, -1)
117
+ : options.publicUrl
118
+ url = `${baseUrl}/${rawKey}`
119
+ } else if (options.apiBasePath) {
120
+ const baseApi = options.apiBasePath.endsWith("/")
121
+ ? options.apiBasePath.slice(0, -1)
122
+ : options.apiBasePath
123
+ url = `${baseApi}/${rawKey}`
124
+ } else {
125
+ url = `/cms/api/media/${rawKey}`
126
+ }
127
+
128
+ return {
129
+ key: rawKey,
130
+ name: cleanedName,
131
+ size: obj.size,
132
+ mimeType,
133
+ uploadedAt: obj.uploaded ? new Date(obj.uploaded).toISOString() : new Date().toISOString(),
134
+ url,
135
+ etag: obj.etag,
136
+ customMetadata: obj.customMetadata
137
+ }
138
+ })
139
+
140
+ return {
141
+ files,
142
+ truncated: result.truncated,
143
+ cursor: result.cursor
144
+ }
145
+ }
146
+
147
+ /**
148
+ * Uploads a file buffer/stream into R2 Bucket.
149
+ */
150
+ export async function uploadR2File(
151
+ bucket: any,
152
+ file: File,
153
+ options: StorageUploadOptions = {}
154
+ ): Promise<StorageFile> {
155
+ if (!bucket || typeof bucket.put !== "function") {
156
+ throw new Error("R2 bucket binding is not available in runtime environment.")
157
+ }
158
+
159
+ const prefix = options.prefix ?? "media/"
160
+ const timestamp = Date.now()
161
+ const safeName = file.name.replace(/[^a-zA-Z0-9._-]/g, "_")
162
+ const randomSuffix = Math.random().toString(36).substring(2, 8)
163
+ const key = options.key ?? `${prefix}${timestamp}-${randomSuffix}/${safeName}`
164
+
165
+ const arrayBuffer = await file.arrayBuffer()
166
+
167
+ const r2Object = await bucket.put(key, arrayBuffer, {
168
+ httpMetadata: {
169
+ contentType: file.type || guessMimeType(safeName),
170
+ contentDisposition: `inline; filename="${safeName}"`
171
+ },
172
+ customMetadata: {
173
+ originalName: file.name,
174
+ uploadedAt: new Date().toISOString()
175
+ }
176
+ })
177
+
178
+ let url = ""
179
+ if (options.publicUrl) {
180
+ const baseUrl = options.publicUrl.endsWith("/")
181
+ ? options.publicUrl.slice(0, -1)
182
+ : options.publicUrl
183
+ url = `${baseUrl}/${key}`
184
+ } else if (options.apiBasePath) {
185
+ const baseApi = options.apiBasePath.endsWith("/")
186
+ ? options.apiBasePath.slice(0, -1)
187
+ : options.apiBasePath
188
+ url = `${baseApi}/${key}`
189
+ } else {
190
+ url = `/cms/api/media/${key}`
191
+ }
192
+
193
+ return {
194
+ key,
195
+ name: safeName,
196
+ size: file.size,
197
+ mimeType: file.type || guessMimeType(safeName),
198
+ uploadedAt: new Date().toISOString(),
199
+ url,
200
+ etag: r2Object?.etag
201
+ }
202
+ }
203
+
204
+ /**
205
+ * Deletes a file from R2 Bucket by key.
206
+ */
207
+ export async function deleteR2File(bucket: any, key: string): Promise<boolean> {
208
+ if (!bucket || typeof bucket.delete !== "function") {
209
+ throw new Error("R2 bucket binding is not available in runtime environment.")
210
+ }
211
+ await bucket.delete(key)
212
+ return true
213
+ }
214
+
215
+ /**
216
+ * MIME type guesser fallback.
217
+ */
218
+ function guessMimeType(filename: string): string {
219
+ const ext = filename.split(".").pop()?.toLowerCase()
220
+ switch (ext) {
221
+ case "jpg":
222
+ case "jpeg":
223
+ return "image/jpeg"
224
+ case "png":
225
+ return "image/png"
226
+ case "webp":
227
+ return "image/webp"
228
+ case "gif":
229
+ return "image/gif"
230
+ case "svg":
231
+ return "image/svg+xml"
232
+ case "avif":
233
+ return "image/avif"
234
+ case "mp4":
235
+ return "video/mp4"
236
+ case "webm":
237
+ return "video/webm"
238
+ case "mp3":
239
+ return "audio/mpeg"
240
+ case "wav":
241
+ return "audio/wav"
242
+ case "pdf":
243
+ return "application/pdf"
244
+ case "json":
245
+ return "application/json"
246
+ case "txt":
247
+ return "text/plain"
248
+ case "zip":
249
+ return "application/zip"
250
+ default:
251
+ return "application/octet-stream"
252
+ }
253
+ }
@@ -1,350 +0,0 @@
1
- ---
2
- import type { BaseBlock } from "@rimelight/cms"
3
- function filterProps(props: any[], options: { hide?: string[]; show?: string[] }): any[] {
4
- if (options.show?.length) return props.filter((p: any) => options.show!.includes(p.name))
5
- if (options.hide?.length) return props.filter((p: any) => !options.hide!.includes(p.name))
6
- return props
7
- }
8
-
9
- interface Props {
10
- block: BaseBlock
11
- locale?: string
12
- }
13
-
14
- const { block } = Astro.props
15
- const { componentName, sections = {}, hide = [], show = [] } = block.props
16
- const compMeta = block.props.meta
17
- const themeCode = block.props.themeCode
18
-
19
- const filterOpts: { hide?: string[]; show?: string[] } = { hide, show }
20
- const props = filterProps(compMeta?.props ?? [], filterOpts)
21
- const slots = compMeta?.slots ?? []
22
- const emits = compMeta?.emits ?? []
23
- ---
24
-
25
- {(sections.props !== false) && (
26
- <div class="api-documentation-section mb-6">
27
- <h3 class="text-lg font-semibold text-foreground mb-3">Props</h3>
28
- {props.length > 0 ? (
29
- <div class="overflow-x-auto">
30
- <table class="w-full text-sm border-collapse">
31
- <thead>
32
- <tr class="border-b border-border">
33
- <th class="text-left py-2 px-3 font-semibold">Name</th>
34
- <th class="text-left py-2 px-3 font-semibold">Type</th>
35
- <th class="text-left py-2 px-3 font-semibold">Required</th>
36
- <th class="text-left py-2 px-3 font-semibold">Default</th>
37
- <th class="text-left py-2 px-3 font-semibold">Description</th>
38
- </tr>
39
- </thead>
40
- <tbody>
41
- {props.map((prop: any) => (
42
- <tr class="border-b border-border/50">
43
- <td><span class="font-mono text-sm">{prop.name}</span></td>
44
- <td><span class="font-mono text-xs text-muted-foreground">{prop.type}</span></td>
45
- <td>{prop.required ? "Yes" : "No"}</td>
46
- <td>{prop.default !== undefined ? <span class="font-mono text-xs text-muted-foreground">{prop.default}</span> : "—"}</td>
47
- <td>{prop.description || "—"}</td>
48
- </tr>
49
- ))}
50
- </tbody>
51
- </table>
52
- </div>
53
- ) : (
54
- <p class="text-sm italic text-muted-foreground">No props to display.</p>
55
- )}
56
- </div>
57
- )}
58
-
59
- {(sections.slots !== false) && (
60
- <div class="api-documentation-section mb-6">
61
- <h3 class="text-lg font-semibold text-foreground mb-3">Slots</h3>
62
- {slots.length === 0 ? (
63
- <p class="text-sm italic text-muted-foreground">No slots to display.</p>
64
- ) : (
65
- <div class="overflow-x-auto">
66
- <table class="w-full text-sm border-collapse">
67
- <thead>
68
- <tr class="border-b border-border">
69
- <th class="text-left py-2 px-3 font-semibold">Slot</th>
70
- <th class="text-left py-2 px-3 font-semibold">Type</th>
71
- </tr>
72
- </thead>
73
- <tbody>
74
- {slots.map((slot: any) => (
75
- <tr class="border-b border-border/50">
76
- <td class="py-2 px-3">
77
- <span class="font-mono text-sm">{slot.name}</span>
78
- </td>
79
- <td class="py-2 px-3">
80
- {Object.keys(slot.bindings || {}).length > 0 ? (
81
- <span class="font-mono text-xs text-muted-foreground">
82
- {Object.values(slot.bindings)[0]}
83
- </span>
84
- ) : (
85
- <span class="text-muted-foreground">-</span>
86
- )}
87
- </td>
88
- </tr>
89
- ))}
90
- </tbody>
91
- </table>
92
- </div>
93
- )}
94
- </div>
95
- )}
96
-
97
- {(sections.emits !== false) && (
98
- <div class="api-documentation-section mb-6">
99
- <h3 class="text-lg font-semibold text-foreground mb-3">Emits</h3>
100
- {emits.length === 0 ? (
101
- <p class="text-sm italic text-muted-foreground">No events emitted by this component.</p>
102
- ) : (
103
- <div class="overflow-x-auto">
104
- <table class="w-full text-sm border-collapse">
105
- <thead>
106
- <tr class="border-b border-border">
107
- <th class="text-left py-2 px-3 font-semibold">Event</th>
108
- <th class="text-left py-2 px-3 font-semibold">Payload</th>
109
- </tr>
110
- </thead>
111
- <tbody>
112
- {emits.map((emit: any) => (
113
- <tr class="border-b border-border/50">
114
- <td class="py-2 px-3">
115
- <span class="font-mono text-sm">{emit.name}</span>
116
- </td>
117
- <td class="py-2 px-3">
118
- <span class="font-mono text-xs text-muted-foreground">{emit.type}</span>
119
- </td>
120
- </tr>
121
- ))}
122
- </tbody>
123
- </table>
124
- </div>
125
- )}
126
- </div>
127
- )}
128
-
129
- {(sections.theme !== false) && (
130
- <div class="api-documentation-section mb-6">
131
- <h3 class="text-lg font-semibold text-foreground mb-3">Theme</h3>
132
- {themeCode ? (
133
- <div class="api-theme-container relative border border-border rounded-lg overflow-hidden group mb-6" data-component={componentName}>
134
- <div class="api-theme-collapse overflow-hidden max-h-[240px] relative transition-[max-height] duration-300 ease-in-out">
135
- <div class="expressive-code m-0 border-0 rounded-none">
136
- <figure class="frame m-0 border-0">
137
- <pre class="language-ts overflow-x-auto m-0 border-0 rounded-none bg-muted"><code class="language-ts m-0">{themeCode}</code></pre>
138
- </figure>
139
- </div>
140
- <div class="api-theme-fade absolute bottom-0 left-0 right-0 h-20 bg-gradient-to-t from-background to-transparent pointer-events-none transition-opacity duration-300"></div>
141
- </div>
142
- <div class="api-theme-trigger-bar absolute bottom-0 left-0 right-0 flex justify-center pb-3 bg-gradient-to-t from-background/90 via-background/70 to-transparent pt-12">
143
- <button class="api-theme-btn px-4 py-1.5 text-xs font-semibold text-primary-foreground bg-primary hover:bg-primary/90 border border-border rounded-md shadow-md transition-all duration-200 cursor-pointer">Show theme config</button>
144
- </div>
145
- </div>
146
- ) : (
147
- <p class="text-sm italic text-muted-foreground">No theme data available.</p>
148
- )}
149
- </div>
150
- )}
151
-
152
- {(sections.changelog) && (
153
- <div class="api-documentation-section mb-6">
154
- <h3 class="text-lg font-semibold text-foreground mb-3">Changelog</h3>
155
- <div class="api-changelog-container group" data-component={componentName}>
156
- <div class="api-changelog-loading py-6 flex flex-col gap-4">
157
- <div class="animate-pulse h-4 bg-muted rounded w-1/4"></div>
158
- <div class="animate-pulse h-3 bg-muted rounded w-full"></div>
159
- <div class="animate-pulse h-3 bg-muted rounded w-5/6"></div>
160
- </div>
161
- <div class="api-changelog-content hidden">
162
- <div class="api-changelog-timeline-target"></div>
163
- </div>
164
- <div class="api-changelog-empty hidden text-sm italic text-muted-foreground py-2">
165
- No recent changes found.
166
- </div>
167
- </div>
168
- </div>
169
- )}
170
-
171
- <script>
172
- function escapeHtml(str: string): string {
173
- return str
174
- .replace(/&/g, "&amp;")
175
- .replace(/</g, "&lt;")
176
- .replace(/>/g, "&gt;")
177
- .replace(/"/g, "&quot;")
178
- .replace(/'/g, "&#039;");
179
- }
180
-
181
- async function loadChangelog() {
182
- if (typeof document === "undefined") return;
183
- const containers = document.querySelectorAll('.api-changelog-container');
184
-
185
- await Promise.all(
186
- Array.from(containers).map(async (container) => {
187
- if (container.getAttribute('data-changelog-ready') === 'true') return;
188
- container.setAttribute('data-changelog-ready', 'true');
189
-
190
- const component = container.getAttribute('data-component');
191
-
192
- const loadingEl = container.querySelector('.api-changelog-loading') as HTMLElement;
193
- const contentEl = container.querySelector('.api-changelog-content') as HTMLElement;
194
- const targetEl = container.querySelector('.api-changelog-timeline-target') as HTMLElement;
195
- const emptyEl = container.querySelector('.api-changelog-empty') as HTMLElement;
196
-
197
- if (!component || !loadingEl || !contentEl || !targetEl || !emptyEl) return;
198
-
199
- try {
200
- const res = await fetch(`/api/github/changelog?component=${component}`);
201
- if (!res.ok) throw new Error();
202
- const data: any[] = await res.json();
203
-
204
- loadingEl.classList.add('hidden');
205
-
206
- if (!data || data.length === 0) {
207
- emptyEl.classList.remove('hidden');
208
- return;
209
- }
210
-
211
- const timelineItems: Array<{ type: 'tag' | 'commit'; versionTag?: string; sha?: string; message?: string }> = [];
212
-
213
- data.forEach((release: any) => {
214
- if (release.tag) {
215
- timelineItems.push({
216
- type: 'tag',
217
- versionTag: release.tag === 'unreleased' ? 'Soon' : release.tag
218
- });
219
- }
220
-
221
- if (release.commits && Array.isArray(release.commits)) {
222
- release.commits.forEach((commit: any) => {
223
- timelineItems.push({
224
- type: 'commit',
225
- sha: commit.sha,
226
- message: commit.message
227
- });
228
- });
229
- }
230
- });
231
-
232
- let rootHtml = `<div data-orientation="vertical" data-slot="root" class="flex flex-col">`;
233
-
234
- timelineItems.forEach((item, index) => {
235
- const isLast = index === timelineItems.length - 1;
236
-
237
- if (item.type === 'tag') {
238
- rootHtml += `
239
- <div data-slot="item" class="group relative flex flex-1 gap-3">
240
- <div data-slot="container" class="relative flex items-center flex-col shrink-0">
241
- <span data-slot="indicator" class="inline-flex items-center justify-center shrink-0 select-none rounded-full align-middle relative size-6 text-xs text-primary bg-primary/10 border border-primary/20 my-1">
242
- <span class="i-lucide-tag size-3.5"></span>
243
- </span>
244
- ${!isLast ? `<div data-slot="separator" class="flex-1 bg-elevated w-0.5 min-h-[16px]"></div>` : ''}
245
- </div>
246
- <div data-slot="wrapper" class="w-full py-1">
247
- <div data-slot="title" class="font-medium text-foreground text-sm flex items-center">
248
- <span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-mono font-semibold bg-primary/10 text-primary border border-primary/20">
249
- ${escapeHtml(item.versionTag || '')}
250
- </span>
251
- </div>
252
- </div>
253
- </div>
254
- `;
255
- } else {
256
- const cleanedMessage = (item.message || '')
257
- .replace(/#(\d+)/g, '<a href="https://github.com/Rimelight-Entertainment/rimelight/issues/$1" target="_blank" class="text-primary hover:underline">#$1</a>')
258
- .replace(/`(.*?)`/g, '<code class="text-xs px-1.5 py-0.5 bg-muted border border-border rounded font-mono text-muted-foreground">$1</code>');
259
-
260
- rootHtml += `
261
- <div data-slot="item" class="group relative flex flex-1 gap-3">
262
- <div data-slot="container" class="relative flex items-center flex-col shrink-0">
263
- <span data-slot="indicator" class="inline-flex items-center justify-center shrink-0 select-none rounded-full align-middle relative size-6 text-xs text-muted bg-transparent">
264
- <span class="size-1.5 rounded-full bg-border"></span>
265
- </span>
266
- ${!isLast ? `<div data-slot="separator" class="flex-1 bg-elevated w-0.5 min-h-[16px]"></div>` : ''}
267
- </div>
268
- <div data-slot="wrapper" class="w-full py-1 flex items-start gap-2 text-sm">
269
- <a href="https://github.com/Rimelight-Entertainment/rimelight/commit/${item.sha}" target="_blank" class="font-mono text-xs px-1.5 py-0.5 bg-muted border border-border rounded text-muted-foreground hover:text-foreground hover:border-border shrink-0">
270
- ${item.sha ? item.sha.slice(0, 7) : ''}
271
- </a>
272
- <span class="text-muted-foreground text-sm leading-snug pt-0.5">
273
- ${cleanedMessage}
274
- </span>
275
- </div>
276
- </div>
277
- `;
278
- }
279
- });
280
-
281
- rootHtml += `</div>`;
282
-
283
- targetEl.innerHTML = rootHtml;
284
- contentEl.classList.remove('hidden');
285
- } catch (err) {
286
- loadingEl.classList.add('hidden');
287
- emptyEl.textContent = 'Failed to load changelog.';
288
- emptyEl.classList.remove('hidden');
289
- }
290
- })
291
- );
292
- }
293
-
294
- function setupThemeCollapser() {
295
- if (typeof document === 'undefined') return;
296
- const containers = document.querySelectorAll('.api-theme-container');
297
- containers.forEach(container => {
298
- if (container.getAttribute('data-collapser-ready') === 'true') return;
299
- container.setAttribute('data-collapser-ready', 'true');
300
-
301
- const collapseWrapper = container.querySelector('.api-theme-collapse') as HTMLElement;
302
- const fadeOverlay = container.querySelector('.api-theme-fade') as HTMLElement;
303
- const btn = container.querySelector('.api-theme-btn') as HTMLButtonElement;
304
- const triggerBar = container.querySelector('.api-theme-trigger-bar') as HTMLElement;
305
-
306
- if (!collapseWrapper || !btn || !triggerBar || !fadeOverlay) return;
307
-
308
- const checkAndSetup = () => {
309
- const scrollHeight = collapseWrapper.scrollHeight;
310
- if (scrollHeight <= 240) {
311
- collapseWrapper.style.maxHeight = 'none';
312
- fadeOverlay.style.display = 'none';
313
- triggerBar.style.display = 'none';
314
- return;
315
- }
316
-
317
- let isExpanded = false;
318
-
319
- btn.addEventListener('click', () => {
320
- isExpanded = !isExpanded;
321
- if (isExpanded) {
322
- collapseWrapper.style.maxHeight = `${collapseWrapper.scrollHeight}px`;
323
- btn.textContent = 'Hide theme config';
324
- fadeOverlay.style.opacity = '0';
325
- triggerBar.classList.remove('absolute', 'bottom-0');
326
- triggerBar.classList.add('relative', 'bg-background', 'py-3', 'border-t', 'border-border');
327
- triggerBar.style.paddingTop = '12px';
328
- } else {
329
- collapseWrapper.style.maxHeight = '240px';
330
- btn.textContent = 'Show theme config';
331
- fadeOverlay.style.opacity = '1';
332
- triggerBar.classList.add('absolute', 'bottom-0');
333
- triggerBar.classList.remove('relative', 'bg-background', 'py-3', 'border-t', 'border-border');
334
- triggerBar.style.paddingTop = '48px';
335
- container.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
336
- }
337
- });
338
- };
339
-
340
- setTimeout(checkAndSetup, 150);
341
- });
342
- }
343
-
344
- if (typeof document !== 'undefined') {
345
- loadChangelog();
346
- setupThemeCollapser();
347
- document.addEventListener('astro:page-load', loadChangelog);
348
- document.addEventListener('astro:after-swap', setupThemeCollapser);
349
- }
350
- </script>
@@ -1,23 +0,0 @@
1
- ---
2
- import type { BaseBlock } from "@rimelight/cms"
3
-
4
- interface Props {
5
- block: BaseBlock
6
- locale?: string
7
- }
8
-
9
- const { block } = Astro.props
10
- const { character, parenthetical, line } = block.props
11
- ---
12
-
13
- <div class="my-3 mx-8 max-w-md">
14
- <div class="text-sm font-bold text-center text-pink-800 dark:text-pink-200">
15
- {character}
16
- </div>
17
- {parenthetical && (
18
- <div class="text-xs italic text-center text-gray-600 dark:text-gray-400 mt-0.5">
19
- ({parenthetical})
20
- </div>
21
- )}
22
- <div class="text-sm text-center text-gray-700 dark:text-gray-300 mt-1">{line}</div>
23
- </div>
@@ -1,27 +0,0 @@
1
- ---
2
- import type { BaseBlock } from "@rimelight/cms"
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 class="cms-live-preview border border-border rounded-xl overflow-visible bg-background text-foreground my-6" data-block-id={block.id} data-framework={framework} data-editable={editable}>
14
- <div class="controls-area p-4 border-b border-border flex items-center justify-between">
15
- <div class="flex items-center gap-2">
16
- <span class="text-xs font-medium text-muted-foreground uppercase tracking-wider">Live Preview</span>
17
- <span class="text-xs font-mono text-muted-foreground bg-muted px-2 py-0.5 rounded">{framework}</span>
18
- </div>
19
- {editable && (
20
- <span class="text-xs text-muted-foreground italic">Editable</span>
21
- )}
22
- </div>
23
-
24
- <div class="preview-area p-4 flex items-center justify-center" style={`height: ${height}; width: ${width};`}>
25
- <pre class="text-sm font-mono text-muted-foreground bg-muted p-4 rounded-lg overflow-x-auto w-full"><code>{componentCode}</code></pre>
26
- </div>
27
- </div>