@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,437 @@
|
|
|
1
|
+
// src/preview/svelte.ts
|
|
2
|
+
import { writable, derived } from "svelte/store";
|
|
3
|
+
|
|
4
|
+
// src/preview/client.ts
|
|
5
|
+
var DEFAULT_ALLOWED_ORIGINS = [
|
|
6
|
+
"https://app.typecms.com",
|
|
7
|
+
"https://staging.typecms.com",
|
|
8
|
+
"http://localhost:3000",
|
|
9
|
+
"http://localhost:3001"
|
|
10
|
+
];
|
|
11
|
+
function createPreviewClient(config = {}) {
|
|
12
|
+
const {
|
|
13
|
+
allowedOrigins = DEFAULT_ALLOWED_ORIGINS,
|
|
14
|
+
onInit,
|
|
15
|
+
onUpdate,
|
|
16
|
+
onNavigate,
|
|
17
|
+
onFocus,
|
|
18
|
+
debug = false
|
|
19
|
+
} = config;
|
|
20
|
+
let state = {
|
|
21
|
+
enabled: false,
|
|
22
|
+
entryId: null,
|
|
23
|
+
projectId: null,
|
|
24
|
+
locale: null,
|
|
25
|
+
typeId: null,
|
|
26
|
+
values: {},
|
|
27
|
+
focusedPath: null
|
|
28
|
+
};
|
|
29
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
30
|
+
let isConnected = false;
|
|
31
|
+
let parentOrigin = null;
|
|
32
|
+
function log(...args) {
|
|
33
|
+
if (debug) {
|
|
34
|
+
console.log("[TypeCMS Preview]", ...args);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
function setState(updates) {
|
|
38
|
+
state = { ...state, ...updates };
|
|
39
|
+
listeners.forEach((listener) => listener(state));
|
|
40
|
+
}
|
|
41
|
+
function isAllowedOrigin(origin) {
|
|
42
|
+
return allowedOrigins.some((allowed) => {
|
|
43
|
+
if (allowed === "*") return true;
|
|
44
|
+
const expected = allowed.endsWith("/*") ? allowed.slice(0, -2) : allowed;
|
|
45
|
+
return origin === expected;
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
function sendMessage(message) {
|
|
49
|
+
if (!parentOrigin) {
|
|
50
|
+
log("Cannot send message: no parent origin");
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
if (typeof window === "undefined" || !window.parent) {
|
|
54
|
+
log("Cannot send message: not in iframe");
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
log("Sending message:", message.type);
|
|
58
|
+
window.parent.postMessage(message, parentOrigin);
|
|
59
|
+
}
|
|
60
|
+
function handleMessage(event) {
|
|
61
|
+
if (!isAllowedOrigin(event.origin)) {
|
|
62
|
+
log("Ignored message from untrusted origin:", event.origin);
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
const message = event.data;
|
|
66
|
+
if (!message?.type?.startsWith("typecms:preview:")) {
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
log("Received message:", message.type, message);
|
|
70
|
+
parentOrigin = event.origin;
|
|
71
|
+
switch (message.type) {
|
|
72
|
+
case "typecms:preview:init": {
|
|
73
|
+
const initMsg = message;
|
|
74
|
+
setState({
|
|
75
|
+
enabled: true,
|
|
76
|
+
entryId: initMsg.payload.entryId,
|
|
77
|
+
projectId: initMsg.payload.projectId,
|
|
78
|
+
locale: initMsg.payload.locale,
|
|
79
|
+
typeId: initMsg.payload.typeId,
|
|
80
|
+
values: {},
|
|
81
|
+
focusedPath: null
|
|
82
|
+
});
|
|
83
|
+
onInit?.(initMsg.payload);
|
|
84
|
+
const readyMessage = {
|
|
85
|
+
type: "typecms:preview:ready",
|
|
86
|
+
timestamp: Date.now(),
|
|
87
|
+
payload: {
|
|
88
|
+
pathname: typeof window !== "undefined" ? window.location.pathname : "/"
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
sendMessage(readyMessage);
|
|
92
|
+
break;
|
|
93
|
+
}
|
|
94
|
+
case "typecms:preview:update": {
|
|
95
|
+
const updateMsg = message;
|
|
96
|
+
if (updateMsg.payload.values) {
|
|
97
|
+
setState({ values: updateMsg.payload.values });
|
|
98
|
+
} else {
|
|
99
|
+
const newValues = { ...state.values };
|
|
100
|
+
setNestedValue(newValues, updateMsg.payload.path, updateMsg.payload.value);
|
|
101
|
+
setState({ values: newValues });
|
|
102
|
+
}
|
|
103
|
+
onUpdate?.(updateMsg.payload);
|
|
104
|
+
break;
|
|
105
|
+
}
|
|
106
|
+
case "typecms:preview:navigate": {
|
|
107
|
+
onNavigate?.(message.payload.url);
|
|
108
|
+
break;
|
|
109
|
+
}
|
|
110
|
+
case "typecms:preview:focus": {
|
|
111
|
+
setState({ focusedPath: message.payload.path });
|
|
112
|
+
onFocus?.(message.payload.path);
|
|
113
|
+
break;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return {
|
|
118
|
+
/**
|
|
119
|
+
* Start listening for preview messages.
|
|
120
|
+
* Call this on mount.
|
|
121
|
+
*/
|
|
122
|
+
connect() {
|
|
123
|
+
if (typeof window === "undefined") {
|
|
124
|
+
log("Cannot connect: not in browser");
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
if (isConnected) {
|
|
128
|
+
log("Already connected");
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
log("Connecting...");
|
|
132
|
+
window.addEventListener("message", handleMessage);
|
|
133
|
+
isConnected = true;
|
|
134
|
+
if (window.parent && window.parent !== window) {
|
|
135
|
+
const hello = {
|
|
136
|
+
type: "typecms:preview:ready",
|
|
137
|
+
timestamp: Date.now(),
|
|
138
|
+
payload: { pathname: window.location?.pathname ?? "/" }
|
|
139
|
+
};
|
|
140
|
+
for (const allowed of allowedOrigins) {
|
|
141
|
+
const target = allowed === "*" ? "*" : allowed.endsWith("/*") ? allowed.slice(0, -2) : allowed;
|
|
142
|
+
try {
|
|
143
|
+
window.parent.postMessage(hello, target);
|
|
144
|
+
} catch {
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
},
|
|
149
|
+
/**
|
|
150
|
+
* Stop listening for preview messages.
|
|
151
|
+
* Call this on unmount.
|
|
152
|
+
*/
|
|
153
|
+
disconnect() {
|
|
154
|
+
if (typeof window === "undefined") return;
|
|
155
|
+
log("Disconnecting...");
|
|
156
|
+
window.removeEventListener("message", handleMessage);
|
|
157
|
+
isConnected = false;
|
|
158
|
+
parentOrigin = null;
|
|
159
|
+
setState({
|
|
160
|
+
enabled: false,
|
|
161
|
+
entryId: null,
|
|
162
|
+
projectId: null,
|
|
163
|
+
locale: null,
|
|
164
|
+
typeId: null,
|
|
165
|
+
values: {},
|
|
166
|
+
focusedPath: null
|
|
167
|
+
});
|
|
168
|
+
},
|
|
169
|
+
/**
|
|
170
|
+
* Get current preview state.
|
|
171
|
+
*/
|
|
172
|
+
getState() {
|
|
173
|
+
return state;
|
|
174
|
+
},
|
|
175
|
+
/**
|
|
176
|
+
* Check if preview mode is active.
|
|
177
|
+
*/
|
|
178
|
+
isEnabled() {
|
|
179
|
+
return state.enabled;
|
|
180
|
+
},
|
|
181
|
+
/**
|
|
182
|
+
* Get a preview value by path.
|
|
183
|
+
* Returns undefined if no preview value exists.
|
|
184
|
+
*/
|
|
185
|
+
getValue(path) {
|
|
186
|
+
return getNestedValue(state.values, path);
|
|
187
|
+
},
|
|
188
|
+
/**
|
|
189
|
+
* Subscribe to state changes.
|
|
190
|
+
*/
|
|
191
|
+
subscribe(listener) {
|
|
192
|
+
listeners.add(listener);
|
|
193
|
+
return () => listeners.delete(listener);
|
|
194
|
+
},
|
|
195
|
+
/**
|
|
196
|
+
* Notify the editor that the preview has navigated.
|
|
197
|
+
*/
|
|
198
|
+
notifyNavigation(pathname) {
|
|
199
|
+
const message = {
|
|
200
|
+
type: "typecms:preview:ready",
|
|
201
|
+
timestamp: Date.now(),
|
|
202
|
+
payload: { pathname }
|
|
203
|
+
};
|
|
204
|
+
sendMessage(message);
|
|
205
|
+
},
|
|
206
|
+
/**
|
|
207
|
+
* Notify the editor that a field was clicked in the preview.
|
|
208
|
+
* The editor will scroll to the corresponding field.
|
|
209
|
+
*
|
|
210
|
+
* @example
|
|
211
|
+
* ```typescript
|
|
212
|
+
* // On click of an element with data-preview-field attribute:
|
|
213
|
+
* const path = el.closest('[data-preview-field]')?.dataset.previewField
|
|
214
|
+
* if (path) preview.notifyFieldFocus(path)
|
|
215
|
+
* ```
|
|
216
|
+
*/
|
|
217
|
+
notifyFieldFocus(path) {
|
|
218
|
+
const message = {
|
|
219
|
+
type: "typecms:preview:focus",
|
|
220
|
+
timestamp: Date.now(),
|
|
221
|
+
payload: { path }
|
|
222
|
+
};
|
|
223
|
+
sendMessage(message);
|
|
224
|
+
}
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
var UNSAFE_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
|
|
228
|
+
function setNestedValue(obj, path, value) {
|
|
229
|
+
const parts = path.split(".");
|
|
230
|
+
let current = obj;
|
|
231
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
232
|
+
const part = parts[i];
|
|
233
|
+
const arrayMatch2 = part.match(/^(\w+)\[(\d+)\]$/);
|
|
234
|
+
if (arrayMatch2) {
|
|
235
|
+
const [, name, indexStr] = arrayMatch2;
|
|
236
|
+
const index = parseInt(indexStr, 10);
|
|
237
|
+
if (UNSAFE_KEYS.has(name)) continue;
|
|
238
|
+
if (!current[name]) {
|
|
239
|
+
current[name] = [];
|
|
240
|
+
}
|
|
241
|
+
const arr = current[name];
|
|
242
|
+
if (!arr[index]) {
|
|
243
|
+
arr[index] = {};
|
|
244
|
+
}
|
|
245
|
+
current = arr[index];
|
|
246
|
+
} else {
|
|
247
|
+
if (UNSAFE_KEYS.has(part)) continue;
|
|
248
|
+
if (!current[part] || typeof current[part] !== "object") {
|
|
249
|
+
current[part] = {};
|
|
250
|
+
}
|
|
251
|
+
current = current[part];
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
const lastPart = parts[parts.length - 1];
|
|
255
|
+
const arrayMatch = lastPart.match(/^(\w+)\[(\d+)\]$/);
|
|
256
|
+
if (arrayMatch) {
|
|
257
|
+
const [, name, indexStr] = arrayMatch;
|
|
258
|
+
const index = parseInt(indexStr, 10);
|
|
259
|
+
if (UNSAFE_KEYS.has(name)) return;
|
|
260
|
+
if (!current[name]) {
|
|
261
|
+
current[name] = [];
|
|
262
|
+
}
|
|
263
|
+
current[name][index] = value;
|
|
264
|
+
} else {
|
|
265
|
+
if (UNSAFE_KEYS.has(lastPart)) return;
|
|
266
|
+
current[lastPart] = value;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
function getNestedValue(obj, path) {
|
|
270
|
+
const parts = path.split(".");
|
|
271
|
+
let current = obj;
|
|
272
|
+
for (const part of parts) {
|
|
273
|
+
if (current === null || current === void 0) {
|
|
274
|
+
return void 0;
|
|
275
|
+
}
|
|
276
|
+
const arrayMatch = part.match(/^(\w+)\[(\d+)\]$/);
|
|
277
|
+
if (arrayMatch) {
|
|
278
|
+
const [, name, indexStr] = arrayMatch;
|
|
279
|
+
const index = parseInt(indexStr, 10);
|
|
280
|
+
const arr = current[name];
|
|
281
|
+
if (!arr) return void 0;
|
|
282
|
+
current = arr[index];
|
|
283
|
+
} else {
|
|
284
|
+
current = current[part];
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
return current;
|
|
288
|
+
}
|
|
289
|
+
function deepMerge(target, source) {
|
|
290
|
+
const result = { ...target };
|
|
291
|
+
for (const key of Object.keys(source)) {
|
|
292
|
+
const sourceValue = source[key];
|
|
293
|
+
const targetValue = result[key];
|
|
294
|
+
if (sourceValue && typeof sourceValue === "object" && !Array.isArray(sourceValue) && targetValue && typeof targetValue === "object" && !Array.isArray(targetValue)) {
|
|
295
|
+
result[key] = deepMerge(
|
|
296
|
+
targetValue,
|
|
297
|
+
sourceValue
|
|
298
|
+
);
|
|
299
|
+
} else {
|
|
300
|
+
result[key] = sourceValue;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
return result;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// src/preview/overlay.ts
|
|
307
|
+
var DEFAULT_OPTIONS = {
|
|
308
|
+
color: "#3b82f6",
|
|
309
|
+
zIndex: 9999,
|
|
310
|
+
scrollIntoView: true,
|
|
311
|
+
attribute: "data-preview-field"
|
|
312
|
+
};
|
|
313
|
+
function attachPreviewOverlay(client, options = {}) {
|
|
314
|
+
if (typeof window === "undefined" || typeof document === "undefined") {
|
|
315
|
+
return () => {
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
const { color, zIndex, scrollIntoView, attribute } = { ...DEFAULT_OPTIONS, ...options };
|
|
319
|
+
const box = document.createElement("div");
|
|
320
|
+
box.setAttribute("data-typecms-preview-overlay", "");
|
|
321
|
+
Object.assign(box.style, {
|
|
322
|
+
position: "fixed",
|
|
323
|
+
display: "none",
|
|
324
|
+
pointerEvents: "none",
|
|
325
|
+
boxSizing: "border-box",
|
|
326
|
+
border: `2px solid ${color}`,
|
|
327
|
+
borderRadius: "4px",
|
|
328
|
+
zIndex: String(zIndex),
|
|
329
|
+
transition: "top 120ms ease, left 120ms ease, width 120ms ease, height 120ms ease"
|
|
330
|
+
});
|
|
331
|
+
document.body.appendChild(box);
|
|
332
|
+
let target = null;
|
|
333
|
+
let lastPath = null;
|
|
334
|
+
function position() {
|
|
335
|
+
if (!target || !target.isConnected) {
|
|
336
|
+
box.style.display = "none";
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
const rect = target.getBoundingClientRect();
|
|
340
|
+
Object.assign(box.style, {
|
|
341
|
+
display: "block",
|
|
342
|
+
top: `${rect.top - 2}px`,
|
|
343
|
+
left: `${rect.left - 2}px`,
|
|
344
|
+
width: `${rect.width + 4}px`,
|
|
345
|
+
height: `${rect.height + 4}px`
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
function update(state) {
|
|
349
|
+
const path = state.enabled ? state.focusedPath : null;
|
|
350
|
+
if (path === lastPath) return;
|
|
351
|
+
lastPath = path;
|
|
352
|
+
target = path ? document.querySelector(`[${attribute}="${CSS.escape(path)}"]`) : null;
|
|
353
|
+
if (target && scrollIntoView) {
|
|
354
|
+
target.scrollIntoView({ behavior: "smooth", block: "center" });
|
|
355
|
+
}
|
|
356
|
+
position();
|
|
357
|
+
}
|
|
358
|
+
window.addEventListener("scroll", position, true);
|
|
359
|
+
window.addEventListener("resize", position);
|
|
360
|
+
const unsubscribe = client.subscribe(update);
|
|
361
|
+
update(client.getState());
|
|
362
|
+
return () => {
|
|
363
|
+
unsubscribe();
|
|
364
|
+
window.removeEventListener("scroll", position, true);
|
|
365
|
+
window.removeEventListener("resize", position);
|
|
366
|
+
box.remove();
|
|
367
|
+
target = null;
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// src/preview/svelte.ts
|
|
372
|
+
function createPreviewContext(options = {}) {
|
|
373
|
+
const { overlay, ...config } = options;
|
|
374
|
+
const client = createPreviewClient(config);
|
|
375
|
+
const state = writable(client.getState());
|
|
376
|
+
let unsubscribe = null;
|
|
377
|
+
let detachOverlay = null;
|
|
378
|
+
function connect() {
|
|
379
|
+
if (unsubscribe) return;
|
|
380
|
+
unsubscribe = client.subscribe((next) => {
|
|
381
|
+
state.set(next);
|
|
382
|
+
});
|
|
383
|
+
client.connect();
|
|
384
|
+
if (overlay) {
|
|
385
|
+
detachOverlay = attachPreviewOverlay(client, overlay === true ? {} : overlay);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
function disconnect() {
|
|
389
|
+
unsubscribe?.();
|
|
390
|
+
unsubscribe = null;
|
|
391
|
+
detachOverlay?.();
|
|
392
|
+
detachOverlay = null;
|
|
393
|
+
client.disconnect();
|
|
394
|
+
state.set(client.getState());
|
|
395
|
+
}
|
|
396
|
+
const isPreview = derived(state, ($state) => $state.enabled);
|
|
397
|
+
const focusedField = derived(state, ($state) => $state.focusedPath);
|
|
398
|
+
function previewValue(path, fallback) {
|
|
399
|
+
return derived(state, ($state) => {
|
|
400
|
+
if (!$state.enabled) {
|
|
401
|
+
return fallback;
|
|
402
|
+
}
|
|
403
|
+
const value = getNestedValue($state.values, path);
|
|
404
|
+
return value !== void 0 ? value : fallback;
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
function previewContent(content) {
|
|
408
|
+
return derived(state, ($state) => {
|
|
409
|
+
if (!$state.enabled || Object.keys($state.values).length === 0) {
|
|
410
|
+
return content;
|
|
411
|
+
}
|
|
412
|
+
return deepMerge(content, $state.values);
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
return {
|
|
416
|
+
state,
|
|
417
|
+
isPreview,
|
|
418
|
+
focusedField,
|
|
419
|
+
getValue: (path) => client.getValue(path),
|
|
420
|
+
previewValue,
|
|
421
|
+
previewContent,
|
|
422
|
+
notifyNavigation: (pathname) => client.notifyNavigation(pathname),
|
|
423
|
+
notifyFieldFocus: (path) => client.notifyFieldFocus(path),
|
|
424
|
+
connect,
|
|
425
|
+
disconnect
|
|
426
|
+
};
|
|
427
|
+
}
|
|
428
|
+
function initPreview(options = {}) {
|
|
429
|
+
const context = createPreviewContext(options);
|
|
430
|
+
context.connect();
|
|
431
|
+
return context;
|
|
432
|
+
}
|
|
433
|
+
export {
|
|
434
|
+
createPreviewContext,
|
|
435
|
+
initPreview
|
|
436
|
+
};
|
|
437
|
+
//# sourceMappingURL=svelte.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/preview/svelte.ts","../../src/preview/client.ts","../../src/preview/overlay.ts"],"sourcesContent":["/**\n * Preview Svelte Stores\n *\n * Svelte integration for Type CMS live preview.\n * Mirrors the Vue layer (vue.ts) 1:1 on top of the\n * framework-agnostic preview client.\n *\n * Built on `svelte/store` only (no component runtime), so it works\n * in both Svelte 4 and Svelte 5, and in SvelteKit load/SSR contexts.\n *\n * @example\n * ```typescript\n * // src/routes/+layout.svelte\n * <script>\n * import { onMount } from 'svelte'\n * import { initPreview } from '@typecms/sdk/preview/svelte'\n * import { setContext } from 'svelte'\n *\n * onMount(() => {\n * const preview = initPreview({ overlay: true })\n * setContext('typecms-preview', preview)\n * return () => preview.disconnect()\n * })\n * </script>\n * ```\n *\n * ```svelte\n * <!-- src/routes/blog/[slug]/+page.svelte -->\n * <script>\n * import { getContext } from 'svelte'\n *\n * export let data\n * const preview = getContext('typecms-preview')\n * const content = preview.previewContent(data.post)\n * const isPreview = preview.isPreview\n * </script>\n *\n * {#if $isPreview}<span>Preview mode</span>{/if}\n * <h1 data-preview-field=\"title\">{$content.title}</h1>\n * ```\n */\n\nimport { writable, derived, type Readable } from 'svelte/store'\nimport { createPreviewClient, deepMerge, getNestedValue } from './client'\nimport { attachPreviewOverlay, type PreviewOverlayOptions } from './overlay'\nimport type { PreviewConfig, PreviewState } from './types'\n\n// =============================================================================\n// Context\n// =============================================================================\n\nexport interface SveltePreviewContext {\n /** Reactive preview state */\n state: Readable<PreviewState>\n /** Check if running in preview mode */\n isPreview: Readable<boolean>\n /** Currently focused field path (if any) */\n focusedField: Readable<string | null>\n /** Get preview value for a field path */\n getValue: <T>(path: string) => T | undefined\n /** Preview value for a field path, with fallback to the published value */\n previewValue: <T>(path: string, fallback: T) => Readable<T>\n /** Merge published content with preview overrides */\n previewContent: <T extends Record<string, unknown>>(content: T) => Readable<T>\n /** Notify editor of navigation */\n notifyNavigation: (pathname: string) => void\n /** Notify editor that a field was clicked in the preview */\n notifyFieldFocus: (path: string) => void\n /** Start listening for preview messages */\n connect: () => void\n /** Stop listening and reset state */\n disconnect: () => void\n}\n\nexport interface SveltePreviewOptions extends PreviewConfig {\n /**\n * Draw an automatic highlight box over the `[data-preview-field]` element\n * matching the field focused in the editor. Pass options to customize.\n */\n overlay?: boolean | PreviewOverlayOptions\n}\n\n/**\n * Create a preview context backed by Svelte stores.\n *\n * Call `connect()` to start listening for editor messages (typically in\n * onMount of a root +layout.svelte) and `disconnect()` on teardown.\n * For the common case, use {@link initPreview} instead.\n */\nexport function createPreviewContext(options: SveltePreviewOptions = {}): SveltePreviewContext {\n const { overlay, ...config } = options\n const client = createPreviewClient(config)\n const state = writable<PreviewState>(client.getState())\n\n let unsubscribe: (() => void) | null = null\n let detachOverlay: (() => void) | null = null\n\n function connect() {\n if (unsubscribe) return\n unsubscribe = client.subscribe((next) => {\n state.set(next)\n })\n client.connect()\n if (overlay) {\n detachOverlay = attachPreviewOverlay(client, overlay === true ? {} : overlay)\n }\n }\n\n function disconnect() {\n unsubscribe?.()\n unsubscribe = null\n detachOverlay?.()\n detachOverlay = null\n client.disconnect()\n state.set(client.getState())\n }\n\n const isPreview = derived(state, ($state) => $state.enabled)\n\n const focusedField = derived(state, ($state) => $state.focusedPath)\n\n function previewValue<T>(path: string, fallback: T): Readable<T> {\n return derived(state, ($state) => {\n if (!$state.enabled) {\n return fallback\n }\n const value = getNestedValue($state.values, path) as T | undefined\n return value !== undefined ? value : fallback\n })\n }\n\n function previewContent<T extends Record<string, unknown>>(content: T): Readable<T> {\n return derived(state, ($state) => {\n if (!$state.enabled || Object.keys($state.values).length === 0) {\n return content\n }\n return deepMerge(content, $state.values as Record<string, unknown>) as T\n })\n }\n\n return {\n state,\n isPreview,\n focusedField,\n getValue: <T>(path: string) => client.getValue<T>(path),\n previewValue,\n previewContent,\n notifyNavigation: (pathname: string) => client.notifyNavigation(pathname),\n notifyFieldFocus: (path: string) => client.notifyFieldFocus(path),\n connect,\n disconnect,\n }\n}\n\n// =============================================================================\n// Convenience\n// =============================================================================\n\n/**\n * Create a preview context and start listening for editor messages\n * immediately. The typical entry point in a SvelteKit app: call from\n * onMount in a root +layout.svelte and disconnect on teardown.\n *\n * @example\n * ```typescript\n * onMount(() => {\n * const preview = initPreview({ overlay: true })\n * return () => preview.disconnect()\n * })\n * ```\n */\nexport function initPreview(options: SveltePreviewOptions = {}): SveltePreviewContext {\n const context = createPreviewContext(options)\n context.connect()\n return context\n}\n","/**\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 Overlay\n *\n * Draws a highlight box over the element whose `data-preview-field`\n * matches the field currently focused in the Type CMS editor.\n * Zero-markup alternative to hand-rolling highlights with useFocusedField().\n */\n\nimport type { PreviewState } from './types'\n\nexport interface PreviewOverlayOptions {\n /** Outline color of the highlight box */\n color?: string\n /** z-index of the highlight box */\n zIndex?: number\n /** Scroll the highlighted element into view when focus changes */\n scrollIntoView?: boolean\n /** Attribute used to locate field elements */\n attribute?: string\n}\n\ninterface OverlayClient {\n subscribe(listener: (state: PreviewState) => void): () => void\n getState(): PreviewState\n}\n\nconst DEFAULT_OPTIONS: Required<PreviewOverlayOptions> = {\n color: '#3b82f6',\n zIndex: 9999,\n scrollIntoView: true,\n attribute: 'data-preview-field',\n}\n\n/**\n * Attach a focus-highlight overlay to the document.\n *\n * Subscribes to the preview client's state; whenever `focusedPath` changes,\n * positions a non-interactive highlight box over the matching\n * `[data-preview-field]` element. Returns a detach function.\n *\n * @example\n * ```typescript\n * const preview = createPreviewClient()\n * preview.connect()\n * const detach = attachPreviewOverlay(preview)\n * // ...on teardown\n * detach()\n * ```\n */\nexport function attachPreviewOverlay(\n client: OverlayClient,\n options: PreviewOverlayOptions = {},\n): () => void {\n if (typeof window === 'undefined' || typeof document === 'undefined') {\n return () => {}\n }\n\n const { color, zIndex, scrollIntoView, attribute } = { ...DEFAULT_OPTIONS, ...options }\n\n const box = document.createElement('div')\n box.setAttribute('data-typecms-preview-overlay', '')\n Object.assign(box.style, {\n position: 'fixed',\n display: 'none',\n pointerEvents: 'none',\n boxSizing: 'border-box',\n border: `2px solid ${color}`,\n borderRadius: '4px',\n zIndex: String(zIndex),\n transition: 'top 120ms ease, left 120ms ease, width 120ms ease, height 120ms ease',\n })\n document.body.appendChild(box)\n\n let target: Element | null = null\n let lastPath: string | null = null\n\n function position() {\n if (!target || !target.isConnected) {\n box.style.display = 'none'\n return\n }\n const rect = target.getBoundingClientRect()\n Object.assign(box.style, {\n display: 'block',\n top: `${rect.top - 2}px`,\n left: `${rect.left - 2}px`,\n width: `${rect.width + 4}px`,\n height: `${rect.height + 4}px`,\n })\n }\n\n function update(state: PreviewState) {\n const path = state.enabled ? state.focusedPath : null\n if (path === lastPath) return\n lastPath = path\n\n target = path\n ? document.querySelector(`[${attribute}=\"${CSS.escape(path)}\"]`)\n : null\n\n if (target && scrollIntoView) {\n target.scrollIntoView({ behavior: 'smooth', block: 'center' })\n }\n position()\n }\n\n // Capture-phase scroll listener catches scrolling in nested containers\n window.addEventListener('scroll', position, true)\n window.addEventListener('resize', position)\n\n const unsubscribe = client.subscribe(update)\n update(client.getState())\n\n return () => {\n unsubscribe()\n window.removeEventListener('scroll', position, true)\n window.removeEventListener('resize', position)\n box.remove()\n target = null\n }\n}\n"],"mappings":"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","names":["arrayMatch"]}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { InjectionKey, Ref, ComputedRef, Plugin, MaybeRefOrGetter } from 'vue';
|
|
2
|
+
import { P as PreviewOverlayOptions } from '../overlay-CLYAeNwg.mjs';
|
|
3
|
+
import { P as PreviewState, a as PreviewConfig } from '../types-BNHDdA2G.mjs';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Preview Vue Composables
|
|
7
|
+
*
|
|
8
|
+
* Vue 3 integration for Type CMS live preview.
|
|
9
|
+
* Mirrors the React layer (react.tsx) 1:1 on top of the
|
|
10
|
+
* framework-agnostic preview client.
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```typescript
|
|
14
|
+
* // main.ts
|
|
15
|
+
* import { createApp } from 'vue'
|
|
16
|
+
* import { TypecmsPreview } from '@typecms/sdk/preview/vue'
|
|
17
|
+
*
|
|
18
|
+
* const app = createApp(App)
|
|
19
|
+
* app.use(TypecmsPreview, { overlay: true })
|
|
20
|
+
* ```
|
|
21
|
+
*
|
|
22
|
+
* ```vue
|
|
23
|
+
* <script setup>
|
|
24
|
+
* import { usePreviewContent, useIsPreview } from '@typecms/sdk/preview/vue'
|
|
25
|
+
*
|
|
26
|
+
* const props = defineProps(['post'])
|
|
27
|
+
* const isPreview = useIsPreview()
|
|
28
|
+
* const content = usePreviewContent(() => props.post)
|
|
29
|
+
* </script>
|
|
30
|
+
* ```
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
interface VuePreviewContext {
|
|
34
|
+
/** Reactive preview state */
|
|
35
|
+
state: Ref<PreviewState>;
|
|
36
|
+
/** Check if running in preview mode */
|
|
37
|
+
isPreview: ComputedRef<boolean>;
|
|
38
|
+
/** Get preview value for a field path */
|
|
39
|
+
getValue: <T>(path: string) => T | undefined;
|
|
40
|
+
/** Merge published content with preview overrides */
|
|
41
|
+
mergeContent: <T extends Record<string, unknown>>(content: T) => T;
|
|
42
|
+
/** Notify editor of navigation */
|
|
43
|
+
notifyNavigation: (pathname: string) => void;
|
|
44
|
+
/** Notify editor that a field was clicked in the preview */
|
|
45
|
+
notifyFieldFocus: (path: string) => void;
|
|
46
|
+
/** Start listening for preview messages */
|
|
47
|
+
connect: () => void;
|
|
48
|
+
/** Stop listening and reset state */
|
|
49
|
+
disconnect: () => void;
|
|
50
|
+
}
|
|
51
|
+
declare const PREVIEW_INJECTION_KEY: InjectionKey<VuePreviewContext>;
|
|
52
|
+
interface VuePreviewOptions extends PreviewConfig {
|
|
53
|
+
/**
|
|
54
|
+
* Draw an automatic highlight box over the `[data-preview-field]` element
|
|
55
|
+
* matching the field focused in the editor. Pass options to customize.
|
|
56
|
+
*/
|
|
57
|
+
overlay?: boolean | PreviewOverlayOptions;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Create a preview context. Used by the TypecmsPreview plugin;
|
|
61
|
+
* exposed for advanced setups (e.g. Nuxt plugins, manual provide()).
|
|
62
|
+
*/
|
|
63
|
+
declare function createPreviewContext(options?: VuePreviewOptions): VuePreviewContext;
|
|
64
|
+
/**
|
|
65
|
+
* Vue plugin for Type CMS live preview.
|
|
66
|
+
*
|
|
67
|
+
* Installs a preview context app-wide and starts listening for
|
|
68
|
+
* editor messages immediately.
|
|
69
|
+
*/
|
|
70
|
+
declare const TypecmsPreview: Plugin<[VuePreviewOptions?]>;
|
|
71
|
+
/**
|
|
72
|
+
* Access the preview context.
|
|
73
|
+
*
|
|
74
|
+
* @throws Error if the TypecmsPreview plugin is not installed
|
|
75
|
+
*/
|
|
76
|
+
declare function usePreviewContext(): VuePreviewContext;
|
|
77
|
+
/** Check if running in preview mode. */
|
|
78
|
+
declare function useIsPreview(): ComputedRef<boolean>;
|
|
79
|
+
/** Get current preview state (null when the plugin is not installed). */
|
|
80
|
+
declare function usePreviewState(): ComputedRef<PreviewState | null>;
|
|
81
|
+
/**
|
|
82
|
+
* Get a preview value by field path, with fallback to the published value.
|
|
83
|
+
*/
|
|
84
|
+
declare function usePreviewValue<T>(path: MaybeRefOrGetter<string>, fallback: MaybeRefOrGetter<T>): ComputedRef<T>;
|
|
85
|
+
/**
|
|
86
|
+
* Merge published content with preview overrides.
|
|
87
|
+
*
|
|
88
|
+
* The primary composable for integrating preview data: pass fetched
|
|
89
|
+
* content and get back a reactive merged version.
|
|
90
|
+
*/
|
|
91
|
+
declare function usePreviewContent<T extends Record<string, unknown>>(content: MaybeRefOrGetter<T>): ComputedRef<T>;
|
|
92
|
+
/** Get the currently focused field path (if any). */
|
|
93
|
+
declare function useFocusedField(): ComputedRef<string | null>;
|
|
94
|
+
/** Function to notify the editor of navigation. */
|
|
95
|
+
declare function usePreviewNavigation(): (pathname: string) => void;
|
|
96
|
+
/** Function to notify the editor when a preview field is clicked. */
|
|
97
|
+
declare function usePreviewFieldFocus(): (path: string) => void;
|
|
98
|
+
/**
|
|
99
|
+
* Get a rich-text preview value by field path with STRUCTURAL SHARING.
|
|
100
|
+
*
|
|
101
|
+
* Like `usePreviewValue`, but pipes the document through a persistent
|
|
102
|
+
* structural-sharing instance: subtrees that are deep-equal to the previous
|
|
103
|
+
* update keep their object references, so keyed/memoized node renderers only
|
|
104
|
+
* re-render nodes that actually changed — instead of re-rendering the whole
|
|
105
|
+
* rich-text tree on every keystroke.
|
|
106
|
+
*
|
|
107
|
+
* Works for both TipTap ('richtextv2') and Slate ('richtext') documents —
|
|
108
|
+
* or any JSON value. Returns the fallback (unshared) when preview is
|
|
109
|
+
* disabled. Sharing memory resets when the path value changes.
|
|
110
|
+
*/
|
|
111
|
+
declare function usePreviewRichText<T>(path: MaybeRefOrGetter<string>, fallback: MaybeRefOrGetter<T>): ComputedRef<T>;
|
|
112
|
+
|
|
113
|
+
export { PREVIEW_INJECTION_KEY, TypecmsPreview, type VuePreviewContext, type VuePreviewOptions, createPreviewContext, useFocusedField, useIsPreview, usePreviewContent, usePreviewContext, usePreviewFieldFocus, usePreviewNavigation, usePreviewRichText, usePreviewState, usePreviewValue };
|