@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.
- package/LICENSE +21 -0
- package/package.json +16 -14
- package/src/admin/api/media-file.ts +39 -0
- package/src/admin/api/media.ts +199 -0
- package/src/admin/layouts/CMSDashboardLayout.astro +178 -0
- package/src/admin/layouts/DefaultParentLayout.astro +50 -0
- package/src/admin/pages/assets.astro +424 -0
- package/src/admin/pages/byline-schema.astro +62 -0
- package/src/admin/pages/bylines.astro +16 -0
- package/src/admin/pages/categories.astro +21 -0
- package/src/admin/pages/content-types.astro +55 -0
- package/src/admin/pages/dashboard.astro +61 -0
- package/src/admin/pages/pages-edit.astro +595 -0
- package/src/admin/pages/pages-index.astro +146 -0
- package/src/admin/pages/pages-new.astro +130 -0
- package/src/admin/pages/pages-preview.astro +111 -0
- package/src/admin/pages/pages-review.astro +236 -0
- package/src/admin/pages/settings.astro +203 -0
- package/src/admin/pages/tags.astro +21 -0
- package/src/admin/pages/templates-edit.astro +234 -0
- package/src/admin/pages/templates-index.astro +95 -0
- package/src/admin/pages/templates-new.astro +193 -0
- package/src/admin/pages/users.astro +74 -0
- package/src/admin/pages/versions.astro +74 -0
- package/src/astro/BlockRenderer.astro +44 -96
- package/src/astro/PageRenderer.astro +9 -2
- package/src/astro/blocks/CalloutBlock.astro +7 -7
- package/src/astro/blocks/CardBlock.astro +5 -5
- package/src/astro/blocks/CodeBlock.astro +2 -2
- package/src/astro/blocks/FileTreeBlock.astro +7 -7
- package/src/astro/blocks/ImageBlock.astro +2 -2
- package/src/astro/blocks/OrderedListBlock.astro +2 -2
- package/src/astro/blocks/ParagraphBlock.astro +3 -3
- package/src/astro/blocks/SectionBlock.astro +6 -6
- package/src/astro/blocks/StepItemBlock.astro +3 -3
- package/src/astro/blocks/StepsBlock.astro +1 -1
- package/src/astro/blocks/TableBlock.astro +6 -6
- package/src/astro/blocks/TabsBlock.astro +9 -9
- package/src/astro/blocks/UnorderedListBlock.astro +2 -2
- package/src/astro/blocks/index.ts +35 -0
- package/src/client/components/RimelightBlock.tsx +19 -19
- package/src/client/components/RimelightBlockPicker.tsx +9 -13
- package/src/client/components/RimelightBylinesManager.tsx +438 -0
- package/src/client/components/RimelightEditor.tsx +3 -3
- package/src/client/components/RimelightPreview.tsx +37 -45
- package/src/client/components/RimelightPropertiesPanel.tsx +368 -26
- package/src/client/components/RimelightTaxonomyManager.tsx +369 -0
- package/src/client/components/RimelightTaxonomyMasterDetail.tsx +412 -0
- package/src/client/components/RimelightTemplateEditor.tsx +6 -8
- package/src/client/components/editors/CalloutEditor.tsx +2 -4
- package/src/client/components/editors/CardsEditor.tsx +2 -4
- package/src/client/components/editors/CodeEditor.tsx +1 -3
- package/src/client/components/editors/FileTreeEditor.tsx +2 -4
- package/src/client/components/editors/ImageEditor.tsx +3 -3
- package/src/client/components/editors/ParagraphEditor.tsx +2 -2
- package/src/client/components/editors/SceneEditor.tsx +6 -12
- package/src/client/components/editors/ScriptEditor.tsx +8 -16
- package/src/client/components/editors/SectionEditor.tsx +4 -4
- package/src/client/components/editors/StepItemEditor.tsx +2 -2
- package/src/client/components/editors/StepsEditor.tsx +2 -4
- package/src/client/components/editors/TabItemEditor.tsx +3 -3
- package/src/client/components/editors/TableEditor.tsx +5 -5
- package/src/client/components/editors/TabsEditor.tsx +2 -4
- package/src/client/components/index.ts +3 -0
- package/src/client/loader.ts +3 -0
- package/src/core/diff.ts +145 -0
- package/src/core/excerpt.ts +159 -0
- package/src/core/preview.ts +87 -0
- package/src/core/registry.ts +0 -39
- package/src/core/types/blocks.ts +83 -6
- package/src/core/types/bylines.ts +14 -0
- package/src/core/types/media.ts +13 -0
- package/src/core/types/taxonomies.ts +20 -0
- package/src/cron/scheduled.ts +57 -0
- package/src/docs/agent.ts +116 -0
- package/src/docs/config.ts +25 -0
- package/src/docs/index.ts +5 -0
- package/src/docs/routing.ts +104 -0
- package/src/docs/sidebar.ts +251 -0
- package/src/docs/toc.ts +47 -0
- package/src/env.d.ts +13 -0
- package/src/index.ts +40 -6
- package/src/integration.ts +196 -0
- package/src/loader/live.ts +254 -0
- package/src/markdown/index.ts +0 -1
- package/src/mcp/index.ts +228 -0
- package/src/schema/bylines.ts +23 -0
- package/src/schema/index.ts +3 -0
- package/src/schema/page_versions.ts +2 -0
- package/src/schema/pages.ts +2 -0
- package/src/schema/site_settings.ts +41 -0
- package/src/schema/taxonomies.ts +49 -0
- package/src/services/bylines.ts +74 -0
- package/src/services/search-indexer.ts +61 -0
- package/src/services/site-settings.ts +108 -0
- package/src/services/taxonomies.ts +276 -0
- package/src/storage/index.ts +253 -0
- package/src/astro/blocks/DialogueBlock.astro +0 -23
- package/src/astro/blocks/SceneBlock.astro +0 -65
- package/src/astro/blocks/ScriptBlock.astro +0 -59
- package/src/core/template-sync.ts +0 -75
- package/src/markdown/parser.ts +0 -347
- package/src/migration.ts +0 -155
- 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
|
-
}
|