@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.
@@ -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
- export function validateByJsonSchemaLike(schema, value) {
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 false;
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) ? schema.required.filter((item) => typeof item === "string") : [];
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 false;
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
- if (!validateByJsonSchemaLike(propSchema, value[key]))
45
- return false;
147
+ const issue = findManifestSchemaIssue(propSchema, value[key], child(key));
148
+ if (issue)
149
+ return issue;
46
150
  }
47
- return true;
151
+ return null;
48
152
  }
49
153
  if (type === "array") {
50
154
  if (!Array.isArray(value))
51
- return false;
155
+ return { path: here, message: `expected array, received ${describeValue(value)}` };
52
156
  const items = schema.items;
53
- if (isObject(items))
54
- return value.every((item) => validateByJsonSchemaLike(items, item));
55
- return true;
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 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;
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. */
@@ -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;
@@ -15,7 +15,26 @@ registerBlock("Footer", {
15
15
  listFields: {
16
16
  columns: {
17
17
  label: "Footer columns",
18
- itemFields: { title: f.text("Column title"), links: f.richtext("Links (one label|url per line)") }
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
  }
@@ -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";
@@ -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";
@@ -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
  }
@@ -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.$strip>;
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.$strip>;
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.$strip>;
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 { blockDefinitionSchema, blockManifestSchema, jsonSchemaLikeSchema, validateByJsonSchemaLike, validateManifestDefaultProps, deriveFieldMetaFromSchema, type BlockDefinition, type BlockManifest } from "./block-manifest.ts";
6
- 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, getPropDisplayName, defaultListItemForBlock, blockInstanceSchema, blockInstanceSchemaLenient, validateBlockProps, getBlockJsonSchema, } from "./blocks/_registry.ts";
7
- export { defaultPropsForType, resolveHeadingTag, resolveItemHeadingTag, DEFAULT_HEADING_LEVELS, } from "./blocks/index.ts";
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";