@foir/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 +110 -0
- package/dist/auth.d.ts +305 -0
- package/dist/auth.js +309 -0
- package/dist/chunk-ADO5FJNM.js +30 -0
- package/dist/client.d.ts +83 -0
- package/dist/client.js +81 -0
- package/dist/codegen/block-registry.d.ts +27 -0
- package/dist/codegen/block-registry.js +35 -0
- package/dist/codegen/block-scaffold.d.ts +28 -0
- package/dist/codegen/block-scaffold.js +45 -0
- package/dist/codegen/index.d.ts +65 -0
- package/dist/codegen/index.js +80 -0
- package/dist/content.d.ts +96 -0
- package/dist/content.js +161 -0
- package/dist/index.d.ts +394 -0
- package/dist/index.js +476 -0
- package/dist/media.d.ts +234 -0
- package/dist/media.js +399 -0
- package/dist/realtime.d.ts +111 -0
- package/dist/realtime.js +168 -0
- package/dist/server.d.ts +457 -0
- package/dist/server.js +296 -0
- package/dist/shared-DkWlEd2Z.d.ts +18 -0
- package/package.json +136 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,476 @@
|
|
|
1
|
+
// src/components/EditorProvider.tsx
|
|
2
|
+
import {
|
|
3
|
+
createContext,
|
|
4
|
+
useCallback,
|
|
5
|
+
useEffect,
|
|
6
|
+
useMemo,
|
|
7
|
+
useRef,
|
|
8
|
+
useState
|
|
9
|
+
} from "react";
|
|
10
|
+
|
|
11
|
+
// src/protocol/constants.ts
|
|
12
|
+
var PROTOCOL_VERSION = "1.0";
|
|
13
|
+
var MAX_RESIZE_HEIGHT = 1e5;
|
|
14
|
+
var DANGEROUS_PATH_SEGMENTS = [
|
|
15
|
+
"__proto__",
|
|
16
|
+
"constructor",
|
|
17
|
+
"prototype"
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
// src/protocol/validation.ts
|
|
21
|
+
var VALID_EDITOR_TYPES = /* @__PURE__ */ new Set([
|
|
22
|
+
"ready",
|
|
23
|
+
"update-values",
|
|
24
|
+
"update-field",
|
|
25
|
+
"request-save",
|
|
26
|
+
"request-publish",
|
|
27
|
+
"resize",
|
|
28
|
+
"dirty",
|
|
29
|
+
"request-navigate",
|
|
30
|
+
"request-operation"
|
|
31
|
+
]);
|
|
32
|
+
var VALID_HOST_TYPES = /* @__PURE__ */ new Set([
|
|
33
|
+
"init",
|
|
34
|
+
"values-changed",
|
|
35
|
+
"variant-changed",
|
|
36
|
+
"save-result",
|
|
37
|
+
"publish-result",
|
|
38
|
+
"theme-changed",
|
|
39
|
+
"selection-changed",
|
|
40
|
+
"locale-changed",
|
|
41
|
+
"operation-result"
|
|
42
|
+
]);
|
|
43
|
+
function isSafeFieldPath(path) {
|
|
44
|
+
const segments = path.split(".");
|
|
45
|
+
return !segments.some((s) => DANGEROUS_PATH_SEGMENTS.includes(s));
|
|
46
|
+
}
|
|
47
|
+
function isValidEditorMessage(data) {
|
|
48
|
+
if (!data || typeof data !== "object") return false;
|
|
49
|
+
const msg = data;
|
|
50
|
+
if (typeof msg.type !== "string") return false;
|
|
51
|
+
if (!VALID_EDITOR_TYPES.has(msg.type)) return false;
|
|
52
|
+
if (!msg.payload || typeof msg.payload !== "object") return false;
|
|
53
|
+
switch (msg.type) {
|
|
54
|
+
case "update-field": {
|
|
55
|
+
const payload = msg.payload;
|
|
56
|
+
if (typeof payload.path !== "string") return false;
|
|
57
|
+
if (!isSafeFieldPath(payload.path)) return false;
|
|
58
|
+
break;
|
|
59
|
+
}
|
|
60
|
+
case "resize": {
|
|
61
|
+
const payload = msg.payload;
|
|
62
|
+
if (typeof payload.height !== "number") return false;
|
|
63
|
+
if (payload.height < 0 || payload.height > MAX_RESIZE_HEIGHT)
|
|
64
|
+
return false;
|
|
65
|
+
if (!Number.isFinite(payload.height)) return false;
|
|
66
|
+
break;
|
|
67
|
+
}
|
|
68
|
+
case "update-values": {
|
|
69
|
+
const payload = msg.payload;
|
|
70
|
+
if (!payload.values || typeof payload.values !== "object") return false;
|
|
71
|
+
break;
|
|
72
|
+
}
|
|
73
|
+
case "dirty": {
|
|
74
|
+
const payload = msg.payload;
|
|
75
|
+
if (typeof payload.isDirty !== "boolean") return false;
|
|
76
|
+
break;
|
|
77
|
+
}
|
|
78
|
+
case "request-navigate": {
|
|
79
|
+
const payload = msg.payload;
|
|
80
|
+
if (typeof payload.path !== "string") return false;
|
|
81
|
+
if (!payload.path.startsWith("/")) return false;
|
|
82
|
+
if (payload.path.length > 500) return false;
|
|
83
|
+
break;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return true;
|
|
87
|
+
}
|
|
88
|
+
function isValidHostMessage(data) {
|
|
89
|
+
if (!data || typeof data !== "object") return false;
|
|
90
|
+
const msg = data;
|
|
91
|
+
if (typeof msg.type !== "string") return false;
|
|
92
|
+
if (!VALID_HOST_TYPES.has(msg.type)) return false;
|
|
93
|
+
if (!msg.payload || typeof msg.payload !== "object") return false;
|
|
94
|
+
return true;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// src/client/platform-client.ts
|
|
98
|
+
var PlatformClientError = class extends Error {
|
|
99
|
+
constructor(message, status, response) {
|
|
100
|
+
super(message);
|
|
101
|
+
this.status = status;
|
|
102
|
+
this.response = response;
|
|
103
|
+
this.name = "PlatformClientError";
|
|
104
|
+
}
|
|
105
|
+
status;
|
|
106
|
+
response;
|
|
107
|
+
};
|
|
108
|
+
function createPlatformClient(options) {
|
|
109
|
+
const { apiUrl, apiKey } = options;
|
|
110
|
+
return {
|
|
111
|
+
async query(query, variables) {
|
|
112
|
+
const res = await fetch(apiUrl, {
|
|
113
|
+
method: "POST",
|
|
114
|
+
headers: {
|
|
115
|
+
"Content-Type": "application/json",
|
|
116
|
+
"x-api-key": apiKey
|
|
117
|
+
},
|
|
118
|
+
body: JSON.stringify({ query, variables })
|
|
119
|
+
});
|
|
120
|
+
if (!res.ok) {
|
|
121
|
+
throw new PlatformClientError(
|
|
122
|
+
`GraphQL request failed: ${res.status} ${res.statusText}`,
|
|
123
|
+
res.status
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
const json = await res.json();
|
|
127
|
+
if (json.errors?.length) {
|
|
128
|
+
throw new PlatformClientError(json.errors[0].message, void 0, json);
|
|
129
|
+
}
|
|
130
|
+
return json;
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// src/components/EditorProvider.tsx
|
|
136
|
+
import { jsx } from "react/jsx-runtime";
|
|
137
|
+
var EditorContext = createContext(null);
|
|
138
|
+
function getEnv(viteKey, nextKey) {
|
|
139
|
+
try {
|
|
140
|
+
const meta = import.meta?.env;
|
|
141
|
+
if (meta?.[viteKey]) return meta[viteKey];
|
|
142
|
+
} catch {
|
|
143
|
+
}
|
|
144
|
+
const proc = globalThis.process;
|
|
145
|
+
if (proc?.env?.[nextKey]) return proc.env[nextKey];
|
|
146
|
+
return void 0;
|
|
147
|
+
}
|
|
148
|
+
function EditorProvider({
|
|
149
|
+
children,
|
|
150
|
+
parentOrigin: parentOriginProp,
|
|
151
|
+
apiUrl: apiUrlProp,
|
|
152
|
+
onInit,
|
|
153
|
+
onValuesChanged,
|
|
154
|
+
onVariantChanged,
|
|
155
|
+
onSaveResult,
|
|
156
|
+
onPublishResult,
|
|
157
|
+
onThemeChanged,
|
|
158
|
+
onSelectionChanged,
|
|
159
|
+
onLocaleChanged
|
|
160
|
+
}) {
|
|
161
|
+
const parentOrigin = parentOriginProp ?? getEnv("VITE_PARENT_ORIGIN", "NEXT_PUBLIC_PARENT_ORIGIN") ?? "*";
|
|
162
|
+
const apiUrl = apiUrlProp ?? getEnv("VITE_API_URL", "NEXT_PUBLIC_API_URL") ?? "http://localhost:4001/graphql";
|
|
163
|
+
if (parentOrigin === "*") {
|
|
164
|
+
console.error(
|
|
165
|
+
"[EditorSDK] No parentOrigin configured \u2014 the host<->editor channel is DISABLED for safety. Pass the `parentOrigin` prop or set VITE_PARENT_ORIGIN / NEXT_PUBLIC_PARENT_ORIGIN to the embedding origin."
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
const [isReady, setIsReady] = useState(false);
|
|
169
|
+
const [init, setInit] = useState(null);
|
|
170
|
+
const [theme, setTheme] = useState("light");
|
|
171
|
+
const [client, setClient] = useState(null);
|
|
172
|
+
const [configMode, setConfigMode] = useState("editor");
|
|
173
|
+
const [selectedFieldKeys, setSelectedFieldKeys] = useState([]);
|
|
174
|
+
const [selectableFields, setSelectableFields] = useState([]);
|
|
175
|
+
const [localeContext, setLocaleContext] = useState(
|
|
176
|
+
null
|
|
177
|
+
);
|
|
178
|
+
const onInitRef = useRef(onInit);
|
|
179
|
+
const onValuesChangedRef = useRef(onValuesChanged);
|
|
180
|
+
const onVariantChangedRef = useRef(onVariantChanged);
|
|
181
|
+
const onSaveResultRef = useRef(onSaveResult);
|
|
182
|
+
const onPublishResultRef = useRef(onPublishResult);
|
|
183
|
+
const onThemeChangedRef = useRef(onThemeChanged);
|
|
184
|
+
const onSelectionChangedRef = useRef(onSelectionChanged);
|
|
185
|
+
const onLocaleChangedRef = useRef(onLocaleChanged);
|
|
186
|
+
useEffect(() => {
|
|
187
|
+
onInitRef.current = onInit;
|
|
188
|
+
onValuesChangedRef.current = onValuesChanged;
|
|
189
|
+
onVariantChangedRef.current = onVariantChanged;
|
|
190
|
+
onSaveResultRef.current = onSaveResult;
|
|
191
|
+
onPublishResultRef.current = onPublishResult;
|
|
192
|
+
onThemeChangedRef.current = onThemeChanged;
|
|
193
|
+
onSelectionChangedRef.current = onSelectionChanged;
|
|
194
|
+
onLocaleChangedRef.current = onLocaleChanged;
|
|
195
|
+
}, [
|
|
196
|
+
onInit,
|
|
197
|
+
onValuesChanged,
|
|
198
|
+
onVariantChanged,
|
|
199
|
+
onSaveResult,
|
|
200
|
+
onPublishResult,
|
|
201
|
+
onThemeChanged,
|
|
202
|
+
onSelectionChanged,
|
|
203
|
+
onLocaleChanged
|
|
204
|
+
]);
|
|
205
|
+
const postMessage = useCallback(
|
|
206
|
+
(message) => {
|
|
207
|
+
if (typeof window === "undefined" || !window.parent) return;
|
|
208
|
+
if (parentOrigin === "*") return;
|
|
209
|
+
window.parent.postMessage(message, parentOrigin);
|
|
210
|
+
},
|
|
211
|
+
[parentOrigin]
|
|
212
|
+
);
|
|
213
|
+
const updateValues = useCallback(
|
|
214
|
+
(values) => {
|
|
215
|
+
postMessage({ type: "update-values", payload: { values } });
|
|
216
|
+
},
|
|
217
|
+
[postMessage]
|
|
218
|
+
);
|
|
219
|
+
const updateField = useCallback(
|
|
220
|
+
(path, value2) => {
|
|
221
|
+
postMessage({ type: "update-field", payload: { path, value: value2 } });
|
|
222
|
+
},
|
|
223
|
+
[postMessage]
|
|
224
|
+
);
|
|
225
|
+
const requestSave = useCallback(() => {
|
|
226
|
+
postMessage({ type: "request-save", payload: {} });
|
|
227
|
+
}, [postMessage]);
|
|
228
|
+
const requestPublish = useCallback(() => {
|
|
229
|
+
postMessage({ type: "request-publish", payload: {} });
|
|
230
|
+
}, [postMessage]);
|
|
231
|
+
const resize = useCallback(
|
|
232
|
+
(height) => {
|
|
233
|
+
postMessage({ type: "resize", payload: { height } });
|
|
234
|
+
},
|
|
235
|
+
[postMessage]
|
|
236
|
+
);
|
|
237
|
+
const setDirty = useCallback(
|
|
238
|
+
(isDirty) => {
|
|
239
|
+
postMessage({ type: "dirty", payload: { isDirty } });
|
|
240
|
+
},
|
|
241
|
+
[postMessage]
|
|
242
|
+
);
|
|
243
|
+
const requestNavigate = useCallback(
|
|
244
|
+
(path) => {
|
|
245
|
+
postMessage({ type: "request-navigate", payload: { path } });
|
|
246
|
+
},
|
|
247
|
+
[postMessage]
|
|
248
|
+
);
|
|
249
|
+
const pendingOperationsRef = useRef(/* @__PURE__ */ new Map());
|
|
250
|
+
const requestOperation = useCallback(
|
|
251
|
+
(operationKey, input, options) => {
|
|
252
|
+
const requestId = crypto.randomUUID();
|
|
253
|
+
return new Promise((resolve) => {
|
|
254
|
+
pendingOperationsRef.current.set(requestId, { resolve });
|
|
255
|
+
postMessage({
|
|
256
|
+
type: "request-operation",
|
|
257
|
+
payload: { operationKey, input, requestId, mode: options?.mode ?? "sync" }
|
|
258
|
+
});
|
|
259
|
+
setTimeout(() => {
|
|
260
|
+
if (pendingOperationsRef.current.has(requestId)) {
|
|
261
|
+
pendingOperationsRef.current.delete(requestId);
|
|
262
|
+
resolve({
|
|
263
|
+
success: false,
|
|
264
|
+
error: { code: "TIMEOUT", message: "Operation timed out" }
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
}, 6e4);
|
|
268
|
+
});
|
|
269
|
+
},
|
|
270
|
+
[postMessage]
|
|
271
|
+
);
|
|
272
|
+
useEffect(() => {
|
|
273
|
+
postMessage({ type: "ready", payload: {} });
|
|
274
|
+
}, [postMessage]);
|
|
275
|
+
useEffect(() => {
|
|
276
|
+
const handleMessage = (event) => {
|
|
277
|
+
if (parentOrigin === "*" || event.origin !== parentOrigin) return;
|
|
278
|
+
if (!isValidHostMessage(event.data)) return;
|
|
279
|
+
const message = event.data;
|
|
280
|
+
switch (message.type) {
|
|
281
|
+
case "init": {
|
|
282
|
+
const initData = message.payload;
|
|
283
|
+
const effectiveApiUrl = initData.apiUrl || apiUrl;
|
|
284
|
+
setIsReady(true);
|
|
285
|
+
setInit(initData);
|
|
286
|
+
setTheme(initData.theme);
|
|
287
|
+
setClient(
|
|
288
|
+
createPlatformClient({
|
|
289
|
+
apiUrl: effectiveApiUrl,
|
|
290
|
+
apiKey: initData.apiKey
|
|
291
|
+
})
|
|
292
|
+
);
|
|
293
|
+
if (initData.configMode) setConfigMode(initData.configMode);
|
|
294
|
+
if (initData.selectedFieldKeys)
|
|
295
|
+
setSelectedFieldKeys(initData.selectedFieldKeys);
|
|
296
|
+
if (initData.selectableFields)
|
|
297
|
+
setSelectableFields(initData.selectableFields);
|
|
298
|
+
if (initData.localeContext !== void 0)
|
|
299
|
+
setLocaleContext(initData.localeContext ?? null);
|
|
300
|
+
onInitRef.current?.(initData);
|
|
301
|
+
break;
|
|
302
|
+
}
|
|
303
|
+
case "values-changed":
|
|
304
|
+
onValuesChangedRef.current?.(message.payload.values);
|
|
305
|
+
break;
|
|
306
|
+
case "variant-changed":
|
|
307
|
+
onVariantChangedRef.current?.(
|
|
308
|
+
message.payload.variantKey,
|
|
309
|
+
message.payload.values
|
|
310
|
+
);
|
|
311
|
+
break;
|
|
312
|
+
case "save-result":
|
|
313
|
+
onSaveResultRef.current?.(
|
|
314
|
+
message.payload.success,
|
|
315
|
+
message.payload.error,
|
|
316
|
+
message.payload.versionId
|
|
317
|
+
);
|
|
318
|
+
break;
|
|
319
|
+
case "publish-result":
|
|
320
|
+
onPublishResultRef.current?.(
|
|
321
|
+
message.payload.success,
|
|
322
|
+
message.payload.error
|
|
323
|
+
);
|
|
324
|
+
break;
|
|
325
|
+
case "theme-changed":
|
|
326
|
+
setTheme(message.payload.theme);
|
|
327
|
+
onThemeChangedRef.current?.(message.payload.theme);
|
|
328
|
+
break;
|
|
329
|
+
case "selection-changed":
|
|
330
|
+
setSelectedFieldKeys(message.payload.selectedFieldKeys);
|
|
331
|
+
setSelectableFields(message.payload.fields);
|
|
332
|
+
onSelectionChangedRef.current?.(
|
|
333
|
+
message.payload.selectedFieldKeys,
|
|
334
|
+
message.payload.fields
|
|
335
|
+
);
|
|
336
|
+
break;
|
|
337
|
+
case "locale-changed":
|
|
338
|
+
setLocaleContext(message.payload);
|
|
339
|
+
onLocaleChangedRef.current?.(message.payload);
|
|
340
|
+
break;
|
|
341
|
+
case "operation-result": {
|
|
342
|
+
const { requestId, ...result } = message.payload;
|
|
343
|
+
const pending = pendingOperationsRef.current.get(requestId);
|
|
344
|
+
if (pending) {
|
|
345
|
+
pendingOperationsRef.current.delete(requestId);
|
|
346
|
+
pending.resolve(result);
|
|
347
|
+
}
|
|
348
|
+
break;
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
};
|
|
352
|
+
window.addEventListener("message", handleMessage);
|
|
353
|
+
return () => window.removeEventListener("message", handleMessage);
|
|
354
|
+
}, [parentOrigin, apiUrl]);
|
|
355
|
+
const noopClient = useMemo(
|
|
356
|
+
() => ({
|
|
357
|
+
async query() {
|
|
358
|
+
throw new Error(
|
|
359
|
+
"[EditorSDK] Client not ready \u2014 waiting for init from host."
|
|
360
|
+
);
|
|
361
|
+
}
|
|
362
|
+
}),
|
|
363
|
+
[]
|
|
364
|
+
);
|
|
365
|
+
const value = useMemo(
|
|
366
|
+
() => ({
|
|
367
|
+
isReady,
|
|
368
|
+
init,
|
|
369
|
+
theme,
|
|
370
|
+
client: client ?? noopClient,
|
|
371
|
+
updateValues,
|
|
372
|
+
updateField,
|
|
373
|
+
requestSave,
|
|
374
|
+
requestPublish,
|
|
375
|
+
resize,
|
|
376
|
+
setDirty,
|
|
377
|
+
requestNavigate,
|
|
378
|
+
requestOperation,
|
|
379
|
+
configMode,
|
|
380
|
+
selectedFieldKeys,
|
|
381
|
+
selectableFields,
|
|
382
|
+
localeContext
|
|
383
|
+
}),
|
|
384
|
+
[
|
|
385
|
+
isReady,
|
|
386
|
+
init,
|
|
387
|
+
theme,
|
|
388
|
+
client,
|
|
389
|
+
noopClient,
|
|
390
|
+
updateValues,
|
|
391
|
+
updateField,
|
|
392
|
+
requestSave,
|
|
393
|
+
requestPublish,
|
|
394
|
+
resize,
|
|
395
|
+
setDirty,
|
|
396
|
+
requestNavigate,
|
|
397
|
+
requestOperation,
|
|
398
|
+
configMode,
|
|
399
|
+
selectedFieldKeys,
|
|
400
|
+
selectableFields,
|
|
401
|
+
localeContext
|
|
402
|
+
]
|
|
403
|
+
);
|
|
404
|
+
return /* @__PURE__ */ jsx(EditorContext.Provider, { value, children });
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// src/hooks/useEditor.ts
|
|
408
|
+
import { useContext } from "react";
|
|
409
|
+
function useEditor() {
|
|
410
|
+
const ctx = useContext(EditorContext);
|
|
411
|
+
if (!ctx) {
|
|
412
|
+
throw new Error(
|
|
413
|
+
"[EditorSDK] useEditor must be used inside <EditorProvider>."
|
|
414
|
+
);
|
|
415
|
+
}
|
|
416
|
+
return ctx;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// src/hooks/useAutoResize.ts
|
|
420
|
+
import { useCallback as useCallback2, useEffect as useEffect2, useRef as useRef2 } from "react";
|
|
421
|
+
function useAutoResize(options = {}) {
|
|
422
|
+
const { minHeight = 400, debounce = 100, enabled = true } = options;
|
|
423
|
+
const { isReady, resize } = useEditor();
|
|
424
|
+
const observerRef = useRef2(null);
|
|
425
|
+
const timeoutRef = useRef2(
|
|
426
|
+
void 0
|
|
427
|
+
);
|
|
428
|
+
const elementRef = useRef2(null);
|
|
429
|
+
const cleanup = useCallback2(() => {
|
|
430
|
+
if (observerRef.current) {
|
|
431
|
+
observerRef.current.disconnect();
|
|
432
|
+
observerRef.current = null;
|
|
433
|
+
}
|
|
434
|
+
if (timeoutRef.current) {
|
|
435
|
+
clearTimeout(timeoutRef.current);
|
|
436
|
+
timeoutRef.current = void 0;
|
|
437
|
+
}
|
|
438
|
+
}, []);
|
|
439
|
+
const callbackRef = useCallback2(
|
|
440
|
+
(node) => {
|
|
441
|
+
cleanup();
|
|
442
|
+
elementRef.current = node;
|
|
443
|
+
if (!node || !enabled || !isReady) return;
|
|
444
|
+
const reportHeight = () => {
|
|
445
|
+
if (!elementRef.current) return;
|
|
446
|
+
const height = Math.max(elementRef.current.scrollHeight, minHeight);
|
|
447
|
+
resize(height);
|
|
448
|
+
};
|
|
449
|
+
reportHeight();
|
|
450
|
+
const observer = new ResizeObserver(() => {
|
|
451
|
+
if (timeoutRef.current) clearTimeout(timeoutRef.current);
|
|
452
|
+
timeoutRef.current = setTimeout(reportHeight, debounce);
|
|
453
|
+
});
|
|
454
|
+
observer.observe(node);
|
|
455
|
+
observerRef.current = observer;
|
|
456
|
+
},
|
|
457
|
+
[isReady, enabled, minHeight, debounce, resize, cleanup]
|
|
458
|
+
);
|
|
459
|
+
useEffect2(() => {
|
|
460
|
+
if (elementRef.current) {
|
|
461
|
+
callbackRef(elementRef.current);
|
|
462
|
+
}
|
|
463
|
+
}, [callbackRef]);
|
|
464
|
+
useEffect2(() => cleanup, [cleanup]);
|
|
465
|
+
return callbackRef;
|
|
466
|
+
}
|
|
467
|
+
export {
|
|
468
|
+
EditorProvider,
|
|
469
|
+
PROTOCOL_VERSION,
|
|
470
|
+
PlatformClientError,
|
|
471
|
+
createPlatformClient,
|
|
472
|
+
isValidEditorMessage,
|
|
473
|
+
isValidHostMessage,
|
|
474
|
+
useAutoResize,
|
|
475
|
+
useEditor
|
|
476
|
+
};
|
package/dist/media.d.ts
ADDED
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Foir media types + adapters.
|
|
5
|
+
*
|
|
6
|
+
* Framework-agnostic plumbing for rendering Foir image/video fields with
|
|
7
|
+
* `<ProgressiveMedia>`: the canonical source shapes, GraphQL-value → source
|
|
8
|
+
* adapters (crop normalization, per-format variant sets, HLS URL
|
|
9
|
+
* absolutising), and crop-aware dimension maths for CLS-free layout.
|
|
10
|
+
*
|
|
11
|
+
* HLS manifest URLs are absolutised via the shared `absolutiseFoirMediaUrl`
|
|
12
|
+
* from the client transport — not re-implemented here.
|
|
13
|
+
*/
|
|
14
|
+
/** One rendered tier: a URL + its TRUE (crop-aware) dims (for srcset `w` + CLS). */
|
|
15
|
+
interface VariantTier {
|
|
16
|
+
url: string;
|
|
17
|
+
width?: number | null;
|
|
18
|
+
height?: number | null;
|
|
19
|
+
}
|
|
20
|
+
/** Curated tier set in one format (Foir `ImageVariantSet`). */
|
|
21
|
+
interface VariantSet {
|
|
22
|
+
thumbnail?: VariantTier | null;
|
|
23
|
+
small?: VariantTier | null;
|
|
24
|
+
medium?: VariantTier | null;
|
|
25
|
+
large?: VariantTier | null;
|
|
26
|
+
xlarge?: VariantTier | null;
|
|
27
|
+
_2xl?: VariantTier | null;
|
|
28
|
+
_3xl?: VariantTier | null;
|
|
29
|
+
_4xl?: VariantTier | null;
|
|
30
|
+
}
|
|
31
|
+
/** A `VariantSet` tier name. */
|
|
32
|
+
type VariantTierKey = keyof VariantSet;
|
|
33
|
+
/**
|
|
34
|
+
* Default tier order, smallest→largest, for walking a `VariantSet`
|
|
35
|
+
* deterministically when building a `srcset`. Override per `<ProgressiveMedia>`
|
|
36
|
+
* via the `variantTiers` prop when a project curates a different tier set.
|
|
37
|
+
*/
|
|
38
|
+
declare const DEFAULT_VARIANT_TIERS: readonly VariantTierKey[];
|
|
39
|
+
/** Image source with progressive loading data. */
|
|
40
|
+
interface ProgressiveImageSource {
|
|
41
|
+
url: string;
|
|
42
|
+
alt?: string;
|
|
43
|
+
width?: number;
|
|
44
|
+
height?: number;
|
|
45
|
+
blurhash?: string;
|
|
46
|
+
dominantColor?: string;
|
|
47
|
+
thumbnailUrl?: string;
|
|
48
|
+
/** Per-format tier sets (WebP universal; AVIF preferred via `<picture>`). */
|
|
49
|
+
webp?: VariantSet | null;
|
|
50
|
+
avif?: VariantSet | null;
|
|
51
|
+
/**
|
|
52
|
+
* Optional crop rectangle expressed as percentages of the source image
|
|
53
|
+
* (`{x, y, width, height}` in 0–100). Foir's image pipeline serves the
|
|
54
|
+
* cropped file at the cropped dimensions, so consumers need this to compute
|
|
55
|
+
* correct `width`/`height` attrs for CLS-free rendering.
|
|
56
|
+
*/
|
|
57
|
+
crop?: {
|
|
58
|
+
x?: number;
|
|
59
|
+
y?: number;
|
|
60
|
+
width?: number;
|
|
61
|
+
height?: number;
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
/** Video source with progressive loading data + optional HLS. */
|
|
65
|
+
interface ProgressiveVideoSource {
|
|
66
|
+
url: string;
|
|
67
|
+
width?: number;
|
|
68
|
+
height?: number;
|
|
69
|
+
blurhash?: string;
|
|
70
|
+
dominantColor?: string;
|
|
71
|
+
thumbnailUrl?: string;
|
|
72
|
+
previewUrl?: string;
|
|
73
|
+
hlsUrl?: string;
|
|
74
|
+
duration?: number;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Foir GraphQL `ImageValue`-like input — the shape a consumer's codegen
|
|
78
|
+
* produces for an image field (intrinsic dims + per-format variant sets +
|
|
79
|
+
* crop as opaque JSON). Structurally compatible with the generated type.
|
|
80
|
+
*/
|
|
81
|
+
interface FoirImageInput {
|
|
82
|
+
url?: string | null;
|
|
83
|
+
alt?: string | null;
|
|
84
|
+
width?: number | null;
|
|
85
|
+
height?: number | null;
|
|
86
|
+
blurhash?: string | null;
|
|
87
|
+
dominantColor?: string | null;
|
|
88
|
+
webp?: VariantSet | null;
|
|
89
|
+
avif?: VariantSet | null;
|
|
90
|
+
crop?: {
|
|
91
|
+
[key: string]: unknown;
|
|
92
|
+
} | null;
|
|
93
|
+
}
|
|
94
|
+
/** Foir GraphQL `VideoValue`-like input. */
|
|
95
|
+
interface FoirVideoInput {
|
|
96
|
+
url?: string | null;
|
|
97
|
+
width?: number | null;
|
|
98
|
+
height?: number | null;
|
|
99
|
+
hlsManifestUrl?: string | null;
|
|
100
|
+
thumbnailUrl?: string | null;
|
|
101
|
+
previewUrl?: string | null;
|
|
102
|
+
blurhash?: string | null;
|
|
103
|
+
dominantColor?: string | null;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Adapt a Foir image field to the `ProgressiveImageSource` consumed by
|
|
107
|
+
* `<ProgressiveMedia>`. Returns `undefined` when the field is empty (the
|
|
108
|
+
* caller falls back to a placeholder). Narrows the JSON-typed `crop` to the
|
|
109
|
+
* numeric keys; out-of-range values are filtered in `effectiveImageDimensions`.
|
|
110
|
+
*/
|
|
111
|
+
declare function toProgressiveImage(img: FoirImageInput | null | undefined): ProgressiveImageSource | undefined;
|
|
112
|
+
/**
|
|
113
|
+
* Adapt a Foir video field to the `ProgressiveVideoSource` consumed by
|
|
114
|
+
* `<ProgressiveMedia>`. Absolutises the HLS manifest URL (Foir returns it
|
|
115
|
+
* relative to its media proxy). Returns `undefined` when the field is empty.
|
|
116
|
+
*/
|
|
117
|
+
declare function toProgressiveVideo(v: FoirVideoInput | null | undefined): ProgressiveVideoSource | undefined;
|
|
118
|
+
/**
|
|
119
|
+
* Resolve the dimensions of the file Foir's pipeline will actually serve,
|
|
120
|
+
* applying any crop percentages to the source's intrinsic size. Returns null
|
|
121
|
+
* when the source has no width/height. Drives both the wrapper aspect-ratio
|
|
122
|
+
* and the `<img>` `width`/`height` attrs.
|
|
123
|
+
*/
|
|
124
|
+
declare function effectiveImageDimensions(image: {
|
|
125
|
+
width?: number | null;
|
|
126
|
+
height?: number | null;
|
|
127
|
+
crop?: unknown;
|
|
128
|
+
} | null | undefined): {
|
|
129
|
+
width: number;
|
|
130
|
+
height: number;
|
|
131
|
+
} | null;
|
|
132
|
+
/**
|
|
133
|
+
* Compute a CSS `aspect-ratio` string from a Foir image's natural dimensions
|
|
134
|
+
* and optional crop. When a usable crop is set the cropped rectangle's aspect
|
|
135
|
+
* wins; otherwise the intrinsic width/height; otherwise `null`.
|
|
136
|
+
*/
|
|
137
|
+
declare function cropAspectRatio(image: {
|
|
138
|
+
width?: number | null;
|
|
139
|
+
height?: number | null;
|
|
140
|
+
crop?: unknown;
|
|
141
|
+
} | null | undefined): string | null;
|
|
142
|
+
|
|
143
|
+
interface ProgressiveMediaProps {
|
|
144
|
+
image?: ProgressiveImageSource;
|
|
145
|
+
video?: ProgressiveVideoSource;
|
|
146
|
+
alt?: string;
|
|
147
|
+
className?: string;
|
|
148
|
+
/** Fill the parent container instead of using intrinsic aspect ratio. */
|
|
149
|
+
fill?: boolean;
|
|
150
|
+
/**
|
|
151
|
+
* Object-fit for fill mode. Defaults to 'cover'. Has no effect when `fill`
|
|
152
|
+
* is false (the image renders at its intrinsic aspect ratio).
|
|
153
|
+
*/
|
|
154
|
+
fit?: 'cover' | 'contain';
|
|
155
|
+
/**
|
|
156
|
+
* Solid-colour placeholder mode. When neither image nor video is provided,
|
|
157
|
+
* render a flat coloured div. `aspectRatio` defaults to 16/10.
|
|
158
|
+
*/
|
|
159
|
+
placeholder?: {
|
|
160
|
+
color: string;
|
|
161
|
+
aspectRatio?: number;
|
|
162
|
+
};
|
|
163
|
+
/** Video playback — autoplay on mount. Default true. */
|
|
164
|
+
autoplay?: boolean;
|
|
165
|
+
/** Video playback — muted. Default true (required for autoplay). */
|
|
166
|
+
muted?: boolean;
|
|
167
|
+
/** Video playback — loop on end. Default true. */
|
|
168
|
+
loop?: boolean;
|
|
169
|
+
/**
|
|
170
|
+
* When false, skip loading the full media layer (no `<video>`/full `<img>`
|
|
171
|
+
* mount, no HLS attach, no full-image preload). The lower layers — colour,
|
|
172
|
+
* blurhash, thumbnail — still render. Used by responsive dual-render so the
|
|
173
|
+
* CSS-hidden branch doesn't download its full asset. Default true.
|
|
174
|
+
*/
|
|
175
|
+
active?: boolean;
|
|
176
|
+
/**
|
|
177
|
+
* `sizes` attribute applied to the full `<img>` — tells the browser how
|
|
178
|
+
* wide the image renders so it can pick the right `srcset` entry. Default
|
|
179
|
+
* `"100vw"`. Has no effect on video sources.
|
|
180
|
+
*/
|
|
181
|
+
sizes?: string;
|
|
182
|
+
/**
|
|
183
|
+
* When true, treat this media as the page's LCP candidate: thumbnail and
|
|
184
|
+
* full `<img>` get `fetchpriority="high"` + `loading="eager"`, and any
|
|
185
|
+
* `<video>` mounts immediately. When false (default) below-fold behaviour:
|
|
186
|
+
* `loading="lazy"` on `<img>`, and `<video>` is gated by an
|
|
187
|
+
* IntersectionObserver so its `src`/HLS only attaches near the viewport.
|
|
188
|
+
*/
|
|
189
|
+
priority?: boolean;
|
|
190
|
+
/**
|
|
191
|
+
* Tier order, smallest→largest, used to build the `srcset` from a
|
|
192
|
+
* `VariantSet`. Defaults to {@link DEFAULT_VARIANT_TIERS}; override when a
|
|
193
|
+
* project curates a different tier set.
|
|
194
|
+
*/
|
|
195
|
+
variantTiers?: readonly VariantTierKey[];
|
|
196
|
+
}
|
|
197
|
+
declare function ProgressiveMedia({ image, video, alt, className, fill, fit, placeholder, autoplay, muted, loop, active, sizes, priority, variantTiers, }: ProgressiveMediaProps): react_jsx_runtime.JSX.Element | null;
|
|
198
|
+
|
|
199
|
+
interface BlurhashCanvasProps {
|
|
200
|
+
hash: string;
|
|
201
|
+
className?: string;
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Decodes a BlurHash string to a 32×32 canvas. `blurhash` is an optional peer
|
|
205
|
+
* dependency loaded via dynamic import — if it isn't installed the canvas
|
|
206
|
+
* stays blank rather than throwing.
|
|
207
|
+
*/
|
|
208
|
+
declare function BlurhashCanvas({ hash, className }: BlurhashCanvasProps): react_jsx_runtime.JSX.Element;
|
|
209
|
+
|
|
210
|
+
type LoadStage = 'color' | 'blurhash' | 'thumbnail' | 'full';
|
|
211
|
+
interface UseProgressiveLoadOptions {
|
|
212
|
+
blurhash?: string;
|
|
213
|
+
thumbnailUrl?: string;
|
|
214
|
+
fullUrl?: string;
|
|
215
|
+
/**
|
|
216
|
+
* Optional srcset + sizes — when provided to image sources, the preloader
|
|
217
|
+
* matches the variant the visible `<img>` will pick, so the browser dedupes
|
|
218
|
+
* the fetch instead of downloading two sizes.
|
|
219
|
+
*/
|
|
220
|
+
fullSrcSet?: string;
|
|
221
|
+
fullSizes?: string;
|
|
222
|
+
isVideo?: boolean;
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* State machine for progressive media loading.
|
|
226
|
+
* Stages advance forward only: color → blurhash → thumbnail → full.
|
|
227
|
+
* Skips stages when data is missing. Never regresses.
|
|
228
|
+
*/
|
|
229
|
+
declare function useProgressiveLoad({ blurhash, thumbnailUrl, fullUrl, fullSrcSet, fullSizes, isVideo, }: UseProgressiveLoadOptions): {
|
|
230
|
+
stage: LoadStage;
|
|
231
|
+
onFullReady: () => void;
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
export { BlurhashCanvas, type BlurhashCanvasProps, DEFAULT_VARIANT_TIERS, type FoirImageInput, type FoirVideoInput, type LoadStage, type ProgressiveImageSource, ProgressiveMedia, type ProgressiveMediaProps, type ProgressiveVideoSource, type UseProgressiveLoadOptions, type VariantSet, type VariantTier, type VariantTierKey, cropAspectRatio, effectiveImageDimensions, toProgressiveImage, toProgressiveVideo, useProgressiveLoad };
|