@avocadostudio-ai/shared 0.1.0
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 +201 -0
- package/README.md +80 -0
- package/contract/operation.schema.json +752 -0
- package/dist/api-responses.d.ts +62 -0
- package/dist/api-responses.js +68 -0
- package/dist/block-manifest.d.ts +28 -0
- package/dist/block-manifest.js +247 -0
- package/dist/block-names.d.ts +19 -0
- package/dist/block-names.js +52 -0
- package/dist/blocks/_helpers.d.ts +14 -0
- package/dist/blocks/_helpers.js +26 -0
- package/dist/blocks/_registry.d.ts +116 -0
- package/dist/blocks/_registry.js +271 -0
- package/dist/blocks/banner.d.ts +1 -0
- package/dist/blocks/banner.js +34 -0
- package/dist/blocks/card-grid.d.ts +1 -0
- package/dist/blocks/card-grid.js +73 -0
- package/dist/blocks/card.d.ts +1 -0
- package/dist/blocks/card.js +37 -0
- package/dist/blocks/carousel.d.ts +1 -0
- package/dist/blocks/carousel.js +51 -0
- package/dist/blocks/cta.d.ts +1 -0
- package/dist/blocks/cta.js +35 -0
- package/dist/blocks/embed.d.ts +1 -0
- package/dist/blocks/embed.js +30 -0
- package/dist/blocks/faq-accordion.d.ts +1 -0
- package/dist/blocks/faq-accordion.js +30 -0
- package/dist/blocks/feature-grid.d.ts +1 -0
- package/dist/blocks/feature-grid.js +46 -0
- package/dist/blocks/footer.d.ts +1 -0
- package/dist/blocks/footer.js +31 -0
- package/dist/blocks/gallery.d.ts +1 -0
- package/dist/blocks/gallery.js +47 -0
- package/dist/blocks/hero.d.ts +1 -0
- package/dist/blocks/hero.js +48 -0
- package/dist/blocks/index.d.ts +3 -0
- package/dist/blocks/index.js +53 -0
- package/dist/blocks/quote.d.ts +1 -0
- package/dist/blocks/quote.js +32 -0
- package/dist/blocks/rich-text.d.ts +1 -0
- package/dist/blocks/rich-text.js +41 -0
- package/dist/blocks/site-header.d.ts +1 -0
- package/dist/blocks/site-header.js +48 -0
- package/dist/blocks/stats.d.ts +1 -0
- package/dist/blocks/stats.js +42 -0
- package/dist/blocks/table.d.ts +1 -0
- package/dist/blocks/table.js +37 -0
- package/dist/blocks/tabs.d.ts +1 -0
- package/dist/blocks/tabs.js +39 -0
- package/dist/blocks/testimonials.d.ts +1 -0
- package/dist/blocks/testimonials.js +45 -0
- package/dist/blocks/two-column.d.ts +1 -0
- package/dist/blocks/two-column.js +61 -0
- package/dist/blocks/video.d.ts +1 -0
- package/dist/blocks/video.js +33 -0
- package/dist/chat-events.d.ts +475 -0
- package/dist/chat-events.js +137 -0
- package/dist/demo-seed-content.d.ts +3 -0
- package/dist/demo-seed-content.js +1128 -0
- package/dist/draft-mode.d.ts +10 -0
- package/dist/draft-mode.js +29 -0
- package/dist/editable-path.d.ts +20 -0
- package/dist/editable-path.js +101 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +26 -0
- package/dist/ops/builders.d.ts +62 -0
- package/dist/ops/builders.js +111 -0
- package/dist/ops/theme-tokens.d.ts +50 -0
- package/dist/ops/theme-tokens.js +73 -0
- package/dist/protocol.d.ts +7 -0
- package/dist/protocol.js +7 -0
- package/dist/publish-diff.d.ts +67 -0
- package/dist/publish-diff.js +9 -0
- package/dist/schemas.d.ts +321 -0
- package/dist/schemas.js +238 -0
- package/package.json +48 -0
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export declare const assistantResponseSchema: z.ZodObject<{
|
|
3
|
+
status: z.ZodOptional<z.ZodString>;
|
|
4
|
+
summary: z.ZodOptional<z.ZodString>;
|
|
5
|
+
changes: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
6
|
+
mentionedSlugs: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
7
|
+
previewVersion: z.ZodOptional<z.ZodNumber>;
|
|
8
|
+
validationErrors: z.ZodOptional<z.ZodUnion<readonly [z.ZodArray<z.ZodString>, z.ZodObject<{
|
|
9
|
+
fieldErrors: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodArray<z.ZodString>>>;
|
|
10
|
+
formErrors: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
11
|
+
}, z.core.$loose>]>>;
|
|
12
|
+
modelUsed: z.ZodOptional<z.ZodString>;
|
|
13
|
+
modelKey: z.ZodOptional<z.ZodString>;
|
|
14
|
+
plannerSource: z.ZodOptional<z.ZodEnum<{
|
|
15
|
+
openai: "openai";
|
|
16
|
+
anthropic: "anthropic";
|
|
17
|
+
gemini: "gemini";
|
|
18
|
+
demo: "demo";
|
|
19
|
+
}>>;
|
|
20
|
+
pendingPlanId: z.ZodOptional<z.ZodString>;
|
|
21
|
+
destructiveReasons: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
22
|
+
continuation: z.ZodOptional<z.ZodObject<{
|
|
23
|
+
chainId: z.ZodString;
|
|
24
|
+
currentStep: z.ZodNumber;
|
|
25
|
+
totalSteps: z.ZodNumber;
|
|
26
|
+
nextStepLabel: z.ZodString;
|
|
27
|
+
}, z.core.$loose>>;
|
|
28
|
+
focusBlockId: z.ZodOptional<z.ZodString>;
|
|
29
|
+
updatedSlug: z.ZodOptional<z.ZodString>;
|
|
30
|
+
undoSlug: z.ZodOptional<z.ZodString>;
|
|
31
|
+
suggestions: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
32
|
+
variations: z.ZodOptional<z.ZodObject<{
|
|
33
|
+
blockId: z.ZodString;
|
|
34
|
+
blockType: z.ZodString;
|
|
35
|
+
pageSlug: z.ZodString;
|
|
36
|
+
baseProps: z.ZodRecord<z.ZodString, z.ZodUnknown>;
|
|
37
|
+
options: z.ZodArray<z.ZodObject<{
|
|
38
|
+
id: z.ZodString;
|
|
39
|
+
title: z.ZodString;
|
|
40
|
+
summary: z.ZodString;
|
|
41
|
+
patch: z.ZodRecord<z.ZodString, z.ZodUnknown>;
|
|
42
|
+
changedKeys: z.ZodArray<z.ZodString>;
|
|
43
|
+
}, z.core.$loose>>;
|
|
44
|
+
}, z.core.$loose>>;
|
|
45
|
+
debug: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
46
|
+
error: z.ZodOptional<z.ZodString>;
|
|
47
|
+
}, z.core.$loose>;
|
|
48
|
+
export type AssistantResponseParsed = z.infer<typeof assistantResponseSchema>;
|
|
49
|
+
export declare const chatStartResponseSchema: z.ZodObject<{
|
|
50
|
+
streamId: z.ZodOptional<z.ZodString>;
|
|
51
|
+
error: z.ZodOptional<z.ZodString>;
|
|
52
|
+
}, z.core.$loose>;
|
|
53
|
+
export declare const slugsResponseSchema: z.ZodObject<{
|
|
54
|
+
slugs: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
55
|
+
}, z.core.$loose>;
|
|
56
|
+
export declare const bootstrapResponseSchema: z.ZodObject<{
|
|
57
|
+
slugs: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
58
|
+
}, z.core.$loose>;
|
|
59
|
+
export declare const cancelResponseSchema: z.ZodObject<{
|
|
60
|
+
status: z.ZodOptional<z.ZodString>;
|
|
61
|
+
error: z.ZodOptional<z.ZodString>;
|
|
62
|
+
}, z.core.$loose>;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
// ---------------------------------------------------------------------------
|
|
3
|
+
// Shared orchestrator API response schemas
|
|
4
|
+
// Used by the editor to validate fetch() responses at runtime.
|
|
5
|
+
// All schemas are lenient (passthrough extra fields) so they survive server
|
|
6
|
+
// additions without requiring client deployments.
|
|
7
|
+
// ---------------------------------------------------------------------------
|
|
8
|
+
const continuationSchema = z.object({
|
|
9
|
+
chainId: z.string(),
|
|
10
|
+
currentStep: z.number(),
|
|
11
|
+
totalSteps: z.number(),
|
|
12
|
+
nextStepLabel: z.string(),
|
|
13
|
+
}).passthrough();
|
|
14
|
+
const variationOptionSchema = z.object({
|
|
15
|
+
id: z.string(),
|
|
16
|
+
title: z.string(),
|
|
17
|
+
summary: z.string(),
|
|
18
|
+
patch: z.record(z.string(), z.unknown()),
|
|
19
|
+
changedKeys: z.array(z.string()),
|
|
20
|
+
}).passthrough();
|
|
21
|
+
const variationsSchema = z.object({
|
|
22
|
+
blockId: z.string(),
|
|
23
|
+
blockType: z.string(),
|
|
24
|
+
pageSlug: z.string(),
|
|
25
|
+
baseProps: z.record(z.string(), z.unknown()),
|
|
26
|
+
options: z.array(variationOptionSchema),
|
|
27
|
+
}).passthrough();
|
|
28
|
+
export const assistantResponseSchema = z.object({
|
|
29
|
+
status: z.string().optional(),
|
|
30
|
+
summary: z.string().optional(),
|
|
31
|
+
changes: z.array(z.string()).optional(),
|
|
32
|
+
mentionedSlugs: z.array(z.string()).optional(),
|
|
33
|
+
previewVersion: z.number().optional(),
|
|
34
|
+
validationErrors: z.union([
|
|
35
|
+
z.array(z.string()),
|
|
36
|
+
z.object({
|
|
37
|
+
fieldErrors: z.record(z.string(), z.array(z.string())).optional(),
|
|
38
|
+
formErrors: z.array(z.string()).optional(),
|
|
39
|
+
}).passthrough(),
|
|
40
|
+
]).optional(),
|
|
41
|
+
modelUsed: z.string().optional(),
|
|
42
|
+
modelKey: z.string().optional(),
|
|
43
|
+
plannerSource: z.enum(["openai", "anthropic", "gemini", "demo"]).optional(),
|
|
44
|
+
pendingPlanId: z.string().optional(),
|
|
45
|
+
destructiveReasons: z.array(z.string()).optional(),
|
|
46
|
+
continuation: continuationSchema.optional(),
|
|
47
|
+
focusBlockId: z.string().optional(),
|
|
48
|
+
updatedSlug: z.string().optional(),
|
|
49
|
+
undoSlug: z.string().optional(),
|
|
50
|
+
suggestions: z.array(z.string()).optional(),
|
|
51
|
+
variations: variationsSchema.optional(),
|
|
52
|
+
debug: z.record(z.string(), z.unknown()).optional(),
|
|
53
|
+
error: z.string().optional(),
|
|
54
|
+
}).passthrough();
|
|
55
|
+
export const chatStartResponseSchema = z.object({
|
|
56
|
+
streamId: z.string().optional(),
|
|
57
|
+
error: z.string().optional(),
|
|
58
|
+
}).passthrough();
|
|
59
|
+
export const slugsResponseSchema = z.object({
|
|
60
|
+
slugs: z.array(z.string()).optional(),
|
|
61
|
+
}).passthrough();
|
|
62
|
+
export const bootstrapResponseSchema = z.object({
|
|
63
|
+
slugs: z.array(z.string()).optional(),
|
|
64
|
+
}).passthrough();
|
|
65
|
+
export const cancelResponseSchema = z.object({
|
|
66
|
+
status: z.string().optional(),
|
|
67
|
+
error: z.string().optional(),
|
|
68
|
+
}).passthrough();
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import type { FieldMeta, ListFieldMeta } from "./blocks/_registry.ts";
|
|
3
|
+
export declare const jsonSchemaLikeSchema: z.ZodType<Record<string, unknown>>;
|
|
4
|
+
export declare const blockDefinitionSchema: z.ZodObject<{
|
|
5
|
+
type: z.ZodString;
|
|
6
|
+
displayName: z.ZodOptional<z.ZodString>;
|
|
7
|
+
editablePaths: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
8
|
+
propsSchema: z.ZodType<Record<string, unknown>, unknown, z.core.$ZodTypeInternals<Record<string, unknown>, unknown>>;
|
|
9
|
+
defaultProps: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
10
|
+
}, z.core.$strip>;
|
|
11
|
+
export declare const blockManifestSchema: z.ZodObject<{
|
|
12
|
+
version: z.ZodNumber;
|
|
13
|
+
blocks: z.ZodArray<z.ZodObject<{
|
|
14
|
+
type: z.ZodString;
|
|
15
|
+
displayName: z.ZodOptional<z.ZodString>;
|
|
16
|
+
editablePaths: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
17
|
+
propsSchema: z.ZodType<Record<string, unknown>, unknown, z.core.$ZodTypeInternals<Record<string, unknown>, unknown>>;
|
|
18
|
+
defaultProps: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
19
|
+
}, z.core.$strip>>;
|
|
20
|
+
}, z.core.$strip>;
|
|
21
|
+
export type BlockDefinition = z.infer<typeof blockDefinitionSchema>;
|
|
22
|
+
export type BlockManifest = z.infer<typeof blockManifestSchema>;
|
|
23
|
+
export declare function validateByJsonSchemaLike(schema: Record<string, unknown>, value: unknown): boolean;
|
|
24
|
+
export declare function deriveFieldMetaFromSchema(propsSchema: Record<string, unknown>): {
|
|
25
|
+
fields: Record<string, FieldMeta>;
|
|
26
|
+
listFields: Record<string, ListFieldMeta>;
|
|
27
|
+
};
|
|
28
|
+
export declare function validateManifestDefaultProps(blocks: BlockDefinition[]): string | null;
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export const jsonSchemaLikeSchema = z.lazy(() => z.object({
|
|
3
|
+
type: z.string().optional(),
|
|
4
|
+
properties: z.record(z.string(), jsonSchemaLikeSchema).optional(),
|
|
5
|
+
required: z.array(z.string()).optional(),
|
|
6
|
+
items: z.union([jsonSchemaLikeSchema, z.array(jsonSchemaLikeSchema)]).optional(),
|
|
7
|
+
enum: z.array(z.unknown()).optional(),
|
|
8
|
+
anyOf: z.array(jsonSchemaLikeSchema).optional(),
|
|
9
|
+
oneOf: z.array(jsonSchemaLikeSchema).optional(),
|
|
10
|
+
allOf: z.array(jsonSchemaLikeSchema).optional(),
|
|
11
|
+
additionalProperties: z.union([z.boolean(), jsonSchemaLikeSchema]).optional(),
|
|
12
|
+
description: z.string().optional()
|
|
13
|
+
}).catchall(z.unknown()));
|
|
14
|
+
export const blockDefinitionSchema = z.object({
|
|
15
|
+
type: z.string().min(1),
|
|
16
|
+
displayName: z.string().min(1).optional(),
|
|
17
|
+
editablePaths: z.array(z.string().min(1)).optional(),
|
|
18
|
+
propsSchema: jsonSchemaLikeSchema,
|
|
19
|
+
defaultProps: z.record(z.string(), z.unknown()).optional()
|
|
20
|
+
});
|
|
21
|
+
export const blockManifestSchema = z.object({
|
|
22
|
+
version: z.number().int().positive(),
|
|
23
|
+
blocks: z.array(blockDefinitionSchema)
|
|
24
|
+
});
|
|
25
|
+
function isObject(value) {
|
|
26
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
27
|
+
}
|
|
28
|
+
export function validateByJsonSchemaLike(schema, value) {
|
|
29
|
+
const type = typeof schema.type === "string" ? schema.type : undefined;
|
|
30
|
+
if (type === "object") {
|
|
31
|
+
if (!isObject(value))
|
|
32
|
+
return false;
|
|
33
|
+
const props = isObject(schema.properties) ? schema.properties : {};
|
|
34
|
+
const required = Array.isArray(schema.required) ? schema.required.filter((item) => typeof item === "string") : [];
|
|
35
|
+
for (const key of required) {
|
|
36
|
+
if (!(key in value))
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
for (const [key, propSchema] of Object.entries(props)) {
|
|
40
|
+
if (!(key in value))
|
|
41
|
+
continue;
|
|
42
|
+
if (!isObject(propSchema))
|
|
43
|
+
continue;
|
|
44
|
+
if (!validateByJsonSchemaLike(propSchema, value[key]))
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
if (type === "array") {
|
|
50
|
+
if (!Array.isArray(value))
|
|
51
|
+
return false;
|
|
52
|
+
const items = schema.items;
|
|
53
|
+
if (isObject(items))
|
|
54
|
+
return value.every((item) => validateByJsonSchemaLike(items, item));
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
if (type === "string")
|
|
58
|
+
return typeof value === "string";
|
|
59
|
+
if (type === "number" || type === "integer")
|
|
60
|
+
return typeof value === "number" && Number.isFinite(value);
|
|
61
|
+
if (type === "boolean")
|
|
62
|
+
return typeof value === "boolean";
|
|
63
|
+
return true;
|
|
64
|
+
}
|
|
65
|
+
// ---------------------------------------------------------------------------
|
|
66
|
+
// Derive FieldMeta from JSON Schema — fallback for custom blocks not in the
|
|
67
|
+
// shared registry. Heuristics map schema types and prop key patterns to the
|
|
68
|
+
// FieldKind enum so the PropertyPanel can render appropriate controls.
|
|
69
|
+
// ---------------------------------------------------------------------------
|
|
70
|
+
const IMAGE_KEY_RE = /(?:image|img)(?:Url|Src)?$|^(?:src|imageUrl|logoUrl|heroImage)$/i;
|
|
71
|
+
const IMAGE_ALT_KEY_RE = /(?:Alt)$/;
|
|
72
|
+
function labelFromKey(key) {
|
|
73
|
+
// ctaText → "CTA text", imageUrl → "Image", logoAlt → "Logo alt",
|
|
74
|
+
// background_image → "Background image", video_url → "Video", src → "Src"
|
|
75
|
+
const spaced = key
|
|
76
|
+
.replace(/[_-]+/g, " ")
|
|
77
|
+
.replace(/([a-z])([A-Z])/g, "$1 $2")
|
|
78
|
+
.replace(/([A-Z]+)([A-Z][a-z])/g, "$1 $2");
|
|
79
|
+
const label = spaced.charAt(0).toUpperCase() + spaced.slice(1);
|
|
80
|
+
// Strip trailing "Url"/"Src" for image fields, but keep the original label if
|
|
81
|
+
// stripping would leave nothing (e.g. a field literally named "src").
|
|
82
|
+
const stripped = label.replace(/\s*(?:Url|Src)$/i, "").trim();
|
|
83
|
+
return stripped || label;
|
|
84
|
+
}
|
|
85
|
+
function inferFieldKind(key, schema) {
|
|
86
|
+
const type = typeof schema.type === "string" ? schema.type : undefined;
|
|
87
|
+
if (isProseMirrorDocSchema(schema))
|
|
88
|
+
return "richtext";
|
|
89
|
+
if (type === "boolean")
|
|
90
|
+
return "boolean";
|
|
91
|
+
if (type !== "string" && type !== "number" && type !== "integer")
|
|
92
|
+
return "text";
|
|
93
|
+
if (type === "number" || type === "integer")
|
|
94
|
+
return "number";
|
|
95
|
+
if (Array.isArray(schema.enum) && schema.enum.length > 0)
|
|
96
|
+
return "enum";
|
|
97
|
+
if (IMAGE_ALT_KEY_RE.test(key))
|
|
98
|
+
return "imageAlt";
|
|
99
|
+
if (IMAGE_KEY_RE.test(key))
|
|
100
|
+
return "image";
|
|
101
|
+
return "text";
|
|
102
|
+
}
|
|
103
|
+
function deriveItemFields(itemSchema) {
|
|
104
|
+
const fields = {};
|
|
105
|
+
const props = isObject(itemSchema.properties) ? itemSchema.properties : {};
|
|
106
|
+
for (const [key, propSchema] of Object.entries(props)) {
|
|
107
|
+
if (!isObject(propSchema))
|
|
108
|
+
continue;
|
|
109
|
+
// Skip opaque object/array props (they'd render as "[object Object]"), but
|
|
110
|
+
// keep richtext docs — those self-identify and get the richtext editor.
|
|
111
|
+
const t = typeof propSchema.type === "string" ? propSchema.type : undefined;
|
|
112
|
+
if ((t === "object" || t === "array") && !isProseMirrorDocSchema(propSchema))
|
|
113
|
+
continue;
|
|
114
|
+
const kind = inferFieldKind(key, propSchema);
|
|
115
|
+
const fm = { kind, label: labelFromKey(key) };
|
|
116
|
+
if (kind === "enum" && Array.isArray(propSchema.enum)) {
|
|
117
|
+
fm.options = propSchema.enum.filter((v) => typeof v === "string");
|
|
118
|
+
}
|
|
119
|
+
fields[key] = fm;
|
|
120
|
+
}
|
|
121
|
+
return fields;
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* A ProseMirror/Tiptap richtext document self-identifies: it is an object whose
|
|
125
|
+
* `type` is pinned to the literal `"doc"`. We key off that signature (rather
|
|
126
|
+
* than a CMS-specific annotation) so any block declaring a `{type:"doc",content}`
|
|
127
|
+
* field gets the richtext editor — and the underlying document is edited as-is,
|
|
128
|
+
* not flattened to a string.
|
|
129
|
+
*/
|
|
130
|
+
function isProseMirrorDocSchema(schema) {
|
|
131
|
+
if (!isObject(schema) || schema.type !== "object" || !isObject(schema.properties))
|
|
132
|
+
return false;
|
|
133
|
+
return constValue(schema.properties.type) === "doc";
|
|
134
|
+
}
|
|
135
|
+
/** Read a string-literal discriminant value (`const`, or a single-value `enum`) from a prop schema. */
|
|
136
|
+
function constValue(schema) {
|
|
137
|
+
if (!isObject(schema))
|
|
138
|
+
return undefined;
|
|
139
|
+
if (typeof schema.const === "string")
|
|
140
|
+
return schema.const;
|
|
141
|
+
if (Array.isArray(schema.enum) && schema.enum.length === 1 && typeof schema.enum[0] === "string")
|
|
142
|
+
return schema.enum[0];
|
|
143
|
+
return undefined;
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Detect an array whose items are a discriminated union (oneOf/anyOf of object
|
|
147
|
+
* branches, each pinning one property to a string literal). Produces a
|
|
148
|
+
* polymorphic ListFieldMeta: a per-branch field set keyed by the discriminant
|
|
149
|
+
* value, plus a merged `itemFields` fallback. Returns undefined if the items
|
|
150
|
+
* are not such a union (caller falls back to the uniform-object path).
|
|
151
|
+
*/
|
|
152
|
+
function derivePolymorphicListField(key, itemSchema) {
|
|
153
|
+
const branchesRaw = Array.isArray(itemSchema.oneOf)
|
|
154
|
+
? itemSchema.oneOf
|
|
155
|
+
: Array.isArray(itemSchema.anyOf)
|
|
156
|
+
? itemSchema.anyOf
|
|
157
|
+
: undefined;
|
|
158
|
+
if (!branchesRaw)
|
|
159
|
+
return undefined;
|
|
160
|
+
const branches = branchesRaw.filter((b) => isObject(b) && isObject(b.properties));
|
|
161
|
+
if (branches.length < 2)
|
|
162
|
+
return undefined;
|
|
163
|
+
// The discriminator is a property that carries a distinct string literal on
|
|
164
|
+
// every branch. Prefer "component"; otherwise pick the first key that qualifies.
|
|
165
|
+
const candidateKeys = Object.keys(branches[0].properties ?? {});
|
|
166
|
+
const orderedKeys = candidateKeys.includes("component")
|
|
167
|
+
? ["component", ...candidateKeys.filter((k) => k !== "component")]
|
|
168
|
+
: candidateKeys;
|
|
169
|
+
let discriminator;
|
|
170
|
+
for (const cand of orderedKeys) {
|
|
171
|
+
if (branches.every((b) => constValue(b.properties[cand]) !== undefined)) {
|
|
172
|
+
discriminator = cand;
|
|
173
|
+
break;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
if (!discriminator)
|
|
177
|
+
return undefined;
|
|
178
|
+
const itemFieldsByType = {};
|
|
179
|
+
const merged = {};
|
|
180
|
+
for (const branch of branches) {
|
|
181
|
+
const disc = constValue(branch.properties[discriminator]);
|
|
182
|
+
if (disc === undefined)
|
|
183
|
+
continue;
|
|
184
|
+
const fields = deriveItemFields(branch);
|
|
185
|
+
delete fields[discriminator]; // the discriminant itself is not an editable field
|
|
186
|
+
itemFieldsByType[disc] = fields;
|
|
187
|
+
for (const [fk, fm] of Object.entries(fields))
|
|
188
|
+
if (!(fk in merged))
|
|
189
|
+
merged[fk] = fm;
|
|
190
|
+
}
|
|
191
|
+
if (Object.keys(itemFieldsByType).length === 0)
|
|
192
|
+
return undefined;
|
|
193
|
+
return { label: labelFromKey(key), itemFields: merged, discriminator, itemFieldsByType };
|
|
194
|
+
}
|
|
195
|
+
export function deriveFieldMetaFromSchema(propsSchema) {
|
|
196
|
+
const fields = {};
|
|
197
|
+
const listFields = {};
|
|
198
|
+
const props = isObject(propsSchema.properties) ? propsSchema.properties : {};
|
|
199
|
+
for (const [key, propSchema] of Object.entries(props)) {
|
|
200
|
+
if (!isObject(propSchema))
|
|
201
|
+
continue;
|
|
202
|
+
const type = typeof propSchema.type === "string" ? propSchema.type : undefined;
|
|
203
|
+
// Array of a discriminated union (oneOf/anyOf of object branches) →
|
|
204
|
+
// polymorphic list field: per-branch fields keyed by the discriminant value.
|
|
205
|
+
if (type === "array" && isObject(propSchema.items)) {
|
|
206
|
+
const polymorphic = derivePolymorphicListField(key, propSchema.items);
|
|
207
|
+
if (polymorphic) {
|
|
208
|
+
listFields[key] = polymorphic;
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
// Array with object items → listFields
|
|
213
|
+
if (type === "array" && isObject(propSchema.items) && propSchema.items.type === "object") {
|
|
214
|
+
const itemFields = deriveItemFields(propSchema.items);
|
|
215
|
+
if (Object.keys(itemFields).length > 0) {
|
|
216
|
+
listFields[key] = { label: labelFromKey(key), itemFields };
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
// Structural props (an object, or an array whose items we couldn't turn
|
|
221
|
+
// into a list field above) have no meaningful single-control affordance —
|
|
222
|
+
// a text input over them renders "[object Object]" and corrupts the value
|
|
223
|
+
// on edit. Skip them rather than emit a broken field; the ops-engine
|
|
224
|
+
// passthrough preserves the underlying data untouched. A richtext document
|
|
225
|
+
// is the exception — it self-identifies and gets the richtext editor.
|
|
226
|
+
if ((type === "object" || type === "array") && !isProseMirrorDocSchema(propSchema))
|
|
227
|
+
continue;
|
|
228
|
+
// Scalar field (or richtext document)
|
|
229
|
+
const kind = inferFieldKind(key, propSchema);
|
|
230
|
+
const fm = { kind, label: labelFromKey(key) };
|
|
231
|
+
if (kind === "enum" && Array.isArray(propSchema.enum)) {
|
|
232
|
+
fm.options = propSchema.enum.filter((v) => typeof v === "string");
|
|
233
|
+
}
|
|
234
|
+
fields[key] = fm;
|
|
235
|
+
}
|
|
236
|
+
return { fields, listFields };
|
|
237
|
+
}
|
|
238
|
+
export function validateManifestDefaultProps(blocks) {
|
|
239
|
+
for (const block of blocks) {
|
|
240
|
+
if (!block.defaultProps)
|
|
241
|
+
continue;
|
|
242
|
+
if (!validateByJsonSchemaLike(block.propsSchema, block.defaultProps)) {
|
|
243
|
+
return `defaultProps do not match propsSchema for block "${block.type}"`;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
return null;
|
|
247
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Block type name mapping utilities.
|
|
3
|
+
*
|
|
4
|
+
* The block registry uses PascalCase (e.g. "FAQAccordion", "CTA", "CardGrid").
|
|
5
|
+
* Different CMSs use different conventions:
|
|
6
|
+
* - Sanity: camelCase ("faqAccordion", "cta", "cardGrid")
|
|
7
|
+
* - Strapi/Contentful: lowercase ("faqaccordion", "cta", "cardgrid")
|
|
8
|
+
*
|
|
9
|
+
* These helpers provide canonical lookups so each CMS integration
|
|
10
|
+
* doesn't need to maintain its own reverse map.
|
|
11
|
+
*/
|
|
12
|
+
/** Convert PascalCase block type to camelCase: CTA → cta, FAQAccordion → faqAccordion */
|
|
13
|
+
export declare function blockTypeToCamel(pascalCase: string): string;
|
|
14
|
+
/** Reverse lookup: camelCase → PascalCase (faqAccordion → FAQAccordion). Sanity convention. */
|
|
15
|
+
export declare function camelToBlockType(camelName: string): string;
|
|
16
|
+
/** Reverse lookup: lowercase → PascalCase (faqaccordion → FAQAccordion). Strapi/Contentful convention. */
|
|
17
|
+
export declare function lowerToBlockType(lowerName: string): string;
|
|
18
|
+
/** Convert PascalCase to lowercase: FAQAccordion → faqaccordion */
|
|
19
|
+
export declare function blockTypeToLower(pascalCase: string): string;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { getAllBlockMeta } from "./blocks/_registry.js";
|
|
2
|
+
/**
|
|
3
|
+
* Block type name mapping utilities.
|
|
4
|
+
*
|
|
5
|
+
* The block registry uses PascalCase (e.g. "FAQAccordion", "CTA", "CardGrid").
|
|
6
|
+
* Different CMSs use different conventions:
|
|
7
|
+
* - Sanity: camelCase ("faqAccordion", "cta", "cardGrid")
|
|
8
|
+
* - Strapi/Contentful: lowercase ("faqaccordion", "cta", "cardgrid")
|
|
9
|
+
*
|
|
10
|
+
* These helpers provide canonical lookups so each CMS integration
|
|
11
|
+
* doesn't need to maintain its own reverse map.
|
|
12
|
+
*/
|
|
13
|
+
// ── PascalCase → camelCase (Sanity convention) ─────────────────────────
|
|
14
|
+
/** Convert PascalCase block type to camelCase: CTA → cta, FAQAccordion → faqAccordion */
|
|
15
|
+
export function blockTypeToCamel(pascalCase) {
|
|
16
|
+
// All-uppercase acronyms: CTA → cta
|
|
17
|
+
if (pascalCase === pascalCase.toUpperCase())
|
|
18
|
+
return pascalCase.toLowerCase();
|
|
19
|
+
// Leading acronym: FAQAccordion → faqAccordion
|
|
20
|
+
const match = pascalCase.match(/^([A-Z]+)([A-Z][a-z].*)$/);
|
|
21
|
+
if (match)
|
|
22
|
+
return match[1].toLowerCase() + match[2];
|
|
23
|
+
// Normal PascalCase: CardGrid → cardGrid
|
|
24
|
+
return pascalCase.charAt(0).toLowerCase() + pascalCase.slice(1);
|
|
25
|
+
}
|
|
26
|
+
// ── Reverse lookup maps (built lazily from registry) ────────────────────
|
|
27
|
+
let _camelMap = null;
|
|
28
|
+
let _lowerMap = null;
|
|
29
|
+
function ensureMaps() {
|
|
30
|
+
if (_camelMap)
|
|
31
|
+
return;
|
|
32
|
+
_camelMap = new Map();
|
|
33
|
+
_lowerMap = new Map();
|
|
34
|
+
for (const blockType of Object.keys(getAllBlockMeta())) {
|
|
35
|
+
_camelMap.set(blockTypeToCamel(blockType), blockType);
|
|
36
|
+
_lowerMap.set(blockType.toLowerCase(), blockType);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
/** Reverse lookup: camelCase → PascalCase (faqAccordion → FAQAccordion). Sanity convention. */
|
|
40
|
+
export function camelToBlockType(camelName) {
|
|
41
|
+
ensureMaps();
|
|
42
|
+
return _camelMap.get(camelName) ?? camelName.charAt(0).toUpperCase() + camelName.slice(1);
|
|
43
|
+
}
|
|
44
|
+
/** Reverse lookup: lowercase → PascalCase (faqaccordion → FAQAccordion). Strapi/Contentful convention. */
|
|
45
|
+
export function lowerToBlockType(lowerName) {
|
|
46
|
+
ensureMaps();
|
|
47
|
+
return _lowerMap.get(lowerName) ?? lowerName.charAt(0).toUpperCase() + lowerName.slice(1);
|
|
48
|
+
}
|
|
49
|
+
/** Convert PascalCase to lowercase: FAQAccordion → faqaccordion */
|
|
50
|
+
export function blockTypeToLower(pascalCase) {
|
|
51
|
+
return pascalCase.toLowerCase();
|
|
52
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { FieldMeta, ImageSpec } from "./_registry.ts";
|
|
2
|
+
export declare const f: {
|
|
3
|
+
readonly text: (label?: string) => FieldMeta;
|
|
4
|
+
readonly longtext: (label?: string) => FieldMeta;
|
|
5
|
+
readonly richtext: (label?: string) => FieldMeta;
|
|
6
|
+
readonly url: (label?: string) => FieldMeta;
|
|
7
|
+
readonly image: (label?: string, imageSpec?: ImageSpec) => FieldMeta;
|
|
8
|
+
readonly imageAlt: (label?: string) => FieldMeta;
|
|
9
|
+
readonly headingLevel: () => FieldMeta;
|
|
10
|
+
};
|
|
11
|
+
export declare const DEFAULT_HEADING_LEVELS: Record<string, string>;
|
|
12
|
+
export declare function resolveHeadingTag(blockType: string, props: Record<string, unknown>): string;
|
|
13
|
+
/** Resolve the heading tag one level below the section heading (for child items). */
|
|
14
|
+
export declare function resolveItemHeadingTag(blockType: string, props: Record<string, unknown>): string;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export const f = {
|
|
2
|
+
text: (label) => ({ kind: "text", label }),
|
|
3
|
+
longtext: (label) => ({ kind: "text", label, multiline: true }),
|
|
4
|
+
richtext: (label) => ({ kind: "richtext", label }),
|
|
5
|
+
url: (label) => ({ kind: "url", label, inlineEditable: false }),
|
|
6
|
+
image: (label, imageSpec) => ({ kind: "image", label, inlineEditable: false, ...(imageSpec ? { imageSpec } : {}) }),
|
|
7
|
+
imageAlt: (label) => ({ kind: "imageAlt", label }),
|
|
8
|
+
headingLevel: () => ({ kind: "headingLevel", label: "Heading type", inlineEditable: false }),
|
|
9
|
+
};
|
|
10
|
+
export const DEFAULT_HEADING_LEVELS = {
|
|
11
|
+
Hero: "h1", FeatureGrid: "h2", Testimonials: "h2", FAQAccordion: "h2",
|
|
12
|
+
CTA: "h2", Card: "h3", CardGrid: "h2", RichText: "h2",
|
|
13
|
+
Stats: "h2", TwoColumn: "h2", Footer: "h4", Gallery: "h2",
|
|
14
|
+
Carousel: "h2", Tabs: "h2",
|
|
15
|
+
};
|
|
16
|
+
export function resolveHeadingTag(blockType, props) {
|
|
17
|
+
const v = props.headingLevel;
|
|
18
|
+
if (typeof v === "string" && /^h[1-6]$/.test(v))
|
|
19
|
+
return v;
|
|
20
|
+
return DEFAULT_HEADING_LEVELS[blockType] ?? "h2";
|
|
21
|
+
}
|
|
22
|
+
/** Resolve the heading tag one level below the section heading (for child items). */
|
|
23
|
+
export function resolveItemHeadingTag(blockType, props) {
|
|
24
|
+
const level = parseInt(resolveHeadingTag(blockType, props)[1]);
|
|
25
|
+
return `h${Math.min(6, level + 1)}`;
|
|
26
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export declare const IMAGE_PLACEHOLDER = "/hero-generated.svg";
|
|
3
|
+
export declare function isImagePlaceholder(url: string | undefined | null): boolean;
|
|
4
|
+
/** Semantic kind for a block prop field. */
|
|
5
|
+
export type FieldKind = "text" | "richtext" | "url" | "image" | "imageAlt" | "enum" | "color" | "number" | "boolean" | "headingLevel";
|
|
6
|
+
/** Recommended image dimensions for an image field. */
|
|
7
|
+
export type ImageSpec = {
|
|
8
|
+
aspectRatio: "landscape" | "square" | "portrait";
|
|
9
|
+
width: number;
|
|
10
|
+
height: number;
|
|
11
|
+
format?: "png" | "webp" | "jpeg";
|
|
12
|
+
};
|
|
13
|
+
/** Metadata for a single prop field on a block. */
|
|
14
|
+
export type FieldMeta = {
|
|
15
|
+
kind: FieldKind;
|
|
16
|
+
/** Human-readable label (e.g. "Heading text"). Falls back to prop key. */
|
|
17
|
+
label?: string;
|
|
18
|
+
/** Whether this field supports inline editing in the preview. Defaults based on kind. */
|
|
19
|
+
inlineEditable?: boolean;
|
|
20
|
+
/** For enum kind: the allowed values. */
|
|
21
|
+
options?: string[];
|
|
22
|
+
/** For image kind: recommended dimensions and aspect ratio. */
|
|
23
|
+
imageSpec?: ImageSpec;
|
|
24
|
+
/** Render as a textarea in the PropertyPanel instead of a single-line input. */
|
|
25
|
+
multiline?: boolean;
|
|
26
|
+
/** Whether the field is required. Auto-derived from Zod schema at registration. */
|
|
27
|
+
required?: boolean;
|
|
28
|
+
};
|
|
29
|
+
/** Metadata for list-type props (features, items, cards). */
|
|
30
|
+
export type ListFieldMeta = {
|
|
31
|
+
/** Display label for the list (e.g. "Features"). */
|
|
32
|
+
label?: string;
|
|
33
|
+
/** Field metadata for each item in the list. For polymorphic lists this is the
|
|
34
|
+
* union of all branch fields, used as a fallback when an item's discriminant
|
|
35
|
+
* value has no matching entry in `itemFieldsByType`. */
|
|
36
|
+
itemFields: Record<string, FieldMeta>;
|
|
37
|
+
/** For polymorphic (discriminated-union) lists: the item property whose value
|
|
38
|
+
* selects which field set to render (e.g. "component"). */
|
|
39
|
+
discriminator?: string;
|
|
40
|
+
/** For polymorphic lists: field metadata per discriminant value, so each item
|
|
41
|
+
* renders only the fields for its own type. */
|
|
42
|
+
itemFieldsByType?: Record<string, Record<string, FieldMeta>>;
|
|
43
|
+
};
|
|
44
|
+
/** Full metadata for a registered block type. */
|
|
45
|
+
export type BlockMeta = {
|
|
46
|
+
displayName: string;
|
|
47
|
+
description?: string;
|
|
48
|
+
category?: "content" | "media" | "navigation" | "conversion" | "layout";
|
|
49
|
+
/** Chrome blocks (header/footer) are structurally pinned — not addable, movable, or removable. Editable via props only. */
|
|
50
|
+
chrome?: boolean;
|
|
51
|
+
/** Metadata for scalar (non-list) props. */
|
|
52
|
+
fields: Record<string, FieldMeta>;
|
|
53
|
+
/** Metadata for array/list props. */
|
|
54
|
+
listFields?: Record<string, ListFieldMeta>;
|
|
55
|
+
};
|
|
56
|
+
export type BlockType = string & {};
|
|
57
|
+
export type BlockInstance = {
|
|
58
|
+
id: string;
|
|
59
|
+
type: BlockType;
|
|
60
|
+
props: Record<string, unknown>;
|
|
61
|
+
};
|
|
62
|
+
export type BlockRegistration = {
|
|
63
|
+
schema: z.ZodObject<any>;
|
|
64
|
+
meta: BlockMeta;
|
|
65
|
+
};
|
|
66
|
+
/**
|
|
67
|
+
* Register a block type. Can be called at module load time.
|
|
68
|
+
* Re-registering the same type overwrites the previous registration.
|
|
69
|
+
*/
|
|
70
|
+
export declare function registerBlock(type: string, config: BlockRegistration): void;
|
|
71
|
+
/** Get metadata for a registered block type, or undefined. */
|
|
72
|
+
export declare function getBlockMeta(type: string): BlockMeta | undefined;
|
|
73
|
+
/** Get all registered block metadata. */
|
|
74
|
+
export declare function getAllBlockMeta(): Readonly<Record<string, BlockMeta>>;
|
|
75
|
+
/** Get the set of prop keys that are image fields for a block type. */
|
|
76
|
+
export declare function getImageFields(blockType: string): Set<string>;
|
|
77
|
+
/** Get list props that contain image fields: Map<listKey, Set<imageFieldKey>>. */
|
|
78
|
+
export declare function getListImageFields(blockType: string): Map<string, Set<string>>;
|
|
79
|
+
/** Check if a block type is a chrome block (structurally pinned). */
|
|
80
|
+
export declare function isChrome(type: string): boolean;
|
|
81
|
+
/** Get all registered chrome block type names. */
|
|
82
|
+
export declare function getChromeTypes(): string[];
|
|
83
|
+
/** Check if a field is inline-editable based on its metadata kind. */
|
|
84
|
+
export declare function isFieldInlineEditable(type: string, fieldPath: string): boolean;
|
|
85
|
+
/** Resolve the ImageSpec for a block field, handling both scalar and list item paths. */
|
|
86
|
+
export declare function getImageSpec(blockType: string, fieldPath: string): ImageSpec | undefined;
|
|
87
|
+
/**
|
|
88
|
+
* Block schemas keyed by type name.
|
|
89
|
+
* Prefer `registerBlock()` for new blocks; this object is kept for backwards compat.
|
|
90
|
+
*/
|
|
91
|
+
export declare const blockSchemas: Record<string, z.ZodObject<any, z.core.$strip>>;
|
|
92
|
+
export declare const allowedBlockTypes: string[];
|
|
93
|
+
export declare function getPropDisplayName(blockType: string | undefined, propKey: string): string;
|
|
94
|
+
export declare function defaultListItemForBlock(type: BlockType, listKey: string): Record<string, unknown> | null;
|
|
95
|
+
/** Base schema — accepts any block type. Used for ingesting external site content with custom blocks. */
|
|
96
|
+
export declare const blockInstanceSchemaLenient: z.ZodObject<{
|
|
97
|
+
id: z.ZodString;
|
|
98
|
+
type: z.ZodString;
|
|
99
|
+
props: z.ZodRecord<z.ZodString, z.ZodUnknown>;
|
|
100
|
+
}, z.core.$strip>;
|
|
101
|
+
/** Strict variant — rejects block types not in the shared registry. */
|
|
102
|
+
export declare const blockInstanceSchema: z.ZodObject<{
|
|
103
|
+
id: z.ZodString;
|
|
104
|
+
props: z.ZodRecord<z.ZodString, z.ZodUnknown>;
|
|
105
|
+
type: z.ZodString;
|
|
106
|
+
}, z.core.$strip>;
|
|
107
|
+
export declare function validateBlockProps(type: string, props: unknown): z.ZodSafeParseSuccess<Record<string, unknown>> | {
|
|
108
|
+
success: false;
|
|
109
|
+
error: z.ZodError<unknown>;
|
|
110
|
+
};
|
|
111
|
+
/**
|
|
112
|
+
* Get a structural JSON schema for a registered block type.
|
|
113
|
+
* Strips validation constraints (minLength, etc.) — the editor only needs
|
|
114
|
+
* the shape to know which fields exist and their types.
|
|
115
|
+
*/
|
|
116
|
+
export declare function getBlockJsonSchema(type: string): Record<string, unknown> | undefined;
|