@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,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,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,65 +0,0 @@
1
- ---
2
- import type { BaseBlock, TimeOfDay, Setting, Transition } from "@rimelight/cms"
3
-
4
- interface Props {
5
- block: BaseBlock
6
- locale?: string
7
- }
8
-
9
- const { block } = Astro.props
10
- const { location, timeOfDay, setting, transition, description } = block.props
11
-
12
- const timeOfDayLabels: Record<TimeOfDay, string> = {
13
- MORNING: "Morning",
14
- NOON: "Noon",
15
- AFTERNOON: "Afternoon",
16
- EVENING: "Evening",
17
- NIGHT: "Night",
18
- OTHER: "Other"
19
- }
20
-
21
- const settingLabels: Record<Setting, string> = {
22
- INTERIOR: "INT.",
23
- EXTERIOR: "EXT.",
24
- OTHER: "Other"
25
- }
26
-
27
- const transitionLabels: Record<Transition, string> = {
28
- CUT_TO: "CUT TO",
29
- CUT_BACK_TO: "CUT BACK TO",
30
- FADE_TO: "FADE TO",
31
- NONE: ""
32
- }
33
-
34
- const settingLabel = settingLabels[setting as Setting] || "Other"
35
- const todLabel = timeOfDayLabels[timeOfDay as TimeOfDay] || "Other"
36
- const transitionLabel = transitionLabels[transition as Transition] || ""
37
-
38
- const headingParts = []
39
- if (settingLabel && settingLabel !== "Other") headingParts.push(settingLabel)
40
- if (location) headingParts.push(location)
41
- if (todLabel && todLabel !== "Other") headingParts.push(`- ${todLabel}`)
42
- ---
43
-
44
- <div class="my-4 p-3 border-l-4 border-indigo-400 bg-indigo-50/20 dark:bg-indigo-950/10 rounded-r-lg">
45
- <div class="flex flex-wrap items-center gap-2 text-sm font-semibold text-indigo-800 dark:text-indigo-200 mb-2">
46
- {headingParts.map((part) => (
47
- <span>{part}</span>
48
- ))}
49
- {transitionLabel && (
50
- <span class="ml-auto text-[10px] italic text-gray-600 dark:text-gray-400">
51
- {transitionLabel}
52
- </span>
53
- )}
54
- </div>
55
- {description && (
56
- <p class="text-sm text-gray-700 dark:text-gray-300 mb-2">
57
- {description}
58
- </p>
59
- )}
60
- {((block.children && block.children.length > 0) || (block.props?.children && block.props.children.length > 0)) && (
61
- <div class="mt-3">
62
- <slot />
63
- </div>
64
- )}
65
- </div>
@@ -1,59 +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 { title, synopsis, characters, aPlot, bPlot, cPlot, stinger } = block.props
11
- ---
12
-
13
- <div class="my-6 p-4 border-l-4 border-amber-400 bg-amber-50/20 dark:bg-amber-950/10 rounded-r-lg">
14
- <div class="mb-4">
15
- <h3 class="text-xl font-bold text-amber-900 dark:text-amber-100">{title}</h3>
16
- {synopsis && <p class="text-sm text-gray-600 dark:text-gray-400 mt-1 italic">{synopsis}</p>}
17
- {characters && characters.length > 0 && (
18
- <div class="flex flex-wrap gap-3 mt-3 text-xs">
19
- {characters.map((char: string) => (
20
- <span class="px-2 py-0.5 rounded-full bg-amber-100 dark:bg-amber-900/30 text-amber-800 dark:text-amber-200">
21
- {char}
22
- </span>
23
- ))}
24
- </div>
25
- )}
26
- </div>
27
- {(aPlot || bPlot || cPlot) && (
28
- <div class="grid grid-cols-1 md:grid-cols-3 gap-3 my-3">
29
- {aPlot && (
30
- <div class="bg-white/50 dark:bg-neutral-900/30 rounded p-2 border border-gray-200 dark:border-gray-700">
31
- <div class="text-[10px] font-bold uppercase tracking-wider text-gray-600 dark:text-gray-400">A Plot</div>
32
- <div class="text-sm mt-0.5">{aPlot}</div>
33
- </div>
34
- )}
35
- {bPlot && (
36
- <div class="bg-white/50 dark:bg-neutral-900/30 rounded p-2 border border-gray-200 dark:border-gray-700">
37
- <div class="text-[10px] font-bold uppercase tracking-wider text-gray-600 dark:text-gray-400">B Plot</div>
38
- <div class="text-sm mt-0.5">{bPlot}</div>
39
- </div>
40
- )}
41
- {cPlot && (
42
- <div class="bg-white/50 dark:bg-neutral-900/30 rounded p-2 border border-gray-200 dark:border-gray-700">
43
- <div class="text-[10px] font-bold uppercase tracking-wider text-gray-600 dark:text-gray-400">C Plot</div>
44
- <div class="text-sm mt-0.5">{cPlot}</div>
45
- </div>
46
- )}
47
- </div>
48
- )}
49
- {stinger && (
50
- <p class="text-sm text-gray-600 dark:text-gray-400 italic mt-2 border-t border-gray-200 dark:border-gray-700 pt-2">
51
- Stinger: {stinger}
52
- </p>
53
- )}
54
- {((block.children && block.children.length > 0) || (block.props?.children && block.props.children.length > 0)) && (
55
- <div class="mt-4">
56
- <slot />
57
- </div>
58
- )}
59
- </div>
@@ -1,75 +0,0 @@
1
- import type { BaseBlock } from "./types/blocks"
2
-
3
- /**
4
- * Merges updated template blocks into an existing page block tree. Matches templated blocks by
5
- * block `type` + `isTemplated: true` tag and structural index. Fits updated template anchors while
6
- * preserving user-inserted custom blocks around them.
7
- */
8
- export function mergeTemplateBlocks(
9
- existingPageBlocks: BaseBlock[],
10
- newTemplateBlocks: BaseBlock[]
11
- ): BaseBlock[] {
12
- const merged: BaseBlock[] = []
13
-
14
- const newTemplatedAnchors = newTemplateBlocks.filter((b) => b.isTemplated)
15
- let existingIndex = 0
16
-
17
- for (const templateAnchor of newTemplatedAnchors) {
18
- // Collect user-inserted custom blocks preceding this template anchor
19
- while (
20
- existingIndex < existingPageBlocks.length &&
21
- !existingPageBlocks[existingIndex]?.isTemplated
22
- ) {
23
- const block = existingPageBlocks[existingIndex]
24
- if (block) merged.push(block)
25
- existingIndex++
26
- }
27
-
28
- // Match existing template anchor by block type
29
- const existingAnchor = existingPageBlocks[existingIndex]
30
-
31
- if (existingAnchor && existingAnchor.type === templateAnchor.type) {
32
- merged.push({
33
- ...templateAnchor,
34
- id: existingAnchor.id,
35
- props: mergeBlockProps(
36
- existingAnchor.props,
37
- templateAnchor.props,
38
- existingAnchor.templatedProps
39
- )
40
- })
41
- existingIndex++
42
- } else {
43
- // New template anchor inserted in template update: add fresh block with new random UUID
44
- merged.push({
45
- ...templateAnchor,
46
- id: crypto.randomUUID()
47
- })
48
- }
49
- }
50
-
51
- // Append any remaining custom user blocks at the end of the document
52
- while (existingIndex < existingPageBlocks.length) {
53
- const block = existingPageBlocks[existingIndex]
54
- if (block) merged.push(block)
55
- existingIndex++
56
- }
57
-
58
- return merged
59
- }
60
-
61
- function mergeBlockProps(
62
- existingProps: any,
63
- templateProps: any,
64
- templatedProps?: boolean | string[]
65
- ) {
66
- if (templatedProps === true) return { ...templateProps }
67
- if (Array.isArray(templatedProps)) {
68
- const updated = { ...existingProps }
69
- for (const key of templatedProps) {
70
- updated[key] = templateProps[key]
71
- }
72
- return updated
73
- }
74
- return { ...existingProps, ...templateProps }
75
- }