@avocadostudio-ai/shared 0.5.0 → 0.6.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/block-manifest.d.ts +41 -1
- package/dist/block-manifest.js +67 -5
- package/dist/blocks/_registry.d.ts +81 -1
- package/dist/blocks/_registry.js +152 -8
- package/dist/blocks/banner.js +2 -2
- package/dist/blocks/card-grid.js +2 -2
- package/dist/blocks/card.js +2 -2
- package/dist/blocks/carousel.js +2 -2
- package/dist/blocks/cta.js +2 -2
- package/dist/blocks/embed.js +2 -2
- package/dist/blocks/faq-accordion.js +2 -2
- package/dist/blocks/feature-grid.js +2 -2
- package/dist/blocks/footer.js +2 -2
- package/dist/blocks/gallery.js +2 -2
- package/dist/blocks/hero.js +2 -2
- package/dist/blocks/quote.js +2 -2
- package/dist/blocks/rich-text.js +2 -2
- package/dist/blocks/site-header.js +2 -2
- package/dist/blocks/stats.js +2 -2
- package/dist/blocks/table.js +2 -2
- package/dist/blocks/tabs.js +2 -2
- package/dist/blocks/testimonials.js +2 -2
- package/dist/blocks/two-column.js +2 -2
- package/dist/blocks/video.js +2 -2
- package/dist/editable-coverage.js +70 -7
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/ops/builders.d.ts +34 -0
- package/dist/ops/builders.js +63 -0
- package/dist/panel-coverage.d.ts +19 -4
- package/dist/panel-coverage.js +0 -0
- package/package.json +2 -2
package/dist/block-manifest.d.ts
CHANGED
|
@@ -15,6 +15,7 @@ export declare const fieldMetaSchema: z.ZodObject<{
|
|
|
15
15
|
imageAlt: "imageAlt";
|
|
16
16
|
color: "color";
|
|
17
17
|
headingLevel: "headingLevel";
|
|
18
|
+
reference: "reference";
|
|
18
19
|
}>;
|
|
19
20
|
label: z.ZodOptional<z.ZodString>;
|
|
20
21
|
inlineEditable: z.ZodOptional<z.ZodBoolean>;
|
|
@@ -34,6 +35,9 @@ export declare const fieldMetaSchema: z.ZodObject<{
|
|
|
34
35
|
}>>;
|
|
35
36
|
}, z.core.$strip>>;
|
|
36
37
|
multiline: z.ZodOptional<z.ZodBoolean>;
|
|
38
|
+
referenceLabelKey: z.ZodOptional<z.ZodString>;
|
|
39
|
+
panelOnly: z.ZodOptional<z.ZodBoolean>;
|
|
40
|
+
internal: z.ZodOptional<z.ZodBoolean>;
|
|
37
41
|
inline: z.ZodOptional<z.ZodBoolean>;
|
|
38
42
|
decorators: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
39
43
|
name: z.ZodString;
|
|
@@ -57,6 +61,7 @@ export declare const listFieldMetaSchema: z.ZodObject<{
|
|
|
57
61
|
imageAlt: "imageAlt";
|
|
58
62
|
color: "color";
|
|
59
63
|
headingLevel: "headingLevel";
|
|
64
|
+
reference: "reference";
|
|
60
65
|
}>;
|
|
61
66
|
label: z.ZodOptional<z.ZodString>;
|
|
62
67
|
inlineEditable: z.ZodOptional<z.ZodBoolean>;
|
|
@@ -76,6 +81,9 @@ export declare const listFieldMetaSchema: z.ZodObject<{
|
|
|
76
81
|
}>>;
|
|
77
82
|
}, z.core.$strip>>;
|
|
78
83
|
multiline: z.ZodOptional<z.ZodBoolean>;
|
|
84
|
+
referenceLabelKey: z.ZodOptional<z.ZodString>;
|
|
85
|
+
panelOnly: z.ZodOptional<z.ZodBoolean>;
|
|
86
|
+
internal: z.ZodOptional<z.ZodBoolean>;
|
|
79
87
|
inline: z.ZodOptional<z.ZodBoolean>;
|
|
80
88
|
decorators: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
81
89
|
name: z.ZodString;
|
|
@@ -98,6 +106,7 @@ export declare const listFieldMetaSchema: z.ZodObject<{
|
|
|
98
106
|
imageAlt: "imageAlt";
|
|
99
107
|
color: "color";
|
|
100
108
|
headingLevel: "headingLevel";
|
|
109
|
+
reference: "reference";
|
|
101
110
|
}>;
|
|
102
111
|
label: z.ZodOptional<z.ZodString>;
|
|
103
112
|
inlineEditable: z.ZodOptional<z.ZodBoolean>;
|
|
@@ -117,6 +126,9 @@ export declare const listFieldMetaSchema: z.ZodObject<{
|
|
|
117
126
|
}>>;
|
|
118
127
|
}, z.core.$strip>>;
|
|
119
128
|
multiline: z.ZodOptional<z.ZodBoolean>;
|
|
129
|
+
referenceLabelKey: z.ZodOptional<z.ZodString>;
|
|
130
|
+
panelOnly: z.ZodOptional<z.ZodBoolean>;
|
|
131
|
+
internal: z.ZodOptional<z.ZodBoolean>;
|
|
120
132
|
inline: z.ZodOptional<z.ZodBoolean>;
|
|
121
133
|
decorators: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
122
134
|
name: z.ZodString;
|
|
@@ -145,6 +157,7 @@ export declare const blockDefinitionSchema: z.ZodObject<{
|
|
|
145
157
|
imageAlt: "imageAlt";
|
|
146
158
|
color: "color";
|
|
147
159
|
headingLevel: "headingLevel";
|
|
160
|
+
reference: "reference";
|
|
148
161
|
}>;
|
|
149
162
|
label: z.ZodOptional<z.ZodString>;
|
|
150
163
|
inlineEditable: z.ZodOptional<z.ZodBoolean>;
|
|
@@ -164,6 +177,9 @@ export declare const blockDefinitionSchema: z.ZodObject<{
|
|
|
164
177
|
}>>;
|
|
165
178
|
}, z.core.$strip>>;
|
|
166
179
|
multiline: z.ZodOptional<z.ZodBoolean>;
|
|
180
|
+
referenceLabelKey: z.ZodOptional<z.ZodString>;
|
|
181
|
+
panelOnly: z.ZodOptional<z.ZodBoolean>;
|
|
182
|
+
internal: z.ZodOptional<z.ZodBoolean>;
|
|
167
183
|
inline: z.ZodOptional<z.ZodBoolean>;
|
|
168
184
|
decorators: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
169
185
|
name: z.ZodString;
|
|
@@ -187,6 +203,7 @@ export declare const blockDefinitionSchema: z.ZodObject<{
|
|
|
187
203
|
imageAlt: "imageAlt";
|
|
188
204
|
color: "color";
|
|
189
205
|
headingLevel: "headingLevel";
|
|
206
|
+
reference: "reference";
|
|
190
207
|
}>;
|
|
191
208
|
label: z.ZodOptional<z.ZodString>;
|
|
192
209
|
inlineEditable: z.ZodOptional<z.ZodBoolean>;
|
|
@@ -206,6 +223,9 @@ export declare const blockDefinitionSchema: z.ZodObject<{
|
|
|
206
223
|
}>>;
|
|
207
224
|
}, z.core.$strip>>;
|
|
208
225
|
multiline: z.ZodOptional<z.ZodBoolean>;
|
|
226
|
+
referenceLabelKey: z.ZodOptional<z.ZodString>;
|
|
227
|
+
panelOnly: z.ZodOptional<z.ZodBoolean>;
|
|
228
|
+
internal: z.ZodOptional<z.ZodBoolean>;
|
|
209
229
|
inline: z.ZodOptional<z.ZodBoolean>;
|
|
210
230
|
decorators: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
211
231
|
name: z.ZodString;
|
|
@@ -228,6 +248,7 @@ export declare const blockDefinitionSchema: z.ZodObject<{
|
|
|
228
248
|
imageAlt: "imageAlt";
|
|
229
249
|
color: "color";
|
|
230
250
|
headingLevel: "headingLevel";
|
|
251
|
+
reference: "reference";
|
|
231
252
|
}>;
|
|
232
253
|
label: z.ZodOptional<z.ZodString>;
|
|
233
254
|
inlineEditable: z.ZodOptional<z.ZodBoolean>;
|
|
@@ -247,6 +268,9 @@ export declare const blockDefinitionSchema: z.ZodObject<{
|
|
|
247
268
|
}>>;
|
|
248
269
|
}, z.core.$strip>>;
|
|
249
270
|
multiline: z.ZodOptional<z.ZodBoolean>;
|
|
271
|
+
referenceLabelKey: z.ZodOptional<z.ZodString>;
|
|
272
|
+
panelOnly: z.ZodOptional<z.ZodBoolean>;
|
|
273
|
+
internal: z.ZodOptional<z.ZodBoolean>;
|
|
250
274
|
inline: z.ZodOptional<z.ZodBoolean>;
|
|
251
275
|
decorators: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
252
276
|
name: z.ZodString;
|
|
@@ -281,6 +305,7 @@ export declare const blockManifestSchema: z.ZodObject<{
|
|
|
281
305
|
imageAlt: "imageAlt";
|
|
282
306
|
color: "color";
|
|
283
307
|
headingLevel: "headingLevel";
|
|
308
|
+
reference: "reference";
|
|
284
309
|
}>;
|
|
285
310
|
label: z.ZodOptional<z.ZodString>;
|
|
286
311
|
inlineEditable: z.ZodOptional<z.ZodBoolean>;
|
|
@@ -300,6 +325,9 @@ export declare const blockManifestSchema: z.ZodObject<{
|
|
|
300
325
|
}>>;
|
|
301
326
|
}, z.core.$strip>>;
|
|
302
327
|
multiline: z.ZodOptional<z.ZodBoolean>;
|
|
328
|
+
referenceLabelKey: z.ZodOptional<z.ZodString>;
|
|
329
|
+
panelOnly: z.ZodOptional<z.ZodBoolean>;
|
|
330
|
+
internal: z.ZodOptional<z.ZodBoolean>;
|
|
303
331
|
inline: z.ZodOptional<z.ZodBoolean>;
|
|
304
332
|
decorators: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
305
333
|
name: z.ZodString;
|
|
@@ -323,6 +351,7 @@ export declare const blockManifestSchema: z.ZodObject<{
|
|
|
323
351
|
imageAlt: "imageAlt";
|
|
324
352
|
color: "color";
|
|
325
353
|
headingLevel: "headingLevel";
|
|
354
|
+
reference: "reference";
|
|
326
355
|
}>;
|
|
327
356
|
label: z.ZodOptional<z.ZodString>;
|
|
328
357
|
inlineEditable: z.ZodOptional<z.ZodBoolean>;
|
|
@@ -342,6 +371,9 @@ export declare const blockManifestSchema: z.ZodObject<{
|
|
|
342
371
|
}>>;
|
|
343
372
|
}, z.core.$strip>>;
|
|
344
373
|
multiline: z.ZodOptional<z.ZodBoolean>;
|
|
374
|
+
referenceLabelKey: z.ZodOptional<z.ZodString>;
|
|
375
|
+
panelOnly: z.ZodOptional<z.ZodBoolean>;
|
|
376
|
+
internal: z.ZodOptional<z.ZodBoolean>;
|
|
345
377
|
inline: z.ZodOptional<z.ZodBoolean>;
|
|
346
378
|
decorators: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
347
379
|
name: z.ZodString;
|
|
@@ -364,6 +396,7 @@ export declare const blockManifestSchema: z.ZodObject<{
|
|
|
364
396
|
imageAlt: "imageAlt";
|
|
365
397
|
color: "color";
|
|
366
398
|
headingLevel: "headingLevel";
|
|
399
|
+
reference: "reference";
|
|
367
400
|
}>;
|
|
368
401
|
label: z.ZodOptional<z.ZodString>;
|
|
369
402
|
inlineEditable: z.ZodOptional<z.ZodBoolean>;
|
|
@@ -383,6 +416,9 @@ export declare const blockManifestSchema: z.ZodObject<{
|
|
|
383
416
|
}>>;
|
|
384
417
|
}, z.core.$strip>>;
|
|
385
418
|
multiline: z.ZodOptional<z.ZodBoolean>;
|
|
419
|
+
referenceLabelKey: z.ZodOptional<z.ZodString>;
|
|
420
|
+
panelOnly: z.ZodOptional<z.ZodBoolean>;
|
|
421
|
+
internal: z.ZodOptional<z.ZodBoolean>;
|
|
386
422
|
inline: z.ZodOptional<z.ZodBoolean>;
|
|
387
423
|
decorators: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
388
424
|
name: z.ZodString;
|
|
@@ -448,7 +484,11 @@ export declare function validateManifestDefaultProps(blocks: BlockDefinition[]):
|
|
|
448
484
|
*
|
|
449
485
|
* - `propsSchema` decides *which* props exist. A declared entry for a key the
|
|
450
486
|
* schema does not expose is ignored, not added — otherwise a stale manifest
|
|
451
|
-
* would put a control on a prop the block cannot store.
|
|
487
|
+
* would put a control on a prop the block cannot store. The one exception is
|
|
488
|
+
* `internal: true`, which adds no control by definition: it is how a site
|
|
489
|
+
* names a prop the derivation skipped for being structural (a Contentful
|
|
490
|
+
* `sys`, a `__source` snapshot) so the coverage checkers stop reporting it as
|
|
491
|
+
* undescribed. Nothing else can be conjured this way.
|
|
452
492
|
* - A declared entry is merged *over* the derived one, key by key, so a site
|
|
453
493
|
* can supply just `multiline` and keep the label and options that were
|
|
454
494
|
* inferred for it.
|
package/dist/block-manifest.js
CHANGED
|
@@ -36,7 +36,7 @@ export const jsonSchemaLikeSchema = z.lazy(() => z.object({
|
|
|
36
36
|
*/
|
|
37
37
|
const FIELD_KINDS = [
|
|
38
38
|
"text", "richtext", "url", "link", "file", "image", "imageAlt",
|
|
39
|
-
"enum", "color", "number", "boolean", "headingLevel"
|
|
39
|
+
"enum", "color", "number", "boolean", "headingLevel", "reference"
|
|
40
40
|
];
|
|
41
41
|
const _everyKindListed = true;
|
|
42
42
|
void _everyKindListed;
|
|
@@ -54,6 +54,22 @@ export const fieldMetaSchema = z.object({
|
|
|
54
54
|
})
|
|
55
55
|
.optional(),
|
|
56
56
|
multiline: z.boolean().optional(),
|
|
57
|
+
/*
|
|
58
|
+
* Travels with the manifest because the panel is what reads it, and the panel
|
|
59
|
+
* is in a different process. A key this schema omits is stripped here, not
|
|
60
|
+
* rejected — so a reference would have arrived in the editor with nothing to
|
|
61
|
+
* label it and no sign that anything had been declared.
|
|
62
|
+
*/
|
|
63
|
+
referenceLabelKey: z.string().optional(),
|
|
64
|
+
/*
|
|
65
|
+
* Both of these decide whether a surface *mentions* a field, and every one
|
|
66
|
+
* of those surfaces is on the far side of this boundary — the panel, the
|
|
67
|
+
* planner's schema summary, the coverage checkers. A key this schema omits
|
|
68
|
+
* is stripped rather than rejected, so an undeclared flag here means the
|
|
69
|
+
* declaration silently does nothing where it was supposed to act.
|
|
70
|
+
*/
|
|
71
|
+
panelOnly: z.boolean().optional(),
|
|
72
|
+
internal: z.boolean().optional(),
|
|
57
73
|
inline: z.boolean().optional(),
|
|
58
74
|
decorators: z
|
|
59
75
|
.array(z.object({ name: z.string().min(1), title: z.string().optional() }))
|
|
@@ -250,6 +266,29 @@ const FILE_MEDIA_TYPE_RE = /^application\/(?!json\b)|^text\/(?:csv|calendar)\b/i
|
|
|
250
266
|
function isInternalPropKey(key) {
|
|
251
267
|
return key.startsWith("_");
|
|
252
268
|
}
|
|
269
|
+
/**
|
|
270
|
+
* The same statement the paragraph above makes, as a field rather than as an
|
|
271
|
+
* absence.
|
|
272
|
+
*
|
|
273
|
+
* These props used to be skipped outright, which kept them out of the panel
|
|
274
|
+
* and away from the planner — correct, and invisible to everything that reads
|
|
275
|
+
* the resolved metadata afterwards. `panelCoverage` is the one that noticed:
|
|
276
|
+
* a prop in the content and in no field set is an `orphan_prop`, and one
|
|
277
|
+
* Storyblok integration's `_uid` produced **~800 of them**, burying every real
|
|
278
|
+
* finding under a wall of noise that could not be closed. There was no way to
|
|
279
|
+
* close it, because "skipped" and "never declared" were the same thing.
|
|
280
|
+
*
|
|
281
|
+
* Emitting `internal: true` says it out loud instead. Every surface that must
|
|
282
|
+
* not mention the prop checks that flag, so nothing about the promise above
|
|
283
|
+
* changes; what changes is that the checkers can tell a known non-content prop
|
|
284
|
+
* from an undescribed one.
|
|
285
|
+
*
|
|
286
|
+
* `kind` is a formality here — nothing renders the field — so it does not
|
|
287
|
+
* matter that a `_key` and a `sys` object both come out as `text`.
|
|
288
|
+
*/
|
|
289
|
+
function internalFieldMeta(key) {
|
|
290
|
+
return { kind: "text", label: labelFromKey(key), internal: true };
|
|
291
|
+
}
|
|
253
292
|
function labelFromKey(key) {
|
|
254
293
|
// ctaText → "CTA text", imageUrl → "Image", logoAlt → "Logo alt",
|
|
255
294
|
// background_image → "Background image", video_url → "Video", src → "Src"
|
|
@@ -341,7 +380,11 @@ function deriveItemFields(itemSchema) {
|
|
|
341
380
|
for (const [key, propSchema] of Object.entries(props)) {
|
|
342
381
|
if (!isObject(propSchema))
|
|
343
382
|
continue;
|
|
344
|
-
if (isInternalPropKey(key)
|
|
383
|
+
if (isInternalPropKey(key)) {
|
|
384
|
+
fields[key] = internalFieldMeta(key);
|
|
385
|
+
continue;
|
|
386
|
+
}
|
|
387
|
+
if (isItemIdentityKey(key))
|
|
345
388
|
continue;
|
|
346
389
|
// Skip opaque object/array props (they'd render as "[object Object]"), but
|
|
347
390
|
// keep richtext docs — those self-identify and get the richtext editor.
|
|
@@ -436,8 +479,10 @@ export function deriveFieldMetaFromSchema(propsSchema) {
|
|
|
436
479
|
for (const [key, propSchema] of Object.entries(props)) {
|
|
437
480
|
if (!isObject(propSchema))
|
|
438
481
|
continue;
|
|
439
|
-
if (isInternalPropKey(key))
|
|
482
|
+
if (isInternalPropKey(key)) {
|
|
483
|
+
fields[key] = internalFieldMeta(key);
|
|
440
484
|
continue;
|
|
485
|
+
}
|
|
441
486
|
const type = typeof propSchema.type === "string" ? propSchema.type : undefined;
|
|
442
487
|
// Array of a discriminated union (oneOf/anyOf of object branches) →
|
|
443
488
|
// polymorphic list field: per-branch fields keyed by the discriminant value.
|
|
@@ -497,7 +542,11 @@ export function validateManifestDefaultProps(blocks) {
|
|
|
497
542
|
*
|
|
498
543
|
* - `propsSchema` decides *which* props exist. A declared entry for a key the
|
|
499
544
|
* schema does not expose is ignored, not added — otherwise a stale manifest
|
|
500
|
-
* would put a control on a prop the block cannot store.
|
|
545
|
+
* would put a control on a prop the block cannot store. The one exception is
|
|
546
|
+
* `internal: true`, which adds no control by definition: it is how a site
|
|
547
|
+
* names a prop the derivation skipped for being structural (a Contentful
|
|
548
|
+
* `sys`, a `__source` snapshot) so the coverage checkers stop reporting it as
|
|
549
|
+
* undescribed. Nothing else can be conjured this way.
|
|
501
550
|
* - A declared entry is merged *over* the derived one, key by key, so a site
|
|
502
551
|
* can supply just `multiline` and keep the label and options that were
|
|
503
552
|
* inferred for it.
|
|
@@ -508,7 +557,20 @@ export function resolveManifestFieldMeta(definition) {
|
|
|
508
557
|
const resolvedFields = {};
|
|
509
558
|
for (const [key, derived] of Object.entries(fields)) {
|
|
510
559
|
const declared = declaredFields[key];
|
|
511
|
-
|
|
560
|
+
const merged = isObject(declared) ? { ...derived, ...declared } : derived;
|
|
561
|
+
// The underscore convention is not negotiable by declaration: a site may
|
|
562
|
+
// label `_key` or change its kind, and it still may not hand it to a
|
|
563
|
+
// person, because the value typed there is the address a field-level
|
|
564
|
+
// publish patches by.
|
|
565
|
+
resolvedFields[key] = isInternalPropKey(key) ? { ...merged, internal: true } : merged;
|
|
566
|
+
}
|
|
567
|
+
for (const [key, declared] of Object.entries(declaredFields)) {
|
|
568
|
+
if (key in resolvedFields)
|
|
569
|
+
continue;
|
|
570
|
+
if (!isObject(declared) || declared.internal !== true)
|
|
571
|
+
continue;
|
|
572
|
+
const fm = declared;
|
|
573
|
+
resolvedFields[key] = { label: labelFromKey(key), ...fm, kind: fm.kind ?? "text" };
|
|
512
574
|
}
|
|
513
575
|
const declaredLists = isObject(definition.listFields) ? definition.listFields : {};
|
|
514
576
|
const resolvedListFields = {};
|
|
@@ -18,8 +18,27 @@ export declare function isImagePlaceholder(url: string | undefined | null): bool
|
|
|
18
18
|
* against nothing at all; a `file` can be checked against what the site's asset
|
|
19
19
|
* store actually holds, which is the only way anyone finds out that a menu link
|
|
20
20
|
* has been pointing at a filename with a typo in it since August.
|
|
21
|
+
*
|
|
22
|
+
* `reference` is a *pointer the CMS owns*, and it is not a flavour of `link`.
|
|
23
|
+
* A Storyblok story link is `{ linktype: "story", id: <uuid>, cached_url: "faq" }`
|
|
24
|
+
* and the Delivery API renders it per language — `/faq` on the German page,
|
|
25
|
+
* `/fr/faq` on the French one. Neither string is what the document holds.
|
|
26
|
+
* Contentful entry links and Sanity references have the same shape and the same
|
|
27
|
+
* problem. Declare one as `link` and the projection stops being invertible in
|
|
28
|
+
* both directions at once: the diff reports every reference on every page as
|
|
29
|
+
* changed because the rendered href never equals the stored object, and writing
|
|
30
|
+
* the href back replaces the reference with a hard-coded URL, which silently
|
|
31
|
+
* stops following renames — the one thing the reference was for.
|
|
32
|
+
*
|
|
33
|
+
* So the panel shows it and does not edit it. The value is carried through
|
|
34
|
+
* untouched, the way `avocadoUnknownBlock` carries a rich-text node the pivot
|
|
35
|
+
* cannot model, and `referenceLabelKey` says where to find something a person
|
|
36
|
+
* can read. Re-pointing a reference needs a picker that knows the CMS's own
|
|
37
|
+
* document ids, which only the integration has; until it exists, showing the
|
|
38
|
+
* target and refusing the edit is the honest answer and the corrupting one is
|
|
39
|
+
* a text input.
|
|
21
40
|
*/
|
|
22
|
-
export type FieldKind = "text" | "richtext" | "url" | "link" | "file" | "image" | "imageAlt" | "enum" | "color" | "number" | "boolean" | "headingLevel";
|
|
41
|
+
export type FieldKind = "text" | "richtext" | "url" | "link" | "file" | "image" | "imageAlt" | "enum" | "color" | "number" | "boolean" | "headingLevel" | "reference";
|
|
23
42
|
/** Recommended image dimensions for an image field. */
|
|
24
43
|
export type ImageSpec = {
|
|
25
44
|
aspectRatio: "landscape" | "square" | "portrait";
|
|
@@ -97,6 +116,46 @@ export type FieldMeta = {
|
|
|
97
116
|
* Use this one when there is no element at all.
|
|
98
117
|
*/
|
|
99
118
|
panelOnly?: boolean;
|
|
119
|
+
/**
|
|
120
|
+
* This prop belongs to the system that stores the content, not to anyone
|
|
121
|
+
* editing it.
|
|
122
|
+
*
|
|
123
|
+
* Storyblok's `_uid`, Contentful's `sys.id`, a `__source` snapshot, a
|
|
124
|
+
* revision stamp: the publisher needs them, a person must never edit them,
|
|
125
|
+
* and the planner must never be told they exist. Until this flag there was
|
|
126
|
+
* no way to say so, so they rode along undeclared — which is not the same as
|
|
127
|
+
* absent. An undeclared prop is an `orphan_prop` to `panelCoverage`, and one
|
|
128
|
+
* integration's `_uid` produced **~800 of them**, burying every real finding
|
|
129
|
+
* under a wall of noise that could not be closed.
|
|
130
|
+
*
|
|
131
|
+
* Declaring the prop internal says the opposite of what leaving it out says:
|
|
132
|
+
* "this is known, it is not content, and nothing that talks to a human or a
|
|
133
|
+
* model should mention it." Concretely it is hidden from the property panel,
|
|
134
|
+
* absent from the schema summary the planner reads, not expected to carry a
|
|
135
|
+
* preview marker, never offered for translation, and not a coverage finding
|
|
136
|
+
* in either direction — neither an orphan nor a phantom.
|
|
137
|
+
*
|
|
138
|
+
* It is not access control. The value still rides in `props`, still
|
|
139
|
+
* round-trips through the draft, and is still what the adapter writes back;
|
|
140
|
+
* an operation that names one is refused the way any undeclared prop is,
|
|
141
|
+
* because the planner was never told about it.
|
|
142
|
+
*
|
|
143
|
+
* `panelOnly` is the weaker neighbour and means almost the reverse: *a
|
|
144
|
+
* person does edit this, just not on the page.* Reach for that one when the
|
|
145
|
+
* field has no element; reach for this one when the field has no audience.
|
|
146
|
+
*/
|
|
147
|
+
internal?: boolean;
|
|
148
|
+
/**
|
|
149
|
+
* For `kind: "reference"`: the key inside the value holding something a
|
|
150
|
+
* person can read.
|
|
151
|
+
*
|
|
152
|
+
* Optional, because the common spellings are tried anyway — Storyblok's
|
|
153
|
+
* `cached_url`, a `slug`, a `title`, a `name`. Set it when the reference
|
|
154
|
+
* carries a readable key under a name nothing would guess, or when it carries
|
|
155
|
+
* several and the wrong one wins. Purely a display concern: it never selects
|
|
156
|
+
* what is written, because a reference is not written here at all.
|
|
157
|
+
*/
|
|
158
|
+
referenceLabelKey?: string;
|
|
100
159
|
};
|
|
101
160
|
/** Metadata for list-type props (features, items, cards). */
|
|
102
161
|
export type ListFieldMeta = {
|
|
@@ -137,6 +196,14 @@ export type BlockRegistration = {
|
|
|
137
196
|
};
|
|
138
197
|
/** Test seam: forget what has already been warned about. */
|
|
139
198
|
export declare function resetListFieldWarnings(): void;
|
|
199
|
+
/** Whether `type` is currently backed by Avocado's own definition. */
|
|
200
|
+
export declare function isBuiltinBlock(type: string): boolean;
|
|
201
|
+
/**
|
|
202
|
+
* Register one of Avocado's own blocks. Internal — a site uses `registerBlock`,
|
|
203
|
+
* and registering over a built-in name deliberately drops the built-in flag
|
|
204
|
+
* with the built-in schema.
|
|
205
|
+
*/
|
|
206
|
+
export declare function registerBuiltinBlock(type: string, config: BlockRegistration): void;
|
|
140
207
|
export declare function registerBlock(type: string, config: BlockRegistration): void;
|
|
141
208
|
/** Get metadata for a registered block type, or undefined. */
|
|
142
209
|
export declare function getBlockMeta(type: string): BlockMeta | undefined;
|
|
@@ -226,6 +293,19 @@ export declare function isInBlockCatalogue(type: string): boolean;
|
|
|
226
293
|
export declare function catalogueBlockTypes(): string[];
|
|
227
294
|
/** Declared types with no registration behind them — a typo, or a missing import. */
|
|
228
295
|
export declare function undeclaredBlockTypes(): string[];
|
|
296
|
+
/**
|
|
297
|
+
* Something a person can read out of a reference value.
|
|
298
|
+
*
|
|
299
|
+
* A reference is opaque by design — the panel shows it and never rewrites it —
|
|
300
|
+
* but "opaque" must not mean `[object Object]` in the one place a person looks
|
|
301
|
+
* to check which document a CTA points at. Storyblok spells the readable half
|
|
302
|
+
* `cached_url`, Contentful puts a `slug` or `title` on the resolved entry,
|
|
303
|
+
* Sanity hands over a bare `_ref`. Falls back to the id, and then to nothing,
|
|
304
|
+
* which the panel renders as "not set" rather than as a broken label.
|
|
305
|
+
*
|
|
306
|
+
* `referenceLabelKey` wins when the meta declares it.
|
|
307
|
+
*/
|
|
308
|
+
export declare function referenceLabel(value: unknown, labelKey?: string): string;
|
|
229
309
|
export declare function getPropDisplayName(blockType: string | undefined, propKey: string): string;
|
|
230
310
|
export declare function defaultListItemForBlock(type: BlockType, listKey: string): Record<string, unknown> | null;
|
|
231
311
|
/** Base schema — accepts any block type. Used for ingesting external site content with custom blocks. */
|
package/dist/blocks/_registry.js
CHANGED
|
@@ -68,6 +68,72 @@ function unwrapZod(schema) {
|
|
|
68
68
|
}
|
|
69
69
|
return current;
|
|
70
70
|
}
|
|
71
|
+
/**
|
|
72
|
+
* The object shapes a list's rows can take.
|
|
73
|
+
*
|
|
74
|
+
* A monomorphic list has one. A polymorphic list — `z.discriminatedUnion`, the
|
|
75
|
+
* shape our own docs tell an integrator to use when rows differ — has one per
|
|
76
|
+
* branch and no `.shape` of its own. Reading `.element.shape` and stopping there
|
|
77
|
+
* declared every correct polymorphic list unbacked, warned about it at startup,
|
|
78
|
+
* and prescribed `z.array(z.object({ … }))`: the one edit that really would
|
|
79
|
+
* break it, because collapsing the union is what stops each row narrowing to its
|
|
80
|
+
* own field set. The panel never used this path — it reads the JSON Schema,
|
|
81
|
+
* where the branches survive as `anyOf` — so the lists worked exactly as
|
|
82
|
+
* declared while their author was being told they could not.
|
|
83
|
+
*/
|
|
84
|
+
function listElementBranches(element) {
|
|
85
|
+
const unwrapped = unwrapZod(element);
|
|
86
|
+
if (!unwrapped)
|
|
87
|
+
return undefined;
|
|
88
|
+
const ownShape = unwrapped.shape;
|
|
89
|
+
if (ownShape)
|
|
90
|
+
return [{ discriminantValues: [], shape: ownShape }];
|
|
91
|
+
const def = unwrapped.def ?? unwrapped._def;
|
|
92
|
+
const options = def?.options;
|
|
93
|
+
if (!Array.isArray(options) || options.length === 0)
|
|
94
|
+
return undefined;
|
|
95
|
+
const discriminator = def?.discriminator;
|
|
96
|
+
const branches = [];
|
|
97
|
+
for (const option of options) {
|
|
98
|
+
const optionShape = unwrapZod(option)?.shape;
|
|
99
|
+
if (!optionShape)
|
|
100
|
+
continue;
|
|
101
|
+
branches.push({
|
|
102
|
+
discriminantValues: discriminator ? literalValuesOf(optionShape[discriminator]) : [],
|
|
103
|
+
shape: optionShape
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
return branches.length > 0 ? branches : undefined;
|
|
107
|
+
}
|
|
108
|
+
/** The values a discriminant field admits, spelled as the strings a row carries. */
|
|
109
|
+
function literalValuesOf(schema) {
|
|
110
|
+
const def = unwrapZod(schema)?.def ?? unwrapZod(schema)?._def;
|
|
111
|
+
if (!def)
|
|
112
|
+
return [];
|
|
113
|
+
if (Array.isArray(def.values))
|
|
114
|
+
return def.values.map(String); // z.literal in Zod 4
|
|
115
|
+
if (def.value !== undefined)
|
|
116
|
+
return [String(def.value)]; // z.literal in Zod 3
|
|
117
|
+
if (def.entries)
|
|
118
|
+
return Object.values(def.entries).map(String); // z.enum
|
|
119
|
+
return [];
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Fill in `required` from the Zod shapes a field set can be backed by.
|
|
123
|
+
*
|
|
124
|
+
* More than one shape means the set spans branches, and a key only one branch
|
|
125
|
+
* declares cannot be required of a row that might be another branch.
|
|
126
|
+
*/
|
|
127
|
+
function deriveRequired(fields, shapes) {
|
|
128
|
+
for (const [key, field] of Object.entries(fields)) {
|
|
129
|
+
if (field.required !== undefined)
|
|
130
|
+
continue;
|
|
131
|
+
const backing = shapes.filter((s) => s[key]);
|
|
132
|
+
if (backing.length === 0)
|
|
133
|
+
continue;
|
|
134
|
+
field.required = backing.length === shapes.length && backing.every((s) => !s[key].isOptional());
|
|
135
|
+
}
|
|
136
|
+
}
|
|
71
137
|
const warnedListFields = new Set();
|
|
72
138
|
function warnUnbackedListField(type, listKey, present) {
|
|
73
139
|
const key = `${type}.${listKey}`;
|
|
@@ -91,8 +157,35 @@ function warnUnbackedListField(type, listKey, present) {
|
|
|
91
157
|
export function resetListFieldWarnings() {
|
|
92
158
|
warnedListFields.clear();
|
|
93
159
|
}
|
|
160
|
+
/**
|
|
161
|
+
* The block types Avocado itself defines, as opposed to a site's own.
|
|
162
|
+
*
|
|
163
|
+
* A site that renders its own `Hero` registers it under that name, and from
|
|
164
|
+
* then on the name is all the two shapes have in common. Anything that reasons
|
|
165
|
+
* about a block by hard-coded key — the demo-content migrations in
|
|
166
|
+
* `session-state`, which stamp `imageUrl` and a `left`/`right` pair onto
|
|
167
|
+
* whatever is called `Hero` or `TwoColumn` — is reasoning about *our* schema,
|
|
168
|
+
* and must not run against a schema it has never seen. Registration order
|
|
169
|
+
* settles it: whoever registered last owns the name, so the flag is written on
|
|
170
|
+
* every call rather than only on the first.
|
|
171
|
+
*/
|
|
172
|
+
const _builtinBlockTypes = G.__ase_builtinBlockTypes ?? (G.__ase_builtinBlockTypes = new Set());
|
|
173
|
+
/** Whether `type` is currently backed by Avocado's own definition. */
|
|
174
|
+
export function isBuiltinBlock(type) {
|
|
175
|
+
return _builtinBlockTypes.has(type);
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Register one of Avocado's own blocks. Internal — a site uses `registerBlock`,
|
|
179
|
+
* and registering over a built-in name deliberately drops the built-in flag
|
|
180
|
+
* with the built-in schema.
|
|
181
|
+
*/
|
|
182
|
+
export function registerBuiltinBlock(type, config) {
|
|
183
|
+
registerBlock(type, config);
|
|
184
|
+
_builtinBlockTypes.add(type);
|
|
185
|
+
}
|
|
94
186
|
export function registerBlock(type, config) {
|
|
95
187
|
_blockSchemas[type] = config.schema;
|
|
188
|
+
_builtinBlockTypes.delete(type);
|
|
96
189
|
// Auto-derive `required` on each FieldMeta from the Zod schema shape
|
|
97
190
|
const shape = config.schema.shape;
|
|
98
191
|
if (shape) {
|
|
@@ -112,18 +205,20 @@ export function registerBlock(type, config) {
|
|
|
112
205
|
// Without the outer unwrap, `z.array(...).optional()` — which is how a
|
|
113
206
|
// list should be declared — looked like no array at all, so every
|
|
114
207
|
// optional list silently skipped its own `required` derivation.
|
|
115
|
-
const
|
|
116
|
-
if (!
|
|
208
|
+
const branches = listElementBranches(unwrapZod(listZod)?.element);
|
|
209
|
+
if (!branches) {
|
|
117
210
|
warnUnbackedListField(type, listKey, listZod !== undefined);
|
|
118
211
|
continue;
|
|
119
212
|
}
|
|
120
|
-
|
|
121
|
-
|
|
213
|
+
// `itemFields` is the union of every branch — the set a row is edited
|
|
214
|
+
// against when its discriminant matches nothing — so a key is required
|
|
215
|
+
// there only if every branch has it and no branch makes it optional.
|
|
216
|
+
deriveRequired(listMeta.itemFields, branches.map((b) => b.shape));
|
|
217
|
+
for (const [value, branchFields] of Object.entries(listMeta.itemFieldsByType ?? {})) {
|
|
218
|
+
const matching = branches.filter((b) => b.discriminantValues.includes(value));
|
|
219
|
+
if (matching.length === 0)
|
|
122
220
|
continue;
|
|
123
|
-
|
|
124
|
-
if (zodItem) {
|
|
125
|
-
itemField.required = !zodItem.isOptional();
|
|
126
|
-
}
|
|
221
|
+
deriveRequired(branchFields, matching.map((b) => b.shape));
|
|
127
222
|
}
|
|
128
223
|
}
|
|
129
224
|
}
|
|
@@ -391,6 +486,47 @@ export function undeclaredBlockTypes() {
|
|
|
391
486
|
return [];
|
|
392
487
|
return [...declared].filter((t) => !(t in _blockSchemas));
|
|
393
488
|
}
|
|
489
|
+
/**
|
|
490
|
+
* Something a person can read out of a reference value.
|
|
491
|
+
*
|
|
492
|
+
* A reference is opaque by design — the panel shows it and never rewrites it —
|
|
493
|
+
* but "opaque" must not mean `[object Object]` in the one place a person looks
|
|
494
|
+
* to check which document a CTA points at. Storyblok spells the readable half
|
|
495
|
+
* `cached_url`, Contentful puts a `slug` or `title` on the resolved entry,
|
|
496
|
+
* Sanity hands over a bare `_ref`. Falls back to the id, and then to nothing,
|
|
497
|
+
* which the panel renders as "not set" rather than as a broken label.
|
|
498
|
+
*
|
|
499
|
+
* `referenceLabelKey` wins when the meta declares it.
|
|
500
|
+
*/
|
|
501
|
+
export function referenceLabel(value, labelKey) {
|
|
502
|
+
if (value === null || value === undefined)
|
|
503
|
+
return "";
|
|
504
|
+
if (typeof value === "string" || typeof value === "number")
|
|
505
|
+
return String(value).trim();
|
|
506
|
+
if (typeof value !== "object" || Array.isArray(value))
|
|
507
|
+
return "";
|
|
508
|
+
const rec = value;
|
|
509
|
+
const readable = (key) => {
|
|
510
|
+
const candidate = rec[key];
|
|
511
|
+
return typeof candidate === "string" || typeof candidate === "number" ? String(candidate).trim() : "";
|
|
512
|
+
};
|
|
513
|
+
if (labelKey) {
|
|
514
|
+
const declared = readable(labelKey);
|
|
515
|
+
if (declared !== "")
|
|
516
|
+
return declared;
|
|
517
|
+
}
|
|
518
|
+
for (const key of ["cached_url", "slug", "url", "href", "title", "name", "label"]) {
|
|
519
|
+
const found = readable(key);
|
|
520
|
+
if (found !== "")
|
|
521
|
+
return found;
|
|
522
|
+
}
|
|
523
|
+
for (const key of ["_ref", "id", "_id", "uuid", "entryId"]) {
|
|
524
|
+
const found = readable(key);
|
|
525
|
+
if (found !== "")
|
|
526
|
+
return found;
|
|
527
|
+
}
|
|
528
|
+
return "";
|
|
529
|
+
}
|
|
394
530
|
export function getPropDisplayName(blockType, propKey) {
|
|
395
531
|
if (!blockType)
|
|
396
532
|
return propKey;
|
|
@@ -429,6 +565,14 @@ function defaultScalarForField(field, fieldKey) {
|
|
|
429
565
|
return false;
|
|
430
566
|
if (field.kind === "enum")
|
|
431
567
|
return Array.isArray(field.options) && field.options.length > 0 ? field.options[0] : "";
|
|
568
|
+
/*
|
|
569
|
+
* A reference points at a document in the CMS. There is no default one, and
|
|
570
|
+
* `"New Story link"` in a field the site resolves to a URL is a broken link
|
|
571
|
+
* dressed as content. An added row simply has no reference until somebody
|
|
572
|
+
* sets one upstream.
|
|
573
|
+
*/
|
|
574
|
+
if (field.kind === "reference")
|
|
575
|
+
return null;
|
|
432
576
|
return `New ${label}`;
|
|
433
577
|
}
|
|
434
578
|
export function defaultListItemForBlock(type, listKey) {
|
package/dist/blocks/banner.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import {
|
|
2
|
+
import { registerBuiltinBlock } from "./_registry.js";
|
|
3
3
|
import { f } from "./_helpers.js";
|
|
4
|
-
|
|
4
|
+
registerBuiltinBlock("Banner", {
|
|
5
5
|
schema: z.object({
|
|
6
6
|
text: z.string().min(1),
|
|
7
7
|
variant: z.enum(["info", "success", "warning"]).default("info").catch("info"),
|
package/dist/blocks/card-grid.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import {
|
|
2
|
+
import { registerBuiltinBlock } from "./_registry.js";
|
|
3
3
|
import { f } from "./_helpers.js";
|
|
4
|
-
|
|
4
|
+
registerBuiltinBlock("CardGrid", {
|
|
5
5
|
schema: z.object({
|
|
6
6
|
title: z.string().min(1),
|
|
7
7
|
subtitle: z.string().optional(),
|
package/dist/blocks/card.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import {
|
|
2
|
+
import { registerBuiltinBlock } from "./_registry.js";
|
|
3
3
|
import { f } from "./_helpers.js";
|
|
4
|
-
|
|
4
|
+
registerBuiltinBlock("Card", {
|
|
5
5
|
schema: z.object({
|
|
6
6
|
title: z.string().min(1),
|
|
7
7
|
description: z.string().min(1),
|
package/dist/blocks/carousel.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import {
|
|
2
|
+
import { registerBuiltinBlock, IMAGE_PLACEHOLDER } from "./_registry.js";
|
|
3
3
|
import { f } from "./_helpers.js";
|
|
4
|
-
|
|
4
|
+
registerBuiltinBlock("Carousel", {
|
|
5
5
|
schema: z.object({
|
|
6
6
|
items: z.array(z.object({
|
|
7
7
|
id: z.string().optional(),
|
package/dist/blocks/cta.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import {
|
|
2
|
+
import { registerBuiltinBlock } from "./_registry.js";
|
|
3
3
|
import { f } from "./_helpers.js";
|
|
4
|
-
|
|
4
|
+
registerBuiltinBlock("CTA", {
|
|
5
5
|
schema: z.object({
|
|
6
6
|
title: z.string().min(1),
|
|
7
7
|
description: z.string().min(1),
|
package/dist/blocks/embed.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import {
|
|
2
|
+
import { registerBuiltinBlock } from "./_registry.js";
|
|
3
3
|
import { f } from "./_helpers.js";
|
|
4
|
-
|
|
4
|
+
registerBuiltinBlock("Embed", {
|
|
5
5
|
schema: z.object({
|
|
6
6
|
embedType: z.enum(["map", "social", "custom"]).default("map").catch("map"),
|
|
7
7
|
url: z.string().min(1),
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import {
|
|
2
|
+
import { registerBuiltinBlock } from "./_registry.js";
|
|
3
3
|
import { f } from "./_helpers.js";
|
|
4
|
-
|
|
4
|
+
registerBuiltinBlock("FAQAccordion", {
|
|
5
5
|
schema: z.object({
|
|
6
6
|
title: z.string().min(1),
|
|
7
7
|
items: z.array(z.object({ id: z.string().optional(), q: z.string().min(1), a: z.string().min(1) })).min(1)
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import {
|
|
2
|
+
import { registerBuiltinBlock } from "./_registry.js";
|
|
3
3
|
import { f } from "./_helpers.js";
|
|
4
|
-
|
|
4
|
+
registerBuiltinBlock("FeatureGrid", {
|
|
5
5
|
schema: z.object({
|
|
6
6
|
title: z.string().min(1),
|
|
7
7
|
columns: z.enum(["2", "3", "4"]).default("3").catch("3"),
|
package/dist/blocks/footer.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import {
|
|
2
|
+
import { registerBuiltinBlock } from "./_registry.js";
|
|
3
3
|
import { f } from "./_helpers.js";
|
|
4
|
-
|
|
4
|
+
registerBuiltinBlock("Footer", {
|
|
5
5
|
schema: z.object({
|
|
6
6
|
copyright: z.string().min(1),
|
|
7
7
|
columns: z.array(z.object({ id: z.string().optional(), title: z.string().min(1), links: z.string().min(1) })).min(1)
|
package/dist/blocks/gallery.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import {
|
|
2
|
+
import { registerBuiltinBlock, IMAGE_PLACEHOLDER } from "./_registry.js";
|
|
3
3
|
import { f } from "./_helpers.js";
|
|
4
|
-
|
|
4
|
+
registerBuiltinBlock("Gallery", {
|
|
5
5
|
schema: z.object({
|
|
6
6
|
title: z.string().optional(),
|
|
7
7
|
headingLevel: z.string().optional(),
|
package/dist/blocks/hero.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import {
|
|
2
|
+
import { registerBuiltinBlock, IMAGE_PLACEHOLDER } from "./_registry.js";
|
|
3
3
|
import { f } from "./_helpers.js";
|
|
4
|
-
|
|
4
|
+
registerBuiltinBlock("Hero", {
|
|
5
5
|
schema: z.object({
|
|
6
6
|
heading: z.string().min(1),
|
|
7
7
|
subheading: z.string().min(1),
|
package/dist/blocks/quote.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import {
|
|
2
|
+
import { registerBuiltinBlock } from "./_registry.js";
|
|
3
3
|
import { f } from "./_helpers.js";
|
|
4
|
-
|
|
4
|
+
registerBuiltinBlock("Quote", {
|
|
5
5
|
schema: z.object({
|
|
6
6
|
quote: z.string().min(1),
|
|
7
7
|
author: z.string().optional(),
|
package/dist/blocks/rich-text.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import {
|
|
2
|
+
import { registerBuiltinBlock } from "./_registry.js";
|
|
3
3
|
import { f } from "./_helpers.js";
|
|
4
|
-
|
|
4
|
+
registerBuiltinBlock("RichText", {
|
|
5
5
|
schema: z.object({
|
|
6
6
|
title: z.string(),
|
|
7
7
|
body: z.string().min(1)
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import {
|
|
2
|
+
import { registerBuiltinBlock } from "./_registry.js";
|
|
3
3
|
import { f } from "./_helpers.js";
|
|
4
4
|
const navLinkLeaf = z.object({
|
|
5
5
|
id: z.string().optional(),
|
|
@@ -7,7 +7,7 @@ const navLinkLeaf = z.object({
|
|
|
7
7
|
href: z.string().min(1),
|
|
8
8
|
newTab: z.boolean().optional(),
|
|
9
9
|
});
|
|
10
|
-
|
|
10
|
+
registerBuiltinBlock("SiteHeader", {
|
|
11
11
|
schema: z.object({
|
|
12
12
|
siteName: z.string().min(1),
|
|
13
13
|
logoUrl: z.string().min(1),
|
package/dist/blocks/stats.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import {
|
|
2
|
+
import { registerBuiltinBlock } from "./_registry.js";
|
|
3
3
|
import { f } from "./_helpers.js";
|
|
4
|
-
|
|
4
|
+
registerBuiltinBlock("Stats", {
|
|
5
5
|
schema: z.object({
|
|
6
6
|
title: z.string().optional(),
|
|
7
7
|
stats: z.array(z.object({
|
package/dist/blocks/table.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import {
|
|
2
|
+
import { registerBuiltinBlock } from "./_registry.js";
|
|
3
3
|
import { f } from "./_helpers.js";
|
|
4
|
-
|
|
4
|
+
registerBuiltinBlock("Table", {
|
|
5
5
|
schema: z.object({
|
|
6
6
|
title: z.string().optional(),
|
|
7
7
|
headingLevel: z.string().optional(),
|
package/dist/blocks/tabs.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import {
|
|
2
|
+
import { registerBuiltinBlock } from "./_registry.js";
|
|
3
3
|
import { f } from "./_helpers.js";
|
|
4
|
-
|
|
4
|
+
registerBuiltinBlock("Tabs", {
|
|
5
5
|
schema: z.object({
|
|
6
6
|
title: z.string().optional(),
|
|
7
7
|
tabs: z.array(z.object({
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import {
|
|
2
|
+
import { registerBuiltinBlock } from "./_registry.js";
|
|
3
3
|
import { f } from "./_helpers.js";
|
|
4
|
-
|
|
4
|
+
registerBuiltinBlock("Testimonials", {
|
|
5
5
|
schema: z.object({
|
|
6
6
|
title: z.string().min(1),
|
|
7
7
|
items: z.array(z.object({
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import {
|
|
2
|
+
import { registerBuiltinBlock, IMAGE_PLACEHOLDER } from "./_registry.js";
|
|
3
3
|
import { f } from "./_helpers.js";
|
|
4
4
|
const twoColumnChild = z.object({
|
|
5
5
|
id: z.string().optional(),
|
|
@@ -62,7 +62,7 @@ const twoColumnItemFieldsByType = {
|
|
|
62
62
|
poster: f.image("Video poster image", { aspectRatio: "landscape", width: 768, height: 512 }),
|
|
63
63
|
},
|
|
64
64
|
};
|
|
65
|
-
|
|
65
|
+
registerBuiltinBlock("TwoColumn", {
|
|
66
66
|
schema: z.object({
|
|
67
67
|
variant: z.enum(["default", "accent"]).default("default").catch("default"),
|
|
68
68
|
left: z.array(twoColumnChild).min(1),
|
package/dist/blocks/video.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import {
|
|
2
|
+
import { registerBuiltinBlock } from "./_registry.js";
|
|
3
3
|
import { f } from "./_helpers.js";
|
|
4
|
-
|
|
4
|
+
registerBuiltinBlock("Video", {
|
|
5
5
|
schema: z.object({
|
|
6
6
|
src: z.string().min(1),
|
|
7
7
|
title: z.string().optional(),
|
|
@@ -49,6 +49,10 @@ const DRAWN_KINDS = new Set(["text", "richtext", "image"]);
|
|
|
49
49
|
* button, and the panel offering one is the site saying the image is editable.
|
|
50
50
|
*/
|
|
51
51
|
function needsMarker(meta) {
|
|
52
|
+
// Not content at all — a CMS's `_uid`, a revision stamp. Nobody edits it
|
|
53
|
+
// anywhere, so there is nothing on the page for it to be missing from.
|
|
54
|
+
if (meta.internal)
|
|
55
|
+
return false;
|
|
52
56
|
// The site has said outright that nothing on the page draws this — a
|
|
53
57
|
// `sectionId`, a `<video poster>`, an input's `placeholder`. See `panelOnly`.
|
|
54
58
|
if (meta.panelOnly)
|
|
@@ -73,10 +77,19 @@ const VOID_ELEMENTS = new Set([
|
|
|
73
77
|
"link", "meta", "param", "source", "track", "wbr",
|
|
74
78
|
]);
|
|
75
79
|
/** One element: its tag name, and the raw attribute text, quotes respected. */
|
|
76
|
-
const TAG_RE = /<([a-zA-Z][\w-]*)((?:"[^"]*"|'[^']*'|[^>"'])*)>/g;
|
|
80
|
+
const TAG_RE = /<(\/?)([a-zA-Z][\w-]*)((?:"[^"]*"|'[^']*'|[^>"'])*)>/g;
|
|
77
81
|
const BLOCK_ID_RE = /\bdata-block-id="([^"]*)"/;
|
|
78
82
|
const BLOCK_TYPE_RE = /\bdata-block-type="([^"]*)"/;
|
|
79
83
|
const TARGET_RE = /\bdata-editable-target="([^"]*)"/;
|
|
84
|
+
const SCOPE_RE = /\bdata-editable-scope="([^"]*)"/;
|
|
85
|
+
/** `items[3]` + `question` → `items[3].question`; `cards` + `[0].title` → `cards[0].title`. */
|
|
86
|
+
function joinPath(scope, path) {
|
|
87
|
+
if (scope === "")
|
|
88
|
+
return path;
|
|
89
|
+
if (path === "")
|
|
90
|
+
return scope;
|
|
91
|
+
return path.startsWith("[") ? `${scope}${path}` : `${scope}.${path}`;
|
|
92
|
+
}
|
|
80
93
|
/**
|
|
81
94
|
* Read the marked blocks out of a rendered page's HTML.
|
|
82
95
|
*
|
|
@@ -90,9 +103,35 @@ const TARGET_RE = /\bdata-editable-target="([^"]*)"/;
|
|
|
90
103
|
export function extractMarkedBlocks(html) {
|
|
91
104
|
const blocks = [];
|
|
92
105
|
let current = null;
|
|
106
|
+
/*
|
|
107
|
+
* The open elements that carry a `data-editable-scope`, innermost last, each
|
|
108
|
+
* remembered with the tag that will close it. A component rendering a list
|
|
109
|
+
* row marks a bare `question` and its parent supplies `items[3]`, so the
|
|
110
|
+
* path this file compares against the manifest exists only as the join —
|
|
111
|
+
* see `editableScopeProps`. Without the stack a scoped site's every field
|
|
112
|
+
* reads as missing, and the check that exists to catch a lost marker becomes
|
|
113
|
+
* the reason nobody runs it.
|
|
114
|
+
*/
|
|
115
|
+
const scopes = [];
|
|
116
|
+
const depths = new Map();
|
|
93
117
|
for (const match of html.matchAll(TAG_RE)) {
|
|
94
|
-
const
|
|
95
|
-
const
|
|
118
|
+
const closing = match[1] === "/";
|
|
119
|
+
const tag = match[2].toLowerCase();
|
|
120
|
+
const attrs = match[3] ?? "";
|
|
121
|
+
const selfClosing = attrs.trimEnd().endsWith("/");
|
|
122
|
+
const isVoid = VOID_ELEMENTS.has(tag);
|
|
123
|
+
if (closing) {
|
|
124
|
+
const depth = (depths.get(tag) ?? 0) - 1;
|
|
125
|
+
depths.set(tag, depth);
|
|
126
|
+
while (scopes.length > 0) {
|
|
127
|
+
const top = scopes[scopes.length - 1];
|
|
128
|
+
if (top.tag === tag && top.depth === depth)
|
|
129
|
+
scopes.pop();
|
|
130
|
+
else
|
|
131
|
+
break;
|
|
132
|
+
}
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
96
135
|
const blockId = BLOCK_ID_RE.exec(attrs)?.[1];
|
|
97
136
|
const blockType = BLOCK_TYPE_RE.exec(attrs)?.[1];
|
|
98
137
|
if (blockId !== undefined || blockType !== undefined) {
|
|
@@ -102,13 +141,29 @@ export function extractMarkedBlocks(html) {
|
|
|
102
141
|
paths: [],
|
|
103
142
|
};
|
|
104
143
|
blocks.push(current);
|
|
144
|
+
// A path is scoped from the block down, so a scope opened outside this
|
|
145
|
+
// block must not reach into it.
|
|
146
|
+
scopes.length = 0;
|
|
105
147
|
}
|
|
106
148
|
const target = TARGET_RE.exec(attrs)?.[1];
|
|
107
149
|
if (target !== undefined && current) {
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
150
|
+
const path = scopes.reduce((acc, s) => joinPath(acc, s.scope), "");
|
|
151
|
+
const full = joinPath(path, target);
|
|
152
|
+
current.paths.push(full);
|
|
153
|
+
if (isVoid)
|
|
154
|
+
(current.voidPaths ??= []).push(full);
|
|
111
155
|
}
|
|
156
|
+
if (isVoid || selfClosing)
|
|
157
|
+
continue;
|
|
158
|
+
const depth = depths.get(tag) ?? 0;
|
|
159
|
+
depths.set(tag, depth + 1);
|
|
160
|
+
const scope = SCOPE_RE.exec(attrs)?.[1];
|
|
161
|
+
// The scoping element's own marker is not inside its scope — a component
|
|
162
|
+
// that both scopes its children and marks a field of its own would
|
|
163
|
+
// otherwise prefix its own path with the scope it sets for them. That is
|
|
164
|
+
// why this comes after the target is recorded.
|
|
165
|
+
if (scope !== undefined && scope !== "")
|
|
166
|
+
scopes.push({ tag, scope, depth });
|
|
112
167
|
}
|
|
113
168
|
return blocks;
|
|
114
169
|
}
|
|
@@ -314,7 +369,15 @@ export function editableCoverage(manifest, blocks) {
|
|
|
314
369
|
for (const [itemKey, itemMeta] of drawnItemFields) {
|
|
315
370
|
const path = `${listKey}[].${itemKey}`;
|
|
316
371
|
expected += 1;
|
|
317
|
-
|
|
372
|
+
/*
|
|
373
|
+
* `isCovered`, not `paths.has`, for the same reason the top-level scan
|
|
374
|
+
* uses it: a row field can itself be a list. `buttons` is marked as
|
|
375
|
+
* `left[].buttons[].label` — the label is the only part of a button a
|
|
376
|
+
* person edits in place — and an exact match called that unmarked,
|
|
377
|
+
* reporting a gap whose only remedy would be a second marker on an
|
|
378
|
+
* element that draws nothing.
|
|
379
|
+
*/
|
|
380
|
+
if (!isCovered(path, paths)) {
|
|
318
381
|
missingItemFields.push(path);
|
|
319
382
|
}
|
|
320
383
|
else if (itemMeta.kind === "image" && voids.has(path)) {
|
package/dist/index.d.ts
CHANGED
|
@@ -12,10 +12,10 @@ export { parseLink, resolveLink, normalizeLinkPath, isKnownRoute, internalPathFo
|
|
|
12
12
|
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";
|
|
13
13
|
export { blockDefinitionSchema, blockManifestSchema, buildBlockManifest, jsonSchemaLikeSchema, validateByJsonSchemaLike, findManifestSchemaIssue, type ManifestSchemaIssue, validateManifestDefaultProps, deriveFieldMetaFromSchema, resolveManifestFieldMeta, isProseMirrorDocSchema, type BlockDefinition, type BlockManifest } from "./block-manifest.ts";
|
|
14
14
|
export { z } from "zod";
|
|
15
|
-
export { type FieldKind, type ImageSpec, type FieldMeta, type ListFieldMeta, type BlockMeta, type BlockType, type BlockInstance, type BlockRegistration, IMAGE_PLACEHOLDER, isImagePlaceholder, registerBlock, resetListFieldWarnings, getBlockMeta, getAllBlockMeta, blockAcceptsProp, blockListItemAcceptsKey, getImageFields, getListImageFields, getMediaFields, isFieldInlineEditable, getImageSpec, isChrome, getChromeTypes, blockSchemas, allowedBlockTypes, declareBlockCatalogue, getBlockCatalogue, isInBlockCatalogue, catalogueBlockTypes, undeclaredBlockTypes, getPropDisplayName, defaultListItemForBlock, blockInstanceSchema, blockInstanceSchemaLenient, validateBlockProps, getBlockJsonSchema, } from "./blocks/_registry.ts";
|
|
15
|
+
export { type FieldKind, type ImageSpec, type FieldMeta, type ListFieldMeta, type BlockMeta, type BlockType, type BlockInstance, type BlockRegistration, IMAGE_PLACEHOLDER, isImagePlaceholder, registerBlock, isBuiltinBlock, resetListFieldWarnings, getBlockMeta, getAllBlockMeta, blockAcceptsProp, blockListItemAcceptsKey, getImageFields, getListImageFields, getMediaFields, isFieldInlineEditable, referenceLabel, getImageSpec, isChrome, getChromeTypes, blockSchemas, allowedBlockTypes, declareBlockCatalogue, getBlockCatalogue, isInBlockCatalogue, catalogueBlockTypes, undeclaredBlockTypes, getPropDisplayName, defaultListItemForBlock, blockInstanceSchema, blockInstanceSchemaLenient, validateBlockProps, getBlockJsonSchema, } from "./blocks/_registry.ts";
|
|
16
16
|
export { defaultPropsForType, declaredDefaultPropsForType, resolveHeadingTag, resolveItemHeadingTag, DEFAULT_HEADING_LEVELS, } from "./blocks/index.ts";
|
|
17
17
|
export { blockTypeToCamel, camelToBlockType, blockTypeToLower, lowerToBlockType, } from "./block-names.ts";
|
|
18
|
-
export { makeAddBlock, generateBlockId, makeAddItem, generateItemId, ensureItemIds, type AddBlockOp, type MakeAddBlockOptions, type AddItemOp, type MakeAddItemOptions, } from "./ops/builders.ts";
|
|
18
|
+
export { makeAddBlock, generateBlockId, makeAddItem, generateItemId, ensureItemIds, withoutGeneratedItemIds, isGeneratedItemId, type AddBlockOp, type MakeAddBlockOptions, type AddItemOp, type MakeAddItemOptions, } from "./ops/builders.ts";
|
|
19
19
|
export { THEME_TOKEN_TO_CSS_VARS, themeTokenKeys, semanticThemeTokensSchema, mapSemanticThemeTokens, type ThemeTokenKey, type SemanticThemeTokens, } from "./ops/theme-tokens.ts";
|
|
20
20
|
export { chatStreamEventSchema, parseChatStreamFrame, type ChatStreamEvent, type ChatStreamEventType, type ChatStreamFrame, } from "./chat-events.ts";
|
|
21
21
|
export { type PageMeta, type PageDoc, type SiteConfig, type Operation, type EditPlan, type PatchRejectReason, type ApplyPatchMessage, type PatchAckMessage, type ResetToServerMessage, pageMetaSchema, pageDocSchema, pageDocSchemaLenient, siteConfigSchema, operationSchema, editPlanSchema, demoPublishedPages, demoSiteConfig, } from "./schemas.ts";
|
package/dist/index.js
CHANGED
|
@@ -37,7 +37,7 @@ export {
|
|
|
37
37
|
// Constants & helpers
|
|
38
38
|
IMAGE_PLACEHOLDER, isImagePlaceholder,
|
|
39
39
|
// Registry functions
|
|
40
|
-
registerBlock, resetListFieldWarnings, getBlockMeta, getAllBlockMeta, blockAcceptsProp, blockListItemAcceptsKey, getImageFields, getListImageFields, getMediaFields, isFieldInlineEditable, getImageSpec, isChrome, getChromeTypes,
|
|
40
|
+
registerBlock, isBuiltinBlock, resetListFieldWarnings, getBlockMeta, getAllBlockMeta, blockAcceptsProp, blockListItemAcceptsKey, getImageFields, getListImageFields, getMediaFields, isFieldInlineEditable, referenceLabel, getImageSpec, isChrome, getChromeTypes,
|
|
41
41
|
// Backwards-compatible exports
|
|
42
42
|
blockSchemas, allowedBlockTypes,
|
|
43
43
|
// The catalogue a site actually renders — see `declareBlockCatalogue`
|
|
@@ -48,7 +48,7 @@ getPropDisplayName, defaultListItemForBlock,
|
|
|
48
48
|
blockInstanceSchema, blockInstanceSchemaLenient, validateBlockProps, getBlockJsonSchema, } from "./blocks/_registry.js";
|
|
49
49
|
export { defaultPropsForType, declaredDefaultPropsForType, resolveHeadingTag, resolveItemHeadingTag, DEFAULT_HEADING_LEVELS, } from "./blocks/index.js";
|
|
50
50
|
export { blockTypeToCamel, camelToBlockType, blockTypeToLower, lowerToBlockType, } from "./block-names.js";
|
|
51
|
-
export { makeAddBlock, generateBlockId, makeAddItem, generateItemId, ensureItemIds, } from "./ops/builders.js";
|
|
51
|
+
export { makeAddBlock, generateBlockId, makeAddItem, generateItemId, ensureItemIds, withoutGeneratedItemIds, isGeneratedItemId, } from "./ops/builders.js";
|
|
52
52
|
export { THEME_TOKEN_TO_CSS_VARS, themeTokenKeys, semanticThemeTokensSchema, mapSemanticThemeTokens, } from "./ops/theme-tokens.js";
|
|
53
53
|
export { chatStreamEventSchema, parseChatStreamFrame, } from "./chat-events.js";
|
|
54
54
|
export {
|
package/dist/ops/builders.d.ts
CHANGED
|
@@ -65,3 +65,37 @@ export declare function ensureItemIds(blocks: Array<{
|
|
|
65
65
|
type: string;
|
|
66
66
|
props: unknown;
|
|
67
67
|
}>): boolean;
|
|
68
|
+
/**
|
|
69
|
+
* Whether `value` is an id this module generated, rather than one the content
|
|
70
|
+
* arrived with.
|
|
71
|
+
*
|
|
72
|
+
* The shape is the whole point: eight lowercase hex characters behind `i_`. A
|
|
73
|
+
* CMS's own row key — a Sanity `_key`, a Contentful `sys.id`, a Storyblok
|
|
74
|
+
* `_uid` flattened to `id` — does not match, and must not be stripped by
|
|
75
|
+
* anything below.
|
|
76
|
+
*/
|
|
77
|
+
export declare function isGeneratedItemId(value: unknown): boolean;
|
|
78
|
+
/**
|
|
79
|
+
* The same blocks with Avocado's own list-row stamps removed.
|
|
80
|
+
*
|
|
81
|
+
* `ensureItemIds` writes an `id` onto every row of every declared list so the
|
|
82
|
+
* panel can keep rows stable under reordering and a planner can address one by
|
|
83
|
+
* name. It goes into `props`, because `props` is the only thing persisted — and
|
|
84
|
+
* it comes back out of `/draft/pages` indistinguishable from content.
|
|
85
|
+
*
|
|
86
|
+
* An adapter that compares its draft against freshly-read CMS content therefore
|
|
87
|
+
* sees **every block with a list** as changed, permanently and from the first
|
|
88
|
+
* load. On the integration that found this, a one-field edit to one page
|
|
89
|
+
* produced a publish that wanted to rewrite 117 stories; the same publish
|
|
90
|
+
* touches one after this. Diagnosing it took a field-level diff of two JSON
|
|
91
|
+
* blobs, which is why the keys are named here in code rather than described in
|
|
92
|
+
* a document.
|
|
93
|
+
*
|
|
94
|
+
* Returns a deep copy — the draft keeps its ids, which every op still needs.
|
|
95
|
+
* Only ids this module generated are removed, so a row that carries the CMS's
|
|
96
|
+
* own `id` keeps it.
|
|
97
|
+
*/
|
|
98
|
+
export declare function withoutGeneratedItemIds<T extends {
|
|
99
|
+
type: string;
|
|
100
|
+
props: unknown;
|
|
101
|
+
}>(blocks: T[]): T[];
|
package/dist/ops/builders.js
CHANGED
|
@@ -108,6 +108,69 @@ export function ensureItemIds(blocks) {
|
|
|
108
108
|
}
|
|
109
109
|
return changed;
|
|
110
110
|
}
|
|
111
|
+
/**
|
|
112
|
+
* Whether `value` is an id this module generated, rather than one the content
|
|
113
|
+
* arrived with.
|
|
114
|
+
*
|
|
115
|
+
* The shape is the whole point: eight lowercase hex characters behind `i_`. A
|
|
116
|
+
* CMS's own row key — a Sanity `_key`, a Contentful `sys.id`, a Storyblok
|
|
117
|
+
* `_uid` flattened to `id` — does not match, and must not be stripped by
|
|
118
|
+
* anything below.
|
|
119
|
+
*/
|
|
120
|
+
export function isGeneratedItemId(value) {
|
|
121
|
+
return typeof value === "string" && /^i_[0-9a-f]{8}$/.test(value);
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* The same blocks with Avocado's own list-row stamps removed.
|
|
125
|
+
*
|
|
126
|
+
* `ensureItemIds` writes an `id` onto every row of every declared list so the
|
|
127
|
+
* panel can keep rows stable under reordering and a planner can address one by
|
|
128
|
+
* name. It goes into `props`, because `props` is the only thing persisted — and
|
|
129
|
+
* it comes back out of `/draft/pages` indistinguishable from content.
|
|
130
|
+
*
|
|
131
|
+
* An adapter that compares its draft against freshly-read CMS content therefore
|
|
132
|
+
* sees **every block with a list** as changed, permanently and from the first
|
|
133
|
+
* load. On the integration that found this, a one-field edit to one page
|
|
134
|
+
* produced a publish that wanted to rewrite 117 stories; the same publish
|
|
135
|
+
* touches one after this. Diagnosing it took a field-level diff of two JSON
|
|
136
|
+
* blobs, which is why the keys are named here in code rather than described in
|
|
137
|
+
* a document.
|
|
138
|
+
*
|
|
139
|
+
* Returns a deep copy — the draft keeps its ids, which every op still needs.
|
|
140
|
+
* Only ids this module generated are removed, so a row that carries the CMS's
|
|
141
|
+
* own `id` keeps it.
|
|
142
|
+
*/
|
|
143
|
+
export function withoutGeneratedItemIds(blocks) {
|
|
144
|
+
return blocks.map((block) => {
|
|
145
|
+
const listFields = getBlockMeta(block.type)?.listFields;
|
|
146
|
+
const props = block.props;
|
|
147
|
+
if (!listFields || !props || typeof props !== "object" || Array.isArray(props))
|
|
148
|
+
return block;
|
|
149
|
+
const nextProps = { ...props };
|
|
150
|
+
let changed = false;
|
|
151
|
+
for (const listKey of Object.keys(listFields)) {
|
|
152
|
+
const list = nextProps[listKey];
|
|
153
|
+
if (!Array.isArray(list))
|
|
154
|
+
continue;
|
|
155
|
+
let listChanged = false;
|
|
156
|
+
const rows = list.map((item) => {
|
|
157
|
+
if (!item || typeof item !== "object" || Array.isArray(item))
|
|
158
|
+
return item;
|
|
159
|
+
const rec = item;
|
|
160
|
+
if (!isGeneratedItemId(rec.id))
|
|
161
|
+
return item;
|
|
162
|
+
const { id: _dropped, ...rest } = rec;
|
|
163
|
+
listChanged = true;
|
|
164
|
+
return rest;
|
|
165
|
+
});
|
|
166
|
+
if (listChanged) {
|
|
167
|
+
nextProps[listKey] = rows;
|
|
168
|
+
changed = true;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return changed ? { ...block, props: nextProps } : block;
|
|
172
|
+
});
|
|
173
|
+
}
|
|
111
174
|
function randomSuffix() {
|
|
112
175
|
const c = globalThis.crypto;
|
|
113
176
|
if (c?.randomUUID)
|
package/dist/panel-coverage.d.ts
CHANGED
|
@@ -37,7 +37,7 @@ export type PanelFindingCode =
|
|
|
37
37
|
| "phantom_field"
|
|
38
38
|
/** The type name exists in the editor's own registry with a different shape. */
|
|
39
39
|
| "colliding_type"
|
|
40
|
-
/** An image row labelled by its filename
|
|
40
|
+
/** An image row labelled by its filename because the alt field beside it is empty. */
|
|
41
41
|
| "filename_row_label";
|
|
42
42
|
export type PanelFinding = {
|
|
43
43
|
code: PanelFindingCode;
|
|
@@ -73,14 +73,29 @@ type ManifestLike = {
|
|
|
73
73
|
* The label the panel puts on a collapsed list row.
|
|
74
74
|
*
|
|
75
75
|
* Mirrors `PropertyPanel`'s own derivation exactly, including its fallbacks: the
|
|
76
|
-
* first text-ish field with a value, else
|
|
77
|
-
* `Item N`. Kept here so the two cannot drift
|
|
76
|
+
* first text-ish field with a value, else that image's alt text, else the
|
|
77
|
+
* filename of the first image, else `Item N`. Kept here so the two cannot drift
|
|
78
|
+
* — the panel imports this.
|
|
79
|
+
*
|
|
80
|
+
* "With a value" is load-bearing and was, for a while, only true of the comment.
|
|
81
|
+
* The code took the first *declared* candidate and read whatever it held, so a
|
|
82
|
+
* field set listing an empty `title` ahead of a populated `text` labelled the row
|
|
83
|
+
* `Item 4` with the answer sitting one key further along. Each step below scans
|
|
84
|
+
* for content instead of stopping at the first key of the right kind.
|
|
85
|
+
*
|
|
86
|
+
* Alt text outranks the filename because it is the only one of the two a person
|
|
87
|
+
* wrote on purpose. A column of `20250904_075546.webp`, `20250904_081233.webp`
|
|
88
|
+
* tells a reader which row is which no better than `Item 4` did, while the alt
|
|
89
|
+
* beside it already says "Pool bei Sonnenuntergang". The first version of this
|
|
90
|
+
* ranked the filename higher and the check then reported the mismatch as a
|
|
91
|
+
* finding — which told a site its alt text was the better label while the panel
|
|
92
|
+
* had no way to use it. A finding with no remedy gets switched off.
|
|
78
93
|
*/
|
|
79
94
|
export declare function deriveRowLabel(fields: Record<string, FieldMeta>, item: Record<string, unknown>, index: number, options?: {
|
|
80
95
|
discriminator?: string;
|
|
81
96
|
}): {
|
|
82
97
|
label: string;
|
|
83
|
-
source: "text" | "filename" | "fallback";
|
|
98
|
+
source: "text" | "alt" | "filename" | "fallback";
|
|
84
99
|
};
|
|
85
100
|
export declare function panelCoverage(manifest: ManifestLike, pages: PageLike[], options?: {
|
|
86
101
|
builtinTypes?: Record<string, BlockMeta>;
|
package/dist/panel-coverage.js
CHANGED
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@avocadostudio-ai/shared",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
],
|
|
20
20
|
"dependencies": {
|
|
21
21
|
"zod": "^4.3.6",
|
|
22
|
-
"@avocadostudio-ai/richtext": "^0.
|
|
22
|
+
"@avocadostudio-ai/richtext": "^0.6.0"
|
|
23
23
|
},
|
|
24
24
|
"devDependencies": {
|
|
25
25
|
"tsx": "^4.21.0",
|