@avocadostudio-ai/shared 0.3.2 → 0.4.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/dist/api-responses.d.ts +5 -0
- package/dist/api-responses.js +12 -0
- package/dist/block-manifest.d.ts +18 -10
- package/dist/block-manifest.js +114 -1
- package/dist/blocks/_helpers.d.ts +21 -0
- package/dist/blocks/_helpers.js +21 -0
- package/dist/blocks/_registry.d.ts +61 -2
- package/dist/blocks/_registry.js +88 -1
- package/dist/blocks/banner.js +3 -1
- package/dist/blocks/card-grid.js +3 -1
- package/dist/blocks/card.js +3 -1
- package/dist/blocks/carousel.js +3 -1
- package/dist/blocks/cta.js +7 -3
- package/dist/blocks/feature-grid.js +1 -1
- package/dist/blocks/hero.js +7 -3
- package/dist/blocks/site-header.js +3 -1
- package/dist/blocks/stats.js +1 -1
- package/dist/blocks/table.js +5 -2
- package/dist/blocks/testimonials.js +1 -1
- package/dist/blocks/two-column.js +49 -3
- package/dist/editable-coverage.d.ts +85 -0
- package/dist/editable-coverage.js +342 -0
- package/dist/editable-path.d.ts +11 -0
- package/dist/editable-path.js +17 -0
- package/dist/index.d.ts +5 -2
- package/dist/index.js +4 -2
- package/dist/links.d.ts +243 -0
- package/dist/links.js +509 -0
- package/package.json +2 -2
package/dist/api-responses.d.ts
CHANGED
|
@@ -52,6 +52,11 @@ export declare const chatStartResponseSchema: z.ZodObject<{
|
|
|
52
52
|
}, z.core.$loose>;
|
|
53
53
|
export declare const slugsResponseSchema: z.ZodObject<{
|
|
54
54
|
slugs: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
55
|
+
pages: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
56
|
+
slug: z.ZodString;
|
|
57
|
+
path: z.ZodOptional<z.ZodString>;
|
|
58
|
+
title: z.ZodOptional<z.ZodString>;
|
|
59
|
+
}, z.core.$loose>>>;
|
|
55
60
|
}, z.core.$loose>;
|
|
56
61
|
export declare const bootstrapResponseSchema: z.ZodObject<{
|
|
57
62
|
slugs: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
package/dist/api-responses.js
CHANGED
|
@@ -58,6 +58,18 @@ export const chatStartResponseSchema = z.object({
|
|
|
58
58
|
}).passthrough();
|
|
59
59
|
export const slugsResponseSchema = z.object({
|
|
60
60
|
slugs: z.array(z.string()).optional(),
|
|
61
|
+
/*
|
|
62
|
+
* The same pages the `slugs` array names, with the title and the URL that
|
|
63
|
+
* `/draft/slugs` has always returned beside them. The editor parsed this
|
|
64
|
+
* response and kept only the bare slugs, so the link picker had nothing to
|
|
65
|
+
* label a page with; `path` is what a locale-prefixed site navigates to when
|
|
66
|
+
* it differs from the slug (see docs/ideas/page-identity-punch-list.md).
|
|
67
|
+
*/
|
|
68
|
+
pages: z.array(z.object({
|
|
69
|
+
slug: z.string(),
|
|
70
|
+
path: z.string().optional(),
|
|
71
|
+
title: z.string().optional(),
|
|
72
|
+
}).passthrough()).optional(),
|
|
61
73
|
}).passthrough();
|
|
62
74
|
export const bootstrapResponseSchema = z.object({
|
|
63
75
|
slugs: z.array(z.string()).optional(),
|
package/dist/block-manifest.d.ts
CHANGED
|
@@ -1,24 +1,16 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import type { FieldMeta, ListFieldMeta } from "./blocks/_registry.ts";
|
|
3
3
|
export declare const jsonSchemaLikeSchema: z.ZodType<Record<string, unknown>>;
|
|
4
|
-
/**
|
|
5
|
-
* Field metadata a site may declare alongside the JSON schema.
|
|
6
|
-
*
|
|
7
|
-
* JSON Schema describes what a value *is*; this describes how a person edits
|
|
8
|
-
* it, and the two are not the same question. A one-word headline and a six
|
|
9
|
-
* sentence body are both `{"type":"string"}`, so a manifest carrying only the
|
|
10
|
-
* schema gives them the same one-line input — which is exactly what happened to
|
|
11
|
-
* every integration with a real prose field. `kind` and `multiline` have no
|
|
12
|
-
* JSON Schema spelling, so they have to travel beside it.
|
|
13
|
-
*/
|
|
14
4
|
export declare const fieldMetaSchema: z.ZodObject<{
|
|
15
5
|
kind: z.ZodEnum<{
|
|
16
6
|
number: "number";
|
|
17
7
|
boolean: "boolean";
|
|
8
|
+
file: "file";
|
|
18
9
|
enum: "enum";
|
|
19
10
|
text: "text";
|
|
20
11
|
richtext: "richtext";
|
|
21
12
|
url: "url";
|
|
13
|
+
link: "link";
|
|
22
14
|
image: "image";
|
|
23
15
|
imageAlt: "imageAlt";
|
|
24
16
|
color: "color";
|
|
@@ -55,10 +47,12 @@ export declare const listFieldMetaSchema: z.ZodObject<{
|
|
|
55
47
|
kind: z.ZodEnum<{
|
|
56
48
|
number: "number";
|
|
57
49
|
boolean: "boolean";
|
|
50
|
+
file: "file";
|
|
58
51
|
enum: "enum";
|
|
59
52
|
text: "text";
|
|
60
53
|
richtext: "richtext";
|
|
61
54
|
url: "url";
|
|
55
|
+
link: "link";
|
|
62
56
|
image: "image";
|
|
63
57
|
imageAlt: "imageAlt";
|
|
64
58
|
color: "color";
|
|
@@ -94,10 +88,12 @@ export declare const listFieldMetaSchema: z.ZodObject<{
|
|
|
94
88
|
kind: z.ZodEnum<{
|
|
95
89
|
number: "number";
|
|
96
90
|
boolean: "boolean";
|
|
91
|
+
file: "file";
|
|
97
92
|
enum: "enum";
|
|
98
93
|
text: "text";
|
|
99
94
|
richtext: "richtext";
|
|
100
95
|
url: "url";
|
|
96
|
+
link: "link";
|
|
101
97
|
image: "image";
|
|
102
98
|
imageAlt: "imageAlt";
|
|
103
99
|
color: "color";
|
|
@@ -139,10 +135,12 @@ export declare const blockDefinitionSchema: z.ZodObject<{
|
|
|
139
135
|
kind: z.ZodEnum<{
|
|
140
136
|
number: "number";
|
|
141
137
|
boolean: "boolean";
|
|
138
|
+
file: "file";
|
|
142
139
|
enum: "enum";
|
|
143
140
|
text: "text";
|
|
144
141
|
richtext: "richtext";
|
|
145
142
|
url: "url";
|
|
143
|
+
link: "link";
|
|
146
144
|
image: "image";
|
|
147
145
|
imageAlt: "imageAlt";
|
|
148
146
|
color: "color";
|
|
@@ -179,10 +177,12 @@ export declare const blockDefinitionSchema: z.ZodObject<{
|
|
|
179
177
|
kind: z.ZodEnum<{
|
|
180
178
|
number: "number";
|
|
181
179
|
boolean: "boolean";
|
|
180
|
+
file: "file";
|
|
182
181
|
enum: "enum";
|
|
183
182
|
text: "text";
|
|
184
183
|
richtext: "richtext";
|
|
185
184
|
url: "url";
|
|
185
|
+
link: "link";
|
|
186
186
|
image: "image";
|
|
187
187
|
imageAlt: "imageAlt";
|
|
188
188
|
color: "color";
|
|
@@ -218,10 +218,12 @@ export declare const blockDefinitionSchema: z.ZodObject<{
|
|
|
218
218
|
kind: z.ZodEnum<{
|
|
219
219
|
number: "number";
|
|
220
220
|
boolean: "boolean";
|
|
221
|
+
file: "file";
|
|
221
222
|
enum: "enum";
|
|
222
223
|
text: "text";
|
|
223
224
|
richtext: "richtext";
|
|
224
225
|
url: "url";
|
|
226
|
+
link: "link";
|
|
225
227
|
image: "image";
|
|
226
228
|
imageAlt: "imageAlt";
|
|
227
229
|
color: "color";
|
|
@@ -269,10 +271,12 @@ export declare const blockManifestSchema: z.ZodObject<{
|
|
|
269
271
|
kind: z.ZodEnum<{
|
|
270
272
|
number: "number";
|
|
271
273
|
boolean: "boolean";
|
|
274
|
+
file: "file";
|
|
272
275
|
enum: "enum";
|
|
273
276
|
text: "text";
|
|
274
277
|
richtext: "richtext";
|
|
275
278
|
url: "url";
|
|
279
|
+
link: "link";
|
|
276
280
|
image: "image";
|
|
277
281
|
imageAlt: "imageAlt";
|
|
278
282
|
color: "color";
|
|
@@ -309,10 +313,12 @@ export declare const blockManifestSchema: z.ZodObject<{
|
|
|
309
313
|
kind: z.ZodEnum<{
|
|
310
314
|
number: "number";
|
|
311
315
|
boolean: "boolean";
|
|
316
|
+
file: "file";
|
|
312
317
|
enum: "enum";
|
|
313
318
|
text: "text";
|
|
314
319
|
richtext: "richtext";
|
|
315
320
|
url: "url";
|
|
321
|
+
link: "link";
|
|
316
322
|
image: "image";
|
|
317
323
|
imageAlt: "imageAlt";
|
|
318
324
|
color: "color";
|
|
@@ -348,10 +354,12 @@ export declare const blockManifestSchema: z.ZodObject<{
|
|
|
348
354
|
kind: z.ZodEnum<{
|
|
349
355
|
number: "number";
|
|
350
356
|
boolean: "boolean";
|
|
357
|
+
file: "file";
|
|
351
358
|
enum: "enum";
|
|
352
359
|
text: "text";
|
|
353
360
|
richtext: "richtext";
|
|
354
361
|
url: "url";
|
|
362
|
+
link: "link";
|
|
355
363
|
image: "image";
|
|
356
364
|
imageAlt: "imageAlt";
|
|
357
365
|
color: "color";
|
package/dist/block-manifest.js
CHANGED
|
@@ -23,8 +23,25 @@ export const jsonSchemaLikeSchema = z.lazy(() => z.object({
|
|
|
23
23
|
* every integration with a real prose field. `kind` and `multiline` have no
|
|
24
24
|
* JSON Schema spelling, so they have to travel beside it.
|
|
25
25
|
*/
|
|
26
|
+
/*
|
|
27
|
+
* The same list as `FieldKind`, and pinned to it.
|
|
28
|
+
*
|
|
29
|
+
* A Zod enum cannot be derived from a union type, so this is the second place
|
|
30
|
+
* the kinds are written — and it drifted the moment a kind was added: `file`
|
|
31
|
+
* went into `FieldKind`, every derivation started emitting it, and this schema
|
|
32
|
+
* went on rejecting it, which surfaced as a type error three packages away in
|
|
33
|
+
* the MCP server rather than as anything about field kinds. The
|
|
34
|
+
* `satisfies`-flavoured assignment below makes the compiler check the two lists
|
|
35
|
+
* against each other here, where the mismatch is.
|
|
36
|
+
*/
|
|
37
|
+
const FIELD_KINDS = [
|
|
38
|
+
"text", "richtext", "url", "link", "file", "image", "imageAlt",
|
|
39
|
+
"enum", "color", "number", "boolean", "headingLevel"
|
|
40
|
+
];
|
|
41
|
+
const _everyKindListed = true;
|
|
42
|
+
void _everyKindListed;
|
|
26
43
|
export const fieldMetaSchema = z.object({
|
|
27
|
-
kind: z.enum(
|
|
44
|
+
kind: z.enum(FIELD_KINDS),
|
|
28
45
|
label: z.string().optional(),
|
|
29
46
|
inlineEditable: z.boolean().optional(),
|
|
30
47
|
options: z.array(z.string()).optional(),
|
|
@@ -184,6 +201,55 @@ export function validateByJsonSchemaLike(schema, value) {
|
|
|
184
201
|
// ---------------------------------------------------------------------------
|
|
185
202
|
const IMAGE_KEY_RE = /(?:image|img)(?:Url|Src)?$|^(?:src|imageUrl|logoUrl|heroImage)$/i;
|
|
186
203
|
const IMAGE_ALT_KEY_RE = /(?:Alt)$/;
|
|
204
|
+
const LINK_KEY_RE = /(?:href|link)$/i;
|
|
205
|
+
/*
|
|
206
|
+
* Prop names that hold a document rather than a route.
|
|
207
|
+
*
|
|
208
|
+
* Deliberately narrow. `download`, `pdf`, `file`, `doc`, `attachment`,
|
|
209
|
+
* `brochure`, `datasheet` and `menu` are the words sites use, each optionally
|
|
210
|
+
* followed by Url/Href/Link/Path — so `menuPdf`, `downloadUrl` and
|
|
211
|
+
* `brochureHref` all land here, and `menuLabel` does not. A name this misses
|
|
212
|
+
* simply stays a `link`, which is where it already was; the cost of guessing
|
|
213
|
+
* wrong in the other direction is a page picker offered for a PDF.
|
|
214
|
+
*/
|
|
215
|
+
const FILE_KEY_RE = /(?:^|[a-z])(?:download|pdf|file|doc|document|attachment|brochure|datasheet|menu)(?:Url|Href|Link|Path)?$/i;
|
|
216
|
+
/*
|
|
217
|
+
* A `contentMediaType` that means "this is a file to download".
|
|
218
|
+
*
|
|
219
|
+
* Not simply "anything that is not an image": `text/html`, `text/plain` and
|
|
220
|
+
* `text/markdown` are prose a block renders inline, and treating them as
|
|
221
|
+
* documents would put a file picker on a body field. `application/json` is
|
|
222
|
+
* excluded for the same reason — a config blob is data the block reads, not
|
|
223
|
+
* something a visitor downloads. What is left is `application/*` (pdf, msword,
|
|
224
|
+
* vnd.*, zip) plus the two text types that really are files.
|
|
225
|
+
*/
|
|
226
|
+
const FILE_MEDIA_TYPE_RE = /^application\/(?!json\b)|^text\/(?:csv|calendar)\b/i;
|
|
227
|
+
/**
|
|
228
|
+
* A prop the CMS owns, which no editor may offer as a control.
|
|
229
|
+
*
|
|
230
|
+
* A leading underscore is the near-universal marker for a document's own
|
|
231
|
+
* bookkeeping — Sanity's `_key`, `_type`, `_id`, `_ref`, `_rev`; GraphQL's
|
|
232
|
+
* `__typename`. These reach a block's schema legitimately: a site that projects
|
|
233
|
+
* CMS array members has to carry `_key` through, because `_key` is the *address*
|
|
234
|
+
* a field-level publish patches by (`pageBuilder[_key=="home-hero"].heading.de`).
|
|
235
|
+
* Declaring it keeps the ops engine from stripping it off an edited item.
|
|
236
|
+
*
|
|
237
|
+
* But "the block can store it" was being read as "a person may type it". The
|
|
238
|
+
* property panel derives its controls from the schema, so `_key` rendered as an
|
|
239
|
+
* empty text box — empty because the projection hands the editor its own `id`,
|
|
240
|
+
* not the CMS key — labelled " key", sitting directly under a CTA's Label. That
|
|
241
|
+
* is not merely a stray control: the value a user typed into it would become the
|
|
242
|
+
* item's `_key`, which is the thing the publish diff addresses the array member
|
|
243
|
+
* by. Editing it re-points or orphans the patch, and the same derivation feeds
|
|
244
|
+
* the planner's block contracts, so the model was being shown `_key` as a prop
|
|
245
|
+
* it could set.
|
|
246
|
+
*
|
|
247
|
+
* Underscore-prefixed props are therefore stored, projected and published as
|
|
248
|
+
* before, and never editable by anyone.
|
|
249
|
+
*/
|
|
250
|
+
function isInternalPropKey(key) {
|
|
251
|
+
return key.startsWith("_");
|
|
252
|
+
}
|
|
187
253
|
function labelFromKey(key) {
|
|
188
254
|
// ctaText → "CTA text", imageUrl → "Image", logoAlt → "Logo alt",
|
|
189
255
|
// background_image → "Background image", video_url → "Video", src → "Src"
|
|
@@ -224,14 +290,59 @@ function inferFieldKind(key, schema) {
|
|
|
224
290
|
return "imageAlt";
|
|
225
291
|
if (IMAGE_KEY_RE.test(key))
|
|
226
292
|
return "image";
|
|
293
|
+
/*
|
|
294
|
+
* A document prop, named as one. `menuPdf`, `brochureFile`, `datasheetDoc`,
|
|
295
|
+
* `downloadUrl` — all of them hold a path to a file rather than a route, and
|
|
296
|
+
* inferring them as `link` puts them in front of the page picker and the
|
|
297
|
+
* dead-route warning, which are both the wrong question. Checked before the
|
|
298
|
+
* link pattern precisely because `downloadUrl` and `fileHref` match both.
|
|
299
|
+
*/
|
|
300
|
+
if (FILE_KEY_RE.test(key))
|
|
301
|
+
return "file";
|
|
302
|
+
/*
|
|
303
|
+
* A schema can also say so outright, and a `contentMediaType` of anything but
|
|
304
|
+
* an image means a document — the same standard vocabulary the markdown case
|
|
305
|
+
* above uses, so an integrator needs no proprietary annotation to get the
|
|
306
|
+
* right control.
|
|
307
|
+
*/
|
|
308
|
+
if (typeof schema.contentMediaType === "string" && FILE_MEDIA_TYPE_RE.test(schema.contentMediaType)) {
|
|
309
|
+
return "file";
|
|
310
|
+
}
|
|
311
|
+
/*
|
|
312
|
+
* A custom block's `ctaHref` is a link, and inferring it as plain text gave
|
|
313
|
+
* an integrator's editors a bare box where the built-in blocks get a page
|
|
314
|
+
* picker. Checked after the image patterns so `imageUrl` and `logoUrl` still
|
|
315
|
+
* resolve to images — those end in "Url" too.
|
|
316
|
+
*/
|
|
317
|
+
if (LINK_KEY_RE.test(key))
|
|
318
|
+
return "link";
|
|
227
319
|
return "text";
|
|
228
320
|
}
|
|
321
|
+
/**
|
|
322
|
+
* A list item's `id` is identity, not content.
|
|
323
|
+
*
|
|
324
|
+
* The same argument as `_key` above, one convention over: every one of this
|
|
325
|
+
* package's own list blocks carries an optional `id` on each item, used as the
|
|
326
|
+
* React key and as the handle an operation addresses the item by. Derived from
|
|
327
|
+
* the schema it looks like any other optional string, so the panel drew a text
|
|
328
|
+
* box for it and the planner was shown it as a prop it could set — and a value
|
|
329
|
+
* typed there re-points or orphans the very patch that would save it.
|
|
330
|
+
*
|
|
331
|
+
* Scoped to list items deliberately. A top-level `id` prop on a block is a
|
|
332
|
+
* perfectly ordinary content field (an anchor, a slug, an embed's video id) and
|
|
333
|
+
* is left alone.
|
|
334
|
+
*/
|
|
335
|
+
function isItemIdentityKey(key) {
|
|
336
|
+
return key === "id";
|
|
337
|
+
}
|
|
229
338
|
function deriveItemFields(itemSchema) {
|
|
230
339
|
const fields = {};
|
|
231
340
|
const props = isObject(itemSchema.properties) ? itemSchema.properties : {};
|
|
232
341
|
for (const [key, propSchema] of Object.entries(props)) {
|
|
233
342
|
if (!isObject(propSchema))
|
|
234
343
|
continue;
|
|
344
|
+
if (isInternalPropKey(key) || isItemIdentityKey(key))
|
|
345
|
+
continue;
|
|
235
346
|
// Skip opaque object/array props (they'd render as "[object Object]"), but
|
|
236
347
|
// keep richtext docs — those self-identify and get the richtext editor.
|
|
237
348
|
const t = typeof propSchema.type === "string" ? propSchema.type : undefined;
|
|
@@ -325,6 +436,8 @@ export function deriveFieldMetaFromSchema(propsSchema) {
|
|
|
325
436
|
for (const [key, propSchema] of Object.entries(props)) {
|
|
326
437
|
if (!isObject(propSchema))
|
|
327
438
|
continue;
|
|
439
|
+
if (isInternalPropKey(key))
|
|
440
|
+
continue;
|
|
328
441
|
const type = typeof propSchema.type === "string" ? propSchema.type : undefined;
|
|
329
442
|
// Array of a discriminated union (oneOf/anyOf of object branches) →
|
|
330
443
|
// polymorphic list field: per-branch fields keyed by the discriminant value.
|
|
@@ -3,8 +3,29 @@ export declare const f: {
|
|
|
3
3
|
readonly text: (label?: string) => FieldMeta;
|
|
4
4
|
readonly longtext: (label?: string) => FieldMeta;
|
|
5
5
|
readonly richtext: (label?: string) => FieldMeta;
|
|
6
|
+
/** A resource address — an iframe src, a video file. Never an internal page. */
|
|
6
7
|
readonly url: (label?: string) => FieldMeta;
|
|
8
|
+
/** A navigation target: an internal page, or an external URL / mailto / tel / anchor. */
|
|
9
|
+
readonly link: (label?: string) => FieldMeta;
|
|
10
|
+
/**
|
|
11
|
+
* The "open in new tab" companion for a link prop. Named by convention
|
|
12
|
+
* (`newTabKeyFor`) so the property panel renders it inside the link control
|
|
13
|
+
* rather than as a row of its own — the same pairing as `image`/`imageAlt`.
|
|
14
|
+
*/
|
|
15
|
+
readonly newTab: (label?: string) => FieldMeta;
|
|
7
16
|
readonly image: (label?: string, imageSpec?: ImageSpec) => FieldMeta;
|
|
17
|
+
/**
|
|
18
|
+
* A single emoji or symbol glyph, or the URL of a small image.
|
|
19
|
+
*
|
|
20
|
+
* Text in the panel, and deliberately *not* editable on the page. Whatever a
|
|
21
|
+
* person types into an icon goes through `renderBlockIcon`, which draws only a
|
|
22
|
+
* glyph or an image and renders **nothing** for anything else — so an inline
|
|
23
|
+
* edit that typed a word ("rocket", the name the planner reaches for) would
|
|
24
|
+
* make the icon silently disappear with the value stored and invisible. The
|
|
25
|
+
* panel's labelled box is the affordance that fits a value from a constrained
|
|
26
|
+
* vocabulary; a cursor blinking inside the emoji is not.
|
|
27
|
+
*/
|
|
28
|
+
readonly icon: (label?: string) => FieldMeta;
|
|
8
29
|
readonly imageAlt: (label?: string) => FieldMeta;
|
|
9
30
|
readonly headingLevel: () => FieldMeta;
|
|
10
31
|
};
|
package/dist/blocks/_helpers.js
CHANGED
|
@@ -2,8 +2,29 @@ export const f = {
|
|
|
2
2
|
text: (label) => ({ kind: "text", label }),
|
|
3
3
|
longtext: (label) => ({ kind: "text", label, multiline: true }),
|
|
4
4
|
richtext: (label) => ({ kind: "richtext", label }),
|
|
5
|
+
/** A resource address — an iframe src, a video file. Never an internal page. */
|
|
5
6
|
url: (label) => ({ kind: "url", label, inlineEditable: false }),
|
|
7
|
+
/** A navigation target: an internal page, or an external URL / mailto / tel / anchor. */
|
|
8
|
+
link: (label) => ({ kind: "link", label, inlineEditable: false }),
|
|
9
|
+
/**
|
|
10
|
+
* The "open in new tab" companion for a link prop. Named by convention
|
|
11
|
+
* (`newTabKeyFor`) so the property panel renders it inside the link control
|
|
12
|
+
* rather than as a row of its own — the same pairing as `image`/`imageAlt`.
|
|
13
|
+
*/
|
|
14
|
+
newTab: (label = "Open in new tab") => ({ kind: "boolean", label, inlineEditable: false }),
|
|
6
15
|
image: (label, imageSpec) => ({ kind: "image", label, inlineEditable: false, ...(imageSpec ? { imageSpec } : {}) }),
|
|
16
|
+
/**
|
|
17
|
+
* A single emoji or symbol glyph, or the URL of a small image.
|
|
18
|
+
*
|
|
19
|
+
* Text in the panel, and deliberately *not* editable on the page. Whatever a
|
|
20
|
+
* person types into an icon goes through `renderBlockIcon`, which draws only a
|
|
21
|
+
* glyph or an image and renders **nothing** for anything else — so an inline
|
|
22
|
+
* edit that typed a word ("rocket", the name the planner reaches for) would
|
|
23
|
+
* make the icon silently disappear with the value stored and invisible. The
|
|
24
|
+
* panel's labelled box is the affordance that fits a value from a constrained
|
|
25
|
+
* vocabulary; a cursor blinking inside the emoji is not.
|
|
26
|
+
*/
|
|
27
|
+
icon: (label = "Icon (single emoji)") => ({ kind: "text", label, inlineEditable: false }),
|
|
7
28
|
imageAlt: (label) => ({ kind: "imageAlt", label }),
|
|
8
29
|
headingLevel: () => ({ kind: "headingLevel", label: "Heading type", inlineEditable: false }),
|
|
9
30
|
};
|
|
@@ -1,8 +1,25 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
export declare const IMAGE_PLACEHOLDER = "/hero-generated.svg";
|
|
3
3
|
export declare function isImagePlaceholder(url: string | undefined | null): boolean;
|
|
4
|
-
/**
|
|
5
|
-
|
|
4
|
+
/**
|
|
5
|
+
* Semantic kind for a block prop field.
|
|
6
|
+
*
|
|
7
|
+
* `link` and `url` are both href-valued strings and deliberately distinct.
|
|
8
|
+
* `link` is a *navigation target* — it may address a page on this site, so it
|
|
9
|
+
* gets the page picker, the dead-route warning, and the rename rewriter.
|
|
10
|
+
* `url` is a *resource address* — an embed's iframe src, a video file, an OG
|
|
11
|
+
* image — which is never an internal page, and where offering a list of pages
|
|
12
|
+
* would be noise.
|
|
13
|
+
*
|
|
14
|
+
* `file` is a *document*: a menu PDF, a price list, a consent form. It is a
|
|
15
|
+
* third thing and not a flavour of either, because the question you ask of it
|
|
16
|
+
* is different. A `link` is checked against the site's pages; a `file` must not
|
|
17
|
+
* be, or every document on the site reports as a dead link. A `url` is checked
|
|
18
|
+
* against nothing at all; a `file` can be checked against what the site's asset
|
|
19
|
+
* store actually holds, which is the only way anyone finds out that a menu link
|
|
20
|
+
* has been pointing at a filename with a typo in it since August.
|
|
21
|
+
*/
|
|
22
|
+
export type FieldKind = "text" | "richtext" | "url" | "link" | "file" | "image" | "imageAlt" | "enum" | "color" | "number" | "boolean" | "headingLevel";
|
|
6
23
|
/** Recommended image dimensions for an image field. */
|
|
7
24
|
export type ImageSpec = {
|
|
8
25
|
aspectRatio: "landscape" | "square" | "portrait";
|
|
@@ -104,10 +121,52 @@ export declare function registerBlock(type: string, config: BlockRegistration):
|
|
|
104
121
|
export declare function getBlockMeta(type: string): BlockMeta | undefined;
|
|
105
122
|
/** Get all registered block metadata. */
|
|
106
123
|
export declare function getAllBlockMeta(): Readonly<Record<string, BlockMeta>>;
|
|
124
|
+
/**
|
|
125
|
+
* Does this block type declare this prop?
|
|
126
|
+
*
|
|
127
|
+
* The only honest answer to "is `heading` a real prop" is the one the block's
|
|
128
|
+
* own schema gives, and it is the question two very different callers need: the
|
|
129
|
+
* plan normalizer, deciding whether a prop name the model emitted needs
|
|
130
|
+
* repairing, and the planner prompt, deciding whether a prop-name correction is
|
|
131
|
+
* even true of this site's catalogue. Both used to answer it by comparing the
|
|
132
|
+
* block type against a literal from Avocado's own catalogue, which is a fact
|
|
133
|
+
* about our names rather than about the site's schema — so a site that brings
|
|
134
|
+
* its own blocks got a confident wrong answer. Asking here means they cannot
|
|
135
|
+
* drift apart again.
|
|
136
|
+
*
|
|
137
|
+
* Manifest-registered blocks may carry a schema richer than their derived meta,
|
|
138
|
+
* so the schema gets the second look rather than the first refusal. An
|
|
139
|
+
* unregistered type answers `false`, which callers must read as "no evidence",
|
|
140
|
+
* not as "no".
|
|
141
|
+
*/
|
|
142
|
+
export declare function blockAcceptsProp(blockType: string, prop: string): boolean;
|
|
143
|
+
/**
|
|
144
|
+
* Does this block type's list `listKey` declare an item field named `itemKey`?
|
|
145
|
+
* The list-shaped counterpart of `blockAcceptsProp`, with the same contract: a
|
|
146
|
+
* block with no declared list metadata answers `false` for every key, meaning
|
|
147
|
+
* "no evidence".
|
|
148
|
+
*/
|
|
149
|
+
export declare function blockListItemAcceptsKey(blockType: string, listKey: string, itemKey: string): boolean;
|
|
107
150
|
/** Get the set of prop keys that are image fields for a block type. */
|
|
108
151
|
export declare function getImageFields(blockType: string): Set<string>;
|
|
109
152
|
/** Get list props that contain image fields: Map<listKey, Set<imageFieldKey>>. */
|
|
110
153
|
export declare function getListImageFields(blockType: string): Map<string, Set<string>>;
|
|
154
|
+
/**
|
|
155
|
+
* Every prop key on a block type that addresses a *resource* rather than a page
|
|
156
|
+
* — images, iframe sources, video files — at any depth, scalar and list-item
|
|
157
|
+
* props flattened into one set.
|
|
158
|
+
*
|
|
159
|
+
* Renaming a page rewrites the links that point at it, and the rewriter cannot
|
|
160
|
+
* tell a link from any other string: it walks the props and remaps every value
|
|
161
|
+
* that starts with the old slug. That silently rewrote `/pricing/demo.mp4` into
|
|
162
|
+
* `/plans/demo.mp4` on a rename — an asset path is not a route, and renaming a
|
|
163
|
+
* page does not move the file. This set is what the walker skips.
|
|
164
|
+
*
|
|
165
|
+
* It is an exclusion rather than a `kind: "link"` allow-list on purpose: a
|
|
166
|
+
* custom block that ships no field metadata still has its links rewritten, the
|
|
167
|
+
* way it always did.
|
|
168
|
+
*/
|
|
169
|
+
export declare function getMediaFields(blockType: string): Set<string>;
|
|
111
170
|
/** Check if a block type is a chrome block (structurally pinned). */
|
|
112
171
|
export declare function isChrome(type: string): boolean;
|
|
113
172
|
/** Get all registered chrome block type names. */
|
package/dist/blocks/_registry.js
CHANGED
|
@@ -141,6 +141,45 @@ export function getBlockMeta(type) {
|
|
|
141
141
|
export function getAllBlockMeta() {
|
|
142
142
|
return _blockMeta;
|
|
143
143
|
}
|
|
144
|
+
/**
|
|
145
|
+
* Does this block type declare this prop?
|
|
146
|
+
*
|
|
147
|
+
* The only honest answer to "is `heading` a real prop" is the one the block's
|
|
148
|
+
* own schema gives, and it is the question two very different callers need: the
|
|
149
|
+
* plan normalizer, deciding whether a prop name the model emitted needs
|
|
150
|
+
* repairing, and the planner prompt, deciding whether a prop-name correction is
|
|
151
|
+
* even true of this site's catalogue. Both used to answer it by comparing the
|
|
152
|
+
* block type against a literal from Avocado's own catalogue, which is a fact
|
|
153
|
+
* about our names rather than about the site's schema — so a site that brings
|
|
154
|
+
* its own blocks got a confident wrong answer. Asking here means they cannot
|
|
155
|
+
* drift apart again.
|
|
156
|
+
*
|
|
157
|
+
* Manifest-registered blocks may carry a schema richer than their derived meta,
|
|
158
|
+
* so the schema gets the second look rather than the first refusal. An
|
|
159
|
+
* unregistered type answers `false`, which callers must read as "no evidence",
|
|
160
|
+
* not as "no".
|
|
161
|
+
*/
|
|
162
|
+
export function blockAcceptsProp(blockType, prop) {
|
|
163
|
+
if (!blockType || !prop)
|
|
164
|
+
return false;
|
|
165
|
+
const meta = _blockMeta[blockType];
|
|
166
|
+
if (meta?.fields && prop in meta.fields)
|
|
167
|
+
return true;
|
|
168
|
+
const shape = _blockSchemas[blockType]?.shape;
|
|
169
|
+
return Boolean(shape && prop in shape);
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Does this block type's list `listKey` declare an item field named `itemKey`?
|
|
173
|
+
* The list-shaped counterpart of `blockAcceptsProp`, with the same contract: a
|
|
174
|
+
* block with no declared list metadata answers `false` for every key, meaning
|
|
175
|
+
* "no evidence".
|
|
176
|
+
*/
|
|
177
|
+
export function blockListItemAcceptsKey(blockType, listKey, itemKey) {
|
|
178
|
+
if (!blockType || !listKey || !itemKey)
|
|
179
|
+
return false;
|
|
180
|
+
const itemFields = _blockMeta[blockType]?.listFields?.[listKey]?.itemFields;
|
|
181
|
+
return Boolean(itemFields && itemKey in itemFields);
|
|
182
|
+
}
|
|
144
183
|
/** Cache for getImageFields results. */
|
|
145
184
|
const _imageFieldsCache = new Map();
|
|
146
185
|
/** Get the set of prop keys that are image fields for a block type. */
|
|
@@ -182,6 +221,46 @@ export function getListImageFields(blockType) {
|
|
|
182
221
|
_listImageFieldsCache.set(blockType, result);
|
|
183
222
|
return result;
|
|
184
223
|
}
|
|
224
|
+
/** Cache for getMediaFields results. */
|
|
225
|
+
const _mediaFieldsCache = new Map();
|
|
226
|
+
/**
|
|
227
|
+
* Every prop key on a block type that addresses a *resource* rather than a page
|
|
228
|
+
* — images, iframe sources, video files — at any depth, scalar and list-item
|
|
229
|
+
* props flattened into one set.
|
|
230
|
+
*
|
|
231
|
+
* Renaming a page rewrites the links that point at it, and the rewriter cannot
|
|
232
|
+
* tell a link from any other string: it walks the props and remaps every value
|
|
233
|
+
* that starts with the old slug. That silently rewrote `/pricing/demo.mp4` into
|
|
234
|
+
* `/plans/demo.mp4` on a rename — an asset path is not a route, and renaming a
|
|
235
|
+
* page does not move the file. This set is what the walker skips.
|
|
236
|
+
*
|
|
237
|
+
* It is an exclusion rather than a `kind: "link"` allow-list on purpose: a
|
|
238
|
+
* custom block that ships no field metadata still has its links rewritten, the
|
|
239
|
+
* way it always did.
|
|
240
|
+
*/
|
|
241
|
+
export function getMediaFields(blockType) {
|
|
242
|
+
const cached = _mediaFieldsCache.get(blockType);
|
|
243
|
+
if (cached)
|
|
244
|
+
return cached;
|
|
245
|
+
const meta = _blockMeta[blockType];
|
|
246
|
+
const result = new Set();
|
|
247
|
+
const collect = (fields) => {
|
|
248
|
+
for (const [key, fm] of Object.entries(fields)) {
|
|
249
|
+
if (fm.kind === "image" || fm.kind === "url")
|
|
250
|
+
result.add(key);
|
|
251
|
+
}
|
|
252
|
+
};
|
|
253
|
+
if (meta) {
|
|
254
|
+
collect(meta.fields);
|
|
255
|
+
for (const listMeta of Object.values(meta.listFields ?? {})) {
|
|
256
|
+
collect(listMeta.itemFields);
|
|
257
|
+
for (const branch of Object.values(listMeta.itemFieldsByType ?? {}))
|
|
258
|
+
collect(branch);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
_mediaFieldsCache.set(blockType, result);
|
|
262
|
+
return result;
|
|
263
|
+
}
|
|
185
264
|
/** Check if a block type is a chrome block (structurally pinned). */
|
|
186
265
|
export function isChrome(type) {
|
|
187
266
|
return _blockMeta[type]?.chrome === true;
|
|
@@ -332,7 +411,7 @@ function defaultScalarForField(field, fieldKey) {
|
|
|
332
411
|
const label = field.label?.trim() || fieldKey;
|
|
333
412
|
if (field.kind === "text" || field.kind === "richtext" || field.kind === "imageAlt")
|
|
334
413
|
return `New ${label}`;
|
|
335
|
-
if (field.kind === "url")
|
|
414
|
+
if (field.kind === "url" || field.kind === "link")
|
|
336
415
|
return "/";
|
|
337
416
|
if (field.kind === "image")
|
|
338
417
|
return IMAGE_PLACEHOLDER;
|
|
@@ -340,6 +419,14 @@ function defaultScalarForField(field, fieldKey) {
|
|
|
340
419
|
return "#0f766e";
|
|
341
420
|
if (field.kind === "number")
|
|
342
421
|
return 0;
|
|
422
|
+
/*
|
|
423
|
+
* Without this a boolean item field seeded a *string* ("New Open in new tab")
|
|
424
|
+
* into a new list item, which the block's own zod schema then rejected — the
|
|
425
|
+
* add-item op failed on a value this function produced. Latent until link
|
|
426
|
+
* fields introduced the first boolean inside a list.
|
|
427
|
+
*/
|
|
428
|
+
if (field.kind === "boolean")
|
|
429
|
+
return false;
|
|
343
430
|
if (field.kind === "enum")
|
|
344
431
|
return Array.isArray(field.options) && field.options.length > 0 ? field.options[0] : "";
|
|
345
432
|
return `New ${label}`;
|
package/dist/blocks/banner.js
CHANGED
|
@@ -7,6 +7,7 @@ registerBlock("Banner", {
|
|
|
7
7
|
variant: z.enum(["info", "success", "warning"]).default("info").catch("info"),
|
|
8
8
|
ctaText: z.string().optional(),
|
|
9
9
|
ctaHref: z.string().optional(),
|
|
10
|
+
ctaNewTab: z.boolean().optional(),
|
|
10
11
|
backgroundColor: z.string().optional(),
|
|
11
12
|
textColor: z.string().optional(),
|
|
12
13
|
}),
|
|
@@ -18,7 +19,8 @@ registerBlock("Banner", {
|
|
|
18
19
|
text: f.text("Banner text"),
|
|
19
20
|
variant: { kind: "enum", label: "Variant", options: ["info", "success", "warning"], inlineEditable: false },
|
|
20
21
|
ctaText: f.text("Button label"),
|
|
21
|
-
ctaHref: f.
|
|
22
|
+
ctaHref: f.link("Button link"),
|
|
23
|
+
ctaNewTab: f.newTab(),
|
|
22
24
|
backgroundColor: { kind: "color", label: "Background color", inlineEditable: false },
|
|
23
25
|
textColor: { kind: "color", label: "Text color", inlineEditable: false },
|
|
24
26
|
},
|
package/dist/blocks/card-grid.js
CHANGED
|
@@ -14,6 +14,7 @@ registerBlock("CardGrid", {
|
|
|
14
14
|
description: z.string().min(1),
|
|
15
15
|
ctaText: z.string().min(1),
|
|
16
16
|
ctaHref: z.string().min(1),
|
|
17
|
+
ctaNewTab: z.boolean().optional(),
|
|
17
18
|
imageUrl: z.string().min(1).optional(),
|
|
18
19
|
imageAlt: z.string().min(1).optional()
|
|
19
20
|
}))
|
|
@@ -37,7 +38,8 @@ registerBlock("CardGrid", {
|
|
|
37
38
|
title: f.text("Card title"),
|
|
38
39
|
description: f.longtext("Card description"),
|
|
39
40
|
ctaText: f.text("Button text"),
|
|
40
|
-
ctaHref: f.
|
|
41
|
+
ctaHref: f.link("Button link"),
|
|
42
|
+
ctaNewTab: f.newTab(),
|
|
41
43
|
imageUrl: f.image("Card image", { aspectRatio: "landscape", width: 768, height: 512 }),
|
|
42
44
|
imageAlt: f.imageAlt("Card image alt text"),
|
|
43
45
|
}
|
package/dist/blocks/card.js
CHANGED
|
@@ -7,6 +7,7 @@ registerBlock("Card", {
|
|
|
7
7
|
description: z.string().min(1),
|
|
8
8
|
ctaText: z.string().min(1),
|
|
9
9
|
ctaHref: z.string().min(1),
|
|
10
|
+
ctaNewTab: z.boolean().optional(),
|
|
10
11
|
imageUrl: z.string().min(1).optional(),
|
|
11
12
|
imageAlt: z.string().min(1).optional(),
|
|
12
13
|
variant: z.enum(["default", "full-bleed"]).default("default").catch("default"),
|
|
@@ -19,7 +20,8 @@ registerBlock("Card", {
|
|
|
19
20
|
title: f.text("Card title"),
|
|
20
21
|
description: f.longtext("Card description"),
|
|
21
22
|
ctaText: f.text("Button text"),
|
|
22
|
-
ctaHref: f.
|
|
23
|
+
ctaHref: f.link("Button link"),
|
|
24
|
+
ctaNewTab: f.newTab(),
|
|
23
25
|
imageUrl: f.image("Card image", { aspectRatio: "landscape", width: 768, height: 512 }),
|
|
24
26
|
imageAlt: f.imageAlt("Card image alt text"),
|
|
25
27
|
variant: { kind: "enum", label: "Variant", options: ["default", "full-bleed"], inlineEditable: false },
|
package/dist/blocks/carousel.js
CHANGED
|
@@ -11,6 +11,7 @@ registerBlock("Carousel", {
|
|
|
11
11
|
description: z.string().optional(),
|
|
12
12
|
ctaText: z.string().optional(),
|
|
13
13
|
ctaHref: z.string().optional(),
|
|
14
|
+
ctaNewTab: z.boolean().optional(),
|
|
14
15
|
})).min(1),
|
|
15
16
|
autoplay: z.enum(["true", "false"]).default("false").catch("false"),
|
|
16
17
|
interval: z.number().optional(),
|
|
@@ -32,7 +33,8 @@ registerBlock("Carousel", {
|
|
|
32
33
|
heading: f.text("Heading"),
|
|
33
34
|
description: f.longtext("Description"),
|
|
34
35
|
ctaText: f.text("Button label"),
|
|
35
|
-
ctaHref: f.
|
|
36
|
+
ctaHref: f.link("Button link"),
|
|
37
|
+
ctaNewTab: f.newTab(),
|
|
36
38
|
}
|
|
37
39
|
}
|
|
38
40
|
}
|