@avocadostudio-ai/shared 0.1.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/LICENSE +201 -0
- package/README.md +80 -0
- package/contract/operation.schema.json +752 -0
- package/dist/api-responses.d.ts +62 -0
- package/dist/api-responses.js +68 -0
- package/dist/block-manifest.d.ts +28 -0
- package/dist/block-manifest.js +247 -0
- package/dist/block-names.d.ts +19 -0
- package/dist/block-names.js +52 -0
- package/dist/blocks/_helpers.d.ts +14 -0
- package/dist/blocks/_helpers.js +26 -0
- package/dist/blocks/_registry.d.ts +116 -0
- package/dist/blocks/_registry.js +271 -0
- package/dist/blocks/banner.d.ts +1 -0
- package/dist/blocks/banner.js +34 -0
- package/dist/blocks/card-grid.d.ts +1 -0
- package/dist/blocks/card-grid.js +73 -0
- package/dist/blocks/card.d.ts +1 -0
- package/dist/blocks/card.js +37 -0
- package/dist/blocks/carousel.d.ts +1 -0
- package/dist/blocks/carousel.js +51 -0
- package/dist/blocks/cta.d.ts +1 -0
- package/dist/blocks/cta.js +35 -0
- package/dist/blocks/embed.d.ts +1 -0
- package/dist/blocks/embed.js +30 -0
- package/dist/blocks/faq-accordion.d.ts +1 -0
- package/dist/blocks/faq-accordion.js +30 -0
- package/dist/blocks/feature-grid.d.ts +1 -0
- package/dist/blocks/feature-grid.js +46 -0
- package/dist/blocks/footer.d.ts +1 -0
- package/dist/blocks/footer.js +31 -0
- package/dist/blocks/gallery.d.ts +1 -0
- package/dist/blocks/gallery.js +47 -0
- package/dist/blocks/hero.d.ts +1 -0
- package/dist/blocks/hero.js +48 -0
- package/dist/blocks/index.d.ts +3 -0
- package/dist/blocks/index.js +53 -0
- package/dist/blocks/quote.d.ts +1 -0
- package/dist/blocks/quote.js +32 -0
- package/dist/blocks/rich-text.d.ts +1 -0
- package/dist/blocks/rich-text.js +41 -0
- package/dist/blocks/site-header.d.ts +1 -0
- package/dist/blocks/site-header.js +48 -0
- package/dist/blocks/stats.d.ts +1 -0
- package/dist/blocks/stats.js +42 -0
- package/dist/blocks/table.d.ts +1 -0
- package/dist/blocks/table.js +37 -0
- package/dist/blocks/tabs.d.ts +1 -0
- package/dist/blocks/tabs.js +39 -0
- package/dist/blocks/testimonials.d.ts +1 -0
- package/dist/blocks/testimonials.js +45 -0
- package/dist/blocks/two-column.d.ts +1 -0
- package/dist/blocks/two-column.js +61 -0
- package/dist/blocks/video.d.ts +1 -0
- package/dist/blocks/video.js +33 -0
- package/dist/chat-events.d.ts +475 -0
- package/dist/chat-events.js +137 -0
- package/dist/demo-seed-content.d.ts +3 -0
- package/dist/demo-seed-content.js +1128 -0
- package/dist/draft-mode.d.ts +10 -0
- package/dist/draft-mode.js +29 -0
- package/dist/editable-path.d.ts +20 -0
- package/dist/editable-path.js +101 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +26 -0
- package/dist/ops/builders.d.ts +62 -0
- package/dist/ops/builders.js +111 -0
- package/dist/ops/theme-tokens.d.ts +50 -0
- package/dist/ops/theme-tokens.js +73 -0
- package/dist/protocol.d.ts +7 -0
- package/dist/protocol.js +7 -0
- package/dist/publish-diff.d.ts +67 -0
- package/dist/publish-diff.js +9 -0
- package/dist/schemas.d.ts +321 -0
- package/dist/schemas.js +238 -0
- package/package.json +48 -0
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export type DraftSecretValidationResult = {
|
|
2
|
+
ok: true;
|
|
3
|
+
reason: null;
|
|
4
|
+
} | {
|
|
5
|
+
ok: false;
|
|
6
|
+
reason: "missing_config" | "invalid_secret";
|
|
7
|
+
};
|
|
8
|
+
export declare function getConfiguredDraftSecret(env: Record<string, string | undefined>, keys?: readonly string[]): string | null;
|
|
9
|
+
export declare function validateDraftSecret(receivedSecret: string | null | undefined, env: Record<string, string | undefined>, keys?: readonly string[]): DraftSecretValidationResult;
|
|
10
|
+
export declare function getSafeInternalRedirectPath(value: string | null): string;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// Accept multiple env var names so the same secret works across site and editor.
|
|
2
|
+
// VITE_SITE_DRAFT_SECRET is the editor's name; accepting it here means you can
|
|
3
|
+
// set one var in a shared .env and it works for both site and editor.
|
|
4
|
+
const DEFAULT_DRAFT_SECRET_KEYS = ["DRAFT_MODE_SECRET", "VITE_SITE_DRAFT_SECRET", "NEXT_DRAFT_MODE_SECRET"];
|
|
5
|
+
export function getConfiguredDraftSecret(env, keys = DEFAULT_DRAFT_SECRET_KEYS) {
|
|
6
|
+
for (const key of keys) {
|
|
7
|
+
const value = env[key]?.trim();
|
|
8
|
+
if (value)
|
|
9
|
+
return value;
|
|
10
|
+
}
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
export function validateDraftSecret(receivedSecret, env, keys = DEFAULT_DRAFT_SECRET_KEYS) {
|
|
14
|
+
const configuredSecret = getConfiguredDraftSecret(env, keys);
|
|
15
|
+
const normalizedReceived = receivedSecret?.trim() ?? "";
|
|
16
|
+
if (!configuredSecret)
|
|
17
|
+
return { ok: false, reason: "missing_config" };
|
|
18
|
+
if (!normalizedReceived || normalizedReceived !== configuredSecret)
|
|
19
|
+
return { ok: false, reason: "invalid_secret" };
|
|
20
|
+
return { ok: true, reason: null };
|
|
21
|
+
}
|
|
22
|
+
export function getSafeInternalRedirectPath(value) {
|
|
23
|
+
if (!value)
|
|
24
|
+
return "/";
|
|
25
|
+
const decoded = decodeURIComponent(value).trim();
|
|
26
|
+
if (!decoded.startsWith("/") || decoded.startsWith("//"))
|
|
27
|
+
return "/";
|
|
28
|
+
return decoded;
|
|
29
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utilities for working with editable-target path strings.
|
|
3
|
+
*
|
|
4
|
+
* Paths follow the conventions: `imageUrl`, `cards[0].imageUrl`, `right[0].src`.
|
|
5
|
+
*/
|
|
6
|
+
/** Returns true if the editable path points to an image field. */
|
|
7
|
+
export declare function isImagePath(editablePath: string): boolean;
|
|
8
|
+
/** Derives the companion alt-text path from an image path. Returns the path unchanged if it is not an image path. */
|
|
9
|
+
export declare function toAltPath(editablePath: string): string;
|
|
10
|
+
/**
|
|
11
|
+
* Return a copy of `props` with `value` written at `editablePath`, using
|
|
12
|
+
* copy-on-write so the input (and every untouched subtree) is shared by reference.
|
|
13
|
+
*
|
|
14
|
+
* Used by the live-draft preview store to layer a streamed field value over the
|
|
15
|
+
* committed block props. The path must address structure that already exists in
|
|
16
|
+
* `props`: if an intermediate object/array is missing or an array index is out of
|
|
17
|
+
* range, the original `props` reference is returned unchanged — drafts preview
|
|
18
|
+
* existing editable targets, they never create new blocks/items/fields.
|
|
19
|
+
*/
|
|
20
|
+
export declare function setPropAtPath(props: Record<string, unknown>, editablePath: string, value: unknown): Record<string, unknown>;
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utilities for working with editable-target path strings.
|
|
3
|
+
*
|
|
4
|
+
* Paths follow the conventions: `imageUrl`, `cards[0].imageUrl`, `right[0].src`.
|
|
5
|
+
*/
|
|
6
|
+
/** Matches editable paths that point to an image field (imageUrl or indexed .src). */
|
|
7
|
+
const IMAGE_PATH_RE = /(imageUrl|\.src)$/i;
|
|
8
|
+
/** Returns true if the editable path points to an image field. */
|
|
9
|
+
export function isImagePath(editablePath) {
|
|
10
|
+
return IMAGE_PATH_RE.test(editablePath);
|
|
11
|
+
}
|
|
12
|
+
/** Derives the companion alt-text path from an image path. Returns the path unchanged if it is not an image path. */
|
|
13
|
+
export function toAltPath(editablePath) {
|
|
14
|
+
return editablePath.replace(/imageUrl$/i, "imageAlt").replace(/\.src$/i, ".alt");
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Parse an editable-target path into a flat list of access steps.
|
|
18
|
+
*
|
|
19
|
+
* The grammar mirrors what `flattenPatchToScopedDrafts` (orchestrator planner) and
|
|
20
|
+
* each renderer's `data-editable-target` emit: a dotted sequence of tokens, where
|
|
21
|
+
* each token is `name` or `name[index]`. Examples:
|
|
22
|
+
* "title" → [key title]
|
|
23
|
+
* "headers[0]" → [key headers, idx 0] (array of strings)
|
|
24
|
+
* "cards[0].title" → [key cards, idx 0, key title]
|
|
25
|
+
* "links[0].children[1].label" → [key links, idx 0, key children, idx 1, key label]
|
|
26
|
+
*
|
|
27
|
+
* Returns null for any token that doesn't fit the grammar (so the caller no-ops
|
|
28
|
+
* rather than guessing at an unknown shape).
|
|
29
|
+
*/
|
|
30
|
+
function parseEditablePath(path) {
|
|
31
|
+
if (!path)
|
|
32
|
+
return null;
|
|
33
|
+
const steps = [];
|
|
34
|
+
for (const token of path.split(".")) {
|
|
35
|
+
const match = /^([A-Za-z_][A-Za-z0-9_]*)(\[\d+\])?$/.exec(token);
|
|
36
|
+
if (!match)
|
|
37
|
+
return null;
|
|
38
|
+
steps.push({ kind: "key", key: match[1] });
|
|
39
|
+
if (match[2])
|
|
40
|
+
steps.push({ kind: "index", index: Number(match[2].slice(1, -1)) });
|
|
41
|
+
}
|
|
42
|
+
return steps.length ? steps : null;
|
|
43
|
+
}
|
|
44
|
+
/** Immutably apply the remaining steps to a container, returning whether anything changed. */
|
|
45
|
+
function setSteps(container, steps, value) {
|
|
46
|
+
const [step, ...rest] = steps;
|
|
47
|
+
if (!step)
|
|
48
|
+
return { changed: false, next: container };
|
|
49
|
+
if (step.kind === "key") {
|
|
50
|
+
// A key step expects a plain object. Anything else means the committed props
|
|
51
|
+
// don't have the structure this draft addresses → no-op (never invent it).
|
|
52
|
+
if (!container || typeof container !== "object" || Array.isArray(container)) {
|
|
53
|
+
return { changed: false, next: container };
|
|
54
|
+
}
|
|
55
|
+
const obj = container;
|
|
56
|
+
if (rest.length === 0) {
|
|
57
|
+
if (obj[step.key] === value)
|
|
58
|
+
return { changed: false, next: obj };
|
|
59
|
+
return { changed: true, next: { ...obj, [step.key]: value } };
|
|
60
|
+
}
|
|
61
|
+
const res = setSteps(obj[step.key], rest, value);
|
|
62
|
+
if (!res.changed)
|
|
63
|
+
return { changed: false, next: obj };
|
|
64
|
+
return { changed: true, next: { ...obj, [step.key]: res.next } };
|
|
65
|
+
}
|
|
66
|
+
// An index step expects an array, with the index in range. Out-of-range → no-op
|
|
67
|
+
// so a draft for `cards[5]` never grows a 3-card list.
|
|
68
|
+
if (!Array.isArray(container) || step.index < 0 || step.index >= container.length) {
|
|
69
|
+
return { changed: false, next: container };
|
|
70
|
+
}
|
|
71
|
+
if (rest.length === 0) {
|
|
72
|
+
if (container[step.index] === value)
|
|
73
|
+
return { changed: false, next: container };
|
|
74
|
+
const copy = container.slice();
|
|
75
|
+
copy[step.index] = value;
|
|
76
|
+
return { changed: true, next: copy };
|
|
77
|
+
}
|
|
78
|
+
const res = setSteps(container[step.index], rest, value);
|
|
79
|
+
if (!res.changed)
|
|
80
|
+
return { changed: false, next: container };
|
|
81
|
+
const copy = container.slice();
|
|
82
|
+
copy[step.index] = res.next;
|
|
83
|
+
return { changed: true, next: copy };
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Return a copy of `props` with `value` written at `editablePath`, using
|
|
87
|
+
* copy-on-write so the input (and every untouched subtree) is shared by reference.
|
|
88
|
+
*
|
|
89
|
+
* Used by the live-draft preview store to layer a streamed field value over the
|
|
90
|
+
* committed block props. The path must address structure that already exists in
|
|
91
|
+
* `props`: if an intermediate object/array is missing or an array index is out of
|
|
92
|
+
* range, the original `props` reference is returned unchanged — drafts preview
|
|
93
|
+
* existing editable targets, they never create new blocks/items/fields.
|
|
94
|
+
*/
|
|
95
|
+
export function setPropAtPath(props, editablePath, value) {
|
|
96
|
+
const steps = parseEditablePath(editablePath);
|
|
97
|
+
if (!steps)
|
|
98
|
+
return props;
|
|
99
|
+
const res = setSteps(props, steps, value);
|
|
100
|
+
return res.changed ? res.next : props;
|
|
101
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export { EDITOR_PROTOCOL_VERSION } from "./protocol.ts";
|
|
2
|
+
export { getConfiguredDraftSecret, getSafeInternalRedirectPath, validateDraftSecret, type DraftSecretValidationResult } from "./draft-mode.ts";
|
|
3
|
+
export type { FieldDiffKind, FieldDiff, BlockDiffStatus, BlockDiff, PageDiffStatus, PageDiff, PublishDiff, SiteConfigFieldDiff, SiteConfigDiff, } from "./publish-diff.ts";
|
|
4
|
+
export { isImagePath, toAltPath, setPropAtPath } from "./editable-path.ts";
|
|
5
|
+
export { blockDefinitionSchema, blockManifestSchema, jsonSchemaLikeSchema, validateByJsonSchemaLike, validateManifestDefaultProps, deriveFieldMetaFromSchema, type BlockDefinition, type BlockManifest } from "./block-manifest.ts";
|
|
6
|
+
export { type FieldKind, type ImageSpec, type FieldMeta, type ListFieldMeta, type BlockMeta, type BlockType, type BlockInstance, type BlockRegistration, IMAGE_PLACEHOLDER, isImagePlaceholder, registerBlock, getBlockMeta, getAllBlockMeta, getImageFields, getListImageFields, isFieldInlineEditable, getImageSpec, isChrome, getChromeTypes, blockSchemas, allowedBlockTypes, getPropDisplayName, defaultListItemForBlock, blockInstanceSchema, blockInstanceSchemaLenient, validateBlockProps, getBlockJsonSchema, } from "./blocks/_registry.ts";
|
|
7
|
+
export { defaultPropsForType, resolveHeadingTag, resolveItemHeadingTag, DEFAULT_HEADING_LEVELS, } from "./blocks/index.ts";
|
|
8
|
+
export { blockTypeToCamel, camelToBlockType, blockTypeToLower, lowerToBlockType, } from "./block-names.ts";
|
|
9
|
+
export { makeAddBlock, generateBlockId, makeAddItem, generateItemId, ensureItemIds, type AddBlockOp, type MakeAddBlockOptions, type AddItemOp, type MakeAddItemOptions, } from "./ops/builders.ts";
|
|
10
|
+
export { THEME_TOKEN_TO_CSS_VARS, themeTokenKeys, semanticThemeTokensSchema, mapSemanticThemeTokens, type ThemeTokenKey, type SemanticThemeTokens, } from "./ops/theme-tokens.ts";
|
|
11
|
+
export { chatStreamEventSchema, parseChatStreamFrame, type ChatStreamEvent, type ChatStreamEventType, type ChatStreamFrame, } from "./chat-events.ts";
|
|
12
|
+
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";
|
|
13
|
+
export { assistantResponseSchema, chatStartResponseSchema, slugsResponseSchema, bootstrapResponseSchema, cancelResponseSchema, type AssistantResponseParsed, } from "./api-responses.ts";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export { EDITOR_PROTOCOL_VERSION } from "./protocol.js";
|
|
2
|
+
export { getConfiguredDraftSecret, getSafeInternalRedirectPath, validateDraftSecret } from "./draft-mode.js";
|
|
3
|
+
export { isImagePath, toAltPath, setPropAtPath } from "./editable-path.js";
|
|
4
|
+
export { blockDefinitionSchema, blockManifestSchema, jsonSchemaLikeSchema, validateByJsonSchemaLike, validateManifestDefaultProps, deriveFieldMetaFromSchema } from "./block-manifest.js";
|
|
5
|
+
export {
|
|
6
|
+
// Constants & helpers
|
|
7
|
+
IMAGE_PLACEHOLDER, isImagePlaceholder,
|
|
8
|
+
// Registry functions
|
|
9
|
+
registerBlock, getBlockMeta, getAllBlockMeta, getImageFields, getListImageFields, isFieldInlineEditable, getImageSpec, isChrome, getChromeTypes,
|
|
10
|
+
// Backwards-compatible exports
|
|
11
|
+
blockSchemas, allowedBlockTypes,
|
|
12
|
+
// Utility functions
|
|
13
|
+
getPropDisplayName, defaultListItemForBlock,
|
|
14
|
+
// Schemas & validation
|
|
15
|
+
blockInstanceSchema, blockInstanceSchemaLenient, validateBlockProps, getBlockJsonSchema, } from "./blocks/_registry.js";
|
|
16
|
+
export { defaultPropsForType, resolveHeadingTag, resolveItemHeadingTag, DEFAULT_HEADING_LEVELS, } from "./blocks/index.js";
|
|
17
|
+
export { blockTypeToCamel, camelToBlockType, blockTypeToLower, lowerToBlockType, } from "./block-names.js";
|
|
18
|
+
export { makeAddBlock, generateBlockId, makeAddItem, generateItemId, ensureItemIds, } from "./ops/builders.js";
|
|
19
|
+
export { THEME_TOKEN_TO_CSS_VARS, themeTokenKeys, semanticThemeTokensSchema, mapSemanticThemeTokens, } from "./ops/theme-tokens.js";
|
|
20
|
+
export { chatStreamEventSchema, parseChatStreamFrame, } from "./chat-events.js";
|
|
21
|
+
export {
|
|
22
|
+
// Schemas
|
|
23
|
+
pageMetaSchema, pageDocSchema, pageDocSchemaLenient, siteConfigSchema, operationSchema, editPlanSchema,
|
|
24
|
+
// Demo data
|
|
25
|
+
demoPublishedPages, demoSiteConfig, } from "./schemas.js";
|
|
26
|
+
export { assistantResponseSchema, chatStartResponseSchema, slugsResponseSchema, bootstrapResponseSchema, cancelResponseSchema, } from "./api-responses.js";
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import type { Operation } from "../schemas.ts";
|
|
2
|
+
import type { BlockType } from "./../blocks/_registry.ts";
|
|
3
|
+
export type AddBlockOp = Extract<Operation, {
|
|
4
|
+
op: "add_block";
|
|
5
|
+
}>;
|
|
6
|
+
export type MakeAddBlockOptions = {
|
|
7
|
+
/** Insert the new block immediately after this block id. Omit to insert at the top of the page. */
|
|
8
|
+
afterBlockId?: string;
|
|
9
|
+
/** Override the auto-generated block id. Must be unique within the target page. */
|
|
10
|
+
id?: string;
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* Build a canonical `add_block` op from a block type and partial props.
|
|
14
|
+
*
|
|
15
|
+
* The returned op's `block` merges `defaultPropsForType(type)` with `partialProps`
|
|
16
|
+
* (partial wins) and carries a generated id, so it is valid against the strict
|
|
17
|
+
* operation schema and ready to send to the orchestrator.
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* makeAddBlock("/", "Hero", { heading: "Welcome" })
|
|
21
|
+
* // → { op: "add_block", pageSlug: "/", block: { id: "b_hero_a1b2c3d4", type: "Hero", props: { ...defaults, heading: "Welcome" } } }
|
|
22
|
+
*/
|
|
23
|
+
export declare function makeAddBlock(pageSlug: string, type: BlockType, partialProps?: Record<string, unknown>, options?: MakeAddBlockOptions): AddBlockOp;
|
|
24
|
+
/** Generate a readable, collision-resistant block id, e.g. `b_hero_a1b2c3d4`. */
|
|
25
|
+
export declare function generateBlockId(type: string): string;
|
|
26
|
+
export type AddItemOp = Extract<Operation, {
|
|
27
|
+
op: "add_item";
|
|
28
|
+
}>;
|
|
29
|
+
export type MakeAddItemOptions = {
|
|
30
|
+
/** Insert the new item immediately after the item with this id. Omit to append. */
|
|
31
|
+
afterItemId?: string;
|
|
32
|
+
/** Override the auto-generated item id. */
|
|
33
|
+
id?: string;
|
|
34
|
+
};
|
|
35
|
+
/**
|
|
36
|
+
* Build a canonical `add_item` op. The item is given a stable `id` (generated if
|
|
37
|
+
* the caller didn't supply one) so it can be targeted by later ops without a
|
|
38
|
+
* fragile array index.
|
|
39
|
+
*
|
|
40
|
+
* @example
|
|
41
|
+
* makeAddItem("/", "b_features", "features", { title: "New", description: "..." })
|
|
42
|
+
* // → { op: "add_item", pageSlug: "/", blockId: "b_features", listKey: "features",
|
|
43
|
+
* // item: { id: "i_a1b2c3d4", title: "New", description: "..." } }
|
|
44
|
+
*/
|
|
45
|
+
export declare function makeAddItem(pageSlug: string, blockId: string, listKey: string, item?: Record<string, unknown>, options?: MakeAddItemOptions): AddItemOp;
|
|
46
|
+
/** Generate a readable, collision-resistant list-item id, e.g. `i_a1b2c3d4`. */
|
|
47
|
+
export declare function generateItemId(): string;
|
|
48
|
+
/**
|
|
49
|
+
* Backfill stable `id`s onto every list item declared by a block's manifest
|
|
50
|
+
* `listFields`, mirroring how blocks carry stable ids. Mutates `blocks` in place
|
|
51
|
+
* and returns `true` if anything changed (so callers can persist).
|
|
52
|
+
*
|
|
53
|
+
* Items that already have a non-empty string `id` are left untouched, so ids
|
|
54
|
+
* stay stable across loads — which is what lets a planner read an id via
|
|
55
|
+
* `get_page` and have it still resolve at apply time. Only object items in
|
|
56
|
+
* declared lists are touched (e.g. Table `rows`, which are `string[][]`, have no
|
|
57
|
+
* place for an id and are skipped — those ops fall back to positional `index`).
|
|
58
|
+
*/
|
|
59
|
+
export declare function ensureItemIds(blocks: Array<{
|
|
60
|
+
type: string;
|
|
61
|
+
props: unknown;
|
|
62
|
+
}>): boolean;
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
// Ergonomic builders for Avocado `Operation`s.
|
|
2
|
+
//
|
|
3
|
+
// The canonical `add_block` op carries a fully-formed `BlockInstance`
|
|
4
|
+
// (`{ id, type, props }`) under `op.block` — the ops engine requires the id to
|
|
5
|
+
// be present and unique, and validates props against the per-type schema. That
|
|
6
|
+
// canonical shape is exactly right for the wire/persisted op, but heavy to
|
|
7
|
+
// construct by hand for the common "add a block of type X" intent: the caller
|
|
8
|
+
// must mint an id and supply a full prop set.
|
|
9
|
+
//
|
|
10
|
+
// `makeAddBlock` closes that gap without denormalizing the op: it generates a
|
|
11
|
+
// collision-resistant id and fills `defaultPropsForType(type)` under any
|
|
12
|
+
// partial props you pass, returning a canonical, schema-valid `add_block`.
|
|
13
|
+
//
|
|
14
|
+
// External integrators: prefer this over hand-constructing the op (and never
|
|
15
|
+
// hand-write the `Operation` union — import it from this package, or validate
|
|
16
|
+
// against `contract/operation.schema.json`).
|
|
17
|
+
import { getBlockMeta } from "./../blocks/_registry.js";
|
|
18
|
+
import { defaultPropsForType } from "../blocks/index.js";
|
|
19
|
+
/**
|
|
20
|
+
* Build a canonical `add_block` op from a block type and partial props.
|
|
21
|
+
*
|
|
22
|
+
* The returned op's `block` merges `defaultPropsForType(type)` with `partialProps`
|
|
23
|
+
* (partial wins) and carries a generated id, so it is valid against the strict
|
|
24
|
+
* operation schema and ready to send to the orchestrator.
|
|
25
|
+
*
|
|
26
|
+
* @example
|
|
27
|
+
* makeAddBlock("/", "Hero", { heading: "Welcome" })
|
|
28
|
+
* // → { op: "add_block", pageSlug: "/", block: { id: "b_hero_a1b2c3d4", type: "Hero", props: { ...defaults, heading: "Welcome" } } }
|
|
29
|
+
*/
|
|
30
|
+
export function makeAddBlock(pageSlug, type, partialProps = {}, options = {}) {
|
|
31
|
+
const block = {
|
|
32
|
+
id: options.id ?? generateBlockId(type),
|
|
33
|
+
type,
|
|
34
|
+
props: { ...defaultPropsForType(type), ...partialProps },
|
|
35
|
+
};
|
|
36
|
+
return options.afterBlockId
|
|
37
|
+
? { op: "add_block", pageSlug, afterBlockId: options.afterBlockId, block }
|
|
38
|
+
: { op: "add_block", pageSlug, block };
|
|
39
|
+
}
|
|
40
|
+
/** Generate a readable, collision-resistant block id, e.g. `b_hero_a1b2c3d4`. */
|
|
41
|
+
export function generateBlockId(type) {
|
|
42
|
+
const slug = String(type)
|
|
43
|
+
.toLowerCase()
|
|
44
|
+
.replace(/[^a-z0-9]+/g, "_")
|
|
45
|
+
.replace(/^_+|_+$/g, "") || "block";
|
|
46
|
+
return `b_${slug}_${randomSuffix()}`;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Build a canonical `add_item` op. The item is given a stable `id` (generated if
|
|
50
|
+
* the caller didn't supply one) so it can be targeted by later ops without a
|
|
51
|
+
* fragile array index.
|
|
52
|
+
*
|
|
53
|
+
* @example
|
|
54
|
+
* makeAddItem("/", "b_features", "features", { title: "New", description: "..." })
|
|
55
|
+
* // → { op: "add_item", pageSlug: "/", blockId: "b_features", listKey: "features",
|
|
56
|
+
* // item: { id: "i_a1b2c3d4", title: "New", description: "..." } }
|
|
57
|
+
*/
|
|
58
|
+
export function makeAddItem(pageSlug, blockId, listKey, item = {}, options = {}) {
|
|
59
|
+
const id = options.id ?? (typeof item.id === "string" && item.id ? item.id : generateItemId());
|
|
60
|
+
const withId = { ...item, id };
|
|
61
|
+
return options.afterItemId
|
|
62
|
+
? { op: "add_item", pageSlug, blockId, listKey, item: withId, afterItemId: options.afterItemId }
|
|
63
|
+
: { op: "add_item", pageSlug, blockId, listKey, item: withId };
|
|
64
|
+
}
|
|
65
|
+
/** Generate a readable, collision-resistant list-item id, e.g. `i_a1b2c3d4`. */
|
|
66
|
+
export function generateItemId() {
|
|
67
|
+
return `i_${randomSuffix()}`;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Backfill stable `id`s onto every list item declared by a block's manifest
|
|
71
|
+
* `listFields`, mirroring how blocks carry stable ids. Mutates `blocks` in place
|
|
72
|
+
* and returns `true` if anything changed (so callers can persist).
|
|
73
|
+
*
|
|
74
|
+
* Items that already have a non-empty string `id` are left untouched, so ids
|
|
75
|
+
* stay stable across loads — which is what lets a planner read an id via
|
|
76
|
+
* `get_page` and have it still resolve at apply time. Only object items in
|
|
77
|
+
* declared lists are touched (e.g. Table `rows`, which are `string[][]`, have no
|
|
78
|
+
* place for an id and are skipped — those ops fall back to positional `index`).
|
|
79
|
+
*/
|
|
80
|
+
export function ensureItemIds(blocks) {
|
|
81
|
+
let changed = false;
|
|
82
|
+
for (const block of blocks) {
|
|
83
|
+
const listFields = getBlockMeta(block.type)?.listFields;
|
|
84
|
+
if (!listFields)
|
|
85
|
+
continue;
|
|
86
|
+
const props = block.props;
|
|
87
|
+
if (!props || typeof props !== "object" || Array.isArray(props))
|
|
88
|
+
continue;
|
|
89
|
+
for (const listKey of Object.keys(listFields)) {
|
|
90
|
+
const list = props[listKey];
|
|
91
|
+
if (!Array.isArray(list))
|
|
92
|
+
continue;
|
|
93
|
+
for (const item of list) {
|
|
94
|
+
if (!item || typeof item !== "object" || Array.isArray(item))
|
|
95
|
+
continue;
|
|
96
|
+
const rec = item;
|
|
97
|
+
if (typeof rec.id !== "string" || rec.id.length === 0) {
|
|
98
|
+
rec.id = generateItemId();
|
|
99
|
+
changed = true;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return changed;
|
|
105
|
+
}
|
|
106
|
+
function randomSuffix() {
|
|
107
|
+
const c = globalThis.crypto;
|
|
108
|
+
if (c?.randomUUID)
|
|
109
|
+
return c.randomUUID().replace(/-/g, "").slice(0, 8);
|
|
110
|
+
return Math.random().toString(36).slice(2, 10).padEnd(8, "0");
|
|
111
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
/**
|
|
3
|
+
* Canonical map: semantic token → the CSS custom properties it sets.
|
|
4
|
+
*
|
|
5
|
+
* One semantic token may fan out to several vars so that intuitive requests
|
|
6
|
+
* ("round the corners", "use a serif heading") change everything a user would
|
|
7
|
+
* expect in one shot. Keep this list conservative and well-understood — every
|
|
8
|
+
* key here becomes part of the public operation contract.
|
|
9
|
+
*/
|
|
10
|
+
export declare const THEME_TOKEN_TO_CSS_VARS: {
|
|
11
|
+
readonly brandColor: readonly ["--brand", "--brand-hover", "--link"];
|
|
12
|
+
readonly accentColor: readonly ["--accent", "--accent-hover"];
|
|
13
|
+
readonly backgroundColor: readonly ["--bg-0", "--bg-000"];
|
|
14
|
+
readonly surfaceColor: readonly ["--surface", "--card-bg", "--bg-100"];
|
|
15
|
+
readonly headingColor: readonly ["--heading", "--text-100"];
|
|
16
|
+
readonly textColor: readonly ["--body", "--text-200"];
|
|
17
|
+
readonly mutedTextColor: readonly ["--body-secondary", "--text-300", "--caption"];
|
|
18
|
+
readonly headingFont: readonly ["--font-heading"];
|
|
19
|
+
readonly bodyFont: readonly ["--font-body"];
|
|
20
|
+
readonly radius: readonly ["--radius", "--radius-btn", "--radius-card", "--radius-feature", "--radius-input"];
|
|
21
|
+
};
|
|
22
|
+
export type ThemeTokenKey = keyof typeof THEME_TOKEN_TO_CSS_VARS;
|
|
23
|
+
export declare const themeTokenKeys: ThemeTokenKey[];
|
|
24
|
+
/**
|
|
25
|
+
* Zod shape for the semantic patch carried by `update_theme.patch`. Every token
|
|
26
|
+
* is an optional string (a color, font stack, or length). Set a token to the
|
|
27
|
+
* empty string to clear its overrides and fall back to the theme default.
|
|
28
|
+
*/
|
|
29
|
+
export declare const semanticThemeTokensSchema: z.ZodObject<{
|
|
30
|
+
brandColor: z.ZodOptional<z.ZodString>;
|
|
31
|
+
accentColor: z.ZodOptional<z.ZodString>;
|
|
32
|
+
backgroundColor: z.ZodOptional<z.ZodString>;
|
|
33
|
+
surfaceColor: z.ZodOptional<z.ZodString>;
|
|
34
|
+
headingColor: z.ZodOptional<z.ZodString>;
|
|
35
|
+
textColor: z.ZodOptional<z.ZodString>;
|
|
36
|
+
mutedTextColor: z.ZodOptional<z.ZodString>;
|
|
37
|
+
headingFont: z.ZodOptional<z.ZodString>;
|
|
38
|
+
bodyFont: z.ZodOptional<z.ZodString>;
|
|
39
|
+
radius: z.ZodOptional<z.ZodString>;
|
|
40
|
+
}, z.core.$strip>;
|
|
41
|
+
export type SemanticThemeTokens = z.infer<typeof semanticThemeTokensSchema>;
|
|
42
|
+
/**
|
|
43
|
+
* Expand a semantic token patch into a flat CSS-variable map.
|
|
44
|
+
*
|
|
45
|
+
* Empty-string values are preserved (mapped to each underlying var) so the
|
|
46
|
+
* engine can interpret them as "clear this override". Raw `cssVars` from the op
|
|
47
|
+
* should be merged AFTER this result so explicit vars win over the semantic
|
|
48
|
+
* expansion.
|
|
49
|
+
*/
|
|
50
|
+
export declare function mapSemanticThemeTokens(patch: SemanticThemeTokens): Record<string, string>;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// Semantic theme tokens for the `update_theme` operation.
|
|
2
|
+
//
|
|
3
|
+
// Block renderers style themselves entirely from CSS custom properties (see
|
|
4
|
+
// `packages/blocks/src/blocks/_tokens.css`), and a site's overrides live in
|
|
5
|
+
// `siteConfig.themeOverrides` (a raw `--var` → value map) consumed at runtime
|
|
6
|
+
// by the site's `ThemeOverrides` component.
|
|
7
|
+
//
|
|
8
|
+
// Exposing those raw var names directly to the planner is error-prone — the
|
|
9
|
+
// LLM has to know that "make the brand blue" means `--brand` (and `--link`,
|
|
10
|
+
// and `--brand-hover`). Instead the `update_theme` op accepts a small, stable
|
|
11
|
+
// SEMANTIC vocabulary (meaning, not var names) which the ops engine expands to
|
|
12
|
+
// the underlying CSS variables via the table below. Integrators who need a var
|
|
13
|
+
// we don't model can still pass a raw `cssVars` map on the op as an escape
|
|
14
|
+
// hatch (merged after the semantic expansion, so raw always wins).
|
|
15
|
+
import { z } from "zod";
|
|
16
|
+
/**
|
|
17
|
+
* Canonical map: semantic token → the CSS custom properties it sets.
|
|
18
|
+
*
|
|
19
|
+
* One semantic token may fan out to several vars so that intuitive requests
|
|
20
|
+
* ("round the corners", "use a serif heading") change everything a user would
|
|
21
|
+
* expect in one shot. Keep this list conservative and well-understood — every
|
|
22
|
+
* key here becomes part of the public operation contract.
|
|
23
|
+
*/
|
|
24
|
+
export const THEME_TOKEN_TO_CSS_VARS = {
|
|
25
|
+
brandColor: ["--brand", "--brand-hover", "--link"],
|
|
26
|
+
accentColor: ["--accent", "--accent-hover"],
|
|
27
|
+
backgroundColor: ["--bg-0", "--bg-000"],
|
|
28
|
+
surfaceColor: ["--surface", "--card-bg", "--bg-100"],
|
|
29
|
+
headingColor: ["--heading", "--text-100"],
|
|
30
|
+
textColor: ["--body", "--text-200"],
|
|
31
|
+
mutedTextColor: ["--body-secondary", "--text-300", "--caption"],
|
|
32
|
+
headingFont: ["--font-heading"],
|
|
33
|
+
bodyFont: ["--font-body"],
|
|
34
|
+
radius: ["--radius", "--radius-btn", "--radius-card", "--radius-feature", "--radius-input"],
|
|
35
|
+
};
|
|
36
|
+
export const themeTokenKeys = Object.keys(THEME_TOKEN_TO_CSS_VARS);
|
|
37
|
+
/**
|
|
38
|
+
* Zod shape for the semantic patch carried by `update_theme.patch`. Every token
|
|
39
|
+
* is an optional string (a color, font stack, or length). Set a token to the
|
|
40
|
+
* empty string to clear its overrides and fall back to the theme default.
|
|
41
|
+
*/
|
|
42
|
+
export const semanticThemeTokensSchema = z.object({
|
|
43
|
+
brandColor: z.string().optional().describe("Primary brand color (sets --brand, --brand-hover, --link). Any CSS color."),
|
|
44
|
+
accentColor: z.string().optional().describe("Secondary accent color (sets --accent, --accent-hover)."),
|
|
45
|
+
backgroundColor: z.string().optional().describe("Page background color (sets --bg-0, --bg-000)."),
|
|
46
|
+
surfaceColor: z.string().optional().describe("Card/panel surface color (sets --surface, --card-bg, --bg-100)."),
|
|
47
|
+
headingColor: z.string().optional().describe("Heading text color (sets --heading, --text-100)."),
|
|
48
|
+
textColor: z.string().optional().describe("Body text color (sets --body, --text-200)."),
|
|
49
|
+
mutedTextColor: z.string().optional().describe("Secondary/muted text color (sets --body-secondary, --text-300, --caption)."),
|
|
50
|
+
headingFont: z.string().optional().describe("Heading font stack (sets --font-heading), e.g. 'Georgia, serif'."),
|
|
51
|
+
bodyFont: z.string().optional().describe("Body font stack (sets --font-body)."),
|
|
52
|
+
radius: z.string().optional().describe("Corner radius applied across buttons/cards/inputs (sets --radius and friends), e.g. '12px'."),
|
|
53
|
+
});
|
|
54
|
+
/**
|
|
55
|
+
* Expand a semantic token patch into a flat CSS-variable map.
|
|
56
|
+
*
|
|
57
|
+
* Empty-string values are preserved (mapped to each underlying var) so the
|
|
58
|
+
* engine can interpret them as "clear this override". Raw `cssVars` from the op
|
|
59
|
+
* should be merged AFTER this result so explicit vars win over the semantic
|
|
60
|
+
* expansion.
|
|
61
|
+
*/
|
|
62
|
+
export function mapSemanticThemeTokens(patch) {
|
|
63
|
+
const out = {};
|
|
64
|
+
for (const key of themeTokenKeys) {
|
|
65
|
+
const value = patch[key];
|
|
66
|
+
if (value === undefined)
|
|
67
|
+
continue;
|
|
68
|
+
for (const cssVar of THEME_TOKEN_TO_CSS_VARS[key]) {
|
|
69
|
+
out[cssVar] = value;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return out;
|
|
73
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Identifier for the postMessage protocol used between the editor and the
|
|
3
|
+
* preview iframe (preview-adapter). Bumped only when the wire shape of
|
|
4
|
+
* messages changes in an incompatible way. Self-host CLI checks this at
|
|
5
|
+
* boot against the orchestrator's reported value to warn on drift.
|
|
6
|
+
*/
|
|
7
|
+
export declare const EDITOR_PROTOCOL_VERSION = "site-editor/v1";
|
package/dist/protocol.js
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Identifier for the postMessage protocol used between the editor and the
|
|
3
|
+
* preview iframe (preview-adapter). Bumped only when the wire shape of
|
|
4
|
+
* messages changes in an incompatible way. Self-host CLI checks this at
|
|
5
|
+
* boot against the orchestrator's reported value to warn on drift.
|
|
6
|
+
*/
|
|
7
|
+
export const EDITOR_PROTOCOL_VERSION = "site-editor/v1";
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Types for the "what will change on publish" diff.
|
|
3
|
+
*
|
|
4
|
+
* The orchestrator computes a PublishDiff by comparing session draft pages
|
|
5
|
+
* to the currently-published pages. Shape is intentionally lightweight so
|
|
6
|
+
* the editor (and later the immersive widget) can render it without extra
|
|
7
|
+
* work.
|
|
8
|
+
*/
|
|
9
|
+
export type FieldDiffKind = "text" | "image" | "other";
|
|
10
|
+
export type FieldDiff = {
|
|
11
|
+
/** Dotted/bracketed path inside the block's props, e.g. `heading` or `items[2].quote`. */
|
|
12
|
+
path: string;
|
|
13
|
+
before: unknown;
|
|
14
|
+
after: unknown;
|
|
15
|
+
/** Rendering hint for the UI (text diff vs thumbnail vs generic). */
|
|
16
|
+
kind: FieldDiffKind;
|
|
17
|
+
};
|
|
18
|
+
export type BlockDiffStatus = "added" | "removed" | "modified" | "moved" | "unchanged";
|
|
19
|
+
export type BlockDiff = {
|
|
20
|
+
blockId: string;
|
|
21
|
+
type: string;
|
|
22
|
+
status: BlockDiffStatus;
|
|
23
|
+
/** When status === "modified" (or "moved" with prop changes) — field-level leaves that changed. */
|
|
24
|
+
fieldDiffs?: FieldDiff[];
|
|
25
|
+
/** 0-based index in published page. Omitted when block was added. */
|
|
26
|
+
positionBefore?: number;
|
|
27
|
+
/** 0-based index in draft page. Omitted when block was removed. */
|
|
28
|
+
positionAfter?: number;
|
|
29
|
+
};
|
|
30
|
+
export type PageDiffStatus = "added" | "removed" | "modified" | "unchanged";
|
|
31
|
+
export type PageDiff = {
|
|
32
|
+
slug: string;
|
|
33
|
+
status: PageDiffStatus;
|
|
34
|
+
titleBefore?: string;
|
|
35
|
+
titleAfter?: string;
|
|
36
|
+
blockDiffs: BlockDiff[];
|
|
37
|
+
};
|
|
38
|
+
/**
|
|
39
|
+
* Per-key change inside the SiteHeader-shaped portion of siteConfig.
|
|
40
|
+
* `path` uses dot/bracket notation rooted at the config (e.g. `name`,
|
|
41
|
+
* `navLabels["/pricing"]`, `navGroups["Products"]`).
|
|
42
|
+
*/
|
|
43
|
+
export type SiteConfigFieldDiff = {
|
|
44
|
+
path: string;
|
|
45
|
+
before: unknown;
|
|
46
|
+
after: unknown;
|
|
47
|
+
kind: FieldDiffKind;
|
|
48
|
+
};
|
|
49
|
+
export type SiteConfigDiff = {
|
|
50
|
+
status: "added" | "removed" | "modified" | "unchanged";
|
|
51
|
+
fieldDiffs: SiteConfigFieldDiff[];
|
|
52
|
+
};
|
|
53
|
+
export type PublishDiff = {
|
|
54
|
+
summary: {
|
|
55
|
+
pagesAdded: number;
|
|
56
|
+
pagesRemoved: number;
|
|
57
|
+
pagesModified: number;
|
|
58
|
+
pagesUnchanged: number;
|
|
59
|
+
/** Total number of field-level changes across all pages. Useful for CTA labels. */
|
|
60
|
+
totalChangedFields: number;
|
|
61
|
+
/** Total number of changed siteConfig fields (header chrome). */
|
|
62
|
+
siteConfigChangedFields: number;
|
|
63
|
+
};
|
|
64
|
+
pages: PageDiff[];
|
|
65
|
+
/** Diff of the SiteHeader-driving siteConfig (name, logo, navLabels, navGroups). Always present; `unchanged` when no diff. */
|
|
66
|
+
siteConfig: SiteConfigDiff;
|
|
67
|
+
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Types for the "what will change on publish" diff.
|
|
3
|
+
*
|
|
4
|
+
* The orchestrator computes a PublishDiff by comparing session draft pages
|
|
5
|
+
* to the currently-published pages. Shape is intentionally lightweight so
|
|
6
|
+
* the editor (and later the immersive widget) can render it without extra
|
|
7
|
+
* work.
|
|
8
|
+
*/
|
|
9
|
+
export {};
|