@avocadostudio-ai/shared 0.1.0 → 0.2.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/contract/operation.schema.json +3 -0
- package/dist/block-manifest.d.ts +443 -0
- package/dist/block-manifest.js +306 -22
- package/dist/blocks/_registry.d.ts +58 -0
- package/dist/blocks/_registry.js +66 -0
- package/dist/blocks/footer.js +20 -1
- package/dist/blocks/index.d.ts +16 -0
- package/dist/blocks/index.js +18 -0
- package/dist/blocks/quote.js +1 -1
- package/dist/chat-events.d.ts +3 -3
- package/dist/index.d.ts +4 -3
- package/dist/index.js +13 -2
- package/dist/ops/builders.d.ts +8 -3
- package/dist/ops/builders.js +11 -6
- package/dist/schemas.d.ts +5 -2
- package/dist/schemas.js +46 -2
- package/package.json +6 -4
package/dist/block-manifest.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
+
import { catalogueBlockTypes, getBlockJsonSchema, getBlockMeta } from "./blocks/_registry.js";
|
|
3
|
+
import { declaredDefaultPropsForType } from "./blocks/index.js";
|
|
2
4
|
export const jsonSchemaLikeSchema = z.lazy(() => z.object({
|
|
3
5
|
type: z.string().optional(),
|
|
4
6
|
properties: z.record(z.string(), jsonSchemaLikeSchema).optional(),
|
|
@@ -11,12 +13,68 @@ export const jsonSchemaLikeSchema = z.lazy(() => z.object({
|
|
|
11
13
|
additionalProperties: z.union([z.boolean(), jsonSchemaLikeSchema]).optional(),
|
|
12
14
|
description: z.string().optional()
|
|
13
15
|
}).catchall(z.unknown()));
|
|
16
|
+
/**
|
|
17
|
+
* Field metadata a site may declare alongside the JSON schema.
|
|
18
|
+
*
|
|
19
|
+
* JSON Schema describes what a value *is*; this describes how a person edits
|
|
20
|
+
* it, and the two are not the same question. A one-word headline and a six
|
|
21
|
+
* sentence body are both `{"type":"string"}`, so a manifest carrying only the
|
|
22
|
+
* schema gives them the same one-line input — which is exactly what happened to
|
|
23
|
+
* every integration with a real prose field. `kind` and `multiline` have no
|
|
24
|
+
* JSON Schema spelling, so they have to travel beside it.
|
|
25
|
+
*/
|
|
26
|
+
export const fieldMetaSchema = z.object({
|
|
27
|
+
kind: z.enum(["text", "richtext", "url", "image", "imageAlt", "enum", "color", "number", "boolean", "headingLevel"]),
|
|
28
|
+
label: z.string().optional(),
|
|
29
|
+
inlineEditable: z.boolean().optional(),
|
|
30
|
+
options: z.array(z.string()).optional(),
|
|
31
|
+
imageSpec: z
|
|
32
|
+
.object({
|
|
33
|
+
aspectRatio: z.enum(["landscape", "square", "portrait"]),
|
|
34
|
+
width: z.number(),
|
|
35
|
+
height: z.number(),
|
|
36
|
+
format: z.enum(["png", "webp", "jpeg"]).optional()
|
|
37
|
+
})
|
|
38
|
+
.optional(),
|
|
39
|
+
multiline: z.boolean().optional(),
|
|
40
|
+
inline: z.boolean().optional(),
|
|
41
|
+
decorators: z
|
|
42
|
+
.array(z.object({ name: z.string().min(1), title: z.string().optional() }))
|
|
43
|
+
.optional(),
|
|
44
|
+
required: z.boolean().optional()
|
|
45
|
+
});
|
|
46
|
+
export const listFieldMetaSchema = z.object({
|
|
47
|
+
label: z.string().optional(),
|
|
48
|
+
itemFields: z.record(z.string(), fieldMetaSchema).optional(),
|
|
49
|
+
discriminator: z.string().optional(),
|
|
50
|
+
itemFieldsByType: z.record(z.string(), z.record(z.string(), fieldMetaSchema)).optional()
|
|
51
|
+
});
|
|
14
52
|
export const blockDefinitionSchema = z.object({
|
|
15
53
|
type: z.string().min(1),
|
|
16
54
|
displayName: z.string().min(1).optional(),
|
|
17
55
|
editablePaths: z.array(z.string().min(1)).optional(),
|
|
18
56
|
propsSchema: jsonSchemaLikeSchema,
|
|
19
|
-
defaultProps: z.record(z.string(), z.unknown()).optional()
|
|
57
|
+
defaultProps: z.record(z.string(), z.unknown()).optional(),
|
|
58
|
+
/**
|
|
59
|
+
* Optional editing metadata, merged over what the schema implies.
|
|
60
|
+
*
|
|
61
|
+
* `propsSchema` stays the source of truth for *which* props exist; this only
|
|
62
|
+
* refines how the ones already there are presented.
|
|
63
|
+
*/
|
|
64
|
+
fields: z.record(z.string(), fieldMetaSchema).optional(),
|
|
65
|
+
listFields: z.record(z.string(), listFieldMetaSchema).optional(),
|
|
66
|
+
/**
|
|
67
|
+
* How the block presents itself in a catalogue, and whether it can be placed
|
|
68
|
+
* at all. None of the three is derivable from `propsSchema`.
|
|
69
|
+
*
|
|
70
|
+
* `chrome` is the one that changes what a caller may do: a chrome block is
|
|
71
|
+
* structurally pinned — always present, never added, moved or removed. An
|
|
72
|
+
* agent reading this manifest to decide what to insert needs to be told
|
|
73
|
+
* that, or it plans an `add_block` the ops engine will refuse.
|
|
74
|
+
*/
|
|
75
|
+
category: z.string().min(1).optional(),
|
|
76
|
+
description: z.string().min(1).optional(),
|
|
77
|
+
chrome: z.boolean().optional()
|
|
20
78
|
});
|
|
21
79
|
export const blockManifestSchema = z.object({
|
|
22
80
|
version: z.number().int().positive(),
|
|
@@ -25,42 +83,99 @@ export const blockManifestSchema = z.object({
|
|
|
25
83
|
function isObject(value) {
|
|
26
84
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
27
85
|
}
|
|
28
|
-
|
|
86
|
+
/** Name a runtime value the way an error message should: array, not object. */
|
|
87
|
+
function describeValue(value) {
|
|
88
|
+
if (value === null)
|
|
89
|
+
return "null";
|
|
90
|
+
if (value === undefined)
|
|
91
|
+
return "undefined";
|
|
92
|
+
if (Array.isArray(value))
|
|
93
|
+
return "array";
|
|
94
|
+
return typeof value;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Validate, and say what went wrong.
|
|
98
|
+
*
|
|
99
|
+
* The boolean form below could only report "does not match block manifest
|
|
100
|
+
* schema" — which is what an integrator saw when a plan wrote one prop of the
|
|
101
|
+
* wrong type, on a block with a dozen of them. The same failing patch reported
|
|
102
|
+
* through the registered-schema path said "bullets: expected string, received
|
|
103
|
+
* array". Two validators, one useless message, and the useful one was the path
|
|
104
|
+
* NOT taken when a manifest was supplied — so custom blocks, the case that
|
|
105
|
+
* needs the detail most, got the least.
|
|
106
|
+
*
|
|
107
|
+
* Returns the first issue found, or null when the value matches. It reports on
|
|
108
|
+
* exactly the constraints the boolean form enforced, with one deliberate
|
|
109
|
+
* loosening since — a key holding `undefined` counts as absent, see below.
|
|
110
|
+
* Note that `enum` and `const` are among the constraints it does not enforce —
|
|
111
|
+
* a value outside a declared enum still passes here.
|
|
112
|
+
*/
|
|
113
|
+
export function findManifestSchemaIssue(schema, value, path = "") {
|
|
29
114
|
const type = typeof schema.type === "string" ? schema.type : undefined;
|
|
115
|
+
const here = path || "(root)";
|
|
116
|
+
const child = (key) => (path ? `${path}.${key}` : key);
|
|
30
117
|
if (type === "object") {
|
|
31
118
|
if (!isObject(value))
|
|
32
|
-
return
|
|
119
|
+
return { path: here, message: `expected object, received ${describeValue(value)}` };
|
|
33
120
|
const props = isObject(schema.properties) ? schema.properties : {};
|
|
34
|
-
const required = Array.isArray(schema.required)
|
|
121
|
+
const required = Array.isArray(schema.required)
|
|
122
|
+
? schema.required.filter((item) => typeof item === "string")
|
|
123
|
+
: [];
|
|
124
|
+
/*
|
|
125
|
+
* A key holding `undefined` is absent.
|
|
126
|
+
*
|
|
127
|
+
* `JSON.stringify` drops such a key, so over HTTP this distinction cannot
|
|
128
|
+
* reach us at all — every request arrives with the key simply missing. In
|
|
129
|
+
* library mode the orchestrator runs in the consumer's own process and the
|
|
130
|
+
* props object is passed by reference, so `{ imageRatio: undefined }`
|
|
131
|
+
* survives and used to be rejected as "expected string, received
|
|
132
|
+
* undefined" — for an *optional* field, on a value the same code accepts
|
|
133
|
+
* over the wire. Zod agrees with the wire: `z.enum(...).optional()` takes
|
|
134
|
+
* `undefined`. Treating it as absent is what makes the two transports and
|
|
135
|
+
* the two validators say the same thing.
|
|
136
|
+
*/
|
|
35
137
|
for (const key of required) {
|
|
36
|
-
if (!(key in value))
|
|
37
|
-
return
|
|
138
|
+
if (!(key in value) || value[key] === undefined) {
|
|
139
|
+
return { path: child(key), message: "required, but missing" };
|
|
140
|
+
}
|
|
38
141
|
}
|
|
39
142
|
for (const [key, propSchema] of Object.entries(props)) {
|
|
40
|
-
if (!(key in value))
|
|
143
|
+
if (!(key in value) || value[key] === undefined)
|
|
41
144
|
continue;
|
|
42
145
|
if (!isObject(propSchema))
|
|
43
146
|
continue;
|
|
44
|
-
|
|
45
|
-
|
|
147
|
+
const issue = findManifestSchemaIssue(propSchema, value[key], child(key));
|
|
148
|
+
if (issue)
|
|
149
|
+
return issue;
|
|
46
150
|
}
|
|
47
|
-
return
|
|
151
|
+
return null;
|
|
48
152
|
}
|
|
49
153
|
if (type === "array") {
|
|
50
154
|
if (!Array.isArray(value))
|
|
51
|
-
return
|
|
155
|
+
return { path: here, message: `expected array, received ${describeValue(value)}` };
|
|
52
156
|
const items = schema.items;
|
|
53
|
-
if (isObject(items))
|
|
54
|
-
|
|
55
|
-
|
|
157
|
+
if (isObject(items)) {
|
|
158
|
+
for (let i = 0; i < value.length; i++) {
|
|
159
|
+
const issue = findManifestSchemaIssue(items, value[i], `${path}[${i}]`);
|
|
160
|
+
if (issue)
|
|
161
|
+
return issue;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return null;
|
|
56
165
|
}
|
|
57
|
-
if (type === "string")
|
|
58
|
-
return
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
166
|
+
if (type === "string" && typeof value !== "string") {
|
|
167
|
+
return { path: here, message: `expected string, received ${describeValue(value)}` };
|
|
168
|
+
}
|
|
169
|
+
if ((type === "number" || type === "integer") && !(typeof value === "number" && Number.isFinite(value))) {
|
|
170
|
+
return { path: here, message: `expected ${type}, received ${describeValue(value)}` };
|
|
171
|
+
}
|
|
172
|
+
if (type === "boolean" && typeof value !== "boolean") {
|
|
173
|
+
return { path: here, message: `expected boolean, received ${describeValue(value)}` };
|
|
174
|
+
}
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
177
|
+
export function validateByJsonSchemaLike(schema, value) {
|
|
178
|
+
return findManifestSchemaIssue(schema, value) === null;
|
|
64
179
|
}
|
|
65
180
|
// ---------------------------------------------------------------------------
|
|
66
181
|
// Derive FieldMeta from JSON Schema — fallback for custom blocks not in the
|
|
@@ -86,6 +201,17 @@ function inferFieldKind(key, schema) {
|
|
|
86
201
|
const type = typeof schema.type === "string" ? schema.type : undefined;
|
|
87
202
|
if (isProseMirrorDocSchema(schema))
|
|
88
203
|
return "richtext";
|
|
204
|
+
/*
|
|
205
|
+
* A markdown string, declared the way JSON Schema already provides for.
|
|
206
|
+
*
|
|
207
|
+
* Without this an integrator has no way to say "this string is prose": every
|
|
208
|
+
* plain string infers as `text`, so an external block's body field got a bare
|
|
209
|
+
* textarea and none of the rich-text handling, whatever it actually held.
|
|
210
|
+
* `contentMediaType` is standard vocabulary — no proprietary annotation, and
|
|
211
|
+
* a validator that does not know about it simply ignores it.
|
|
212
|
+
*/
|
|
213
|
+
if (type === "string" && schema.contentMediaType === "text/markdown")
|
|
214
|
+
return "richtext";
|
|
89
215
|
if (type === "boolean")
|
|
90
216
|
return "boolean";
|
|
91
217
|
if (type !== "string" && type !== "number" && type !== "integer")
|
|
@@ -127,7 +253,7 @@ function deriveItemFields(itemSchema) {
|
|
|
127
253
|
* field gets the richtext editor — and the underlying document is edited as-is,
|
|
128
254
|
* not flattened to a string.
|
|
129
255
|
*/
|
|
130
|
-
function isProseMirrorDocSchema(schema) {
|
|
256
|
+
export function isProseMirrorDocSchema(schema) {
|
|
131
257
|
if (!isObject(schema) || schema.type !== "object" || !isObject(schema.properties))
|
|
132
258
|
return false;
|
|
133
259
|
return constValue(schema.properties.type) === "doc";
|
|
@@ -245,3 +371,161 @@ export function validateManifestDefaultProps(blocks) {
|
|
|
245
371
|
}
|
|
246
372
|
return null;
|
|
247
373
|
}
|
|
374
|
+
/**
|
|
375
|
+
* The field metadata a manifest block should actually be edited with.
|
|
376
|
+
*
|
|
377
|
+
* Derived from the JSON schema, then refined by whatever the definition
|
|
378
|
+
* declared. Every surface that renders a manifest block — the property panel,
|
|
379
|
+
* the Puck adapter, the planner's block contracts, an integration's own
|
|
380
|
+
* boundary code — has to agree on this, so it lives here rather than being
|
|
381
|
+
* re-derived four times with four sets of assumptions.
|
|
382
|
+
*
|
|
383
|
+
* Two rules:
|
|
384
|
+
*
|
|
385
|
+
* - `propsSchema` decides *which* props exist. A declared entry for a key the
|
|
386
|
+
* schema does not expose is ignored, not added — otherwise a stale manifest
|
|
387
|
+
* would put a control on a prop the block cannot store.
|
|
388
|
+
* - A declared entry is merged *over* the derived one, key by key, so a site
|
|
389
|
+
* can supply just `multiline` and keep the label and options that were
|
|
390
|
+
* inferred for it.
|
|
391
|
+
*/
|
|
392
|
+
export function resolveManifestFieldMeta(definition) {
|
|
393
|
+
const { fields, listFields } = deriveFieldMetaFromSchema(definition.propsSchema);
|
|
394
|
+
const declaredFields = isObject(definition.fields) ? definition.fields : {};
|
|
395
|
+
const resolvedFields = {};
|
|
396
|
+
for (const [key, derived] of Object.entries(fields)) {
|
|
397
|
+
const declared = declaredFields[key];
|
|
398
|
+
resolvedFields[key] = isObject(declared) ? { ...derived, ...declared } : derived;
|
|
399
|
+
}
|
|
400
|
+
const declaredLists = isObject(definition.listFields) ? definition.listFields : {};
|
|
401
|
+
const resolvedListFields = {};
|
|
402
|
+
for (const [key, derived] of Object.entries(listFields)) {
|
|
403
|
+
const declared = declaredLists[key];
|
|
404
|
+
if (!isObject(declared)) {
|
|
405
|
+
resolvedListFields[key] = derived;
|
|
406
|
+
continue;
|
|
407
|
+
}
|
|
408
|
+
const declaredItems = isObject(declared.itemFields) ? declared.itemFields : {};
|
|
409
|
+
const itemFields = {};
|
|
410
|
+
for (const [itemKey, derivedItem] of Object.entries(derived.itemFields)) {
|
|
411
|
+
const declaredItem = declaredItems[itemKey];
|
|
412
|
+
itemFields[itemKey] = isObject(declaredItem)
|
|
413
|
+
? { ...derivedItem, ...declaredItem }
|
|
414
|
+
: derivedItem;
|
|
415
|
+
}
|
|
416
|
+
resolvedListFields[key] = { ...derived, ...declared, itemFields };
|
|
417
|
+
}
|
|
418
|
+
return { fields: resolvedFields, listFields: resolvedListFields };
|
|
419
|
+
}
|
|
420
|
+
// ---------------------------------------------------------------------------
|
|
421
|
+
// Manifest construction
|
|
422
|
+
// ---------------------------------------------------------------------------
|
|
423
|
+
/**
|
|
424
|
+
* Build a manifest describing every block type registered in THIS process.
|
|
425
|
+
*
|
|
426
|
+
* Built fresh on every call rather than memoised, because the registry is not
|
|
427
|
+
* fixed at module load: a site's custom blocks call `registerBlock()` from its
|
|
428
|
+
* own `blocks/register.ts`, which may run after this module was first imported.
|
|
429
|
+
* A cached manifest is a manifest missing exactly the blocks the caller most
|
|
430
|
+
* needs to know about.
|
|
431
|
+
*
|
|
432
|
+
* This lives in `shared`, not in the SDK, because two very different consumers
|
|
433
|
+
* need the same answer. The editor asks the *site* for it over
|
|
434
|
+
* `/api/editor/blocks`; an MCP agent asks the *orchestrator*, which in library
|
|
435
|
+
* mode is mounted in that same site process and so shares its registry. Keeping
|
|
436
|
+
* one builder is what makes those two answers the same answer.
|
|
437
|
+
*/
|
|
438
|
+
export function buildBlockManifest() {
|
|
439
|
+
/*
|
|
440
|
+
* The site's catalogue, not the registry.
|
|
441
|
+
*
|
|
442
|
+
* These are the same list until a site declares otherwise — and a Tier B site
|
|
443
|
+
* has to, because importing anything from this package registers Avocado's 18
|
|
444
|
+
* built-ins whether or not the site has a renderer for one. PBA's manifest
|
|
445
|
+
* offered 32 types for 14 renderers; an agent that took the list at its word
|
|
446
|
+
* could add a `Hero` that applied cleanly and drew nothing.
|
|
447
|
+
*/
|
|
448
|
+
const blocks = catalogueBlockTypes()
|
|
449
|
+
.map((type) => {
|
|
450
|
+
const meta = getBlockMeta(type);
|
|
451
|
+
const propsSchema = getBlockJsonSchema(type);
|
|
452
|
+
if (!propsSchema)
|
|
453
|
+
return null; // skip blocks without schema (shouldn't happen but be safe)
|
|
454
|
+
const finalSchema = applyMetaEnumsToSchema(propsSchema, meta);
|
|
455
|
+
/*
|
|
456
|
+
* Only blocks that declare defaults get a `defaultProps`.
|
|
457
|
+
*
|
|
458
|
+
* This used to attach `defaultPropsForType(type)` whenever it validated
|
|
459
|
+
* against the schema, which sounds like the same rule and is not: the
|
|
460
|
+
* generic CTA-shaped fallback that function hands to every *custom* block
|
|
461
|
+
* validates against any schema permissive enough to allow unknown keys —
|
|
462
|
+
* and a site's own blocks are usually registered with a catchall so chat
|
|
463
|
+
* edits don't strip props the site cares about. So `get-block-schema
|
|
464
|
+
* pba_heroImage` answered with a `title`/`description`/`ctaText`/`ctaHref`
|
|
465
|
+
* shape that appears nowhere in that block's own schema, and an agent
|
|
466
|
+
* following it would have built a broken block.
|
|
467
|
+
*
|
|
468
|
+
* The validation guard stays underneath: a declared default that violates
|
|
469
|
+
* its own schema is still dropped rather than allowed to fail
|
|
470
|
+
* `validateManifestDefaultProps` and poison the whole manifest, which
|
|
471
|
+
* would make every block read as "unknown" in the editor.
|
|
472
|
+
*/
|
|
473
|
+
const candidateDefaults = declaredDefaultPropsForType(type);
|
|
474
|
+
const defaultProps = candidateDefaults && validateByJsonSchemaLike(finalSchema, candidateDefaults)
|
|
475
|
+
? candidateDefaults
|
|
476
|
+
: undefined;
|
|
477
|
+
return {
|
|
478
|
+
type,
|
|
479
|
+
displayName: meta?.displayName ?? type,
|
|
480
|
+
propsSchema: finalSchema,
|
|
481
|
+
defaultProps,
|
|
482
|
+
/*
|
|
483
|
+
* Carry the block's own field metadata, not just its schema.
|
|
484
|
+
*
|
|
485
|
+
* `kind` and `multiline` have no JSON Schema spelling, so a manifest
|
|
486
|
+
* built from the schema alone hands every string the same one-line
|
|
487
|
+
* input — a site that declared `richtext` for a body paragraph got the
|
|
488
|
+
* control it would have got for a one-word headline. The consumer
|
|
489
|
+
* merges this over what it derives; the schema still decides which
|
|
490
|
+
* props exist.
|
|
491
|
+
*/
|
|
492
|
+
...(meta?.fields ? { fields: meta.fields } : {}),
|
|
493
|
+
...(meta?.listFields ? { listFields: meta.listFields } : {}),
|
|
494
|
+
...(meta?.category ? { category: meta.category } : {}),
|
|
495
|
+
...(meta?.description ? { description: meta.description } : {}),
|
|
496
|
+
...(meta?.chrome === true ? { chrome: true } : {})
|
|
497
|
+
};
|
|
498
|
+
})
|
|
499
|
+
.filter((b) => b !== null);
|
|
500
|
+
return { version: 1, blocks };
|
|
501
|
+
}
|
|
502
|
+
/**
|
|
503
|
+
* Fold enum options declared in the block's registry `meta.fields` into the
|
|
504
|
+
* derived JSON schema. A site can declare `f.enum("Gap", ["sm","md","lg"])` in
|
|
505
|
+
* its block meta while the Zod prop stays a plain `z.string()` (so any value
|
|
506
|
+
* still validates). Without this, manifest-driven custom blocks lose the enum
|
|
507
|
+
* and the editor renders a free-text input instead of a dropdown. We only add
|
|
508
|
+
* `enum` (the editor reads it to pick a select control); validation is type-only
|
|
509
|
+
* (see validateByJsonSchemaLike), so widening here never rejects stored content.
|
|
510
|
+
*/
|
|
511
|
+
function applyMetaEnumsToSchema(schema, meta) {
|
|
512
|
+
if (!meta?.fields)
|
|
513
|
+
return schema;
|
|
514
|
+
const properties = schema.properties;
|
|
515
|
+
if (!properties || typeof properties !== "object")
|
|
516
|
+
return schema;
|
|
517
|
+
let next;
|
|
518
|
+
for (const [key, fieldMeta] of Object.entries(meta.fields)) {
|
|
519
|
+
if (fieldMeta.kind !== "enum" || !fieldMeta.options?.length)
|
|
520
|
+
continue;
|
|
521
|
+
const prop = properties[key];
|
|
522
|
+
if (!prop || typeof prop !== "object" || Array.isArray(prop))
|
|
523
|
+
continue;
|
|
524
|
+
const propObj = prop;
|
|
525
|
+
if (Array.isArray(propObj.enum))
|
|
526
|
+
continue; // schema already carries an enum
|
|
527
|
+
next ??= { ...schema, properties: { ...properties } };
|
|
528
|
+
next.properties[key] = { ...propObj, type: "string", enum: [...fieldMeta.options] };
|
|
529
|
+
}
|
|
530
|
+
return next ?? schema;
|
|
531
|
+
}
|
|
@@ -23,6 +23,40 @@ export type FieldMeta = {
|
|
|
23
23
|
imageSpec?: ImageSpec;
|
|
24
24
|
/** Render as a textarea in the PropertyPanel instead of a single-line input. */
|
|
25
25
|
multiline?: boolean;
|
|
26
|
+
/**
|
|
27
|
+
* This rich-text value is one line: marks, no block structure.
|
|
28
|
+
*
|
|
29
|
+
* A headline is not a document. It carries character styles — a brand
|
|
30
|
+
* highlight, a bold phrase — but a paragraph break, a bullet list or an H2
|
|
31
|
+
* inside it is meaningless, and offering those controls invites an editor to
|
|
32
|
+
* break the design. Declaring `inline` gives the field the document editor
|
|
33
|
+
* with only the mark controls, and a schema whose top node holds a single
|
|
34
|
+
* paragraph.
|
|
35
|
+
*
|
|
36
|
+
* Only meaningful with `kind: "richtext"` on a document-valued prop. The
|
|
37
|
+
* value must genuinely be one paragraph: ProseMirror drops what its schema
|
|
38
|
+
* cannot hold, so declaring this on a multi-block value would discard every
|
|
39
|
+
* block after the first.
|
|
40
|
+
*/
|
|
41
|
+
inline?: boolean;
|
|
42
|
+
/**
|
|
43
|
+
* Character styles this rich-text field permits beyond the built-in set.
|
|
44
|
+
*
|
|
45
|
+
* A *decorator* in the rich-text sense: a boolean toggle over a range that
|
|
46
|
+
* carries no data, the same kind of thing bold and italic are. Every CMS has
|
|
47
|
+
* a name for them — Portable Text calls them `decorators`, ProseMirror calls
|
|
48
|
+
* them marks with no attributes — and every CMS lets a project declare its
|
|
49
|
+
* own. Avocado's built-in five cannot cover a site's brand highlight, so a
|
|
50
|
+
* block declares the extra names here and the document editor accepts them
|
|
51
|
+
* instead of deleting them on load.
|
|
52
|
+
*
|
|
53
|
+
* Marks that carry *data* (a link's href, a reference to another document)
|
|
54
|
+
* are a different thing and are not declared this way.
|
|
55
|
+
*/
|
|
56
|
+
decorators?: {
|
|
57
|
+
name: string;
|
|
58
|
+
title?: string;
|
|
59
|
+
}[];
|
|
26
60
|
/** Whether the field is required. Auto-derived from Zod schema at registration. */
|
|
27
61
|
required?: boolean;
|
|
28
62
|
};
|
|
@@ -90,6 +124,30 @@ export declare function getImageSpec(blockType: string, fieldPath: string): Imag
|
|
|
90
124
|
*/
|
|
91
125
|
export declare const blockSchemas: Record<string, z.ZodObject<any, z.core.$strip>>;
|
|
92
126
|
export declare const allowedBlockTypes: string[];
|
|
127
|
+
/**
|
|
128
|
+
* Declare the block types this site renders. Pass `null` to lift a declaration.
|
|
129
|
+
*
|
|
130
|
+
* Call it once, from wherever the site registers its blocks. Order does not
|
|
131
|
+
* matter: this records names, and the manifest is rebuilt per request.
|
|
132
|
+
*
|
|
133
|
+
* A declared type that is never registered is not an error here — it simply has
|
|
134
|
+
* no schema to describe, so it does not reach the manifest. `undeclaredBlockTypes()`
|
|
135
|
+
* exists so a caller that wants to complain about that can.
|
|
136
|
+
*/
|
|
137
|
+
export declare function declareBlockCatalogue(types: readonly string[] | null): void;
|
|
138
|
+
/** The declared catalogue, or null when the site has not declared one. */
|
|
139
|
+
export declare function getBlockCatalogue(): ReadonlySet<string> | null;
|
|
140
|
+
/**
|
|
141
|
+
* Whether a type belongs to this site's catalogue.
|
|
142
|
+
*
|
|
143
|
+
* True for everything when no catalogue is declared — the guard has to be
|
|
144
|
+
* invisible to the sites that never opted in.
|
|
145
|
+
*/
|
|
146
|
+
export declare function isInBlockCatalogue(type: string): boolean;
|
|
147
|
+
/** Registered, non-chrome types this site renders — `allowedBlockTypes` through the catalogue. */
|
|
148
|
+
export declare function catalogueBlockTypes(): string[];
|
|
149
|
+
/** Declared types with no registration behind them — a typo, or a missing import. */
|
|
150
|
+
export declare function undeclaredBlockTypes(): string[];
|
|
93
151
|
export declare function getPropDisplayName(blockType: string | undefined, propKey: string): string;
|
|
94
152
|
export declare function defaultListItemForBlock(type: BlockType, listKey: string): Record<string, unknown> | null;
|
|
95
153
|
/** Base schema — accepts any block type. Used for ingesting external site content with custom blocks. */
|
package/dist/blocks/_registry.js
CHANGED
|
@@ -173,6 +173,72 @@ export function getImageSpec(blockType, fieldPath) {
|
|
|
173
173
|
*/
|
|
174
174
|
export const blockSchemas = _blockSchemas;
|
|
175
175
|
export const allowedBlockTypes = G.__ase_allowedBlockTypes ?? (G.__ase_allowedBlockTypes = []);
|
|
176
|
+
// ---------------------------------------------------------------------------
|
|
177
|
+
// The catalogue a site actually renders
|
|
178
|
+
// ---------------------------------------------------------------------------
|
|
179
|
+
/*
|
|
180
|
+
* `allowedBlockTypes` answers "what is registered", which is not the same
|
|
181
|
+
* question as "what can this site draw".
|
|
182
|
+
*
|
|
183
|
+
* Importing anything at all from this package registers Avocado's built-ins,
|
|
184
|
+
* transitively and unavoidably — so a site that brought its own fourteen block
|
|
185
|
+
* types advertised thirty-two, and an agent reading that list could add a `Hero`
|
|
186
|
+
* that applied cleanly and rendered nothing. The registry was not lying: the
|
|
187
|
+
* built-ins really are registered. They are simply registered in a process that
|
|
188
|
+
* has no renderer for them.
|
|
189
|
+
*
|
|
190
|
+
* A site declares its catalogue and the two questions come apart. Declaring
|
|
191
|
+
* nothing keeps the old behaviour exactly, which is what every Tier A site and
|
|
192
|
+
* every example wants: there, registered and renderable are the same set.
|
|
193
|
+
*
|
|
194
|
+
* Stored on `globalThis` for the same reason the registries above are — Next
|
|
195
|
+
* duplicates this module across the RSC / SSR / route-handler layers, and a
|
|
196
|
+
* declaration made in one copy has to be visible to the manifest built in
|
|
197
|
+
* another.
|
|
198
|
+
*/
|
|
199
|
+
function catalogueSlot() {
|
|
200
|
+
return G.__ase_blockCatalogue ??
|
|
201
|
+
(G.__ase_blockCatalogue = { types: null });
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Declare the block types this site renders. Pass `null` to lift a declaration.
|
|
205
|
+
*
|
|
206
|
+
* Call it once, from wherever the site registers its blocks. Order does not
|
|
207
|
+
* matter: this records names, and the manifest is rebuilt per request.
|
|
208
|
+
*
|
|
209
|
+
* A declared type that is never registered is not an error here — it simply has
|
|
210
|
+
* no schema to describe, so it does not reach the manifest. `undeclaredBlockTypes()`
|
|
211
|
+
* exists so a caller that wants to complain about that can.
|
|
212
|
+
*/
|
|
213
|
+
export function declareBlockCatalogue(types) {
|
|
214
|
+
catalogueSlot().types = types === null ? null : new Set(types);
|
|
215
|
+
}
|
|
216
|
+
/** The declared catalogue, or null when the site has not declared one. */
|
|
217
|
+
export function getBlockCatalogue() {
|
|
218
|
+
return catalogueSlot().types;
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* Whether a type belongs to this site's catalogue.
|
|
222
|
+
*
|
|
223
|
+
* True for everything when no catalogue is declared — the guard has to be
|
|
224
|
+
* invisible to the sites that never opted in.
|
|
225
|
+
*/
|
|
226
|
+
export function isInBlockCatalogue(type) {
|
|
227
|
+
const declared = catalogueSlot().types;
|
|
228
|
+
return declared === null || declared.has(type);
|
|
229
|
+
}
|
|
230
|
+
/** Registered, non-chrome types this site renders — `allowedBlockTypes` through the catalogue. */
|
|
231
|
+
export function catalogueBlockTypes() {
|
|
232
|
+
const declared = catalogueSlot().types;
|
|
233
|
+
return declared === null ? allowedBlockTypes : allowedBlockTypes.filter((t) => declared.has(t));
|
|
234
|
+
}
|
|
235
|
+
/** Declared types with no registration behind them — a typo, or a missing import. */
|
|
236
|
+
export function undeclaredBlockTypes() {
|
|
237
|
+
const declared = catalogueSlot().types;
|
|
238
|
+
if (declared === null)
|
|
239
|
+
return [];
|
|
240
|
+
return [...declared].filter((t) => !(t in _blockSchemas));
|
|
241
|
+
}
|
|
176
242
|
export function getPropDisplayName(blockType, propKey) {
|
|
177
243
|
if (!blockType)
|
|
178
244
|
return propKey;
|
package/dist/blocks/footer.js
CHANGED
|
@@ -15,7 +15,26 @@ registerBlock("Footer", {
|
|
|
15
15
|
listFields: {
|
|
16
16
|
columns: {
|
|
17
17
|
label: "Footer columns",
|
|
18
|
-
itemFields: {
|
|
18
|
+
itemFields: {
|
|
19
|
+
title: f.text("Column title"),
|
|
20
|
+
/*
|
|
21
|
+
* A newline-delimited list of `label|url` pairs — NOT prose.
|
|
22
|
+
*
|
|
23
|
+
* This was `f.richtext`, which put a markdown editor on it. Markdown
|
|
24
|
+
* has no bare newline: Tiptap parsed the lines as one paragraph with
|
|
25
|
+
* a soft break and serialised it back with a space, so a single
|
|
26
|
+
* keystroke turned "Features|/features\nPricing|/pricing" into
|
|
27
|
+
* "Features|/features Pricing|/pricing" — one link, with the rest of
|
|
28
|
+
* the column swallowed into its href. Footer is a chrome block, so
|
|
29
|
+
* that was live on every page of every site.
|
|
30
|
+
*
|
|
31
|
+
* A plain textarea preserves the separator. The real fix is a field
|
|
32
|
+
* kind for a list of scalars, which needs a schema change and a
|
|
33
|
+
* migration of stored footers; until then, do not put a prose editor
|
|
34
|
+
* on a delimited value.
|
|
35
|
+
*/
|
|
36
|
+
links: f.longtext("Links (one label|url per line)")
|
|
37
|
+
}
|
|
19
38
|
}
|
|
20
39
|
}
|
|
21
40
|
}
|
package/dist/blocks/index.d.ts
CHANGED
|
@@ -1,3 +1,19 @@
|
|
|
1
1
|
import type { BlockType } from "./_registry.ts";
|
|
2
2
|
export declare function defaultPropsForType(type: BlockType): Record<string, unknown>;
|
|
3
|
+
/**
|
|
4
|
+
* The defaults a block type *actually declares*, or null when it declares none.
|
|
5
|
+
*
|
|
6
|
+
* `defaultPropsForType` never answers "I don't know" — an unregistered type gets
|
|
7
|
+
* the CTA-shaped `fallback` above, which is a reasonable last resort when
|
|
8
|
+
* something must be rendered and a bad answer when the question was "what are
|
|
9
|
+
* this block's defaults?". A site's own block is exactly that case: it is
|
|
10
|
+
* registered, it has a real schema, and it has no declared defaults, so the
|
|
11
|
+
* fallback describes it as having a `title`, a `description` and a CTA it has
|
|
12
|
+
* never heard of.
|
|
13
|
+
*
|
|
14
|
+
* Callers that report on a block, or that build one from a caller's own props,
|
|
15
|
+
* want the honest null. Callers that must produce *something* renderable keep
|
|
16
|
+
* using `defaultPropsForType`.
|
|
17
|
+
*/
|
|
18
|
+
export declare function declaredDefaultPropsForType(type: BlockType): Record<string, unknown> | null;
|
|
3
19
|
export { DEFAULT_HEADING_LEVELS, resolveHeadingTag, resolveItemHeadingTag } from "./_helpers.ts";
|
package/dist/blocks/index.js
CHANGED
|
@@ -50,4 +50,22 @@ const fallback = {
|
|
|
50
50
|
export function defaultPropsForType(type) {
|
|
51
51
|
return defaults[type]?.() ?? { ...fallback };
|
|
52
52
|
}
|
|
53
|
+
/**
|
|
54
|
+
* The defaults a block type *actually declares*, or null when it declares none.
|
|
55
|
+
*
|
|
56
|
+
* `defaultPropsForType` never answers "I don't know" — an unregistered type gets
|
|
57
|
+
* the CTA-shaped `fallback` above, which is a reasonable last resort when
|
|
58
|
+
* something must be rendered and a bad answer when the question was "what are
|
|
59
|
+
* this block's defaults?". A site's own block is exactly that case: it is
|
|
60
|
+
* registered, it has a real schema, and it has no declared defaults, so the
|
|
61
|
+
* fallback describes it as having a `title`, a `description` and a CTA it has
|
|
62
|
+
* never heard of.
|
|
63
|
+
*
|
|
64
|
+
* Callers that report on a block, or that build one from a caller's own props,
|
|
65
|
+
* want the honest null. Callers that must produce *something* renderable keep
|
|
66
|
+
* using `defaultPropsForType`.
|
|
67
|
+
*/
|
|
68
|
+
export function declaredDefaultPropsForType(type) {
|
|
69
|
+
return defaults[type]?.() ?? null;
|
|
70
|
+
}
|
|
53
71
|
export { DEFAULT_HEADING_LEVELS, resolveHeadingTag, resolveItemHeadingTag } from "./_helpers.js";
|
package/dist/blocks/quote.js
CHANGED
|
@@ -27,6 +27,6 @@ export function quoteDefaultProps() {
|
|
|
27
27
|
quote: "The best way to predict the future is to create it.",
|
|
28
28
|
author: "Peter Drucker",
|
|
29
29
|
role: "Management Consultant",
|
|
30
|
-
imageUrl: "https://placehold.co/300x300/e2e8f0/475569?text=PD",
|
|
30
|
+
imageUrl: "https://placehold.co/300x300/e2e8f0/475569.png?text=PD",
|
|
31
31
|
};
|
|
32
32
|
}
|
package/dist/chat-events.d.ts
CHANGED
|
@@ -133,7 +133,7 @@ export declare const chatStreamEventSchema: z.ZodDiscriminatedUnion<[z.ZodObject
|
|
|
133
133
|
title: z.ZodOptional<z.ZodString>;
|
|
134
134
|
description: z.ZodOptional<z.ZodString>;
|
|
135
135
|
ogImage: z.ZodOptional<z.ZodString>;
|
|
136
|
-
}, z.core.$
|
|
136
|
+
}, z.core.$strict>;
|
|
137
137
|
}, z.core.$strip>, z.ZodObject<{
|
|
138
138
|
op: z.ZodLiteral<"update_site_config">;
|
|
139
139
|
patch: z.ZodObject<{
|
|
@@ -262,7 +262,7 @@ export declare const chatStreamEventSchema: z.ZodDiscriminatedUnion<[z.ZodObject
|
|
|
262
262
|
title: z.ZodOptional<z.ZodString>;
|
|
263
263
|
description: z.ZodOptional<z.ZodString>;
|
|
264
264
|
ogImage: z.ZodOptional<z.ZodString>;
|
|
265
|
-
}, z.core.$
|
|
265
|
+
}, z.core.$strict>;
|
|
266
266
|
}, z.core.$strip>, z.ZodObject<{
|
|
267
267
|
op: z.ZodLiteral<"update_site_config">;
|
|
268
268
|
patch: z.ZodObject<{
|
|
@@ -397,7 +397,7 @@ export declare const chatStreamEventSchema: z.ZodDiscriminatedUnion<[z.ZodObject
|
|
|
397
397
|
title: z.ZodOptional<z.ZodString>;
|
|
398
398
|
description: z.ZodOptional<z.ZodString>;
|
|
399
399
|
ogImage: z.ZodOptional<z.ZodString>;
|
|
400
|
-
}, z.core.$
|
|
400
|
+
}, z.core.$strict>;
|
|
401
401
|
}, z.core.$strip>, z.ZodObject<{
|
|
402
402
|
op: z.ZodLiteral<"update_site_config">;
|
|
403
403
|
patch: z.ZodObject<{
|
package/dist/index.d.ts
CHANGED
|
@@ -2,9 +2,10 @@ export { EDITOR_PROTOCOL_VERSION } from "./protocol.ts";
|
|
|
2
2
|
export { getConfiguredDraftSecret, getSafeInternalRedirectPath, validateDraftSecret, type DraftSecretValidationResult } from "./draft-mode.ts";
|
|
3
3
|
export type { FieldDiffKind, FieldDiff, BlockDiffStatus, BlockDiff, PageDiffStatus, PageDiff, PublishDiff, SiteConfigFieldDiff, SiteConfigDiff, } from "./publish-diff.ts";
|
|
4
4
|
export { isImagePath, toAltPath, setPropAtPath } from "./editable-path.ts";
|
|
5
|
-
export {
|
|
6
|
-
export {
|
|
7
|
-
export {
|
|
5
|
+
export { parseInline, parseRichText, parseRichTextBlocks, normalizeRichTextBody, resolveRichTextHeadingLevel, clampMarkdownHeadings, unescapeMarkdownText, isRichTextDoc, fromMarkdown, toMarkdown, mergeRichTextDoc, NODE, MARK, type InlineToken, type RichTextBlock, type RichTextList, type RichTextListItem, type RichTextDoc, type RichTextNode, type RichTextMark } from "@avocadostudio-ai/richtext";
|
|
6
|
+
export { blockDefinitionSchema, blockManifestSchema, buildBlockManifest, jsonSchemaLikeSchema, validateByJsonSchemaLike, findManifestSchemaIssue, type ManifestSchemaIssue, validateManifestDefaultProps, deriveFieldMetaFromSchema, resolveManifestFieldMeta, isProseMirrorDocSchema, type BlockDefinition, type BlockManifest } from "./block-manifest.ts";
|
|
7
|
+
export { type FieldKind, type ImageSpec, type FieldMeta, type ListFieldMeta, type BlockMeta, type BlockType, type BlockInstance, type BlockRegistration, IMAGE_PLACEHOLDER, isImagePlaceholder, registerBlock, getBlockMeta, getAllBlockMeta, getImageFields, getListImageFields, isFieldInlineEditable, getImageSpec, isChrome, getChromeTypes, blockSchemas, allowedBlockTypes, declareBlockCatalogue, getBlockCatalogue, isInBlockCatalogue, catalogueBlockTypes, undeclaredBlockTypes, getPropDisplayName, defaultListItemForBlock, blockInstanceSchema, blockInstanceSchemaLenient, validateBlockProps, getBlockJsonSchema, } from "./blocks/_registry.ts";
|
|
8
|
+
export { defaultPropsForType, declaredDefaultPropsForType, resolveHeadingTag, resolveItemHeadingTag, DEFAULT_HEADING_LEVELS, } from "./blocks/index.ts";
|
|
8
9
|
export { blockTypeToCamel, camelToBlockType, blockTypeToLower, lowerToBlockType, } from "./block-names.ts";
|
|
9
10
|
export { makeAddBlock, generateBlockId, makeAddItem, generateItemId, ensureItemIds, type AddBlockOp, type MakeAddBlockOptions, type AddItemOp, type MakeAddItemOptions, } from "./ops/builders.ts";
|
|
10
11
|
export { THEME_TOKEN_TO_CSS_VARS, themeTokenKeys, semanticThemeTokensSchema, mapSemanticThemeTokens, type ThemeTokenKey, type SemanticThemeTokens, } from "./ops/theme-tokens.ts";
|