@duckpic/content-spec 0.2.0 → 0.2.2
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/dist/components/ComponentSpec.d.ts +19 -0
- package/dist/components/Layout.js +6 -1
- package/dist/components/Multipage.d.ts +7 -0
- package/dist/components/Multipage.js +15 -0
- package/dist/components/Page.js +6 -1
- package/dist/components/Quiz.js +6 -1
- package/dist/components/Text.js +6 -1
- package/dist/components/Video.d.ts +7 -0
- package/dist/components/Video.js +15 -0
- package/dist/files.txt +265 -0
- package/dist/index.d.ts +8 -2
- package/dist/index.js +32 -1
- package/package.json +1 -1
- package/src/components/ComponentSpec.ts +20 -0
- package/src/components/Layout.ts +6 -1
- package/src/components/Multipage.ts +18 -0
- package/src/components/Page.ts +6 -1
- package/src/components/Quiz.ts +6 -1
- package/src/components/Text.ts +6 -1
- package/src/components/Video.ts +18 -0
- package/src/index.ts +39 -3
- package/src/files.txt +0 -160
|
@@ -1,6 +1,25 @@
|
|
|
1
1
|
import type { ZodType } from "zod";
|
|
2
|
+
export interface ComponentUiMetadata {
|
|
3
|
+
/**
|
|
4
|
+
* Human-friendly label for menus and UI surfaces.
|
|
5
|
+
*/
|
|
6
|
+
label?: string;
|
|
7
|
+
/**
|
|
8
|
+
* Whether this component can be inserted by users. Defaults to true.
|
|
9
|
+
*/
|
|
10
|
+
insertable?: boolean;
|
|
11
|
+
/**
|
|
12
|
+
* Optional sort order for insertion menus. Lower comes first.
|
|
13
|
+
*/
|
|
14
|
+
insertMenuOrder?: number;
|
|
15
|
+
/**
|
|
16
|
+
* Optional icon identifier the host app can map to an asset set.
|
|
17
|
+
*/
|
|
18
|
+
iconId?: string;
|
|
19
|
+
}
|
|
2
20
|
export interface ComponentSpec {
|
|
3
21
|
type: string;
|
|
4
22
|
propsSchema: ZodType<any>;
|
|
5
23
|
slots: string[];
|
|
24
|
+
ui?: ComponentUiMetadata;
|
|
6
25
|
}
|
|
@@ -10,5 +10,10 @@ export const LayoutPropsSchema = z.object({
|
|
|
10
10
|
export const LayoutComponentSpec = {
|
|
11
11
|
type: "Layout",
|
|
12
12
|
propsSchema: LayoutPropsSchema,
|
|
13
|
-
slots: ["content"]
|
|
13
|
+
slots: ["content"],
|
|
14
|
+
ui: {
|
|
15
|
+
label: "Layout",
|
|
16
|
+
insertable: true,
|
|
17
|
+
insertMenuOrder: 20,
|
|
18
|
+
}
|
|
14
19
|
};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import type { ComponentSpec } from "./ComponentSpec";
|
|
3
|
+
export declare const MultipagePropsSchema: z.ZodObject<{
|
|
4
|
+
title: z.ZodOptional<z.ZodString>;
|
|
5
|
+
description: z.ZodOptional<z.ZodString>;
|
|
6
|
+
}, z.core.$strip>;
|
|
7
|
+
export declare const MultipageComponentSpec: ComponentSpec;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export const MultipagePropsSchema = z.object({
|
|
3
|
+
title: z.string().optional(),
|
|
4
|
+
description: z.string().optional(),
|
|
5
|
+
});
|
|
6
|
+
export const MultipageComponentSpec = {
|
|
7
|
+
type: "Multipage",
|
|
8
|
+
propsSchema: MultipagePropsSchema,
|
|
9
|
+
slots: ["pages"],
|
|
10
|
+
ui: {
|
|
11
|
+
label: "Multipage",
|
|
12
|
+
insertable: true,
|
|
13
|
+
insertMenuOrder: 40,
|
|
14
|
+
},
|
|
15
|
+
};
|
package/dist/components/Page.js
CHANGED
package/dist/components/Quiz.js
CHANGED
|
@@ -11,5 +11,10 @@ export const QuizPropsSchema = z.object({
|
|
|
11
11
|
export const QuizComponentSpec = {
|
|
12
12
|
type: "Quiz",
|
|
13
13
|
propsSchema: QuizPropsSchema,
|
|
14
|
-
slots: [] // quizzes don’t have child nodes
|
|
14
|
+
slots: [], // quizzes don’t have child nodes
|
|
15
|
+
ui: {
|
|
16
|
+
label: "Quiz",
|
|
17
|
+
insertable: false,
|
|
18
|
+
insertMenuOrder: 30,
|
|
19
|
+
}
|
|
15
20
|
};
|
package/dist/components/Text.js
CHANGED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import type { ComponentSpec } from "./ComponentSpec";
|
|
3
|
+
export declare const VideoPropsSchema: z.ZodObject<{
|
|
4
|
+
title: z.ZodOptional<z.ZodString>;
|
|
5
|
+
link: z.ZodOptional<z.ZodString>;
|
|
6
|
+
}, z.core.$strip>;
|
|
7
|
+
export declare const VideoComponentSpec: ComponentSpec;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export const VideoPropsSchema = z.object({
|
|
3
|
+
title: z.string().optional(),
|
|
4
|
+
link: z.string().optional(),
|
|
5
|
+
});
|
|
6
|
+
export const VideoComponentSpec = {
|
|
7
|
+
type: "Video",
|
|
8
|
+
propsSchema: VideoPropsSchema,
|
|
9
|
+
slots: [],
|
|
10
|
+
ui: {
|
|
11
|
+
label: "Video",
|
|
12
|
+
insertable: true,
|
|
13
|
+
insertMenuOrder: 25,
|
|
14
|
+
}
|
|
15
|
+
};
|
package/dist/files.txt
ADDED
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
--components/ComponentSpec.d.ts--
|
|
2
|
+
import type { ZodType } from "zod";
|
|
3
|
+
export interface ComponentSpec {
|
|
4
|
+
type: string;
|
|
5
|
+
propsSchema: ZodType<any>;
|
|
6
|
+
slots: string[];
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
--components/ComponentSpec.js--
|
|
11
|
+
export {};
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
--components/Layout.d.ts--
|
|
15
|
+
import { z } from "zod";
|
|
16
|
+
import type { ComponentSpec } from "./ComponentSpec";
|
|
17
|
+
export declare const LayoutPropsSchema: z.ZodObject<{
|
|
18
|
+
backgroundColor: z.ZodOptional<z.ZodString>;
|
|
19
|
+
style: z.ZodOptional<z.ZodObject<{
|
|
20
|
+
backgroundColor: z.ZodOptional<z.ZodString>;
|
|
21
|
+
padding: z.ZodOptional<z.ZodString>;
|
|
22
|
+
borderRadius: z.ZodOptional<z.ZodString>;
|
|
23
|
+
}, z.core.$strip>>;
|
|
24
|
+
}, z.core.$strip>;
|
|
25
|
+
export declare const LayoutComponentSpec: ComponentSpec;
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
--components/Layout.js--
|
|
29
|
+
import { z } from "zod";
|
|
30
|
+
export const LayoutPropsSchema = z.object({
|
|
31
|
+
backgroundColor: z.string().optional(),
|
|
32
|
+
style: z.object({
|
|
33
|
+
backgroundColor: z.string().optional(),
|
|
34
|
+
padding: z.string().optional(),
|
|
35
|
+
borderRadius: z.string().optional()
|
|
36
|
+
}).optional()
|
|
37
|
+
});
|
|
38
|
+
export const LayoutComponentSpec = {
|
|
39
|
+
type: "Layout",
|
|
40
|
+
propsSchema: LayoutPropsSchema,
|
|
41
|
+
slots: ["content"]
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
--components/Page.d.ts--
|
|
46
|
+
import { z } from "zod";
|
|
47
|
+
import type { ComponentSpec } from "./ComponentSpec";
|
|
48
|
+
export declare const PagePropsSchema: z.ZodObject<{
|
|
49
|
+
title: z.ZodOptional<z.ZodString>;
|
|
50
|
+
backgroundColor: z.ZodOptional<z.ZodString>;
|
|
51
|
+
}, z.core.$strip>;
|
|
52
|
+
export declare const PageComponentSpec: ComponentSpec;
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
--components/Page.js--
|
|
56
|
+
import { z } from "zod";
|
|
57
|
+
export const PagePropsSchema = z.object({
|
|
58
|
+
title: z.string().optional(),
|
|
59
|
+
backgroundColor: z.string().optional()
|
|
60
|
+
});
|
|
61
|
+
export const PageComponentSpec = {
|
|
62
|
+
type: "Page",
|
|
63
|
+
propsSchema: PagePropsSchema,
|
|
64
|
+
slots: ["main"]
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
--components/Quiz.d.ts--
|
|
69
|
+
import { z } from "zod";
|
|
70
|
+
import { ComponentSpec } from "./ComponentSpec";
|
|
71
|
+
export declare const QuizQuestionSchema: z.ZodObject<{
|
|
72
|
+
q: z.ZodString;
|
|
73
|
+
a: z.ZodString;
|
|
74
|
+
}, z.core.$strip>;
|
|
75
|
+
export declare const QuizPropsSchema: z.ZodObject<{
|
|
76
|
+
title: z.ZodOptional<z.ZodString>;
|
|
77
|
+
questions: z.ZodArray<z.ZodObject<{
|
|
78
|
+
q: z.ZodString;
|
|
79
|
+
a: z.ZodString;
|
|
80
|
+
}, z.core.$strip>>;
|
|
81
|
+
backgroundColor: z.ZodOptional<z.ZodString>;
|
|
82
|
+
}, z.core.$strip>;
|
|
83
|
+
export type QuizProps = z.infer<typeof QuizPropsSchema>;
|
|
84
|
+
export declare const QuizComponentSpec: ComponentSpec;
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
--components/Quiz.js--
|
|
88
|
+
import { z } from "zod";
|
|
89
|
+
export const QuizQuestionSchema = z.object({
|
|
90
|
+
q: z.string(),
|
|
91
|
+
a: z.string()
|
|
92
|
+
});
|
|
93
|
+
export const QuizPropsSchema = z.object({
|
|
94
|
+
title: z.string().optional(),
|
|
95
|
+
questions: z.array(QuizQuestionSchema).min(1),
|
|
96
|
+
backgroundColor: z.string().optional()
|
|
97
|
+
});
|
|
98
|
+
export const QuizComponentSpec = {
|
|
99
|
+
type: "Quiz",
|
|
100
|
+
propsSchema: QuizPropsSchema,
|
|
101
|
+
slots: [] // quizzes don’t have child nodes
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
--components/Text.d.ts--
|
|
106
|
+
import { z } from "zod";
|
|
107
|
+
import type { ComponentSpec } from "./ComponentSpec";
|
|
108
|
+
export declare const TextPropsSchema: z.ZodObject<{
|
|
109
|
+
variant: z.ZodEnum<{
|
|
110
|
+
body: "body";
|
|
111
|
+
h1: "h1";
|
|
112
|
+
h2: "h2";
|
|
113
|
+
h3: "h3";
|
|
114
|
+
}>;
|
|
115
|
+
text: z.ZodString;
|
|
116
|
+
backgroundColor: z.ZodOptional<z.ZodString>;
|
|
117
|
+
}, z.core.$strip>;
|
|
118
|
+
export declare const TextComponentSpec: ComponentSpec;
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
--components/Text.js--
|
|
122
|
+
import { z } from "zod";
|
|
123
|
+
export const TextPropsSchema = z.object({
|
|
124
|
+
variant: z.enum(["h1", "h2", "h3", "body"]),
|
|
125
|
+
text: z.string(),
|
|
126
|
+
backgroundColor: z.string().optional()
|
|
127
|
+
});
|
|
128
|
+
export const TextComponentSpec = {
|
|
129
|
+
type: "Text",
|
|
130
|
+
propsSchema: TextPropsSchema,
|
|
131
|
+
slots: []
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
--index.d.ts--
|
|
136
|
+
export * from "./components/Text";
|
|
137
|
+
export * from "./components/Quiz";
|
|
138
|
+
export * from "./components/Layout";
|
|
139
|
+
export * from "./components/Page";
|
|
140
|
+
export * from "./schema/PageTree";
|
|
141
|
+
import type { ComponentSpec } from "./components/ComponentSpec.ts";
|
|
142
|
+
export declare const ComponentRegistry: {
|
|
143
|
+
Text: ComponentSpec;
|
|
144
|
+
Quiz: ComponentSpec;
|
|
145
|
+
Layout: ComponentSpec;
|
|
146
|
+
Page: ComponentSpec;
|
|
147
|
+
};
|
|
148
|
+
export type ComponentType = keyof typeof ComponentRegistry;
|
|
149
|
+
export declare function getPropsSchema(type: ComponentType): import("zod").ZodType<any, unknown, import("zod/v4/core").$ZodTypeInternals<any, unknown>>;
|
|
150
|
+
export type { ComponentSpec };
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
--index.js--
|
|
154
|
+
export * from "./components/Text";
|
|
155
|
+
export * from "./components/Quiz";
|
|
156
|
+
export * from "./components/Layout";
|
|
157
|
+
export * from "./components/Page";
|
|
158
|
+
export * from "./schema/PageTree";
|
|
159
|
+
import { TextComponentSpec } from "./components/Text";
|
|
160
|
+
import { QuizComponentSpec } from "./components/Quiz";
|
|
161
|
+
import { LayoutComponentSpec } from "./components/Layout";
|
|
162
|
+
import { PageComponentSpec } from "./components/Page";
|
|
163
|
+
export const ComponentRegistry = {
|
|
164
|
+
Text: TextComponentSpec,
|
|
165
|
+
Quiz: QuizComponentSpec,
|
|
166
|
+
Layout: LayoutComponentSpec,
|
|
167
|
+
Page: PageComponentSpec
|
|
168
|
+
};
|
|
169
|
+
// Helper to get a schema by type
|
|
170
|
+
export function getPropsSchema(type) {
|
|
171
|
+
return ComponentRegistry[type].propsSchema;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
--schema/PageTree.d.ts--
|
|
176
|
+
import { z } from "zod";
|
|
177
|
+
export declare const NodeIdSchema: z.ZodString;
|
|
178
|
+
export declare const ContentNodeSchema: z.ZodObject<{
|
|
179
|
+
id: z.ZodString;
|
|
180
|
+
type: z.ZodString;
|
|
181
|
+
props: z.ZodRecord<z.ZodString, z.ZodAny>;
|
|
182
|
+
slots: z.ZodRecord<z.ZodString, z.ZodArray<z.ZodString>>;
|
|
183
|
+
}, z.core.$strip>;
|
|
184
|
+
export type ContentNode = z.infer<typeof ContentNodeSchema>;
|
|
185
|
+
export declare const PageTreeSchema: z.ZodObject<{
|
|
186
|
+
version: z.ZodString;
|
|
187
|
+
root: z.ZodString;
|
|
188
|
+
nodes: z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
189
|
+
id: z.ZodString;
|
|
190
|
+
type: z.ZodString;
|
|
191
|
+
props: z.ZodRecord<z.ZodString, z.ZodAny>;
|
|
192
|
+
slots: z.ZodRecord<z.ZodString, z.ZodArray<z.ZodString>>;
|
|
193
|
+
}, z.core.$strip>>;
|
|
194
|
+
}, z.core.$strip>;
|
|
195
|
+
export type PageTree = z.infer<typeof PageTreeSchema>;
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
--schema/PageTree.js--
|
|
199
|
+
import { z } from "zod";
|
|
200
|
+
export const NodeIdSchema = z.string().min(1);
|
|
201
|
+
// You can switch to stricter UUID validation later.
|
|
202
|
+
export const ContentNodeSchema = z.object({
|
|
203
|
+
id: NodeIdSchema,
|
|
204
|
+
type: z.string(), // we'll refine typing later
|
|
205
|
+
props: z.record(z.string(), z.any()), // key: string, value: any
|
|
206
|
+
slots: z.record(z.string(), z.array(NodeIdSchema))
|
|
207
|
+
});
|
|
208
|
+
export const PageTreeSchema = z.object({
|
|
209
|
+
version: z.string(),
|
|
210
|
+
root: NodeIdSchema,
|
|
211
|
+
nodes: z.record(z.string(), ContentNodeSchema)
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
--validators/validateNode.d.ts--
|
|
216
|
+
import { ContentNode } from "..";
|
|
217
|
+
export declare function validateNode(node: ContentNode): void;
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
--validators/validateNode.js--
|
|
221
|
+
import { ComponentRegistry } from "..";
|
|
222
|
+
export function validateNode(node) {
|
|
223
|
+
const type = node.type;
|
|
224
|
+
const spec = ComponentRegistry[type];
|
|
225
|
+
if (!spec) {
|
|
226
|
+
throw new Error(`Unknown component type: ${node.type}`);
|
|
227
|
+
}
|
|
228
|
+
// 1) Validate props
|
|
229
|
+
spec.propsSchema.parse(node.props);
|
|
230
|
+
// 2) Validate slots
|
|
231
|
+
const allowedSlots = (spec.slots ?? []);
|
|
232
|
+
for (const slotName of Object.keys(node.slots)) {
|
|
233
|
+
if (!allowedSlots.includes(slotName)) {
|
|
234
|
+
throw new Error(`Invalid slot '${slotName}' for component type ${node.type}`);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
--validators/validatePageTree.d.ts--
|
|
241
|
+
export declare function validatePageTree(tree: unknown): {
|
|
242
|
+
version: string;
|
|
243
|
+
root: string;
|
|
244
|
+
nodes: Record<string, {
|
|
245
|
+
id: string;
|
|
246
|
+
type: string;
|
|
247
|
+
props: Record<string, any>;
|
|
248
|
+
slots: Record<string, string[]>;
|
|
249
|
+
}>;
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
--validators/validatePageTree.js--
|
|
254
|
+
import { PageTreeSchema } from "../schema/PageTree";
|
|
255
|
+
import { validateNode } from "./validateNode";
|
|
256
|
+
export function validatePageTree(tree) {
|
|
257
|
+
const data = PageTreeSchema.parse(tree); // main shape
|
|
258
|
+
// Validate nodes individually
|
|
259
|
+
for (const node of Object.values(data.nodes)) {
|
|
260
|
+
validateNode(node);
|
|
261
|
+
}
|
|
262
|
+
return data;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
|
package/dist/index.d.ts
CHANGED
|
@@ -2,14 +2,20 @@ export * from "./components/Text";
|
|
|
2
2
|
export * from "./components/Quiz";
|
|
3
3
|
export * from "./components/Layout";
|
|
4
4
|
export * from "./components/Page";
|
|
5
|
+
export * from "./components/Multipage";
|
|
6
|
+
export * from "./components/Video";
|
|
5
7
|
export * from "./schema/PageTree";
|
|
6
|
-
import type { ComponentSpec } from "./components/ComponentSpec.ts";
|
|
8
|
+
import type { ComponentSpec, ComponentUiMetadata } from "./components/ComponentSpec.ts";
|
|
7
9
|
export declare const ComponentRegistry: {
|
|
8
10
|
Text: ComponentSpec;
|
|
9
11
|
Quiz: ComponentSpec;
|
|
10
12
|
Layout: ComponentSpec;
|
|
11
13
|
Page: ComponentSpec;
|
|
14
|
+
Multipage: ComponentSpec;
|
|
15
|
+
Video: ComponentSpec;
|
|
12
16
|
};
|
|
13
17
|
export type ComponentType = keyof typeof ComponentRegistry;
|
|
14
18
|
export declare function getPropsSchema(type: ComponentType): import("zod").ZodType<any, unknown, import("zod/v4/core").$ZodTypeInternals<any, unknown>>;
|
|
15
|
-
export
|
|
19
|
+
export declare function getInsertableComponentEntries(): Array<[ComponentType, ComponentSpec]>;
|
|
20
|
+
export declare function getInsertableComponentTypes(): ComponentType[];
|
|
21
|
+
export type { ComponentSpec, ComponentUiMetadata };
|
package/dist/index.js
CHANGED
|
@@ -2,18 +2,49 @@ export * from "./components/Text";
|
|
|
2
2
|
export * from "./components/Quiz";
|
|
3
3
|
export * from "./components/Layout";
|
|
4
4
|
export * from "./components/Page";
|
|
5
|
+
export * from "./components/Multipage";
|
|
6
|
+
export * from "./components/Video";
|
|
5
7
|
export * from "./schema/PageTree";
|
|
6
8
|
import { TextComponentSpec } from "./components/Text";
|
|
7
9
|
import { QuizComponentSpec } from "./components/Quiz";
|
|
8
10
|
import { LayoutComponentSpec } from "./components/Layout";
|
|
9
11
|
import { PageComponentSpec } from "./components/Page";
|
|
12
|
+
import { MultipageComponentSpec } from "./components/Multipage";
|
|
13
|
+
import { VideoComponentSpec } from "./components/Video";
|
|
10
14
|
export const ComponentRegistry = {
|
|
11
15
|
Text: TextComponentSpec,
|
|
12
16
|
Quiz: QuizComponentSpec,
|
|
13
17
|
Layout: LayoutComponentSpec,
|
|
14
|
-
Page: PageComponentSpec
|
|
18
|
+
Page: PageComponentSpec,
|
|
19
|
+
Multipage: MultipageComponentSpec,
|
|
20
|
+
Video: VideoComponentSpec,
|
|
15
21
|
};
|
|
16
22
|
// Helper to get a schema by type
|
|
17
23
|
export function getPropsSchema(type) {
|
|
18
24
|
return ComponentRegistry[type].propsSchema;
|
|
19
25
|
}
|
|
26
|
+
function isInsertable(spec) {
|
|
27
|
+
return spec.ui?.insertable !== false;
|
|
28
|
+
}
|
|
29
|
+
function getInsertOrder(spec) {
|
|
30
|
+
if (typeof spec.ui?.insertMenuOrder === "number") {
|
|
31
|
+
return spec.ui.insertMenuOrder;
|
|
32
|
+
}
|
|
33
|
+
return Number.POSITIVE_INFINITY;
|
|
34
|
+
}
|
|
35
|
+
export function getInsertableComponentEntries() {
|
|
36
|
+
return Object.entries(ComponentRegistry)
|
|
37
|
+
.filter(([, spec]) => isInsertable(spec))
|
|
38
|
+
.sort(([, aSpec], [, bSpec]) => {
|
|
39
|
+
const aOrder = getInsertOrder(aSpec);
|
|
40
|
+
const bOrder = getInsertOrder(bSpec);
|
|
41
|
+
if (aOrder !== bOrder)
|
|
42
|
+
return aOrder - bOrder;
|
|
43
|
+
const aLabel = aSpec.ui?.label ?? aSpec.type;
|
|
44
|
+
const bLabel = bSpec.ui?.label ?? bSpec.type;
|
|
45
|
+
return aLabel.localeCompare(bLabel);
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
export function getInsertableComponentTypes() {
|
|
49
|
+
return getInsertableComponentEntries().map(([type]) => type);
|
|
50
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,27 @@
|
|
|
1
1
|
import type { ZodType } from "zod";
|
|
2
2
|
|
|
3
|
+
export interface ComponentUiMetadata {
|
|
4
|
+
/**
|
|
5
|
+
* Human-friendly label for menus and UI surfaces.
|
|
6
|
+
*/
|
|
7
|
+
label?: string;
|
|
8
|
+
/**
|
|
9
|
+
* Whether this component can be inserted by users. Defaults to true.
|
|
10
|
+
*/
|
|
11
|
+
insertable?: boolean;
|
|
12
|
+
/**
|
|
13
|
+
* Optional sort order for insertion menus. Lower comes first.
|
|
14
|
+
*/
|
|
15
|
+
insertMenuOrder?: number;
|
|
16
|
+
/**
|
|
17
|
+
* Optional icon identifier the host app can map to an asset set.
|
|
18
|
+
*/
|
|
19
|
+
iconId?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
3
22
|
export interface ComponentSpec {
|
|
4
23
|
type: string;
|
|
5
24
|
propsSchema: ZodType<any>;
|
|
6
25
|
slots: string[];
|
|
26
|
+
ui?: ComponentUiMetadata;
|
|
7
27
|
}
|
package/src/components/Layout.ts
CHANGED
|
@@ -13,5 +13,10 @@ export const LayoutPropsSchema = z.object({
|
|
|
13
13
|
export const LayoutComponentSpec: ComponentSpec = {
|
|
14
14
|
type: "Layout",
|
|
15
15
|
propsSchema: LayoutPropsSchema,
|
|
16
|
-
slots: ["content"]
|
|
16
|
+
slots: ["content"],
|
|
17
|
+
ui: {
|
|
18
|
+
label: "Layout",
|
|
19
|
+
insertable: true,
|
|
20
|
+
insertMenuOrder: 20,
|
|
21
|
+
}
|
|
17
22
|
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import type { ComponentSpec } from "./ComponentSpec";
|
|
3
|
+
|
|
4
|
+
export const MultipagePropsSchema = z.object({
|
|
5
|
+
title: z.string().optional(),
|
|
6
|
+
description: z.string().optional(),
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
export const MultipageComponentSpec: ComponentSpec = {
|
|
10
|
+
type: "Multipage",
|
|
11
|
+
propsSchema: MultipagePropsSchema,
|
|
12
|
+
slots: ["pages"],
|
|
13
|
+
ui: {
|
|
14
|
+
label: "Multipage",
|
|
15
|
+
insertable: true,
|
|
16
|
+
insertMenuOrder: 40,
|
|
17
|
+
},
|
|
18
|
+
};
|
package/src/components/Page.ts
CHANGED
|
@@ -9,5 +9,10 @@ export const PagePropsSchema = z.object({
|
|
|
9
9
|
export const PageComponentSpec: ComponentSpec = {
|
|
10
10
|
type: "Page",
|
|
11
11
|
propsSchema: PagePropsSchema,
|
|
12
|
-
slots: ["main"]
|
|
12
|
+
slots: ["main"],
|
|
13
|
+
ui: {
|
|
14
|
+
label: "Page",
|
|
15
|
+
insertable: false,
|
|
16
|
+
insertMenuOrder: 0,
|
|
17
|
+
}
|
|
13
18
|
};
|
package/src/components/Quiz.ts
CHANGED
|
@@ -17,5 +17,10 @@ export type QuizProps = z.infer<typeof QuizPropsSchema>;
|
|
|
17
17
|
export const QuizComponentSpec: ComponentSpec = {
|
|
18
18
|
type: "Quiz",
|
|
19
19
|
propsSchema: QuizPropsSchema,
|
|
20
|
-
slots: [] // quizzes don’t have child nodes
|
|
20
|
+
slots: [], // quizzes don’t have child nodes
|
|
21
|
+
ui: {
|
|
22
|
+
label: "Quiz",
|
|
23
|
+
insertable: false,
|
|
24
|
+
insertMenuOrder: 30,
|
|
25
|
+
}
|
|
21
26
|
};
|
package/src/components/Text.ts
CHANGED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import type { ComponentSpec } from "./ComponentSpec";
|
|
3
|
+
|
|
4
|
+
export const VideoPropsSchema = z.object({
|
|
5
|
+
title: z.string().optional(),
|
|
6
|
+
link: z.string().optional(),
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
export const VideoComponentSpec: ComponentSpec = {
|
|
10
|
+
type: "Video",
|
|
11
|
+
propsSchema: VideoPropsSchema,
|
|
12
|
+
slots: [],
|
|
13
|
+
ui: {
|
|
14
|
+
label: "Video",
|
|
15
|
+
insertable: true,
|
|
16
|
+
insertMenuOrder: 25,
|
|
17
|
+
}
|
|
18
|
+
};
|
package/src/index.ts
CHANGED
|
@@ -2,6 +2,8 @@ export * from "./components/Text";
|
|
|
2
2
|
export * from "./components/Quiz";
|
|
3
3
|
export * from "./components/Layout";
|
|
4
4
|
export * from "./components/Page";
|
|
5
|
+
export * from "./components/Multipage";
|
|
6
|
+
export * from "./components/Video";
|
|
5
7
|
export * from "./schema/PageTree";
|
|
6
8
|
|
|
7
9
|
|
|
@@ -9,13 +11,17 @@ import { TextComponentSpec } from "./components/Text";
|
|
|
9
11
|
import { QuizComponentSpec } from "./components/Quiz";
|
|
10
12
|
import { LayoutComponentSpec } from "./components/Layout";
|
|
11
13
|
import { PageComponentSpec } from "./components/Page";
|
|
12
|
-
import
|
|
14
|
+
import { MultipageComponentSpec } from "./components/Multipage";
|
|
15
|
+
import { VideoComponentSpec } from "./components/Video";
|
|
16
|
+
import type { ComponentSpec, ComponentUiMetadata } from "./components/ComponentSpec.ts";
|
|
13
17
|
|
|
14
18
|
export const ComponentRegistry = {
|
|
15
19
|
Text: TextComponentSpec,
|
|
16
20
|
Quiz: QuizComponentSpec,
|
|
17
21
|
Layout: LayoutComponentSpec,
|
|
18
|
-
Page: PageComponentSpec
|
|
22
|
+
Page: PageComponentSpec,
|
|
23
|
+
Multipage: MultipageComponentSpec,
|
|
24
|
+
Video: VideoComponentSpec,
|
|
19
25
|
};
|
|
20
26
|
|
|
21
27
|
export type ComponentType = keyof typeof ComponentRegistry;
|
|
@@ -24,4 +30,34 @@ export type ComponentType = keyof typeof ComponentRegistry;
|
|
|
24
30
|
export function getPropsSchema(type: ComponentType) {
|
|
25
31
|
return ComponentRegistry[type].propsSchema;
|
|
26
32
|
}
|
|
27
|
-
|
|
33
|
+
|
|
34
|
+
function isInsertable(spec: ComponentSpec) {
|
|
35
|
+
return spec.ui?.insertable !== false;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function getInsertOrder(spec: ComponentSpec) {
|
|
39
|
+
if (typeof spec.ui?.insertMenuOrder === "number") {
|
|
40
|
+
return spec.ui.insertMenuOrder;
|
|
41
|
+
}
|
|
42
|
+
return Number.POSITIVE_INFINITY;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function getInsertableComponentEntries(): Array<[ComponentType, ComponentSpec]> {
|
|
46
|
+
return (Object.entries(ComponentRegistry) as Array<[ComponentType, ComponentSpec]>)
|
|
47
|
+
.filter(([, spec]) => isInsertable(spec))
|
|
48
|
+
.sort(([, aSpec], [, bSpec]) => {
|
|
49
|
+
const aOrder = getInsertOrder(aSpec);
|
|
50
|
+
const bOrder = getInsertOrder(bSpec);
|
|
51
|
+
if (aOrder !== bOrder) return aOrder - bOrder;
|
|
52
|
+
|
|
53
|
+
const aLabel = aSpec.ui?.label ?? aSpec.type;
|
|
54
|
+
const bLabel = bSpec.ui?.label ?? bSpec.type;
|
|
55
|
+
return aLabel.localeCompare(bLabel);
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function getInsertableComponentTypes(): ComponentType[] {
|
|
60
|
+
return getInsertableComponentEntries().map(([type]) => type);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export type { ComponentSpec, ComponentUiMetadata };
|
package/src/files.txt
DELETED
|
@@ -1,160 +0,0 @@
|
|
|
1
|
-
--components/ComponentSpec.ts--
|
|
2
|
-
import type { ZodType } from "zod";
|
|
3
|
-
|
|
4
|
-
export interface ComponentSpec {
|
|
5
|
-
type: string;
|
|
6
|
-
propsSchema: ZodType<any>;
|
|
7
|
-
slots: string[];
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
--components/Layout.ts--
|
|
12
|
-
import { z } from "zod";
|
|
13
|
-
import type { ComponentSpec } from "./ComponentSpec";
|
|
14
|
-
|
|
15
|
-
export const LayoutPropsSchema = z.object({
|
|
16
|
-
style: z.object({
|
|
17
|
-
backgroundColor: z.string().optional(),
|
|
18
|
-
padding: z.string().optional(),
|
|
19
|
-
borderRadius: z.string().optional()
|
|
20
|
-
}).optional()
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
export const LayoutComponentSpec: ComponentSpec = {
|
|
24
|
-
type: "Layout",
|
|
25
|
-
propsSchema: LayoutPropsSchema,
|
|
26
|
-
slots: ["content"]
|
|
27
|
-
};
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
--components/Quiz.ts--
|
|
31
|
-
import { z } from "zod";
|
|
32
|
-
import { ComponentSpec } from "./ComponentSpec";
|
|
33
|
-
|
|
34
|
-
export const QuizQuestionSchema = z.object({
|
|
35
|
-
q: z.string(),
|
|
36
|
-
a: z.string()
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
export const QuizPropsSchema = z.object({
|
|
40
|
-
title: z.string().optional(),
|
|
41
|
-
questions: z.array(QuizQuestionSchema).min(1)
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
export type QuizProps = z.infer<typeof QuizPropsSchema>;
|
|
45
|
-
|
|
46
|
-
export const QuizComponentSpec: ComponentSpec = {
|
|
47
|
-
type: "Quiz",
|
|
48
|
-
propsSchema: QuizPropsSchema,
|
|
49
|
-
slots: [] // quizzes don’t have child nodes
|
|
50
|
-
};
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
--components/Text.ts--
|
|
54
|
-
import { z } from "zod";
|
|
55
|
-
import type { ComponentSpec } from "./ComponentSpec";
|
|
56
|
-
|
|
57
|
-
export const TextPropsSchema = z.object({
|
|
58
|
-
variant: z.enum(["h1", "h2", "h3", "body"]),
|
|
59
|
-
text: z.string()
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
export const TextComponentSpec: ComponentSpec = {
|
|
63
|
-
type: "Text",
|
|
64
|
-
propsSchema: TextPropsSchema,
|
|
65
|
-
slots: []
|
|
66
|
-
};
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
--index.ts--
|
|
70
|
-
export * from "./components/Text";
|
|
71
|
-
export * from "./components/Quiz";
|
|
72
|
-
export * from "./components/Layout";
|
|
73
|
-
export * from "./schema/PageTree";
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
import { TextComponentSpec } from "./components/Text";
|
|
77
|
-
import { QuizComponentSpec } from "./components/Quiz";
|
|
78
|
-
import { LayoutComponentSpec } from "./components/Layout";
|
|
79
|
-
import type { ComponentSpec } from "./components/ComponentSpec.ts";
|
|
80
|
-
|
|
81
|
-
export const ComponentRegistry = {
|
|
82
|
-
Text: TextComponentSpec,
|
|
83
|
-
Quiz: QuizComponentSpec,
|
|
84
|
-
Layout: LayoutComponentSpec
|
|
85
|
-
};
|
|
86
|
-
|
|
87
|
-
export type ComponentType = keyof typeof ComponentRegistry;
|
|
88
|
-
|
|
89
|
-
// Helper to get a schema by type
|
|
90
|
-
export function getPropsSchema(type: ComponentType) {
|
|
91
|
-
return ComponentRegistry[type].propsSchema;
|
|
92
|
-
}
|
|
93
|
-
export type { ComponentSpec };
|
|
94
|
-
|
|
95
|
-
--schema/PageTree.ts--
|
|
96
|
-
import { z } from "zod";
|
|
97
|
-
|
|
98
|
-
export const NodeIdSchema = z.string().min(1);
|
|
99
|
-
// You can switch to stricter UUID validation later.
|
|
100
|
-
|
|
101
|
-
export const ContentNodeSchema = z.object({
|
|
102
|
-
id: NodeIdSchema,
|
|
103
|
-
type: z.string(), // we'll refine typing later
|
|
104
|
-
props: z.record(z.string(), z.any()), // key: string, value: any
|
|
105
|
-
slots: z.record(z.string(), z.array(NodeIdSchema))
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
export type ContentNode = z.infer<typeof ContentNodeSchema>;
|
|
109
|
-
|
|
110
|
-
export const PageTreeSchema = z.object({
|
|
111
|
-
version: z.string(),
|
|
112
|
-
root: NodeIdSchema,
|
|
113
|
-
nodes: z.record(z.string(), ContentNodeSchema)
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
export type PageTree = z.infer<typeof PageTreeSchema>;
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
--validators/validateNode.ts--
|
|
120
|
-
import { ComponentRegistry, ComponentType, ContentNode } from "..";
|
|
121
|
-
|
|
122
|
-
export function validateNode(node: ContentNode) {
|
|
123
|
-
const type = node.type as ComponentType;
|
|
124
|
-
const spec = ComponentRegistry[type];
|
|
125
|
-
|
|
126
|
-
if (!spec) {
|
|
127
|
-
throw new Error(`Unknown component type: ${node.type}`);
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
// 1) Validate props
|
|
131
|
-
spec.propsSchema.parse(node.props);
|
|
132
|
-
|
|
133
|
-
// 2) Validate slots
|
|
134
|
-
const allowedSlots = (spec.slots ?? []) as string[];
|
|
135
|
-
for (const slotName of Object.keys(node.slots)) {
|
|
136
|
-
if (!allowedSlots.includes(slotName)) {
|
|
137
|
-
throw new Error(
|
|
138
|
-
`Invalid slot '${slotName}' for component type ${node.type}`
|
|
139
|
-
);
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
--validators/validatePageTree.ts--
|
|
146
|
-
import { PageTreeSchema } from "../schema/PageTree";
|
|
147
|
-
import { validateNode } from "./validateNode";
|
|
148
|
-
|
|
149
|
-
export function validatePageTree(tree: unknown) {
|
|
150
|
-
const data = PageTreeSchema.parse(tree); // main shape
|
|
151
|
-
|
|
152
|
-
// Validate nodes individually
|
|
153
|
-
for (const node of Object.values(data.nodes)) {
|
|
154
|
-
validateNode(node);
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
return data;
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
|