/** * Visual editing annotation system * * Creates Proxy objects that emit data-emdash-ref attributes when spread onto elements. */ export interface CMSAnnotation { collection: string; id: string; field?: string; /** Entry status — only present on entry-level annotations (not field-level) */ status?: string; /** Whether the entry has unpublished draft changes */ hasDraft?: boolean; } /** The shape returned when spreading an edit annotation onto an element */ export interface FieldAnnotation { "data-emdash-ref": string; } export interface EditableOptions { /** Entry status: "draft", "published", "scheduled" */ status?: string; /** true when draftRevisionId exists and differs from liveRevisionId */ hasDraft?: boolean; } /** * Create an editable proxy for an entry. * * Usage: * - `{...entry.edit}` - entry-level annotation (includes status/hasDraft) * - `{...entry.edit.title}` - field-level annotation * - `{...entry.edit['nested.field']}` - nested field (bracket notation) */ export function createEditable( collection: string, id: string, options?: EditableOptions, ): EditProxy { const base: CMSAnnotation = { collection, id, ...(options?.status && { status: options.status }), ...(options?.hasDraft && { hasDraft: true }), }; return new Proxy({} as EditProxy, { get(_, prop) { if (prop === "toJSON") return () => ({ "data-emdash-ref": JSON.stringify(base) }); if (typeof prop === "symbol") return undefined; // data-emdash-ref access returns the entry-level string if (prop === "data-emdash-ref") return JSON.stringify(base); // Field-level: return a FieldAnnotation for the specific field return { "data-emdash-ref": JSON.stringify({ ...base, field: String(prop) }), } satisfies FieldAnnotation; }, ownKeys() { return ["data-emdash-ref"]; }, getOwnPropertyDescriptor(_, prop) { if (prop === "data-emdash-ref") { return { configurable: true, enumerable: true, value: JSON.stringify(base), }; } return undefined; }, }); } /** * Create a noop proxy for production mode. * Spreading this produces no attributes. */ export function createNoop(): EditProxy { return new Proxy({} as EditProxy, { get(_, prop) { if (typeof prop === "symbol") return undefined; // All property access returns undefined in noop mode return undefined; }, ownKeys() { return []; }, getOwnPropertyDescriptor() { return undefined; }, }); } /** * Visual editing proxy type. * * Spread directly onto elements for entry-level annotations: `{...entry.edit}` * Access a field for field-level annotations: `{...entry.edit.title}` * * In production, spreading produces no attributes (noop). */ export type EditProxy = { readonly [field: string]: Partial; };