@widgetic/editor 4.0.10 → 4.0.11

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.
Files changed (55) hide show
  1. package/README.md +118 -0
  2. package/dist/components/WidgetPreview.svelte +65 -27
  3. package/dist/components/WidgetPreview.svelte.d.ts +8 -0
  4. package/dist/components/editors/CompositionEditorApp.svelte +508 -0
  5. package/dist/components/editors/CompositionEditorApp.svelte.d.ts +75 -0
  6. package/dist/components/editors/PropsEditor.svelte +193 -792
  7. package/dist/components/editors/PropsEditor.svelte.d.ts +7 -0
  8. package/dist/components/ic/BrowserInputController.svelte +21 -2
  9. package/dist/components/ic/ColorPickerInputController.svelte +24 -6
  10. package/dist/components/ic/DateFieldInputController.svelte +7 -3
  11. package/dist/components/ic/DatePickerInputController.svelte +10 -4
  12. package/dist/components/ic/DropdownInputController.svelte +11 -4
  13. package/dist/components/ic/FontInputController.svelte +68 -49
  14. package/dist/components/ic/InputControllerHeader.svelte +11 -8
  15. package/dist/components/ic/NumberInputController.svelte +4 -2
  16. package/dist/components/ic/PositionInputController.svelte +14 -11
  17. package/dist/components/ic/RangeCalendarInputController.svelte +10 -4
  18. package/dist/components/ic/RoundedCornersInputController.svelte +17 -9
  19. package/dist/components/ic/TextAreaInputController.svelte +20 -7
  20. package/dist/components/ic/TextAreaPreviewInputController.svelte +19 -69
  21. package/dist/components/ic/TimeInputController.svelte +9 -3
  22. package/dist/components/ic/VectorInputController.svelte +80 -54
  23. package/dist/components/ic/shared/ItemGrid.svelte +5 -1
  24. package/dist/composition/compositionApi.d.ts +95 -0
  25. package/dist/composition/compositionApi.js +289 -0
  26. package/dist/composition/compositionEditorController.d.ts +254 -0
  27. package/dist/composition/compositionEditorController.js +734 -0
  28. package/dist/composition/compositionHelpers.d.ts +72 -0
  29. package/dist/composition/compositionHelpers.js +403 -0
  30. package/dist/index.d.ts +3 -0
  31. package/dist/index.js +4 -0
  32. package/dist/mediaLibraryContext.d.ts +10 -0
  33. package/dist/mediaLibraryContext.js +30 -0
  34. package/dist/utils/FontManager.d.ts +21 -5
  35. package/dist/utils/FontManager.js +62 -67
  36. package/dist/utils/VectorManager.d.ts +0 -1
  37. package/dist/utils/VectorManager.js +10 -12
  38. package/dist/utils/editorStorage.d.ts +23 -0
  39. package/dist/utils/editorStorage.js +95 -0
  40. package/dist/utils/fontUtils.js +5 -3
  41. package/dist/utils/htmlSafety.d.ts +21 -0
  42. package/dist/utils/htmlSafety.js +38 -0
  43. package/dist/utils/itemReorder.d.ts +90 -0
  44. package/dist/utils/itemReorder.js +354 -0
  45. package/dist/utils/recentColors.js +4 -2
  46. package/dist/utils/richTextPreview.d.ts +38 -0
  47. package/dist/utils/richTextPreview.js +207 -0
  48. package/dist/utils/scrollActivity.d.ts +14 -0
  49. package/dist/utils/scrollActivity.js +63 -0
  50. package/dist/utils/shadowDom.d.ts +67 -0
  51. package/dist/utils/shadowDom.js +211 -0
  52. package/dist/utils/vectorUtils.js +5 -3
  53. package/dist/utils/visibilityCondition.d.ts +40 -0
  54. package/dist/utils/visibilityCondition.js +535 -0
  55. package/package.json +6 -5
@@ -0,0 +1,508 @@
1
+ <script lang="ts">
2
+ /**
3
+ * The composition editor: the settings of one Widgetic composition, beside a live preview of its widget.
4
+ *
5
+ * It loads the composition, the address of its widget's preview and both schemas through the Widgetic API
6
+ * with the token `getAccessToken` gives (an editor session token is enough: that is what a partner's page
7
+ * has for its managed users), draws the settings editor and the preview, and saves what is changed: by itself
8
+ * a moment after the last change, and when "Save" is pressed.
9
+ *
10
+ * The page that holds it hears about it through events:
11
+ * ready { name, widgetId } the editor is on the screen
12
+ * dirty { dirty } there is, or is no longer, work that is not saved
13
+ * saved { composition, savedAt }
14
+ * error { during, failure, fatal } during is 'load', 'schemas' or 'save'
15
+ * deviceView { deviceView } the preview is looked at as 'desktop' or 'mobile' now (a button of the
16
+ * preview, a size the user gave it, or `setDeviceView`)
17
+ *
18
+ * A page with controls of its own for what the editor shows can use `setDeviceView` (the same toggle as the
19
+ * preview's buttons) and, when it saved the composition's embed settings itself, `adoptEmbedSettings`.
20
+ *
21
+ * Closing it: `await flush()` first, then remove the component. Removing it cancels whatever is on its way,
22
+ * and edits that were not saved are lost.
23
+ *
24
+ * The component is made for ONE composition: to edit another one, make another component.
25
+ */
26
+ import { createEventDispatcher, onDestroy, onMount, tick } from 'svelte';
27
+ import { createWidgeticMediaLibrary, type MediaLibraryProvider } from '@widgetic/file-browser';
28
+ import PropsEditor from './PropsEditor.svelte';
29
+ import WidgetPreview from '../WidgetPreview.svelte';
30
+ import { setGoogleFontsApiKey } from '../../config';
31
+ import FontManager from '../../utils/FontManager';
32
+ import { createCompositionApi, type TokenReason } from '../../composition/compositionApi.js';
33
+ import {
34
+ CompositionEditorController,
35
+ failureMessage,
36
+ type CompositionEditorEvent,
37
+ type CompositionEditorState,
38
+ type EditorFailure,
39
+ type PreviewMessage,
40
+ type PropertyChange
41
+ } from '../../composition/compositionEditorController.js';
42
+
43
+ const dispatch = createEventDispatcher<{
44
+ ready: { name: string; widgetId: string };
45
+ dirty: { dirty: boolean };
46
+ saved: { composition: Record<string, unknown>; savedAt: number };
47
+ error: { during: 'load' | 'schemas' | 'save'; failure: EditorFailure; fatal: boolean };
48
+ deviceView: { deviceView: 'desktop' | 'mobile' };
49
+ }>();
50
+
51
+ let className = '';
52
+ export { className as class };
53
+ export let style = '';
54
+
55
+ /** The composition to edit. */
56
+ export let compositionId: string;
57
+ /** The Widgetic API's address, with its version: `https://api.widgetic.com/v1`. */
58
+ export let apiBasePath: string;
59
+ /**
60
+ * Gives the token to call the API with. Asked before every request, so a host that keeps its token fresh is
61
+ * always used up to date; asked again with `'expired'` when the API refuses a token, after which the request
62
+ * is sent once more if the token that comes back is a different one. That second call also gets the token that
63
+ * was refused (`refusedToken`), so a page can tell the refusal of its current token from the late refusal of
64
+ * one it has already replaced, when several requests are refused together.
65
+ */
66
+ export let getAccessToken: (reason: TokenReason, refusedToken?: string) => Promise<string> | string;
67
+ /**
68
+ * Where the file picker gets its files, when they are not the Widgetic library: the partner's own media
69
+ * library. Without it the files are the ones of the Widgetic library the token can see.
70
+ */
71
+ export let mediaLibrary: MediaLibraryProvider | undefined = undefined;
72
+ /** The key the font pickers list Google Fonts with. Without one only the fonts that need no key are listed. */
73
+ export let googleFontsApiKey = '';
74
+ /** Wait this long after the last change before saving by itself; `0` turns saving by itself off. Read once, when the editor starts. */
75
+ export let autosaveDelayMs: number | undefined = undefined;
76
+ /** Writes what the layout does to the console. */
77
+ export let debug = false;
78
+
79
+ // --- the controller -------------------------------------------------------------------------------
80
+
81
+ // A wrong address or token function throws here, when the editor is made: it is a mistake in the page, not a failure to handle.
82
+ // The token function is read when it is called, so a page that gives a new one later is heard.
83
+ const api = createCompositionApi({
84
+ apiBasePath,
85
+ getAccessToken: (reason, refusedToken) => getAccessToken(reason, refusedToken)
86
+ });
87
+ const controller = new CompositionEditorController({
88
+ compositionId,
89
+ api,
90
+ autosaveDelayMs,
91
+ // The preview page hears about every change as it happens (and everything, once its page has loaded).
92
+ onPreviewMessage: (message: PreviewMessage) =>
93
+ previewRef?.sendMessageToPreview?.({ ...message })
94
+ });
95
+
96
+ let state: CompositionEditorState = controller.getState();
97
+ // What the props editor is given is only replaced when it really is another object: the props editor takes a
98
+ // replaced prop for a change, whether the value differs or not.
99
+ let propsEditorContentSchema: any = controller.getPropsEditorContentSchema();
100
+ let propsEditorContentValues: Record<string, unknown> = controller.getPropsEditorContentValues();
101
+
102
+ // The view the page was last told about, so it hears of a change once, whoever made it.
103
+ let announcedDeviceView = state.deviceView;
104
+
105
+ const stopWatchingState = controller.subscribe((next) => {
106
+ state = next;
107
+ const contentSchema = controller.getPropsEditorContentSchema();
108
+ if (contentSchema !== propsEditorContentSchema) propsEditorContentSchema = contentSchema;
109
+ const contentValues = controller.getPropsEditorContentValues();
110
+ if (contentValues !== propsEditorContentValues) propsEditorContentValues = contentValues;
111
+ if (next.deviceView !== announcedDeviceView) {
112
+ announcedDeviceView = next.deviceView;
113
+ dispatch('deviceView', { deviceView: next.deviceView });
114
+ }
115
+ });
116
+
117
+ const stopWatchingEvents = controller.onEvent((event: CompositionEditorEvent) => {
118
+ switch (event.type) {
119
+ case 'ready':
120
+ dispatch('ready', {
121
+ name: controller.getState().name,
122
+ widgetId: controller.getState().widgetId
123
+ });
124
+ break;
125
+ case 'dirty':
126
+ dispatch('dirty', { dirty: event.dirty });
127
+ break;
128
+ case 'saved':
129
+ dispatch('saved', { composition: event.composition, savedAt: event.savedAt });
130
+ break;
131
+ case 'error':
132
+ dispatch('error', { during: event.during, failure: event.failure, fatal: event.fatal });
133
+ break;
134
+ }
135
+ });
136
+
137
+ // --- what the page can ask of it ------------------------------------------------------------------
138
+
139
+ /** Saves what is unsaved now. Resolves `true` when nothing is left unsaved afterwards. */
140
+ export function save(): Promise<boolean> {
141
+ return controller.save('manual');
142
+ }
143
+
144
+ /** Saves everything unsaved and waits until it is. Resolves `true` when nothing is left unsaved. Call it before removing the editor. */
145
+ export function flush(): Promise<boolean> {
146
+ return controller.flush();
147
+ }
148
+
149
+ /** Whether there is work that is not saved. */
150
+ export function isDirty(): boolean {
151
+ return controller.getState().dirty;
152
+ }
153
+
154
+ /** What the editor knows now (the composition's values, whether it is saving, the last error...). */
155
+ export function getState(): CompositionEditorState {
156
+ return controller.getState();
157
+ }
158
+
159
+ /** Loads the composition again. Edits that are not saved are lost. */
160
+ export function reload(): Promise<void> {
161
+ return controller.load();
162
+ }
163
+
164
+ /**
165
+ * Looks at the preview as a phone (`'mobile'`) or at the size that is saved (`'desktop'`): what the preview's own
166
+ * buttons do. Nothing becomes unsaved. Does nothing before the editor is ready.
167
+ */
168
+ export function setDeviceView(deviceView: 'desktop' | 'mobile'): void {
169
+ setDevicePreview(deviceView === 'mobile');
170
+ }
171
+
172
+ /**
173
+ * Tells the editor that the page saved the composition's embed settings itself, so a later save does not put
174
+ * the ones the editor loaded back. Call it after `flush()`, with the settings the API now holds.
175
+ */
176
+ export function adoptEmbedSettings(settings: Record<string, unknown>): void {
177
+ controller.adoptEmbedSettings(settings);
178
+ // (The preview may have a new size: the pair is placed again.)
179
+ void tick().then(() => dockFloatingEditorOnce());
180
+ }
181
+
182
+ // --- the preview ----------------------------------------------------------------------------------
183
+
184
+ let previewRef: {
185
+ sendMessageToPreview?: (message: Record<string, unknown>) => void;
186
+ setShellPosition?: (left: number, top: number) => void;
187
+ } | null = null;
188
+ let previewHydrating = false;
189
+ let isContentReady = false;
190
+
191
+ $: previewSrc = state.preview?.previewUrl ?? null;
192
+ $: previewLoading = state.phase === 'idle' || state.phase === 'loading';
193
+ $: isMobilePreview = state.deviceView === 'mobile';
194
+ // "Loading editor…" sits in the middle while the composition loads, and where the editor is about to appear after.
195
+ $: loadingCardPosition =
196
+ state.phase === 'ready' ? 'left-4 top-4' : 'left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2';
197
+ // A preview address that has just arrived is a page still to load: the preview says so until its frame has loaded.
198
+ $: startHydrating(previewSrc);
199
+
200
+ function startHydrating(address: string | null) {
201
+ previewHydrating = Boolean(address);
202
+ isContentReady = false;
203
+ }
204
+
205
+ async function handlePreviewIframeLoad() {
206
+ log('Preview frame loaded');
207
+ controller.syncPreview();
208
+ await tick();
209
+ requestAnimationFrame(() => {
210
+ previewHydrating = false;
211
+ isContentReady = true;
212
+ });
213
+ }
214
+
215
+ function handlePreviewResize(
216
+ event: CustomEvent<{ width: number; height: number; source?: string }>
217
+ ) {
218
+ // A box smaller than the preview shrinks the preview to fit; that is the box's doing, not a size the user chose,
219
+ // so it is not saved (the composition would change just by being opened in a small window).
220
+ if (event.detail.source === 'clamp') {
221
+ log('The preview was shrunk to fit its box; the saved size is unchanged', event.detail);
222
+ return;
223
+ }
224
+ controller.setPreviewSize(event.detail.width, event.detail.height);
225
+ }
226
+
227
+ function setDevicePreview(mobile: boolean) {
228
+ controller.setDeviceView(mobile ? 'mobile' : 'desktop');
229
+ void tick().then(() => dockFloatingEditorOnce());
230
+ }
231
+
232
+ // --- the props editor -----------------------------------------------------------------------------
233
+
234
+ let propsEditorRef: {
235
+ setPosition?: (left: number, top: number) => void;
236
+ getPosition?: () => { left: number; top: number };
237
+ } | null = null;
238
+
239
+ function handleEditorPropertyChange(event: CustomEvent<PropertyChange>) {
240
+ const { schema, propertyId, value, itemId } = event.detail;
241
+ controller.changeProperty({ schema, propertyId, value, itemId });
242
+ }
243
+
244
+ function handleEditorContentItemsChanged(event: CustomEvent<{ items: unknown[] }>) {
245
+ controller.changeContentItems(event.detail.items);
246
+ }
247
+
248
+ // The Widgetic library, as the file picker's source, when the page brings none of its own.
249
+ $: widgetId = state.widgetId;
250
+ $: defaultMediaLibrary =
251
+ !mediaLibrary && widgetId ? createDefaultMediaLibrary(widgetId) : undefined;
252
+
253
+ function createDefaultMediaLibrary(forWidgetId: string): MediaLibraryProvider {
254
+ return createWidgeticMediaLibrary({
255
+ apiBasePath,
256
+ getAccessToken: (reason, refusedToken) => getAccessToken(reason, refusedToken),
257
+ contextType: 'content-property',
258
+ source: 'file-browser-upload',
259
+ widgetId: forWidgetId,
260
+ // An editor session cannot use the resumable upload: files over its limit are refused in words instead of with a 403.
261
+ allowResumableUploads: false
262
+ });
263
+ }
264
+
265
+ $: if (googleFontsApiKey) setGoogleFontsApiKey(googleFontsApiKey);
266
+
267
+ // --- the layout: settings editor left of the middle, preview right of it ------------------------
268
+
269
+ const EDITOR_WIDTH_PX = 450;
270
+ const EDITOR_PADDING = 20;
271
+ const EDITOR_MIN_HEIGHT = 360;
272
+ const PREVIEW_CHROME_HEIGHT = 36;
273
+
274
+ let editorShellEl: HTMLElement | null = null;
275
+ let floatingEditorHeight = 640;
276
+
277
+ $: schemasAvailable = Boolean(state.designSchema || state.contentSchema);
278
+ $: editorVisible =
279
+ state.phase === 'ready' && !state.schemasLoading && !state.schemasError && schemasAvailable;
280
+ // Placed once the editor is on the screen, not on every change: it must not jump back while it is being dragged.
281
+ $: if (editorVisible) requestDock();
282
+
283
+ function log(message: string, details?: unknown) {
284
+ if (!debug) return;
285
+ if (details === undefined) console.log(`[CompositionEditorApp] ${message}`);
286
+ else console.log(`[CompositionEditorApp] ${message}`, details);
287
+ }
288
+
289
+ function clampEditorHeight(containerHeight: number): number {
290
+ return Math.max(EDITOR_MIN_HEIGHT, containerHeight - EDITOR_PADDING * 2);
291
+ }
292
+
293
+ function requestDock() {
294
+ void tick().then(() => requestAnimationFrame(() => dockFloatingEditorOnce()));
295
+ }
296
+
297
+ /** Editor left of the middle, preview right of it, each EDITOR_PADDING from the middle. */
298
+ function dockFloatingEditorOnce() {
299
+ if (!editorShellEl) return;
300
+ try {
301
+ const shellWidth = editorShellEl.clientWidth || window.innerWidth;
302
+ const shellHeight = editorShellEl.clientHeight || window.innerHeight;
303
+ floatingEditorHeight = clampEditorHeight(shellHeight);
304
+
305
+ const middleX = Math.round(shellWidth / 2);
306
+ const previewHeight = state.previewHeight + PREVIEW_CHROME_HEIGHT;
307
+ const leftEditor = Math.max(EDITOR_PADDING, middleX - EDITOR_PADDING - EDITOR_WIDTH_PX);
308
+ const topEditor = EDITOR_PADDING;
309
+ const leftPreview = middleX + EDITOR_PADDING;
310
+ const topPreview = Math.max(EDITOR_PADDING, Math.round((shellHeight - previewHeight) / 2));
311
+
312
+ void tick().then(() => {
313
+ propsEditorRef?.setPosition?.(leftEditor, topEditor);
314
+ previewRef?.setShellPosition?.(leftPreview, topPreview);
315
+ });
316
+ log('Docked the editor and the preview', {
317
+ height: floatingEditorHeight,
318
+ middleX,
319
+ leftEditor,
320
+ topEditor,
321
+ leftPreview,
322
+ topPreview
323
+ });
324
+ } catch (error) {
325
+ console.error('[CompositionEditorApp] Placing the editor and the preview failed:', error);
326
+ }
327
+ }
328
+
329
+ /** When the box the editor is in changes size: the height follows, and the pair is placed again. */
330
+ function syncFloatingEditorHeight() {
331
+ if (!editorShellEl) return;
332
+ floatingEditorHeight = clampEditorHeight(editorShellEl.clientHeight || window.innerHeight);
333
+ dockFloatingEditorOnce();
334
+ }
335
+
336
+ // --- life -----------------------------------------------------------------------------------------
337
+
338
+ let shellObserver: ResizeObserver | null = null;
339
+
340
+ onMount(() => {
341
+ void controller.load();
342
+
343
+ window.addEventListener('resize', syncFloatingEditorHeight);
344
+ if (editorShellEl && typeof ResizeObserver !== 'undefined') {
345
+ shellObserver = new ResizeObserver(() => syncFloatingEditorHeight());
346
+ shellObserver.observe(editorShellEl);
347
+ }
348
+ });
349
+
350
+ onDestroy(() => {
351
+ // (Also runs when the page is rendered on a server, where there is no window.)
352
+ if (typeof window !== 'undefined')
353
+ window.removeEventListener('resize', syncFloatingEditorHeight);
354
+ shellObserver?.disconnect();
355
+ stopWatchingState();
356
+ stopWatchingEvents();
357
+ controller.dispose();
358
+ // The font stylesheets the editor put in the page's <head> go with it (a page of somebody else's is left as found)
359
+ FontManager.releasePageResources();
360
+ });
361
+ </script>
362
+
363
+ <div
364
+ class="composition-editor-ct relative h-full min-h-0 w-full overflow-hidden {className}"
365
+ {style}
366
+ bind:this={editorShellEl}
367
+ >
368
+ <!-- Keep preview resize handles in their own layer below the floating editor. -->
369
+ <div
370
+ class="composition-editor-preview-area absolute inset-0 z-0 flex min-h-0 min-w-0 bg-[#f3f4f6]"
371
+ >
372
+ <!--
373
+ This editor can run inside a partner's page, and a widget is code written by somebody else. The preview may
374
+ run its scripts, keep its own storage (allow-same-origin), open links in a new tab and submit forms, and
375
+ nothing more: no top navigation (it cannot send the visitor's tab away), no alert/confirm dialogs, no
376
+ downloads; of the page's permissions it may only write to the clipboard.
377
+ -->
378
+ <WidgetPreview
379
+ bind:this={previewRef}
380
+ class="composition-widget-preview h-full w-full"
381
+ mode="controlled"
382
+ sandbox="allow-scripts allow-same-origin allow-popups allow-forms"
383
+ allow="clipboard-write"
384
+ enableResize={true}
385
+ enableDrag={true}
386
+ showDimensionInputs={true}
387
+ showChromeDeviceToggle={true}
388
+ deviceMobile={isMobilePreview}
389
+ on:deviceView={(event) => setDevicePreview(event.detail.mobile)}
390
+ resizeMode="corner"
391
+ defaultStageAlign="center"
392
+ compactChrome={true}
393
+ showUrlBar={false}
394
+ src={previewSrc}
395
+ displayUrl={previewSrc}
396
+ resizeWidth={state.previewWidth}
397
+ resizeHeight={state.previewHeight}
398
+ {previewLoading}
399
+ {previewHydrating}
400
+ previewError={state.previewError}
401
+ {isContentReady}
402
+ on:iframeLoad={handlePreviewIframeLoad}
403
+ on:resize={handlePreviewResize}
404
+ />
405
+ </div>
406
+
407
+ {#if state.phase === 'failed' && state.loadError}
408
+ <!-- Nothing can be shown behind this one (no composition, so no editor and no preview), so it sits in the middle. -->
409
+ <div
410
+ role="alert"
411
+ class="composition-editor-error absolute left-1/2 top-1/2 z-30 flex w-max max-w-sm -translate-x-1/2 -translate-y-1/2 flex-col gap-2 rounded-lg border border-red-200 bg-white p-4 shadow-sm"
412
+ >
413
+ <p class="text-sm text-red-600">{failureMessage(state.loadError, 'load')}</p>
414
+ <button
415
+ type="button"
416
+ class="retry-load-btn w-fit cursor-pointer rounded-lg bg-blue-100 px-2 py-1 text-xs text-blue-700 hover:bg-blue-200 focus:outline-none focus-visible:outline-none"
417
+ on:click={() => {
418
+ void controller.load();
419
+ }}
420
+ >
421
+ Try again
422
+ </button>
423
+ </div>
424
+ {:else if state.phase !== 'ready' || state.schemasLoading}
425
+ <div
426
+ role="status"
427
+ class="composition-editor-loading absolute z-30 rounded-lg border border-gray-200 bg-white px-4 py-3 text-sm text-gray-500 shadow-sm {loadingCardPosition}"
428
+ >
429
+ Loading editor…
430
+ </div>
431
+ {:else if state.schemasError}
432
+ <div
433
+ role="alert"
434
+ class="composition-editor-error absolute left-4 top-4 z-30 flex max-w-sm flex-col gap-2 rounded-lg border border-red-200 bg-white p-4 shadow-sm"
435
+ >
436
+ <span class="text-xs text-red-600">{state.schemasError}</span>
437
+ <button
438
+ type="button"
439
+ class="retry-schemas-btn w-fit cursor-pointer rounded-lg bg-blue-100 px-2 py-1 text-xs text-blue-700 hover:bg-blue-200 focus:outline-none focus-visible:outline-none"
440
+ on:click={() => {
441
+ void controller.reloadSchemas();
442
+ }}
443
+ >
444
+ Retry
445
+ </button>
446
+ </div>
447
+ {:else if schemasAvailable}
448
+ <PropsEditor
449
+ bind:this={propsEditorRef}
450
+ class="composition-props-editor absolute z-20 shadow-lg"
451
+ style="left: {EDITOR_PADDING}px; top: {EDITOR_PADDING}px;"
452
+ designSchema={state.designSchema}
453
+ contentSchema={propsEditorContentSchema}
454
+ initialDesignValues={state.designValues}
455
+ initialContentValues={propsEditorContentValues}
456
+ mediaLibrary={mediaLibrary ?? defaultMediaLibrary}
457
+ width="{EDITOR_WIDTH_PX}px"
458
+ height="{floatingEditorHeight}px"
459
+ showDragHandle={true}
460
+ enableDrag={true}
461
+ showSaveButton={true}
462
+ saveDirty={state.dirty}
463
+ saveSaving={state.saving}
464
+ defaultTab="content"
465
+ on:propertyChange={handleEditorPropertyChange}
466
+ on:contentItemsChanged={handleEditorContentItemsChanged}
467
+ on:save={() => {
468
+ void controller.save('manual');
469
+ }}
470
+ />
471
+ {/if}
472
+
473
+ {#if state.phase === 'ready' && state.saveError}
474
+ <div
475
+ role="alert"
476
+ class="composition-editor-save-error absolute bottom-4 left-4 z-30 flex max-w-sm flex-col gap-2 rounded-lg border border-red-200 bg-white p-4 shadow-sm"
477
+ >
478
+ <p class="text-sm text-red-600">{failureMessage(state.saveError, 'save')}</p>
479
+ <button
480
+ type="button"
481
+ class="retry-save-btn w-fit cursor-pointer rounded-lg bg-blue-100 px-2 py-1 text-xs text-blue-700 hover:bg-blue-200 focus:outline-none focus-visible:outline-none"
482
+ on:click={() => {
483
+ void controller.save('manual');
484
+ }}
485
+ >
486
+ Try again
487
+ </button>
488
+ </div>
489
+ {/if}
490
+ </div>
491
+
492
+ <style>
493
+ /* The props editor's own classes are out of reach of this component, so they are styled from the box it sits in. */
494
+ .composition-editor-ct :global(.composition-props-editor .ic-input-ct) {
495
+ width: 152px !important;
496
+ }
497
+ .composition-editor-ct :global(.composition-props-editor .input-controller > div:last-child) {
498
+ width: 152px !important;
499
+ }
500
+ .composition-editor-ct :global(.composition-props-editor .color-input-main-container) {
501
+ width: 152px !important;
502
+ }
503
+ .composition-editor-ct :global(.composition-props-editor .input-controller-header .subtext) {
504
+ overflow: visible;
505
+ text-overflow: unset;
506
+ white-space: nowrap;
507
+ }
508
+ </style>
@@ -0,0 +1,75 @@
1
+ import { type MediaLibraryProvider } from '@widgetic/file-browser';
2
+ import { type TokenReason } from '../../composition/compositionApi.js';
3
+ import { type CompositionEditorState, type EditorFailure } from '../../composition/compositionEditorController.js';
4
+ interface $$__sveltets_2_IsomorphicComponent<Props extends Record<string, any> = any, Events extends Record<string, any> = any, Slots extends Record<string, any> = any, Exports = {}, Bindings = string> {
5
+ new (options: import('svelte').ComponentConstructorOptions<Props>): import('svelte').SvelteComponent<Props, Events, Slots> & {
6
+ $$bindings?: Bindings;
7
+ } & Exports;
8
+ (internal: unknown, props: Props & {
9
+ $$events?: Events;
10
+ $$slots?: Slots;
11
+ }): Exports & {
12
+ $set?: any;
13
+ $on?: any;
14
+ };
15
+ z_$$bindings?: Bindings;
16
+ }
17
+ declare const CompositionEditorApp: $$__sveltets_2_IsomorphicComponent<{
18
+ class?: string;
19
+ style?: string;
20
+ /** The composition to edit. */ compositionId: string;
21
+ /** The Widgetic API's address, with its version: `https://api.widgetic.com/v1`. */ apiBasePath: string;
22
+ /**
23
+ * Gives the token to call the API with. Asked before every request, so a host that keeps its token fresh is
24
+ * always used up to date; asked again with `'expired'` when the API refuses a token, after which the request
25
+ * is sent once more if the token that comes back is a different one. That second call also gets the token that
26
+ * was refused (`refusedToken`), so a page can tell the refusal of its current token from the late refusal of
27
+ * one it has already replaced, when several requests are refused together.
28
+ */ getAccessToken: (reason: TokenReason, refusedToken?: string) => Promise<string> | string;
29
+ /**
30
+ * Where the file picker gets its files, when they are not the Widgetic library: the partner's own media
31
+ * library. Without it the files are the ones of the Widgetic library the token can see.
32
+ */ mediaLibrary?: MediaLibraryProvider | undefined;
33
+ /** The key the font pickers list Google Fonts with. Without one only the fonts that need no key are listed. */ googleFontsApiKey?: string;
34
+ /** Wait this long after the last change before saving by itself; `0` turns saving by itself off. Read once, when the editor starts. */ autosaveDelayMs?: number | undefined;
35
+ /** Writes what the layout does to the console. */ debug?: boolean;
36
+ save?: () => Promise<boolean>;
37
+ flush?: () => Promise<boolean>;
38
+ isDirty?: () => boolean;
39
+ getState?: () => CompositionEditorState;
40
+ reload?: () => Promise<void>;
41
+ setDeviceView?: (deviceView: "desktop" | "mobile") => void;
42
+ adoptEmbedSettings?: (settings: Record<string, unknown>) => void;
43
+ }, {
44
+ ready: CustomEvent<{
45
+ name: string;
46
+ widgetId: string;
47
+ }>;
48
+ dirty: CustomEvent<{
49
+ dirty: boolean;
50
+ }>;
51
+ saved: CustomEvent<{
52
+ composition: Record<string, unknown>;
53
+ savedAt: number;
54
+ }>;
55
+ error: CustomEvent<{
56
+ during: "load" | "schemas" | "save";
57
+ failure: EditorFailure;
58
+ fatal: boolean;
59
+ }>;
60
+ deviceView: CustomEvent<{
61
+ deviceView: "desktop" | "mobile";
62
+ }>;
63
+ } & {
64
+ [evt: string]: CustomEvent<any>;
65
+ }, {}, {
66
+ save: () => Promise<boolean>;
67
+ flush: () => Promise<boolean>;
68
+ isDirty: () => boolean;
69
+ getState: () => CompositionEditorState;
70
+ reload: () => Promise<void>;
71
+ setDeviceView: (deviceView: "desktop" | "mobile") => void;
72
+ adoptEmbedSettings: (settings: Record<string, unknown>) => void;
73
+ }, string>;
74
+ type CompositionEditorApp = InstanceType<typeof CompositionEditorApp>;
75
+ export default CompositionEditorApp;