@8ux-co/eelzap 0.11.0 → 0.11.2

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.
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/fields.ts","../src/refs.ts","../src/stega.ts"],"sourcesContent":["import { recordRefsOf } from './refs'\nimport { cleanStega, hasStega } from './stega'\n\n/**\n * `@8ux-co/eelzap/fields` (`.docs/proposals/zap-cms-v2.md` §2.4, §2.7): typed\n * pick helpers over an item or a document that tag themselves in preview.\n *\n * ```tsx\n * import { fields } from '@8ux-co/eelzap/fields'\n *\n * const f = fields(home) // a HomeDocument from codegen\n * <h1>{f.text('hero_title')}</h1> // carries its stega marker in preview\n * <a href={f.value('cta_url')} {...f.attrs('cta_url')}>…</a>\n * <img {...f.image('hero_image')} /> // src, alt, width, height (+ data-zap in preview)\n *\n * // Numbered slots: `stat_1` … `stat_4`\n * {f.list('stat', 4).map((s) => <p key={s.key}>{s.text()}</p>)}\n * // Slots of several fields: `nav_1_texto` + `nav_1_url` … `nav_5_…`\n * {f.list('nav', 5).filter((nav) => !nav.empty).map((nav) => (\n * <a key={nav.key} href={nav.value('url') ?? '/'} {...nav.attrs('url')}>{nav.text('texto')}</a>\n * ))}\n * ```\n *\n * Keys are typed from the record's `content`, so with codegen types a typo in\n * a field key fails to compile.\n *\n * Outside preview every helper returns plain values and EMPTY attribute\n * objects, so production HTML is byte-identical to reading values directly.\n * In preview (the record's text carries stega, or `{ preview: true }`), text\n * keeps its marker (the overlay finds it with no tag), and the non-text\n * helpers add `data-zap` (a manual tag). Markers never reach an attribute:\n * `value`, `image` and `attrs` clean them.\n */\n\n/** What `fields` reads: an item (`collection.key` + `slug`) or a document (`key`). */\nexport interface FieldsRecord {\n content: object\n slug?: string | null\n key?: string | null\n collection?: { key?: string | null } | null\n}\n\ntype Content<R extends FieldsRecord> = R['content']\ntype Key<R extends FieldsRecord> = keyof Content<R> & string\n/** Field keys whose value is text (string, possibly null). */\ntype TextKey<R extends FieldsRecord> = {\n [K in Key<R>]: NonNullable<Content<R>[K]> extends string ? K : never\n}[Key<R>]\n/**\n * Every numbered prefix in `K`: `stat` for `stat_1`, `nav` for `nav_1_texto`,\n * `hero_stat` for `hero_stat_1`. Walks the key one `_` segment at a time, so a\n * prefix may itself contain `_`.\n */\ntype Prefixes<K extends string, Head extends string = ''> = K extends `${infer P}_${infer Rest}`\n ?\n | (Rest extends `${number}` | `${number}_${string}` ? `${Head}${P}` : never)\n | Prefixes<Rest, `${Head}${P}_`>\n : never\n/** Numbered prefixes of the record (any string for an untyped record). */\ntype ListPrefix<R extends FieldsRecord> = string extends Key<R> ? string : Prefixes<Key<R>>\n/** The suffixes of `prefix_{i}_{suffix}` slots: `texto` and `url` for `nav`. */\ntype SlotSuffix<R extends FieldsRecord, P extends string> =\n string extends Key<R>\n ? string\n : Key<R> extends infer K\n ? K extends `${P}_${number}_${infer S}`\n ? S\n : never\n : never\n/** A slot field's value type; `unknown` for an untyped record. */\ntype SlotValue<R extends FieldsRecord, K> =\n string extends Key<R> ? unknown : Content<R>[Extract<Key<R>, K>]\n\n/** `{ 'data-zap': 'blog/hola#cover' }` in preview, `{}` otherwise. */\nexport type ZapAttrs = { 'data-zap'?: string }\n\nexport interface FieldImage extends ZapAttrs {\n src: string | undefined\n alt: string | undefined\n width: number | undefined\n height: number | undefined\n}\n\n/**\n * One numbered slot of `f.list(prefix, n)`: the same helpers as `fields`,\n * scoped to the slot. With a suffix they read `prefix_{i}_{suffix}`\n * (`nav.text('texto')` is `nav_1_texto`); without one, `prefix_{i}` itself\n * (`stat.text()` is `stat_1`).\n */\nexport interface FieldSlot<R extends FieldsRecord, P extends string> {\n /** `prefix_{i}`, unique in the list: a React key. */\n readonly key: string\n /** 1 for the first slot. */\n readonly index: number\n /** True when every field of the slot is empty (or the slot has no field). */\n readonly empty: boolean\n /** A text field as delivered, with its stega marker in preview. */\n text(suffix?: SlotSuffix<R, P>): string\n /** The single field `prefix_{i}`, with any marker removed. */\n value(): SlotValue<R, `${P}_${number}`>\n /** The slot's field `prefix_{i}_{suffix}`, with any marker removed. */\n value<S extends SlotSuffix<R, P>>(suffix: S): SlotValue<R, `${P}_${number}_${S}`>\n /** `{ 'data-zap': ref#key }` in preview, `{}` otherwise. */\n attrs(suffix?: SlotSuffix<R, P>): ZapAttrs\n /** An image or media field as `<img>` attributes, tagged in preview. */\n image(suffix?: SlotSuffix<R, P>): FieldImage\n}\n\nexport interface Fields<R extends FieldsRecord> {\n /** True when the helpers tag (the record was read in preview). */\n readonly preview: boolean\n /** A text field as delivered: with its stega marker in preview, so the overlay finds it. */\n text<K extends TextKey<R>>(key: K): string\n /** A field's value with any marker removed: safe in attributes, URLs, comparisons. */\n value<K extends Key<R>>(key: K): Content<R>[K]\n /** `{ 'data-zap': ref#key }` in preview, `{}` otherwise: spread it on the element. */\n attrs(key: Key<R>): ZapAttrs\n /** An image or media field as `<img>` attributes, tagged in preview. */\n image(key: Key<R>): FieldImage\n /**\n * Slots `1` … `count` of a numbered prefix, each a `FieldSlot`: fields\n * `prefix_1` … `prefix_n`, or slots of several fields\n * `prefix_{i}_{suffix}` (`nav_1_texto`, `nav_1_url`), read by suffix.\n */\n list<P extends ListPrefix<R>>(prefix: P, count: number): FieldSlot<R, P>[]\n}\n\nexport interface FieldsOptions {\n /** Force preview on or off; default: on when any text value carries stega. */\n preview?: boolean\n}\n\nexport function fields<R extends FieldsRecord>(record: R, options: FieldsOptions = {}): Fields<R> {\n const content = (record?.content ?? {}) as Record<string, unknown>\n const preview = options.preview ?? Object.values(content).some((value) => hasStega(value))\n const [ref] = recordRefsOf(record)\n const attrs = (key: string): ZapAttrs => (preview && ref ? { 'data-zap': `${ref}#${key}` } : {})\n const value = (key: string) => cleanStega(content[key])\n const text = (key: string) => {\n const raw = content[key]\n return typeof raw === 'string' ? (preview ? raw : cleanStega(raw)) : ''\n }\n const image = (key: string): FieldImage => {\n const media = (value(key) ?? {}) as Record<string, unknown>\n const pick = <T>(name: string, type: string) =>\n typeof media[name] === type ? (media[name] as T) : undefined\n return {\n src: pick<string>('url', 'string'),\n alt: pick<string>('alt', 'string'),\n width: pick<number>('width', 'number'),\n height: pick<number>('height', 'number'),\n ...attrs(key),\n }\n }\n\n return {\n preview,\n text,\n value: value as Fields<R>['value'],\n attrs,\n image,\n list: (prefix, count) =>\n Array.from({ length: Math.max(0, Math.floor(count)) }, (_, i) => {\n const key = `${prefix}_${i + 1}`\n const at = (suffix?: string) => (suffix ? `${key}_${suffix}` : key)\n const own = Object.keys(content).filter((k) => k === key || k.startsWith(`${key}_`))\n return {\n key,\n index: i + 1,\n empty: own.every((k) => {\n const v = value(k)\n return v === null || v === undefined || (typeof v === 'string' && !v.trim())\n }),\n text: (suffix?: string) => text(at(suffix)),\n value: (suffix?: string) => value(at(suffix)),\n attrs: (suffix?: string) => attrs(at(suffix)),\n image: (suffix?: string) => image(at(suffix)),\n } as FieldSlot<R, typeof prefix>\n }),\n }\n}\n","/**\n * The record and field grammar of tags (`.docs/proposals/zap-cms-v2.md` §2.4),\n * in a module of its own so the light entries (`./fields`, `./react`, the\n * boot) can name records without pulling in the preview protocol.\n * No imports, no side effects.\n */\n\n/**\n * A record reference in its canonical string form: `collectionKey/slug` for an\n * entry, `doc:documentKey` for a document (the tag grammar, §2.4).\n */\nexport type RecordRef = string\n\n/** Zap's field key pattern. */\nexport const FIELD_KEY_RE = /^[a-z][a-z0-9_]{0,127}$/\n/** Entry and document references; keys and slugs are `[a-z0-9]+(?:-[a-z0-9]+)*`. */\nconst ENTRY_REF_RE = /^([a-z0-9]+(?:-[a-z0-9]+)*)\\/([a-z0-9]+(?:-[a-z0-9]+)*)$/\nconst DOC_REF_RE = /^doc:([a-z0-9]+(?:-[a-z0-9]+)*)$/\nconst MAX_REF_LENGTH = 300\n\nexport type ParsedRecordRef =\n | { kind: 'entry'; collection: string; slug: string }\n | { kind: 'document'; key: string }\n\nexport function parseRecordRef(value: unknown): ParsedRecordRef | null {\n if (typeof value !== 'string' || value.length > MAX_REF_LENGTH) return null\n const doc = DOC_REF_RE.exec(value)\n if (doc) return { kind: 'document', key: doc[1]! }\n const entry = ENTRY_REF_RE.exec(value)\n if (entry) return { kind: 'entry', collection: entry[1]!, slug: entry[2]! }\n return null\n}\n\nexport function isRecordRef(value: unknown): value is RecordRef {\n return parseRecordRef(value) !== null\n}\n\n/** `zpt_` + 32 bytes in base64url, as Zap mints it (zap-cms-v2 §3.5). */\nexport const PREVIEW_TOKEN_RE = /^zpt_[A-Za-z0-9_-]{43}$/\n\n/** A preview token in the shape Zap mints; says nothing about whether it is live. */\nexport function isPreviewTokenShape(value: unknown): value is string {\n return typeof value === 'string' && PREVIEW_TOKEN_RE.test(value)\n}\n\nexport function isFieldKey(value: unknown): value is string {\n return typeof value === 'string' && FIELD_KEY_RE.test(value)\n}\n\n/**\n * What `zapAttrs` and `onValues` accept: a record reference string\n * (`blog/hola`, `doc:home`) or a delivery API entry (an item carries\n * `collection.key` and `slug`, a document `key`).\n */\nexport type EntryLike =\n | RecordRef\n | {\n slug?: string | null\n key?: string | null\n collection?: { key?: string | null } | null\n localizedSlugs?: Record<string, string | null | undefined> | null\n }\n\n/** Every reference an entry answers to (default slug first, then localized ones). */\nexport function recordRefsOf(entry: EntryLike | null | undefined): RecordRef[] {\n if (!entry) return []\n if (typeof entry === 'string') return isRecordRef(entry) ? [entry] : []\n const refs: RecordRef[] = []\n const add = (ref: string) => {\n if (isRecordRef(ref) && !refs.includes(ref)) refs.push(ref)\n }\n const collection = entry.collection?.key\n if (collection) {\n if (entry.slug) add(`${collection}/${entry.slug}`)\n for (const slug of Object.values(entry.localizedSlugs ?? {})) {\n if (slug) add(`${collection}/${slug}`)\n }\n } else if (entry.key) {\n add(`doc:${entry.key}`)\n }\n return refs\n}\n\n/**\n * The tag for one field, as attributes to spread on the element that renders\n * it: `{ 'data-zap': 'blog/hola#title' }`. Empty when the entry or the field\n * key is not one Zap can name, so a bad call never breaks the page.\n */\nexport function zapAttrs(entry: EntryLike, fieldKey: string): { 'data-zap'?: string } {\n const [ref] = recordRefsOf(entry)\n if (!ref || !isFieldKey(fieldKey)) return {}\n return { 'data-zap': `${ref}#${fieldKey}` }\n}\n","/**\n * Stega: invisible field markers on prose values read in preview\n * (`.docs/proposals/zap-cms-v2.md` §2.4, §7.5).\n *\n * Zap appends a marker to TEXT and LONG_TEXT values, and to the last text\n * node of each RICH_TEXT block, ONLY in `preview=1` delivery responses. The\n * preview overlay finds the marker in the page's text nodes and so knows\n * which element renders which field, with no manual tag. Published responses\n * never carry one.\n *\n * ## Format (eelzap stega v1)\n *\n * A marker is the tag the overlay would read from a `data-zap` attribute,\n * `recordRef#fieldKey`, plus `@locale` when the value belongs to a locale:\n *\n * blog-posts/mi-primer-post#title@es\n * doc:home#hero_title@es\n *\n * written as zero-width characters:\n *\n * - four digits, base 4: U+200B (0), U+200C (1), U+200D (2), U+2060 (3);\n * - a prefix of 8 digits, the bytes 0xE5 0xA7 (`3211 2213`);\n * - then every byte of the ASCII payload as 4 digits, most significant first.\n *\n * The payload is printable ASCII (the tag grammar allows nothing else), so\n * every payload byte starts with digit 0 or 1 and the prefix, which starts\n * with 3, can never be read inside a payload. A decoder reads groups of four\n * after the prefix until a group is not a payload byte, then validates the\n * result against the tag grammar; anything that does not parse is ignored. A\n * value never carries two markers (`stegaEncode` refuses to add a second).\n *\n * Why not `@vercel/stega`'s format: it encodes a JSON object (a URL and\n * origin in Sanity's and Vercel's use) and uses U+FEFF, which some tools strip\n * as a byte order mark. Here the decoded marker IS the tag grammar\n * (`data-zap`), so one parser serves both, and U+FEFF is avoided. No\n * dependency either way.\n *\n * Zero dependencies, no side effects: `.` re-exports `cleanStega` and\n * `hasStega`; the encoder is internal (Zap's delivery imports it); the\n * decoder serves `./preview`.\n */\n\nconst DIGITS = ['​', '‌', '‍', '⁠'] as const\nconst PREFIX = '⁠‍‌‌‍‍‌⁠'\n/** Longest payload a marker may carry, in bytes (ref 300 + field 128 + locale). */\nconst MAX_PAYLOAD = 512\n\n/** `recordRef#fieldKey` with an optional `@locale`: the tag grammar (`refs.ts`). */\nconst PAYLOAD_RE =\n /^(doc:[a-z0-9]+(?:-[a-z0-9]+)*|[a-z0-9]+(?:-[a-z0-9]+)*\\/[a-z0-9]+(?:-[a-z0-9]+)*)#([a-z][a-z0-9_]{0,127})(?:@([A-Za-z]{2,3}(?:-[A-Za-z0-9]{2,8}){0,3}))?$/\n/** The prefix, then whole payload bytes (first digit 0 or 1). */\nconst MARKER_RE = /⁠‍‌‌‍‍‌⁠(?:[​‌][​‌‍⁠]{3})*/g\n\n/** What one marker says: the record, the field, and the locale when known. */\nexport interface StegaInfo {\n /** `collectionKey/slug` or `doc:documentKey`. */\n recordRef: string\n fieldKey: string\n locale?: string | null\n}\n\nexport interface DecodedStega {\n recordRef: string\n fieldKey: string\n locale: string | null\n /** `recordRef#fieldKey`, the `data-zap` tag. */\n tag: string\n}\n\n/** The marker for one field, or `''` when the info does not fit the tag grammar. */\nexport function stegaMarker(info: StegaInfo): string {\n const payload = `${info.recordRef}#${info.fieldKey}${info.locale ? `@${info.locale}` : ''}`\n if (payload.length > MAX_PAYLOAD || !PAYLOAD_RE.test(payload)) return ''\n let out = PREFIX\n for (let i = 0; i < payload.length; i++) {\n const byte = payload.charCodeAt(i)\n out +=\n DIGITS[(byte >> 6) & 3]! +\n DIGITS[(byte >> 4) & 3]! +\n DIGITS[(byte >> 2) & 3]! +\n DIGITS[byte & 3]!\n }\n return out\n}\n\n/**\n * `text` with the field's marker appended. A string that already carries a\n * marker, or info outside the grammar, comes back unchanged.\n */\nexport function stegaEncode(text: string, info: StegaInfo): string {\n if (hasStega(text)) return text\n return text + stegaMarker(info)\n}\n\n/** True when `value` is a string carrying at least one marker. */\nexport function hasStega(value: unknown): boolean {\n return typeof value === 'string' && value.includes(PREFIX)\n}\n\n/** Every valid marker in `text`, in order. Invalid ones are skipped. */\nexport function stegaDecode(text: string): DecodedStega[] {\n if (!text.includes(PREFIX)) return []\n const found: DecodedStega[] = []\n for (const match of text.match(MARKER_RE) ?? []) {\n let payload = ''\n for (let i = PREFIX.length; i + 4 <= match.length; i += 4) {\n let byte = 0\n for (let j = 0; j < 4; j++) byte = byte * 4 + DIGITS.indexOf(match[i + j] as never)\n payload += String.fromCharCode(byte)\n }\n const parsed = PAYLOAD_RE.exec(payload)\n if (!parsed) continue\n found.push({\n recordRef: parsed[1]!,\n fieldKey: parsed[2]!,\n locale: parsed[3] ?? null,\n tag: `${parsed[1]}#${parsed[2]}`,\n })\n }\n return found\n}\n\n/** `text` split into its visible part and the markers it carries, in order. */\nexport function splitStega(text: string): { text: string; markers: string } {\n if (!text.includes(PREFIX)) return { text, markers: '' }\n return { text: text.replace(MARKER_RE, ''), markers: (text.match(MARKER_RE) ?? []).join('') }\n}\n\n/** `text` without any marker. */\nfunction cleanString(text: string): string {\n return text.includes(PREFIX) ? text.replace(MARKER_RE, '') : text\n}\n\n/**\n * `value` with every stega marker removed: a string is cleaned, an array or a\n * plain object is cleaned deeply (into new objects), anything else is\n * returned as is. Use it where a preview value is compared, parsed or put in\n * an attribute, a URL or `<head>`; outside preview it changes nothing.\n */\nexport function cleanStega<T>(value: T): T {\n if (typeof value === 'string') return cleanString(value) as T\n if (Array.isArray(value)) return value.map((item) => cleanStega(item)) as T\n if (value && typeof value === 'object' && Object.getPrototypeOf(value) === Object.prototype) {\n const out: Record<string, unknown> = {}\n for (const [key, item] of Object.entries(value)) out[key] = cleanStega(item)\n return out as T\n }\n return value\n}\n"],"mappings":"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","names":["value"]}
1
+ {"version":3,"sources":["../src/fields.ts","../src/refs.ts","../src/stega.ts"],"sourcesContent":["import { recordRefsOf } from './refs'\nimport { cleanStega, hasStega } from './stega'\n\n/**\n * `@8ux-co/eelzap/fields` (`.docs/proposals/zap-cms-v2.md` §2.4, §2.7): typed\n * pick helpers over an item or a document that tag themselves in preview.\n *\n * ```tsx\n * import { fields } from '@8ux-co/eelzap/fields'\n *\n * const f = fields(home) // a HomeDocument from codegen\n * <h1>{f.text('hero_title')}</h1> // carries its stega marker in preview\n * <a href={f.value('cta_url')} {...f.attrs('cta_url')}>…</a>\n * <img {...f.image('hero_image')} /> // src, alt, width, height (+ data-zap in preview)\n *\n * // A value the site renders itself (split, uppercased, formatted):\n * <h1 {...f.derived('title')}>{splitTitle(f.value('title'))}</h1>\n *\n * // Numbered slots: `stat_1` … `stat_4`\n * {f.list('stat', 4).map((s) => <p key={s.key}>{s.text()}</p>)}\n * // Slots of several fields: `nav_1_texto` + `nav_1_url` … `nav_5_…`\n * {f.list('nav', 5).filter((nav) => !nav.empty).map((nav) => (\n * <a key={nav.key} href={nav.value('url') ?? '/'} {...nav.attrs('url')}>{nav.text('texto')}</a>\n * ))}\n * ```\n *\n * Keys are typed from the record's `content`, so with codegen types a typo in\n * a field key fails to compile.\n *\n * Outside preview every helper returns plain values and EMPTY attribute\n * objects, so production HTML is byte-identical to reading values directly.\n * In preview (the record's text carries stega, or `{ preview: true }`), text\n * keeps its marker (the overlay finds it with no tag), and the non-text\n * helpers add `data-zap` (a manual tag). Markers never reach an attribute:\n * `value`, `image` and `attrs` clean them.\n *\n * `derived` is for an element that shows what the site computes from a field\n * (an italic-split title, a long text split into paragraphs, an uppercased\n * label): in preview it adds `data-zap-site-renders`, so the overlay outlines\n * the element and opens the field on a click but never writes into it. The\n * site keeps it current with `useZapLiveUpdates` or `onValues`.\n */\n\n/** What `fields` reads: an item (`collection.key` + `slug`) or a document (`key`). */\nexport interface FieldsRecord {\n content: object\n slug?: string | null\n key?: string | null\n collection?: { key?: string | null } | null\n}\n\ntype Content<R extends FieldsRecord> = R['content']\ntype Key<R extends FieldsRecord> = keyof Content<R> & string\n/** Field keys whose value is text (string, possibly null). */\ntype TextKey<R extends FieldsRecord> = {\n [K in Key<R>]: NonNullable<Content<R>[K]> extends string ? K : never\n}[Key<R>]\n/**\n * Every numbered prefix in `K`: `stat` for `stat_1`, `nav` for `nav_1_texto`,\n * `hero_stat` for `hero_stat_1`. Walks the key one `_` segment at a time, so a\n * prefix may itself contain `_`.\n */\ntype Prefixes<K extends string, Head extends string = ''> = K extends `${infer P}_${infer Rest}`\n ?\n | (Rest extends `${number}` | `${number}_${string}` ? `${Head}${P}` : never)\n | Prefixes<Rest, `${Head}${P}_`>\n : never\n/** Numbered prefixes of the record (any string for an untyped record). */\ntype ListPrefix<R extends FieldsRecord> = string extends Key<R> ? string : Prefixes<Key<R>>\n/** The suffixes of `prefix_{i}_{suffix}` slots: `texto` and `url` for `nav`. */\ntype SlotSuffix<R extends FieldsRecord, P extends string> =\n string extends Key<R>\n ? string\n : Key<R> extends infer K\n ? K extends `${P}_${number}_${infer S}`\n ? S\n : never\n : never\n/** A slot field's value type; `unknown` for an untyped record. */\ntype SlotValue<R extends FieldsRecord, K> =\n string extends Key<R> ? unknown : Content<R>[Extract<Key<R>, K>]\n\n/** `{ 'data-zap': 'blog/hola#cover' }` in preview, `{}` otherwise. */\nexport type ZapAttrs = { 'data-zap'?: string }\n\n/** `{ 'data-zap-site-renders': 'blog/hola#title' }` in preview, `{}` otherwise. */\nexport type ZapSiteRendersAttrs = { 'data-zap-site-renders'?: string }\n\nexport interface FieldImage extends ZapAttrs {\n src: string | undefined\n alt: string | undefined\n width: number | undefined\n height: number | undefined\n}\n\n/**\n * One numbered slot of `f.list(prefix, n)`: the same helpers as `fields`,\n * scoped to the slot. With a suffix they read `prefix_{i}_{suffix}`\n * (`nav.text('texto')` is `nav_1_texto`); without one, `prefix_{i}` itself\n * (`stat.text()` is `stat_1`).\n */\nexport interface FieldSlot<R extends FieldsRecord, P extends string> {\n /** `prefix_{i}`, unique in the list: a React key. */\n readonly key: string\n /** 1 for the first slot. */\n readonly index: number\n /** True when every field of the slot is empty (or the slot has no field). */\n readonly empty: boolean\n /** A text field as delivered, with its stega marker in preview. */\n text(suffix?: SlotSuffix<R, P>): string\n /** The single field `prefix_{i}`, with any marker removed. */\n value(): SlotValue<R, `${P}_${number}`>\n /** The slot's field `prefix_{i}_{suffix}`, with any marker removed. */\n value<S extends SlotSuffix<R, P>>(suffix: S): SlotValue<R, `${P}_${number}_${S}`>\n /** `{ 'data-zap': ref#key }` in preview, `{}` otherwise. */\n attrs(suffix?: SlotSuffix<R, P>): ZapAttrs\n /** `{ 'data-zap-site-renders': ref#key }` in preview: the site renders it, never written into. */\n derived(suffix?: SlotSuffix<R, P>): ZapSiteRendersAttrs\n /** An image or media field as `<img>` attributes, tagged in preview. */\n image(suffix?: SlotSuffix<R, P>): FieldImage\n}\n\nexport interface Fields<R extends FieldsRecord> {\n /** True when the helpers tag (the record was read in preview). */\n readonly preview: boolean\n /** A text field as delivered: with its stega marker in preview, so the overlay finds it. */\n text<K extends TextKey<R>>(key: K): string\n /** A field's value with any marker removed: safe in attributes, URLs, comparisons. */\n value<K extends Key<R>>(key: K): Content<R>[K]\n /** `{ 'data-zap': ref#key }` in preview, `{}` otherwise: spread it on the element. */\n attrs(key: Key<R>): ZapAttrs\n /**\n * `{ 'data-zap-site-renders': ref#key }` in preview, `{}` otherwise: for an\n * element that shows what the site computes from the field. The overlay\n * outlines it and opens the field on a click, and never writes into it.\n */\n derived(key: Key<R>): ZapSiteRendersAttrs\n /** An image or media field as `<img>` attributes, tagged in preview. */\n image(key: Key<R>): FieldImage\n /**\n * Slots `1` … `count` of a numbered prefix, each a `FieldSlot`: fields\n * `prefix_1` … `prefix_n`, or slots of several fields\n * `prefix_{i}_{suffix}` (`nav_1_texto`, `nav_1_url`), read by suffix.\n */\n list<P extends ListPrefix<R>>(prefix: P, count: number): FieldSlot<R, P>[]\n}\n\nexport interface FieldsOptions {\n /** Force preview on or off; default: on when any text value carries stega. */\n preview?: boolean\n}\n\nexport function fields<R extends FieldsRecord>(record: R, options: FieldsOptions = {}): Fields<R> {\n const content = (record?.content ?? {}) as Record<string, unknown>\n const preview = options.preview ?? Object.values(content).some((value) => hasStega(value))\n const [ref] = recordRefsOf(record)\n const attrs = (key: string): ZapAttrs => (preview && ref ? { 'data-zap': `${ref}#${key}` } : {})\n const derived = (key: string): ZapSiteRendersAttrs =>\n preview && ref ? { 'data-zap-site-renders': `${ref}#${key}` } : {}\n const value = (key: string) => cleanStega(content[key])\n const text = (key: string) => {\n const raw = content[key]\n return typeof raw === 'string' ? (preview ? raw : cleanStega(raw)) : ''\n }\n const image = (key: string): FieldImage => {\n const media = (value(key) ?? {}) as Record<string, unknown>\n const pick = <T>(name: string, type: string) =>\n typeof media[name] === type ? (media[name] as T) : undefined\n return {\n src: pick<string>('url', 'string'),\n alt: pick<string>('alt', 'string'),\n width: pick<number>('width', 'number'),\n height: pick<number>('height', 'number'),\n ...attrs(key),\n }\n }\n\n return {\n preview,\n text,\n value: value as Fields<R>['value'],\n attrs,\n derived,\n image,\n list: (prefix, count) =>\n Array.from({ length: Math.max(0, Math.floor(count)) }, (_, i) => {\n const key = `${prefix}_${i + 1}`\n const at = (suffix?: string) => (suffix ? `${key}_${suffix}` : key)\n const own = Object.keys(content).filter((k) => k === key || k.startsWith(`${key}_`))\n return {\n key,\n index: i + 1,\n empty: own.every((k) => {\n const v = value(k)\n return v === null || v === undefined || (typeof v === 'string' && !v.trim())\n }),\n text: (suffix?: string) => text(at(suffix)),\n value: (suffix?: string) => value(at(suffix)),\n attrs: (suffix?: string) => attrs(at(suffix)),\n derived: (suffix?: string) => derived(at(suffix)),\n image: (suffix?: string) => image(at(suffix)),\n } as FieldSlot<R, typeof prefix>\n }),\n }\n}\n","/**\n * The record and field grammar of tags (`.docs/proposals/zap-cms-v2.md` §2.4),\n * in a module of its own so the light entries (`./fields`, `./react`, the\n * boot) can name records without pulling in the preview protocol.\n * No imports, no side effects.\n */\n\n/**\n * A record reference in its canonical string form: `collectionKey/slug` for an\n * entry, `doc:documentKey` for a document (the tag grammar, §2.4).\n */\nexport type RecordRef = string\n\n/** Zap's field key pattern. */\nexport const FIELD_KEY_RE = /^[a-z][a-z0-9_]{0,127}$/\n/** Entry and document references; keys and slugs are `[a-z0-9]+(?:-[a-z0-9]+)*`. */\nconst ENTRY_REF_RE = /^([a-z0-9]+(?:-[a-z0-9]+)*)\\/([a-z0-9]+(?:-[a-z0-9]+)*)$/\nconst DOC_REF_RE = /^doc:([a-z0-9]+(?:-[a-z0-9]+)*)$/\nconst MAX_REF_LENGTH = 300\n\nexport type ParsedRecordRef =\n | { kind: 'entry'; collection: string; slug: string }\n | { kind: 'document'; key: string }\n\nexport function parseRecordRef(value: unknown): ParsedRecordRef | null {\n if (typeof value !== 'string' || value.length > MAX_REF_LENGTH) return null\n const doc = DOC_REF_RE.exec(value)\n if (doc) return { kind: 'document', key: doc[1]! }\n const entry = ENTRY_REF_RE.exec(value)\n if (entry) return { kind: 'entry', collection: entry[1]!, slug: entry[2]! }\n return null\n}\n\nexport function isRecordRef(value: unknown): value is RecordRef {\n return parseRecordRef(value) !== null\n}\n\n/** `zpt_` + 32 bytes in base64url, as Zap mints it (zap-cms-v2 §3.5). */\nexport const PREVIEW_TOKEN_RE = /^zpt_[A-Za-z0-9_-]{43}$/\n\n/** A preview token in the shape Zap mints; says nothing about whether it is live. */\nexport function isPreviewTokenShape(value: unknown): value is string {\n return typeof value === 'string' && PREVIEW_TOKEN_RE.test(value)\n}\n\nexport function isFieldKey(value: unknown): value is string {\n return typeof value === 'string' && FIELD_KEY_RE.test(value)\n}\n\n/**\n * What `zapAttrs` and `onValues` accept: a record reference string\n * (`blog/hola`, `doc:home`) or a delivery API entry (an item carries\n * `collection.key` and `slug`, a document `key`).\n */\nexport type EntryLike =\n | RecordRef\n | {\n slug?: string | null\n key?: string | null\n collection?: { key?: string | null } | null\n localizedSlugs?: Record<string, string | null | undefined> | null\n }\n\n/** Every reference an entry answers to (default slug first, then localized ones). */\nexport function recordRefsOf(entry: EntryLike | null | undefined): RecordRef[] {\n if (!entry) return []\n if (typeof entry === 'string') return isRecordRef(entry) ? [entry] : []\n const refs: RecordRef[] = []\n const add = (ref: string) => {\n if (isRecordRef(ref) && !refs.includes(ref)) refs.push(ref)\n }\n const collection = entry.collection?.key\n if (collection) {\n if (entry.slug) add(`${collection}/${entry.slug}`)\n for (const slug of Object.values(entry.localizedSlugs ?? {})) {\n if (slug) add(`${collection}/${slug}`)\n }\n } else if (entry.key) {\n add(`doc:${entry.key}`)\n }\n return refs\n}\n\n/**\n * The tag for one field, as attributes to spread on the element that renders\n * it: `{ 'data-zap': 'blog/hola#title' }`. Empty when the entry or the field\n * key is not one Zap can name, so a bad call never breaks the page.\n */\nexport function zapAttrs(entry: EntryLike, fieldKey: string): { 'data-zap'?: string } {\n const [ref] = recordRefsOf(entry)\n if (!ref || !isFieldKey(fieldKey)) return {}\n return { 'data-zap': `${ref}#${fieldKey}` }\n}\n","/**\n * Stega: invisible field markers on prose values read in preview\n * (`.docs/proposals/zap-cms-v2.md` §2.4, §7.5).\n *\n * Zap appends a marker to TEXT and LONG_TEXT values, and to the last text\n * node of each RICH_TEXT block, ONLY in `preview=1` delivery responses. The\n * preview overlay finds the marker in the page's text nodes and so knows\n * which element renders which field, with no manual tag. Published responses\n * never carry one.\n *\n * ## Format (eelzap stega v1)\n *\n * A marker is the tag the overlay would read from a `data-zap` attribute,\n * `recordRef#fieldKey`, plus `@locale` when the value belongs to a locale:\n *\n * blog-posts/mi-primer-post#title@es\n * doc:home#hero_title@es\n *\n * written as zero-width characters:\n *\n * - four digits, base 4: U+200B (0), U+200C (1), U+200D (2), U+2060 (3);\n * - a prefix of 8 digits, the bytes 0xE5 0xA7 (`3211 2213`);\n * - then every byte of the ASCII payload as 4 digits, most significant first.\n *\n * The payload is printable ASCII (the tag grammar allows nothing else), so\n * every payload byte starts with digit 0 or 1 and the prefix, which starts\n * with 3, can never be read inside a payload. A decoder reads groups of four\n * after the prefix until a group is not a payload byte, then validates the\n * result against the tag grammar; anything that does not parse is ignored. A\n * value never carries two markers (`stegaEncode` refuses to add a second).\n *\n * Why not `@vercel/stega`'s format: it encodes a JSON object (a URL and\n * origin in Sanity's and Vercel's use) and uses U+FEFF, which some tools strip\n * as a byte order mark. Here the decoded marker IS the tag grammar\n * (`data-zap`), so one parser serves both, and U+FEFF is avoided. No\n * dependency either way.\n *\n * Zero dependencies, no side effects: `.` re-exports `cleanStega` and\n * `hasStega`; the encoder is internal (Zap's delivery imports it); the\n * decoder serves `./preview`.\n */\n\nconst DIGITS = ['​', '‌', '‍', '⁠'] as const\nconst PREFIX = '⁠‍‌‌‍‍‌⁠'\n/** Longest payload a marker may carry, in bytes (ref 300 + field 128 + locale). */\nconst MAX_PAYLOAD = 512\n\n/** `recordRef#fieldKey` with an optional `@locale`: the tag grammar (`refs.ts`). */\nconst PAYLOAD_RE =\n /^(doc:[a-z0-9]+(?:-[a-z0-9]+)*|[a-z0-9]+(?:-[a-z0-9]+)*\\/[a-z0-9]+(?:-[a-z0-9]+)*)#([a-z][a-z0-9_]{0,127})(?:@([A-Za-z]{2,3}(?:-[A-Za-z0-9]{2,8}){0,3}))?$/\n/** The prefix, then whole payload bytes (first digit 0 or 1). */\nconst MARKER_RE = /⁠‍‌‌‍‍‌⁠(?:[​‌][​‌‍⁠]{3})*/g\n\n/** What one marker says: the record, the field, and the locale when known. */\nexport interface StegaInfo {\n /** `collectionKey/slug` or `doc:documentKey`. */\n recordRef: string\n fieldKey: string\n locale?: string | null\n}\n\nexport interface DecodedStega {\n recordRef: string\n fieldKey: string\n locale: string | null\n /** `recordRef#fieldKey`, the `data-zap` tag. */\n tag: string\n}\n\n/** The marker for one field, or `''` when the info does not fit the tag grammar. */\nexport function stegaMarker(info: StegaInfo): string {\n const payload = `${info.recordRef}#${info.fieldKey}${info.locale ? `@${info.locale}` : ''}`\n if (payload.length > MAX_PAYLOAD || !PAYLOAD_RE.test(payload)) return ''\n let out = PREFIX\n for (let i = 0; i < payload.length; i++) {\n const byte = payload.charCodeAt(i)\n out +=\n DIGITS[(byte >> 6) & 3]! +\n DIGITS[(byte >> 4) & 3]! +\n DIGITS[(byte >> 2) & 3]! +\n DIGITS[byte & 3]!\n }\n return out\n}\n\n/**\n * `text` with the field's marker appended. A string that already carries a\n * marker, or info outside the grammar, comes back unchanged.\n */\nexport function stegaEncode(text: string, info: StegaInfo): string {\n if (hasStega(text)) return text\n return text + stegaMarker(info)\n}\n\n/** True when `value` is a string carrying at least one marker. */\nexport function hasStega(value: unknown): boolean {\n return typeof value === 'string' && value.includes(PREFIX)\n}\n\n/** Every valid marker in `text`, in order. Invalid ones are skipped. */\nexport function stegaDecode(text: string): DecodedStega[] {\n if (!text.includes(PREFIX)) return []\n const found: DecodedStega[] = []\n for (const match of text.match(MARKER_RE) ?? []) {\n let payload = ''\n for (let i = PREFIX.length; i + 4 <= match.length; i += 4) {\n let byte = 0\n for (let j = 0; j < 4; j++) byte = byte * 4 + DIGITS.indexOf(match[i + j] as never)\n payload += String.fromCharCode(byte)\n }\n const parsed = PAYLOAD_RE.exec(payload)\n if (!parsed) continue\n found.push({\n recordRef: parsed[1]!,\n fieldKey: parsed[2]!,\n locale: parsed[3] ?? null,\n tag: `${parsed[1]}#${parsed[2]}`,\n })\n }\n return found\n}\n\n/** `text` split into its visible part and the markers it carries, in order. */\nexport function splitStega(text: string): { text: string; markers: string } {\n if (!text.includes(PREFIX)) return { text, markers: '' }\n return { text: text.replace(MARKER_RE, ''), markers: (text.match(MARKER_RE) ?? []).join('') }\n}\n\n/** `text` without any marker. */\nfunction cleanString(text: string): string {\n return text.includes(PREFIX) ? text.replace(MARKER_RE, '') : text\n}\n\n/**\n * `value` with every stega marker removed: a string is cleaned, an array or a\n * plain object is cleaned deeply (into new objects), anything else is\n * returned as is. Use it where a preview value is compared, parsed or put in\n * an attribute, a URL or `<head>`; outside preview it changes nothing.\n */\nexport function cleanStega<T>(value: T): T {\n if (typeof value === 'string') return cleanString(value) as T\n if (Array.isArray(value)) return value.map((item) => cleanStega(item)) as T\n if (value && typeof value === 'object' && Object.getPrototypeOf(value) === Object.prototype) {\n const out: Record<string, unknown> = {}\n for (const [key, item] of Object.entries(value)) out[key] = cleanStega(item)\n return out as T\n }\n return value\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACgBA,IAAM,eAAe;AACrB,IAAM,aAAa;AACnB,IAAM,iBAAiB;AAMhB,SAAS,eAAe,OAAwC;AACrE,MAAI,OAAO,UAAU,YAAY,MAAM,SAAS,eAAgB,QAAO;AACvE,QAAM,MAAM,WAAW,KAAK,KAAK;AACjC,MAAI,IAAK,QAAO,EAAE,MAAM,YAAY,KAAK,IAAI,CAAC,EAAG;AACjD,QAAM,QAAQ,aAAa,KAAK,KAAK;AACrC,MAAI,MAAO,QAAO,EAAE,MAAM,SAAS,YAAY,MAAM,CAAC,GAAI,MAAM,MAAM,CAAC,EAAG;AAC1E,SAAO;AACT;AAEO,SAAS,YAAY,OAAoC;AAC9D,SAAO,eAAe,KAAK,MAAM;AACnC;AA6BO,SAAS,aAAa,OAAkD;AAC7E,MAAI,CAAC,MAAO,QAAO,CAAC;AACpB,MAAI,OAAO,UAAU,SAAU,QAAO,YAAY,KAAK,IAAI,CAAC,KAAK,IAAI,CAAC;AACtE,QAAM,OAAoB,CAAC;AAC3B,QAAM,MAAM,CAAC,QAAgB;AAC3B,QAAI,YAAY,GAAG,KAAK,CAAC,KAAK,SAAS,GAAG,EAAG,MAAK,KAAK,GAAG;AAAA,EAC5D;AACA,QAAM,aAAa,MAAM,YAAY;AACrC,MAAI,YAAY;AACd,QAAI,MAAM,KAAM,KAAI,GAAG,UAAU,IAAI,MAAM,IAAI,EAAE;AACjD,eAAW,QAAQ,OAAO,OAAO,MAAM,kBAAkB,CAAC,CAAC,GAAG;AAC5D,UAAI,KAAM,KAAI,GAAG,UAAU,IAAI,IAAI,EAAE;AAAA,IACvC;AAAA,EACF,WAAW,MAAM,KAAK;AACpB,QAAI,OAAO,MAAM,GAAG,EAAE;AAAA,EACxB;AACA,SAAO;AACT;;;ACtCA,IAAM,SAAS;AAQf,IAAM,YAAY;AA4CX,SAAS,SAAS,OAAyB;AAChD,SAAO,OAAO,UAAU,YAAY,MAAM,SAAS,MAAM;AAC3D;AAgCA,SAAS,YAAY,MAAsB;AACzC,SAAO,KAAK,SAAS,MAAM,IAAI,KAAK,QAAQ,WAAW,EAAE,IAAI;AAC/D;AAQO,SAAS,WAAc,OAAa;AACzC,MAAI,OAAO,UAAU,SAAU,QAAO,YAAY,KAAK;AACvD,MAAI,MAAM,QAAQ,KAAK,EAAG,QAAO,MAAM,IAAI,CAAC,SAAS,WAAW,IAAI,CAAC;AACrE,MAAI,SAAS,OAAO,UAAU,YAAY,OAAO,eAAe,KAAK,MAAM,OAAO,WAAW;AAC3F,UAAM,MAA+B,CAAC;AACtC,eAAW,CAAC,KAAK,IAAI,KAAK,OAAO,QAAQ,KAAK,EAAG,KAAI,GAAG,IAAI,WAAW,IAAI;AAC3E,WAAO;AAAA,EACT;AACA,SAAO;AACT;;;AFIO,SAAS,OAA+B,QAAW,UAAyB,CAAC,GAAc;AAChG,QAAM,UAAW,QAAQ,WAAW,CAAC;AACrC,QAAM,UAAU,QAAQ,WAAW,OAAO,OAAO,OAAO,EAAE,KAAK,CAACA,WAAU,SAASA,MAAK,CAAC;AACzF,QAAM,CAAC,GAAG,IAAI,aAAa,MAAM;AACjC,QAAM,QAAQ,CAAC,QAA2B,WAAW,MAAM,EAAE,YAAY,GAAG,GAAG,IAAI,GAAG,GAAG,IAAI,CAAC;AAC9F,QAAM,UAAU,CAAC,QACf,WAAW,MAAM,EAAE,yBAAyB,GAAG,GAAG,IAAI,GAAG,GAAG,IAAI,CAAC;AACnE,QAAM,QAAQ,CAAC,QAAgB,WAAW,QAAQ,GAAG,CAAC;AACtD,QAAM,OAAO,CAAC,QAAgB;AAC5B,UAAM,MAAM,QAAQ,GAAG;AACvB,WAAO,OAAO,QAAQ,WAAY,UAAU,MAAM,WAAW,GAAG,IAAK;AAAA,EACvE;AACA,QAAM,QAAQ,CAAC,QAA4B;AACzC,UAAM,QAAS,MAAM,GAAG,KAAK,CAAC;AAC9B,UAAM,OAAO,CAAI,MAAc,SAC7B,OAAO,MAAM,IAAI,MAAM,OAAQ,MAAM,IAAI,IAAU;AACrD,WAAO;AAAA,MACL,KAAK,KAAa,OAAO,QAAQ;AAAA,MACjC,KAAK,KAAa,OAAO,QAAQ;AAAA,MACjC,OAAO,KAAa,SAAS,QAAQ;AAAA,MACrC,QAAQ,KAAa,UAAU,QAAQ;AAAA,MACvC,GAAG,MAAM,GAAG;AAAA,IACd;AAAA,EACF;AAEA,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,MAAM,CAAC,QAAQ,UACb,MAAM,KAAK,EAAE,QAAQ,KAAK,IAAI,GAAG,KAAK,MAAM,KAAK,CAAC,EAAE,GAAG,CAAC,GAAG,MAAM;AAC/D,YAAM,MAAM,GAAG,MAAM,IAAI,IAAI,CAAC;AAC9B,YAAM,KAAK,CAAC,WAAqB,SAAS,GAAG,GAAG,IAAI,MAAM,KAAK;AAC/D,YAAM,MAAM,OAAO,KAAK,OAAO,EAAE,OAAO,CAAC,MAAM,MAAM,OAAO,EAAE,WAAW,GAAG,GAAG,GAAG,CAAC;AACnF,aAAO;AAAA,QACL;AAAA,QACA,OAAO,IAAI;AAAA,QACX,OAAO,IAAI,MAAM,CAAC,MAAM;AACtB,gBAAM,IAAI,MAAM,CAAC;AACjB,iBAAO,MAAM,QAAQ,MAAM,UAAc,OAAO,MAAM,YAAY,CAAC,EAAE,KAAK;AAAA,QAC5E,CAAC;AAAA,QACD,MAAM,CAAC,WAAoB,KAAK,GAAG,MAAM,CAAC;AAAA,QAC1C,OAAO,CAAC,WAAoB,MAAM,GAAG,MAAM,CAAC;AAAA,QAC5C,OAAO,CAAC,WAAoB,MAAM,GAAG,MAAM,CAAC;AAAA,QAC5C,SAAS,CAAC,WAAoB,QAAQ,GAAG,MAAM,CAAC;AAAA,QAChD,OAAO,CAAC,WAAoB,MAAM,GAAG,MAAM,CAAC;AAAA,MAC9C;AAAA,IACF,CAAC;AAAA,EACL;AACF;","names":["value"]}
package/dist/fields.d.cts CHANGED
@@ -10,6 +10,9 @@
10
10
  * <a href={f.value('cta_url')} {...f.attrs('cta_url')}>…</a>
11
11
  * <img {...f.image('hero_image')} /> // src, alt, width, height (+ data-zap in preview)
12
12
  *
13
+ * // A value the site renders itself (split, uppercased, formatted):
14
+ * <h1 {...f.derived('title')}>{splitTitle(f.value('title'))}</h1>
15
+ *
13
16
  * // Numbered slots: `stat_1` … `stat_4`
14
17
  * {f.list('stat', 4).map((s) => <p key={s.key}>{s.text()}</p>)}
15
18
  * // Slots of several fields: `nav_1_texto` + `nav_1_url` … `nav_5_…`
@@ -27,6 +30,12 @@
27
30
  * keeps its marker (the overlay finds it with no tag), and the non-text
28
31
  * helpers add `data-zap` (a manual tag). Markers never reach an attribute:
29
32
  * `value`, `image` and `attrs` clean them.
33
+ *
34
+ * `derived` is for an element that shows what the site computes from a field
35
+ * (an italic-split title, a long text split into paragraphs, an uppercased
36
+ * label): in preview it adds `data-zap-site-renders`, so the overlay outlines
37
+ * the element and opens the field on a click but never writes into it. The
38
+ * site keeps it current with `useZapLiveUpdates` or `onValues`.
30
39
  */
31
40
  /** What `fields` reads: an item (`collection.key` + `slug`) or a document (`key`). */
32
41
  interface FieldsRecord {
@@ -59,6 +68,10 @@ type SlotValue<R extends FieldsRecord, K> = string extends Key<R> ? unknown : Co
59
68
  type ZapAttrs = {
60
69
  'data-zap'?: string;
61
70
  };
71
+ /** `{ 'data-zap-site-renders': 'blog/hola#title' }` in preview, `{}` otherwise. */
72
+ type ZapSiteRendersAttrs = {
73
+ 'data-zap-site-renders'?: string;
74
+ };
62
75
  interface FieldImage extends ZapAttrs {
63
76
  src: string | undefined;
64
77
  alt: string | undefined;
@@ -86,6 +99,8 @@ interface FieldSlot<R extends FieldsRecord, P extends string> {
86
99
  value<S extends SlotSuffix<R, P>>(suffix: S): SlotValue<R, `${P}_${number}_${S}`>;
87
100
  /** `{ 'data-zap': ref#key }` in preview, `{}` otherwise. */
88
101
  attrs(suffix?: SlotSuffix<R, P>): ZapAttrs;
102
+ /** `{ 'data-zap-site-renders': ref#key }` in preview: the site renders it, never written into. */
103
+ derived(suffix?: SlotSuffix<R, P>): ZapSiteRendersAttrs;
89
104
  /** An image or media field as `<img>` attributes, tagged in preview. */
90
105
  image(suffix?: SlotSuffix<R, P>): FieldImage;
91
106
  }
@@ -98,6 +113,12 @@ interface Fields<R extends FieldsRecord> {
98
113
  value<K extends Key<R>>(key: K): Content<R>[K];
99
114
  /** `{ 'data-zap': ref#key }` in preview, `{}` otherwise: spread it on the element. */
100
115
  attrs(key: Key<R>): ZapAttrs;
116
+ /**
117
+ * `{ 'data-zap-site-renders': ref#key }` in preview, `{}` otherwise: for an
118
+ * element that shows what the site computes from the field. The overlay
119
+ * outlines it and opens the field on a click, and never writes into it.
120
+ */
121
+ derived(key: Key<R>): ZapSiteRendersAttrs;
101
122
  /** An image or media field as `<img>` attributes, tagged in preview. */
102
123
  image(key: Key<R>): FieldImage;
103
124
  /**
@@ -113,4 +134,4 @@ interface FieldsOptions {
113
134
  }
114
135
  declare function fields<R extends FieldsRecord>(record: R, options?: FieldsOptions): Fields<R>;
115
136
 
116
- export { type FieldImage, type FieldSlot, type Fields, type FieldsOptions, type FieldsRecord, type ZapAttrs, fields };
137
+ export { type FieldImage, type FieldSlot, type Fields, type FieldsOptions, type FieldsRecord, type ZapAttrs, type ZapSiteRendersAttrs, fields };
package/dist/fields.d.ts CHANGED
@@ -10,6 +10,9 @@
10
10
  * <a href={f.value('cta_url')} {...f.attrs('cta_url')}>…</a>
11
11
  * <img {...f.image('hero_image')} /> // src, alt, width, height (+ data-zap in preview)
12
12
  *
13
+ * // A value the site renders itself (split, uppercased, formatted):
14
+ * <h1 {...f.derived('title')}>{splitTitle(f.value('title'))}</h1>
15
+ *
13
16
  * // Numbered slots: `stat_1` … `stat_4`
14
17
  * {f.list('stat', 4).map((s) => <p key={s.key}>{s.text()}</p>)}
15
18
  * // Slots of several fields: `nav_1_texto` + `nav_1_url` … `nav_5_…`
@@ -27,6 +30,12 @@
27
30
  * keeps its marker (the overlay finds it with no tag), and the non-text
28
31
  * helpers add `data-zap` (a manual tag). Markers never reach an attribute:
29
32
  * `value`, `image` and `attrs` clean them.
33
+ *
34
+ * `derived` is for an element that shows what the site computes from a field
35
+ * (an italic-split title, a long text split into paragraphs, an uppercased
36
+ * label): in preview it adds `data-zap-site-renders`, so the overlay outlines
37
+ * the element and opens the field on a click but never writes into it. The
38
+ * site keeps it current with `useZapLiveUpdates` or `onValues`.
30
39
  */
31
40
  /** What `fields` reads: an item (`collection.key` + `slug`) or a document (`key`). */
32
41
  interface FieldsRecord {
@@ -59,6 +68,10 @@ type SlotValue<R extends FieldsRecord, K> = string extends Key<R> ? unknown : Co
59
68
  type ZapAttrs = {
60
69
  'data-zap'?: string;
61
70
  };
71
+ /** `{ 'data-zap-site-renders': 'blog/hola#title' }` in preview, `{}` otherwise. */
72
+ type ZapSiteRendersAttrs = {
73
+ 'data-zap-site-renders'?: string;
74
+ };
62
75
  interface FieldImage extends ZapAttrs {
63
76
  src: string | undefined;
64
77
  alt: string | undefined;
@@ -86,6 +99,8 @@ interface FieldSlot<R extends FieldsRecord, P extends string> {
86
99
  value<S extends SlotSuffix<R, P>>(suffix: S): SlotValue<R, `${P}_${number}_${S}`>;
87
100
  /** `{ 'data-zap': ref#key }` in preview, `{}` otherwise. */
88
101
  attrs(suffix?: SlotSuffix<R, P>): ZapAttrs;
102
+ /** `{ 'data-zap-site-renders': ref#key }` in preview: the site renders it, never written into. */
103
+ derived(suffix?: SlotSuffix<R, P>): ZapSiteRendersAttrs;
89
104
  /** An image or media field as `<img>` attributes, tagged in preview. */
90
105
  image(suffix?: SlotSuffix<R, P>): FieldImage;
91
106
  }
@@ -98,6 +113,12 @@ interface Fields<R extends FieldsRecord> {
98
113
  value<K extends Key<R>>(key: K): Content<R>[K];
99
114
  /** `{ 'data-zap': ref#key }` in preview, `{}` otherwise: spread it on the element. */
100
115
  attrs(key: Key<R>): ZapAttrs;
116
+ /**
117
+ * `{ 'data-zap-site-renders': ref#key }` in preview, `{}` otherwise: for an
118
+ * element that shows what the site computes from the field. The overlay
119
+ * outlines it and opens the field on a click, and never writes into it.
120
+ */
121
+ derived(key: Key<R>): ZapSiteRendersAttrs;
101
122
  /** An image or media field as `<img>` attributes, tagged in preview. */
102
123
  image(key: Key<R>): FieldImage;
103
124
  /**
@@ -113,4 +134,4 @@ interface FieldsOptions {
113
134
  }
114
135
  declare function fields<R extends FieldsRecord>(record: R, options?: FieldsOptions): Fields<R>;
115
136
 
116
- export { type FieldImage, type FieldSlot, type Fields, type FieldsOptions, type FieldsRecord, type ZapAttrs, fields };
137
+ export { type FieldImage, type FieldSlot, type Fields, type FieldsOptions, type FieldsRecord, type ZapAttrs, type ZapSiteRendersAttrs, fields };
package/dist/fields.js CHANGED
@@ -58,6 +58,7 @@ function fields(record, options = {}) {
58
58
  const preview = options.preview ?? Object.values(content).some((value2) => hasStega(value2));
59
59
  const [ref] = recordRefsOf(record);
60
60
  const attrs = (key) => preview && ref ? { "data-zap": `${ref}#${key}` } : {};
61
+ const derived = (key) => preview && ref ? { "data-zap-site-renders": `${ref}#${key}` } : {};
61
62
  const value = (key) => cleanStega(content[key]);
62
63
  const text = (key) => {
63
64
  const raw = content[key];
@@ -79,6 +80,7 @@ function fields(record, options = {}) {
79
80
  text,
80
81
  value,
81
82
  attrs,
83
+ derived,
82
84
  image,
83
85
  list: (prefix, count) => Array.from({ length: Math.max(0, Math.floor(count)) }, (_, i) => {
84
86
  const key = `${prefix}_${i + 1}`;
@@ -94,6 +96,7 @@ function fields(record, options = {}) {
94
96
  text: (suffix) => text(at(suffix)),
95
97
  value: (suffix) => value(at(suffix)),
96
98
  attrs: (suffix) => attrs(at(suffix)),
99
+ derived: (suffix) => derived(at(suffix)),
97
100
  image: (suffix) => image(at(suffix))
98
101
  };
99
102
  })
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/refs.ts","../src/stega.ts","../src/fields.ts"],"sourcesContent":["/**\n * The record and field grammar of tags (`.docs/proposals/zap-cms-v2.md` §2.4),\n * in a module of its own so the light entries (`./fields`, `./react`, the\n * boot) can name records without pulling in the preview protocol.\n * No imports, no side effects.\n */\n\n/**\n * A record reference in its canonical string form: `collectionKey/slug` for an\n * entry, `doc:documentKey` for a document (the tag grammar, §2.4).\n */\nexport type RecordRef = string\n\n/** Zap's field key pattern. */\nexport const FIELD_KEY_RE = /^[a-z][a-z0-9_]{0,127}$/\n/** Entry and document references; keys and slugs are `[a-z0-9]+(?:-[a-z0-9]+)*`. */\nconst ENTRY_REF_RE = /^([a-z0-9]+(?:-[a-z0-9]+)*)\\/([a-z0-9]+(?:-[a-z0-9]+)*)$/\nconst DOC_REF_RE = /^doc:([a-z0-9]+(?:-[a-z0-9]+)*)$/\nconst MAX_REF_LENGTH = 300\n\nexport type ParsedRecordRef =\n | { kind: 'entry'; collection: string; slug: string }\n | { kind: 'document'; key: string }\n\nexport function parseRecordRef(value: unknown): ParsedRecordRef | null {\n if (typeof value !== 'string' || value.length > MAX_REF_LENGTH) return null\n const doc = DOC_REF_RE.exec(value)\n if (doc) return { kind: 'document', key: doc[1]! }\n const entry = ENTRY_REF_RE.exec(value)\n if (entry) return { kind: 'entry', collection: entry[1]!, slug: entry[2]! }\n return null\n}\n\nexport function isRecordRef(value: unknown): value is RecordRef {\n return parseRecordRef(value) !== null\n}\n\n/** `zpt_` + 32 bytes in base64url, as Zap mints it (zap-cms-v2 §3.5). */\nexport const PREVIEW_TOKEN_RE = /^zpt_[A-Za-z0-9_-]{43}$/\n\n/** A preview token in the shape Zap mints; says nothing about whether it is live. */\nexport function isPreviewTokenShape(value: unknown): value is string {\n return typeof value === 'string' && PREVIEW_TOKEN_RE.test(value)\n}\n\nexport function isFieldKey(value: unknown): value is string {\n return typeof value === 'string' && FIELD_KEY_RE.test(value)\n}\n\n/**\n * What `zapAttrs` and `onValues` accept: a record reference string\n * (`blog/hola`, `doc:home`) or a delivery API entry (an item carries\n * `collection.key` and `slug`, a document `key`).\n */\nexport type EntryLike =\n | RecordRef\n | {\n slug?: string | null\n key?: string | null\n collection?: { key?: string | null } | null\n localizedSlugs?: Record<string, string | null | undefined> | null\n }\n\n/** Every reference an entry answers to (default slug first, then localized ones). */\nexport function recordRefsOf(entry: EntryLike | null | undefined): RecordRef[] {\n if (!entry) return []\n if (typeof entry === 'string') return isRecordRef(entry) ? [entry] : []\n const refs: RecordRef[] = []\n const add = (ref: string) => {\n if (isRecordRef(ref) && !refs.includes(ref)) refs.push(ref)\n }\n const collection = entry.collection?.key\n if (collection) {\n if (entry.slug) add(`${collection}/${entry.slug}`)\n for (const slug of Object.values(entry.localizedSlugs ?? {})) {\n if (slug) add(`${collection}/${slug}`)\n }\n } else if (entry.key) {\n add(`doc:${entry.key}`)\n }\n return refs\n}\n\n/**\n * The tag for one field, as attributes to spread on the element that renders\n * it: `{ 'data-zap': 'blog/hola#title' }`. Empty when the entry or the field\n * key is not one Zap can name, so a bad call never breaks the page.\n */\nexport function zapAttrs(entry: EntryLike, fieldKey: string): { 'data-zap'?: string } {\n const [ref] = recordRefsOf(entry)\n if (!ref || !isFieldKey(fieldKey)) return {}\n return { 'data-zap': `${ref}#${fieldKey}` }\n}\n","/**\n * Stega: invisible field markers on prose values read in preview\n * (`.docs/proposals/zap-cms-v2.md` §2.4, §7.5).\n *\n * Zap appends a marker to TEXT and LONG_TEXT values, and to the last text\n * node of each RICH_TEXT block, ONLY in `preview=1` delivery responses. The\n * preview overlay finds the marker in the page's text nodes and so knows\n * which element renders which field, with no manual tag. Published responses\n * never carry one.\n *\n * ## Format (eelzap stega v1)\n *\n * A marker is the tag the overlay would read from a `data-zap` attribute,\n * `recordRef#fieldKey`, plus `@locale` when the value belongs to a locale:\n *\n * blog-posts/mi-primer-post#title@es\n * doc:home#hero_title@es\n *\n * written as zero-width characters:\n *\n * - four digits, base 4: U+200B (0), U+200C (1), U+200D (2), U+2060 (3);\n * - a prefix of 8 digits, the bytes 0xE5 0xA7 (`3211 2213`);\n * - then every byte of the ASCII payload as 4 digits, most significant first.\n *\n * The payload is printable ASCII (the tag grammar allows nothing else), so\n * every payload byte starts with digit 0 or 1 and the prefix, which starts\n * with 3, can never be read inside a payload. A decoder reads groups of four\n * after the prefix until a group is not a payload byte, then validates the\n * result against the tag grammar; anything that does not parse is ignored. A\n * value never carries two markers (`stegaEncode` refuses to add a second).\n *\n * Why not `@vercel/stega`'s format: it encodes a JSON object (a URL and\n * origin in Sanity's and Vercel's use) and uses U+FEFF, which some tools strip\n * as a byte order mark. Here the decoded marker IS the tag grammar\n * (`data-zap`), so one parser serves both, and U+FEFF is avoided. No\n * dependency either way.\n *\n * Zero dependencies, no side effects: `.` re-exports `cleanStega` and\n * `hasStega`; the encoder is internal (Zap's delivery imports it); the\n * decoder serves `./preview`.\n */\n\nconst DIGITS = ['​', '‌', '‍', '⁠'] as const\nconst PREFIX = '⁠‍‌‌‍‍‌⁠'\n/** Longest payload a marker may carry, in bytes (ref 300 + field 128 + locale). */\nconst MAX_PAYLOAD = 512\n\n/** `recordRef#fieldKey` with an optional `@locale`: the tag grammar (`refs.ts`). */\nconst PAYLOAD_RE =\n /^(doc:[a-z0-9]+(?:-[a-z0-9]+)*|[a-z0-9]+(?:-[a-z0-9]+)*\\/[a-z0-9]+(?:-[a-z0-9]+)*)#([a-z][a-z0-9_]{0,127})(?:@([A-Za-z]{2,3}(?:-[A-Za-z0-9]{2,8}){0,3}))?$/\n/** The prefix, then whole payload bytes (first digit 0 or 1). */\nconst MARKER_RE = /⁠‍‌‌‍‍‌⁠(?:[​‌][​‌‍⁠]{3})*/g\n\n/** What one marker says: the record, the field, and the locale when known. */\nexport interface StegaInfo {\n /** `collectionKey/slug` or `doc:documentKey`. */\n recordRef: string\n fieldKey: string\n locale?: string | null\n}\n\nexport interface DecodedStega {\n recordRef: string\n fieldKey: string\n locale: string | null\n /** `recordRef#fieldKey`, the `data-zap` tag. */\n tag: string\n}\n\n/** The marker for one field, or `''` when the info does not fit the tag grammar. */\nexport function stegaMarker(info: StegaInfo): string {\n const payload = `${info.recordRef}#${info.fieldKey}${info.locale ? `@${info.locale}` : ''}`\n if (payload.length > MAX_PAYLOAD || !PAYLOAD_RE.test(payload)) return ''\n let out = PREFIX\n for (let i = 0; i < payload.length; i++) {\n const byte = payload.charCodeAt(i)\n out +=\n DIGITS[(byte >> 6) & 3]! +\n DIGITS[(byte >> 4) & 3]! +\n DIGITS[(byte >> 2) & 3]! +\n DIGITS[byte & 3]!\n }\n return out\n}\n\n/**\n * `text` with the field's marker appended. A string that already carries a\n * marker, or info outside the grammar, comes back unchanged.\n */\nexport function stegaEncode(text: string, info: StegaInfo): string {\n if (hasStega(text)) return text\n return text + stegaMarker(info)\n}\n\n/** True when `value` is a string carrying at least one marker. */\nexport function hasStega(value: unknown): boolean {\n return typeof value === 'string' && value.includes(PREFIX)\n}\n\n/** Every valid marker in `text`, in order. Invalid ones are skipped. */\nexport function stegaDecode(text: string): DecodedStega[] {\n if (!text.includes(PREFIX)) return []\n const found: DecodedStega[] = []\n for (const match of text.match(MARKER_RE) ?? []) {\n let payload = ''\n for (let i = PREFIX.length; i + 4 <= match.length; i += 4) {\n let byte = 0\n for (let j = 0; j < 4; j++) byte = byte * 4 + DIGITS.indexOf(match[i + j] as never)\n payload += String.fromCharCode(byte)\n }\n const parsed = PAYLOAD_RE.exec(payload)\n if (!parsed) continue\n found.push({\n recordRef: parsed[1]!,\n fieldKey: parsed[2]!,\n locale: parsed[3] ?? null,\n tag: `${parsed[1]}#${parsed[2]}`,\n })\n }\n return found\n}\n\n/** `text` split into its visible part and the markers it carries, in order. */\nexport function splitStega(text: string): { text: string; markers: string } {\n if (!text.includes(PREFIX)) return { text, markers: '' }\n return { text: text.replace(MARKER_RE, ''), markers: (text.match(MARKER_RE) ?? []).join('') }\n}\n\n/** `text` without any marker. */\nfunction cleanString(text: string): string {\n return text.includes(PREFIX) ? text.replace(MARKER_RE, '') : text\n}\n\n/**\n * `value` with every stega marker removed: a string is cleaned, an array or a\n * plain object is cleaned deeply (into new objects), anything else is\n * returned as is. Use it where a preview value is compared, parsed or put in\n * an attribute, a URL or `<head>`; outside preview it changes nothing.\n */\nexport function cleanStega<T>(value: T): T {\n if (typeof value === 'string') return cleanString(value) as T\n if (Array.isArray(value)) return value.map((item) => cleanStega(item)) as T\n if (value && typeof value === 'object' && Object.getPrototypeOf(value) === Object.prototype) {\n const out: Record<string, unknown> = {}\n for (const [key, item] of Object.entries(value)) out[key] = cleanStega(item)\n return out as T\n }\n return value\n}\n","import { recordRefsOf } from './refs'\nimport { cleanStega, hasStega } from './stega'\n\n/**\n * `@8ux-co/eelzap/fields` (`.docs/proposals/zap-cms-v2.md` §2.4, §2.7): typed\n * pick helpers over an item or a document that tag themselves in preview.\n *\n * ```tsx\n * import { fields } from '@8ux-co/eelzap/fields'\n *\n * const f = fields(home) // a HomeDocument from codegen\n * <h1>{f.text('hero_title')}</h1> // carries its stega marker in preview\n * <a href={f.value('cta_url')} {...f.attrs('cta_url')}>…</a>\n * <img {...f.image('hero_image')} /> // src, alt, width, height (+ data-zap in preview)\n *\n * // Numbered slots: `stat_1` … `stat_4`\n * {f.list('stat', 4).map((s) => <p key={s.key}>{s.text()}</p>)}\n * // Slots of several fields: `nav_1_texto` + `nav_1_url` … `nav_5_…`\n * {f.list('nav', 5).filter((nav) => !nav.empty).map((nav) => (\n * <a key={nav.key} href={nav.value('url') ?? '/'} {...nav.attrs('url')}>{nav.text('texto')}</a>\n * ))}\n * ```\n *\n * Keys are typed from the record's `content`, so with codegen types a typo in\n * a field key fails to compile.\n *\n * Outside preview every helper returns plain values and EMPTY attribute\n * objects, so production HTML is byte-identical to reading values directly.\n * In preview (the record's text carries stega, or `{ preview: true }`), text\n * keeps its marker (the overlay finds it with no tag), and the non-text\n * helpers add `data-zap` (a manual tag). Markers never reach an attribute:\n * `value`, `image` and `attrs` clean them.\n */\n\n/** What `fields` reads: an item (`collection.key` + `slug`) or a document (`key`). */\nexport interface FieldsRecord {\n content: object\n slug?: string | null\n key?: string | null\n collection?: { key?: string | null } | null\n}\n\ntype Content<R extends FieldsRecord> = R['content']\ntype Key<R extends FieldsRecord> = keyof Content<R> & string\n/** Field keys whose value is text (string, possibly null). */\ntype TextKey<R extends FieldsRecord> = {\n [K in Key<R>]: NonNullable<Content<R>[K]> extends string ? K : never\n}[Key<R>]\n/**\n * Every numbered prefix in `K`: `stat` for `stat_1`, `nav` for `nav_1_texto`,\n * `hero_stat` for `hero_stat_1`. Walks the key one `_` segment at a time, so a\n * prefix may itself contain `_`.\n */\ntype Prefixes<K extends string, Head extends string = ''> = K extends `${infer P}_${infer Rest}`\n ?\n | (Rest extends `${number}` | `${number}_${string}` ? `${Head}${P}` : never)\n | Prefixes<Rest, `${Head}${P}_`>\n : never\n/** Numbered prefixes of the record (any string for an untyped record). */\ntype ListPrefix<R extends FieldsRecord> = string extends Key<R> ? string : Prefixes<Key<R>>\n/** The suffixes of `prefix_{i}_{suffix}` slots: `texto` and `url` for `nav`. */\ntype SlotSuffix<R extends FieldsRecord, P extends string> =\n string extends Key<R>\n ? string\n : Key<R> extends infer K\n ? K extends `${P}_${number}_${infer S}`\n ? S\n : never\n : never\n/** A slot field's value type; `unknown` for an untyped record. */\ntype SlotValue<R extends FieldsRecord, K> =\n string extends Key<R> ? unknown : Content<R>[Extract<Key<R>, K>]\n\n/** `{ 'data-zap': 'blog/hola#cover' }` in preview, `{}` otherwise. */\nexport type ZapAttrs = { 'data-zap'?: string }\n\nexport interface FieldImage extends ZapAttrs {\n src: string | undefined\n alt: string | undefined\n width: number | undefined\n height: number | undefined\n}\n\n/**\n * One numbered slot of `f.list(prefix, n)`: the same helpers as `fields`,\n * scoped to the slot. With a suffix they read `prefix_{i}_{suffix}`\n * (`nav.text('texto')` is `nav_1_texto`); without one, `prefix_{i}` itself\n * (`stat.text()` is `stat_1`).\n */\nexport interface FieldSlot<R extends FieldsRecord, P extends string> {\n /** `prefix_{i}`, unique in the list: a React key. */\n readonly key: string\n /** 1 for the first slot. */\n readonly index: number\n /** True when every field of the slot is empty (or the slot has no field). */\n readonly empty: boolean\n /** A text field as delivered, with its stega marker in preview. */\n text(suffix?: SlotSuffix<R, P>): string\n /** The single field `prefix_{i}`, with any marker removed. */\n value(): SlotValue<R, `${P}_${number}`>\n /** The slot's field `prefix_{i}_{suffix}`, with any marker removed. */\n value<S extends SlotSuffix<R, P>>(suffix: S): SlotValue<R, `${P}_${number}_${S}`>\n /** `{ 'data-zap': ref#key }` in preview, `{}` otherwise. */\n attrs(suffix?: SlotSuffix<R, P>): ZapAttrs\n /** An image or media field as `<img>` attributes, tagged in preview. */\n image(suffix?: SlotSuffix<R, P>): FieldImage\n}\n\nexport interface Fields<R extends FieldsRecord> {\n /** True when the helpers tag (the record was read in preview). */\n readonly preview: boolean\n /** A text field as delivered: with its stega marker in preview, so the overlay finds it. */\n text<K extends TextKey<R>>(key: K): string\n /** A field's value with any marker removed: safe in attributes, URLs, comparisons. */\n value<K extends Key<R>>(key: K): Content<R>[K]\n /** `{ 'data-zap': ref#key }` in preview, `{}` otherwise: spread it on the element. */\n attrs(key: Key<R>): ZapAttrs\n /** An image or media field as `<img>` attributes, tagged in preview. */\n image(key: Key<R>): FieldImage\n /**\n * Slots `1` … `count` of a numbered prefix, each a `FieldSlot`: fields\n * `prefix_1` … `prefix_n`, or slots of several fields\n * `prefix_{i}_{suffix}` (`nav_1_texto`, `nav_1_url`), read by suffix.\n */\n list<P extends ListPrefix<R>>(prefix: P, count: number): FieldSlot<R, P>[]\n}\n\nexport interface FieldsOptions {\n /** Force preview on or off; default: on when any text value carries stega. */\n preview?: boolean\n}\n\nexport function fields<R extends FieldsRecord>(record: R, options: FieldsOptions = {}): Fields<R> {\n const content = (record?.content ?? {}) as Record<string, unknown>\n const preview = options.preview ?? Object.values(content).some((value) => hasStega(value))\n const [ref] = recordRefsOf(record)\n const attrs = (key: string): ZapAttrs => (preview && ref ? { 'data-zap': `${ref}#${key}` } : {})\n const value = (key: string) => cleanStega(content[key])\n const text = (key: string) => {\n const raw = content[key]\n return typeof raw === 'string' ? (preview ? raw : cleanStega(raw)) : ''\n }\n const image = (key: string): FieldImage => {\n const media = (value(key) ?? {}) as Record<string, unknown>\n const pick = <T>(name: string, type: string) =>\n typeof media[name] === type ? (media[name] as T) : undefined\n return {\n src: pick<string>('url', 'string'),\n alt: pick<string>('alt', 'string'),\n width: pick<number>('width', 'number'),\n height: pick<number>('height', 'number'),\n ...attrs(key),\n }\n }\n\n return {\n preview,\n text,\n value: value as Fields<R>['value'],\n attrs,\n image,\n list: (prefix, count) =>\n Array.from({ length: Math.max(0, Math.floor(count)) }, (_, i) => {\n const key = `${prefix}_${i + 1}`\n const at = (suffix?: string) => (suffix ? `${key}_${suffix}` : key)\n const own = Object.keys(content).filter((k) => k === key || k.startsWith(`${key}_`))\n return {\n key,\n index: i + 1,\n empty: own.every((k) => {\n const v = value(k)\n return v === null || v === undefined || (typeof v === 'string' && !v.trim())\n }),\n text: (suffix?: string) => text(at(suffix)),\n value: (suffix?: string) => value(at(suffix)),\n attrs: (suffix?: string) => attrs(at(suffix)),\n image: (suffix?: string) => image(at(suffix)),\n } as FieldSlot<R, typeof prefix>\n }),\n }\n}\n"],"mappings":"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","names":["value"]}
1
+ {"version":3,"sources":["../src/refs.ts","../src/stega.ts","../src/fields.ts"],"sourcesContent":["/**\n * The record and field grammar of tags (`.docs/proposals/zap-cms-v2.md` §2.4),\n * in a module of its own so the light entries (`./fields`, `./react`, the\n * boot) can name records without pulling in the preview protocol.\n * No imports, no side effects.\n */\n\n/**\n * A record reference in its canonical string form: `collectionKey/slug` for an\n * entry, `doc:documentKey` for a document (the tag grammar, §2.4).\n */\nexport type RecordRef = string\n\n/** Zap's field key pattern. */\nexport const FIELD_KEY_RE = /^[a-z][a-z0-9_]{0,127}$/\n/** Entry and document references; keys and slugs are `[a-z0-9]+(?:-[a-z0-9]+)*`. */\nconst ENTRY_REF_RE = /^([a-z0-9]+(?:-[a-z0-9]+)*)\\/([a-z0-9]+(?:-[a-z0-9]+)*)$/\nconst DOC_REF_RE = /^doc:([a-z0-9]+(?:-[a-z0-9]+)*)$/\nconst MAX_REF_LENGTH = 300\n\nexport type ParsedRecordRef =\n | { kind: 'entry'; collection: string; slug: string }\n | { kind: 'document'; key: string }\n\nexport function parseRecordRef(value: unknown): ParsedRecordRef | null {\n if (typeof value !== 'string' || value.length > MAX_REF_LENGTH) return null\n const doc = DOC_REF_RE.exec(value)\n if (doc) return { kind: 'document', key: doc[1]! }\n const entry = ENTRY_REF_RE.exec(value)\n if (entry) return { kind: 'entry', collection: entry[1]!, slug: entry[2]! }\n return null\n}\n\nexport function isRecordRef(value: unknown): value is RecordRef {\n return parseRecordRef(value) !== null\n}\n\n/** `zpt_` + 32 bytes in base64url, as Zap mints it (zap-cms-v2 §3.5). */\nexport const PREVIEW_TOKEN_RE = /^zpt_[A-Za-z0-9_-]{43}$/\n\n/** A preview token in the shape Zap mints; says nothing about whether it is live. */\nexport function isPreviewTokenShape(value: unknown): value is string {\n return typeof value === 'string' && PREVIEW_TOKEN_RE.test(value)\n}\n\nexport function isFieldKey(value: unknown): value is string {\n return typeof value === 'string' && FIELD_KEY_RE.test(value)\n}\n\n/**\n * What `zapAttrs` and `onValues` accept: a record reference string\n * (`blog/hola`, `doc:home`) or a delivery API entry (an item carries\n * `collection.key` and `slug`, a document `key`).\n */\nexport type EntryLike =\n | RecordRef\n | {\n slug?: string | null\n key?: string | null\n collection?: { key?: string | null } | null\n localizedSlugs?: Record<string, string | null | undefined> | null\n }\n\n/** Every reference an entry answers to (default slug first, then localized ones). */\nexport function recordRefsOf(entry: EntryLike | null | undefined): RecordRef[] {\n if (!entry) return []\n if (typeof entry === 'string') return isRecordRef(entry) ? [entry] : []\n const refs: RecordRef[] = []\n const add = (ref: string) => {\n if (isRecordRef(ref) && !refs.includes(ref)) refs.push(ref)\n }\n const collection = entry.collection?.key\n if (collection) {\n if (entry.slug) add(`${collection}/${entry.slug}`)\n for (const slug of Object.values(entry.localizedSlugs ?? {})) {\n if (slug) add(`${collection}/${slug}`)\n }\n } else if (entry.key) {\n add(`doc:${entry.key}`)\n }\n return refs\n}\n\n/**\n * The tag for one field, as attributes to spread on the element that renders\n * it: `{ 'data-zap': 'blog/hola#title' }`. Empty when the entry or the field\n * key is not one Zap can name, so a bad call never breaks the page.\n */\nexport function zapAttrs(entry: EntryLike, fieldKey: string): { 'data-zap'?: string } {\n const [ref] = recordRefsOf(entry)\n if (!ref || !isFieldKey(fieldKey)) return {}\n return { 'data-zap': `${ref}#${fieldKey}` }\n}\n","/**\n * Stega: invisible field markers on prose values read in preview\n * (`.docs/proposals/zap-cms-v2.md` §2.4, §7.5).\n *\n * Zap appends a marker to TEXT and LONG_TEXT values, and to the last text\n * node of each RICH_TEXT block, ONLY in `preview=1` delivery responses. The\n * preview overlay finds the marker in the page's text nodes and so knows\n * which element renders which field, with no manual tag. Published responses\n * never carry one.\n *\n * ## Format (eelzap stega v1)\n *\n * A marker is the tag the overlay would read from a `data-zap` attribute,\n * `recordRef#fieldKey`, plus `@locale` when the value belongs to a locale:\n *\n * blog-posts/mi-primer-post#title@es\n * doc:home#hero_title@es\n *\n * written as zero-width characters:\n *\n * - four digits, base 4: U+200B (0), U+200C (1), U+200D (2), U+2060 (3);\n * - a prefix of 8 digits, the bytes 0xE5 0xA7 (`3211 2213`);\n * - then every byte of the ASCII payload as 4 digits, most significant first.\n *\n * The payload is printable ASCII (the tag grammar allows nothing else), so\n * every payload byte starts with digit 0 or 1 and the prefix, which starts\n * with 3, can never be read inside a payload. A decoder reads groups of four\n * after the prefix until a group is not a payload byte, then validates the\n * result against the tag grammar; anything that does not parse is ignored. A\n * value never carries two markers (`stegaEncode` refuses to add a second).\n *\n * Why not `@vercel/stega`'s format: it encodes a JSON object (a URL and\n * origin in Sanity's and Vercel's use) and uses U+FEFF, which some tools strip\n * as a byte order mark. Here the decoded marker IS the tag grammar\n * (`data-zap`), so one parser serves both, and U+FEFF is avoided. No\n * dependency either way.\n *\n * Zero dependencies, no side effects: `.` re-exports `cleanStega` and\n * `hasStega`; the encoder is internal (Zap's delivery imports it); the\n * decoder serves `./preview`.\n */\n\nconst DIGITS = ['​', '‌', '‍', '⁠'] as const\nconst PREFIX = '⁠‍‌‌‍‍‌⁠'\n/** Longest payload a marker may carry, in bytes (ref 300 + field 128 + locale). */\nconst MAX_PAYLOAD = 512\n\n/** `recordRef#fieldKey` with an optional `@locale`: the tag grammar (`refs.ts`). */\nconst PAYLOAD_RE =\n /^(doc:[a-z0-9]+(?:-[a-z0-9]+)*|[a-z0-9]+(?:-[a-z0-9]+)*\\/[a-z0-9]+(?:-[a-z0-9]+)*)#([a-z][a-z0-9_]{0,127})(?:@([A-Za-z]{2,3}(?:-[A-Za-z0-9]{2,8}){0,3}))?$/\n/** The prefix, then whole payload bytes (first digit 0 or 1). */\nconst MARKER_RE = /⁠‍‌‌‍‍‌⁠(?:[​‌][​‌‍⁠]{3})*/g\n\n/** What one marker says: the record, the field, and the locale when known. */\nexport interface StegaInfo {\n /** `collectionKey/slug` or `doc:documentKey`. */\n recordRef: string\n fieldKey: string\n locale?: string | null\n}\n\nexport interface DecodedStega {\n recordRef: string\n fieldKey: string\n locale: string | null\n /** `recordRef#fieldKey`, the `data-zap` tag. */\n tag: string\n}\n\n/** The marker for one field, or `''` when the info does not fit the tag grammar. */\nexport function stegaMarker(info: StegaInfo): string {\n const payload = `${info.recordRef}#${info.fieldKey}${info.locale ? `@${info.locale}` : ''}`\n if (payload.length > MAX_PAYLOAD || !PAYLOAD_RE.test(payload)) return ''\n let out = PREFIX\n for (let i = 0; i < payload.length; i++) {\n const byte = payload.charCodeAt(i)\n out +=\n DIGITS[(byte >> 6) & 3]! +\n DIGITS[(byte >> 4) & 3]! +\n DIGITS[(byte >> 2) & 3]! +\n DIGITS[byte & 3]!\n }\n return out\n}\n\n/**\n * `text` with the field's marker appended. A string that already carries a\n * marker, or info outside the grammar, comes back unchanged.\n */\nexport function stegaEncode(text: string, info: StegaInfo): string {\n if (hasStega(text)) return text\n return text + stegaMarker(info)\n}\n\n/** True when `value` is a string carrying at least one marker. */\nexport function hasStega(value: unknown): boolean {\n return typeof value === 'string' && value.includes(PREFIX)\n}\n\n/** Every valid marker in `text`, in order. Invalid ones are skipped. */\nexport function stegaDecode(text: string): DecodedStega[] {\n if (!text.includes(PREFIX)) return []\n const found: DecodedStega[] = []\n for (const match of text.match(MARKER_RE) ?? []) {\n let payload = ''\n for (let i = PREFIX.length; i + 4 <= match.length; i += 4) {\n let byte = 0\n for (let j = 0; j < 4; j++) byte = byte * 4 + DIGITS.indexOf(match[i + j] as never)\n payload += String.fromCharCode(byte)\n }\n const parsed = PAYLOAD_RE.exec(payload)\n if (!parsed) continue\n found.push({\n recordRef: parsed[1]!,\n fieldKey: parsed[2]!,\n locale: parsed[3] ?? null,\n tag: `${parsed[1]}#${parsed[2]}`,\n })\n }\n return found\n}\n\n/** `text` split into its visible part and the markers it carries, in order. */\nexport function splitStega(text: string): { text: string; markers: string } {\n if (!text.includes(PREFIX)) return { text, markers: '' }\n return { text: text.replace(MARKER_RE, ''), markers: (text.match(MARKER_RE) ?? []).join('') }\n}\n\n/** `text` without any marker. */\nfunction cleanString(text: string): string {\n return text.includes(PREFIX) ? text.replace(MARKER_RE, '') : text\n}\n\n/**\n * `value` with every stega marker removed: a string is cleaned, an array or a\n * plain object is cleaned deeply (into new objects), anything else is\n * returned as is. Use it where a preview value is compared, parsed or put in\n * an attribute, a URL or `<head>`; outside preview it changes nothing.\n */\nexport function cleanStega<T>(value: T): T {\n if (typeof value === 'string') return cleanString(value) as T\n if (Array.isArray(value)) return value.map((item) => cleanStega(item)) as T\n if (value && typeof value === 'object' && Object.getPrototypeOf(value) === Object.prototype) {\n const out: Record<string, unknown> = {}\n for (const [key, item] of Object.entries(value)) out[key] = cleanStega(item)\n return out as T\n }\n return value\n}\n","import { recordRefsOf } from './refs'\nimport { cleanStega, hasStega } from './stega'\n\n/**\n * `@8ux-co/eelzap/fields` (`.docs/proposals/zap-cms-v2.md` §2.4, §2.7): typed\n * pick helpers over an item or a document that tag themselves in preview.\n *\n * ```tsx\n * import { fields } from '@8ux-co/eelzap/fields'\n *\n * const f = fields(home) // a HomeDocument from codegen\n * <h1>{f.text('hero_title')}</h1> // carries its stega marker in preview\n * <a href={f.value('cta_url')} {...f.attrs('cta_url')}>…</a>\n * <img {...f.image('hero_image')} /> // src, alt, width, height (+ data-zap in preview)\n *\n * // A value the site renders itself (split, uppercased, formatted):\n * <h1 {...f.derived('title')}>{splitTitle(f.value('title'))}</h1>\n *\n * // Numbered slots: `stat_1` … `stat_4`\n * {f.list('stat', 4).map((s) => <p key={s.key}>{s.text()}</p>)}\n * // Slots of several fields: `nav_1_texto` + `nav_1_url` … `nav_5_…`\n * {f.list('nav', 5).filter((nav) => !nav.empty).map((nav) => (\n * <a key={nav.key} href={nav.value('url') ?? '/'} {...nav.attrs('url')}>{nav.text('texto')}</a>\n * ))}\n * ```\n *\n * Keys are typed from the record's `content`, so with codegen types a typo in\n * a field key fails to compile.\n *\n * Outside preview every helper returns plain values and EMPTY attribute\n * objects, so production HTML is byte-identical to reading values directly.\n * In preview (the record's text carries stega, or `{ preview: true }`), text\n * keeps its marker (the overlay finds it with no tag), and the non-text\n * helpers add `data-zap` (a manual tag). Markers never reach an attribute:\n * `value`, `image` and `attrs` clean them.\n *\n * `derived` is for an element that shows what the site computes from a field\n * (an italic-split title, a long text split into paragraphs, an uppercased\n * label): in preview it adds `data-zap-site-renders`, so the overlay outlines\n * the element and opens the field on a click but never writes into it. The\n * site keeps it current with `useZapLiveUpdates` or `onValues`.\n */\n\n/** What `fields` reads: an item (`collection.key` + `slug`) or a document (`key`). */\nexport interface FieldsRecord {\n content: object\n slug?: string | null\n key?: string | null\n collection?: { key?: string | null } | null\n}\n\ntype Content<R extends FieldsRecord> = R['content']\ntype Key<R extends FieldsRecord> = keyof Content<R> & string\n/** Field keys whose value is text (string, possibly null). */\ntype TextKey<R extends FieldsRecord> = {\n [K in Key<R>]: NonNullable<Content<R>[K]> extends string ? K : never\n}[Key<R>]\n/**\n * Every numbered prefix in `K`: `stat` for `stat_1`, `nav` for `nav_1_texto`,\n * `hero_stat` for `hero_stat_1`. Walks the key one `_` segment at a time, so a\n * prefix may itself contain `_`.\n */\ntype Prefixes<K extends string, Head extends string = ''> = K extends `${infer P}_${infer Rest}`\n ?\n | (Rest extends `${number}` | `${number}_${string}` ? `${Head}${P}` : never)\n | Prefixes<Rest, `${Head}${P}_`>\n : never\n/** Numbered prefixes of the record (any string for an untyped record). */\ntype ListPrefix<R extends FieldsRecord> = string extends Key<R> ? string : Prefixes<Key<R>>\n/** The suffixes of `prefix_{i}_{suffix}` slots: `texto` and `url` for `nav`. */\ntype SlotSuffix<R extends FieldsRecord, P extends string> =\n string extends Key<R>\n ? string\n : Key<R> extends infer K\n ? K extends `${P}_${number}_${infer S}`\n ? S\n : never\n : never\n/** A slot field's value type; `unknown` for an untyped record. */\ntype SlotValue<R extends FieldsRecord, K> =\n string extends Key<R> ? unknown : Content<R>[Extract<Key<R>, K>]\n\n/** `{ 'data-zap': 'blog/hola#cover' }` in preview, `{}` otherwise. */\nexport type ZapAttrs = { 'data-zap'?: string }\n\n/** `{ 'data-zap-site-renders': 'blog/hola#title' }` in preview, `{}` otherwise. */\nexport type ZapSiteRendersAttrs = { 'data-zap-site-renders'?: string }\n\nexport interface FieldImage extends ZapAttrs {\n src: string | undefined\n alt: string | undefined\n width: number | undefined\n height: number | undefined\n}\n\n/**\n * One numbered slot of `f.list(prefix, n)`: the same helpers as `fields`,\n * scoped to the slot. With a suffix they read `prefix_{i}_{suffix}`\n * (`nav.text('texto')` is `nav_1_texto`); without one, `prefix_{i}` itself\n * (`stat.text()` is `stat_1`).\n */\nexport interface FieldSlot<R extends FieldsRecord, P extends string> {\n /** `prefix_{i}`, unique in the list: a React key. */\n readonly key: string\n /** 1 for the first slot. */\n readonly index: number\n /** True when every field of the slot is empty (or the slot has no field). */\n readonly empty: boolean\n /** A text field as delivered, with its stega marker in preview. */\n text(suffix?: SlotSuffix<R, P>): string\n /** The single field `prefix_{i}`, with any marker removed. */\n value(): SlotValue<R, `${P}_${number}`>\n /** The slot's field `prefix_{i}_{suffix}`, with any marker removed. */\n value<S extends SlotSuffix<R, P>>(suffix: S): SlotValue<R, `${P}_${number}_${S}`>\n /** `{ 'data-zap': ref#key }` in preview, `{}` otherwise. */\n attrs(suffix?: SlotSuffix<R, P>): ZapAttrs\n /** `{ 'data-zap-site-renders': ref#key }` in preview: the site renders it, never written into. */\n derived(suffix?: SlotSuffix<R, P>): ZapSiteRendersAttrs\n /** An image or media field as `<img>` attributes, tagged in preview. */\n image(suffix?: SlotSuffix<R, P>): FieldImage\n}\n\nexport interface Fields<R extends FieldsRecord> {\n /** True when the helpers tag (the record was read in preview). */\n readonly preview: boolean\n /** A text field as delivered: with its stega marker in preview, so the overlay finds it. */\n text<K extends TextKey<R>>(key: K): string\n /** A field's value with any marker removed: safe in attributes, URLs, comparisons. */\n value<K extends Key<R>>(key: K): Content<R>[K]\n /** `{ 'data-zap': ref#key }` in preview, `{}` otherwise: spread it on the element. */\n attrs(key: Key<R>): ZapAttrs\n /**\n * `{ 'data-zap-site-renders': ref#key }` in preview, `{}` otherwise: for an\n * element that shows what the site computes from the field. The overlay\n * outlines it and opens the field on a click, and never writes into it.\n */\n derived(key: Key<R>): ZapSiteRendersAttrs\n /** An image or media field as `<img>` attributes, tagged in preview. */\n image(key: Key<R>): FieldImage\n /**\n * Slots `1` … `count` of a numbered prefix, each a `FieldSlot`: fields\n * `prefix_1` … `prefix_n`, or slots of several fields\n * `prefix_{i}_{suffix}` (`nav_1_texto`, `nav_1_url`), read by suffix.\n */\n list<P extends ListPrefix<R>>(prefix: P, count: number): FieldSlot<R, P>[]\n}\n\nexport interface FieldsOptions {\n /** Force preview on or off; default: on when any text value carries stega. */\n preview?: boolean\n}\n\nexport function fields<R extends FieldsRecord>(record: R, options: FieldsOptions = {}): Fields<R> {\n const content = (record?.content ?? {}) as Record<string, unknown>\n const preview = options.preview ?? Object.values(content).some((value) => hasStega(value))\n const [ref] = recordRefsOf(record)\n const attrs = (key: string): ZapAttrs => (preview && ref ? { 'data-zap': `${ref}#${key}` } : {})\n const derived = (key: string): ZapSiteRendersAttrs =>\n preview && ref ? { 'data-zap-site-renders': `${ref}#${key}` } : {}\n const value = (key: string) => cleanStega(content[key])\n const text = (key: string) => {\n const raw = content[key]\n return typeof raw === 'string' ? (preview ? raw : cleanStega(raw)) : ''\n }\n const image = (key: string): FieldImage => {\n const media = (value(key) ?? {}) as Record<string, unknown>\n const pick = <T>(name: string, type: string) =>\n typeof media[name] === type ? (media[name] as T) : undefined\n return {\n src: pick<string>('url', 'string'),\n alt: pick<string>('alt', 'string'),\n width: pick<number>('width', 'number'),\n height: pick<number>('height', 'number'),\n ...attrs(key),\n }\n }\n\n return {\n preview,\n text,\n value: value as Fields<R>['value'],\n attrs,\n derived,\n image,\n list: (prefix, count) =>\n Array.from({ length: Math.max(0, Math.floor(count)) }, (_, i) => {\n const key = `${prefix}_${i + 1}`\n const at = (suffix?: string) => (suffix ? `${key}_${suffix}` : key)\n const own = Object.keys(content).filter((k) => k === key || k.startsWith(`${key}_`))\n return {\n key,\n index: i + 1,\n empty: own.every((k) => {\n const v = value(k)\n return v === null || v === undefined || (typeof v === 'string' && !v.trim())\n }),\n text: (suffix?: string) => text(at(suffix)),\n value: (suffix?: string) => value(at(suffix)),\n attrs: (suffix?: string) => attrs(at(suffix)),\n derived: (suffix?: string) => derived(at(suffix)),\n image: (suffix?: string) => image(at(suffix)),\n } as FieldSlot<R, typeof prefix>\n }),\n }\n}\n"],"mappings":"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","names":["value"]}
package/dist/index.d.cts CHANGED
@@ -208,11 +208,16 @@ interface EnumValue {
208
208
  * A CURRENCY field's value, read and written in this one shape.
209
209
  *
210
210
  * `amountMinor` is in the currency's minor unit (cents: `4800000` is
211
- * COP 48.000,00), an integer. `currency` is the ISO 4217 code.
211
+ * COP 48.000,00), an integer. `currency` is the ISO 4217 code. Reads also
212
+ * carry `amount`, the same value in major units (`48000`): `amountMinor`
213
+ * divided by ten to the currency's ISO 4217 exponent (COP and USD 2, JPY 0).
214
+ * Writes ignore it.
212
215
  */
213
216
  interface CurrencyValue {
214
217
  amountMinor: number;
215
218
  currency: string;
219
+ /** Major units, on reads: `amountMinor / 10^exponent`. */
220
+ amount?: number;
216
221
  }
217
222
  /**
218
223
  * Access mode for a media value.
@@ -410,6 +415,12 @@ interface CollectionSummary {
410
415
  key: string;
411
416
  name: string;
412
417
  description: string | null;
418
+ /**
419
+ * «Ruta en tu sitio»: the page an entry appears on, `/blog/{slug}`; `null`
420
+ * when unset. Read with a `secret_` key or a suite credential only; a
421
+ * `public_` key's answer does not carry it.
422
+ */
423
+ previewPath?: string | null;
413
424
  itemCount: number;
414
425
  fields?: FieldInfo[];
415
426
  sections?: SectionInfo[];
@@ -734,7 +745,7 @@ type CommentStatus = 'OPEN' | 'RESOLVED';
734
745
  /** How a change request was resolved; APPLIED also covers "changed by hand". */
735
746
  type CommentResolution = 'APPLIED' | 'DISMISSED';
736
747
  /** Where the thread was made. The API derives it from the credential. */
737
- type CommentSource = 'EDITOR' | 'PREVIEW' | 'SITE' | 'API' | 'MCP';
748
+ type CommentSource = 'EDITOR' | 'PREVIEW' | 'SITE' | 'API' | 'MCP' | 'AI';
738
749
  type CommentAnchorKind = 'field' | 'dom' | 'spot';
739
750
  /** A page-normalised rectangle: every value between 0 and 1. */
740
751
  interface CommentRect {
@@ -955,7 +966,7 @@ interface CommentWriteBase {
955
966
  type CreateCommentInput = CommentRecord & CommentWriteBase & {
956
967
  isChangeRequest?: boolean;
957
968
  assigneeId?: string | null;
958
- /** At most 20; TEXT, LONG_TEXT, NUMBER and URL fields. */
969
+ /** At most 20; TEXT, LONG_TEXT, NUMBER, URL and RICH_TEXT (an HTML string) fields. */
959
970
  proposedValues?: CommentProposedValueInput[];
960
971
  };
961
972
  /** `saveToDraft`: always a change request, no assignee, at least one proposed value. */
@@ -1124,6 +1135,7 @@ declare class CommentsResource {
1124
1135
  * string.
1125
1136
  */
1126
1137
  type VersionStatus = 'DRAFT' | 'PUBLISHED' | 'ARCHIVED';
1138
+ type VersionActorKind = 'PERSON' | 'ASK_ZAP' | 'API_KEY' | 'MCP' | 'SITE_KEY' | 'AI_JOB';
1127
1139
  /**
1128
1140
  * One row of an entry's history. What `list`, `createDraft`, `publishDraft`
1129
1141
  * and `rollback` answer with: no values.
@@ -1134,6 +1146,20 @@ interface VersionSummary {
1134
1146
  status: VersionStatus;
1135
1147
  /** The account that made the version, or null for an API key or a deleted account. */
1136
1148
  createdById: string | null;
1149
+ /**
1150
+ * What wrote the version, recorded when it was written: `PERSON` (the editor,
1151
+ * and every version from before this field), `ASK_ZAP`, `API_KEY` (a workspace
1152
+ * API key), `MCP` (an OAuth connection such as Claude), `SITE_KEY` (a site
1153
+ * secret key) or `AI_JOB` (the dashboard's bulk site translation).
1154
+ */
1155
+ actorKind: VersionActorKind;
1156
+ /**
1157
+ * The API key's or connection's name as it was when the version was written
1158
+ * (not renamed or removed with it); null for `PERSON` and `ASK_ZAP`.
1159
+ */
1160
+ actorName: string | null;
1161
+ /** The OAuth client's display name ("Claude Code") for an MCP write; null otherwise. */
1162
+ actorApp: string | null;
1137
1163
  /** The author's display name, or null when the account is gone. */
1138
1164
  authorName: string | null;
1139
1165
  authorAvatarUrl: string | null;
@@ -1307,6 +1333,11 @@ interface DocumentListEntry {
1307
1333
  key: string;
1308
1334
  name: string;
1309
1335
  description: string | null;
1336
+ /**
1337
+ * «Ruta en tu sitio»: the page this document is, `/nosotros`; `null` when
1338
+ * unset. Read with a `secret_` key or a suite credential only.
1339
+ */
1340
+ previewPath?: string | null;
1310
1341
  status: string;
1311
1342
  updatedAt: string;
1312
1343
  publishedAt: string | null;
@@ -1319,6 +1350,12 @@ interface DocumentListEntry {
1319
1350
  interface DocumentDetail<TContent = Record<string, unknown>> {
1320
1351
  key: string;
1321
1352
  name: string;
1353
+ /**
1354
+ * «Ruta en tu sitio»: the page this document is, `/nosotros`; `null` when
1355
+ * unset. Read with a `secret_` key or a suite credential only; a `public_`
1356
+ * key's answer does not carry it.
1357
+ */
1358
+ previewPath?: string | null;
1322
1359
  status: string;
1323
1360
  locale: string;
1324
1361
  meta: DocumentMeta;
@@ -2424,7 +2461,7 @@ interface WebhookCommentData<TEntry extends WebhookCommentRef = WebhookCommentRe
2424
2461
  } | null;
2425
2462
  comments: TEntry[];
2426
2463
  }
2427
- type WebhookCommentSource = 'EDITOR' | 'PREVIEW' | 'SITE' | 'API' | 'MCP';
2464
+ type WebhookCommentSource = 'EDITOR' | 'PREVIEW' | 'SITE' | 'API' | 'MCP' | 'AI';
2428
2465
  /** `data` of `zap.comment.created`. */
2429
2466
  type WebhookCommentCreatedData = WebhookCommentData<WebhookCommentRef & {
2430
2467
  is_change_request: boolean;
@@ -2513,4 +2550,4 @@ declare function verifyWebhookSignature(payload: string, headers: WebhookHeaders
2513
2550
  */
2514
2551
  declare function webhookChanges(payload: WebhookPayload): WebhookChange[];
2515
2552
 
2516
- export { type ApiErrorDetail, type ApiErrorPayload, type CacheAdapter, type CacheStrategy, type CachedFetchOptions, type ClientConfig, type ClientDefaults, type CollectionDetail, type CollectionFieldListResponse, CollectionFieldsResource, type CollectionListResponse, type CollectionSectionListResponse, CollectionSectionsResource, type CollectionSummary, CollectionsResource, type CommentAnchor, type CommentAnchorInput, type CommentAnchorKind, type CommentDomAnchor, type CommentDraftAnswer, type CommentFieldAnchorInput, type CommentListOptions, type CommentListResponse, type CommentPersonRef, type CommentProposedValue, type CommentProposedValueInput, type CommentRecord, type CommentRect, type CommentReplyAnswer, type CommentResolution, type CommentSource, type CommentSpotAnchor, type CommentStatus, type CommentTarget, type CommentThread, type CommentThreadAnswer, type CommentVersionRef, type CommentViewport, CommentsResource, type CommonRequestOptions, type ConfirmUploadInput, type CreateCollectionInput, type CreateCommentInput, type CreateDocumentFieldInput, type CreateDocumentInput, type CreateDraftInput, type CreateFieldInput, type CreateItemInput, type CreateSectionInput, type CreateUploadUrlInput, type CurrencyValue, type DeleteDocumentOptions, type DeleteItemOptions, type DeleteOptions, type DeliveryStatus, type DocumentDeleteResponse, type DocumentDetail, type DocumentFieldListResponse, DocumentFieldsResource, type DocumentGetOptions, type DocumentListEntry, type DocumentListOptions, type DocumentListResponse, type DocumentMeta, type DocumentMutationResponse, type DocumentSectionListResponse, DocumentSectionsResource, type DocumentSeo, DocumentSeoResource, type DocumentSeoResponse, DocumentValuesResource, type DocumentValuesResponse, type DocumentVersionDetail, type DocumentVersionSeo, type DocumentVersionSummary, type DocumentVersionValue, DocumentVersionsResource, DocumentsResource, EelZapClient, EelZapError, type EelZapErrorCode, EelZapNetworkError, type EnumValue, type FieldConstraints, type FieldInfo, type FieldOptionInput, type FieldType, type FilterOperatorMap, type FilterValue, type GalleryItemValue, type IdempotencyOptions, type ItemDeleteResponse, type ItemDetail, type ItemFilters, type ItemGetOptions, type ItemListOptions, type ItemListResponse, type ItemMeta, type ItemMutationResponse, ItemQueryBuilder, type ItemSeo, ItemSeoResource, type ItemSeoResponse, type ItemVersionDetail, type ItemVersionSeo, type ItemVersionSlug, type ItemVersionSummary, type ItemVersionValue, ItemVersionsResource, ItemsResource, type JsonObject, type MediaAccess, type MediaDetail, type MediaDetailResponse, type MediaFromUrlInput, type MediaListParams, type MediaListResponse, MediaResource, type MediaSort, type MediaType, type MediaUploadInput, type MediaUsage, type MediaValue, MemoryCacheAdapter, type OnPageComment, type OnPageCommentAnchor, type OnPageComments, type OnPageField, type Pagination, type PreviewOption, type PreviewTokenValidation, PreviewTokensResource, type QueryPrimitive, type ReplyCommentInput, type RetryEvent, type RetryOptions, type SaveCommentToDraftInput, type SectionInfo, type SectionMeta, type Seo, type SeoImage, type SeoInput, type SiteInfo, type SiteListEntry, SiteResource, SitesResource, type ThreadComment, type UpdateCollectionInput, type UpdateCommentInput, type UpdateDocumentDraftInput, type UpdateDocumentFieldInput, type UpdateDocumentInput, type UpdateFieldInput, type UpdateItemDraftInput, type UpdateItemInput, type UpdateMediaInput, type UpdateSectionInput, type UpdateSiteInput, type UploadFile, type UploadUrlResponse, type VerifyWebhookOptions, type VersionDetail, type VersionGalleryItem, type VersionListResponse, type VersionSeo, type VersionStatus, type VersionSummary, type VersionValue, WEBHOOK_SIGNATURE_HEADER, WEBHOOK_TIMESTAMP_HEADER, WEBHOOK_TOLERANCE_SECONDS, type WebhookAction, type WebhookActor, type WebhookChange, type WebhookCollectionEventData, type WebhookCollectionRef, type WebhookCommentCreatedData, type WebhookCommentData, type WebhookCommentRef, type WebhookCommentReopenedData, type WebhookCommentRepliedData, type WebhookCommentResolvedData, type WebhookCommentSource, type WebhookCommentTarget, type WebhookCommentUpdatedData, type WebhookDocumentEventData, type WebhookEventName, type WebhookEventType, type WebhookHeaders, type WebhookItemEventData, type WebhookMediaEventData, type WebhookPayload, type WebhookSchemaEventData, type WebhookSeoEventData, type WebhookSiteEventData, type WebhookSiteRef, type WebhookSubject, cachedFetch, cleanStega, createClient, getMediaUrl, hasStega, isEelZapError, verifyWebhookSignature, webhookChanges };
2553
+ export { type ApiErrorDetail, type ApiErrorPayload, type CacheAdapter, type CacheStrategy, type CachedFetchOptions, type ClientConfig, type ClientDefaults, type CollectionDetail, type CollectionFieldListResponse, CollectionFieldsResource, type CollectionListResponse, type CollectionSectionListResponse, CollectionSectionsResource, type CollectionSummary, CollectionsResource, type CommentAnchor, type CommentAnchorInput, type CommentAnchorKind, type CommentDomAnchor, type CommentDraftAnswer, type CommentFieldAnchorInput, type CommentListOptions, type CommentListResponse, type CommentPersonRef, type CommentProposedValue, type CommentProposedValueInput, type CommentRecord, type CommentRect, type CommentReplyAnswer, type CommentResolution, type CommentSource, type CommentSpotAnchor, type CommentStatus, type CommentTarget, type CommentThread, type CommentThreadAnswer, type CommentVersionRef, type CommentViewport, CommentsResource, type CommonRequestOptions, type ConfirmUploadInput, type CreateCollectionInput, type CreateCommentInput, type CreateDocumentFieldInput, type CreateDocumentInput, type CreateDraftInput, type CreateFieldInput, type CreateItemInput, type CreateSectionInput, type CreateUploadUrlInput, type CurrencyValue, type DeleteDocumentOptions, type DeleteItemOptions, type DeleteOptions, type DeliveryStatus, type DocumentDeleteResponse, type DocumentDetail, type DocumentFieldListResponse, DocumentFieldsResource, type DocumentGetOptions, type DocumentListEntry, type DocumentListOptions, type DocumentListResponse, type DocumentMeta, type DocumentMutationResponse, type DocumentSectionListResponse, DocumentSectionsResource, type DocumentSeo, DocumentSeoResource, type DocumentSeoResponse, DocumentValuesResource, type DocumentValuesResponse, type DocumentVersionDetail, type DocumentVersionSeo, type DocumentVersionSummary, type DocumentVersionValue, DocumentVersionsResource, DocumentsResource, EelZapClient, EelZapError, type EelZapErrorCode, EelZapNetworkError, type EnumValue, type FieldConstraints, type FieldInfo, type FieldOptionInput, type FieldType, type FilterOperatorMap, type FilterValue, type GalleryItemValue, type IdempotencyOptions, type ItemDeleteResponse, type ItemDetail, type ItemFilters, type ItemGetOptions, type ItemListOptions, type ItemListResponse, type ItemMeta, type ItemMutationResponse, ItemQueryBuilder, type ItemSeo, ItemSeoResource, type ItemSeoResponse, type ItemVersionDetail, type ItemVersionSeo, type ItemVersionSlug, type ItemVersionSummary, type ItemVersionValue, ItemVersionsResource, ItemsResource, type JsonObject, type MediaAccess, type MediaDetail, type MediaDetailResponse, type MediaFromUrlInput, type MediaListParams, type MediaListResponse, MediaResource, type MediaSort, type MediaType, type MediaUploadInput, type MediaUsage, type MediaValue, MemoryCacheAdapter, type OnPageComment, type OnPageCommentAnchor, type OnPageComments, type OnPageField, type Pagination, type PreviewOption, type PreviewTokenValidation, PreviewTokensResource, type QueryPrimitive, type ReplyCommentInput, type RetryEvent, type RetryOptions, type SaveCommentToDraftInput, type SectionInfo, type SectionMeta, type Seo, type SeoImage, type SeoInput, type SiteInfo, type SiteListEntry, SiteResource, SitesResource, type ThreadComment, type UpdateCollectionInput, type UpdateCommentInput, type UpdateDocumentDraftInput, type UpdateDocumentFieldInput, type UpdateDocumentInput, type UpdateFieldInput, type UpdateItemDraftInput, type UpdateItemInput, type UpdateMediaInput, type UpdateSectionInput, type UpdateSiteInput, type UploadFile, type UploadUrlResponse, type VerifyWebhookOptions, type VersionActorKind, type VersionDetail, type VersionGalleryItem, type VersionListResponse, type VersionSeo, type VersionStatus, type VersionSummary, type VersionValue, WEBHOOK_SIGNATURE_HEADER, WEBHOOK_TIMESTAMP_HEADER, WEBHOOK_TOLERANCE_SECONDS, type WebhookAction, type WebhookActor, type WebhookChange, type WebhookCollectionEventData, type WebhookCollectionRef, type WebhookCommentCreatedData, type WebhookCommentData, type WebhookCommentRef, type WebhookCommentReopenedData, type WebhookCommentRepliedData, type WebhookCommentResolvedData, type WebhookCommentSource, type WebhookCommentTarget, type WebhookCommentUpdatedData, type WebhookDocumentEventData, type WebhookEventName, type WebhookEventType, type WebhookHeaders, type WebhookItemEventData, type WebhookMediaEventData, type WebhookPayload, type WebhookSchemaEventData, type WebhookSeoEventData, type WebhookSiteEventData, type WebhookSiteRef, type WebhookSubject, cachedFetch, cleanStega, createClient, getMediaUrl, hasStega, isEelZapError, verifyWebhookSignature, webhookChanges };