@widgetic/editor 4.0.10 → 4.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) hide show
  1. package/README.md +242 -0
  2. package/dist/components/WidgetPreview.svelte +123 -86
  3. package/dist/components/WidgetPreview.svelte.d.ts +8 -0
  4. package/dist/components/color-select/ColorSelect.svelte +4 -4
  5. package/dist/components/editors/CompositionEditorApp.svelte +562 -0
  6. package/dist/components/editors/CompositionEditorApp.svelte.d.ts +95 -0
  7. package/dist/components/editors/EmbedEditor.svelte +33 -22
  8. package/dist/components/editors/EmbedEditor.svelte.d.ts +1 -1
  9. package/dist/components/editors/PropsEditor.svelte +267 -850
  10. package/dist/components/editors/PropsEditor.svelte.d.ts +10 -3
  11. package/dist/components/ic/AngleInputController.svelte +1 -1
  12. package/dist/components/ic/BrowserInputController.svelte +58 -31
  13. package/dist/components/ic/ColorPickerInputController.svelte +55 -21
  14. package/dist/components/ic/DateFieldInputController.svelte +34 -14
  15. package/dist/components/ic/DatePickerInputController.svelte +37 -21
  16. package/dist/components/ic/DropdownInputController.svelte +21 -9
  17. package/dist/components/ic/DropdownInputController.svelte.d.ts +1 -1
  18. package/dist/components/ic/FixedPositionInputController.svelte +12 -6
  19. package/dist/components/ic/FontInputController.svelte +100 -76
  20. package/dist/components/ic/InputControllerHeader.svelte +16 -9
  21. package/dist/components/ic/NumberControls.svelte +4 -4
  22. package/dist/components/ic/NumberInputController.svelte +6 -4
  23. package/dist/components/ic/PositionInputController.svelte +23 -15
  24. package/dist/components/ic/RangeCalendarInputController.svelte +36 -20
  25. package/dist/components/ic/RoundedCornersInputController.svelte +39 -26
  26. package/dist/components/ic/SliderInputController.svelte +7 -2
  27. package/dist/components/ic/SwitchInputController.svelte +12 -3
  28. package/dist/components/ic/TagsInputController.svelte +19 -4
  29. package/dist/components/ic/TextAreaInputController.svelte +79 -51
  30. package/dist/components/ic/TextAreaPreviewInputController.svelte +27 -72
  31. package/dist/components/ic/TextInputController.svelte +17 -1
  32. package/dist/components/ic/TimeInputController.svelte +11 -4
  33. package/dist/components/ic/VectorInputController.svelte +101 -70
  34. package/dist/components/ic/shared/ItemGrid.svelte +12 -4
  35. package/dist/components/ic/shared/RecentItemsSection.svelte +16 -11
  36. package/dist/components/ic/shared/RecentItemsSection.svelte.d.ts +1 -1
  37. package/dist/composition/compositionApi.d.ts +95 -0
  38. package/dist/composition/compositionApi.js +289 -0
  39. package/dist/composition/compositionEditorController.d.ts +263 -0
  40. package/dist/composition/compositionEditorController.js +746 -0
  41. package/dist/composition/compositionHelpers.d.ts +74 -0
  42. package/dist/composition/compositionHelpers.js +405 -0
  43. package/dist/i18n/index.d.ts +4 -0
  44. package/dist/i18n/index.js +4 -0
  45. package/dist/i18n/labels.d.ts +15 -0
  46. package/dist/i18n/labels.js +57 -0
  47. package/dist/i18n/language.d.ts +228 -0
  48. package/dist/i18n/language.js +34 -0
  49. package/dist/i18n/languageKit.d.ts +54 -0
  50. package/dist/i18n/languageKit.js +102 -0
  51. package/dist/i18n/messages.d.ts +241 -0
  52. package/dist/i18n/messages.js +253 -0
  53. package/dist/i18n/translate.d.ts +6 -0
  54. package/dist/i18n/translate.js +8 -0
  55. package/dist/index.d.ts +4 -0
  56. package/dist/index.js +8 -0
  57. package/dist/mediaLibraryContext.d.ts +10 -0
  58. package/dist/mediaLibraryContext.js +30 -0
  59. package/dist/utils/FontManager.d.ts +21 -5
  60. package/dist/utils/FontManager.js +62 -67
  61. package/dist/utils/VectorManager.d.ts +0 -1
  62. package/dist/utils/VectorManager.js +10 -12
  63. package/dist/utils/dockPlacement.d.ts +12 -0
  64. package/dist/utils/dockPlacement.js +13 -0
  65. package/dist/utils/editorStorage.d.ts +23 -0
  66. package/dist/utils/editorStorage.js +95 -0
  67. package/dist/utils/fieldDirection.d.ts +13 -0
  68. package/dist/utils/fieldDirection.js +25 -0
  69. package/dist/utils/fontUtils.js +5 -3
  70. package/dist/utils/htmlSafety.d.ts +21 -0
  71. package/dist/utils/htmlSafety.js +38 -0
  72. package/dist/utils/itemReorder.d.ts +90 -0
  73. package/dist/utils/itemReorder.js +354 -0
  74. package/dist/utils/recentColors.js +4 -2
  75. package/dist/utils/richTextPreview.d.ts +38 -0
  76. package/dist/utils/richTextPreview.js +207 -0
  77. package/dist/utils/scrollActivity.d.ts +14 -0
  78. package/dist/utils/scrollActivity.js +63 -0
  79. package/dist/utils/shadowDom.d.ts +67 -0
  80. package/dist/utils/shadowDom.js +211 -0
  81. package/dist/utils/vectorUtils.js +5 -3
  82. package/dist/utils/visibilityCondition.d.ts +40 -0
  83. package/dist/utils/visibilityCondition.js +535 -0
  84. package/package.json +7 -6
package/README.md CHANGED
@@ -38,6 +38,243 @@ dispatch('propertyChange', { schema, property, value});
38
38
  }} />
39
39
  ```
40
40
 
41
+ ## Using your own media library
42
+
43
+ By default the file pickers inside the editor (the controls that open the File Browser) list and upload to the
44
+ user's Widgetic uploads. A platform that keeps its media somewhere else passes a `mediaLibrary` and every picker uses
45
+ it instead:
46
+
47
+ ```svelte
48
+ <PropsEditor {designSchema} {contentSchema} mediaLibrary={myMediaLibrary} />
49
+ ```
50
+
51
+ `mediaLibrary` is a `MediaLibraryProvider` (`list`, and optionally `upload`, `rename`, `delete`), exactly as the File
52
+ Browser defines it: see "Using your own media library" in the
53
+ [File Browser README](../file-browser/README.md). The value the editor reports for a picked file is `{ id, file, url }`
54
+ (plus `hash` when the library gives one), taken from the library's file: `id` is the library's own id, `file` its
55
+ name and `url` its address.
56
+
57
+ With a library set, the pickers make no Widgetic upload request, not even when the page address carries `apiUrl` and
58
+ `token`, and the File Browser keeps nothing on the device. Without one, nothing changes.
59
+
60
+ Needs a `@widgetic/file-browser` that supports `mediaLibrary`; an older one ignores the prop and keeps using the
61
+ Widgetic uploads.
62
+
63
+ ## Editing a whole composition
64
+
65
+ `CompositionEditorApp` is the editor with everything around it. It loads one Widgetic composition, shows its settings
66
+ (the `PropsEditor`) beside a live preview of the widget, and saves what the user changes. A page that has a token for
67
+ the Widgetic API (a user's own, or an editor session token for a partner's managed user) only has to name the
68
+ composition:
69
+
70
+ ```svelte
71
+ <script>
72
+ import { CompositionEditorApp } from '@widgetic/editor';
73
+
74
+ // Called before every request, and again with 'expired' when the API refuses the token.
75
+ async function getAccessToken(reason) {
76
+ const response = await fetch('/my-backend/widgetic-editor-session', { method: 'POST' });
77
+ return (await response.json()).token;
78
+ }
79
+ </script>
80
+
81
+ <!-- It fills the box it is put in, so give the box a height. -->
82
+ <div style="height: 100vh">
83
+ <CompositionEditorApp
84
+ compositionId="comp_123"
85
+ apiBasePath="https://api.widgetic.com/v1"
86
+ {getAccessToken}
87
+ on:saved={(event) => console.log('saved at', event.detail.savedAt)}
88
+ />
89
+ </div>
90
+ ```
91
+
92
+ Props:
93
+
94
+ - `compositionId`: the composition to edit. One component edits one composition: for another one, make another component.
95
+ - `apiBasePath`: the API's address with its version (`https://api.widgetic.com/v1`). A blank one throws when the
96
+ component is made.
97
+ - `getAccessToken`: `(reason: 'initial' | 'expired', refusedToken?: string) => string | Promise<string>`. See "The
98
+ token" below.
99
+ - `mediaLibrary` (optional): the partner's own `MediaLibraryProvider` for the file pickers (see above). Without it the
100
+ pickers use the Widgetic library the token can see.
101
+ - `googleFontsApiKey` (optional): the key the font pickers list Google Fonts with.
102
+ - `autosaveDelayMs` (optional): how long to wait after the last change before saving by itself (default 2500); `0`
103
+ turns saving by itself off. Read once, when the component is made.
104
+ - `class`, `style`: for the box the editor fills.
105
+
106
+ **The token.** `getAccessToken` is called before every request, so a page that keeps its token fresh is always used
107
+ up to date. When the API answers 401 the function is called again with `'expired'` and the token that was refused
108
+ (`refusedToken`) and, only if the token that comes back is a different one, the request is sent once more (a save is
109
+ sent with the same `Idempotency-Key` and body, so it cannot be applied twice). Several requests that start together with
110
+ the same token are refused together, and each asks: if your function has already replaced `refusedToken`, hand out the
111
+ new token instead of asking your server for another one. If the session is really over, the editor says so and `error`
112
+ reports `failure.sessionEnded`.
113
+
114
+ **Saving.** A change is saved a moment after the last one (2.5 seconds), or at once with the "Save" button. Only one
115
+ save is on its way at a time, and a change made meanwhile is saved by the next one. A failure that may pass (no
116
+ connection, a timeout, 429, 5xx) is tried again after 5, 15 and 45 seconds, the user can press "Try again" at any time,
117
+ and a failure that cannot pass (the session ended, a refusal) is not retried.
118
+
119
+ **The size.** The size the user gives the preview, by dragging its corner or typing it in, is saved with the composition
120
+ (`embedSettings.width` and `height`) and is the size the widget gets where it is embedded. Nothing else changes it: the
121
+ desktop and mobile buttons only change how the preview is looked at, and a box smaller than the preview shrinks it to
122
+ fit without changing what is saved (so opening a composition in a small window does not edit it).
123
+
124
+ Events (the data is in `event.detail`):
125
+
126
+ - `ready` `{ name, widgetId }`: the editor is on the screen.
127
+ - `dirty` `{ dirty }`: there is, or is no longer, work that is not saved.
128
+ - `saved` `{ composition, savedAt }`: a save went through (`composition` is what the API answered).
129
+ - `error` `{ during, failure, fatal }`: `during` is `'load'`, `'schemas'` or `'save'`; `failure` is
130
+ `{ message, code, status, retryable, sessionEnded, correlationId? }`; `fatal` is true when the composition could not
131
+ be loaded at all (the editor then shows its own message with "Try again").
132
+ - `deviceView` `{ deviceView }`: the preview is looked at as `'desktop'` or `'mobile'` now, whether a button of the
133
+ preview, a size the user gave it or `setDeviceView()` did it. Told once per change.
134
+
135
+ The component also has the methods `save()`, `flush()`, `isDirty()`, `getState()` and `reload()`. **Before removing
136
+ the editor, `await flush()`**: removing it cancels whatever is on its way, and changes that were not saved are lost.
137
+
138
+ For a page that has controls of its own around the editor:
139
+
140
+ - `setDeviceView('desktop' | 'mobile')` does what the preview's desktop and mobile buttons do. It is a way of looking,
141
+ not an edit: nothing becomes unsaved.
142
+ - `adoptEmbedSettings(settings)` is for a page that saves the composition's `embedSettings` itself (its own "Embed"
143
+ screen, say). The editor sends back the embed settings it loaded with every save, so without this a later save would put
144
+ them back. Call it after `await flush()` (so no save of the editor is on its way), with the settings the API now holds.
145
+ Nothing becomes unsaved, and the preview takes the size the settings name.
146
+
147
+ What it asks of the API (all of it is on an editor session's allow-list): `GET /compositions/{id}`,
148
+ `GET /compositions/{id}/preview`, `GET /compositions/{id}/schema/design`, `GET /compositions/{id}/schema/content` and
149
+ `PUT /compositions/{id}`, plus `/uploads` for the default file library. A composition whose widget is not published
150
+ has no preview address (`WIDGET_NOT_PUBLISHED`): its settings stay editable and the editor says why there is no preview.
151
+
152
+ It is a Svelte component for a page that can import it, and it takes the page's styles and fonts as they are. A
153
+ plain-JavaScript module that works on any page, inside a shadow root, is a separate step.
154
+
155
+ ## Languages
156
+
157
+ The editor shows English until it is told otherwise. `CompositionEditorApp` takes two props, and both can change while
158
+ the editor is open:
159
+
160
+ ```svelte
161
+ <CompositionEditorApp
162
+ {compositionId}
163
+ {apiBasePath}
164
+ {getAccessToken}
165
+ locale="de"
166
+ messages={{
167
+ 'editor.props.save': 'Speichern',
168
+ 'editor.app.tryAgain': 'Erneut versuchen',
169
+ 'action.cancel': 'Abbrechen',
170
+ 'shadEditor.toolbar.bold': 'Fett (⌘B)'
171
+ }}
172
+ />
173
+ ```
174
+
175
+ - **`locale`** is a BCP 47 language tag (`de`, `pt-BR`, `ar-EG`). It decides the plural rules and how numbers, dates and the
176
+ names of months and weekdays are written (all through `Intl`). It translates nothing. Blank or left out is English.
177
+ - **`messages`** replaces words, and it is ONE flat object for everything inside the editor: the editor's own words
178
+ (keys that start with `editor.`), the File Browser that opens from its file pickers (`action.`, `upload.`, `dialog.` ...,
179
+ see the File Browser's README, "Languages") and the rich text editor of its text fields (`shadEditor.`). Give the keys
180
+ you want to change; every key you leave out stays English, so a language can be added one screen at a time. A key that
181
+ does not exist is ignored, and the console says `[Editor] ...` once, with the key you probably meant.
182
+
183
+ `ALL_EDITOR_MESSAGES` is the list to hand to a translator: every key of the three catalogs with its English text.
184
+ `EDITOR_MESSAGES` is the editor's own part of it.
185
+
186
+ ```ts
187
+ import { ALL_EDITOR_MESSAGES, EDITOR_MESSAGES } from '@widgetic/editor';
188
+
189
+ console.log(Object.keys(ALL_EDITOR_MESSAGES).length); // every word a person can read in the editor
190
+ console.log(EDITOR_MESSAGES['editor.preview.compilationErrorsDetail']); // 'The generated code has syntax or compilation errors (HTTP {status}).'
191
+ ```
192
+
193
+ A message is text. `{name}` puts a value in, and `{count, plural, one {# file} other {# files}}` picks a form for a
194
+ number in the page's language (the format is the design system's: `@widgetic/design-system/i18n`). Each English message
195
+ names the values it is given; a translation may leave one out or use them in any order, but it cannot ask for a value the
196
+ message is never given.
197
+
198
+ **Finding a mistake before a person does.** `checkEditorMessages(messages)` checks a `messages` object without showing
199
+ anything: a key that does not exist (with the nearest real one), a value that is not text, a template that does not parse,
200
+ a message that asks for a value it is never given. An empty list means every message will be used. Put it in a test of your
201
+ page; `describeMessageProblem` (from `@widgetic/design-system/i18n`) turns each problem into a sentence:
202
+
203
+ ```ts
204
+ import { checkEditorMessages } from '@widgetic/editor';
205
+ import { describeMessageProblem } from '@widgetic/design-system/i18n';
206
+ import german from './editor.de.json';
207
+
208
+ expect(checkEditorMessages(german).map(describeMessageProblem)).toEqual([]);
209
+ ```
210
+
211
+ While the editor runs, a message that cannot be used is never thrown: the English text of that key is shown.
212
+
213
+ **Parts used on their own.** `PropsEditor`, `EmbedEditor` and `WidgetPreview` read their words from the language of the
214
+ `CompositionEditorApp` they sit in. A page that mounts one of them without it says English, unless a component above it
215
+ calls `provideEditorLanguage({ locale, messages })` while it is being made (it gives back a handle whose `update(...)`
216
+ changes the language later). `useEditorLanguage()` is what the parts read it with.
217
+
218
+ **What stays as it is**
219
+
220
+ - **The widget's own words.** The names, descriptions and options of a widget's settings are written by the person who made
221
+ the widget, and are shown as written.
222
+ - **What is saved with the composition.** The title of a new item ("Item 03"), "Untitled Composition" and the names a font
223
+ weight or a position is saved under ("ExtraBold", "top-left") are kept in English whatever the language. The editor shows
224
+ the weights, the tabs of the font and vector lists and the positions in the language; it saves the English name.
225
+ - **What the editor tells the page's code.** `error` events, `failure.message` and the other event details stay English: they
226
+ are for your logs. The person is told by the editor's own screen, in the language.
227
+ - **What comes from outside.** A sentence the Widgetic API sends back, or a media library you give the file pickers, is
228
+ shown as it arrived.
229
+ - **A sentence the editor said once** (why there is no preview) keeps the language it was said in until the editor says
230
+ something new. Every label and button changes at once, and so do the banners of a composition that could not be loaded and
231
+ of a save that failed: they are written when they are drawn.
232
+ - **Dates.** Months and weekdays follow `locale`, and so does the way the range control shows the chosen dates: 7 October 2026
233
+ is `10/07/26` with `en` (and with no `locale`), `07/10/26` with `en-GB` and `07.10.26` with `de`. In a language written right
234
+ to left a calendar writes each weekday in one letter (Arabic `ح ن ث ر خ ج س`) and gives a screen reader the whole name;
235
+ where the browser has no one-letter names (Urdu) the short names stay.
236
+ - **Right to left.** The layout turns around when the editor is given `dir="rtl"` (see "Right to left" below). For a language written right to left a name that is put inside a sentence sits between two invisible marks (Unicode isolates U+2068 and U+2069, made by the shared translator), so the words around it cannot turn it around; a test that compares text has to leave them out.
237
+ - **The design system.** The editor needs `@widgetic/design-system` **0.5.16 or newer** and a `@widgetic/file-browser` that
238
+ takes `locale` and `messages`: its words come from the design system's `@widgetic/design-system/i18n`.
239
+
240
+ The `@widgetic/ui-sdk` module of the composition editor takes the same `locale` and `messages` and adds `setLanguage()`.
241
+
242
+ ### Right to left
243
+
244
+ Arabic, Hebrew, Persian and Urdu start a line on the right. `CompositionEditorApp` takes one more prop for that, and it can
245
+ change while the editor is open:
246
+
247
+ ```svelte
248
+ <CompositionEditorApp {compositionId} {apiBasePath} {getAccessToken} locale="ar" messages={arabic} dir="rtl" />
249
+ ```
250
+
251
+ - **`dir`** is `'rtl'` or `'ltr'`. It is not taken from `locale`: it says which way the screen is read, and a page decides
252
+ that (`resolveTextDirection(locale)` from `@widgetic/design-system/i18n` answers it for a language tag; the `@widgetic/ui-sdk`
253
+ module does exactly this). Left out, the editor follows the direction of the screen it sits in (a component above it that
254
+ calls `provideTextDirection(store)` from `@widgetic/design-system/components/textDirection`), and with neither it is left
255
+ to right, as it always was. The editor writes `dir` on its own root, so it is read the right way wherever it is put.
256
+ - **What turns around.** The settings editor and the preview float over the editor's area and are put in place in pixels
257
+ when the editor appears and again when `dir` changes: on a right to left screen the settings editor is docked at the
258
+ right of the middle and the preview left of it, with the same gaps (`leftOnThisScreen`, `src/lib/utils/dockPlacement.ts`).
259
+ The Design and Content tabs, the label and the control of a row, the calendars (their arrows are mirrored), the sliders,
260
+ the switches and the file picker (the File Browser, which opens inside the editor's own root and is read the way the root
261
+ is) all start at the start of the line; the arrow keys of the date fields follow it, the help tip opens at the end of
262
+ the line, and each tab panel slides in from the side its tab is on.
263
+ - **What a person types** is read by its own first letter on a right to left screen (`directionOfFieldValue`,
264
+ `src/lib/utils/fieldDirection.ts`): a sentence in Latin letters is set from the left and one in Arabic from the right,
265
+ in the same form. A field with nothing in it yet has no first letter (`dir="auto"` on an empty field reads left to right,
266
+ which would put the hint and the caret at the wrong edge), so it takes the direction of the screen. The kinds `email` and
267
+ `url` are addresses and stay left to right on every screen. Note that the kind a schema gives a plain text field is
268
+ `textInput`, not an HTML `type`: only those two names are addresses.
269
+ - **Left to right on every screen.** The colour code, the drawing of the colour picker (saturation and hue), the grid that
270
+ places a box and the picker of the corners of a rounded box are drawings, not lines of text, and say `dir="ltr"` themselves.
271
+ - **Keeping it so.** `tests/editor-placement.test.mjs` reads the source of the editor with `auditPhysicalPlacement` from
272
+ `@widgetic/design-system/rtl/audit` and fails on a class or a style that puts something on a physical side (`left`,
273
+ `right`, `ml-`, `pr-`, `text-left`, four-value shorthands such as `padding: 4px 6px 4px 0` ...): write `start`/`end`
274
+ (`ms-`, `pe-`, `text-start`, `border-s`) or say why with the note `rtl-ok` on the same line or the line above.
275
+ `tests/editor-direction.test.mjs` pins the rest. A panel that went off the screen in Arabic (a `position: relative` box
276
+ is moved from where it sits in the flow, which on a right to left screen is the right edge) and a field that never read
277
+ its text by its own letter were not found by the audit but by looking at a screen: look at one.
41
278
 
42
279
  ## Input Controllers
43
280
 
@@ -91,6 +328,11 @@ npm run dev
91
328
  npm run dev -- --open
92
329
  ```
93
330
 
331
+ The dev server also serves three playground pages that need no sign-in:
332
+ `/ic` (an input controller on its own), `/embed` (the Embed editor) and
333
+ `/mock-widget`. They compile the sibling packages from source, so a change in
334
+ `design-system` or `file-browser` shows up there without a build.
335
+
94
336
  ## Build the project
95
337
 
96
338
  To create a production version of your app: