@joai/react 1.0.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/dist/index.mjs ADDED
@@ -0,0 +1,1788 @@
1
+ 'use client';
2
+ import { createContext, useCallback, useContext, useEffect, useId, useMemo, useRef, useState, useSyncExternalStore } from "react";
3
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
4
+ import { containedImageDimensions, containedImageDimensions as containedImageDimensions$1, cropOutputDimensions, cropOutputDimensions as cropOutputDimensions$1, getLoginUrl, imageCropRectangle, imageCropRectangle as imageCropRectangle$1, validateImageDimensions } from "@joai/core";
5
+ import { createClient, getAuthFragmentToken } from "@joai/sdk";
6
+ //#region src/guest.ts
7
+ function safeGet(key) {
8
+ try {
9
+ return localStorage.getItem(key);
10
+ } catch {
11
+ return null;
12
+ }
13
+ }
14
+ function safeSet(key, value) {
15
+ try {
16
+ localStorage.setItem(key, value);
17
+ } catch {}
18
+ }
19
+ function safeRemove(key) {
20
+ try {
21
+ localStorage.removeItem(key);
22
+ } catch {}
23
+ }
24
+ function getOrCreateGuestId(storageKey = "joai_embed_guest") {
25
+ if (typeof window === "undefined") return "";
26
+ const existing = safeGet(storageKey);
27
+ if (existing) return existing;
28
+ const id = crypto.randomUUID();
29
+ safeSet(storageKey, id);
30
+ return id;
31
+ }
32
+ function loadRoom(storageKey) {
33
+ return safeGet(storageKey);
34
+ }
35
+ function saveRoom(storageKey, roomId) {
36
+ safeSet(storageKey, roomId);
37
+ }
38
+ function clearRoom(storageKey) {
39
+ safeRemove(storageKey);
40
+ }
41
+ //#endregion
42
+ //#region src/stream-connection.ts
43
+ function openEventSourceStream(url, onData, options) {
44
+ const es = new EventSource(url, { withCredentials: options?.withCredentials ?? false });
45
+ es.onmessage = (event) => {
46
+ try {
47
+ onData(JSON.parse(event.data));
48
+ } catch (e) {
49
+ options?.onError?.(e);
50
+ }
51
+ };
52
+ es.onerror = (event) => {
53
+ options?.onConnectionError?.(event);
54
+ };
55
+ return () => es.close();
56
+ }
57
+ //#endregion
58
+ //#region src/components.tsx
59
+ function EmbedChatList({ items, agentName, awaitingResponse, classNames = {}, renderEmptyState, renderAgentContent }) {
60
+ const containerRef = useRef(null);
61
+ useEffect(() => {
62
+ const el = containerRef.current;
63
+ if (!el) return;
64
+ if (el.scrollHeight - el.scrollTop - el.clientHeight < 120) el.scrollTop = el.scrollHeight;
65
+ }, [items, awaitingResponse]);
66
+ if (items.length === 0 && !awaitingResponse) return renderEmptyState ? renderEmptyState() : /* @__PURE__ */ jsx("div", {
67
+ className: classNames.container,
68
+ children: /* @__PURE__ */ jsxs("p", { children: [
69
+ "Send ",
70
+ agentName,
71
+ " a message to start chatting"
72
+ ] })
73
+ });
74
+ return /* @__PURE__ */ jsxs("div", {
75
+ ref: containerRef,
76
+ className: classNames.container,
77
+ role: "log",
78
+ "aria-live": "polite",
79
+ children: [items.map((item, i) => /* @__PURE__ */ jsx(EmbedChatBubble, {
80
+ item,
81
+ classNames,
82
+ renderAgentContent
83
+ }, i)), awaitingResponse && /* @__PURE__ */ jsx(EmbedThinkingIndicator, {
84
+ wrapperClassName: classNames.thinkingWrapper,
85
+ dotClassName: classNames.dot
86
+ })]
87
+ });
88
+ }
89
+ function EmbedChatBubble({ item, classNames, renderAgentContent }) {
90
+ if (item.byUser) return /* @__PURE__ */ jsx("div", {
91
+ className: classNames.userBubble,
92
+ children: item.content
93
+ });
94
+ return /* @__PURE__ */ jsx("div", {
95
+ className: item.error ? classNames.errorBubble : classNames.agentBubble,
96
+ children: renderAgentContent ? renderAgentContent(item) : /* @__PURE__ */ jsxs(Fragment, { children: [item.content, item.streaming && /* @__PURE__ */ jsx("span", { className: "inline-block w-1 h-3 ml-1 bg-current animate-pulse rounded-full" })] })
97
+ });
98
+ }
99
+ function EmbedThinkingIndicator({ wrapperClassName, dotClassName }) {
100
+ return /* @__PURE__ */ jsx("div", {
101
+ className: wrapperClassName,
102
+ children: /* @__PURE__ */ jsxs("span", {
103
+ className: "inline-flex gap-1",
104
+ children: [
105
+ /* @__PURE__ */ jsx("span", {
106
+ className: dotClassName,
107
+ style: { animationDelay: "0ms" }
108
+ }),
109
+ /* @__PURE__ */ jsx("span", {
110
+ className: dotClassName,
111
+ style: { animationDelay: "150ms" }
112
+ }),
113
+ /* @__PURE__ */ jsx("span", {
114
+ className: dotClassName,
115
+ style: { animationDelay: "300ms" }
116
+ })
117
+ ]
118
+ })
119
+ });
120
+ }
121
+ function EmbedChatInput({ onSend, disabled, placeholder = "Type a message...", maxLength = 4e3, classNames = {}, renderSendButton }) {
122
+ const [text, setText] = useState("");
123
+ const textareaRef = useRef(null);
124
+ function handleSend() {
125
+ const trimmed = text.trim();
126
+ if (!trimmed || disabled) return;
127
+ onSend(trimmed);
128
+ setText("");
129
+ if (textareaRef.current) textareaRef.current.style.height = "auto";
130
+ }
131
+ function handleKeyDown(e) {
132
+ if (e.key === "Enter" && !e.shiftKey) {
133
+ e.preventDefault();
134
+ handleSend();
135
+ }
136
+ }
137
+ function handleInput() {
138
+ if (textareaRef.current) {
139
+ textareaRef.current.style.height = "auto";
140
+ textareaRef.current.style.height = Math.min(textareaRef.current.scrollHeight, 120) + "px";
141
+ }
142
+ }
143
+ return /* @__PURE__ */ jsxs("div", {
144
+ className: classNames.container,
145
+ children: [/* @__PURE__ */ jsx("textarea", {
146
+ ref: textareaRef,
147
+ value: text,
148
+ onChange: (e) => setText(e.target.value),
149
+ onKeyDown: handleKeyDown,
150
+ onInput: handleInput,
151
+ placeholder,
152
+ "aria-label": "Message input",
153
+ maxLength,
154
+ rows: 1,
155
+ disabled,
156
+ className: classNames.textarea
157
+ }), renderSendButton({
158
+ onClick: handleSend,
159
+ disabled: disabled || !text.trim()
160
+ })]
161
+ });
162
+ }
163
+ //#endregion
164
+ //#region src/use-embed-chat.ts
165
+ const RESPONSE_TIMEOUT_MS = 3e4;
166
+ function useEmbedChat({ agentUuid, http, cortexBaseUrl, guestId, roomStorageKey, enabled = true, extraPayload }) {
167
+ const [items, setItems] = useState([]);
168
+ const [sending, setSending] = useState(false);
169
+ const [awaitingResponse, setAwaitingResponse] = useState(false);
170
+ const [roomId, setRoomId] = useState(null);
171
+ const [connectionError, setConnectionError] = useState(false);
172
+ const roomIdRef = useRef(null);
173
+ roomIdRef.current = roomId;
174
+ const extraPayloadRef = useRef(extraPayload);
175
+ extraPayloadRef.current = extraPayload;
176
+ useEffect(() => {
177
+ if (!roomId) return;
178
+ setConnectionError(false);
179
+ return openEventSourceStream(`${cortexBaseUrl}/agents/stream/${roomId}`, (raw) => {
180
+ setConnectionError(false);
181
+ const data = raw;
182
+ if (data.type === "response:chunk") {
183
+ setAwaitingResponse(false);
184
+ setItems((prev) => {
185
+ const last = prev[prev.length - 1];
186
+ if (last && last.streaming && !last.byUser) {
187
+ const updated = [...prev];
188
+ updated[updated.length - 1] = {
189
+ ...last,
190
+ content: last.content + (data.content ?? "")
191
+ };
192
+ return updated;
193
+ }
194
+ return [...prev, {
195
+ content: data.content ?? "",
196
+ byUser: false,
197
+ streaming: true
198
+ }];
199
+ });
200
+ } else if (data.type === "response") {
201
+ setAwaitingResponse(false);
202
+ setItems((prev) => {
203
+ const updated = [...prev];
204
+ const lastIdx = updated.length - 1;
205
+ if (lastIdx >= 0 && updated[lastIdx].streaming) updated[lastIdx] = {
206
+ ...updated[lastIdx],
207
+ content: data.content ?? updated[lastIdx].content,
208
+ streaming: false
209
+ };
210
+ else updated.push({
211
+ content: data.content ?? "",
212
+ byUser: false
213
+ });
214
+ return updated;
215
+ });
216
+ } else if (data.type === "complete") {
217
+ setAwaitingResponse(false);
218
+ setItems((prev) => prev.map((item) => item.streaming ? {
219
+ ...item,
220
+ streaming: false
221
+ } : item));
222
+ }
223
+ }, { onConnectionError: () => setConnectionError(true) });
224
+ }, [roomId, cortexBaseUrl]);
225
+ useEffect(() => {
226
+ if (!awaitingResponse) return;
227
+ const timer = setTimeout(() => {
228
+ setAwaitingResponse(false);
229
+ setItems((prev) => [...prev, {
230
+ content: "Response timed out. Please try again.",
231
+ byUser: false,
232
+ error: true
233
+ }]);
234
+ }, RESPONSE_TIMEOUT_MS);
235
+ return () => clearTimeout(timer);
236
+ }, [awaitingResponse]);
237
+ return {
238
+ items,
239
+ setItems,
240
+ sending,
241
+ awaitingResponse,
242
+ roomId,
243
+ setRoomId,
244
+ send: useCallback(async (message) => {
245
+ if (!enabled || sending || !agentUuid) return;
246
+ setItems((prev) => [...prev, {
247
+ content: message,
248
+ byUser: true
249
+ }]);
250
+ setSending(true);
251
+ setConnectionError(false);
252
+ try {
253
+ const res = await http.post(`integrations/embed/${agentUuid}/execute`, {
254
+ message,
255
+ guestId,
256
+ ...extraPayloadRef.current
257
+ });
258
+ if (!res.ok) {
259
+ const msg = res.data && typeof res.data === "object" && "message" in res.data ? res.data.message : "Something went wrong. Please try again.";
260
+ setItems((prev) => [...prev, {
261
+ content: msg,
262
+ byUser: false,
263
+ error: true
264
+ }]);
265
+ return;
266
+ }
267
+ setAwaitingResponse(true);
268
+ const newRoomId = res.data?.roomId;
269
+ if (newRoomId && !roomIdRef.current) {
270
+ setRoomId(newRoomId);
271
+ saveRoom(roomStorageKey, newRoomId);
272
+ }
273
+ } catch {
274
+ setItems((prev) => [...prev, {
275
+ content: "Something went wrong. Please try again.",
276
+ byUser: false,
277
+ error: true
278
+ }]);
279
+ } finally {
280
+ setSending(false);
281
+ }
282
+ }, [
283
+ enabled,
284
+ sending,
285
+ agentUuid,
286
+ http,
287
+ guestId,
288
+ roomStorageKey
289
+ ]),
290
+ connectionError
291
+ };
292
+ }
293
+ //#endregion
294
+ //#region src/content/context.ts
295
+ const ContentContext = createContext(null);
296
+ function useContent() {
297
+ const context = useContext(ContentContext);
298
+ if (!context) throw new Error("useContent must be used within a ContentProvider.");
299
+ return context;
300
+ }
301
+ //#endregion
302
+ //#region src/content/content-cache.ts
303
+ const contentCache = /* @__PURE__ */ new Map();
304
+ const pendingLoads = /* @__PURE__ */ new Map();
305
+ function cacheContent(content) {
306
+ contentCache.set(`id:${content.id}`, content);
307
+ contentCache.set(`slug:${content.slug}`, content);
308
+ }
309
+ async function loadContent(client, contentId, publicSlug) {
310
+ const target = contentId ?? publicSlug;
311
+ if (!target) return null;
312
+ const key = contentId ? `id:${contentId}` : `slug:${publicSlug}`;
313
+ const cached = contentCache.get(key);
314
+ if (cached) return cached;
315
+ const pending = pendingLoads.get(key);
316
+ if (pending) return pending;
317
+ const request = (contentId ? client.content.get(target) : client.content.getPublic(target)).then((result) => result.ok && result.data ? result.data : null).then((content) => {
318
+ if (content) cacheContent(content);
319
+ return content;
320
+ }).finally(() => pendingLoads.delete(key));
321
+ pendingLoads.set(key, request);
322
+ return request;
323
+ }
324
+ //#endregion
325
+ //#region src/content/state.ts
326
+ function contentValues(content) {
327
+ return Object.fromEntries(content?.version?.entries.map((entry) => [entry.key, entry.value]) ?? []);
328
+ }
329
+ function contentPatches(content, values) {
330
+ const original = contentValues(content);
331
+ return Object.entries(values).filter(([key, value]) => JSON.stringify(value) !== JSON.stringify(original[key])).map(([key, value]) => ({
332
+ key,
333
+ value
334
+ }));
335
+ }
336
+ //#endregion
337
+ //#region src/content/use-unsaved-navigation.ts
338
+ const navigationApi = () => {
339
+ if (typeof window === "undefined") return null;
340
+ return window.navigation ?? null;
341
+ };
342
+ const useUnsavedNavigation = (shouldBlock, confirmLeave) => {
343
+ const allowed = useRef(false);
344
+ useEffect(() => {
345
+ if (!shouldBlock || typeof window === "undefined" || typeof document === "undefined") return;
346
+ allowed.current = false;
347
+ const beforeUnload = (event) => {
348
+ if (allowed.current) return;
349
+ event.preventDefault();
350
+ event.returnValue = "";
351
+ };
352
+ const confirmNavigation = () => {
353
+ if (!confirmLeave()) return false;
354
+ allowed.current = true;
355
+ return true;
356
+ };
357
+ const navigation = navigationApi();
358
+ const navigate = ((event) => {
359
+ if (allowed.current || event.hashChange || !event.cancelable) return;
360
+ const destination = event.destination?.url;
361
+ if (destination) {
362
+ const current = new URL(window.location.href);
363
+ const next = new URL(destination, current);
364
+ if (current.origin === next.origin && current.pathname === next.pathname && current.search === next.search) return;
365
+ }
366
+ if (!confirmNavigation()) event.preventDefault();
367
+ });
368
+ const click = (event) => {
369
+ if (allowed.current || event.defaultPrevented || event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return;
370
+ const target = event.target instanceof Element ? event.target.closest("a[href]") : null;
371
+ if (!target || target.target === "_blank" || target.hasAttribute("download")) return;
372
+ if (target.hasAttribute("data-content-navigation-allowed")) {
373
+ allowed.current = true;
374
+ return;
375
+ }
376
+ const current = new URL(window.location.href);
377
+ const next = new URL(target.href, current);
378
+ if (current.origin === next.origin && current.pathname === next.pathname && current.search === next.search) return;
379
+ if (!confirmNavigation()) {
380
+ event.preventDefault();
381
+ event.stopImmediatePropagation();
382
+ }
383
+ };
384
+ window.addEventListener("beforeunload", beforeUnload);
385
+ if (navigation) navigation.addEventListener("navigate", navigate);
386
+ else document.addEventListener("click", click, true);
387
+ return () => {
388
+ window.removeEventListener("beforeunload", beforeUnload);
389
+ if (navigation) navigation.removeEventListener("navigate", navigate);
390
+ else document.removeEventListener("click", click, true);
391
+ };
392
+ }, [shouldBlock]);
393
+ };
394
+ //#endregion
395
+ //#region src/content/provider.tsx
396
+ const DEFAULT_MESSAGES = {
397
+ conflict: "This content changed elsewhere. Your edits are preserved on the latest version. Review them and try again.",
398
+ loadError: "Content could not be loaded.",
399
+ publishError: "Changes could not be published. Try again.",
400
+ published: "Changes published.",
401
+ saveError: "Changes could not be saved. Try again.",
402
+ saved: "Draft saved.",
403
+ sessionExpired: "Your session expired. Sign in again to continue.",
404
+ unsavedNavigation: "You have unsaved changes. Do you really want to leave this page?"
405
+ };
406
+ const DEFAULT_IMAGE_PICKER_LABELS = {
407
+ applyCrop: "Apply crop",
408
+ back: "Back",
409
+ choose: "Choose image",
410
+ close: "Close",
411
+ cropDescription: "Adjust the position and zoom before uploading.",
412
+ cropTitle: "Crop image",
413
+ dimensionsTooLarge: "The image dimensions are too large.",
414
+ dimensionsTooSmall: "The image dimensions are too small.",
415
+ drop: "Drop an image here",
416
+ empty: "No images are available yet.",
417
+ field: "Editable image",
418
+ invalidType: "Choose an image file.",
419
+ libraryDescription: "Choose an existing image or upload a new one.",
420
+ libraryTitle: "Image library",
421
+ loadError: "Images could not be loaded.",
422
+ loading: "Loading images…",
423
+ positionHorizontal: "Horizontal position",
424
+ positionVertical: "Vertical position",
425
+ processing: "Preparing image…",
426
+ retry: "Try again",
427
+ tooLarge: "The image is too large.",
428
+ upload: "Upload image",
429
+ uploadError: "The image could not be uploaded.",
430
+ uploading: "Uploading…",
431
+ zoom: "Zoom"
432
+ };
433
+ const isContentConflict = (result) => {
434
+ return result.original?.status === 409 || Boolean(result.errors && "expectedVersion" in result.errors);
435
+ };
436
+ const isUnauthorized = (result) => result.original?.status === 401 || result.original?.status === 403;
437
+ const valuesWithDraft = (source, canEdit, contentId, draftStore) => {
438
+ const values = contentValues(source);
439
+ if (!canEdit || !draftStore) return values;
440
+ try {
441
+ const draft = draftStore.get();
442
+ if (!draft || draft.contentId && contentId && draft.contentId !== contentId) return values;
443
+ return {
444
+ ...values,
445
+ ...draft.values
446
+ };
447
+ } catch {
448
+ return values;
449
+ }
450
+ };
451
+ const clearDraft = (draftStore) => {
452
+ try {
453
+ draftStore?.clear();
454
+ } catch {}
455
+ };
456
+ const persistDraft = (draftStore, contentId, values) => {
457
+ try {
458
+ draftStore?.set({
459
+ contentId,
460
+ values
461
+ });
462
+ } catch {}
463
+ };
464
+ function ContentProvider({ children, client, contentId, publicSlug, initialContent, canEdit = false, canPublish = false, teamSlug, confirmLeave, draftStore, imagePickerLabels: imagePickerLabelOverrides, messages: messageOverrides, onUnauthorized }) {
465
+ const messages = {
466
+ ...DEFAULT_MESSAGES,
467
+ ...messageOverrides
468
+ };
469
+ const imagePickerLabels = {
470
+ ...DEFAULT_IMAGE_PICKER_LABELS,
471
+ ...imagePickerLabelOverrides
472
+ };
473
+ const [content, setContent] = useState(initialContent ?? null);
474
+ const [values, setValues] = useState(() => valuesWithDraft(initialContent ?? null, canEdit, contentId, draftStore));
475
+ const [isLoading, setIsLoading] = useState(!initialContent && Boolean(contentId || publicSlug));
476
+ const [operation, setOperation] = useState(null);
477
+ const [isEditing, setEditing] = useState(false);
478
+ const [error, setError] = useState(null);
479
+ const [message, setMessage] = useState(null);
480
+ useEffect(() => {
481
+ if (initialContent) {
482
+ cacheContent(initialContent);
483
+ setContent(initialContent);
484
+ setValues(valuesWithDraft(initialContent, canEdit, contentId, draftStore));
485
+ setIsLoading(false);
486
+ return;
487
+ }
488
+ const load = async () => {
489
+ if (!contentId && !publicSlug) return;
490
+ setIsLoading(true);
491
+ setError(null);
492
+ setMessage(null);
493
+ try {
494
+ const nextContent = await loadContent(client, contentId, publicSlug);
495
+ if (!nextContent) setError(messages.loadError);
496
+ else {
497
+ setContent(nextContent);
498
+ setValues(valuesWithDraft(nextContent, canEdit, contentId, draftStore));
499
+ }
500
+ } catch {
501
+ setError(messages.loadError);
502
+ } finally {
503
+ setIsLoading(false);
504
+ }
505
+ };
506
+ load();
507
+ }, [
508
+ contentId,
509
+ initialContent?.id,
510
+ initialContent?.version?.number,
511
+ publicSlug
512
+ ]);
513
+ const setValue = useCallback((key, value) => {
514
+ setError(null);
515
+ setMessage(null);
516
+ setValues((current) => ({
517
+ ...current,
518
+ [key]: value
519
+ }));
520
+ }, []);
521
+ const updateEditing = useCallback((editing) => {
522
+ if (editing) {
523
+ setError(null);
524
+ setMessage(null);
525
+ }
526
+ setEditing(editing);
527
+ }, []);
528
+ const isDirty = contentPatches(content, values).length > 0;
529
+ const isSaving = operation !== null;
530
+ const versionNumber = content?.version?.number;
531
+ const valuesSignature = JSON.stringify(values);
532
+ const requestLeave = useCallback(() => confirmLeave ? confirmLeave() : window.confirm(messages.unsavedNavigation), [messages.unsavedNavigation]);
533
+ useUnsavedNavigation(canEdit && isDirty, requestLeave);
534
+ useEffect(() => {
535
+ if (!canEdit || !draftStore || !versionNumber) return;
536
+ if (isDirty) persistDraft(draftStore, contentId, JSON.parse(valuesSignature));
537
+ else clearDraft(draftStore);
538
+ }, [
539
+ canEdit,
540
+ contentId,
541
+ isDirty,
542
+ valuesSignature,
543
+ versionNumber
544
+ ]);
545
+ const recoverConflict = useCallback(async () => {
546
+ if (!contentId) return false;
547
+ try {
548
+ const latest = await client.content.get(contentId);
549
+ if (!latest.ok || !latest.data) return false;
550
+ cacheContent(latest.data);
551
+ setContent(latest.data);
552
+ setError(messages.conflict);
553
+ return true;
554
+ } catch {
555
+ return false;
556
+ }
557
+ }, [contentId, messages.conflict]);
558
+ const cancel = useCallback(() => {
559
+ setValues(contentValues(content));
560
+ clearDraft(draftStore);
561
+ setError(null);
562
+ setMessage(null);
563
+ setEditing(false);
564
+ }, [content]);
565
+ const save = useCallback(async () => {
566
+ if (!content?.version || !contentId || !canEdit) return false;
567
+ const patches = contentPatches(content, values);
568
+ if (patches.length === 0) return true;
569
+ setOperation("save");
570
+ setError(null);
571
+ setMessage(null);
572
+ try {
573
+ const result = await client.content.patch(contentId, {
574
+ expectedVersion: content.version.number,
575
+ patches,
576
+ origin: "inlineEditor"
577
+ });
578
+ if (!result.ok || !result.data) {
579
+ if (isUnauthorized(result)) {
580
+ setError(messages.sessionExpired);
581
+ onUnauthorized?.();
582
+ return false;
583
+ }
584
+ if (isContentConflict(result) && await recoverConflict()) return false;
585
+ setError(messages.saveError);
586
+ return false;
587
+ }
588
+ const nextContent = {
589
+ ...content,
590
+ version: result.data,
591
+ updatedAt: result.data.updatedAt
592
+ };
593
+ cacheContent(nextContent);
594
+ setContent(nextContent);
595
+ setValues(contentValues(nextContent));
596
+ clearDraft(draftStore);
597
+ setMessage(messages.saved);
598
+ return true;
599
+ } catch {
600
+ setError(messages.saveError);
601
+ return false;
602
+ } finally {
603
+ setOperation(null);
604
+ }
605
+ }, [
606
+ canEdit,
607
+ content,
608
+ contentId,
609
+ messages.saveError,
610
+ messages.saved,
611
+ messages.sessionExpired,
612
+ recoverConflict,
613
+ values
614
+ ]);
615
+ const publish = useCallback(async () => {
616
+ if (!content?.version || !contentId || !canPublish) return false;
617
+ const patches = contentPatches(content, values);
618
+ setOperation("publish");
619
+ setError(null);
620
+ setMessage(null);
621
+ try {
622
+ const result = patches.length > 0 ? await client.content.patch(contentId, {
623
+ expectedVersion: content.version.number,
624
+ patches,
625
+ origin: "inlineEditor",
626
+ publish: true
627
+ }) : await client.content.publish(contentId, content.version.number);
628
+ if (!result.ok || !result.data) {
629
+ if (isUnauthorized(result)) {
630
+ setError(messages.sessionExpired);
631
+ onUnauthorized?.();
632
+ return false;
633
+ }
634
+ if (isContentConflict(result) && await recoverConflict()) return false;
635
+ setError(messages.publishError);
636
+ return false;
637
+ }
638
+ const nextContent = {
639
+ ...content,
640
+ version: result.data,
641
+ updatedAt: result.data.updatedAt
642
+ };
643
+ cacheContent(nextContent);
644
+ setContent(nextContent);
645
+ setValues(contentValues(nextContent));
646
+ clearDraft(draftStore);
647
+ setEditing(false);
648
+ setMessage(messages.published);
649
+ return true;
650
+ } catch {
651
+ setError(messages.publishError);
652
+ return false;
653
+ } finally {
654
+ setOperation(null);
655
+ }
656
+ }, [
657
+ canPublish,
658
+ content,
659
+ contentId,
660
+ messages.publishError,
661
+ messages.published,
662
+ messages.sessionExpired,
663
+ recoverConflict,
664
+ values
665
+ ]);
666
+ const context = useMemo(() => ({
667
+ content,
668
+ values,
669
+ isLoading,
670
+ isSaving,
671
+ operation,
672
+ isEditing,
673
+ isDirty,
674
+ canEdit,
675
+ canPublish,
676
+ teamSlug,
677
+ media: client.media,
678
+ error,
679
+ message,
680
+ imagePickerLabels,
681
+ cancel,
682
+ setEditing: updateEditing,
683
+ setValue,
684
+ save,
685
+ publish
686
+ }), [
687
+ cancel,
688
+ canEdit,
689
+ canPublish,
690
+ content,
691
+ error,
692
+ imagePickerLabels.applyCrop,
693
+ imagePickerLabels.back,
694
+ imagePickerLabels.choose,
695
+ imagePickerLabels.close,
696
+ imagePickerLabels.cropDescription,
697
+ imagePickerLabels.cropTitle,
698
+ imagePickerLabels.dimensionsTooLarge,
699
+ imagePickerLabels.dimensionsTooSmall,
700
+ imagePickerLabels.drop,
701
+ imagePickerLabels.empty,
702
+ imagePickerLabels.field,
703
+ imagePickerLabels.invalidType,
704
+ imagePickerLabels.libraryDescription,
705
+ imagePickerLabels.libraryTitle,
706
+ imagePickerLabels.loadError,
707
+ imagePickerLabels.loading,
708
+ imagePickerLabels.positionHorizontal,
709
+ imagePickerLabels.positionVertical,
710
+ imagePickerLabels.processing,
711
+ imagePickerLabels.retry,
712
+ imagePickerLabels.tooLarge,
713
+ imagePickerLabels.upload,
714
+ imagePickerLabels.uploadError,
715
+ imagePickerLabels.uploading,
716
+ imagePickerLabels.zoom,
717
+ isDirty,
718
+ isEditing,
719
+ isLoading,
720
+ isSaving,
721
+ message,
722
+ operation,
723
+ publish,
724
+ save,
725
+ setValue,
726
+ teamSlug,
727
+ updateEditing,
728
+ values
729
+ ]);
730
+ return /* @__PURE__ */ jsx(ContentContext.Provider, {
731
+ value: context,
732
+ children
733
+ });
734
+ }
735
+ //#endregion
736
+ //#region src/content/editable-content.tsx
737
+ function EditableContent({ field, as: Tag = "span", className, placeholder = "Add text", multiline = false, editorClassName, label }) {
738
+ const { values, isEditing, canEdit, setValue } = useContent();
739
+ const value = values[field];
740
+ const text = typeof value === "string" || typeof value === "number" ? String(value) : "";
741
+ const fieldLabel = label ?? field;
742
+ if (!canEdit || !isEditing) return /* @__PURE__ */ jsx(Tag, {
743
+ className,
744
+ children: text
745
+ });
746
+ const editorClasses = `${className ?? ""} ${editorClassName ?? ""} block min-h-[2.5rem] w-full rounded-md border border-dashed border-blue-400/80 bg-white/95 px-3 py-2 [font:inherit] text-inherit shadow-sm outline-none transition hover:border-blue-500 hover:bg-blue-50/60 focus:border-blue-600 focus:bg-white focus:ring-2 focus:ring-blue-500/30`.trim();
747
+ return /* @__PURE__ */ jsxs("span", {
748
+ className: "group/content-field relative my-1 block",
749
+ "data-content-editable": field,
750
+ children: [/* @__PURE__ */ jsx("span", {
751
+ className: "pointer-events-none absolute -top-2 left-2 z-10 rounded bg-blue-600 px-1.5 py-0.5 text-[10px] font-semibold uppercase leading-none tracking-wide text-white opacity-90 shadow-sm transition group-hover/content-field:opacity-100",
752
+ children: fieldLabel
753
+ }), multiline ? /* @__PURE__ */ jsx("textarea", {
754
+ "aria-label": fieldLabel,
755
+ className: `${editorClasses} min-h-[7rem] resize-y`,
756
+ placeholder,
757
+ value: text,
758
+ onChange: (event) => setValue(field, event.target.value)
759
+ }) : /* @__PURE__ */ jsx("input", {
760
+ "aria-label": fieldLabel,
761
+ className: editorClasses,
762
+ placeholder,
763
+ value: text,
764
+ onChange: (event) => setValue(field, event.target.value)
765
+ })]
766
+ });
767
+ }
768
+ function useContentValue(key) {
769
+ return useContent().values[key];
770
+ }
771
+ //#endregion
772
+ //#region src/content/image-processing.ts
773
+ const loadImage = (file) => new Promise((resolve, reject) => {
774
+ const url = URL.createObjectURL(file);
775
+ const image = new Image();
776
+ image.onload = () => {
777
+ URL.revokeObjectURL(url);
778
+ resolve({
779
+ image,
780
+ dimensions: {
781
+ width: image.naturalWidth,
782
+ height: image.naturalHeight
783
+ }
784
+ });
785
+ };
786
+ image.onerror = () => {
787
+ URL.revokeObjectURL(url);
788
+ reject(/* @__PURE__ */ new Error("Image could not be decoded."));
789
+ };
790
+ image.src = url;
791
+ });
792
+ const readImageDimensions = async (file) => (await loadImage(file)).dimensions;
793
+ const outputFileName = (name, mimeType) => {
794
+ const extension = mimeType === "image/png" ? "png" : mimeType === "image/jpeg" ? "jpg" : "webp";
795
+ return `${name.replace(/\.[^.]+$/, "") || "image"}.${extension}`;
796
+ };
797
+ const processImageFile = async (file, compression, cropOptions, cropState = {
798
+ x: 0,
799
+ y: 0,
800
+ zoom: 1
801
+ }) => {
802
+ const { image, dimensions } = await loadImage(file);
803
+ const source = cropOptions ? imageCropRectangle$1(dimensions, cropOptions.aspectRatio, cropState) : {
804
+ x: 0,
805
+ y: 0,
806
+ ...dimensions
807
+ };
808
+ const requested = cropOptions ? cropOutputDimensions$1({
809
+ width: source.width,
810
+ height: source.height
811
+ }, cropOptions, compression) : containedImageDimensions$1({
812
+ width: source.width,
813
+ height: source.height
814
+ }, compression);
815
+ const canvas = document.createElement("canvas");
816
+ canvas.width = requested.width;
817
+ canvas.height = requested.height;
818
+ const context = canvas.getContext("2d");
819
+ if (!context) throw new Error("Canvas is unavailable.");
820
+ context.drawImage(image, source.x, source.y, source.width, source.height, 0, 0, requested.width, requested.height);
821
+ const mimeType = compression.mimeType ?? "image/webp";
822
+ const quality = typeof compression.quality === "number" && Number.isFinite(compression.quality) ? Math.max(.1, Math.min(1, compression.quality)) : .84;
823
+ const blob = await new Promise((resolve, reject) => canvas.toBlob((value) => value ? resolve(value) : reject(/* @__PURE__ */ new Error("Image processing failed.")), mimeType, quality));
824
+ const outputType = blob.type || mimeType;
825
+ return new File([blob], outputFileName(file.name, outputType), {
826
+ type: outputType,
827
+ lastModified: Date.now()
828
+ });
829
+ };
830
+ //#endregion
831
+ //#region src/content/editable-image.tsx
832
+ const DEFAULT_MAX_UPLOAD_BYTES = 10 * 1024 * 1024;
833
+ const DEFAULT_MAX_SOURCE_BYTES = 40 * 1024 * 1024;
834
+ const DEFAULT_PROCESSING = {
835
+ compression: {
836
+ maxWidth: 2560,
837
+ maxHeight: 2560,
838
+ mimeType: "image/webp",
839
+ quality: .84
840
+ },
841
+ dimensions: { maxPixels: 5e7 }
842
+ };
843
+ const FOCUSABLE_SELECTOR = "button:not([disabled]), [href], input:not([disabled]), [tabindex]:not([tabindex=\"-1\"])";
844
+ const focusableElements = (container) => Array.from(container.querySelectorAll(FOCUSABLE_SELECTOR));
845
+ const validateImageFile = (file, maxUploadBytes, labels) => {
846
+ if (!file.type.startsWith("image/")) return labels.invalidType;
847
+ if (file.size > maxUploadBytes) return labels.tooLarge;
848
+ return null;
849
+ };
850
+ const compressionFor = (file, compression) => {
851
+ if (compression !== false) return compression ?? DEFAULT_PROCESSING.compression;
852
+ return { mimeType: file.type === "image/png" || file.type === "image/jpeg" || file.type === "image/webp" ? file.type : "image/webp" };
853
+ };
854
+ const preservesOriginalFormat = (file) => file.type === "image/gif" || file.type === "image/svg+xml";
855
+ const processingDimensionError = (dimensions, processing) => {
856
+ if (processing.dimensions === false) return null;
857
+ return validateImageDimensions(dimensions, {
858
+ ...DEFAULT_PROCESSING.dimensions,
859
+ ...processing.dimensions
860
+ });
861
+ };
862
+ function EditableImage({ field, alt = "", className, inputClassName, placeholder = "Image URL", onSelect, label, labels: labelOverrides, maxUploadBytes = DEFAULT_MAX_UPLOAD_BYTES, maxSourceBytes = DEFAULT_MAX_SOURCE_BYTES, processing = DEFAULT_PROCESSING }) {
863
+ const { values, isEditing, canEdit, media, setValue, teamSlug, imagePickerLabels: defaultLabels } = useContent();
864
+ const labels = {
865
+ ...defaultLabels,
866
+ ...labelOverrides
867
+ };
868
+ const fieldLabel = label ?? labels.field;
869
+ const source = typeof values[field] === "string" ? values[field] : "";
870
+ const [isPickerOpen, setPickerOpen] = useState(false);
871
+ const [images, setImages] = useState([]);
872
+ const [isLoading, setLoading] = useState(false);
873
+ const [operation, setOperation] = useState(null);
874
+ const [error, setError] = useState(null);
875
+ const [canRetry, setCanRetry] = useState(false);
876
+ const [pendingImage, setPendingImage] = useState(null);
877
+ const [crop, setCrop] = useState({
878
+ x: 0,
879
+ y: 0,
880
+ zoom: 1
881
+ });
882
+ const loadRequest = useRef(0);
883
+ const uploadInProgress = useRef(false);
884
+ useEffect(() => () => {
885
+ if (pendingImage) URL.revokeObjectURL(pendingImage.previewUrl);
886
+ }, [pendingImage?.previewUrl]);
887
+ const loadImages = async () => {
888
+ if (!media || !teamSlug) return;
889
+ const request = ++loadRequest.current;
890
+ setLoading(true);
891
+ setError(null);
892
+ setCanRetry(false);
893
+ try {
894
+ const result = await media.list(teamSlug, { mime: "image/" });
895
+ if (request !== loadRequest.current) return;
896
+ if (!result.ok) {
897
+ setError(labels.loadError);
898
+ setCanRetry(true);
899
+ return;
900
+ }
901
+ setImages(result.data ?? []);
902
+ } catch {
903
+ if (request === loadRequest.current) {
904
+ setError(labels.loadError);
905
+ setCanRetry(true);
906
+ }
907
+ } finally {
908
+ if (request === loadRequest.current) setLoading(false);
909
+ }
910
+ };
911
+ useEffect(() => {
912
+ if (!isPickerOpen || !media || !teamSlug) return;
913
+ loadImages();
914
+ return () => {
915
+ loadRequest.current += 1;
916
+ };
917
+ }, [isPickerOpen, teamSlug]);
918
+ const uploadFile = async (file) => {
919
+ if (!media || !teamSlug || uploadInProgress.current) return;
920
+ uploadInProgress.current = true;
921
+ setOperation("uploading");
922
+ try {
923
+ const result = await media.upload({
924
+ team: teamSlug,
925
+ file,
926
+ type: "output",
927
+ label: file.name
928
+ });
929
+ if (!result.ok || !result.data) {
930
+ setError(labels.uploadError);
931
+ return;
932
+ }
933
+ setImages((current) => [result.data, ...current]);
934
+ setValue(field, result.data.url);
935
+ setPickerOpen(false);
936
+ } catch {
937
+ setError(labels.uploadError);
938
+ } finally {
939
+ uploadInProgress.current = false;
940
+ setOperation(null);
941
+ }
942
+ };
943
+ const prepareUpload = async (file) => {
944
+ if (uploadInProgress.current) return;
945
+ setError(null);
946
+ setCanRetry(false);
947
+ const validationError = validateImageFile(file, maxSourceBytes, labels);
948
+ if (validationError) {
949
+ setError(validationError);
950
+ return;
951
+ }
952
+ uploadInProgress.current = true;
953
+ setOperation("processing");
954
+ try {
955
+ if (processing === false) {
956
+ if (file.size > maxUploadBytes) setError(labels.tooLarge);
957
+ else {
958
+ uploadInProgress.current = false;
959
+ setOperation(null);
960
+ await uploadFile(file);
961
+ }
962
+ return;
963
+ }
964
+ const dimensions = await readImageDimensions(file);
965
+ const dimensionError = processingDimensionError(dimensions, processing);
966
+ if (dimensionError) {
967
+ setError(dimensionError === "too-small" ? labels.dimensionsTooSmall : labels.dimensionsTooLarge);
968
+ return;
969
+ }
970
+ if (processing.crop) {
971
+ if (!Number.isFinite(processing.crop.aspectRatio) || processing.crop.aspectRatio <= 0) throw new RangeError("Crop aspect ratio must be a positive finite number.");
972
+ setCrop({
973
+ x: 0,
974
+ y: 0,
975
+ zoom: 1
976
+ });
977
+ setPendingImage({
978
+ file,
979
+ dimensions,
980
+ previewUrl: URL.createObjectURL(file)
981
+ });
982
+ return;
983
+ }
984
+ if (processing.compression === false || preservesOriginalFormat(file)) {
985
+ if (file.size > maxUploadBytes) {
986
+ setError(labels.tooLarge);
987
+ return;
988
+ }
989
+ uploadInProgress.current = false;
990
+ setOperation(null);
991
+ await uploadFile(file);
992
+ return;
993
+ }
994
+ const compression = {
995
+ ...DEFAULT_PROCESSING.compression,
996
+ ...processing.compression
997
+ };
998
+ const outputDimensionError = processingDimensionError(containedImageDimensions(dimensions, compression), processing);
999
+ if (outputDimensionError) {
1000
+ setError(outputDimensionError === "too-small" ? labels.dimensionsTooSmall : labels.dimensionsTooLarge);
1001
+ return;
1002
+ }
1003
+ const output = await processImageFile(file, compression);
1004
+ if (output.size > maxUploadBytes) {
1005
+ setError(labels.tooLarge);
1006
+ return;
1007
+ }
1008
+ uploadInProgress.current = false;
1009
+ setOperation(null);
1010
+ await uploadFile(output);
1011
+ } catch {
1012
+ setError(labels.uploadError);
1013
+ } finally {
1014
+ if (uploadInProgress.current) {
1015
+ uploadInProgress.current = false;
1016
+ setOperation(null);
1017
+ }
1018
+ }
1019
+ };
1020
+ const applyCrop = async () => {
1021
+ if (!pendingImage || processing === false || !processing.crop || uploadInProgress.current) return;
1022
+ uploadInProgress.current = true;
1023
+ setOperation("processing");
1024
+ setError(null);
1025
+ try {
1026
+ const compression = {
1027
+ ...DEFAULT_PROCESSING.compression,
1028
+ ...processing.compression === false ? compressionFor(pendingImage.file, false) : processing.compression
1029
+ };
1030
+ const source = imageCropRectangle(pendingImage.dimensions, processing.crop.aspectRatio, crop);
1031
+ const outputDimensionError = processingDimensionError(cropOutputDimensions({
1032
+ width: source.width,
1033
+ height: source.height
1034
+ }, processing.crop, compression), processing);
1035
+ if (outputDimensionError) {
1036
+ setError(outputDimensionError === "too-small" ? labels.dimensionsTooSmall : labels.dimensionsTooLarge);
1037
+ return;
1038
+ }
1039
+ const output = await processImageFile(pendingImage.file, compression, processing.crop, crop);
1040
+ if (output.size > maxUploadBytes) {
1041
+ setError(labels.tooLarge);
1042
+ return;
1043
+ }
1044
+ setPendingImage(null);
1045
+ uploadInProgress.current = false;
1046
+ setOperation(null);
1047
+ await uploadFile(output);
1048
+ } catch {
1049
+ setError(labels.uploadError);
1050
+ } finally {
1051
+ if (uploadInProgress.current) {
1052
+ uploadInProgress.current = false;
1053
+ setOperation(null);
1054
+ }
1055
+ }
1056
+ };
1057
+ if (!canEdit || !isEditing) return source ? /* @__PURE__ */ jsx("img", {
1058
+ src: source,
1059
+ alt,
1060
+ className
1061
+ }) : null;
1062
+ return /* @__PURE__ */ jsxs("div", {
1063
+ className: `group/content-field relative rounded-lg border border-dashed border-blue-400/80 bg-blue-50/30 p-2 transition hover:border-blue-500 hover:bg-blue-50/60 focus-within:border-blue-600 focus-within:ring-2 focus-within:ring-blue-500/30 ${className ?? ""}`,
1064
+ "data-content-editable": field,
1065
+ children: [
1066
+ /* @__PURE__ */ jsx("span", {
1067
+ className: "pointer-events-none absolute -top-2 left-2 z-10 rounded bg-blue-600 px-1.5 py-0.5 text-[10px] font-semibold uppercase leading-none tracking-wide text-white shadow-sm",
1068
+ children: fieldLabel
1069
+ }),
1070
+ source && /* @__PURE__ */ jsx("img", {
1071
+ src: source,
1072
+ alt,
1073
+ className: "h-auto w-full rounded-md"
1074
+ }),
1075
+ /* @__PURE__ */ jsxs("div", {
1076
+ className: "mt-2 flex flex-wrap items-center gap-2",
1077
+ children: [
1078
+ onSelect && /* @__PURE__ */ jsx("button", {
1079
+ className: "rounded-lg bg-blue-600 px-3 py-2 text-sm font-medium text-white transition hover:bg-blue-500 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2",
1080
+ type: "button",
1081
+ onClick: onSelect,
1082
+ children: labels.choose
1083
+ }),
1084
+ !onSelect && media && teamSlug && /* @__PURE__ */ jsx("button", {
1085
+ className: "rounded-lg bg-blue-600 px-3 py-2 text-sm font-medium text-white transition hover:bg-blue-500 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2",
1086
+ type: "button",
1087
+ onClick: () => setPickerOpen(true),
1088
+ children: labels.choose
1089
+ }),
1090
+ /* @__PURE__ */ jsx("input", {
1091
+ "aria-label": fieldLabel,
1092
+ className: `${inputClassName ?? ""} min-w-0 flex-1 rounded-md border border-slate-300 bg-white px-3 py-2 text-sm text-slate-900 outline-none transition hover:border-blue-400 focus:border-blue-600 focus:ring-2 focus:ring-blue-500/30`,
1093
+ type: "url",
1094
+ placeholder,
1095
+ value: source,
1096
+ onChange: (event) => setValue(field, event.target.value)
1097
+ })
1098
+ ]
1099
+ }),
1100
+ /* @__PURE__ */ jsx(ImagePicker, {
1101
+ open: isPickerOpen,
1102
+ onClose: () => {
1103
+ if (!uploadInProgress.current) {
1104
+ setPendingImage(null);
1105
+ setPickerOpen(false);
1106
+ }
1107
+ },
1108
+ images,
1109
+ isLoading,
1110
+ operation,
1111
+ error,
1112
+ canRetry,
1113
+ labels,
1114
+ pendingImage,
1115
+ crop,
1116
+ cropAspectRatio: processing === false ? void 0 : processing.crop?.aspectRatio,
1117
+ onCropChange: setCrop,
1118
+ onApplyCrop: applyCrop,
1119
+ onBack: () => setPendingImage(null),
1120
+ onRetry: loadImages,
1121
+ onUpload: prepareUpload,
1122
+ onSelect: (url) => {
1123
+ if (uploadInProgress.current) return;
1124
+ setValue(field, url);
1125
+ setPickerOpen(false);
1126
+ }
1127
+ })
1128
+ ]
1129
+ });
1130
+ }
1131
+ function ImagePicker({ open, onClose, images, isLoading, operation, error, canRetry, labels, pendingImage, crop, cropAspectRatio, onCropChange, onApplyCrop, onBack, onRetry, onUpload, onSelect }) {
1132
+ const dialogRef = useRef(null);
1133
+ const closeButtonRef = useRef(null);
1134
+ const previousFocusRef = useRef(null);
1135
+ const [isDragging, setDragging] = useState(false);
1136
+ const titleId = useId();
1137
+ const descriptionId = useId();
1138
+ const isBusy = operation !== null;
1139
+ useEffect(() => {
1140
+ if (!open) return;
1141
+ previousFocusRef.current = document.activeElement instanceof HTMLElement ? document.activeElement : null;
1142
+ const previousOverflow = document.body.style.overflow;
1143
+ document.body.style.overflow = "hidden";
1144
+ const frame = window.requestAnimationFrame(() => closeButtonRef.current?.focus());
1145
+ const keydown = (event) => {
1146
+ if (event.key === "Escape") {
1147
+ event.preventDefault();
1148
+ if (!isBusy) onClose();
1149
+ return;
1150
+ }
1151
+ if (event.key !== "Tab" || !dialogRef.current) return;
1152
+ const focusable = focusableElements(dialogRef.current);
1153
+ if (focusable.length === 0) return;
1154
+ const first = focusable[0];
1155
+ const last = focusable.at(-1);
1156
+ if (event.shiftKey && document.activeElement === first) {
1157
+ event.preventDefault();
1158
+ last.focus();
1159
+ } else if (!event.shiftKey && document.activeElement === last) {
1160
+ event.preventDefault();
1161
+ first.focus();
1162
+ }
1163
+ };
1164
+ document.addEventListener("keydown", keydown);
1165
+ return () => {
1166
+ window.cancelAnimationFrame(frame);
1167
+ document.removeEventListener("keydown", keydown);
1168
+ document.body.style.overflow = previousOverflow;
1169
+ previousFocusRef.current?.focus();
1170
+ };
1171
+ }, [open]);
1172
+ if (!open) return null;
1173
+ return /* @__PURE__ */ jsx("div", {
1174
+ className: "fixed inset-0 z-[1100] grid place-items-center bg-slate-950/55 p-4",
1175
+ role: "presentation",
1176
+ onMouseDown: (event) => {
1177
+ if (event.target === event.currentTarget) onClose();
1178
+ },
1179
+ children: /* @__PURE__ */ jsxs("div", {
1180
+ ref: dialogRef,
1181
+ className: "max-h-[min(44rem,calc(100vh-2rem))] w-full max-w-2xl overflow-y-auto rounded-xl bg-white p-5 shadow-2xl",
1182
+ role: "dialog",
1183
+ "aria-modal": "true",
1184
+ "aria-labelledby": titleId,
1185
+ "aria-describedby": descriptionId,
1186
+ children: [
1187
+ /* @__PURE__ */ jsxs("div", {
1188
+ className: "flex items-start justify-between gap-4",
1189
+ children: [/* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("h2", {
1190
+ id: titleId,
1191
+ className: "text-lg font-semibold text-slate-950",
1192
+ children: pendingImage ? labels.cropTitle : labels.libraryTitle
1193
+ }), /* @__PURE__ */ jsx("p", {
1194
+ id: descriptionId,
1195
+ className: "mt-1 text-sm text-slate-500",
1196
+ children: pendingImage ? labels.cropDescription : labels.libraryDescription
1197
+ })] }), /* @__PURE__ */ jsx("button", {
1198
+ ref: closeButtonRef,
1199
+ "aria-label": labels.close,
1200
+ className: "rounded-lg px-3 py-2 text-sm font-medium text-slate-700 transition hover:bg-slate-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 disabled:cursor-not-allowed disabled:opacity-50",
1201
+ type: "button",
1202
+ disabled: isBusy,
1203
+ onClick: onClose,
1204
+ children: labels.close
1205
+ })]
1206
+ }),
1207
+ error && /* @__PURE__ */ jsxs("div", {
1208
+ className: "mt-4 flex items-center justify-between gap-3 rounded-lg bg-red-50 px-3 py-2 text-sm text-red-700",
1209
+ role: "alert",
1210
+ children: [/* @__PURE__ */ jsx("span", { children: error }), canRetry && !isBusy && /* @__PURE__ */ jsx("button", {
1211
+ className: "shrink-0 rounded px-2 py-1 font-semibold hover:bg-red-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-red-500",
1212
+ type: "button",
1213
+ onClick: () => void onRetry(),
1214
+ children: labels.retry
1215
+ })]
1216
+ }),
1217
+ pendingImage && cropAspectRatio ? /* @__PURE__ */ jsx(ImageCropEditor, {
1218
+ pendingImage,
1219
+ aspectRatio: cropAspectRatio,
1220
+ crop,
1221
+ isProcessing: isBusy,
1222
+ labels,
1223
+ onChange: onCropChange,
1224
+ onApply: onApplyCrop,
1225
+ onBack
1226
+ }) : /* @__PURE__ */ jsxs(Fragment, { children: [
1227
+ /* @__PURE__ */ jsxs("label", {
1228
+ className: `mt-5 flex flex-col items-center justify-center rounded-lg border border-dashed px-4 py-7 text-center text-sm font-medium transition focus-within:ring-2 focus-within:ring-blue-500 ${isBusy ? "cursor-wait border-slate-200 bg-slate-50 text-slate-400" : isDragging ? "cursor-copy border-blue-600 bg-blue-50 text-blue-700" : "cursor-pointer border-slate-300 text-slate-600 hover:border-blue-400 hover:text-blue-600"}`,
1229
+ onDragEnter: (event) => {
1230
+ event.preventDefault();
1231
+ if (!isBusy) setDragging(true);
1232
+ },
1233
+ onDragOver: (event) => {
1234
+ event.preventDefault();
1235
+ if (!isBusy) setDragging(true);
1236
+ },
1237
+ onDragLeave: (event) => {
1238
+ if (!event.currentTarget.contains(event.relatedTarget)) setDragging(false);
1239
+ },
1240
+ onDrop: (event) => {
1241
+ event.preventDefault();
1242
+ setDragging(false);
1243
+ if (isBusy) return;
1244
+ const file = event.dataTransfer.files?.[0];
1245
+ if (file) onUpload(file);
1246
+ },
1247
+ children: [
1248
+ /* @__PURE__ */ jsx("input", {
1249
+ className: "sr-only",
1250
+ type: "file",
1251
+ accept: "image/*",
1252
+ disabled: isBusy,
1253
+ onChange: (event) => {
1254
+ const file = event.target.files?.[0];
1255
+ if (file) onUpload(file);
1256
+ event.currentTarget.value = "";
1257
+ }
1258
+ }),
1259
+ /* @__PURE__ */ jsx("span", { children: isDragging ? labels.drop : labels.upload }),
1260
+ operation && /* @__PURE__ */ jsx("span", {
1261
+ className: "mt-2 text-xs font-normal text-slate-500",
1262
+ role: "status",
1263
+ children: operation === "processing" ? labels.processing : labels.uploading
1264
+ })
1265
+ ]
1266
+ }),
1267
+ isLoading && /* @__PURE__ */ jsx("div", {
1268
+ className: "mt-5 rounded-lg bg-slate-50 p-6 text-center text-sm text-slate-500",
1269
+ role: "status",
1270
+ children: labels.loading
1271
+ }),
1272
+ !isLoading && !error && images.length === 0 && /* @__PURE__ */ jsx("div", {
1273
+ className: "mt-5 rounded-lg bg-slate-50 p-6 text-center text-sm text-slate-500",
1274
+ children: labels.empty
1275
+ }),
1276
+ !isLoading && images.length > 0 && /* @__PURE__ */ jsx("div", {
1277
+ className: "mt-5 grid grid-cols-2 gap-3 sm:grid-cols-3",
1278
+ role: "list",
1279
+ children: images.map((image) => /* @__PURE__ */ jsxs("button", {
1280
+ className: "overflow-hidden rounded-lg border border-slate-200 text-left transition hover:border-blue-400 hover:opacity-90 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 disabled:cursor-not-allowed disabled:opacity-50",
1281
+ type: "button",
1282
+ disabled: isBusy,
1283
+ onClick: () => onSelect(image.url),
1284
+ children: [/* @__PURE__ */ jsx("img", {
1285
+ className: "aspect-square w-full object-cover",
1286
+ src: image.url,
1287
+ alt: image.name
1288
+ }), /* @__PURE__ */ jsx("span", {
1289
+ className: "block truncate p-2 text-xs text-slate-700",
1290
+ children: image.name
1291
+ })]
1292
+ }, image.id))
1293
+ })
1294
+ ] })
1295
+ ]
1296
+ })
1297
+ });
1298
+ }
1299
+ function ImageCropEditor({ pendingImage, aspectRatio, crop, isProcessing, labels, onChange, onApply, onBack }) {
1300
+ const previewRef = useRef(null);
1301
+ const drag = useRef(null);
1302
+ const control = (key, value) => onChange({
1303
+ ...crop,
1304
+ [key]: value
1305
+ });
1306
+ const clampedX = Math.max(-1, Math.min(1, crop.x));
1307
+ const clampedY = Math.max(-1, Math.min(1, crop.y));
1308
+ const clampedZoom = Math.max(1, crop.zoom);
1309
+ const sourceAspect = pendingImage.dimensions.width / pendingImage.dimensions.height;
1310
+ const baseWidth = sourceAspect > aspectRatio ? sourceAspect / aspectRatio * 100 : 100;
1311
+ const baseHeight = sourceAspect > aspectRatio ? 100 : aspectRatio / sourceAspect * 100;
1312
+ const width = baseWidth * clampedZoom;
1313
+ const height = baseHeight * clampedZoom;
1314
+ const left = -(width - 100) * ((clampedX + 1) / 2);
1315
+ const top = -(height - 100) * ((clampedY + 1) / 2);
1316
+ const moveCrop = (clientX, clientY) => {
1317
+ const start = drag.current;
1318
+ const bounds = previewRef.current?.getBoundingClientRect();
1319
+ if (!start || !bounds) return;
1320
+ const overflowX = bounds.width * ((width - 100) / 100);
1321
+ const overflowY = bounds.height * ((height - 100) / 100);
1322
+ const x = overflowX > 0 ? start.crop.x - 2 * (clientX - start.x) / overflowX : start.crop.x;
1323
+ const y = overflowY > 0 ? start.crop.y - 2 * (clientY - start.y) / overflowY : start.crop.y;
1324
+ onChange({
1325
+ ...crop,
1326
+ x: Math.max(-1, Math.min(1, x)),
1327
+ y: Math.max(-1, Math.min(1, y))
1328
+ });
1329
+ };
1330
+ return /* @__PURE__ */ jsxs("div", {
1331
+ className: "mt-5",
1332
+ children: [
1333
+ /* @__PURE__ */ jsxs("div", {
1334
+ ref: previewRef,
1335
+ className: `relative mx-auto w-full max-w-xl touch-none overflow-hidden rounded-lg bg-slate-950 ${isProcessing ? "cursor-wait" : "cursor-grab active:cursor-grabbing"}`,
1336
+ style: { aspectRatio },
1337
+ onPointerDown: (event) => {
1338
+ if (isProcessing) return;
1339
+ drag.current = {
1340
+ pointerId: event.pointerId,
1341
+ x: event.clientX,
1342
+ y: event.clientY,
1343
+ crop
1344
+ };
1345
+ event.currentTarget.setPointerCapture(event.pointerId);
1346
+ },
1347
+ onPointerMove: (event) => {
1348
+ if (drag.current?.pointerId === event.pointerId) moveCrop(event.clientX, event.clientY);
1349
+ },
1350
+ onPointerUp: (event) => {
1351
+ if (drag.current?.pointerId !== event.pointerId) return;
1352
+ drag.current = null;
1353
+ event.currentTarget.releasePointerCapture(event.pointerId);
1354
+ },
1355
+ onPointerCancel: () => {
1356
+ drag.current = null;
1357
+ },
1358
+ children: [/* @__PURE__ */ jsx("img", {
1359
+ src: pendingImage.previewUrl,
1360
+ alt: "",
1361
+ className: "absolute max-w-none select-none",
1362
+ draggable: false,
1363
+ style: {
1364
+ width: `${width}%`,
1365
+ height: `${height}%`,
1366
+ left: `${left}%`,
1367
+ top: `${top}%`
1368
+ }
1369
+ }), /* @__PURE__ */ jsx("div", { className: "pointer-events-none absolute inset-0 ring-1 ring-inset ring-white/70" })]
1370
+ }),
1371
+ /* @__PURE__ */ jsxs("div", {
1372
+ className: "mt-5 grid gap-4 sm:grid-cols-3",
1373
+ children: [
1374
+ /* @__PURE__ */ jsxs("label", {
1375
+ className: "grid gap-1 text-xs font-medium text-slate-700",
1376
+ children: [/* @__PURE__ */ jsx("span", { children: labels.zoom }), /* @__PURE__ */ jsx("input", {
1377
+ "aria-label": labels.zoom,
1378
+ type: "range",
1379
+ min: "1",
1380
+ max: "3",
1381
+ step: "0.01",
1382
+ value: crop.zoom,
1383
+ disabled: isProcessing,
1384
+ onChange: (event) => control("zoom", Number(event.target.value))
1385
+ })]
1386
+ }),
1387
+ /* @__PURE__ */ jsxs("label", {
1388
+ className: "grid gap-1 text-xs font-medium text-slate-700",
1389
+ children: [/* @__PURE__ */ jsx("span", { children: labels.positionHorizontal }), /* @__PURE__ */ jsx("input", {
1390
+ "aria-label": labels.positionHorizontal,
1391
+ type: "range",
1392
+ min: "-1",
1393
+ max: "1",
1394
+ step: "0.01",
1395
+ value: crop.x,
1396
+ disabled: isProcessing,
1397
+ onChange: (event) => control("x", Number(event.target.value))
1398
+ })]
1399
+ }),
1400
+ /* @__PURE__ */ jsxs("label", {
1401
+ className: "grid gap-1 text-xs font-medium text-slate-700",
1402
+ children: [/* @__PURE__ */ jsx("span", { children: labels.positionVertical }), /* @__PURE__ */ jsx("input", {
1403
+ "aria-label": labels.positionVertical,
1404
+ type: "range",
1405
+ min: "-1",
1406
+ max: "1",
1407
+ step: "0.01",
1408
+ value: crop.y,
1409
+ disabled: isProcessing,
1410
+ onChange: (event) => control("y", Number(event.target.value))
1411
+ })]
1412
+ })
1413
+ ]
1414
+ }),
1415
+ /* @__PURE__ */ jsxs("div", {
1416
+ className: "mt-5 flex justify-end gap-2",
1417
+ children: [/* @__PURE__ */ jsx("button", {
1418
+ className: "rounded-lg px-3 py-2 text-sm font-semibold text-slate-700 hover:bg-slate-100 disabled:opacity-50",
1419
+ type: "button",
1420
+ disabled: isProcessing,
1421
+ onClick: onBack,
1422
+ children: labels.back
1423
+ }), /* @__PURE__ */ jsx("button", {
1424
+ className: "rounded-lg bg-blue-600 px-3 py-2 text-sm font-semibold text-white hover:bg-blue-500 disabled:opacity-50",
1425
+ type: "button",
1426
+ disabled: isProcessing,
1427
+ onClick: () => void onApply(),
1428
+ children: isProcessing ? labels.processing : labels.applyCrop
1429
+ })]
1430
+ })
1431
+ ]
1432
+ });
1433
+ }
1434
+ //#endregion
1435
+ //#region src/content/toolbar.tsx
1436
+ function ContentToolbar({ className, labels = {}, confirmCancel, confirmPublish }) {
1437
+ const { canEdit, canPublish, isDirty, isEditing, isSaving, operation, error, message, cancel: cancelEditing, setEditing, save, publish } = useContent();
1438
+ if (!canEdit) return null;
1439
+ const edit = labels.edit ?? "Edit";
1440
+ const saveLabel = labels.save ?? "Save draft";
1441
+ const savingLabel = labels.saving ?? "Saving…";
1442
+ const publishLabel = labels.publish ?? "Publish";
1443
+ const publishingLabel = labels.publishing ?? "Publishing…";
1444
+ const cancel = labels.cancel ?? "Cancel";
1445
+ const toolbar = labels.toolbar ?? "Content editor";
1446
+ const unsaved = labels.unsaved ?? "Unsaved changes";
1447
+ const handlePublish = async () => {
1448
+ if (confirmPublish && !await confirmPublish()) return;
1449
+ await publish();
1450
+ };
1451
+ const handleCancel = async () => {
1452
+ if (isDirty && confirmCancel && !await confirmCancel()) return;
1453
+ cancelEditing();
1454
+ };
1455
+ return /* @__PURE__ */ jsxs("div", {
1456
+ className: `flex flex-wrap items-center gap-2 ${className ?? ""}`,
1457
+ role: "toolbar",
1458
+ "aria-label": toolbar,
1459
+ "aria-busy": isSaving,
1460
+ children: [
1461
+ !isEditing ? /* @__PURE__ */ jsx("button", {
1462
+ className: "rounded-lg bg-blue-600 px-3 py-2 text-sm font-medium text-white transition hover:bg-blue-500",
1463
+ type: "button",
1464
+ onClick: () => setEditing(true),
1465
+ children: edit
1466
+ }) : /* @__PURE__ */ jsxs(Fragment, { children: [
1467
+ /* @__PURE__ */ jsx("button", {
1468
+ className: "rounded-lg bg-blue-600 px-3 py-2 text-sm font-medium text-white transition hover:bg-blue-500 disabled:opacity-60",
1469
+ type: "button",
1470
+ disabled: isSaving || !isDirty,
1471
+ onClick: () => void save(),
1472
+ children: operation === "save" ? savingLabel : saveLabel
1473
+ }),
1474
+ canPublish && /* @__PURE__ */ jsx("button", {
1475
+ className: "rounded-lg bg-emerald-600 px-3 py-2 text-sm font-medium text-white transition hover:bg-emerald-500 disabled:opacity-60",
1476
+ type: "button",
1477
+ disabled: isSaving,
1478
+ onClick: () => void handlePublish(),
1479
+ children: operation === "publish" ? publishingLabel : publishLabel
1480
+ }),
1481
+ /* @__PURE__ */ jsx("button", {
1482
+ className: "rounded-lg px-3 py-2 text-sm font-medium text-slate-700 transition hover:bg-slate-100 disabled:opacity-60",
1483
+ type: "button",
1484
+ disabled: isSaving,
1485
+ onClick: () => void handleCancel(),
1486
+ children: cancel
1487
+ })
1488
+ ] }),
1489
+ error && /* @__PURE__ */ jsx("span", {
1490
+ className: "w-full text-sm text-red-600 dark:text-red-400",
1491
+ role: "alert",
1492
+ children: error
1493
+ }),
1494
+ message && /* @__PURE__ */ jsx("span", {
1495
+ className: "w-full text-sm text-emerald-700",
1496
+ "aria-live": "polite",
1497
+ children: message
1498
+ }),
1499
+ isEditing && isDirty && /* @__PURE__ */ jsx("span", {
1500
+ className: "w-full text-xs text-slate-500",
1501
+ "aria-live": "polite",
1502
+ children: unsaved
1503
+ })
1504
+ ]
1505
+ });
1506
+ }
1507
+ //#endregion
1508
+ //#region src/content/content-editor.tsx
1509
+ function EditorPanel({ confirmCancel, confirmPublish, labels, title }) {
1510
+ const { canEdit } = useContent();
1511
+ if (!canEdit) return null;
1512
+ return /* @__PURE__ */ jsxs("div", {
1513
+ className: "fixed right-4 bottom-4 z-[1000] w-[min(24rem,calc(100vw-2rem))] rounded-xl border border-slate-200 bg-white p-3 text-slate-950 shadow-xl",
1514
+ children: [/* @__PURE__ */ jsx("strong", {
1515
+ className: "mb-2 block text-sm font-semibold",
1516
+ children: title
1517
+ }), /* @__PURE__ */ jsx(ContentToolbar, {
1518
+ confirmCancel,
1519
+ confirmPublish,
1520
+ labels
1521
+ })]
1522
+ });
1523
+ }
1524
+ function ContentEditor({ children, confirmCancel, confirmPublish, labels, title = "Edit content", ...provider }) {
1525
+ return /* @__PURE__ */ jsx(ContentProvider, {
1526
+ ...provider,
1527
+ children: /* @__PURE__ */ jsxs("div", {
1528
+ "data-content-editor": "true",
1529
+ children: [/* @__PURE__ */ jsx(EditorPanel, {
1530
+ confirmCancel,
1531
+ confirmPublish,
1532
+ labels,
1533
+ title
1534
+ }), children]
1535
+ })
1536
+ });
1537
+ }
1538
+ //#endregion
1539
+ //#region src/content/draft-store.ts
1540
+ const isDraft = (value) => {
1541
+ if (!value || typeof value !== "object") return false;
1542
+ const draft = value;
1543
+ if (draft.contentId !== void 0 && typeof draft.contentId !== "string") return false;
1544
+ return Boolean(draft.values && typeof draft.values === "object" && !Array.isArray(draft.values));
1545
+ };
1546
+ const createContentDraftStore = (storageKey, storage) => {
1547
+ const resolvedStorage = () => {
1548
+ if (storage) return storage;
1549
+ if (typeof window === "undefined") return null;
1550
+ try {
1551
+ return window.sessionStorage;
1552
+ } catch {
1553
+ return null;
1554
+ }
1555
+ };
1556
+ return {
1557
+ get: () => {
1558
+ try {
1559
+ const serialized = resolvedStorage()?.getItem(storageKey);
1560
+ if (!serialized) return null;
1561
+ const value = JSON.parse(serialized);
1562
+ if (!isDraft(value)) return null;
1563
+ return {
1564
+ contentId: value.contentId,
1565
+ values: value.values
1566
+ };
1567
+ } catch {
1568
+ return null;
1569
+ }
1570
+ },
1571
+ set: (draft) => {
1572
+ try {
1573
+ const target = resolvedStorage();
1574
+ if (!target) return false;
1575
+ target.setItem(storageKey, JSON.stringify(draft));
1576
+ return true;
1577
+ } catch {
1578
+ return false;
1579
+ }
1580
+ },
1581
+ clear: () => {
1582
+ try {
1583
+ const target = resolvedStorage();
1584
+ if (!target) return false;
1585
+ target.removeItem(storageKey);
1586
+ return true;
1587
+ } catch {
1588
+ return false;
1589
+ }
1590
+ }
1591
+ };
1592
+ };
1593
+ //#endregion
1594
+ //#region src/content/return-url.ts
1595
+ const getContentSessionReturnUrl = (currentUrl) => {
1596
+ const returnUrl = new URL(currentUrl);
1597
+ returnUrl.searchParams.set("platform", "app");
1598
+ return returnUrl.toString();
1599
+ };
1600
+ //#endregion
1601
+ //#region src/content/session-store.ts
1602
+ const createSessionTokenStore = (storageKey, storage) => {
1603
+ const listeners = /* @__PURE__ */ new Set();
1604
+ const resolvedStorage = () => {
1605
+ if (storage) return storage;
1606
+ if (typeof window === "undefined") return null;
1607
+ try {
1608
+ return window.localStorage;
1609
+ } catch {
1610
+ return null;
1611
+ }
1612
+ };
1613
+ const emit = () => {
1614
+ for (const listener of listeners) listener();
1615
+ };
1616
+ const onStorage = (event) => {
1617
+ if (event.key === storageKey) emit();
1618
+ };
1619
+ return {
1620
+ getSnapshot: () => {
1621
+ try {
1622
+ return resolvedStorage()?.getItem(storageKey) ?? null;
1623
+ } catch {
1624
+ return null;
1625
+ }
1626
+ },
1627
+ getServerSnapshot: () => null,
1628
+ subscribe: (listener) => {
1629
+ listeners.add(listener);
1630
+ if (typeof window !== "undefined") window.addEventListener("storage", onStorage);
1631
+ return () => {
1632
+ listeners.delete(listener);
1633
+ if (listeners.size === 0 && typeof window !== "undefined") window.removeEventListener("storage", onStorage);
1634
+ };
1635
+ },
1636
+ set: (token) => {
1637
+ try {
1638
+ const target = resolvedStorage();
1639
+ if (!target) return false;
1640
+ target.setItem(storageKey, token);
1641
+ emit();
1642
+ return true;
1643
+ } catch {
1644
+ return false;
1645
+ }
1646
+ },
1647
+ clear: () => {
1648
+ try {
1649
+ const target = resolvedStorage();
1650
+ if (!target) return false;
1651
+ target.removeItem(storageKey);
1652
+ emit();
1653
+ return true;
1654
+ } catch {
1655
+ return false;
1656
+ }
1657
+ }
1658
+ };
1659
+ };
1660
+ //#endregion
1661
+ //#region src/content/use-content-session.ts
1662
+ const useContentSession = ({ env = "mainnet", exchange, sessionStore, sessionStorageKey, team }) => {
1663
+ const defaultStore = useMemo(() => createSessionTokenStore(sessionStorageKey), [sessionStorageKey]);
1664
+ const store = sessionStore ?? defaultStore;
1665
+ const storedToken = useSyncExternalStore(store.subscribe, store.getSnapshot, store.getServerSnapshot);
1666
+ const [handoffToken, setHandoffToken] = useState(null);
1667
+ const [error, setError] = useState(null);
1668
+ useEffect(() => {
1669
+ const handoff = getAuthFragmentToken(window.location.href);
1670
+ if (!handoff) return;
1671
+ window.history.replaceState(null, "", `${window.location.pathname}${window.location.search}`);
1672
+ const completeExchange = async () => {
1673
+ const token = await (exchange ? exchange(handoff) : createClient({
1674
+ env,
1675
+ team
1676
+ }).auth.exchangeNativeSession(handoff).then((response) => {
1677
+ if (!response.ok || !response.data?.token) throw new Error("Session exchange failed.");
1678
+ return response.data.token;
1679
+ }));
1680
+ if (!store.set(token)) throw new Error("Session storage is unavailable.");
1681
+ setHandoffToken(token);
1682
+ };
1683
+ completeExchange().catch(() => setError("The session could not be established."));
1684
+ }, [
1685
+ env,
1686
+ sessionStorageKey,
1687
+ team
1688
+ ]);
1689
+ return {
1690
+ clear: useCallback(() => {
1691
+ setHandoffToken(null);
1692
+ store.clear();
1693
+ }, [sessionStorageKey]),
1694
+ error,
1695
+ token: handoffToken ?? storedToken
1696
+ };
1697
+ };
1698
+ //#endregion
1699
+ //#region src/content/managed-content-editor.tsx
1700
+ function ManagedContentEditor({ authErrorClassName, authErrorMessage = "The session could not be established.", children, config, confirmLeave, confirmCancel, confirmPublish, draftStore, imagePickerLabels, initialContent, labels, messages, reauthenticateLabel = "Sign in again", reauthenticateUrl, sessionStore, sessionExpiredMessage = "Your editing session expired. Sign in again to continue; your unsaved changes will be restored.", title = "Edit content" }) {
1701
+ const { contentId, env = "mainnet", publicSlug, sessionStorageKey, team } = config;
1702
+ const { error, token } = useContentSession({
1703
+ env,
1704
+ sessionStore,
1705
+ sessionStorageKey,
1706
+ team
1707
+ });
1708
+ const client = useMemo(() => createClient({
1709
+ apiToken: token ?? void 0,
1710
+ env,
1711
+ team
1712
+ }), [
1713
+ env,
1714
+ team,
1715
+ token
1716
+ ]);
1717
+ const defaultDraftStore = useMemo(() => createContentDraftStore(`${sessionStorageKey}:draft`), [sessionStorageKey]);
1718
+ const [sessionExpired, setSessionExpired] = useState(false);
1719
+ const [loginUrl, setLoginUrl] = useState(null);
1720
+ useEffect(() => {
1721
+ if (!token) return;
1722
+ setSessionExpired(false);
1723
+ const previous = client.http.onUnauthorized;
1724
+ client.http.onUnauthorized = () => {
1725
+ setSessionExpired(true);
1726
+ };
1727
+ return () => {
1728
+ client.http.onUnauthorized = previous;
1729
+ };
1730
+ }, [token]);
1731
+ useEffect(() => {
1732
+ if (!sessionExpired || typeof window === "undefined") return;
1733
+ const returnTo = getContentSessionReturnUrl(window.location.href);
1734
+ setLoginUrl(typeof reauthenticateUrl === "function" ? reauthenticateUrl(returnTo) : reauthenticateUrl ?? getLoginUrl(env, returnTo, { team }));
1735
+ }, [
1736
+ env,
1737
+ sessionExpired,
1738
+ team
1739
+ ]);
1740
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
1741
+ error && /* @__PURE__ */ jsx("p", {
1742
+ className: authErrorClassName ?? "fixed right-4 bottom-4 z-[1001] max-w-[min(24rem,calc(100vw-2rem))] rounded-xl border border-red-200 bg-white p-3 text-sm text-red-700 shadow-xl",
1743
+ "data-content-auth-error": "true",
1744
+ role: "alert",
1745
+ children: authErrorMessage
1746
+ }),
1747
+ sessionExpired && /* @__PURE__ */ jsxs("div", {
1748
+ className: authErrorClassName ?? "fixed right-4 bottom-4 z-[1002] max-w-[min(26rem,calc(100vw-2rem))] rounded-xl border border-amber-200 bg-white p-4 text-sm text-slate-800 shadow-xl",
1749
+ "data-content-session-expired": "true",
1750
+ role: "alert",
1751
+ children: [/* @__PURE__ */ jsx("strong", {
1752
+ className: "block font-semibold text-slate-950",
1753
+ children: sessionExpiredMessage
1754
+ }), loginUrl && /* @__PURE__ */ jsx("a", {
1755
+ className: "mt-3 inline-flex rounded-lg bg-blue-600 px-3 py-2 font-semibold text-white transition hover:bg-blue-500 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2",
1756
+ "data-content-navigation-allowed": true,
1757
+ href: loginUrl,
1758
+ children: reauthenticateLabel
1759
+ })]
1760
+ }),
1761
+ /* @__PURE__ */ jsx(ContentEditor, {
1762
+ client,
1763
+ contentId: token ? contentId : void 0,
1764
+ publicSlug: token ? void 0 : publicSlug,
1765
+ initialContent: token ? void 0 : initialContent,
1766
+ canEdit: Boolean(token),
1767
+ canPublish: Boolean(token),
1768
+ teamSlug: team,
1769
+ confirmLeave,
1770
+ confirmCancel,
1771
+ confirmPublish,
1772
+ draftStore: draftStore ?? defaultDraftStore,
1773
+ imagePickerLabels,
1774
+ labels,
1775
+ messages: {
1776
+ ...messages,
1777
+ sessionExpired: sessionExpiredMessage
1778
+ },
1779
+ onUnauthorized: () => {
1780
+ setSessionExpired(true);
1781
+ },
1782
+ title,
1783
+ children
1784
+ })
1785
+ ] });
1786
+ }
1787
+ //#endregion
1788
+ export { ContentEditor, ContentProvider, ContentToolbar, EditableContent, EditableImage, EmbedChatInput, EmbedChatList, ManagedContentEditor, clearRoom, containedImageDimensions, createContentDraftStore, createSessionTokenStore, cropOutputDimensions, getContentSessionReturnUrl, getOrCreateGuestId, imageCropRectangle, loadRoom, openEventSourceStream, processImageFile, readImageDimensions, saveRoom, useContentSession, useContentValue, useEmbedChat, useUnsavedNavigation, validateImageDimensions, validateImageFile };