@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.
Files changed (76) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +80 -0
  3. package/contract/operation.schema.json +752 -0
  4. package/dist/api-responses.d.ts +62 -0
  5. package/dist/api-responses.js +68 -0
  6. package/dist/block-manifest.d.ts +28 -0
  7. package/dist/block-manifest.js +247 -0
  8. package/dist/block-names.d.ts +19 -0
  9. package/dist/block-names.js +52 -0
  10. package/dist/blocks/_helpers.d.ts +14 -0
  11. package/dist/blocks/_helpers.js +26 -0
  12. package/dist/blocks/_registry.d.ts +116 -0
  13. package/dist/blocks/_registry.js +271 -0
  14. package/dist/blocks/banner.d.ts +1 -0
  15. package/dist/blocks/banner.js +34 -0
  16. package/dist/blocks/card-grid.d.ts +1 -0
  17. package/dist/blocks/card-grid.js +73 -0
  18. package/dist/blocks/card.d.ts +1 -0
  19. package/dist/blocks/card.js +37 -0
  20. package/dist/blocks/carousel.d.ts +1 -0
  21. package/dist/blocks/carousel.js +51 -0
  22. package/dist/blocks/cta.d.ts +1 -0
  23. package/dist/blocks/cta.js +35 -0
  24. package/dist/blocks/embed.d.ts +1 -0
  25. package/dist/blocks/embed.js +30 -0
  26. package/dist/blocks/faq-accordion.d.ts +1 -0
  27. package/dist/blocks/faq-accordion.js +30 -0
  28. package/dist/blocks/feature-grid.d.ts +1 -0
  29. package/dist/blocks/feature-grid.js +46 -0
  30. package/dist/blocks/footer.d.ts +1 -0
  31. package/dist/blocks/footer.js +31 -0
  32. package/dist/blocks/gallery.d.ts +1 -0
  33. package/dist/blocks/gallery.js +47 -0
  34. package/dist/blocks/hero.d.ts +1 -0
  35. package/dist/blocks/hero.js +48 -0
  36. package/dist/blocks/index.d.ts +3 -0
  37. package/dist/blocks/index.js +53 -0
  38. package/dist/blocks/quote.d.ts +1 -0
  39. package/dist/blocks/quote.js +32 -0
  40. package/dist/blocks/rich-text.d.ts +1 -0
  41. package/dist/blocks/rich-text.js +41 -0
  42. package/dist/blocks/site-header.d.ts +1 -0
  43. package/dist/blocks/site-header.js +48 -0
  44. package/dist/blocks/stats.d.ts +1 -0
  45. package/dist/blocks/stats.js +42 -0
  46. package/dist/blocks/table.d.ts +1 -0
  47. package/dist/blocks/table.js +37 -0
  48. package/dist/blocks/tabs.d.ts +1 -0
  49. package/dist/blocks/tabs.js +39 -0
  50. package/dist/blocks/testimonials.d.ts +1 -0
  51. package/dist/blocks/testimonials.js +45 -0
  52. package/dist/blocks/two-column.d.ts +1 -0
  53. package/dist/blocks/two-column.js +61 -0
  54. package/dist/blocks/video.d.ts +1 -0
  55. package/dist/blocks/video.js +33 -0
  56. package/dist/chat-events.d.ts +475 -0
  57. package/dist/chat-events.js +137 -0
  58. package/dist/demo-seed-content.d.ts +3 -0
  59. package/dist/demo-seed-content.js +1128 -0
  60. package/dist/draft-mode.d.ts +10 -0
  61. package/dist/draft-mode.js +29 -0
  62. package/dist/editable-path.d.ts +20 -0
  63. package/dist/editable-path.js +101 -0
  64. package/dist/index.d.ts +13 -0
  65. package/dist/index.js +26 -0
  66. package/dist/ops/builders.d.ts +62 -0
  67. package/dist/ops/builders.js +111 -0
  68. package/dist/ops/theme-tokens.d.ts +50 -0
  69. package/dist/ops/theme-tokens.js +73 -0
  70. package/dist/protocol.d.ts +7 -0
  71. package/dist/protocol.js +7 -0
  72. package/dist/publish-diff.d.ts +67 -0
  73. package/dist/publish-diff.js +9 -0
  74. package/dist/schemas.d.ts +321 -0
  75. package/dist/schemas.js +238 -0
  76. 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
+ }
@@ -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";
@@ -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 {};