@typecms/sdk 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +151 -0
- package/dist/client-BW6BWpfF.d.mts +92 -0
- package/dist/flags/index.d.mts +100 -0
- package/dist/flags/index.mjs +99 -0
- package/dist/flags/index.mjs.map +1 -0
- package/dist/index.d.mts +67 -0
- package/dist/index.mjs +174 -0
- package/dist/index.mjs.map +1 -0
- package/dist/overlay-CLYAeNwg.d.mts +43 -0
- package/dist/preview/dom.d.mts +76 -0
- package/dist/preview/dom.mjs +352 -0
- package/dist/preview/dom.mjs.map +1 -0
- package/dist/preview/index.d.mts +438 -0
- package/dist/preview/index.mjs +736 -0
- package/dist/preview/index.mjs.map +1 -0
- package/dist/preview/next.d.mts +99 -0
- package/dist/preview/next.mjs +31 -0
- package/dist/preview/next.mjs.map +1 -0
- package/dist/preview/svelte.d.mts +99 -0
- package/dist/preview/svelte.mjs +437 -0
- package/dist/preview/svelte.mjs.map +1 -0
- package/dist/preview/vue.d.mts +113 -0
- package/dist/preview/vue.mjs +587 -0
- package/dist/preview/vue.mjs.map +1 -0
- package/dist/types/index.d.mts +358 -0
- package/dist/types/index.mjs +8 -0
- package/dist/types/index.mjs.map +1 -0
- package/dist/types-BNHDdA2G.d.mts +117 -0
- package/package.json +90 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/preview/client.ts","../../src/preview/dom.ts"],"sourcesContent":["/**\n * Preview Client\n *\n * Core client for handling live preview PostMessage communication.\n * This runs on the preview site (your frontend) inside an iframe.\n */\n\nimport type {\n PreviewConfig,\n PreviewState,\n PreviewMessage,\n PreviewUpdateMessage,\n PreviewInitMessage,\n PreviewReadyMessage,\n PreviewFocusMessage,\n} from './types'\n\n// =============================================================================\n// Constants\n// =============================================================================\n\nconst DEFAULT_ALLOWED_ORIGINS = [\n 'https://app.typecms.com',\n 'https://staging.typecms.com',\n 'http://localhost:3000',\n 'http://localhost:3001',\n]\n\n// =============================================================================\n// Preview Client\n// =============================================================================\n\nexport type PreviewListener = (state: PreviewState) => void\n\n/**\n * Create a preview client instance.\n *\n * This handles PostMessage communication between the Type CMS editor\n * and your preview site. The client runs in the preview iframe.\n *\n * @example\n * ```typescript\n * import { createPreviewClient } from '@typecms/sdk/preview'\n *\n * const preview = createPreviewClient({\n * debug: true,\n * onUpdate: (data) => {\n * console.log('Value changed:', data.path, data.value)\n * },\n * })\n *\n * // Start listening for messages\n * preview.connect()\n *\n * // Get current preview state\n * const state = preview.getState()\n *\n * // Cleanup when done\n * preview.disconnect()\n * ```\n */\nexport function createPreviewClient(config: PreviewConfig = {}) {\n const {\n allowedOrigins = DEFAULT_ALLOWED_ORIGINS,\n onInit,\n onUpdate,\n onNavigate,\n onFocus,\n debug = false,\n } = config\n\n // Internal state\n let state: PreviewState = {\n enabled: false,\n entryId: null,\n projectId: null,\n locale: null,\n typeId: null,\n values: {},\n focusedPath: null,\n }\n\n const listeners = new Set<PreviewListener>()\n let isConnected = false\n let parentOrigin: string | null = null\n\n // ==========================================================================\n // Internal Helpers\n // ==========================================================================\n\n function log(...args: unknown[]) {\n if (debug) {\n console.log('[TypeCMS Preview]', ...args)\n }\n }\n\n function setState(updates: Partial<PreviewState>) {\n state = { ...state, ...updates }\n listeners.forEach((listener) => listener(state))\n }\n\n function isAllowedOrigin(origin: string): boolean {\n return allowedOrigins.some((allowed) => {\n if (allowed === '*') return true\n // A trailing \"/*\" historically read as \"this origin\". Origins have no\n // path, so a startsWith() prefix match is unsafe — \"https://app.x.com/*\"\n // would also match the hostile origin \"https://app.x.com.evil.com\".\n // Treat \"/*\" as the exact origin (strip it) and always compare for\n // equality.\n const expected = allowed.endsWith('/*') ? allowed.slice(0, -2) : allowed\n return origin === expected\n })\n }\n\n function sendMessage(message: PreviewMessage) {\n if (!parentOrigin) {\n log('Cannot send message: no parent origin')\n return\n }\n\n if (typeof window === 'undefined' || !window.parent) {\n log('Cannot send message: not in iframe')\n return\n }\n\n log('Sending message:', message.type)\n window.parent.postMessage(message, parentOrigin)\n }\n\n // ==========================================================================\n // Message Handler\n // ==========================================================================\n\n function handleMessage(event: MessageEvent) {\n // Validate origin\n if (!isAllowedOrigin(event.origin)) {\n log('Ignored message from untrusted origin:', event.origin)\n return\n }\n\n // Validate message structure\n const message = event.data as PreviewMessage\n if (!message?.type?.startsWith('typecms:preview:')) {\n return\n }\n\n log('Received message:', message.type, message)\n parentOrigin = event.origin\n\n switch (message.type) {\n case 'typecms:preview:init': {\n const initMsg = message as PreviewInitMessage\n setState({\n enabled: true,\n entryId: initMsg.payload.entryId,\n projectId: initMsg.payload.projectId,\n locale: initMsg.payload.locale,\n typeId: initMsg.payload.typeId,\n values: {},\n focusedPath: null,\n })\n onInit?.(initMsg.payload)\n\n // Send ready response\n const readyMessage: PreviewReadyMessage = {\n type: 'typecms:preview:ready',\n timestamp: Date.now(),\n payload: {\n pathname: typeof window !== 'undefined' ? window.location.pathname : '/',\n },\n }\n sendMessage(readyMessage)\n break\n }\n\n case 'typecms:preview:update': {\n const updateMsg = message as PreviewUpdateMessage\n\n // If full values provided, replace entirely\n if (updateMsg.payload.values) {\n setState({ values: updateMsg.payload.values })\n } else {\n // Otherwise, merge the single path update\n const newValues = { ...state.values }\n setNestedValue(newValues, updateMsg.payload.path, updateMsg.payload.value)\n setState({ values: newValues })\n }\n\n onUpdate?.(updateMsg.payload)\n break\n }\n\n case 'typecms:preview:navigate': {\n onNavigate?.(message.payload.url)\n break\n }\n\n case 'typecms:preview:focus': {\n setState({ focusedPath: message.payload.path })\n onFocus?.(message.payload.path)\n break\n }\n }\n }\n\n // ==========================================================================\n // Public API\n // ==========================================================================\n\n return {\n /**\n * Start listening for preview messages.\n * Call this on mount.\n */\n connect() {\n if (typeof window === 'undefined') {\n log('Cannot connect: not in browser')\n return\n }\n\n if (isConnected) {\n log('Already connected')\n return\n }\n\n log('Connecting...')\n window.addEventListener('message', handleMessage)\n isConnected = true\n\n // Announce readiness to the embedding editor. The editor waits for a\n // `ready` before sending `init`, and we don't yet know its origin\n // (parentOrigin is only learned from an incoming message) — so without\n // this hello the handshake would deadlock. The payload is just our\n // pathname (non-sensitive), so it is safe to offer it to every\n // configured candidate origin; the browser delivers it only to the one\n // that actually matches the parent.\n if (window.parent && window.parent !== (window as unknown as Window)) {\n const hello: PreviewReadyMessage = {\n type: 'typecms:preview:ready',\n timestamp: Date.now(),\n payload: { pathname: window.location?.pathname ?? '/' },\n }\n for (const allowed of allowedOrigins) {\n const target = allowed === '*'\n ? '*'\n : allowed.endsWith('/*') ? allowed.slice(0, -2) : allowed\n try {\n window.parent.postMessage(hello, target)\n } catch {\n // Invalid target origin string — skip this candidate.\n }\n }\n }\n },\n\n /**\n * Stop listening for preview messages.\n * Call this on unmount.\n */\n disconnect() {\n if (typeof window === 'undefined') return\n\n log('Disconnecting...')\n window.removeEventListener('message', handleMessage)\n isConnected = false\n parentOrigin = null\n setState({\n enabled: false,\n entryId: null,\n projectId: null,\n locale: null,\n typeId: null,\n values: {},\n focusedPath: null,\n })\n },\n\n /**\n * Get current preview state.\n */\n getState(): PreviewState {\n return state\n },\n\n /**\n * Check if preview mode is active.\n */\n isEnabled(): boolean {\n return state.enabled\n },\n\n /**\n * Get a preview value by path.\n * Returns undefined if no preview value exists.\n */\n getValue<T = unknown>(path: string): T | undefined {\n return getNestedValue(state.values, path) as T | undefined\n },\n\n /**\n * Subscribe to state changes.\n */\n subscribe(listener: PreviewListener): () => void {\n listeners.add(listener)\n return () => listeners.delete(listener)\n },\n\n /**\n * Notify the editor that the preview has navigated.\n */\n notifyNavigation(pathname: string) {\n const message: PreviewReadyMessage = {\n type: 'typecms:preview:ready',\n timestamp: Date.now(),\n payload: { pathname },\n }\n sendMessage(message)\n },\n\n /**\n * Notify the editor that a field was clicked in the preview.\n * The editor will scroll to the corresponding field.\n *\n * @example\n * ```typescript\n * // On click of an element with data-preview-field attribute:\n * const path = el.closest('[data-preview-field]')?.dataset.previewField\n * if (path) preview.notifyFieldFocus(path)\n * ```\n */\n notifyFieldFocus(path: string) {\n const message: PreviewFocusMessage = {\n type: 'typecms:preview:focus',\n timestamp: Date.now(),\n payload: { path },\n }\n sendMessage(message)\n },\n }\n}\n\n// =============================================================================\n// Utility Functions\n// =============================================================================\n\n/**\n * Set a nested value by path (e.g., \"content.title\")\n */\nconst UNSAFE_KEYS = new Set(['__proto__', 'constructor', 'prototype'])\n\nfunction setNestedValue(obj: Record<string, unknown>, path: string, value: unknown): void {\n const parts = path.split('.')\n let current: Record<string, unknown> = obj\n\n for (let i = 0; i < parts.length - 1; i++) {\n const part = parts[i]\n // Handle array notation like \"items[0]\"\n const arrayMatch = part.match(/^(\\w+)\\[(\\d+)\\]$/)\n\n if (arrayMatch) {\n const [, name, indexStr] = arrayMatch\n const index = parseInt(indexStr, 10)\n if (UNSAFE_KEYS.has(name)) continue\n if (!current[name]) {\n current[name] = []\n }\n const arr = current[name] as unknown[]\n if (!arr[index]) {\n arr[index] = {}\n }\n current = arr[index] as Record<string, unknown>\n } else {\n if (UNSAFE_KEYS.has(part)) continue\n if (!current[part] || typeof current[part] !== 'object') {\n current[part] = {}\n }\n current = current[part] as Record<string, unknown>\n }\n }\n\n const lastPart = parts[parts.length - 1]\n const arrayMatch = lastPart.match(/^(\\w+)\\[(\\d+)\\]$/)\n\n if (arrayMatch) {\n const [, name, indexStr] = arrayMatch\n const index = parseInt(indexStr, 10)\n if (UNSAFE_KEYS.has(name)) return\n if (!current[name]) {\n current[name] = []\n }\n (current[name] as unknown[])[index] = value\n } else {\n if (UNSAFE_KEYS.has(lastPart)) return\n current[lastPart] = value\n }\n}\n\n/**\n * Get a nested value by path (supports \"items[0].name\" array notation)\n */\nexport function getNestedValue(obj: Record<string, unknown>, path: string): unknown {\n const parts = path.split('.')\n let current: unknown = obj\n\n for (const part of parts) {\n if (current === null || current === undefined) {\n return undefined\n }\n\n // Handle array notation like \"items[0]\"\n const arrayMatch = part.match(/^(\\w+)\\[(\\d+)\\]$/)\n\n if (arrayMatch) {\n const [, name, indexStr] = arrayMatch\n const index = parseInt(indexStr, 10)\n const arr = (current as Record<string, unknown>)[name] as unknown[] | undefined\n if (!arr) return undefined\n current = arr[index]\n } else {\n current = (current as Record<string, unknown>)[part]\n }\n }\n\n return current\n}\n\n/**\n * Deep merge two objects\n */\nexport function deepMerge<T extends Record<string, unknown>>(\n target: T,\n source: Record<string, unknown>\n): T {\n const result = { ...target }\n\n for (const key of Object.keys(source)) {\n const sourceValue = source[key]\n const targetValue = result[key as keyof T]\n\n if (\n sourceValue &&\n typeof sourceValue === 'object' &&\n !Array.isArray(sourceValue) &&\n targetValue &&\n typeof targetValue === 'object' &&\n !Array.isArray(targetValue)\n ) {\n result[key as keyof T] = deepMerge(\n targetValue as Record<string, unknown>,\n sourceValue as Record<string, unknown>\n ) as T[keyof T]\n } else {\n result[key as keyof T] = sourceValue as T[keyof T]\n }\n }\n\n return result\n}\n","/**\n * Preview DOM Binding\n *\n * Vanilla DOM binding for live preview — no framework required.\n * Elements annotated with `data-preview-field=\"path\"` are kept in sync\n * with the values streamed from the Type CMS editor. Ideal for Astro\n * and plain-HTML sites where a React/Vue layer isn't available.\n */\n\nimport { createPreviewClient, getNestedValue } from './client'\nimport type { PreviewConfig, PreviewState } from './types'\n\n// =============================================================================\n// Types\n// =============================================================================\n\nexport interface PreviewDomBindingOptions {\n /** Root node to search for bound elements (defaults to `document`) */\n root?: ParentNode\n /** Attribute carrying the field path (defaults to `data-preview-field`) */\n attribute?: string\n /**\n * Called for non-primitive values (objects, arrays, booleans, null).\n * Return a string to set as the element's textContent, or null/undefined\n * to skip the element. Without this callback, non-primitive values are\n * skipped entirely.\n */\n renderValue?: (el: Element, path: string, value: unknown) => string | null | undefined\n}\n\ninterface DomClient {\n subscribe(listener: (state: PreviewState) => void): () => void\n getState(): PreviewState\n}\n\nconst DEFAULT_ATTRIBUTE = 'data-preview-field'\n\n// =============================================================================\n// DOM Binding\n// =============================================================================\n\n/**\n * Bind preview values to the DOM.\n *\n * Subscribes to the preview client's state; whenever values change while\n * preview is enabled, every `[data-preview-field]` element whose path\n * resolves to a value (array notation like `items[0].name` is supported)\n * has its textContent updated. Each element's original textContent is\n * remembered the first time it is overwritten and restored on detach.\n *\n * SECURITY: values come from the editor via postMessage and are treated as\n * untrusted content. This binding only ever writes `textContent` — never\n * `innerHTML` — so preview values cannot inject markup or scripts (XSS).\n *\n * @returns A detach function that unsubscribes and restores originals.\n *\n * @example\n * ```typescript\n * const preview = createPreviewClient()\n * preview.connect()\n * const detach = bindPreviewToDom(preview)\n * // ...on teardown\n * detach()\n * ```\n */\nexport function bindPreviewToDom(\n client: DomClient,\n options: PreviewDomBindingOptions = {},\n): () => void {\n // SSR guard: nothing to bind outside the browser\n if (typeof document === 'undefined') {\n return () => {}\n }\n\n const attribute = options.attribute ?? DEFAULT_ATTRIBUTE\n const renderValue = options.renderValue\n\n // Original textContent per element, captured before the first overwrite\n const originals = new Map<Element, string | null>()\n\n function update(state: PreviewState) {\n if (!state.enabled) return\n\n const root: ParentNode = options.root ?? document\n const elements = root.querySelectorAll(`[${attribute}]`)\n\n elements.forEach((el) => {\n const path = el.getAttribute(attribute)\n if (!path) return\n\n const value = getNestedValue(state.values, path)\n if (value === undefined) return\n\n // Resolve the text to apply. Primitives render directly; everything\n // else is delegated to the site via renderValue (or skipped).\n let text: string | null = null\n if (typeof value === 'string') {\n text = value\n } else if (typeof value === 'number') {\n text = String(value)\n } else if (renderValue) {\n const result = renderValue(el, path, value)\n if (typeof result === 'string') {\n text = result\n }\n }\n if (text === null) return\n\n // Only write when changed, to avoid DOM churn (and layout/observer noise)\n if (el.textContent === text) return\n\n if (!originals.has(el)) {\n originals.set(el, el.textContent)\n }\n // textContent only — never innerHTML (untrusted content, XSS)\n el.textContent = text\n })\n }\n\n const unsubscribe = client.subscribe(update)\n update(client.getState())\n\n return () => {\n unsubscribe()\n originals.forEach((text, el) => {\n el.textContent = text\n })\n originals.clear()\n }\n}\n\n// =============================================================================\n// Convenience Entry Point\n// =============================================================================\n\n/**\n * One-call live preview for vanilla DOM sites.\n *\n * Creates a preview client, connects it, and binds it to the DOM.\n * Returns the client (for advanced use like `notifyFieldFocus`) and a\n * `destroy` function that unbinds and disconnects.\n *\n * @example Astro — add to a shared layout:\n * ```astro\n * <script>\n * import { initDomPreview } from '@typecms/sdk/preview/dom'\n * initDomPreview()\n * </script>\n * ```\n * Then annotate markup: `<h1 data-preview-field=\"title\">{entry.title}</h1>`\n */\nexport function initDomPreview(options: PreviewConfig & PreviewDomBindingOptions = {}) {\n const { root, attribute, renderValue, ...config } = options\n\n const client = createPreviewClient(config)\n client.connect()\n\n const unbind = bindPreviewToDom(client, { root, attribute, renderValue })\n\n return {\n client,\n destroy() {\n unbind()\n client.disconnect()\n },\n }\n}\n"],"mappings":"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","names":["arrayMatch"]}
|
|
@@ -0,0 +1,438 @@
|
|
|
1
|
+
export { P as PreviewListener, c as createPreviewClient, d as deepMerge, g as getNestedValue } from '../client-BW6BWpfF.mjs';
|
|
2
|
+
import { P as PreviewOverlayOptions } from '../overlay-CLYAeNwg.mjs';
|
|
3
|
+
export { a as attachPreviewOverlay } from '../overlay-CLYAeNwg.mjs';
|
|
4
|
+
import { P as PreviewState, a as PreviewConfig } from '../types-BNHDdA2G.mjs';
|
|
5
|
+
export { b as PreviewFocusMessage, c as PreviewInitMessage, d as PreviewMessage, e as PreviewMessageBase, f as PreviewMessageType, g as PreviewNavigateMessage, h as PreviewReadyMessage, i as PreviewUpdateMessage } from '../types-BNHDdA2G.mjs';
|
|
6
|
+
import * as react from 'react';
|
|
7
|
+
import { ReactNode } from 'react';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Preview Content Source Mapping (v1)
|
|
11
|
+
*
|
|
12
|
+
* Automatically tags preview-site DOM elements with `data-preview-field`
|
|
13
|
+
* attributes by matching element text against live preview field values,
|
|
14
|
+
* so click-to-edit and focus overlays work without the site hand-tagging
|
|
15
|
+
* every component.
|
|
16
|
+
*
|
|
17
|
+
* This is a deliberate alternative to stega-style source maps: no invisible
|
|
18
|
+
* unicode is encoded into API responses. Instead, the DOM is scanned and
|
|
19
|
+
* matched against the value map by plain string equality.
|
|
20
|
+
*
|
|
21
|
+
* HONEST LIMITS — this is heuristic value-matching:
|
|
22
|
+
* - Only exact, unique string values of at least `minLength` characters are
|
|
23
|
+
* matched. Values appearing under more than one field path are skipped
|
|
24
|
+
* (they cannot be attributed safely), as are short values like "OK" or
|
|
25
|
+
* "Go" that would tag buttons everywhere.
|
|
26
|
+
* - Only an element's own direct text is compared — never text inside nested
|
|
27
|
+
* elements — so the innermost rendering element is what gets tagged and
|
|
28
|
+
* wrapper containers never are.
|
|
29
|
+
* - Values the site transforms before rendering (uppercased, truncated,
|
|
30
|
+
* interpolated, markdown-rendered) will not match.
|
|
31
|
+
* - Framework re-renders are survived: when a tagged element is replaced,
|
|
32
|
+
* the next pass (value update or DOM mutation) notices the remembered
|
|
33
|
+
* element is disconnected and re-tags the replacement.
|
|
34
|
+
*
|
|
35
|
+
* Manual `data-preview-field` attributes remain the precise option and
|
|
36
|
+
* always win: auto-tagging never overwrites an existing attribute, and
|
|
37
|
+
* detach removes only the attributes it added.
|
|
38
|
+
*
|
|
39
|
+
* @example
|
|
40
|
+
* ```typescript
|
|
41
|
+
* import { createPreviewClient } from '@typecms/sdk/preview'
|
|
42
|
+
* import { autoTagPreviewFields } from '@typecms/sdk/preview'
|
|
43
|
+
*
|
|
44
|
+
* const preview = createPreviewClient()
|
|
45
|
+
* preview.connect()
|
|
46
|
+
*
|
|
47
|
+
* const detach = autoTagPreviewFields(preview)
|
|
48
|
+
* // Elements whose text equals a field value now carry
|
|
49
|
+
* // data-preview-field="path" — click-to-edit and overlays just work.
|
|
50
|
+
*
|
|
51
|
+
* // ...on teardown
|
|
52
|
+
* detach()
|
|
53
|
+
* ```
|
|
54
|
+
*/
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Minimal structural view of a DOM node — everything the tagger touches.
|
|
58
|
+
* Real `Element`/`Document` objects satisfy this, and tests can provide
|
|
59
|
+
* lightweight fakes without a DOM implementation.
|
|
60
|
+
*/
|
|
61
|
+
interface AutoTagNode {
|
|
62
|
+
nodeType: number;
|
|
63
|
+
textContent?: string | null;
|
|
64
|
+
childNodes?: ArrayLike<AutoTagNode>;
|
|
65
|
+
/**
|
|
66
|
+
* Whether the node is still attached to the document. Real DOM elements
|
|
67
|
+
* provide this; when absent the node is assumed connected. Used to detect
|
|
68
|
+
* framework re-renders that replaced a tagged element.
|
|
69
|
+
*/
|
|
70
|
+
isConnected?: boolean;
|
|
71
|
+
getAttribute?(name: string): string | null;
|
|
72
|
+
setAttribute?(name: string, value: string): void;
|
|
73
|
+
removeAttribute?(name: string): void;
|
|
74
|
+
}
|
|
75
|
+
interface AutoTagOptions {
|
|
76
|
+
/** Root node to scan for candidate elements (defaults to `document.body`) */
|
|
77
|
+
root?: AutoTagNode;
|
|
78
|
+
/** Attribute to write the field path into (defaults to `data-preview-field`) */
|
|
79
|
+
attribute?: string;
|
|
80
|
+
/**
|
|
81
|
+
* Minimum (trimmed) string value length to consider for matching
|
|
82
|
+
* (defaults to 3). Shorter values like "OK" would tag unrelated UI.
|
|
83
|
+
*/
|
|
84
|
+
minLength?: number;
|
|
85
|
+
/**
|
|
86
|
+
* Re-run the matching pass when new nodes are added under `root`
|
|
87
|
+
* (client-side navigation, lazy rendering). Defaults to true; ignored
|
|
88
|
+
* in environments without `MutationObserver`.
|
|
89
|
+
*/
|
|
90
|
+
observe?: boolean;
|
|
91
|
+
}
|
|
92
|
+
interface SourceMapClient {
|
|
93
|
+
subscribe(listener: (state: PreviewState) => void): () => void;
|
|
94
|
+
getState(): PreviewState;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Automatically tag DOM elements with preview field paths.
|
|
98
|
+
*
|
|
99
|
+
* Subscribes to the preview client; whenever preview is enabled and values
|
|
100
|
+
* are present (the editor's init sends a full values sync), string leaf
|
|
101
|
+
* values are matched against element text and matching elements receive
|
|
102
|
+
* `data-preview-field="<path>"`. The pass re-runs on value updates (new
|
|
103
|
+
* fields may have arrived) and — when `observe` is on — when new nodes are
|
|
104
|
+
* added under `root`. Re-runs are cheap: disabled states bail immediately
|
|
105
|
+
* and already-tagged paths are skipped.
|
|
106
|
+
*
|
|
107
|
+
* @returns A detach function that unsubscribes, stops observing, and removes
|
|
108
|
+
* only the attributes this tagger added (manual tags are untouched).
|
|
109
|
+
*/
|
|
110
|
+
declare function autoTagPreviewFields(client: SourceMapClient, options?: AutoTagOptions): () => void;
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Rich-Text Live Preview Helpers
|
|
114
|
+
*
|
|
115
|
+
* Rich-text fields ('richtextv2' → TipTap, 'richtext' → Slate) store large
|
|
116
|
+
* JSON documents. During live preview every keystroke sends the WHOLE new
|
|
117
|
+
* document, and naive consumers re-render the entire rich-text tree — losing
|
|
118
|
+
* performance and, in editable embeds, cursor state.
|
|
119
|
+
*
|
|
120
|
+
* These helpers apply STRUCTURAL SHARING: when a new document arrives, any
|
|
121
|
+
* subtree that is deep-equal to the previous document's subtree at the same
|
|
122
|
+
* position is returned as the SAME reference from the previous document.
|
|
123
|
+
* React/Vue memoization and keyed renderers then only re-render the nodes
|
|
124
|
+
* that actually changed.
|
|
125
|
+
*
|
|
126
|
+
* This module is framework-agnostic and dependency-free. Documents are
|
|
127
|
+
* assumed to be plain JSON data (no cycles, no functions, no Dates).
|
|
128
|
+
*
|
|
129
|
+
* @example
|
|
130
|
+
* ```typescript
|
|
131
|
+
* import { createRichTextPreview } from '@typecms/sdk/preview'
|
|
132
|
+
*
|
|
133
|
+
* const preview = createRichTextPreview()
|
|
134
|
+
* const docA = preview.apply(incomingDocA)
|
|
135
|
+
* const docB = preview.apply(incomingDocB)
|
|
136
|
+
* // Unchanged paragraphs in docB are the SAME object references as in docA.
|
|
137
|
+
* ```
|
|
138
|
+
*/
|
|
139
|
+
/**
|
|
140
|
+
* A TipTap/ProseMirror node. Kept permissive on purpose — the editor may
|
|
141
|
+
* attach arbitrary attrs/marks and custom node types.
|
|
142
|
+
*/
|
|
143
|
+
interface TipTapNode {
|
|
144
|
+
type: string;
|
|
145
|
+
content?: TipTapNode[];
|
|
146
|
+
text?: string;
|
|
147
|
+
marks?: Array<{
|
|
148
|
+
type: string;
|
|
149
|
+
attrs?: Record<string, unknown>;
|
|
150
|
+
}>;
|
|
151
|
+
attrs?: Record<string, unknown>;
|
|
152
|
+
[key: string]: unknown;
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* A TipTap document: `{ type: 'doc', content: [...] }`.
|
|
156
|
+
* Produced by 'richtextv2' fields.
|
|
157
|
+
*/
|
|
158
|
+
interface TipTapDoc extends TipTapNode {
|
|
159
|
+
type: 'doc';
|
|
160
|
+
content: TipTapNode[];
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* A Slate node. Element nodes carry `children`; text leaves carry `text`
|
|
164
|
+
* (plus arbitrary mark properties like `bold`). Kept permissive on purpose.
|
|
165
|
+
*/
|
|
166
|
+
interface SlateNode {
|
|
167
|
+
type?: string;
|
|
168
|
+
children?: SlateNode[];
|
|
169
|
+
text?: string;
|
|
170
|
+
[key: string]: unknown;
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Check whether a value looks like a TipTap document
|
|
174
|
+
* (`{ type: 'doc', content: [...] }`, produced by 'richtextv2' fields).
|
|
175
|
+
*
|
|
176
|
+
* Permissive: only the root shape is inspected, children are not validated.
|
|
177
|
+
* Use it to decide which renderer to hand a preview value to.
|
|
178
|
+
*/
|
|
179
|
+
declare function isTipTapDoc(value: unknown): value is TipTapDoc;
|
|
180
|
+
/**
|
|
181
|
+
* Check whether a value looks like a Slate document — an array of nodes
|
|
182
|
+
* where each node has `children` or is a text leaf with `text`
|
|
183
|
+
* (produced by 'richtext' fields).
|
|
184
|
+
*
|
|
185
|
+
* Permissive: only one level is inspected, grandchildren are not validated.
|
|
186
|
+
* An empty array is treated as a valid (empty) Slate fragment.
|
|
187
|
+
*/
|
|
188
|
+
declare function isSlateDoc(value: unknown): value is SlateNode[];
|
|
189
|
+
/**
|
|
190
|
+
* Structurally share `next` against `prev`.
|
|
191
|
+
*
|
|
192
|
+
* Returns `prev` itself when the two are deep-equal. Otherwise returns a copy
|
|
193
|
+
* of `next` in which every child object/array is replaced by the structurally
|
|
194
|
+
* shared version against the corresponding `prev` child.
|
|
195
|
+
*
|
|
196
|
+
* Guarantee: any subtree of the result that is deep-equal to the previous
|
|
197
|
+
* document's subtree at the same position is the SAME reference from `prev`,
|
|
198
|
+
* so memoized/keyed renderers skip it.
|
|
199
|
+
*
|
|
200
|
+
* Arrays are aligned from the tail first, then by index from the head, so a
|
|
201
|
+
* mid-array insert or delete (the common rich-text edit) still preserves
|
|
202
|
+
* reference equality for the deep-equal siblings around the edit point.
|
|
203
|
+
*
|
|
204
|
+
* Handles arrays, plain objects, primitives, `null`, and mismatched types
|
|
205
|
+
* (mismatches simply return `next`). Input is assumed to be acyclic JSON.
|
|
206
|
+
*/
|
|
207
|
+
declare function shareStructure<T>(prev: T | undefined, next: T): T;
|
|
208
|
+
interface RichTextPreviewOptions {
|
|
209
|
+
/**
|
|
210
|
+
* Seed the "previous document" memory, e.g. with the published document
|
|
211
|
+
* already rendered on the page, so the very first preview update can
|
|
212
|
+
* share unchanged subtrees with it.
|
|
213
|
+
*/
|
|
214
|
+
initial?: unknown;
|
|
215
|
+
}
|
|
216
|
+
interface RichTextPreview {
|
|
217
|
+
/**
|
|
218
|
+
* Structurally share `nextDoc` against the last applied document and
|
|
219
|
+
* remember the result for the next call.
|
|
220
|
+
*/
|
|
221
|
+
apply<T>(nextDoc: T): T;
|
|
222
|
+
/** Forget the last document. The next apply() returns its input as-is. */
|
|
223
|
+
reset(): void;
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* Create a small stateful structural-sharing instance for a single
|
|
227
|
+
* rich-text field. Useful outside React/Vue (vanilla JS, Svelte stores,
|
|
228
|
+
* custom render loops).
|
|
229
|
+
*
|
|
230
|
+
* @example
|
|
231
|
+
* ```typescript
|
|
232
|
+
* const preview = createRichTextPreview()
|
|
233
|
+
*
|
|
234
|
+
* client.subscribe((state) => {
|
|
235
|
+
* const doc = preview.apply(state.values.body)
|
|
236
|
+
* render(doc) // unchanged nodes keep their references
|
|
237
|
+
* })
|
|
238
|
+
* ```
|
|
239
|
+
*/
|
|
240
|
+
declare function createRichTextPreview(options?: RichTextPreviewOptions): RichTextPreview;
|
|
241
|
+
|
|
242
|
+
interface PreviewContextValue {
|
|
243
|
+
state: PreviewState;
|
|
244
|
+
/** Check if running in preview mode */
|
|
245
|
+
isPreview: boolean;
|
|
246
|
+
/** Get preview value for a field path */
|
|
247
|
+
getValue: <T>(path: string) => T | undefined;
|
|
248
|
+
/** Merge published content with preview overrides */
|
|
249
|
+
mergeContent: <T extends Record<string, unknown>>(content: T) => T;
|
|
250
|
+
/** Notify editor of navigation */
|
|
251
|
+
notifyNavigation: (pathname: string) => void;
|
|
252
|
+
/** Notify editor that a field was clicked in the preview */
|
|
253
|
+
notifyFieldFocus: (path: string) => void;
|
|
254
|
+
}
|
|
255
|
+
interface PreviewProviderProps extends PreviewConfig {
|
|
256
|
+
children: ReactNode;
|
|
257
|
+
/**
|
|
258
|
+
* Draw an automatic highlight box over the `[data-preview-field]` element
|
|
259
|
+
* matching the field focused in the editor. Pass options to customize.
|
|
260
|
+
*/
|
|
261
|
+
overlay?: boolean | PreviewOverlayOptions;
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* Provider component for Type CMS live preview.
|
|
265
|
+
*
|
|
266
|
+
* Wrap your app (or specific pages) with this provider to enable
|
|
267
|
+
* real-time preview updates from the Type CMS editor.
|
|
268
|
+
*
|
|
269
|
+
* @example
|
|
270
|
+
* ```tsx
|
|
271
|
+
* // app/layout.tsx or pages/_app.tsx
|
|
272
|
+
* import { PreviewProvider } from '@typecms/sdk/preview'
|
|
273
|
+
*
|
|
274
|
+
* export default function RootLayout({ children }) {
|
|
275
|
+
* return (
|
|
276
|
+
* <PreviewProvider debug={process.env.NODE_ENV === 'development'}>
|
|
277
|
+
* {children}
|
|
278
|
+
* </PreviewProvider>
|
|
279
|
+
* )
|
|
280
|
+
* }
|
|
281
|
+
* ```
|
|
282
|
+
*/
|
|
283
|
+
declare function PreviewProvider({ children, overlay, ...config }: PreviewProviderProps): react.JSX.Element;
|
|
284
|
+
/**
|
|
285
|
+
* Access the preview context.
|
|
286
|
+
*
|
|
287
|
+
* @throws Error if used outside of PreviewProvider
|
|
288
|
+
*/
|
|
289
|
+
declare function usePreviewContext(): PreviewContextValue;
|
|
290
|
+
/**
|
|
291
|
+
* Check if running in preview mode.
|
|
292
|
+
*
|
|
293
|
+
* @example
|
|
294
|
+
* ```tsx
|
|
295
|
+
* function Page() {
|
|
296
|
+
* const isPreview = useIsPreview()
|
|
297
|
+
*
|
|
298
|
+
* return (
|
|
299
|
+
* <div>
|
|
300
|
+
* {isPreview && <div className="preview-badge">Preview Mode</div>}
|
|
301
|
+
* <Content />
|
|
302
|
+
* </div>
|
|
303
|
+
* )
|
|
304
|
+
* }
|
|
305
|
+
* ```
|
|
306
|
+
*/
|
|
307
|
+
declare function useIsPreview(): boolean;
|
|
308
|
+
/**
|
|
309
|
+
* Get current preview state.
|
|
310
|
+
*/
|
|
311
|
+
declare function usePreviewState(): PreviewState | null;
|
|
312
|
+
/**
|
|
313
|
+
* Get a preview value by field path, with fallback to published value.
|
|
314
|
+
*
|
|
315
|
+
* @example
|
|
316
|
+
* ```tsx
|
|
317
|
+
* function Title({ entry }) {
|
|
318
|
+
* const title = usePreviewValue('title', entry.title)
|
|
319
|
+
* return <h1>{title}</h1>
|
|
320
|
+
* }
|
|
321
|
+
* ```
|
|
322
|
+
*/
|
|
323
|
+
declare function usePreviewValue<T>(path: string, fallback: T): T;
|
|
324
|
+
/**
|
|
325
|
+
* Merge published content with preview overrides.
|
|
326
|
+
*
|
|
327
|
+
* This is the primary hook for integrating preview data.
|
|
328
|
+
* Pass your fetched/published content and get back a merged
|
|
329
|
+
* version that includes any live preview changes.
|
|
330
|
+
*
|
|
331
|
+
* @example
|
|
332
|
+
* ```tsx
|
|
333
|
+
* // In your page component
|
|
334
|
+
* function BlogPost({ post }) {
|
|
335
|
+
* // post is fetched from the API (published content)
|
|
336
|
+
* const content = usePreviewContent(post)
|
|
337
|
+
*
|
|
338
|
+
* // content now includes any live preview changes
|
|
339
|
+
* return (
|
|
340
|
+
* <article>
|
|
341
|
+
* <h1>{content.title}</h1>
|
|
342
|
+
* <div>{content.body}</div>
|
|
343
|
+
* </article>
|
|
344
|
+
* )
|
|
345
|
+
* }
|
|
346
|
+
* ```
|
|
347
|
+
*/
|
|
348
|
+
declare function usePreviewContent<T extends Record<string, unknown>>(content: T): T;
|
|
349
|
+
/**
|
|
350
|
+
* Get the currently focused field path (if any).
|
|
351
|
+
*
|
|
352
|
+
* Use this to highlight fields in your preview when they're
|
|
353
|
+
* being edited in the Type CMS editor.
|
|
354
|
+
*
|
|
355
|
+
* @example
|
|
356
|
+
* ```tsx
|
|
357
|
+
* function Field({ path, children }) {
|
|
358
|
+
* const focusedPath = useFocusedField()
|
|
359
|
+
* const isFocused = focusedPath === path
|
|
360
|
+
*
|
|
361
|
+
* return (
|
|
362
|
+
* <div className={isFocused ? 'ring-2 ring-blue-500' : ''}>
|
|
363
|
+
* {children}
|
|
364
|
+
* </div>
|
|
365
|
+
* )
|
|
366
|
+
* }
|
|
367
|
+
* ```
|
|
368
|
+
*/
|
|
369
|
+
declare function useFocusedField(): string | null;
|
|
370
|
+
/**
|
|
371
|
+
* Hook that provides a function to notify the editor of navigation.
|
|
372
|
+
*
|
|
373
|
+
* Call this when your app navigates to a new page so the editor
|
|
374
|
+
* can update its state accordingly.
|
|
375
|
+
*
|
|
376
|
+
* @example
|
|
377
|
+
* ```tsx
|
|
378
|
+
* function Page() {
|
|
379
|
+
* const pathname = usePathname()
|
|
380
|
+
* const notifyNavigation = usePreviewNavigation()
|
|
381
|
+
*
|
|
382
|
+
* useEffect(() => {
|
|
383
|
+
* notifyNavigation(pathname)
|
|
384
|
+
* }, [pathname, notifyNavigation])
|
|
385
|
+
*
|
|
386
|
+
* return <Content />
|
|
387
|
+
* }
|
|
388
|
+
* ```
|
|
389
|
+
*/
|
|
390
|
+
declare function usePreviewNavigation(): (pathname: string) => void;
|
|
391
|
+
/**
|
|
392
|
+
* Hook that provides a function to notify the editor when a preview field is clicked.
|
|
393
|
+
*
|
|
394
|
+
* Add `data-preview-field="<path>"` to elements in your preview site. When a user
|
|
395
|
+
* clicks one, call this function with the path to highlight and scroll to the
|
|
396
|
+
* corresponding field in the Type CMS editor.
|
|
397
|
+
*
|
|
398
|
+
* @example
|
|
399
|
+
* ```tsx
|
|
400
|
+
* function PreviewField({ path, children }) {
|
|
401
|
+
* const notifyFieldFocus = usePreviewFieldFocus()
|
|
402
|
+
*
|
|
403
|
+
* return (
|
|
404
|
+
* <div
|
|
405
|
+
* data-preview-field={path}
|
|
406
|
+
* onClick={() => notifyFieldFocus(path)}
|
|
407
|
+
* >
|
|
408
|
+
* {children}
|
|
409
|
+
* </div>
|
|
410
|
+
* )
|
|
411
|
+
* }
|
|
412
|
+
* ```
|
|
413
|
+
*/
|
|
414
|
+
declare function usePreviewFieldFocus(): (path: string) => void;
|
|
415
|
+
/**
|
|
416
|
+
* Get a rich-text preview value by field path with STRUCTURAL SHARING.
|
|
417
|
+
*
|
|
418
|
+
* Like `usePreviewValue`, but pipes the document through a persistent
|
|
419
|
+
* structural-sharing instance: subtrees that are deep-equal to the previous
|
|
420
|
+
* update keep their object references, so memoized node renderers
|
|
421
|
+
* (`React.memo`, keyed lists) only re-render nodes that actually changed —
|
|
422
|
+
* instead of re-rendering the whole rich-text tree on every keystroke.
|
|
423
|
+
*
|
|
424
|
+
* Works for both TipTap ('richtextv2') and Slate ('richtext') documents —
|
|
425
|
+
* or any JSON value. Returns the fallback (unshared) when preview is
|
|
426
|
+
* disabled. Sharing memory resets when `path` changes.
|
|
427
|
+
*
|
|
428
|
+
* @example
|
|
429
|
+
* ```tsx
|
|
430
|
+
* function Body({ entry }) {
|
|
431
|
+
* const doc = usePreviewRichText('body', entry.body)
|
|
432
|
+
* return <RichTextRenderer doc={doc} />
|
|
433
|
+
* }
|
|
434
|
+
* ```
|
|
435
|
+
*/
|
|
436
|
+
declare function usePreviewRichText<T>(path: string, fallback: T): T;
|
|
437
|
+
|
|
438
|
+
export { type AutoTagOptions, PreviewConfig, PreviewOverlayOptions, PreviewProvider, PreviewState, type RichTextPreview, type RichTextPreviewOptions, autoTagPreviewFields, createRichTextPreview, isSlateDoc, isTipTapDoc, shareStructure, useFocusedField, useIsPreview, usePreviewContent, usePreviewContext, usePreviewFieldFocus, usePreviewNavigation, usePreviewRichText, usePreviewState, usePreviewValue };
|