@widgetic/editor 4.0.9 → 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 -44
  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,72 @@
1
+ /**
2
+ * Pure helpers for editing a composition: reading what the API sends, merging it with the widget's schemas, and
3
+ * turning the editor's values into what the widget's preview page understands.
4
+ *
5
+ * They hold no state and touch no browser API, so they behave the same wherever the editor runs: in Widgetic's
6
+ * own site and in a partner's page. They were written for the site's composition page and moved here unchanged in
7
+ * behavior, so the site and an embedded editor read and write a composition in exactly the same way (see
8
+ * `tests/composition-helpers.test.mjs`, which pins what they do).
9
+ *
10
+ * "Schema" is the widget's description of what can be edited. Its shape is the producer's (the API's), so the
11
+ * helpers read it loosely and keep unknown fields as they are.
12
+ */
13
+ /** A JSON object whose fields are not known to the editor. */
14
+ export type JsonRecord = Record<string, unknown>;
15
+ /**
16
+ * The payload of an API answer. The API wraps what it returns as `{ success, data }`; code that already holds the
17
+ * payload (or an older, unwrapped answer) passes through unchanged.
18
+ */
19
+ export declare function unwrapApiData<T = any>(response: unknown): T;
20
+ /** The widget a composition is made of, or `''` when the record does not name one. */
21
+ export declare function compositionWidgetId(composition: JsonRecord | null | undefined): string;
22
+ /** The project a composition is filed in, or `null` when it is in none. */
23
+ export declare function compositionRecordProjectId(composition: JsonRecord | null | undefined): string | null;
24
+ /**
25
+ * The design schema as the editor draws it: a list of component groups, or `null` when there is nothing to edit.
26
+ * A schema with several groups becomes ONE group (the editor draws a single design tab) holding all their
27
+ * components; the first group names it.
28
+ */
29
+ export declare function normalizeDesignSchemaFromApiResponse(designResponse: unknown): any;
30
+ /**
31
+ * Gives a design schema that has a title font but no subtitle font a subtitle font beside the title's (a copy of
32
+ * it), so widgets that read `subtitleFont` can be edited. A schema that already has one, or has no title font to
33
+ * copy, is returned as it is.
34
+ */
35
+ export declare function ensureSubtitleFontInDesignSchema(schema: unknown): unknown;
36
+ /** The content schema as the editor draws it, or `null` when there is none. */
37
+ export declare function normalizeContentSchemaFromApiResponse(contentResponse: unknown): any;
38
+ /**
39
+ * The design values as the widget's preview page reads them: every value a string. A font becomes the CSS family
40
+ * (`"Roboto", sans-serif`) plus `<name>Family`, `<name>Weight` and `<name>Style`; a `{ value }` option becomes its
41
+ * value; an aspect ratio is written as a CSS ratio.
42
+ */
43
+ export declare function flattenDesignValuesForPreview(design: JsonRecord): Record<string, string>;
44
+ /** The default of every generic content property the schema declares, by property name. */
45
+ export declare function extractContentDefaultsFromSchema(schema: unknown): JsonRecord;
46
+ /** The saved content values, with the schema's defaults filled in for whatever was never saved. Saved values win. */
47
+ export declare function mergeMissingContentValues(existing: JsonRecord, schema: unknown): JsonRecord;
48
+ /** The flat content values (not the items) out of what a composition saved as its `content`. */
49
+ export declare function extractContentValuesFromCompositionRaw(rawContent: JsonRecord): JsonRecord;
50
+ /** The content items (the repeated entries: slides, rows, cards…) out of what a composition saved as its `content`. */
51
+ export declare function extractContentItemsFromCompositionRaw(rawContent: JsonRecord): any[];
52
+ /** The content items as the widget's preview page reads them: flat, with each property's value on the item itself. */
53
+ export declare function flattenContentItemsForPreview(items: any[]): any[];
54
+ /** The content items as the `items` map the props editor starts from: flat items by id. */
55
+ export declare function contentItemsToEditorValueMap(items: any[]): Record<string, Record<string, unknown>>;
56
+ /**
57
+ * The content items the composition holds, with the schema's definitions attached. The composition decides which
58
+ * items there are and in which order: the user can delete the items the widget starts with and drag them into another
59
+ * order, and that must still be so after the items have been through here (they used to come back in the schema's
60
+ * order, with the deleted ones among them). An item the schema lists carries the schema's definition and its saved
61
+ * values; one it does not list is shaped like the schema's template item. With no saved items (a composition nobody
62
+ * has edited yet) the schema's own are used; with no schema items the saved ones are returned as they are.
63
+ */
64
+ export declare function mergeContentItemsWithSchema(savedItems: any[], schemaItems: any[]): any[];
65
+ /** The content schema the props editor is given: the widget's, with the composition's items merged into it. */
66
+ export declare function buildPropsEditorContentSchema(schema: any, items: any[]): any;
67
+ /** A design with a title font but no subtitle font gets the title's as its subtitle font. */
68
+ export declare function seedMissingFontFaces(design: JsonRecord): JsonRecord;
69
+ /** The embed settings a composition saved, or `{}` when it saved none. */
70
+ export declare function getCompositionEmbedSettings(composition: JsonRecord): JsonRecord;
71
+ /** A size saved as text or number ("800", 800, "800px") as a positive whole number; anything else is the fallback. */
72
+ export declare function parseEmbedDimension(value: unknown, fallback: number): number;
@@ -0,0 +1,403 @@
1
+ /**
2
+ * Pure helpers for editing a composition: reading what the API sends, merging it with the widget's schemas, and
3
+ * turning the editor's values into what the widget's preview page understands.
4
+ *
5
+ * They hold no state and touch no browser API, so they behave the same wherever the editor runs: in Widgetic's
6
+ * own site and in a partner's page. They were written for the site's composition page and moved here unchanged in
7
+ * behavior, so the site and an embedded editor read and write a composition in exactly the same way (see
8
+ * `tests/composition-helpers.test.mjs`, which pins what they do).
9
+ *
10
+ * "Schema" is the widget's description of what can be edited. Its shape is the producer's (the API's), so the
11
+ * helpers read it loosely and keep unknown fields as they are.
12
+ */
13
+ /** The font family a font value falls back to in the preview when it names none. */
14
+ const DEFAULT_PREVIEW_FONT_FAMILY = 'Inter';
15
+ /**
16
+ * The payload of an API answer. The API wraps what it returns as `{ success, data }`; code that already holds the
17
+ * payload (or an older, unwrapped answer) passes through unchanged.
18
+ */
19
+ export function unwrapApiData(response) {
20
+ if (response && typeof response === 'object' && 'data' in response) {
21
+ return (response.data ?? response);
22
+ }
23
+ return response;
24
+ }
25
+ /** The widget a composition is made of, or `''` when the record does not name one. */
26
+ export function compositionWidgetId(composition) {
27
+ if (!composition)
28
+ return '';
29
+ const id = composition.widgetId ?? composition.widget_id;
30
+ return typeof id === 'string' ? id : '';
31
+ }
32
+ /** The project a composition is filed in, or `null` when it is in none. */
33
+ export function compositionRecordProjectId(composition) {
34
+ if (!composition)
35
+ return null;
36
+ const projectId = composition.projectId ?? composition.project_id;
37
+ return typeof projectId === 'string' && projectId ? projectId : null;
38
+ }
39
+ /**
40
+ * The design schema as the editor draws it: a list of component groups, or `null` when there is nothing to edit.
41
+ * A schema with several groups becomes ONE group (the editor draws a single design tab) holding all their
42
+ * components; the first group names it.
43
+ */
44
+ export function normalizeDesignSchemaFromApiResponse(designResponse) {
45
+ if (!designResponse)
46
+ return null;
47
+ const payload = designResponse?.data ?? designResponse;
48
+ if (!payload || typeof payload !== 'object')
49
+ return null;
50
+ if (Array.isArray(payload))
51
+ return payload;
52
+ const record = payload;
53
+ const groups = record.componentGroups ?? record.component_groups;
54
+ if (Array.isArray(groups)) {
55
+ if (groups.length > 1) {
56
+ const mergedComponents = groups.flatMap((group) => group.components || []);
57
+ return [
58
+ { name: groups[0]?.name || 'Design', components: mergedComponents }
59
+ ];
60
+ }
61
+ return groups;
62
+ }
63
+ return null;
64
+ }
65
+ /**
66
+ * Gives a design schema that has a title font but no subtitle font a subtitle font beside the title's (a copy of
67
+ * it), so widgets that read `subtitleFont` can be edited. A schema that already has one, or has no title font to
68
+ * copy, is returned as it is.
69
+ */
70
+ export function ensureSubtitleFontInDesignSchema(schema) {
71
+ if (!schema)
72
+ return schema;
73
+ try {
74
+ const clone = structuredClone(schema);
75
+ const groups = Array.isArray(clone) ? clone : clone.componentGroups;
76
+ if (!Array.isArray(groups) || groups.length === 0)
77
+ return schema;
78
+ const allProps = groups.flatMap((group) => (group.components || []).flatMap((component) => component.properties || []));
79
+ const hasSubtitle = allProps.some((prop) => /subtitleFont|captionFont/i.test(String(prop?.name || prop?.id || '')));
80
+ if (hasSubtitle)
81
+ return clone;
82
+ const titleProp = allProps.find((prop) => {
83
+ const key = String(prop?.name || prop?.id || '');
84
+ const type = String(prop?.inputController?.type || '').toLowerCase();
85
+ return type === 'font' && /titleFont|^font$/i.test(key);
86
+ });
87
+ if (!titleProp || typeof titleProp !== 'object')
88
+ return clone;
89
+ const firstComponent = groups[0]?.components?.[0];
90
+ if (!firstComponent)
91
+ return clone;
92
+ firstComponent.properties = Array.isArray(firstComponent.properties)
93
+ ? firstComponent.properties
94
+ : [];
95
+ const subtitleProp = {
96
+ ...structuredClone(titleProp),
97
+ id: 'subtitleFont',
98
+ name: 'subtitleFont',
99
+ description: 'Subtitle Font'
100
+ };
101
+ const titleIndex = firstComponent.properties.findIndex((prop) => (prop?.name || prop?.id) ===
102
+ (titleProp.name ||
103
+ titleProp.id));
104
+ if (titleIndex >= 0)
105
+ firstComponent.properties.splice(titleIndex + 1, 0, subtitleProp);
106
+ else
107
+ firstComponent.properties.push(subtitleProp);
108
+ return clone;
109
+ }
110
+ catch (error) {
111
+ console.warn('[CompositionEditor] The subtitle font could not be added to the design schema:', error);
112
+ return schema;
113
+ }
114
+ }
115
+ /** The content schema as the editor draws it, or `null` when there is none. */
116
+ export function normalizeContentSchemaFromApiResponse(contentResponse) {
117
+ if (!contentResponse)
118
+ return null;
119
+ const payload = contentResponse?.data ?? contentResponse;
120
+ return payload && typeof payload === 'object' ? payload : null;
121
+ }
122
+ /** A font weight name ("Bold Italic", "600") as the CSS weight and style a page needs. Unknown names are 400/normal. */
123
+ function parseFontFace(weight) {
124
+ const raw = String(weight || 'Regular');
125
+ const cssStyle = /italic|oblique/i.test(raw) ? 'italic' : 'normal';
126
+ const name = raw
127
+ .toLowerCase()
128
+ .replace(/italic|oblique/g, '')
129
+ .replace(/[\s_-]/g, '');
130
+ const table = {
131
+ thin: '100',
132
+ hairline: '100',
133
+ extralight: '200',
134
+ ultralight: '200',
135
+ light: '300',
136
+ regular: '400',
137
+ normal: '400',
138
+ '': '400',
139
+ medium: '500',
140
+ semibold: '600',
141
+ demibold: '600',
142
+ bold: '700',
143
+ extrabold: '800',
144
+ ultrabold: '800',
145
+ black: '900',
146
+ heavy: '900'
147
+ };
148
+ if (/^\d{3}$/.test(name))
149
+ return { cssWeight: name, cssStyle };
150
+ return { cssWeight: table[name] || '400', cssStyle };
151
+ }
152
+ /** An aspect ratio the way a stylesheet writes it ("landscape", "16:9" → "4/3", "16/9"). Unknown values stay as they are. */
153
+ function mapAspectRatioToCss(raw) {
154
+ const key = raw.trim().toLowerCase();
155
+ const table = {
156
+ square: '1/1',
157
+ '1:1': '1/1',
158
+ '1/1': '1/1',
159
+ landscape: '4/3',
160
+ '4:3': '4/3',
161
+ '4/3': '4/3',
162
+ wide: '16/9',
163
+ '16:9': '16/9',
164
+ '16/9': '16/9',
165
+ portrait: '3/4',
166
+ '3:4': '3/4',
167
+ '3/4': '3/4'
168
+ };
169
+ return table[key] || raw;
170
+ }
171
+ /**
172
+ * The design values as the widget's preview page reads them: every value a string. A font becomes the CSS family
173
+ * (`"Roboto", sans-serif`) plus `<name>Family`, `<name>Weight` and `<name>Style`; a `{ value }` option becomes its
174
+ * value; an aspect ratio is written as a CSS ratio.
175
+ */
176
+ export function flattenDesignValuesForPreview(design) {
177
+ const out = {};
178
+ for (const [key, value] of Object.entries(design || {})) {
179
+ if (value && typeof value === 'object' && 'family' in value) {
180
+ const font = value;
181
+ const family = String(font.family || DEFAULT_PREVIEW_FONT_FAMILY);
182
+ const face = parseFontFace(font.weight);
183
+ out[key] = `"${family}", sans-serif`;
184
+ out[`${key}Family`] = family;
185
+ out[`${key}Weight`] = face.cssWeight;
186
+ out[`${key}Style`] = face.cssStyle;
187
+ continue;
188
+ }
189
+ if (value && typeof value === 'object' && 'value' in value) {
190
+ out[key] = mapAspectRatioToCss(String(value.value ?? ''));
191
+ continue;
192
+ }
193
+ if (typeof value === 'string') {
194
+ out[key] = /aspect/i.test(key) ? mapAspectRatioToCss(value) : value;
195
+ continue;
196
+ }
197
+ if (value == null)
198
+ continue;
199
+ out[key] = String(value);
200
+ }
201
+ return out;
202
+ }
203
+ /** The default of every generic content property the schema declares, by property name. */
204
+ export function extractContentDefaultsFromSchema(schema) {
205
+ const values = {};
206
+ if (!schema || typeof schema !== 'object')
207
+ return values;
208
+ const record = schema;
209
+ const props = record.genericContent?.properties ?? record.data?.genericContent?.properties ?? [];
210
+ if (!Array.isArray(props))
211
+ return values;
212
+ for (const prop of props) {
213
+ const p = prop;
214
+ const name = p?.name || p?.id;
215
+ if (name && p.defaultValue !== undefined && p.defaultValue !== null) {
216
+ values[name] = p.defaultValue;
217
+ }
218
+ }
219
+ return values;
220
+ }
221
+ /** The saved content values, with the schema's defaults filled in for whatever was never saved. Saved values win. */
222
+ export function mergeMissingContentValues(existing, schema) {
223
+ return { ...extractContentDefaultsFromSchema(schema), ...existing };
224
+ }
225
+ /** The flat content values (not the items) out of what a composition saved as its `content`. */
226
+ export function extractContentValuesFromCompositionRaw(rawContent) {
227
+ const nested = rawContent.content;
228
+ if (nested && typeof nested === 'object' && !Array.isArray(nested)) {
229
+ const { items: _items, contentItemsSnapshot: _snapshot, contentItems: _contentItems, content_items: _contentItemsSnake, ...flatContent } = nested;
230
+ return flatContent;
231
+ }
232
+ return (rawContent.content ?? {});
233
+ }
234
+ /** The content items (the repeated entries: slides, rows, cards…) out of what a composition saved as its `content`. */
235
+ export function extractContentItemsFromCompositionRaw(rawContent) {
236
+ if (Array.isArray(rawContent.contentItems))
237
+ return [...rawContent.contentItems];
238
+ if (Array.isArray(rawContent.content_items))
239
+ return [...rawContent.content_items];
240
+ const nested = rawContent.content;
241
+ if (nested && typeof nested === 'object' && !Array.isArray(nested)) {
242
+ const nestedContent = nested;
243
+ if (Array.isArray(nestedContent.contentItemsSnapshot)) {
244
+ return [...nestedContent.contentItemsSnapshot];
245
+ }
246
+ if (Array.isArray(nestedContent.contentItems)) {
247
+ return [...nestedContent.contentItems];
248
+ }
249
+ if (Array.isArray(nestedContent.content_items)) {
250
+ return [...nestedContent.content_items];
251
+ }
252
+ const itemsMap = nestedContent.items;
253
+ if (itemsMap && typeof itemsMap === 'object' && !Array.isArray(itemsMap)) {
254
+ return Object.entries(itemsMap).map(([id, item]) => ({
255
+ id,
256
+ ...(typeof item === 'object' && item !== null ? item : {})
257
+ }));
258
+ }
259
+ }
260
+ return [];
261
+ }
262
+ /** A picked file or picture is stored as `{ url, … }`; the preview wants only its address. */
263
+ function unwrapContentMediaValue(raw) {
264
+ if (raw && typeof raw === 'object' && 'url' in raw) {
265
+ return raw.url ?? '';
266
+ }
267
+ return raw;
268
+ }
269
+ /** The first candidate that holds something (not undefined, null or an empty string), as its plain value; else `''`. */
270
+ function pickLiveContentValue(...candidates) {
271
+ for (const candidate of candidates) {
272
+ const unwrapped = unwrapContentMediaValue(candidate);
273
+ if (unwrapped === undefined || unwrapped === null || unwrapped === '')
274
+ continue;
275
+ return unwrapped;
276
+ }
277
+ return '';
278
+ }
279
+ /** The content items as the widget's preview page reads them: flat, with each property's value on the item itself. */
280
+ export function flattenContentItemsForPreview(items) {
281
+ return items.map((item) => {
282
+ if (!item || typeof item !== 'object')
283
+ return item;
284
+ const flat = { id: item.id, title: item.title };
285
+ for (const [key, value] of Object.entries(item)) {
286
+ if (key === 'properties' || key === 'inputController' || key === 'id' || key === 'title')
287
+ continue;
288
+ const unwrapped = unwrapContentMediaValue(value);
289
+ if (unwrapped === undefined)
290
+ continue;
291
+ flat[key] = unwrapped;
292
+ }
293
+ const props = item.properties;
294
+ if (Array.isArray(props)) {
295
+ for (const prop of props) {
296
+ const name = prop?.name || prop?.id;
297
+ if (!name)
298
+ continue;
299
+ if (flat[name] !== undefined && flat[name] !== '')
300
+ continue;
301
+ const picked = pickLiveContentValue(prop.value, prop.defaultValue);
302
+ if (picked !== '')
303
+ flat[name] = picked;
304
+ }
305
+ }
306
+ return flat;
307
+ });
308
+ }
309
+ /** The content items as the `items` map the props editor starts from: flat items by id. */
310
+ export function contentItemsToEditorValueMap(items) {
311
+ const map = {};
312
+ for (const item of flattenContentItemsForPreview(items || [])) {
313
+ if (!item?.id)
314
+ continue;
315
+ map[String(item.id)] = item;
316
+ }
317
+ return map;
318
+ }
319
+ function contentItemHasProperties(item) {
320
+ return Array.isArray(item?.properties) && item.properties.length > 0;
321
+ }
322
+ /** Gives a saved item the schema's property definitions, holding the item's own value for each. */
323
+ function attachSchemaProperties(item, template) {
324
+ if (!item || typeof item !== 'object')
325
+ return item;
326
+ const templateHasProps = contentItemHasProperties(template);
327
+ if (!contentItemHasProperties(item) && !templateHasProps)
328
+ return item;
329
+ const sourceProperties = contentItemHasProperties(item)
330
+ ? item.properties
331
+ : structuredClone(template.properties);
332
+ const nextProperties = sourceProperties.map((prop) => {
333
+ const key = prop.id || prop.name;
334
+ const hasOwn = key != null && Object.prototype.hasOwnProperty.call(item, key);
335
+ const incoming = hasOwn ? item[key] : undefined;
336
+ const picked = hasOwn
337
+ ? (unwrapContentMediaValue(incoming) ?? '')
338
+ : pickLiveContentValue(prop.value, prop.defaultValue);
339
+ return {
340
+ ...prop,
341
+ value: picked
342
+ };
343
+ });
344
+ return {
345
+ ...(templateHasProps ? structuredClone(template) : {}),
346
+ ...item,
347
+ id: item.id,
348
+ title: item.title || template?.title,
349
+ properties: nextProperties
350
+ };
351
+ }
352
+ /**
353
+ * The content items the composition holds, with the schema's definitions attached. The composition decides which
354
+ * items there are and in which order: the user can delete the items the widget starts with and drag them into another
355
+ * order, and that must still be so after the items have been through here (they used to come back in the schema's
356
+ * order, with the deleted ones among them). An item the schema lists carries the schema's definition and its saved
357
+ * values; one it does not list is shaped like the schema's template item. With no saved items (a composition nobody
358
+ * has edited yet) the schema's own are used; with no schema items the saved ones are returned as they are.
359
+ */
360
+ export function mergeContentItemsWithSchema(savedItems, schemaItems) {
361
+ if (!Array.isArray(schemaItems) || schemaItems.length === 0) {
362
+ return Array.isArray(savedItems) ? [...savedItems] : [];
363
+ }
364
+ if (!Array.isArray(savedItems) || savedItems.length === 0) {
365
+ return structuredClone(schemaItems);
366
+ }
367
+ const template = schemaItems.find(contentItemHasProperties) || schemaItems[0];
368
+ const schemaItemsById = new Map(schemaItems.map((schemaItem) => [schemaItem.id, schemaItem]));
369
+ return savedItems.map((savedItem) => {
370
+ const schemaItem = schemaItemsById.get(savedItem?.id);
371
+ if (!schemaItem)
372
+ return attachSchemaProperties(savedItem, template);
373
+ const clonedSchema = structuredClone(schemaItem);
374
+ return attachSchemaProperties({ ...clonedSchema, ...savedItem }, clonedSchema);
375
+ });
376
+ }
377
+ /** The content schema the props editor is given: the widget's, with the composition's items merged into it. */
378
+ export function buildPropsEditorContentSchema(schema, items) {
379
+ if (!schema)
380
+ return null;
381
+ const cloned = structuredClone(schema);
382
+ const schemaItems = Array.isArray(cloned.contentItems) ? cloned.contentItems : [];
383
+ cloned.contentItems = mergeContentItemsWithSchema(items ?? [], schemaItems);
384
+ return cloned;
385
+ }
386
+ /** A design with a title font but no subtitle font gets the title's as its subtitle font. */
387
+ export function seedMissingFontFaces(design) {
388
+ const next = { ...design };
389
+ if (!next.subtitleFont && next.titleFont) {
390
+ next.subtitleFont = next.titleFont;
391
+ }
392
+ return next;
393
+ }
394
+ /** The embed settings a composition saved, or `{}` when it saved none. */
395
+ export function getCompositionEmbedSettings(composition) {
396
+ const settings = composition.embedSettings ?? composition.embed_settings;
397
+ return settings && typeof settings === 'object' ? settings : {};
398
+ }
399
+ /** A size saved as text or number ("800", 800, "800px") as a positive whole number; anything else is the fallback. */
400
+ export function parseEmbedDimension(value, fallback) {
401
+ const parsed = parseInt(String(value ?? ''), 10);
402
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
403
+ }
package/dist/index.d.ts CHANGED
@@ -5,3 +5,6 @@ export { default as WidgetPreview } from './components/WidgetPreview.svelte';
5
5
  export { setFileBrowserUploadConfig } from '@widgetic/file-browser';
6
6
  export { setGoogleFontsApiKey, getGoogleFontsKey } from './config';
7
7
  export { default as EmbedEditor } from './components/editors/EmbedEditor.svelte';
8
+ export { default as CompositionEditorApp } from './components/editors/CompositionEditorApp.svelte';
9
+ export { CompositionApiError, createCompositionApi, type CompositionApi, type CompositionApiOptions, type CompositionPreviewInfo, type SaveCompositionBody, type TokenReason } from './composition/compositionApi.js';
10
+ export { failureMessage, type CompositionEditorEvent, type CompositionEditorState, type DeviceView, type EditorFailure } from './composition/compositionEditorController.js';
package/dist/index.js CHANGED
@@ -8,3 +8,7 @@ export { setFileBrowserUploadConfig } from '@widgetic/file-browser';
8
8
  export { setGoogleFontsApiKey, getGoogleFontsKey } from './config';
9
9
  // Embed Editor
10
10
  export { default as EmbedEditor } from './components/editors/EmbedEditor.svelte';
11
+ // Composition Editor: one Widgetic composition, loaded, edited beside a live preview, and saved through the API.
12
+ export { default as CompositionEditorApp } from './components/editors/CompositionEditorApp.svelte';
13
+ export { CompositionApiError, createCompositionApi } from './composition/compositionApi.js';
14
+ export { failureMessage } from './composition/compositionEditorController.js';
@@ -0,0 +1,10 @@
1
+ import type { MediaLibraryProvider } from '@widgetic/file-browser';
2
+ /** Gives the current media library, or `undefined` for the File Browser's own (Widgetic account) storage. */
3
+ export type MediaLibraryGetter = () => MediaLibraryProvider | undefined;
4
+ /** Called while an editor component is being created: everything below it can reach the library with {@link useEditorMediaLibrary}. */
5
+ export declare function provideEditorMediaLibrary(getMediaLibrary: MediaLibraryGetter): void;
6
+ /**
7
+ * Called while a component is being created. Gives the function that tells which media library the editor above it was
8
+ * given. Outside an editor (a control used on its own) it always gives `undefined`, as the File Browser did before.
9
+ */
10
+ export declare function useEditorMediaLibrary(): MediaLibraryGetter;
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Hands the File Browser's media library from the editor down to the file controls inside it.
3
+ *
4
+ * `PropsEditor` is given a `mediaLibrary` (a partner's own storage, for instance). The file control
5
+ * (`BrowserInputController`) sits several components below it and is created from a schema, so passing the library
6
+ * as a prop through every layer would mean threading it through the controller renderer too. A Svelte context
7
+ * reaches it directly, and stays separate for each editor on a page.
8
+ *
9
+ * What is stored is a function that reads the editor's `mediaLibrary` prop, not the library itself.
10
+ */
11
+ import { getContext, setContext } from 'svelte';
12
+ const MEDIA_LIBRARY_CONTEXT_KEY = Symbol.for('widgetic.editor.mediaLibrary');
13
+ /** Called while an editor component is being created: everything below it can reach the library with {@link useEditorMediaLibrary}. */
14
+ export function provideEditorMediaLibrary(getMediaLibrary) {
15
+ setContext(MEDIA_LIBRARY_CONTEXT_KEY, getMediaLibrary);
16
+ }
17
+ /**
18
+ * Called while a component is being created. Gives the function that tells which media library the editor above it was
19
+ * given. Outside an editor (a control used on its own) it always gives `undefined`, as the File Browser did before.
20
+ */
21
+ export function useEditorMediaLibrary() {
22
+ try {
23
+ return getContext(MEDIA_LIBRARY_CONTEXT_KEY) ?? (() => undefined);
24
+ }
25
+ catch (error) {
26
+ // getContext only works while a component is being created; anywhere else there is no editor above to ask.
27
+ console.warn('[editor] useEditorMediaLibrary was called outside a component; using the default media library', error);
28
+ return () => undefined;
29
+ }
30
+ }
@@ -32,6 +32,8 @@ declare class FontManager {
32
32
  private static readonly WAIT_TIME;
33
33
  private static instance;
34
34
  private loadedFonts;
35
+ private addedHeadElements;
36
+ private pageWasReleased;
35
37
  private observers;
36
38
  private scrollTimeout;
37
39
  private apiKey;
@@ -49,9 +51,6 @@ declare class FontManager {
49
51
  private static readonly CACHE_KEY;
50
52
  private static readonly CACHE_DURATION;
51
53
  private static readonly LOADED_FONTS_KEY;
52
- private static readonly LEGACY_CACHE_KEY;
53
- private static readonly LEGACY_LOADED_FONTS_KEY;
54
- private static readonly CACHE_CONTROL;
55
54
  private visibleFonts;
56
55
  private static initialPreloadComplete;
57
56
  private static preloadPromise;
@@ -106,7 +105,7 @@ declare class FontManager {
106
105
  */
107
106
  getPreInitializedObserver(): IntersectionObserver | null;
108
107
  /**
109
- * Loads previously cached fonts from the durable L1 store (legacy localStorage fallback)
108
+ * Loads previously cached fonts from the durable L1 store
110
109
  */
111
110
  private loadCachedFonts;
112
111
  /**
@@ -121,6 +120,23 @@ declare class FontManager {
121
120
  * Checks if a font is available in Google Fonts
122
121
  */
123
122
  private isGoogleFont;
123
+ /**
124
+ * Puts an element in the page's <head> and remembers it, so that it can be taken away again
125
+ * ({@link removeAddedHeadElements}): the editor can be on somebody else's page, which it should leave as it found it.
126
+ */
127
+ private addToHead;
128
+ /**
129
+ * Takes out of the page's <head> everything this manager put there (the stylesheets of the fonts, their preload
130
+ * hints and the connection hint), and forgets which fonts were loaded, because they are not any more. The next
131
+ * font to be asked for puts its stylesheet back. The page's own links are never touched: only what was added here
132
+ * is removed.
133
+ */
134
+ removeAddedHeadElements(): void;
135
+ /**
136
+ * Leaves the page as the editor found it as far as fonts go. Meant for when the editor is taken away; does nothing
137
+ * if no font was ever asked for.
138
+ */
139
+ static releasePageResources(): void;
124
140
  /**
125
141
  * Inserts a font stylesheet into the document
126
142
  * @param family - Font family name
@@ -161,7 +177,7 @@ declare class FontManager {
161
177
  */
162
178
  private saveFontListToCache;
163
179
  /**
164
- * Gets font list from the durable L1 cache (legacy localStorage fallback)
180
+ * Gets font list from the durable L1 cache
165
181
  */
166
182
  private getFontListFromCache;
167
183
  /**