@distinctagency/cms-client 1.26.0 → 1.27.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.
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/markup/react.tsx","../src/visual-editing/protocol.ts","../src/markup/protocol.ts","../src/markup/selector.ts","../src/markup/bridge.ts"],"sourcesContent":["\"use client\"\nimport { useEffect } from \"react\"\nimport { createMarkupBridge } from \"./bridge\"\n\nexport interface CmsMarkupBridgeProps {\n trustedOrigins?: string[]\n /** Query param that activates markup mode. Defaults to \"cms_markup\". */\n param?: string\n}\n\n/**\n * Mount once in your site (e.g. root layout, alongside CmsVisualBridge).\n * Renders nothing; activates only when loaded with the markup param inside a\n * trusted CMS frame.\n */\nexport function CmsMarkupBridge({ trustedOrigins, param = \"cms_markup\" }: CmsMarkupBridgeProps) {\n useEffect(() => {\n if (typeof window === \"undefined\") return\n const params = new URLSearchParams(window.location.search)\n if (!params.has(param)) return\n return createMarkupBridge({ trustedOrigins })\n }, [trustedOrigins, param])\n return null\n}\n","/** Discriminator stamped on every visual-editing postMessage. */\nexport const CMS_VISUAL_SOURCE = \"cms-visual\" as const\n\n/** CMS origins the bridge will trust by default. */\nexport const DEFAULT_TRUSTED_ORIGINS: readonly string[] = [\n \"https://cms.distinctstudio.co.nz\",\n \"https://distinctcms.com\",\n]\n\nexport type VisualFieldType = \"text\" | \"image\"\n\n/** Compact wire type announced in `ready`; the schema adds length constraints. */\nexport interface VisualFieldMeta {\n name: string\n type: VisualFieldType\n}\n\nexport interface VisualFieldSchema {\n name: string\n type: VisualFieldType\n max_length?: number\n recommended_length?: number\n}\n\n// bridge -> parent\nexport interface ReadyMessage {\n source: typeof CMS_VISUAL_SOURCE\n type: \"ready\"\n fields: VisualFieldMeta[]\n}\nexport interface EditMessage {\n source: typeof CMS_VISUAL_SOURCE\n type: \"edit\"\n name: string\n value: string\n length: number\n /** When the field is inside a repeating group item, the group field name. */\n list?: string\n /** Item index within the group (document order). */\n index?: number\n}\nexport interface PickImageMessage {\n source: typeof CMS_VISUAL_SOURCE\n type: \"pick-image\"\n name: string\n /** When the field is inside a repeating group item, the group field name. */\n list?: string\n /** Item index within the group (document order). */\n index?: number\n}\n\n// parent -> bridge\nexport interface InitMessage {\n source: typeof CMS_VISUAL_SOURCE\n type: \"init\"\n values: Record<string, string>\n schema: Record<string, VisualFieldSchema>\n}\nexport interface SetMessage {\n source: typeof CMS_VISUAL_SOURCE\n type: \"set\"\n name: string\n value: string\n /** When the field is inside a repeating group item, the group field name. */\n list?: string\n /** Item index within the group (document order). */\n index?: number\n}\n\nexport type BridgeOutbound = ReadyMessage | EditMessage | PickImageMessage\nexport type BridgeInbound = InitMessage | SetMessage\nexport type VisualMessage = BridgeOutbound | BridgeInbound\n\n/** Exact-match origin allowlist, plus any localhost port for local dev. */\nexport function isTrustedOrigin(\n origin: string,\n allowlist: readonly string[]\n): boolean {\n if (allowlist.includes(origin)) return true\n return /^https?:\\/\\/localhost(:\\d+)?$/.test(origin)\n}\n\nconst VALID_VISUAL_TYPES = new Set([\"ready\", \"edit\", \"pick-image\", \"init\", \"set\"])\n\n/** Narrow an unknown postMessage payload to a visual message, or null. */\nexport function parseVisualMessage(data: unknown): VisualMessage | null {\n if (\n typeof data === \"object\" &&\n data !== null &&\n (data as Record<string, unknown>).source === CMS_VISUAL_SOURCE &&\n typeof (data as Record<string, unknown>).type === \"string\" &&\n VALID_VISUAL_TYPES.has((data as Record<string, unknown>).type as string)\n ) {\n return data as VisualMessage\n }\n return null\n}\n","import { isTrustedOrigin } from \"../visual-editing/protocol\"\n\nexport { isTrustedOrigin }\nexport const CMS_MARKUP_SOURCE = \"cms-markup\" as const\n\nexport interface MarkupReady {\n source: typeof CMS_MARKUP_SOURCE\n type: \"markup-ready\"\n docW: number\n docH: number\n}\nexport interface MarkupPin {\n source: typeof CMS_MARKUP_SOURCE\n type: \"markup-pin\"\n xPct: number\n yPct: number\n relX: number\n relY: number\n selector: string | null\n viewportW: number\n docW: number\n docH: number\n}\nexport interface MarkupViewport {\n source: typeof CMS_MARKUP_SOURCE\n type: \"markup-viewport\"\n scrollX: number\n scrollY: number\n docW: number\n docH: number\n viewportW: number\n}\nexport interface MarkupLocated {\n source: typeof CMS_MARKUP_SOURCE\n type: \"markup-located\"\n id: string\n rect: { x: number; y: number; w: number; h: number } | null\n}\nexport interface MarkupInit {\n source: typeof CMS_MARKUP_SOURCE\n type: \"markup-init\"\n}\nexport interface MarkupLocate {\n source: typeof CMS_MARKUP_SOURCE\n type: \"markup-locate\"\n id: string\n selector: string\n}\n\nexport type MarkupOutbound = MarkupReady | MarkupPin | MarkupViewport | MarkupLocated\nexport type MarkupInbound = MarkupInit | MarkupLocate\nexport type MarkupMessage = MarkupOutbound | MarkupInbound\n\nconst VALID = new Set([\n \"markup-ready\", \"markup-pin\", \"markup-viewport\", \"markup-located\",\n \"markup-init\", \"markup-locate\",\n])\n\nexport function parseMarkupMessage(data: unknown): MarkupMessage | null {\n if (\n typeof data === \"object\" && data !== null &&\n (data as Record<string, unknown>).source === CMS_MARKUP_SOURCE &&\n typeof (data as Record<string, unknown>).type === \"string\" &&\n VALID.has((data as Record<string, unknown>).type as string)\n ) {\n return data as MarkupMessage\n }\n return null\n}\n","/** Build a stable-ish CSS selector for an element, for re-anchoring pins. */\nexport function computeSelector(el: Element): string | null {\n if (!el || !el.isConnected) return null\n if (el.id) return `#${CSS.escape(el.id)}`\n const field = el.getAttribute(\"data-cms-field\")\n if (field) return `[data-cms-field=\"${CSS.escape(field)}\"]`\n\n const parts: string[] = []\n let node: Element | null = el\n while (node && node.nodeType === 1 && node.tagName.toLowerCase() !== \"html\") {\n if (node.id) { parts.unshift(`#${CSS.escape(node.id)}`); break }\n const tag = node.tagName.toLowerCase()\n const parent: Element | null = node.parentElement\n if (!parent) { parts.unshift(tag); break }\n const sameType = Array.from(parent.children).filter((c) => c.tagName === node!.tagName)\n const idx = sameType.indexOf(node) + 1\n parts.unshift(sameType.length > 1 ? `${tag}:nth-of-type(${idx})` : tag)\n node = parent\n }\n return parts.join(\" > \")\n}\n","import { DEFAULT_TRUSTED_ORIGINS } from \"../visual-editing/protocol\"\nimport { CMS_MARKUP_SOURCE, isTrustedOrigin, parseMarkupMessage, type MarkupOutbound } from \"./protocol\"\nimport { computeSelector } from \"./selector\"\n\nexport interface MarkupBridgeOptions { trustedOrigins?: string[] }\n\n/**\n * In-page markup capture bridge. Dormant until a trusted CMS parent sends\n * `markup-init`. Captures clicks anywhere, reports pin coords + selector, and\n * answers `markup-locate` so the parent can re-anchor pins. Returns cleanup.\n */\nexport function createMarkupBridge(options: MarkupBridgeOptions = {}): () => void {\n if (typeof window === \"undefined\" || window.parent === window) return () => {}\n const allow = options.trustedOrigins ?? DEFAULT_TRUSTED_ORIGINS\n let parentOrigin: string | null = null\n let capturing = false\n\n const doc = () => ({\n docW: document.documentElement.scrollWidth,\n docH: document.documentElement.scrollHeight,\n })\n\n function post(msg: MarkupOutbound) {\n if (parentOrigin) window.parent.postMessage(msg, parentOrigin)\n }\n\n function onClick(e: MouseEvent) {\n if (!capturing || !parentOrigin) return\n const el = e.target as Element | null\n if (!el) return\n e.preventDefault()\n e.stopPropagation()\n const { docW, docH } = doc()\n const sx = window.scrollX, sy = window.scrollY\n const docX = e.clientX + sx, docY = e.clientY + sy\n const r = el.getBoundingClientRect()\n const rx = r.width > 0 ? (e.clientX - r.left) / r.width : 0\n const ry = r.height > 0 ? (e.clientY - r.top) / r.height : 0\n post({\n source: CMS_MARKUP_SOURCE, type: \"markup-pin\",\n xPct: docW > 0 ? docX / docW : 0,\n yPct: docH > 0 ? docY / docH : 0,\n relX: Math.min(1, Math.max(0, rx)),\n relY: Math.min(1, Math.max(0, ry)),\n selector: computeSelector(el),\n viewportW: window.innerWidth, docW, docH,\n })\n }\n\n function emitViewport() {\n if (!parentOrigin) return\n const { docW, docH } = doc()\n post({\n source: CMS_MARKUP_SOURCE, type: \"markup-viewport\",\n scrollX: window.scrollX, scrollY: window.scrollY,\n docW, docH, viewportW: window.innerWidth,\n })\n }\n\n function onMessage(e: MessageEvent) {\n if (!isTrustedOrigin(e.origin, allow)) return\n const msg = parseMarkupMessage(e.data)\n if (!msg) return\n if (msg.type === \"markup-init\") {\n parentOrigin = e.origin\n capturing = true\n document.addEventListener(\"click\", onClick, true)\n document.body.style.cursor = \"crosshair\"\n emitViewport()\n } else if (msg.type === \"markup-locate\") {\n const el = (() => { try { return document.querySelector(msg.selector) } catch { return null } })()\n const rect = el\n ? (() => {\n const r = (el as Element).getBoundingClientRect()\n return { x: r.left + window.scrollX, y: r.top + window.scrollY, w: r.width, h: r.height }\n })()\n : null\n post({ source: CMS_MARKUP_SOURCE, type: \"markup-located\", id: msg.id, rect })\n }\n }\n\n window.addEventListener(\"message\", onMessage)\n window.addEventListener(\"scroll\", emitViewport, true)\n window.addEventListener(\"resize\", emitViewport)\n\n // Announce readiness on mount — parent replies with markup-init to start capture.\n const initialDoc = doc()\n window.parent.postMessage(\n { source: CMS_MARKUP_SOURCE, type: \"markup-ready\", docW: initialDoc.docW, docH: initialDoc.docH },\n \"*\" // markup-ready carries no sensitive data; parent validates origin on its side\n )\n\n return () => {\n window.removeEventListener(\"message\", onMessage)\n window.removeEventListener(\"scroll\", emitViewport, true)\n window.removeEventListener(\"resize\", emitViewport)\n document.removeEventListener(\"click\", onClick, true)\n document.body.style.cursor = \"\"\n }\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,169 @@
1
+ "use client";
2
+ "use client";
3
+
4
+ // src/markup/react.tsx
5
+ import { useEffect } from "react";
6
+
7
+ // src/visual-editing/protocol.ts
8
+ var DEFAULT_TRUSTED_ORIGINS = [
9
+ "https://cms.distinctstudio.co.nz",
10
+ "https://distinctcms.com"
11
+ ];
12
+ function isTrustedOrigin(origin, allowlist) {
13
+ if (allowlist.includes(origin)) return true;
14
+ return /^https?:\/\/localhost(:\d+)?$/.test(origin);
15
+ }
16
+
17
+ // src/markup/protocol.ts
18
+ var CMS_MARKUP_SOURCE = "cms-markup";
19
+ var VALID = /* @__PURE__ */ new Set([
20
+ "markup-ready",
21
+ "markup-pin",
22
+ "markup-viewport",
23
+ "markup-located",
24
+ "markup-init",
25
+ "markup-locate"
26
+ ]);
27
+ function parseMarkupMessage(data) {
28
+ if (typeof data === "object" && data !== null && data.source === CMS_MARKUP_SOURCE && typeof data.type === "string" && VALID.has(data.type)) {
29
+ return data;
30
+ }
31
+ return null;
32
+ }
33
+
34
+ // src/markup/selector.ts
35
+ function computeSelector(el) {
36
+ if (!el || !el.isConnected) return null;
37
+ if (el.id) return `#${CSS.escape(el.id)}`;
38
+ const field = el.getAttribute("data-cms-field");
39
+ if (field) return `[data-cms-field="${CSS.escape(field)}"]`;
40
+ const parts = [];
41
+ let node = el;
42
+ while (node && node.nodeType === 1 && node.tagName.toLowerCase() !== "html") {
43
+ if (node.id) {
44
+ parts.unshift(`#${CSS.escape(node.id)}`);
45
+ break;
46
+ }
47
+ const tag = node.tagName.toLowerCase();
48
+ const parent = node.parentElement;
49
+ if (!parent) {
50
+ parts.unshift(tag);
51
+ break;
52
+ }
53
+ const sameType = Array.from(parent.children).filter((c) => c.tagName === node.tagName);
54
+ const idx = sameType.indexOf(node) + 1;
55
+ parts.unshift(sameType.length > 1 ? `${tag}:nth-of-type(${idx})` : tag);
56
+ node = parent;
57
+ }
58
+ return parts.join(" > ");
59
+ }
60
+
61
+ // src/markup/bridge.ts
62
+ function createMarkupBridge(options = {}) {
63
+ if (typeof window === "undefined" || window.parent === window) return () => {
64
+ };
65
+ const allow = options.trustedOrigins ?? DEFAULT_TRUSTED_ORIGINS;
66
+ let parentOrigin = null;
67
+ let capturing = false;
68
+ const doc = () => ({
69
+ docW: document.documentElement.scrollWidth,
70
+ docH: document.documentElement.scrollHeight
71
+ });
72
+ function post(msg) {
73
+ if (parentOrigin) window.parent.postMessage(msg, parentOrigin);
74
+ }
75
+ function onClick(e) {
76
+ if (!capturing || !parentOrigin) return;
77
+ const el = e.target;
78
+ if (!el) return;
79
+ e.preventDefault();
80
+ e.stopPropagation();
81
+ const { docW, docH } = doc();
82
+ const sx = window.scrollX, sy = window.scrollY;
83
+ const docX = e.clientX + sx, docY = e.clientY + sy;
84
+ const r = el.getBoundingClientRect();
85
+ const rx = r.width > 0 ? (e.clientX - r.left) / r.width : 0;
86
+ const ry = r.height > 0 ? (e.clientY - r.top) / r.height : 0;
87
+ post({
88
+ source: CMS_MARKUP_SOURCE,
89
+ type: "markup-pin",
90
+ xPct: docW > 0 ? docX / docW : 0,
91
+ yPct: docH > 0 ? docY / docH : 0,
92
+ relX: Math.min(1, Math.max(0, rx)),
93
+ relY: Math.min(1, Math.max(0, ry)),
94
+ selector: computeSelector(el),
95
+ viewportW: window.innerWidth,
96
+ docW,
97
+ docH
98
+ });
99
+ }
100
+ function emitViewport() {
101
+ if (!parentOrigin) return;
102
+ const { docW, docH } = doc();
103
+ post({
104
+ source: CMS_MARKUP_SOURCE,
105
+ type: "markup-viewport",
106
+ scrollX: window.scrollX,
107
+ scrollY: window.scrollY,
108
+ docW,
109
+ docH,
110
+ viewportW: window.innerWidth
111
+ });
112
+ }
113
+ function onMessage(e) {
114
+ if (!isTrustedOrigin(e.origin, allow)) return;
115
+ const msg = parseMarkupMessage(e.data);
116
+ if (!msg) return;
117
+ if (msg.type === "markup-init") {
118
+ parentOrigin = e.origin;
119
+ capturing = true;
120
+ document.addEventListener("click", onClick, true);
121
+ document.body.style.cursor = "crosshair";
122
+ emitViewport();
123
+ } else if (msg.type === "markup-locate") {
124
+ const el = (() => {
125
+ try {
126
+ return document.querySelector(msg.selector);
127
+ } catch {
128
+ return null;
129
+ }
130
+ })();
131
+ const rect = el ? (() => {
132
+ const r = el.getBoundingClientRect();
133
+ return { x: r.left + window.scrollX, y: r.top + window.scrollY, w: r.width, h: r.height };
134
+ })() : null;
135
+ post({ source: CMS_MARKUP_SOURCE, type: "markup-located", id: msg.id, rect });
136
+ }
137
+ }
138
+ window.addEventListener("message", onMessage);
139
+ window.addEventListener("scroll", emitViewport, true);
140
+ window.addEventListener("resize", emitViewport);
141
+ const initialDoc = doc();
142
+ window.parent.postMessage(
143
+ { source: CMS_MARKUP_SOURCE, type: "markup-ready", docW: initialDoc.docW, docH: initialDoc.docH },
144
+ "*"
145
+ // markup-ready carries no sensitive data; parent validates origin on its side
146
+ );
147
+ return () => {
148
+ window.removeEventListener("message", onMessage);
149
+ window.removeEventListener("scroll", emitViewport, true);
150
+ window.removeEventListener("resize", emitViewport);
151
+ document.removeEventListener("click", onClick, true);
152
+ document.body.style.cursor = "";
153
+ };
154
+ }
155
+
156
+ // src/markup/react.tsx
157
+ function CmsMarkupBridge({ trustedOrigins, param = "cms_markup" }) {
158
+ useEffect(() => {
159
+ if (typeof window === "undefined") return;
160
+ const params = new URLSearchParams(window.location.search);
161
+ if (!params.has(param)) return;
162
+ return createMarkupBridge({ trustedOrigins });
163
+ }, [trustedOrigins, param]);
164
+ return null;
165
+ }
166
+ export {
167
+ CmsMarkupBridge
168
+ };
169
+ //# sourceMappingURL=markup.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/markup/react.tsx","../src/visual-editing/protocol.ts","../src/markup/protocol.ts","../src/markup/selector.ts","../src/markup/bridge.ts"],"sourcesContent":["\"use client\"\nimport { useEffect } from \"react\"\nimport { createMarkupBridge } from \"./bridge\"\n\nexport interface CmsMarkupBridgeProps {\n trustedOrigins?: string[]\n /** Query param that activates markup mode. Defaults to \"cms_markup\". */\n param?: string\n}\n\n/**\n * Mount once in your site (e.g. root layout, alongside CmsVisualBridge).\n * Renders nothing; activates only when loaded with the markup param inside a\n * trusted CMS frame.\n */\nexport function CmsMarkupBridge({ trustedOrigins, param = \"cms_markup\" }: CmsMarkupBridgeProps) {\n useEffect(() => {\n if (typeof window === \"undefined\") return\n const params = new URLSearchParams(window.location.search)\n if (!params.has(param)) return\n return createMarkupBridge({ trustedOrigins })\n }, [trustedOrigins, param])\n return null\n}\n","/** Discriminator stamped on every visual-editing postMessage. */\nexport const CMS_VISUAL_SOURCE = \"cms-visual\" as const\n\n/** CMS origins the bridge will trust by default. */\nexport const DEFAULT_TRUSTED_ORIGINS: readonly string[] = [\n \"https://cms.distinctstudio.co.nz\",\n \"https://distinctcms.com\",\n]\n\nexport type VisualFieldType = \"text\" | \"image\"\n\n/** Compact wire type announced in `ready`; the schema adds length constraints. */\nexport interface VisualFieldMeta {\n name: string\n type: VisualFieldType\n}\n\nexport interface VisualFieldSchema {\n name: string\n type: VisualFieldType\n max_length?: number\n recommended_length?: number\n}\n\n// bridge -> parent\nexport interface ReadyMessage {\n source: typeof CMS_VISUAL_SOURCE\n type: \"ready\"\n fields: VisualFieldMeta[]\n}\nexport interface EditMessage {\n source: typeof CMS_VISUAL_SOURCE\n type: \"edit\"\n name: string\n value: string\n length: number\n /** When the field is inside a repeating group item, the group field name. */\n list?: string\n /** Item index within the group (document order). */\n index?: number\n}\nexport interface PickImageMessage {\n source: typeof CMS_VISUAL_SOURCE\n type: \"pick-image\"\n name: string\n /** When the field is inside a repeating group item, the group field name. */\n list?: string\n /** Item index within the group (document order). */\n index?: number\n}\n\n// parent -> bridge\nexport interface InitMessage {\n source: typeof CMS_VISUAL_SOURCE\n type: \"init\"\n values: Record<string, string>\n schema: Record<string, VisualFieldSchema>\n}\nexport interface SetMessage {\n source: typeof CMS_VISUAL_SOURCE\n type: \"set\"\n name: string\n value: string\n /** When the field is inside a repeating group item, the group field name. */\n list?: string\n /** Item index within the group (document order). */\n index?: number\n}\n\nexport type BridgeOutbound = ReadyMessage | EditMessage | PickImageMessage\nexport type BridgeInbound = InitMessage | SetMessage\nexport type VisualMessage = BridgeOutbound | BridgeInbound\n\n/** Exact-match origin allowlist, plus any localhost port for local dev. */\nexport function isTrustedOrigin(\n origin: string,\n allowlist: readonly string[]\n): boolean {\n if (allowlist.includes(origin)) return true\n return /^https?:\\/\\/localhost(:\\d+)?$/.test(origin)\n}\n\nconst VALID_VISUAL_TYPES = new Set([\"ready\", \"edit\", \"pick-image\", \"init\", \"set\"])\n\n/** Narrow an unknown postMessage payload to a visual message, or null. */\nexport function parseVisualMessage(data: unknown): VisualMessage | null {\n if (\n typeof data === \"object\" &&\n data !== null &&\n (data as Record<string, unknown>).source === CMS_VISUAL_SOURCE &&\n typeof (data as Record<string, unknown>).type === \"string\" &&\n VALID_VISUAL_TYPES.has((data as Record<string, unknown>).type as string)\n ) {\n return data as VisualMessage\n }\n return null\n}\n","import { isTrustedOrigin } from \"../visual-editing/protocol\"\n\nexport { isTrustedOrigin }\nexport const CMS_MARKUP_SOURCE = \"cms-markup\" as const\n\nexport interface MarkupReady {\n source: typeof CMS_MARKUP_SOURCE\n type: \"markup-ready\"\n docW: number\n docH: number\n}\nexport interface MarkupPin {\n source: typeof CMS_MARKUP_SOURCE\n type: \"markup-pin\"\n xPct: number\n yPct: number\n relX: number\n relY: number\n selector: string | null\n viewportW: number\n docW: number\n docH: number\n}\nexport interface MarkupViewport {\n source: typeof CMS_MARKUP_SOURCE\n type: \"markup-viewport\"\n scrollX: number\n scrollY: number\n docW: number\n docH: number\n viewportW: number\n}\nexport interface MarkupLocated {\n source: typeof CMS_MARKUP_SOURCE\n type: \"markup-located\"\n id: string\n rect: { x: number; y: number; w: number; h: number } | null\n}\nexport interface MarkupInit {\n source: typeof CMS_MARKUP_SOURCE\n type: \"markup-init\"\n}\nexport interface MarkupLocate {\n source: typeof CMS_MARKUP_SOURCE\n type: \"markup-locate\"\n id: string\n selector: string\n}\n\nexport type MarkupOutbound = MarkupReady | MarkupPin | MarkupViewport | MarkupLocated\nexport type MarkupInbound = MarkupInit | MarkupLocate\nexport type MarkupMessage = MarkupOutbound | MarkupInbound\n\nconst VALID = new Set([\n \"markup-ready\", \"markup-pin\", \"markup-viewport\", \"markup-located\",\n \"markup-init\", \"markup-locate\",\n])\n\nexport function parseMarkupMessage(data: unknown): MarkupMessage | null {\n if (\n typeof data === \"object\" && data !== null &&\n (data as Record<string, unknown>).source === CMS_MARKUP_SOURCE &&\n typeof (data as Record<string, unknown>).type === \"string\" &&\n VALID.has((data as Record<string, unknown>).type as string)\n ) {\n return data as MarkupMessage\n }\n return null\n}\n","/** Build a stable-ish CSS selector for an element, for re-anchoring pins. */\nexport function computeSelector(el: Element): string | null {\n if (!el || !el.isConnected) return null\n if (el.id) return `#${CSS.escape(el.id)}`\n const field = el.getAttribute(\"data-cms-field\")\n if (field) return `[data-cms-field=\"${CSS.escape(field)}\"]`\n\n const parts: string[] = []\n let node: Element | null = el\n while (node && node.nodeType === 1 && node.tagName.toLowerCase() !== \"html\") {\n if (node.id) { parts.unshift(`#${CSS.escape(node.id)}`); break }\n const tag = node.tagName.toLowerCase()\n const parent: Element | null = node.parentElement\n if (!parent) { parts.unshift(tag); break }\n const sameType = Array.from(parent.children).filter((c) => c.tagName === node!.tagName)\n const idx = sameType.indexOf(node) + 1\n parts.unshift(sameType.length > 1 ? `${tag}:nth-of-type(${idx})` : tag)\n node = parent\n }\n return parts.join(\" > \")\n}\n","import { DEFAULT_TRUSTED_ORIGINS } from \"../visual-editing/protocol\"\nimport { CMS_MARKUP_SOURCE, isTrustedOrigin, parseMarkupMessage, type MarkupOutbound } from \"./protocol\"\nimport { computeSelector } from \"./selector\"\n\nexport interface MarkupBridgeOptions { trustedOrigins?: string[] }\n\n/**\n * In-page markup capture bridge. Dormant until a trusted CMS parent sends\n * `markup-init`. Captures clicks anywhere, reports pin coords + selector, and\n * answers `markup-locate` so the parent can re-anchor pins. Returns cleanup.\n */\nexport function createMarkupBridge(options: MarkupBridgeOptions = {}): () => void {\n if (typeof window === \"undefined\" || window.parent === window) return () => {}\n const allow = options.trustedOrigins ?? DEFAULT_TRUSTED_ORIGINS\n let parentOrigin: string | null = null\n let capturing = false\n\n const doc = () => ({\n docW: document.documentElement.scrollWidth,\n docH: document.documentElement.scrollHeight,\n })\n\n function post(msg: MarkupOutbound) {\n if (parentOrigin) window.parent.postMessage(msg, parentOrigin)\n }\n\n function onClick(e: MouseEvent) {\n if (!capturing || !parentOrigin) return\n const el = e.target as Element | null\n if (!el) return\n e.preventDefault()\n e.stopPropagation()\n const { docW, docH } = doc()\n const sx = window.scrollX, sy = window.scrollY\n const docX = e.clientX + sx, docY = e.clientY + sy\n const r = el.getBoundingClientRect()\n const rx = r.width > 0 ? (e.clientX - r.left) / r.width : 0\n const ry = r.height > 0 ? (e.clientY - r.top) / r.height : 0\n post({\n source: CMS_MARKUP_SOURCE, type: \"markup-pin\",\n xPct: docW > 0 ? docX / docW : 0,\n yPct: docH > 0 ? docY / docH : 0,\n relX: Math.min(1, Math.max(0, rx)),\n relY: Math.min(1, Math.max(0, ry)),\n selector: computeSelector(el),\n viewportW: window.innerWidth, docW, docH,\n })\n }\n\n function emitViewport() {\n if (!parentOrigin) return\n const { docW, docH } = doc()\n post({\n source: CMS_MARKUP_SOURCE, type: \"markup-viewport\",\n scrollX: window.scrollX, scrollY: window.scrollY,\n docW, docH, viewportW: window.innerWidth,\n })\n }\n\n function onMessage(e: MessageEvent) {\n if (!isTrustedOrigin(e.origin, allow)) return\n const msg = parseMarkupMessage(e.data)\n if (!msg) return\n if (msg.type === \"markup-init\") {\n parentOrigin = e.origin\n capturing = true\n document.addEventListener(\"click\", onClick, true)\n document.body.style.cursor = \"crosshair\"\n emitViewport()\n } else if (msg.type === \"markup-locate\") {\n const el = (() => { try { return document.querySelector(msg.selector) } catch { return null } })()\n const rect = el\n ? (() => {\n const r = (el as Element).getBoundingClientRect()\n return { x: r.left + window.scrollX, y: r.top + window.scrollY, w: r.width, h: r.height }\n })()\n : null\n post({ source: CMS_MARKUP_SOURCE, type: \"markup-located\", id: msg.id, rect })\n }\n }\n\n window.addEventListener(\"message\", onMessage)\n window.addEventListener(\"scroll\", emitViewport, true)\n window.addEventListener(\"resize\", emitViewport)\n\n // Announce readiness on mount — parent replies with markup-init to start capture.\n const initialDoc = doc()\n window.parent.postMessage(\n { source: CMS_MARKUP_SOURCE, type: \"markup-ready\", docW: initialDoc.docW, docH: initialDoc.docH },\n \"*\" // markup-ready carries no sensitive data; parent validates origin on its side\n )\n\n return () => {\n window.removeEventListener(\"message\", onMessage)\n window.removeEventListener(\"scroll\", emitViewport, true)\n window.removeEventListener(\"resize\", emitViewport)\n document.removeEventListener(\"click\", onClick, true)\n document.body.style.cursor = \"\"\n }\n}\n"],"mappings":";;;;AACA,SAAS,iBAAiB;;;ACGnB,IAAM,0BAA6C;AAAA,EACxD;AAAA,EACA;AACF;AAmEO,SAAS,gBACd,QACA,WACS;AACT,MAAI,UAAU,SAAS,MAAM,EAAG,QAAO;AACvC,SAAO,gCAAgC,KAAK,MAAM;AACpD;;;AC7EO,IAAM,oBAAoB;AAkDjC,IAAM,QAAQ,oBAAI,IAAI;AAAA,EACpB;AAAA,EAAgB;AAAA,EAAc;AAAA,EAAmB;AAAA,EACjD;AAAA,EAAe;AACjB,CAAC;AAEM,SAAS,mBAAmB,MAAqC;AACtE,MACE,OAAO,SAAS,YAAY,SAAS,QACpC,KAAiC,WAAW,qBAC7C,OAAQ,KAAiC,SAAS,YAClD,MAAM,IAAK,KAAiC,IAAc,GAC1D;AACA,WAAO;AAAA,EACT;AACA,SAAO;AACT;;;ACnEO,SAAS,gBAAgB,IAA4B;AAC1D,MAAI,CAAC,MAAM,CAAC,GAAG,YAAa,QAAO;AACnC,MAAI,GAAG,GAAI,QAAO,IAAI,IAAI,OAAO,GAAG,EAAE,CAAC;AACvC,QAAM,QAAQ,GAAG,aAAa,gBAAgB;AAC9C,MAAI,MAAO,QAAO,oBAAoB,IAAI,OAAO,KAAK,CAAC;AAEvD,QAAM,QAAkB,CAAC;AACzB,MAAI,OAAuB;AAC3B,SAAO,QAAQ,KAAK,aAAa,KAAK,KAAK,QAAQ,YAAY,MAAM,QAAQ;AAC3E,QAAI,KAAK,IAAI;AAAE,YAAM,QAAQ,IAAI,IAAI,OAAO,KAAK,EAAE,CAAC,EAAE;AAAG;AAAA,IAAM;AAC/D,UAAM,MAAM,KAAK,QAAQ,YAAY;AACrC,UAAM,SAAyB,KAAK;AACpC,QAAI,CAAC,QAAQ;AAAE,YAAM,QAAQ,GAAG;AAAG;AAAA,IAAM;AACzC,UAAM,WAAW,MAAM,KAAK,OAAO,QAAQ,EAAE,OAAO,CAAC,MAAM,EAAE,YAAY,KAAM,OAAO;AACtF,UAAM,MAAM,SAAS,QAAQ,IAAI,IAAI;AACrC,UAAM,QAAQ,SAAS,SAAS,IAAI,GAAG,GAAG,gBAAgB,GAAG,MAAM,GAAG;AACtE,WAAO;AAAA,EACT;AACA,SAAO,MAAM,KAAK,KAAK;AACzB;;;ACTO,SAAS,mBAAmB,UAA+B,CAAC,GAAe;AAChF,MAAI,OAAO,WAAW,eAAe,OAAO,WAAW,OAAQ,QAAO,MAAM;AAAA,EAAC;AAC7E,QAAM,QAAQ,QAAQ,kBAAkB;AACxC,MAAI,eAA8B;AAClC,MAAI,YAAY;AAEhB,QAAM,MAAM,OAAO;AAAA,IACjB,MAAM,SAAS,gBAAgB;AAAA,IAC/B,MAAM,SAAS,gBAAgB;AAAA,EACjC;AAEA,WAAS,KAAK,KAAqB;AACjC,QAAI,aAAc,QAAO,OAAO,YAAY,KAAK,YAAY;AAAA,EAC/D;AAEA,WAAS,QAAQ,GAAe;AAC9B,QAAI,CAAC,aAAa,CAAC,aAAc;AACjC,UAAM,KAAK,EAAE;AACb,QAAI,CAAC,GAAI;AACT,MAAE,eAAe;AACjB,MAAE,gBAAgB;AAClB,UAAM,EAAE,MAAM,KAAK,IAAI,IAAI;AAC3B,UAAM,KAAK,OAAO,SAAS,KAAK,OAAO;AACvC,UAAM,OAAO,EAAE,UAAU,IAAI,OAAO,EAAE,UAAU;AAChD,UAAM,IAAI,GAAG,sBAAsB;AACnC,UAAM,KAAK,EAAE,QAAQ,KAAK,EAAE,UAAU,EAAE,QAAQ,EAAE,QAAQ;AAC1D,UAAM,KAAK,EAAE,SAAS,KAAK,EAAE,UAAU,EAAE,OAAO,EAAE,SAAS;AAC3D,SAAK;AAAA,MACH,QAAQ;AAAA,MAAmB,MAAM;AAAA,MACjC,MAAM,OAAO,IAAI,OAAO,OAAO;AAAA,MAC/B,MAAM,OAAO,IAAI,OAAO,OAAO;AAAA,MAC/B,MAAM,KAAK,IAAI,GAAG,KAAK,IAAI,GAAG,EAAE,CAAC;AAAA,MACjC,MAAM,KAAK,IAAI,GAAG,KAAK,IAAI,GAAG,EAAE,CAAC;AAAA,MACjC,UAAU,gBAAgB,EAAE;AAAA,MAC5B,WAAW,OAAO;AAAA,MAAY;AAAA,MAAM;AAAA,IACtC,CAAC;AAAA,EACH;AAEA,WAAS,eAAe;AACtB,QAAI,CAAC,aAAc;AACnB,UAAM,EAAE,MAAM,KAAK,IAAI,IAAI;AAC3B,SAAK;AAAA,MACH,QAAQ;AAAA,MAAmB,MAAM;AAAA,MACjC,SAAS,OAAO;AAAA,MAAS,SAAS,OAAO;AAAA,MACzC;AAAA,MAAM;AAAA,MAAM,WAAW,OAAO;AAAA,IAChC,CAAC;AAAA,EACH;AAEA,WAAS,UAAU,GAAiB;AAClC,QAAI,CAAC,gBAAgB,EAAE,QAAQ,KAAK,EAAG;AACvC,UAAM,MAAM,mBAAmB,EAAE,IAAI;AACrC,QAAI,CAAC,IAAK;AACV,QAAI,IAAI,SAAS,eAAe;AAC9B,qBAAe,EAAE;AACjB,kBAAY;AACZ,eAAS,iBAAiB,SAAS,SAAS,IAAI;AAChD,eAAS,KAAK,MAAM,SAAS;AAC7B,mBAAa;AAAA,IACf,WAAW,IAAI,SAAS,iBAAiB;AACvC,YAAM,MAAM,MAAM;AAAE,YAAI;AAAE,iBAAO,SAAS,cAAc,IAAI,QAAQ;AAAA,QAAE,QAAQ;AAAE,iBAAO;AAAA,QAAK;AAAA,MAAE,GAAG;AACjG,YAAM,OAAO,MACR,MAAM;AACL,cAAM,IAAK,GAAe,sBAAsB;AAChD,eAAO,EAAE,GAAG,EAAE,OAAO,OAAO,SAAS,GAAG,EAAE,MAAM,OAAO,SAAS,GAAG,EAAE,OAAO,GAAG,EAAE,OAAO;AAAA,MAC1F,GAAG,IACH;AACJ,WAAK,EAAE,QAAQ,mBAAmB,MAAM,kBAAkB,IAAI,IAAI,IAAI,KAAK,CAAC;AAAA,IAC9E;AAAA,EACF;AAEA,SAAO,iBAAiB,WAAW,SAAS;AAC5C,SAAO,iBAAiB,UAAU,cAAc,IAAI;AACpD,SAAO,iBAAiB,UAAU,YAAY;AAG9C,QAAM,aAAa,IAAI;AACvB,SAAO,OAAO;AAAA,IACZ,EAAE,QAAQ,mBAAmB,MAAM,gBAAgB,MAAM,WAAW,MAAM,MAAM,WAAW,KAAK;AAAA,IAChG;AAAA;AAAA,EACF;AAEA,SAAO,MAAM;AACX,WAAO,oBAAoB,WAAW,SAAS;AAC/C,WAAO,oBAAoB,UAAU,cAAc,IAAI;AACvD,WAAO,oBAAoB,UAAU,YAAY;AACjD,aAAS,oBAAoB,SAAS,SAAS,IAAI;AACnD,aAAS,KAAK,MAAM,SAAS;AAAA,EAC/B;AACF;;;AJpFO,SAAS,gBAAgB,EAAE,gBAAgB,QAAQ,aAAa,GAAyB;AAC9F,YAAU,MAAM;AACd,QAAI,OAAO,WAAW,YAAa;AACnC,UAAM,SAAS,IAAI,gBAAgB,OAAO,SAAS,MAAM;AACzD,QAAI,CAAC,OAAO,IAAI,KAAK,EAAG;AACxB,WAAO,mBAAmB,EAAE,eAAe,CAAC;AAAA,EAC9C,GAAG,CAAC,gBAAgB,KAAK,CAAC;AAC1B,SAAO;AACT;","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@distinctagency/cms-client",
3
- "version": "1.26.0",
3
+ "version": "1.27.0",
4
4
  "description": "Client library for Distinct CMS — query content, products, and manage orders",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",
@@ -29,6 +29,11 @@
29
29
  "types": "./dist/visual-editing.d.ts",
30
30
  "import": "./dist/visual-editing.mjs",
31
31
  "require": "./dist/visual-editing.js"
32
+ },
33
+ "./markup": {
34
+ "types": "./dist/markup.d.ts",
35
+ "import": "./dist/markup.mjs",
36
+ "require": "./dist/markup.js"
32
37
  }
33
38
  },
34
39
  "scripts": {