@rimelight/cms 0.0.1
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/README.md +19 -0
- package/package.json +69 -0
- package/src/astro/BlockRenderer.astro +111 -0
- package/src/astro/PageRenderer.astro +23 -0
- package/src/astro/blocks/CalloutBlock.astro +78 -0
- package/src/astro/blocks/CardBlock.astro +43 -0
- package/src/astro/blocks/CardsBlock.astro +24 -0
- package/src/astro/blocks/CodeBlock.astro +16 -0
- package/src/astro/blocks/DialogueBlock.astro +23 -0
- package/src/astro/blocks/FileTreeBlock.astro +45 -0
- package/src/astro/blocks/ImageBlock.astro +17 -0
- package/src/astro/blocks/OrderedListBlock.astro +55 -0
- package/src/astro/blocks/ParagraphBlock.astro +65 -0
- package/src/astro/blocks/SceneBlock.astro +65 -0
- package/src/astro/blocks/ScriptBlock.astro +59 -0
- package/src/astro/blocks/SectionBlock.astro +46 -0
- package/src/astro/blocks/StepItemBlock.astro +25 -0
- package/src/astro/blocks/StepsBlock.astro +14 -0
- package/src/astro/blocks/TabItemBlock.astro +19 -0
- package/src/astro/blocks/TableBlock.astro +49 -0
- package/src/astro/blocks/TabsBlock.astro +67 -0
- package/src/astro/blocks/UnorderedListBlock.astro +55 -0
- package/src/auth/editor-guards.ts +148 -0
- package/src/auth/filter-blocks.ts +44 -0
- package/src/auth/filter-templates.ts +81 -0
- package/src/auth/permissions.ts +40 -0
- package/src/cache/purge.ts +37 -0
- package/src/client/components/RimelightBlock.tsx +487 -0
- package/src/client/components/RimelightBlockPicker.tsx +305 -0
- package/src/client/components/RimelightEditor.tsx +131 -0
- package/src/client/components/RimelightEditorLayout.tsx +143 -0
- package/src/client/components/RimelightPreview.tsx +354 -0
- package/src/client/components/RimelightPropertiesPanel.tsx +408 -0
- package/src/client/components/RimelightTemplateEditor.tsx +245 -0
- package/src/client/components/editors/CalloutEditor.tsx +185 -0
- package/src/client/components/editors/CardEditor.tsx +60 -0
- package/src/client/components/editors/CardsEditor.tsx +132 -0
- package/src/client/components/editors/CodeEditor.tsx +101 -0
- package/src/client/components/editors/DialogueEditor.tsx +52 -0
- package/src/client/components/editors/FileTreeEditor.tsx +65 -0
- package/src/client/components/editors/ImageEditor.tsx +70 -0
- package/src/client/components/editors/ParagraphEditor.tsx +314 -0
- package/src/client/components/editors/SceneEditor.tsx +245 -0
- package/src/client/components/editors/ScriptEditor.tsx +245 -0
- package/src/client/components/editors/SectionEditor.tsx +139 -0
- package/src/client/components/editors/StepItemEditor.tsx +117 -0
- package/src/client/components/editors/StepsEditor.tsx +105 -0
- package/src/client/components/editors/TabItemEditor.tsx +113 -0
- package/src/client/components/editors/TableEditor.tsx +181 -0
- package/src/client/components/editors/TabsEditor.tsx +105 -0
- package/src/client/components/index.ts +7 -0
- package/src/client/loader.ts +19 -0
- package/src/client/state/autosave.ts +122 -0
- package/src/client/state/editor-store.ts +411 -0
- package/src/client/state/history.ts +251 -0
- package/src/client/state/lock-manager.ts +89 -0
- package/src/client/styles/cms-editor.css +653 -0
- package/src/client/toast.ts +12 -0
- package/src/client/utils/sortable.ts +184 -0
- package/src/client/utils/splitpane.ts +87 -0
- package/src/client/utils/tree-sanitizer.ts +33 -0
- package/src/core/page-definitions.ts +502 -0
- package/src/core/registry.ts +99 -0
- package/src/core/template-sync.ts +75 -0
- package/src/core/types/blocks.ts +367 -0
- package/src/core/types/inline.ts +23 -0
- package/src/core/types/pages.ts +36 -0
- package/src/core/types/templates.ts +32 -0
- package/src/core/types/versioning.ts +44 -0
- package/src/core/validator.ts +28 -0
- package/src/env.d.ts +4 -0
- package/src/index.ts +47 -0
- package/src/markdown/index.ts +2 -0
- package/src/markdown/parser.ts +347 -0
- package/src/markdown/serializer.ts +219 -0
- package/src/migration.ts +155 -0
- package/src/schema/index.ts +8 -0
- package/src/schema/page_draft_locks.ts +13 -0
- package/src/schema/page_drafts.ts +18 -0
- package/src/schema/page_templates.ts +27 -0
- package/src/schema/page_version_approvals.ts +12 -0
- package/src/schema/page_version_comments.ts +14 -0
- package/src/schema/page_versions.ts +34 -0
- package/src/schema/pages.ts +42 -0
- package/src/schema/search.ts +23 -0
- package/src/search/query.ts +10 -0
- package/src/search/sync.ts +32 -0
|
@@ -0,0 +1,502 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Page type definitions for the CMS.
|
|
3
|
+
*
|
|
4
|
+
* Ported from rimelight.com/src/types/pageDefinitions.ts. Each definition specifies typed property
|
|
5
|
+
* groups with field schemas and optional initial template blocks seeded on page creation.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { BaseBlock } from "./types/blocks"
|
|
9
|
+
|
|
10
|
+
// ─── Field type definitions ───────────────────────────────────────────────────
|
|
11
|
+
|
|
12
|
+
export type FieldType = "text" | "number" | "enum" | "text-array" | "page" | "page-array"
|
|
13
|
+
|
|
14
|
+
export interface FieldDefinition {
|
|
15
|
+
type: FieldType
|
|
16
|
+
label: { en: string }
|
|
17
|
+
defaultValue: any
|
|
18
|
+
options?: { en: string }[]
|
|
19
|
+
allowedPageTypes?: string[]
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface PropertyGroup {
|
|
23
|
+
label: { en: string }
|
|
24
|
+
defaultOpen: boolean
|
|
25
|
+
fields: Record<string, FieldDefinition>
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface PageDefinition {
|
|
29
|
+
typeLabelKey: string
|
|
30
|
+
properties: Record<string, PropertyGroup>
|
|
31
|
+
/**
|
|
32
|
+
* Factory that returns the initial blocks seeded when a page of this type is created
|
|
33
|
+
*/
|
|
34
|
+
initialBlocks?: () => BaseBlock[]
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function uuid(): string {
|
|
38
|
+
return typeof crypto !== "undefined" && crypto.randomUUID
|
|
39
|
+
? crypto.randomUUID()
|
|
40
|
+
: Math.random().toString(36).slice(2, 11)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function definePageDefinition(def: PageDefinition): PageDefinition {
|
|
44
|
+
return def
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// ─── Standard base definitions ────────────────────────────────────────────────
|
|
48
|
+
|
|
49
|
+
export const DOCUMENT_DEFINITION = definePageDefinition({
|
|
50
|
+
typeLabelKey: "page.type.document",
|
|
51
|
+
properties: {}
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
export const BLOG_POST_DEFINITION = definePageDefinition({
|
|
55
|
+
typeLabelKey: "page.type.blogPost",
|
|
56
|
+
properties: {
|
|
57
|
+
meta: {
|
|
58
|
+
label: { en: "Post Metadata" },
|
|
59
|
+
defaultOpen: true,
|
|
60
|
+
fields: {
|
|
61
|
+
category: {
|
|
62
|
+
type: "enum",
|
|
63
|
+
label: { en: "Category" },
|
|
64
|
+
defaultValue: { en: "Company News" },
|
|
65
|
+
options: [{ en: "Company News" }, { en: "Development Log" }, { en: "New Release" }]
|
|
66
|
+
},
|
|
67
|
+
publishedAt: {
|
|
68
|
+
type: "text",
|
|
69
|
+
label: { en: "Published At" },
|
|
70
|
+
defaultValue: { en: "" }
|
|
71
|
+
},
|
|
72
|
+
author: {
|
|
73
|
+
type: "text",
|
|
74
|
+
label: { en: "Author" },
|
|
75
|
+
defaultValue: { en: "" }
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
export const PATCH_NOTE_DEFINITION = definePageDefinition({
|
|
83
|
+
typeLabelKey: "page.type.patchNote",
|
|
84
|
+
properties: {
|
|
85
|
+
version: {
|
|
86
|
+
label: { en: "Version Info" },
|
|
87
|
+
defaultOpen: true,
|
|
88
|
+
fields: {
|
|
89
|
+
versionNumber: {
|
|
90
|
+
type: "text",
|
|
91
|
+
label: { en: "Version Number" },
|
|
92
|
+
defaultValue: "1.0.0"
|
|
93
|
+
},
|
|
94
|
+
releaseDate: {
|
|
95
|
+
type: "text",
|
|
96
|
+
label: { en: "Release Date" },
|
|
97
|
+
defaultValue: { en: "" }
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
export const LOCATION_DEFINITION = definePageDefinition({
|
|
105
|
+
typeLabelKey: "page.type.location",
|
|
106
|
+
properties: {
|
|
107
|
+
geography: {
|
|
108
|
+
label: { en: "Geography" },
|
|
109
|
+
defaultOpen: true,
|
|
110
|
+
fields: {
|
|
111
|
+
region: { type: "text", label: { en: "Region" }, defaultValue: { en: "" } },
|
|
112
|
+
climate: { type: "text", label: { en: "Climate" }, defaultValue: "Temperate" }
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
export const SPECIES_DEFINITION = definePageDefinition({
|
|
119
|
+
typeLabelKey: "page.type.species",
|
|
120
|
+
properties: {
|
|
121
|
+
biology: {
|
|
122
|
+
label: { en: "Biology" },
|
|
123
|
+
defaultOpen: true,
|
|
124
|
+
fields: {
|
|
125
|
+
lifespan: { type: "text", label: { en: "Average Lifespan" }, defaultValue: { en: "" } },
|
|
126
|
+
homeworld: {
|
|
127
|
+
type: "page",
|
|
128
|
+
label: { en: "Homeworld" },
|
|
129
|
+
defaultValue: "",
|
|
130
|
+
allowedPageTypes: ["Location"]
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
export const CHARACTER_DEFINITION = definePageDefinition({
|
|
138
|
+
typeLabelKey: "page.type.character",
|
|
139
|
+
properties: {
|
|
140
|
+
identity: {
|
|
141
|
+
label: { en: "Identity" },
|
|
142
|
+
defaultOpen: true,
|
|
143
|
+
fields: {
|
|
144
|
+
name: { type: "text", label: { en: "Name" }, defaultValue: { en: "" } },
|
|
145
|
+
title: { type: "text", label: { en: "Title" }, defaultValue: { en: "" } },
|
|
146
|
+
aliases: { type: "text-array", label: { en: "Aliases" }, defaultValue: [] },
|
|
147
|
+
pronouns: {
|
|
148
|
+
type: "enum",
|
|
149
|
+
label: { en: "Pronouns" },
|
|
150
|
+
defaultValue: { en: "Unknown" },
|
|
151
|
+
options: [
|
|
152
|
+
{ en: "He/Him" },
|
|
153
|
+
{ en: "She/Her" },
|
|
154
|
+
{ en: "They/Them" },
|
|
155
|
+
{ en: "He/They" },
|
|
156
|
+
{ en: "She/They" },
|
|
157
|
+
{ en: "Other" },
|
|
158
|
+
{ en: "Unknown" }
|
|
159
|
+
]
|
|
160
|
+
},
|
|
161
|
+
flavourText: { type: "text", label: { en: "Flavour Text" }, defaultValue: { en: "" } }
|
|
162
|
+
}
|
|
163
|
+
},
|
|
164
|
+
characteristics: {
|
|
165
|
+
label: { en: "Characteristics" },
|
|
166
|
+
defaultOpen: true,
|
|
167
|
+
fields: {
|
|
168
|
+
species: {
|
|
169
|
+
type: "page",
|
|
170
|
+
label: { en: "Species" },
|
|
171
|
+
defaultValue: "",
|
|
172
|
+
allowedPageTypes: ["Species"]
|
|
173
|
+
},
|
|
174
|
+
sex: {
|
|
175
|
+
type: "enum",
|
|
176
|
+
label: { en: "Sex" },
|
|
177
|
+
defaultValue: { en: "Unknown" },
|
|
178
|
+
options: [{ en: "Male" }, { en: "Female" }, { en: "Other" }, { en: "Unknown" }]
|
|
179
|
+
},
|
|
180
|
+
height: { type: "number", label: { en: "Height" }, defaultValue: 0 },
|
|
181
|
+
weight: { type: "number", label: { en: "Weight" }, defaultValue: 0 },
|
|
182
|
+
dateOfBirth: { type: "text", label: { en: "Date of Birth" }, defaultValue: { en: "" } },
|
|
183
|
+
placeOfBirth: { type: "text", label: { en: "Place of Birth" }, defaultValue: { en: "" } },
|
|
184
|
+
dateOfDeath: { type: "text", label: { en: "Date of Death" }, defaultValue: { en: "" } },
|
|
185
|
+
placeOfDeath: { type: "text", label: { en: "Place of Death" }, defaultValue: { en: "" } }
|
|
186
|
+
}
|
|
187
|
+
},
|
|
188
|
+
inventory: {
|
|
189
|
+
label: { en: "Inventory & Assets" },
|
|
190
|
+
defaultOpen: false,
|
|
191
|
+
fields: {
|
|
192
|
+
equipment: {
|
|
193
|
+
type: "page-array",
|
|
194
|
+
label: { en: "Equipment" },
|
|
195
|
+
defaultValue: [],
|
|
196
|
+
allowedPageTypes: ["Object"]
|
|
197
|
+
},
|
|
198
|
+
pets: {
|
|
199
|
+
type: "page-array",
|
|
200
|
+
label: { en: "Pets" },
|
|
201
|
+
defaultValue: [],
|
|
202
|
+
allowedPageTypes: ["Character"]
|
|
203
|
+
},
|
|
204
|
+
mounts: {
|
|
205
|
+
type: "page-array",
|
|
206
|
+
label: { en: "Mounts" },
|
|
207
|
+
defaultValue: [],
|
|
208
|
+
allowedPageTypes: ["Character"]
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
},
|
|
212
|
+
affiliations: {
|
|
213
|
+
label: { en: "Affiliations" },
|
|
214
|
+
defaultOpen: false,
|
|
215
|
+
fields: {
|
|
216
|
+
currentAffiliations: {
|
|
217
|
+
type: "page-array",
|
|
218
|
+
label: { en: "Current Affiliations" },
|
|
219
|
+
defaultValue: [],
|
|
220
|
+
allowedPageTypes: ["Group"]
|
|
221
|
+
},
|
|
222
|
+
formerAffiliations: {
|
|
223
|
+
type: "page-array",
|
|
224
|
+
label: { en: "Former Affiliations" },
|
|
225
|
+
defaultValue: [],
|
|
226
|
+
allowedPageTypes: ["Group"]
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
},
|
|
230
|
+
other: {
|
|
231
|
+
label: { en: "Other" },
|
|
232
|
+
defaultOpen: false,
|
|
233
|
+
fields: {
|
|
234
|
+
favouriteFood: {
|
|
235
|
+
type: "page",
|
|
236
|
+
label: { en: "Favourite Food" },
|
|
237
|
+
defaultValue: "",
|
|
238
|
+
allowedPageTypes: ["Object"]
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
},
|
|
243
|
+
initialBlocks: () => [
|
|
244
|
+
{
|
|
245
|
+
id: uuid(),
|
|
246
|
+
type: "SectionBlock",
|
|
247
|
+
props: { level: 2, title: "Appearance", children: [] },
|
|
248
|
+
isTemplated: true
|
|
249
|
+
},
|
|
250
|
+
{
|
|
251
|
+
id: uuid(),
|
|
252
|
+
type: "SectionBlock",
|
|
253
|
+
props: { level: 2, title: "Abilities", children: [] },
|
|
254
|
+
isTemplated: true
|
|
255
|
+
},
|
|
256
|
+
{
|
|
257
|
+
id: uuid(),
|
|
258
|
+
type: "SectionBlock",
|
|
259
|
+
props: { level: 2, title: "History", children: [] },
|
|
260
|
+
isTemplated: true
|
|
261
|
+
}
|
|
262
|
+
]
|
|
263
|
+
})
|
|
264
|
+
|
|
265
|
+
export const SKILL_DEFINITION = definePageDefinition({
|
|
266
|
+
typeLabelKey: "page.type.skill",
|
|
267
|
+
properties: {
|
|
268
|
+
mechanics: {
|
|
269
|
+
label: { en: "Mechanics" },
|
|
270
|
+
defaultOpen: true,
|
|
271
|
+
fields: {
|
|
272
|
+
cooldown: { type: "number", label: { en: "Cooldown (sec)" }, defaultValue: 10 },
|
|
273
|
+
manaCost: { type: "number", label: { en: "Mana Cost" }, defaultValue: 50 },
|
|
274
|
+
damageType: {
|
|
275
|
+
type: "enum",
|
|
276
|
+
label: { en: "Damage Type" },
|
|
277
|
+
defaultValue: { en: "Physical" },
|
|
278
|
+
options: [{ en: "Physical" }, { en: "Magic" }, { en: "True" }, { en: "None" }]
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
})
|
|
284
|
+
|
|
285
|
+
export const ITEM_DEFINITION = definePageDefinition({
|
|
286
|
+
typeLabelKey: "page.type.item",
|
|
287
|
+
properties: {
|
|
288
|
+
details: {
|
|
289
|
+
label: { en: "Item Details" },
|
|
290
|
+
defaultOpen: true,
|
|
291
|
+
fields: {
|
|
292
|
+
rarity: {
|
|
293
|
+
type: "enum",
|
|
294
|
+
label: { en: "Rarity" },
|
|
295
|
+
defaultValue: { en: "Common" },
|
|
296
|
+
options: [
|
|
297
|
+
{ en: "Common" },
|
|
298
|
+
{ en: "Uncommon" },
|
|
299
|
+
{ en: "Rare" },
|
|
300
|
+
{ en: "Epic" },
|
|
301
|
+
{ en: "Legendary" }
|
|
302
|
+
]
|
|
303
|
+
},
|
|
304
|
+
price: { type: "number", label: { en: "Gold Price" }, defaultValue: 100 },
|
|
305
|
+
isQuestItem: {
|
|
306
|
+
type: "enum",
|
|
307
|
+
label: { en: "Quest Item" },
|
|
308
|
+
defaultValue: { en: "No" },
|
|
309
|
+
options: [{ en: "Yes" }, { en: "No" }]
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
})
|
|
315
|
+
|
|
316
|
+
export const OBJECT_DEFINITION = definePageDefinition({
|
|
317
|
+
typeLabelKey: "page.type.object",
|
|
318
|
+
properties: {
|
|
319
|
+
details: {
|
|
320
|
+
label: { en: "Object Details" },
|
|
321
|
+
defaultOpen: true,
|
|
322
|
+
fields: {
|
|
323
|
+
category: { type: "text", label: { en: "Category" }, defaultValue: { en: "Miscellaneous" } }
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
})
|
|
328
|
+
|
|
329
|
+
export const GROUP_DEFINITION = definePageDefinition({
|
|
330
|
+
typeLabelKey: "page.type.group",
|
|
331
|
+
properties: {
|
|
332
|
+
details: {
|
|
333
|
+
label: { en: "Group Details" },
|
|
334
|
+
defaultOpen: true,
|
|
335
|
+
fields: {
|
|
336
|
+
leader: {
|
|
337
|
+
type: "page",
|
|
338
|
+
label: { en: "Leader" },
|
|
339
|
+
defaultValue: "",
|
|
340
|
+
allowedPageTypes: ["Character"]
|
|
341
|
+
},
|
|
342
|
+
headquarters: {
|
|
343
|
+
type: "page",
|
|
344
|
+
label: { en: "Headquarters" },
|
|
345
|
+
defaultValue: "",
|
|
346
|
+
allowedPageTypes: ["Location"]
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
})
|
|
352
|
+
|
|
353
|
+
export const HERO_DEFINITION = definePageDefinition({
|
|
354
|
+
typeLabelKey: "page.type.hero",
|
|
355
|
+
properties: {
|
|
356
|
+
combat: {
|
|
357
|
+
label: { en: "Combat Stats" },
|
|
358
|
+
defaultOpen: true,
|
|
359
|
+
fields: {
|
|
360
|
+
class: {
|
|
361
|
+
type: "enum",
|
|
362
|
+
label: { en: "Class" },
|
|
363
|
+
defaultValue: { en: "Warrior" },
|
|
364
|
+
options: [{ en: "Warrior" }, { en: "Mage" }, { en: "Rogue" }, { en: "Paladin" }]
|
|
365
|
+
},
|
|
366
|
+
difficulty: { type: "number", label: { en: "Difficulty" }, defaultValue: 1 },
|
|
367
|
+
primaryRole: {
|
|
368
|
+
type: "enum",
|
|
369
|
+
label: { en: "Primary Role" },
|
|
370
|
+
defaultValue: { en: "Tank" },
|
|
371
|
+
options: [{ en: "Tank" }, { en: "DPS" }, { en: "Support" }]
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
},
|
|
375
|
+
progression: {
|
|
376
|
+
label: { en: "Progression" },
|
|
377
|
+
defaultOpen: true,
|
|
378
|
+
fields: {
|
|
379
|
+
baseHp: { type: "number", label: { en: "Base HP" }, defaultValue: 500 },
|
|
380
|
+
baseMana: { type: "number", label: { en: "Base Mana" }, defaultValue: 100 }
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
},
|
|
384
|
+
initialBlocks: () => [
|
|
385
|
+
{
|
|
386
|
+
id: uuid(),
|
|
387
|
+
type: "SectionBlock",
|
|
388
|
+
props: { level: 2, title: "Playstyle", children: [] },
|
|
389
|
+
isTemplated: true
|
|
390
|
+
},
|
|
391
|
+
{
|
|
392
|
+
id: uuid(),
|
|
393
|
+
type: "SectionBlock",
|
|
394
|
+
props: { level: 2, title: "Background Lore", children: [] },
|
|
395
|
+
isTemplated: true
|
|
396
|
+
}
|
|
397
|
+
]
|
|
398
|
+
})
|
|
399
|
+
|
|
400
|
+
export const CARD_DEFINITION = definePageDefinition({
|
|
401
|
+
typeLabelKey: "page.type.card",
|
|
402
|
+
properties: {},
|
|
403
|
+
initialBlocks: () => []
|
|
404
|
+
})
|
|
405
|
+
|
|
406
|
+
export const SERIES_DEFINITION = definePageDefinition({
|
|
407
|
+
typeLabelKey: "page.type.series",
|
|
408
|
+
properties: {
|
|
409
|
+
details: {
|
|
410
|
+
label: { en: "Details" },
|
|
411
|
+
defaultOpen: true,
|
|
412
|
+
fields: {
|
|
413
|
+
genre: { type: "text", label: { en: "Genre" }, defaultValue: { en: "Fantasy" } },
|
|
414
|
+
status: {
|
|
415
|
+
type: "enum",
|
|
416
|
+
label: { en: "Status" },
|
|
417
|
+
defaultValue: { en: "In Development" },
|
|
418
|
+
options: [
|
|
419
|
+
{ en: "In Development" },
|
|
420
|
+
{ en: "Ongoing" },
|
|
421
|
+
{ en: "Completed" },
|
|
422
|
+
{ en: "Cancelled" }
|
|
423
|
+
]
|
|
424
|
+
},
|
|
425
|
+
seasons: { type: "number", label: { en: "Seasons" }, defaultValue: 1 }
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
},
|
|
429
|
+
initialBlocks: () => [
|
|
430
|
+
{
|
|
431
|
+
id: uuid(),
|
|
432
|
+
type: "SectionBlock",
|
|
433
|
+
props: { level: 2, title: "Overview", children: [] },
|
|
434
|
+
isTemplated: true
|
|
435
|
+
},
|
|
436
|
+
{
|
|
437
|
+
id: uuid(),
|
|
438
|
+
type: "SectionBlock",
|
|
439
|
+
props: { level: 2, title: "Seasons", children: [] },
|
|
440
|
+
isTemplated: true
|
|
441
|
+
}
|
|
442
|
+
]
|
|
443
|
+
})
|
|
444
|
+
|
|
445
|
+
// ─── Page Map ─────────────────────────────────────────────────────────────────
|
|
446
|
+
|
|
447
|
+
export const PAGE_MAP: Record<string, PageDefinition> = {
|
|
448
|
+
Document: DOCUMENT_DEFINITION,
|
|
449
|
+
BlogPost: BLOG_POST_DEFINITION,
|
|
450
|
+
PatchNote: PATCH_NOTE_DEFINITION,
|
|
451
|
+
Location: LOCATION_DEFINITION,
|
|
452
|
+
Species: SPECIES_DEFINITION,
|
|
453
|
+
Character: CHARACTER_DEFINITION,
|
|
454
|
+
Skill: SKILL_DEFINITION,
|
|
455
|
+
Item: ITEM_DEFINITION,
|
|
456
|
+
Object: OBJECT_DEFINITION,
|
|
457
|
+
Group: GROUP_DEFINITION,
|
|
458
|
+
Card: CARD_DEFINITION,
|
|
459
|
+
Hero: HERO_DEFINITION,
|
|
460
|
+
Series: SERIES_DEFINITION,
|
|
461
|
+
Tale: definePageDefinition({ typeLabelKey: "page.type.tale", properties: {} }),
|
|
462
|
+
Episode: definePageDefinition({ typeLabelKey: "page.type.episode", properties: {} }),
|
|
463
|
+
Default: definePageDefinition({ typeLabelKey: "page.type.default", properties: {} })
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
export function getPageDefinition(type: string): PageDefinition {
|
|
467
|
+
if (!type) return PAGE_MAP.Default!
|
|
468
|
+
|
|
469
|
+
// Exact match
|
|
470
|
+
if (PAGE_MAP[type]) return PAGE_MAP[type]
|
|
471
|
+
|
|
472
|
+
// Case-insensitive / normalized lookup
|
|
473
|
+
const normalized = type.toLowerCase().replace(/[^a-z0-9]/g, "")
|
|
474
|
+
|
|
475
|
+
const ALIAS_MAP: Record<string, PageDefinition> = {
|
|
476
|
+
blog: BLOG_POST_DEFINITION,
|
|
477
|
+
blogpost: BLOG_POST_DEFINITION,
|
|
478
|
+
article: BLOG_POST_DEFINITION,
|
|
479
|
+
doc: DOCUMENT_DEFINITION,
|
|
480
|
+
document: DOCUMENT_DEFINITION,
|
|
481
|
+
patchnote: PATCH_NOTE_DEFINITION,
|
|
482
|
+
location: LOCATION_DEFINITION,
|
|
483
|
+
species: SPECIES_DEFINITION,
|
|
484
|
+
character: CHARACTER_DEFINITION,
|
|
485
|
+
skill: SKILL_DEFINITION,
|
|
486
|
+
item: ITEM_DEFINITION,
|
|
487
|
+
object: OBJECT_DEFINITION,
|
|
488
|
+
group: GROUP_DEFINITION,
|
|
489
|
+
card: CARD_DEFINITION,
|
|
490
|
+
hero: HERO_DEFINITION,
|
|
491
|
+
series: SERIES_DEFINITION
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
if (ALIAS_MAP[normalized]) return ALIAS_MAP[normalized]
|
|
495
|
+
|
|
496
|
+
// Fallback scan
|
|
497
|
+
for (const [key, def] of Object.entries(PAGE_MAP)) {
|
|
498
|
+
if (key.toLowerCase() === normalized) return def
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
return PAGE_MAP.Default!
|
|
502
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import type { TimeOfDay, Setting, Transition } from "./types/blocks"
|
|
2
|
+
|
|
3
|
+
export interface BlockDefinition {
|
|
4
|
+
type: string
|
|
5
|
+
label: string
|
|
6
|
+
description?: string
|
|
7
|
+
defaultProps: Record<string, any>
|
|
8
|
+
allowChildren?: boolean
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const definitions = new Map<string, BlockDefinition>()
|
|
12
|
+
|
|
13
|
+
export const BlockRegistry = {
|
|
14
|
+
register(def: BlockDefinition): void {
|
|
15
|
+
definitions.set(def.type, def)
|
|
16
|
+
},
|
|
17
|
+
|
|
18
|
+
get(type: string): BlockDefinition | undefined {
|
|
19
|
+
return definitions.get(type)
|
|
20
|
+
},
|
|
21
|
+
|
|
22
|
+
getAll(): BlockDefinition[] {
|
|
23
|
+
return Array.from(definitions.values())
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Register built-in block types
|
|
28
|
+
BlockRegistry.register({
|
|
29
|
+
type: "SectionBlock",
|
|
30
|
+
label: "Section",
|
|
31
|
+
description: "Container block for grouping nested content",
|
|
32
|
+
defaultProps: { title: "" },
|
|
33
|
+
allowChildren: true
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
BlockRegistry.register({
|
|
37
|
+
type: "ParagraphBlock",
|
|
38
|
+
label: "Paragraph",
|
|
39
|
+
description: "Rich text paragraph block",
|
|
40
|
+
defaultProps: { text: { en: "" } }
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
BlockRegistry.register({
|
|
44
|
+
type: "CalloutBlock",
|
|
45
|
+
label: "Callout",
|
|
46
|
+
description: "Highlighted callout box (info, warning, note)",
|
|
47
|
+
defaultProps: { variant: "info", text: { en: "" } }
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
BlockRegistry.register({
|
|
51
|
+
type: "ImageBlock",
|
|
52
|
+
label: "Image",
|
|
53
|
+
description: "Image component with caption and alt text",
|
|
54
|
+
defaultProps: { url: "", alt: "", caption: "" }
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
BlockRegistry.register({
|
|
58
|
+
type: "CodeBlock",
|
|
59
|
+
label: "Code",
|
|
60
|
+
description: "Code block with syntax highlighting",
|
|
61
|
+
defaultProps: { code: "", language: "typescript" }
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
BlockRegistry.register({
|
|
65
|
+
type: "ScriptBlock",
|
|
66
|
+
label: "Script",
|
|
67
|
+
description: "Script block with title, synopsis, characters, and plot tracking",
|
|
68
|
+
defaultProps: {
|
|
69
|
+
title: "",
|
|
70
|
+
synopsis: "",
|
|
71
|
+
characters: [],
|
|
72
|
+
aPlot: "",
|
|
73
|
+
bPlot: "",
|
|
74
|
+
cPlot: "",
|
|
75
|
+
stinger: ""
|
|
76
|
+
},
|
|
77
|
+
allowChildren: true
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
BlockRegistry.register({
|
|
81
|
+
type: "SceneBlock",
|
|
82
|
+
label: "Scene",
|
|
83
|
+
description: "Scene block with location, time, setting, and transition",
|
|
84
|
+
defaultProps: {
|
|
85
|
+
location: "",
|
|
86
|
+
timeOfDay: "OTHER" as TimeOfDay,
|
|
87
|
+
setting: "OTHER" as Setting,
|
|
88
|
+
transition: "NONE" as Transition,
|
|
89
|
+
description: ""
|
|
90
|
+
},
|
|
91
|
+
allowChildren: true
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
BlockRegistry.register({
|
|
95
|
+
type: "DialogueBlock",
|
|
96
|
+
label: "Dialogue",
|
|
97
|
+
description: "Dialogue block with character, parenthetical, and line",
|
|
98
|
+
defaultProps: { character: "", parenthetical: "", line: "" }
|
|
99
|
+
})
|
|
@@ -0,0 +1,75 @@
|
|
|
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
|
+
}
|