@avocadostudio-ai/shared 0.4.0 → 0.5.1
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 +18 -0
- package/dist/block-manifest.js +8 -1
- package/dist/blocks/_registry.d.ts +73 -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 +36 -2
- package/dist/editor-block-meta.d.ts +70 -0
- package/dist/editor-block-meta.js +131 -0
- package/dist/index.d.ts +6 -2
- package/dist/index.js +4 -2
- package/dist/ops/builders.d.ts +34 -0
- package/dist/ops/builders.js +63 -0
- package/dist/panel-coverage.d.ts +105 -0
- package/dist/panel-coverage.js +395 -0
- package/package.json +2 -2
|
@@ -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
|
+
// The site has said outright that nothing on the page draws this — a
|
|
53
|
+
// `sectionId`, a `<video poster>`, an input's `placeholder`. See `panelOnly`.
|
|
54
|
+
if (meta.panelOnly)
|
|
55
|
+
return false;
|
|
52
56
|
if (!DRAWN_KINDS.has(meta.kind))
|
|
53
57
|
return false;
|
|
54
58
|
if (meta.kind === "image")
|
|
@@ -122,6 +126,28 @@ function hasContent(value) {
|
|
|
122
126
|
function generalizeIndex(path) {
|
|
123
127
|
return path.replace(/\[\d+\]/g, "[]");
|
|
124
128
|
}
|
|
129
|
+
/**
|
|
130
|
+
* Is this field marked — at its own path, or at a finer one inside it?
|
|
131
|
+
*
|
|
132
|
+
* A prop the manifest calls `buttons` is not always drawn as one element. A
|
|
133
|
+
* renderer that maps over it and marks `buttons[].label` on each button has
|
|
134
|
+
* instrumented that field *better* than a single marker on the container would,
|
|
135
|
+
* and reporting it as missing is the kind of unclosable finding that gets a
|
|
136
|
+
* checker ignored. Villa hit exactly this, and it was one of the six gaps that
|
|
137
|
+
* kept its report off 100%.
|
|
138
|
+
*
|
|
139
|
+
* The prefix test is on `key[` and `key.` rather than on `key`, so `title` is
|
|
140
|
+
* not counted as covering `titleColor`.
|
|
141
|
+
*/
|
|
142
|
+
function isCovered(key, paths) {
|
|
143
|
+
if (paths.has(key))
|
|
144
|
+
return true;
|
|
145
|
+
for (const path of paths) {
|
|
146
|
+
if (path.startsWith(`${key}[`) || path.startsWith(`${key}.`))
|
|
147
|
+
return true;
|
|
148
|
+
}
|
|
149
|
+
return false;
|
|
150
|
+
}
|
|
125
151
|
/**
|
|
126
152
|
* The item fields a list is expected to have markers for, given what is in it.
|
|
127
153
|
*
|
|
@@ -260,7 +286,7 @@ export function editableCoverage(manifest, blocks) {
|
|
|
260
286
|
if (knowsContent && !filled.has(key))
|
|
261
287
|
continue;
|
|
262
288
|
expected += 1;
|
|
263
|
-
if (!
|
|
289
|
+
if (!isCovered(key, paths)) {
|
|
264
290
|
missing.push(key);
|
|
265
291
|
}
|
|
266
292
|
else if (meta.kind === "image" && voids.has(key)) {
|
|
@@ -288,7 +314,15 @@ export function editableCoverage(manifest, blocks) {
|
|
|
288
314
|
for (const [itemKey, itemMeta] of drawnItemFields) {
|
|
289
315
|
const path = `${listKey}[].${itemKey}`;
|
|
290
316
|
expected += 1;
|
|
291
|
-
|
|
317
|
+
/*
|
|
318
|
+
* `isCovered`, not `paths.has`, for the same reason the top-level scan
|
|
319
|
+
* uses it: a row field can itself be a list. `buttons` is marked as
|
|
320
|
+
* `left[].buttons[].label` — the label is the only part of a button a
|
|
321
|
+
* person edits in place — and an exact match called that unmarked,
|
|
322
|
+
* reporting a gap whose only remedy would be a second marker on an
|
|
323
|
+
* element that draws nothing.
|
|
324
|
+
*/
|
|
325
|
+
if (!isCovered(path, paths)) {
|
|
292
326
|
missingItemFields.push(path);
|
|
293
327
|
}
|
|
294
328
|
else if (itemMeta.kind === "image" && voids.has(path)) {
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the property panel should render for one block type.
|
|
3
|
+
*
|
|
4
|
+
* There are two sources and they disagree. The **manifest** is what the site
|
|
5
|
+
* serves over HTTP: its own schema, its own labels, its own list metadata. The
|
|
6
|
+
* **registry** is whatever is registered in the process doing the rendering —
|
|
7
|
+
* and in the editor that is its *own* bundled copy of `@avocadostudio-ai/blocks`,
|
|
8
|
+
* which registers the built-in types on import. A site that calls
|
|
9
|
+
* `registerBlocks` fills the registry inside the *orchestrator*; the editor's
|
|
10
|
+
* in-browser registry is a third source of truth that nobody registered into.
|
|
11
|
+
*
|
|
12
|
+
* This used to be inline in `PropertyPanel`, with the rule "the manifest decides
|
|
13
|
+
* *which* fields exist; the registry supplies richer metadata for any field in
|
|
14
|
+
* both". That is right when the site's `Hero` really is Avocado's `Hero`, and
|
|
15
|
+
* exactly wrong when the site re-registered that name with its own shape — which
|
|
16
|
+
* is the common case, because these are the names blocks *have*. A real
|
|
17
|
+
* integration collided on seven of its eight types and got a property panel
|
|
18
|
+
* describing Avocado's blocks: its `Left column` list was labelled
|
|
19
|
+
* `Left column items`, its `Variant` became `Style variant`.
|
|
20
|
+
*
|
|
21
|
+
* So the rule is now about *who said it*, not *who has it*:
|
|
22
|
+
*
|
|
23
|
+
* - The manifest **declared** the field (it is in `definition.fields`, not merely
|
|
24
|
+
* derived from the JSON schema) → the site said this out loud, and wins.
|
|
25
|
+
* - The manifest only **derived** it → the site said nothing beyond the schema,
|
|
26
|
+
* and the registry's entry is a better answer than an inference, because it
|
|
27
|
+
* carries `imageSpec`, `options`, `inlineEditable` and a human label.
|
|
28
|
+
*
|
|
29
|
+
* It also carries `discriminator` and `itemFieldsByType` through, which the old
|
|
30
|
+
* merge dropped by constructing a two-key object. That was pure data loss: the
|
|
31
|
+
* manifest carries both, and without them the panel narrows nothing, so every
|
|
32
|
+
* row of a polymorphic list is measured against the merged union of all
|
|
33
|
+
* branches. Rows whose branch has no text field then fall back to `Item 4`,
|
|
34
|
+
* `Item 5` — in the case that found this, a bullet list and a pair of CTAs, both
|
|
35
|
+
* fully described in the manifest the panel had in hand.
|
|
36
|
+
*/
|
|
37
|
+
import type { BlockMeta, FieldMeta, ListFieldMeta } from "./blocks/_registry.ts";
|
|
38
|
+
/** The shape this needs from a manifest entry — a structural subset of `BlockDefinition`. */
|
|
39
|
+
export type EditorBlockDefinition = {
|
|
40
|
+
type: string;
|
|
41
|
+
displayName?: string;
|
|
42
|
+
propsSchema: Record<string, unknown>;
|
|
43
|
+
fields?: Record<string, unknown>;
|
|
44
|
+
listFields?: Record<string, unknown>;
|
|
45
|
+
};
|
|
46
|
+
/**
|
|
47
|
+
* Merge one manifest block definition with whatever the local registry knows
|
|
48
|
+
* about the same type.
|
|
49
|
+
*
|
|
50
|
+
* `registryMeta` is `undefined` for a type the registry has never heard of — a
|
|
51
|
+
* net-new block — and then the manifest is simply the answer.
|
|
52
|
+
*/
|
|
53
|
+
export declare function resolveEditorBlockMeta(definition: EditorBlockDefinition, registryMeta: BlockMeta | undefined): BlockMeta;
|
|
54
|
+
/**
|
|
55
|
+
* The fields one list row is actually edited with.
|
|
56
|
+
*
|
|
57
|
+
* Polymorphic lists narrow to the branch named by the row's own discriminant
|
|
58
|
+
* value; anything else — no discriminator, or a value with no branch — falls
|
|
59
|
+
* back to the union in `itemFields`, which is the pre-narrowing behaviour and
|
|
60
|
+
* the right answer when there is nothing to narrow by.
|
|
61
|
+
*
|
|
62
|
+
* Exported because the panel and the editing-surface check must agree exactly.
|
|
63
|
+
* A checker that approximates this reports gaps the panel does not have, and
|
|
64
|
+
* misses the ones it does.
|
|
65
|
+
*/
|
|
66
|
+
export declare function resolveListItemFields(listField: ListFieldMeta, item: Record<string, unknown>): {
|
|
67
|
+
fields: Record<string, FieldMeta>;
|
|
68
|
+
discriminantValue: string;
|
|
69
|
+
matchedBranch: boolean;
|
|
70
|
+
};
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the property panel should render for one block type.
|
|
3
|
+
*
|
|
4
|
+
* There are two sources and they disagree. The **manifest** is what the site
|
|
5
|
+
* serves over HTTP: its own schema, its own labels, its own list metadata. The
|
|
6
|
+
* **registry** is whatever is registered in the process doing the rendering —
|
|
7
|
+
* and in the editor that is its *own* bundled copy of `@avocadostudio-ai/blocks`,
|
|
8
|
+
* which registers the built-in types on import. A site that calls
|
|
9
|
+
* `registerBlocks` fills the registry inside the *orchestrator*; the editor's
|
|
10
|
+
* in-browser registry is a third source of truth that nobody registered into.
|
|
11
|
+
*
|
|
12
|
+
* This used to be inline in `PropertyPanel`, with the rule "the manifest decides
|
|
13
|
+
* *which* fields exist; the registry supplies richer metadata for any field in
|
|
14
|
+
* both". That is right when the site's `Hero` really is Avocado's `Hero`, and
|
|
15
|
+
* exactly wrong when the site re-registered that name with its own shape — which
|
|
16
|
+
* is the common case, because these are the names blocks *have*. A real
|
|
17
|
+
* integration collided on seven of its eight types and got a property panel
|
|
18
|
+
* describing Avocado's blocks: its `Left column` list was labelled
|
|
19
|
+
* `Left column items`, its `Variant` became `Style variant`.
|
|
20
|
+
*
|
|
21
|
+
* So the rule is now about *who said it*, not *who has it*:
|
|
22
|
+
*
|
|
23
|
+
* - The manifest **declared** the field (it is in `definition.fields`, not merely
|
|
24
|
+
* derived from the JSON schema) → the site said this out loud, and wins.
|
|
25
|
+
* - The manifest only **derived** it → the site said nothing beyond the schema,
|
|
26
|
+
* and the registry's entry is a better answer than an inference, because it
|
|
27
|
+
* carries `imageSpec`, `options`, `inlineEditable` and a human label.
|
|
28
|
+
*
|
|
29
|
+
* It also carries `discriminator` and `itemFieldsByType` through, which the old
|
|
30
|
+
* merge dropped by constructing a two-key object. That was pure data loss: the
|
|
31
|
+
* manifest carries both, and without them the panel narrows nothing, so every
|
|
32
|
+
* row of a polymorphic list is measured against the merged union of all
|
|
33
|
+
* branches. Rows whose branch has no text field then fall back to `Item 4`,
|
|
34
|
+
* `Item 5` — in the case that found this, a bullet list and a pair of CTAs, both
|
|
35
|
+
* fully described in the manifest the panel had in hand.
|
|
36
|
+
*/
|
|
37
|
+
import { resolveManifestFieldMeta } from "./block-manifest.js";
|
|
38
|
+
function isRecord(value) {
|
|
39
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
40
|
+
}
|
|
41
|
+
/** Keys the definition stated for itself, as opposed to keys inferred from its schema. */
|
|
42
|
+
function declaredKeys(source) {
|
|
43
|
+
return isRecord(source) ? new Set(Object.keys(source)) : new Set();
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Merge one manifest block definition with whatever the local registry knows
|
|
47
|
+
* about the same type.
|
|
48
|
+
*
|
|
49
|
+
* `registryMeta` is `undefined` for a type the registry has never heard of — a
|
|
50
|
+
* net-new block — and then the manifest is simply the answer.
|
|
51
|
+
*/
|
|
52
|
+
export function resolveEditorBlockMeta(definition, registryMeta) {
|
|
53
|
+
const derived = resolveManifestFieldMeta(definition);
|
|
54
|
+
if (!registryMeta) {
|
|
55
|
+
return {
|
|
56
|
+
displayName: definition.displayName ?? definition.type,
|
|
57
|
+
fields: derived.fields,
|
|
58
|
+
...(Object.keys(derived.listFields).length > 0 ? { listFields: derived.listFields } : {})
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
const declaredFields = declaredKeys(definition.fields);
|
|
62
|
+
const fields = {};
|
|
63
|
+
for (const [key, fromManifest] of Object.entries(derived.fields)) {
|
|
64
|
+
const fromRegistry = registryMeta.fields[key];
|
|
65
|
+
// The site said it out loud, or the registry has nothing to add.
|
|
66
|
+
fields[key] = declaredFields.has(key) || !fromRegistry ? fromManifest : fromRegistry;
|
|
67
|
+
}
|
|
68
|
+
const declaredLists = declaredKeys(definition.listFields);
|
|
69
|
+
const listFields = {};
|
|
70
|
+
for (const [key, fromManifest] of Object.entries(derived.listFields)) {
|
|
71
|
+
const fromRegistry = registryMeta.listFields?.[key];
|
|
72
|
+
if (!fromRegistry || declaredLists.has(key)) {
|
|
73
|
+
listFields[key] = fromManifest;
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
const declaredItems = declaredKeys(isRecord(definition.listFields?.[key])
|
|
77
|
+
? definition.listFields[key].itemFields
|
|
78
|
+
: undefined);
|
|
79
|
+
const itemFields = {};
|
|
80
|
+
for (const [itemKey, itemFromManifest] of Object.entries(fromManifest.itemFields)) {
|
|
81
|
+
const itemFromRegistry = fromRegistry.itemFields[itemKey];
|
|
82
|
+
itemFields[itemKey] =
|
|
83
|
+
declaredItems.has(itemKey) || !itemFromRegistry ? itemFromManifest : itemFromRegistry;
|
|
84
|
+
}
|
|
85
|
+
listFields[key] = {
|
|
86
|
+
...fromManifest,
|
|
87
|
+
...(fromManifest.label ?? fromRegistry.label
|
|
88
|
+
? { label: fromManifest.label ?? fromRegistry.label }
|
|
89
|
+
: {}),
|
|
90
|
+
itemFields,
|
|
91
|
+
// Never synthesised, never dropped: whichever source has a branch map, keep it.
|
|
92
|
+
...(fromManifest.discriminator ?? fromRegistry.discriminator
|
|
93
|
+
? { discriminator: fromManifest.discriminator ?? fromRegistry.discriminator }
|
|
94
|
+
: {}),
|
|
95
|
+
...(fromManifest.itemFieldsByType ?? fromRegistry.itemFieldsByType
|
|
96
|
+
? { itemFieldsByType: fromManifest.itemFieldsByType ?? fromRegistry.itemFieldsByType }
|
|
97
|
+
: {})
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
return {
|
|
101
|
+
...registryMeta,
|
|
102
|
+
displayName: definition.displayName ?? registryMeta.displayName,
|
|
103
|
+
fields,
|
|
104
|
+
...(Object.keys(listFields).length > 0 ? { listFields } : {})
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* The fields one list row is actually edited with.
|
|
109
|
+
*
|
|
110
|
+
* Polymorphic lists narrow to the branch named by the row's own discriminant
|
|
111
|
+
* value; anything else — no discriminator, or a value with no branch — falls
|
|
112
|
+
* back to the union in `itemFields`, which is the pre-narrowing behaviour and
|
|
113
|
+
* the right answer when there is nothing to narrow by.
|
|
114
|
+
*
|
|
115
|
+
* Exported because the panel and the editing-surface check must agree exactly.
|
|
116
|
+
* A checker that approximates this reports gaps the panel does not have, and
|
|
117
|
+
* misses the ones it does.
|
|
118
|
+
*/
|
|
119
|
+
export function resolveListItemFields(listField, item) {
|
|
120
|
+
if (!listField.discriminator) {
|
|
121
|
+
return { fields: listField.itemFields, discriminantValue: "", matchedBranch: false };
|
|
122
|
+
}
|
|
123
|
+
const raw = item[listField.discriminator];
|
|
124
|
+
const discriminantValue = raw === undefined || raw === null ? "" : String(raw);
|
|
125
|
+
const branch = listField.itemFieldsByType?.[discriminantValue];
|
|
126
|
+
return {
|
|
127
|
+
fields: branch ?? listField.itemFields,
|
|
128
|
+
discriminantValue,
|
|
129
|
+
matchedBranch: Boolean(branch)
|
|
130
|
+
};
|
|
131
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -4,14 +4,18 @@ export type { FieldDiffKind, FieldDiff, BlockDiffStatus, BlockDiff, PageDiffStat
|
|
|
4
4
|
export { isImagePath, toAltPath, isAltPath, toImagePath, setPropAtPath } from "./editable-path.ts";
|
|
5
5
|
export { extractMarkedBlocks, editableCoverage, formatEditableCoverage } from "./editable-coverage.ts";
|
|
6
6
|
export type { MarkedBlock, BlockCoverageGap, EditableCoverage } from "./editable-coverage.ts";
|
|
7
|
+
export { panelCoverage, formatPanelCoverage, deriveRowLabel } from "./panel-coverage.ts";
|
|
8
|
+
export type { PanelCoverage, PanelFinding, PanelFindingCode } from "./panel-coverage.ts";
|
|
9
|
+
export { resolveEditorBlockMeta, resolveListItemFields } from "./editor-block-meta.ts";
|
|
10
|
+
export type { EditorBlockDefinition } from "./editor-block-meta.ts";
|
|
7
11
|
export { parseLink, resolveLink, normalizeLinkPath, isKnownRoute, internalPathForUrl, rankLinkTargets, rankFileTargets, suggestLinkTargets, scoreLinkCandidate, suggestLinkTarget, newTabKeyFor, linkAttrs, isFilePath, knownFileExtensions, linksInRichText, type LinkKind, type ParsedLink, type ResolvedLink, type LinkPageOption, type LinkFileOption, type LinkSuggestions, } from "./links.ts";
|
|
8
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";
|
|
9
13
|
export { blockDefinitionSchema, blockManifestSchema, buildBlockManifest, jsonSchemaLikeSchema, validateByJsonSchemaLike, findManifestSchemaIssue, type ManifestSchemaIssue, validateManifestDefaultProps, deriveFieldMetaFromSchema, resolveManifestFieldMeta, isProseMirrorDocSchema, type BlockDefinition, type BlockManifest } from "./block-manifest.ts";
|
|
10
14
|
export { z } from "zod";
|
|
11
|
-
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";
|
|
12
16
|
export { defaultPropsForType, declaredDefaultPropsForType, resolveHeadingTag, resolveItemHeadingTag, DEFAULT_HEADING_LEVELS, } from "./blocks/index.ts";
|
|
13
17
|
export { blockTypeToCamel, camelToBlockType, blockTypeToLower, lowerToBlockType, } from "./block-names.ts";
|
|
14
|
-
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";
|
|
15
19
|
export { THEME_TOKEN_TO_CSS_VARS, themeTokenKeys, semanticThemeTokensSchema, mapSemanticThemeTokens, type ThemeTokenKey, type SemanticThemeTokens, } from "./ops/theme-tokens.ts";
|
|
16
20
|
export { chatStreamEventSchema, parseChatStreamFrame, type ChatStreamEvent, type ChatStreamEventType, type ChatStreamFrame, } from "./chat-events.ts";
|
|
17
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
|
@@ -2,6 +2,8 @@ export { EDITOR_PROTOCOL_VERSION } from "./protocol.js";
|
|
|
2
2
|
export { getConfiguredDraftSecret, getSafeInternalRedirectPath, validateDraftSecret } from "./draft-mode.js";
|
|
3
3
|
export { isImagePath, toAltPath, isAltPath, toImagePath, setPropAtPath } from "./editable-path.js";
|
|
4
4
|
export { extractMarkedBlocks, editableCoverage, formatEditableCoverage } from "./editable-coverage.js";
|
|
5
|
+
export { panelCoverage, formatPanelCoverage, deriveRowLabel } from "./panel-coverage.js";
|
|
6
|
+
export { resolveEditorBlockMeta, resolveListItemFields } from "./editor-block-meta.js";
|
|
5
7
|
export { parseLink, resolveLink, normalizeLinkPath, isKnownRoute, internalPathForUrl, rankLinkTargets, rankFileTargets, suggestLinkTargets, scoreLinkCandidate, suggestLinkTarget, newTabKeyFor, linkAttrs, isFilePath, knownFileExtensions, linksInRichText, } from "./links.js";
|
|
6
8
|
/*
|
|
7
9
|
* The rich-text grammar lives in `@avocadostudio-ai/richtext`, which owns the
|
|
@@ -35,7 +37,7 @@ export {
|
|
|
35
37
|
// Constants & helpers
|
|
36
38
|
IMAGE_PLACEHOLDER, isImagePlaceholder,
|
|
37
39
|
// Registry functions
|
|
38
|
-
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,
|
|
39
41
|
// Backwards-compatible exports
|
|
40
42
|
blockSchemas, allowedBlockTypes,
|
|
41
43
|
// The catalogue a site actually renders — see `declareBlockCatalogue`
|
|
@@ -46,7 +48,7 @@ getPropDisplayName, defaultListItemForBlock,
|
|
|
46
48
|
blockInstanceSchema, blockInstanceSchemaLenient, validateBlockProps, getBlockJsonSchema, } from "./blocks/_registry.js";
|
|
47
49
|
export { defaultPropsForType, declaredDefaultPropsForType, resolveHeadingTag, resolveItemHeadingTag, DEFAULT_HEADING_LEVELS, } from "./blocks/index.js";
|
|
48
50
|
export { blockTypeToCamel, camelToBlockType, blockTypeToLower, lowerToBlockType, } from "./block-names.js";
|
|
49
|
-
export { makeAddBlock, generateBlockId, makeAddItem, generateItemId, ensureItemIds, } from "./ops/builders.js";
|
|
51
|
+
export { makeAddBlock, generateBlockId, makeAddItem, generateItemId, ensureItemIds, withoutGeneratedItemIds, isGeneratedItemId, } from "./ops/builders.js";
|
|
50
52
|
export { THEME_TOKEN_TO_CSS_VARS, themeTokenKeys, semanticThemeTokensSchema, mapSemanticThemeTokens, } from "./ops/theme-tokens.js";
|
|
51
53
|
export { chatStreamEventSchema, parseChatStreamFrame, } from "./chat-events.js";
|
|
52
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)
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Can a human actually edit this site in the property panel?
|
|
3
|
+
*
|
|
4
|
+
* `editableCoverage` answers the same question for the *preview*: which fields
|
|
5
|
+
* carry a marker the overlay can find. This is the panel's half, and it was the
|
|
6
|
+
* half with no check at all — which is why a real integration shipped a panel
|
|
7
|
+
* whose list rows read `Item 4`, `Item 5`, whose labels came from somebody
|
|
8
|
+
* else's block, and whose polymorphic branches were never narrowed. Every one of
|
|
9
|
+
* those was visible in the data the whole time. Nobody was asking.
|
|
10
|
+
*
|
|
11
|
+
* The check needs no browser, no screenshot and no model. It has the manifest
|
|
12
|
+
* (what the panel will render) and the site's own pages (what the rows really
|
|
13
|
+
* contain), and every finding below is a disagreement between the two.
|
|
14
|
+
*
|
|
15
|
+
* It resolves metadata through `resolveEditorBlockMeta` and rows through
|
|
16
|
+
* `resolveListItemFields` — the same functions the panel itself uses — so its
|
|
17
|
+
* findings are the panel's behaviour rather than a model of it. A checker that
|
|
18
|
+
* approximates the panel reports gaps the panel does not have and misses the
|
|
19
|
+
* ones it does, and gets switched off within a week.
|
|
20
|
+
*
|
|
21
|
+
* Pass `builtinTypes` (the registry the *editor* will run with) to get the
|
|
22
|
+
* collision findings. Without it, collisions are simply not reported — an
|
|
23
|
+
* absent input is never evidence.
|
|
24
|
+
*/
|
|
25
|
+
import { type EditorBlockDefinition } from "./editor-block-meta.ts";
|
|
26
|
+
import type { BlockMeta, FieldMeta } from "./blocks/_registry.ts";
|
|
27
|
+
export type PanelFindingCode =
|
|
28
|
+
/** A list row the panel labels `Item N` — present in the content, unidentifiable in the panel. */
|
|
29
|
+
"unlabelled_row"
|
|
30
|
+
/** A row whose discriminant value has no branch, so it is edited against the union of all branches. */
|
|
31
|
+
| "unmatched_branch"
|
|
32
|
+
/** A list declares a discriminator and no branch map, or vice versa. */
|
|
33
|
+
| "incomplete_polymorphism"
|
|
34
|
+
/** A prop the content holds that no field or list describes — uneditable, and invisible. */
|
|
35
|
+
| "orphan_prop"
|
|
36
|
+
/** A field declared for rows that no row ever has. Panel noise. */
|
|
37
|
+
| "phantom_field"
|
|
38
|
+
/** The type name exists in the editor's own registry with a different shape. */
|
|
39
|
+
| "colliding_type"
|
|
40
|
+
/** An image row labelled by its filename because the alt field beside it is empty. */
|
|
41
|
+
| "filename_row_label";
|
|
42
|
+
export type PanelFinding = {
|
|
43
|
+
code: PanelFindingCode;
|
|
44
|
+
blockType: string;
|
|
45
|
+
/** Where the problem is, in the same path grammar operations use. */
|
|
46
|
+
path?: string;
|
|
47
|
+
/** One page slug that exhibits it, so the report points somewhere. */
|
|
48
|
+
exampleSlug?: string;
|
|
49
|
+
/** How many occurrences across everything examined. */
|
|
50
|
+
count: number;
|
|
51
|
+
detail: string;
|
|
52
|
+
};
|
|
53
|
+
export type PanelCoverage = {
|
|
54
|
+
/** List rows examined across every page. */
|
|
55
|
+
rowsExamined: number;
|
|
56
|
+
/** Rows the panel can label from their own content. */
|
|
57
|
+
rowsLabelled: number;
|
|
58
|
+
findings: PanelFinding[];
|
|
59
|
+
/** Block types on a page that the manifest does not describe. Not our business, but worth saying. */
|
|
60
|
+
unknownBlockTypes: string[];
|
|
61
|
+
};
|
|
62
|
+
type PageLike = {
|
|
63
|
+
slug?: string;
|
|
64
|
+
blocks?: Array<{
|
|
65
|
+
type?: string;
|
|
66
|
+
props?: Record<string, unknown>;
|
|
67
|
+
} | null | undefined>;
|
|
68
|
+
};
|
|
69
|
+
type ManifestLike = {
|
|
70
|
+
blocks: EditorBlockDefinition[];
|
|
71
|
+
};
|
|
72
|
+
/**
|
|
73
|
+
* The label the panel puts on a collapsed list row.
|
|
74
|
+
*
|
|
75
|
+
* Mirrors `PropertyPanel`'s own derivation exactly, including its fallbacks: the
|
|
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.
|
|
93
|
+
*/
|
|
94
|
+
export declare function deriveRowLabel(fields: Record<string, FieldMeta>, item: Record<string, unknown>, index: number, options?: {
|
|
95
|
+
discriminator?: string;
|
|
96
|
+
}): {
|
|
97
|
+
label: string;
|
|
98
|
+
source: "text" | "alt" | "filename" | "fallback";
|
|
99
|
+
};
|
|
100
|
+
export declare function panelCoverage(manifest: ManifestLike, pages: PageLike[], options?: {
|
|
101
|
+
builtinTypes?: Record<string, BlockMeta>;
|
|
102
|
+
}): PanelCoverage;
|
|
103
|
+
/** A human-readable report, in the shape `formatEditableCoverage` uses. */
|
|
104
|
+
export declare function formatPanelCoverage(report: PanelCoverage): string;
|
|
105
|
+
export {};
|