@scaleflex/template-builder 0.4.1 → 0.5.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/.claude/skills/integrate-template-builder/SKILL.md +72 -23
- package/CHANGELOG.md +21 -1
- package/README.md +7 -7
- package/dist/protocol.d.ts +4 -4
- package/dist/react.cjs.map +1 -1
- package/dist/react.d.ts +2 -2
- package/dist/react.js.map +1 -1
- package/dist/template-builder-B9Cwo_Q-.js.map +1 -1
- package/dist/template-builder-Byqg1q93.cjs.map +1 -1
- package/dist/template-builder.d.ts +2 -2
- package/package.json +1 -1
- package/src/protocol.ts +4 -4
- package/src/react.ts +2 -2
- package/src/template-builder.ts +2 -2
|
@@ -12,7 +12,7 @@ metadata:
|
|
|
12
12
|
- design-templates
|
|
13
13
|
- web-component
|
|
14
14
|
status: ready
|
|
15
|
-
version:
|
|
15
|
+
version: 2
|
|
16
16
|
---
|
|
17
17
|
|
|
18
18
|
# Scaleflex Template Builder Integration Skill
|
|
@@ -24,6 +24,10 @@ metadata:
|
|
|
24
24
|
- User asks how to use `@scaleflex/template-builder` in their project
|
|
25
25
|
- User wants a banner / creative editor backed by Scaleflex rendering
|
|
26
26
|
|
|
27
|
+
A complete working React host (gallery, save round trip, live render
|
|
28
|
+
preview) is published at <https://stackblitz.com/edit/fzfz7f2j> — read its
|
|
29
|
+
`App.tsx` for the whole contract in one file.
|
|
30
|
+
|
|
27
31
|
## Step 1 — Decide who stores the template
|
|
28
32
|
|
|
29
33
|
This is the first question, and it changes everything downstream. Ask if it is
|
|
@@ -43,7 +47,7 @@ storage and does not want to map them onto Scaleflex tenants. Pick
|
|
|
43
47
|
Statelessness applies to the **document only**. The editor still needs an
|
|
44
48
|
authenticated Scaleflex tenant for server-side text rendering, custom fonts,
|
|
45
49
|
asset browsing, and metadata variables — so a stateless embed still needs a
|
|
46
|
-
credential (Step
|
|
50
|
+
credential (Step 8).
|
|
47
51
|
|
|
48
52
|
## Step 2 — Detect the target framework
|
|
49
53
|
|
|
@@ -61,7 +65,7 @@ npm i @scaleflex/template-builder
|
|
|
61
65
|
Or via CDN (self-registering, Lit bundled in — pin the major):
|
|
62
66
|
|
|
63
67
|
```html
|
|
64
|
-
<script type="module" src="https://cdn.cloudimage.io/template-builder/0.
|
|
68
|
+
<script type="module" src="https://cdn.cloudimage.io/template-builder/0.5.0/template-builder.min.js"></script>
|
|
65
69
|
```
|
|
66
70
|
|
|
67
71
|
**SSR caution:** the element extends `HTMLElement`, so importing `.`,
|
|
@@ -213,7 +217,46 @@ try {
|
|
|
213
217
|
|
|
214
218
|
The React wrapper does this from what `onSave` returns or throws.
|
|
215
219
|
|
|
216
|
-
## Step 6 —
|
|
220
|
+
## Step 6 — Render links and previews (optional): `dam-store`
|
|
221
|
+
|
|
222
|
+
The CDN renders only files it stores, so a stateless host that wants image
|
|
223
|
+
URLs out of its templates (previews, production banners) needs a copy in
|
|
224
|
+
Filerobot too. Set `dam-store` and the element uploads that copy itself on
|
|
225
|
+
every save, before the `save` event fires — the host writes no upload code:
|
|
226
|
+
|
|
227
|
+
```html
|
|
228
|
+
<sfx-template-builder stateless dam-store store-folder="/my-app" …>
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
```js
|
|
232
|
+
builder.addEventListener('save', (e) => {
|
|
233
|
+
const { content, templateQuery, stored, storeError } = e.detail
|
|
234
|
+
// stored = { uuid, url } — append templateQuery to url and it IS an image:
|
|
235
|
+
// <img src=`${stored.url}&${templateQuery}&force_format=png`>
|
|
236
|
+
// storeError replaces stored when the copy failed; the raw content
|
|
237
|
+
// arrives either way — whether that fails the save is your confirmSave call.
|
|
238
|
+
})
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
Wiring rules that matter:
|
|
242
|
+
|
|
243
|
+
- **Branch on the detail** (`'stored' in e.detail || 'storeError' in e.detail`),
|
|
244
|
+
never on the live `damStore` property — saves arrive from a serialized
|
|
245
|
+
upload queue and the property can change before a queued event lands.
|
|
246
|
+
- **Persist `stored.uuid` and pass it back as `stored-uuid` / `storedUuid`
|
|
247
|
+
(or the `storedUuid` field of `load()`) when reopening the template.** The
|
|
248
|
+
element's memory of its copy dies with the page; the seed is what lets a
|
|
249
|
+
re-save version the existing file in place instead of erroring on
|
|
250
|
+
unchanged content. Do NOT echo the uuid into `template-id` — an id change
|
|
251
|
+
deliberately reloads the editor.
|
|
252
|
+
- **A save can arrive after `close`** when its upload is still in flight —
|
|
253
|
+
keep the `save` listener attached until it does. Removing the element from
|
|
254
|
+
the DOM flushes pending saves to still-attached listeners immediately.
|
|
255
|
+
- The credential needs **upload rights**, or every save reports `storeError`.
|
|
256
|
+
- `store-folder` names where templates with no existing DAM file land; an
|
|
257
|
+
existing file's own folder always wins.
|
|
258
|
+
|
|
259
|
+
## Step 7 — Guard against losing edits
|
|
217
260
|
|
|
218
261
|
Assigning a new `content` / `templateId` / `templateName` / `templateQuery`
|
|
219
262
|
reloads the editor and
|
|
@@ -229,7 +272,7 @@ builder.addEventListener('dirtychange', (e) => {
|
|
|
229
272
|
Re-assigning an identical template is a no-op, so an unrelated re-render cannot
|
|
230
273
|
destroy work by accident.
|
|
231
274
|
|
|
232
|
-
## Step
|
|
275
|
+
## Step 8 — Authentication
|
|
233
276
|
|
|
234
277
|
Two credentials work. Both are minted **server-side** and injected into the
|
|
235
278
|
page; neither belongs in a public bundle.
|
|
@@ -278,7 +321,7 @@ server-side, so it authenticates on the handed-over credential and returns
|
|
|
278
321
|
template is what authorizes it when there is no Hub session. Full explanation:
|
|
279
322
|
*Why the editor still calls a server* in the README.
|
|
280
323
|
|
|
281
|
-
## Step
|
|
324
|
+
## Step 9 — Register the embedding origin (required)
|
|
282
325
|
|
|
283
326
|
The host page's origin must be in the deployment's embedding allowlist, via
|
|
284
327
|
`NEXT_PUBLIC_TRUSTED_HUB_ORIGINS` on the `design-templates-app` deployment.
|
|
@@ -288,7 +331,7 @@ This is baked in at build time, so it needs a redeploy.
|
|
|
288
331
|
and the widget reports `error` with code `handshake-timeout`. That is the
|
|
289
332
|
single most common first-integration failure — check it before anything else.
|
|
290
333
|
|
|
291
|
-
## Step
|
|
334
|
+
## Step 10 — Theming (optional)
|
|
292
335
|
|
|
293
336
|
```html
|
|
294
337
|
<sfx-template-builder brand-color="#FF6600" theme="dark" ...>
|
|
@@ -312,7 +355,7 @@ template document.
|
|
|
312
355
|
| `token` | yes | Scaleflex token (`ftoken`) |
|
|
313
356
|
| `sass-key` / `sassKey` | session auth | Project sass key |
|
|
314
357
|
| `session-uuid` / `sessionUuid` | session auth | Hub session uuid |
|
|
315
|
-
| `sec-template` / `secTemplate` | guest auth | Security-template key, instead of sass key + session uuid. Stateless only (Step
|
|
358
|
+
| `sec-template` / `secTemplate` | guest auth | Security-template key, instead of sass key + session uuid. Stateless only (Step 8) |
|
|
316
359
|
| `company-uuid`, `project-uuid` | no | Company / project scoping (session auth only) |
|
|
317
360
|
| `template-id` / `templateId` | no | DAM: the file uuid. Stateless: opaque host id |
|
|
318
361
|
| `mode` | no | `inline` (default) or `modal` (starts closed — call `open()`) |
|
|
@@ -321,7 +364,12 @@ template document.
|
|
|
321
364
|
| `new-template` / `newTemplate` | no | Stateless: open on a new, empty template — the widget supplies the blank document. Ignored when `content` is set |
|
|
322
365
|
| `template-name` / `templateName` | no | Stateless: header title |
|
|
323
366
|
| `template-query` / `templateQuery` | no | Stateless: the render to open on — the `templateQuery` from the last save. Empty uses the XML's `default=` values. |
|
|
324
|
-
| `
|
|
367
|
+
| `dam-store` / `damStore` | no | Stateless: store each save in Filerobot too; the `save` detail gains `stored: { uuid, url }` or `storeError` (Step 6) |
|
|
368
|
+
| `store-folder` / `storeFolder` | no | `dam-store`: folder for templates with no existing DAM file (default `/`) |
|
|
369
|
+
| `stored-uuid` / `storedUuid` | no | `dam-store`: the persisted `stored.uuid`, passed back so re-saves version the existing copy across reloads (Step 6) |
|
|
370
|
+
| `custom-metadata` / `customMetadata` | no | Metadata model offered as the **Custom metadata** source type: `[{ key, title?, group? }]` — names only, values go into the render query as `$slug=value` |
|
|
371
|
+
| `custom-metadata-label` / `customMetadataLabel` | no | Renames that source type in the editor's UI (wording only) |
|
|
372
|
+
| `brand-color`, `theme` | no | See Step 10 |
|
|
325
373
|
| `ready-timeout` / `readyTimeout` | no | Ms before `handshake-timeout` (default 20000) |
|
|
326
374
|
|
|
327
375
|
## Events Reference
|
|
@@ -330,26 +378,27 @@ template document.
|
|
|
330
378
|
|---|---|
|
|
331
379
|
| `ready` | — (editor mounted, auth valid) |
|
|
332
380
|
| `open` | — |
|
|
333
|
-
| `save` | `{ uuid, name }` (DAM) or `{ templateId, content, name, templateQuery }` (stateless) |
|
|
381
|
+
| `save` | `{ uuid, name }` (DAM) or `{ templateId, content, name, templateQuery }` (stateless; under `dam-store` also `stored: { uuid, url }` or `storeError`) |
|
|
334
382
|
| `dirtychange` | `{ isDirty }` (stateless) |
|
|
335
|
-
| `close` | — (user left the editor, or it unmounted) |
|
|
383
|
+
| `close` | — (user left the editor, or it unmounted). A `dam-store` save whose upload is still in flight emits **after** `close` — keep listening |
|
|
336
384
|
| `error` | `{ code, message? }` |
|
|
337
385
|
|
|
338
386
|
`error` codes: `auth`, `invalid-content`, `invalid-config`,
|
|
339
387
|
`handshake-timeout`, `invalid-base-url`, `unknown`.
|
|
340
388
|
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
save in Filerobot and adds `stored: { uuid, url }` (or `storeError`) to the
|
|
345
|
-
`save` detail — `url` + `templateQuery` is a render URL, and `store-folder`
|
|
346
|
-
names where templates with no existing DAM file land.
|
|
389
|
+
`dam-store`, `stored-uuid` and `custom-metadata-label` need bundle/package
|
|
390
|
+
**0.4.0+** — an older pinned bundle silently ignores the attributes. Check
|
|
391
|
+
the CHANGELOG against what the host pins.
|
|
347
392
|
|
|
348
393
|
## Public Methods
|
|
349
394
|
|
|
350
|
-
`open(templateId?)`, `close()`,
|
|
351
|
-
`
|
|
352
|
-
`
|
|
395
|
+
`open(templateId?)`, `close()`,
|
|
396
|
+
`load({ content, templateId?, name?, templateQuery?, storedUuid? })`
|
|
397
|
+
(omitting `storedUuid` clears the seed — it belongs to the document),
|
|
398
|
+
`createNew({ templateId?, name? })`, `confirmSave(ok, message?)`, and
|
|
399
|
+
`flushPendingSaves(reason?)` (only for framework wrappers that detach
|
|
400
|
+
listeners without removing the element; the React wrapper calls it itself).
|
|
401
|
+
Read-only: `status`, `isDirty`.
|
|
353
402
|
|
|
354
403
|
In React these are reached through a forwarded ref — **required for
|
|
355
404
|
`mode="modal"`**, which renders nothing until `open()` is called:
|
|
@@ -393,11 +442,11 @@ ignored; and `layout` / `locale` / `force_format` are reserved slugs.
|
|
|
393
442
|
|
|
394
443
|
| Symptom | Cause |
|
|
395
444
|
|---|---|
|
|
396
|
-
| `error` code `handshake-timeout`, nothing rendered | Host origin not in the deployment's embedding allowlist (Step
|
|
445
|
+
| `error` code `handshake-timeout`, nothing rendered | Host origin not in the deployment's embedding allowlist (Step 9), or third-party cookies blocked |
|
|
397
446
|
| `error` code `auth` | Bad/expired session credentials, a rejected security template, or cookies blocked |
|
|
398
447
|
| `error` code `invalid-content` | The `content` handed over is not parseable `.fdt` XML |
|
|
399
|
-
| `error` code `invalid-config` | `sec-template` without `stateless` (Step
|
|
400
|
-
| Metadata / regional / branding panels empty | Expected under `sec-template` — those come from the Hub project (Step
|
|
448
|
+
| `error` code `invalid-config` | `sec-template` without `stateless` (Step 8) |
|
|
449
|
+
| Metadata / regional / branding panels empty | Expected under `sec-template` — those come from the Hub project (Step 8) |
|
|
401
450
|
| Nothing renders, no events | `inline` mode inside a zero-height parent — size the element |
|
|
402
451
|
| Modal never appears | `open()` never called; in React that needs a ref |
|
|
403
452
|
| Saves look successful but nothing is stored | `confirmSave(false)` not wired (Step 5) |
|
package/CHANGELOG.md
CHANGED
|
@@ -8,7 +8,27 @@ Protocol message *values* are wire format: an existing string is never changed,
|
|
|
8
8
|
only new messages added, so an older widget keeps working against a newer app
|
|
9
9
|
deployment and vice versa.
|
|
10
10
|
|
|
11
|
-
## [
|
|
11
|
+
## [0.5.0] - 2026-08-31
|
|
12
|
+
|
|
13
|
+
### Changed
|
|
14
|
+
|
|
15
|
+
- Documentation follows the editor's renamed source-type labels: the value
|
|
16
|
+
source a text variable draws on is now **Source type**, and its options are
|
|
17
|
+
**Manual** (was "Free text") and **Asset metadata** (was "File metadata").
|
|
18
|
+
**Custom metadata** and `custom-metadata-label` are unchanged — the label
|
|
19
|
+
stays host-controlled, and the worked example still renames it to
|
|
20
|
+
`External metadata`. Docs only; no library or protocol changes, and nothing
|
|
21
|
+
in the saved template or the render query moves.
|
|
22
|
+
|
|
23
|
+
## [0.4.2] - 2026-08-26
|
|
24
|
+
|
|
25
|
+
### Changed
|
|
26
|
+
|
|
27
|
+
- The bundled `integrate-template-builder` skill now documents the 0.4.x
|
|
28
|
+
API: `dam-store` as a first-class integration step (detail-based
|
|
29
|
+
branching, `stored-uuid` seeding, saves trailing `close`), the full
|
|
30
|
+
attribute/event/method references, and a link to the published StackBlitz
|
|
31
|
+
host example. Docs only — no library changes.
|
|
12
32
|
|
|
13
33
|
## [0.4.1] - 2026-08-26
|
|
14
34
|
|
package/README.md
CHANGED
|
@@ -115,7 +115,7 @@ npm i @scaleflex/template-builder
|
|
|
115
115
|
### CDN
|
|
116
116
|
|
|
117
117
|
```html
|
|
118
|
-
<script type="module" src="https://cdn.cloudimage.io/template-builder/0.
|
|
118
|
+
<script type="module" src="https://cdn.cloudimage.io/template-builder/0.5.0/template-builder.min.js"></script>
|
|
119
119
|
```
|
|
120
120
|
|
|
121
121
|
The CDN bundle is self-registering — it defines `<sfx-template-builder>` on
|
|
@@ -662,8 +662,8 @@ without it a failed write is invisible to the user.
|
|
|
662
662
|
| `new-template` / `newTemplate` | no | Stateless: open on a new, empty template instead of supplying `content` — the widget provides the blank document. Ignored when `content` is set. See [Starting a template from scratch](#starting-a-template-from-scratch) |
|
|
663
663
|
| `template-name` / `templateName` | no | Stateless: header title |
|
|
664
664
|
| `template-query` / `templateQuery` | no | Stateless: the render to open on — the `templateQuery` from the last save. Empty uses the XML's `default=` values. |
|
|
665
|
-
| `custom-metadata` / `customMetadata` | no | Metadata model offered as the **Custom metadata**
|
|
666
|
-
| `custom-metadata-label` / `customMetadataLabel` | no | Renames the **Custom metadata**
|
|
665
|
+
| `custom-metadata` / `customMetadata` | no | Metadata model offered as the **Custom metadata** source type: `[{ key, title?, group? }]`, as an array (property) or JSON (attribute). See [Custom metadata fields](#custom-metadata-fields) |
|
|
666
|
+
| `custom-metadata-label` / `customMetadataLabel` | no | Renames the **Custom metadata** source type in the editor's UI (e.g. `External metadata`). Wording only — the stored template is unaffected. Empty uses the default |
|
|
667
667
|
| `dam-store` / `damStore` | no | Stateless only: the element stores each save in Filerobot too, and `save`'s detail carries `stored: { uuid, url }` (or `storeError`) next to the raw `content`. See [Storing a rendering copy](#storing-a-rendering-copy-dam-store) |
|
|
668
668
|
| `store-folder` / `storeFolder` | no | `dam-store`: folder for templates whose id names no existing DAM file (default `/`); an existing file's own folder always wins |
|
|
669
669
|
| `stored-uuid` / `storedUuid` | no | `dam-store`: the `stored.uuid` you persisted for this document, passed back in so re-saves after a reload resolve to (and version) the existing copy. Per-document — `load()` clears it when omitted |
|
|
@@ -754,10 +754,10 @@ before you pick it:
|
|
|
754
754
|
|
|
755
755
|
### Custom metadata fields
|
|
756
756
|
|
|
757
|
-
A text variable normally takes its value from the render query (**
|
|
758
|
-
or from the source asset's DAM metadata (**
|
|
757
|
+
A text variable normally takes its value from the render query (**Manual**),
|
|
758
|
+
or from the source asset's DAM metadata (**Asset metadata**, which needs a
|
|
759
759
|
Hub session — it is unavailable under `sec-template`). `custom-metadata` adds a
|
|
760
|
-
third source: your own field names, so an author binds a variable to `sku`
|
|
760
|
+
third source type: your own field names, so an author binds a variable to `sku`
|
|
761
761
|
instead of having to remember which slug happens to mean the SKU.
|
|
762
762
|
|
|
763
763
|
```html
|
|
@@ -783,7 +783,7 @@ Fields appear in the editor's picker in the order you declare them, grouped
|
|
|
783
783
|
under `group` where present, with `title` (or the bare `key`) as the label. Set
|
|
784
784
|
no model and the source is not offered at all.
|
|
785
785
|
|
|
786
|
-
The source is called **Custom metadata** in the editor by default;
|
|
786
|
+
The source type is called **Custom metadata** in the editor by default;
|
|
787
787
|
`custom-metadata-label` renames it to fit your domain — `External metadata`,
|
|
788
788
|
`Product attributes`, whatever your authors know it as. Wording only: the saved
|
|
789
789
|
template carries the same `custom_ckey` either way. (The label is another
|
package/dist/protocol.d.ts
CHANGED
|
@@ -189,7 +189,7 @@ export declare const BLANK_TEMPLATE_XML: string;
|
|
|
189
189
|
export declare const HOST_CONFIG = "design-templates:host:config";
|
|
190
190
|
/**
|
|
191
191
|
* One field of a host-supplied metadata model, offered in the editor as the
|
|
192
|
-
* "Custom metadata"
|
|
192
|
+
* "Custom metadata" source type.
|
|
193
193
|
*
|
|
194
194
|
* The model is a vocabulary, not data: it names the fields the host can fill at
|
|
195
195
|
* render time, so an author can bind a variable to `sku` rather than having to
|
|
@@ -198,7 +198,7 @@ export declare const HOST_CONFIG = "design-templates:host:config";
|
|
|
198
198
|
* query, exactly as it would for a free-text variable.
|
|
199
199
|
*
|
|
200
200
|
* This is what makes named fields workable in `secTemplate` / stateless embeds,
|
|
201
|
-
* where the Hub project model (and with it the "
|
|
201
|
+
* where the Hub project model (and with it the "Asset metadata" source) is
|
|
202
202
|
* unavailable.
|
|
203
203
|
*/
|
|
204
204
|
export interface CustomMetadataField {
|
|
@@ -214,13 +214,13 @@ export interface CustomMetadataField {
|
|
|
214
214
|
}
|
|
215
215
|
export interface HostConfigData {
|
|
216
216
|
/**
|
|
217
|
-
* Metadata model offered as the "Custom metadata"
|
|
217
|
+
* Metadata model offered as the "Custom metadata" source type. Omitted or
|
|
218
218
|
* empty hides that source in the editor, so a host that sends nothing sees
|
|
219
219
|
* the two sources it always had.
|
|
220
220
|
*/
|
|
221
221
|
customMetadata?: CustomMetadataField[];
|
|
222
222
|
/**
|
|
223
|
-
* Display name for the custom-metadata
|
|
223
|
+
* Display name for the custom-metadata source type in the editor's UI
|
|
224
224
|
* (source dropdowns, properties-panel section). Defaults to "Custom
|
|
225
225
|
* metadata"; a host can rename it after its own domain — e.g. "External
|
|
226
226
|
* metadata" or "Product attributes". Pure wording: the stored template is
|
package/dist/react.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"react.cjs","sources":["../src/react.ts"],"sourcesContent":["import {\n createElement,\n forwardRef,\n useImperativeHandle,\n useLayoutEffect,\n useRef,\n type CSSProperties,\n type ReactElement,\n} from 'react'\nimport './define'\nimport type { SfxTemplateBuilder } from './template-builder'\nimport type { TemplateBuilderSaveDetail } from './template-builder'\nimport type {\n BuilderDirtyData,\n BuilderErrorData,\n BuilderTheme,\n CustomMetadataField,\n} from './protocol'\n\n/**\n * Hub session — the full-featured credential.\n */\nexport interface TemplateBuilderSessionAuth {\n sassKey: string\n sessionUuid: string\n companyUuid?: string\n projectUuid?: string\n secTemplate?: never\n}\n\n/**\n * Filerobot security template — a guest credential for hosts that have no Hub\n * session to hand over. `stateless` is required rather than merely implied:\n * the app takes a security template on the stateless embed route only, so the\n * combination is a compile-time error instead of a runtime one.\n *\n * `companyUuid` / `projectUuid` are absent by design — they name a Hub project\n * that cannot be looked up without a session.\n */\nexport interface TemplateBuilderSecTemplateAuth {\n secTemplate: string\n stateless: true\n sassKey?: never\n sessionUuid?: never\n companyUuid?: never\n projectUuid?: never\n}\n\nexport interface TemplateBuilderBaseProps {\n baseUrl: string\n token: string\n templateId?: string\n mode?: 'inline' | 'modal'\n /** Hand the template in and take it back out instead of using the DAM. */\n stateless?: boolean\n /** Stateless mode: the template to edit, as `.fdt` XML. */\n content?: string\n /**\n * Stateless mode: open on a new, empty template instead of supplying\n * `content`. The widget provides the blank document, the user picks the\n * canvas size in the editor, and `onSave` receives a complete `.fdt` to\n * store. Ignored when `content` is set.\n */\n newTemplate?: boolean\n /** Stateless mode: display name for the editor header. */\n templateName?: string\n /**\n * Stateless mode: the `template_query` to open on — the value handed back on\n * save. Reopens the template on the same layout and variable values; empty\n * falls back to the XML's own `default=` attributes.\n */\n templateQuery?: string\n /** Accent colour for the editor chrome, as `#rgb` / `#rrggbb`. */\n brandColor?: string\n /** Colour scheme for the editor chrome. */\n theme?: BuilderTheme\n /**\n * Metadata model offered in the editor as the \"Custom metadata\" value source\n * — names only, no values. Omit it and the source is not offered.\n *\n * Compared by identity, like every other prop here, so a freshly built array\n * counts as a change. Nothing is re-sent to the editor over it — the element\n * de-dupes by value — but hoisting or memoising the array avoids the churn.\n */\n customMetadata?: CustomMetadataField[]\n /**\n * Display name for the custom-metadata value source in the editor's UI —\n * e.g. \"External metadata\". Wording only: the stored template is unaffected.\n * Empty uses the editor's default, \"Custom metadata\".\n */\n customMetadataLabel?: string\n /**\n * Stateless only: store each save in Filerobot too, so the CDN can render\n * it. `onSave`'s detail then carries `stored: { uuid, url }` next to the raw\n * `content` — or `storeError` when the copy failed.\n */\n damStore?: boolean\n /**\n * Folder new templates land in under `damStore` when the template id names\n * no existing DAM file (an existing file's own folder always wins).\n */\n storeFolder?: string\n /**\n * `damStore`: the `stored.uuid` a previous session's save reported for THIS\n * document, so re-saves after a reload resolve to (and version) the copy\n * that already exists instead of erroring on unchanged content or starting\n * a fresh file. Per-document — pass it with the content it belongs to.\n */\n storedUuid?: string\n readyTimeout?: number\n className?: string\n style?: CSSProperties\n onReady?: () => void\n onOpen?: () => void\n onClose?: () => void\n /**\n * Fired on save. In stateless mode the detail carries the edited `content`\n * for you to persist; otherwise it reports the uuid the app uploaded to.\n *\n * In stateless mode the outcome is reported back to the editor: return (or\n * resolve to) `false`, or throw, and the editor restores its unsaved-changes\n * flag and tells the user the save failed. Anything else counts as persisted.\n */\n onSave?: (\n data: TemplateBuilderSaveDetail,\n ) => void | boolean | Promise<void | boolean>\n onError?: (data: BuilderErrorData) => void\n /**\n * Stateless mode: unsaved-changes flag changed. Use it to prompt before\n * swapping `content`, which discards in-progress edits.\n */\n onDirtyChange?: (data: BuilderDirtyData) => void\n}\n\nexport type TemplateBuilderProps = TemplateBuilderBaseProps &\n (TemplateBuilderSessionAuth | TemplateBuilderSecTemplateAuth)\n\n/**\n * React wrapper around `<sfx-template-builder>`. Props are assigned as\n * element properties via ref (works on React 18 and 19 alike); callbacks\n * subscribe to the element's CustomEvents.\n *\n * Forwards a ref to the underlying element, which is the only way to reach the\n * imperative API — `open()` in particular, without which `mode=\"modal\"` can\n * never be shown:\n *\n * ```tsx\n * const builder = useRef<SfxTemplateBuilder>(null)\n * <TemplateBuilder ref={builder} mode=\"modal\" … />\n * <button onClick={() => builder.current?.open('tpl-1')}>Edit</button>\n * ```\n *\n * `forwardRef` rather than a plain `ref` prop: React 19 accepts the latter for\n * function components, React 18 does not, and both are supported peers.\n */\nexport const TemplateBuilder = forwardRef<\n SfxTemplateBuilder,\n TemplateBuilderProps\n>(function TemplateBuilder(props, forwardedRef): ReactElement {\n const {\n className,\n style,\n onReady,\n onOpen,\n onClose,\n onSave,\n onError,\n onDirtyChange,\n ...config\n } = props\n const ref = useRef<SfxTemplateBuilder>(null)\n\n // Hand the same element out to the caller without giving up the internal ref\n // the effects below rely on.\n useImperativeHandle(forwardedRef, () => ref.current as SfxTemplateBuilder, [])\n\n // Assign config before paint so the iframe doesn't first mount with defaults.\n useLayoutEffect(() => {\n const el = ref.current\n if (!el) return\n el.baseUrl = config.baseUrl\n el.token = config.token\n el.sassKey = config.sassKey ?? ''\n el.sessionUuid = config.sessionUuid ?? ''\n el.secTemplate = config.secTemplate ?? ''\n el.companyUuid = config.companyUuid ?? ''\n el.projectUuid = config.projectUuid ?? ''\n el.templateId = config.templateId ?? ''\n el.mode = config.mode ?? 'inline'\n el.stateless = config.stateless ?? false\n el.templateName = config.templateName ?? ''\n el.templateQuery = config.templateQuery ?? ''\n el.brandColor = config.brandColor ?? ''\n el.theme = config.theme ?? ''\n el.newTemplate = config.newTemplate ?? false\n el.customMetadata = config.customMetadata ?? []\n el.customMetadataLabel = config.customMetadataLabel ?? ''\n el.damStore = config.damStore ?? false\n el.storeFolder = config.storeFolder ?? '/'\n el.storedUuid = config.storedUuid ?? ''\n // Assigned last: the element sends content to the app as soon as it has\n // both a request and a value, so the id, name and query must already be\n // set — all four ship as one message.\n el.content = config.content ?? ''\n if (config.readyTimeout !== undefined) el.readyTimeout = config.readyTimeout\n }, [\n config.baseUrl,\n config.token,\n config.sassKey,\n config.sessionUuid,\n config.secTemplate,\n config.companyUuid,\n config.projectUuid,\n config.templateId,\n config.mode,\n config.stateless,\n config.content,\n config.newTemplate,\n config.templateName,\n config.templateQuery,\n config.brandColor,\n config.theme,\n config.customMetadata,\n config.customMetadataLabel,\n config.damStore,\n config.storeFolder,\n config.storedUuid,\n config.readyTimeout,\n ])\n\n // The callbacks the listeners read at event time. A ref rather than effect\n // dependencies: listeners are attached once per element (below), so a parent\n // re-render swapping handler identities costs nothing — and, decisively, the\n // listeners are still attached during the element's disconnect-time flush of\n // pending dam-store saves, which a resubscribe-per-change cleanup would have\n // already torn down.\n const handlers = useRef({\n onReady,\n onOpen,\n onClose,\n onSave,\n onError,\n onDirtyChange,\n })\n useLayoutEffect(() => {\n handlers.current = { onReady, onOpen, onClose, onSave, onError, onDirtyChange }\n })\n\n // Layout effect, not passive: the element reports config errors (e.g.\n // `invalid-base-url`) in a microtask queued during this same commit, and a\n // passive effect would subscribe only after that microtask has fired —\n // making a mount-time error unobservable from React. This effect is declared\n // after the config one, so it still runs once the config is assigned.\n useLayoutEffect(() => {\n const el = ref.current\n if (!el) return\n const subs: Array<[string, EventListener]> = []\n const on = (\n name: string,\n pick: (h: typeof handlers.current) => ((detail: never) => void) | undefined,\n ) => {\n const listener = ((e: CustomEvent) =>\n pick(handlers.current)?.(e.detail as never)) as EventListener\n el.addEventListener(name, listener)\n subs.push([name, listener])\n }\n on('ready', (h) => h.onReady)\n on('open', (h) => h.onOpen)\n on('close', (h) => h.onClose)\n on('error', (h) => h.onError)\n on('dirtychange', (h) => h.onDirtyChange)\n\n // `save` is not just re-emitted: in stateless mode the handler's outcome\n // is acked back, so a failed write on the host side doesn't leave the\n // editor showing the template as saved. `confirmSave` no-ops in DAM mode.\n const saveListener = ((e: CustomEvent<TemplateBuilderSaveDetail>) => {\n const onSaveNow = handlers.current.onSave\n if (!onSaveNow) return\n // Wrapped in a promise so a synchronous throw is handled like a\n // rejection, and a sync `false` like a resolved one.\n Promise.resolve()\n .then(() => onSaveNow(e.detail))\n .then((result) => el.confirmSave(result !== false))\n .catch((err) => {\n // No message: an internal error string is not something to put in\n // front of the end user. The editor uses its own wording.\n console.error('[sfx-template-builder] onSave failed:', err)\n el.confirmSave(false)\n })\n }) as EventListener\n el.addEventListener('save', saveListener)\n subs.push(['save', saveListener])\n\n return () => {\n // React runs this cleanup BEFORE it detaches the node, so the element's\n // own disconnect-time flush of in-flight dam-store saves would fire\n // after every listener is gone — and the raw save would be silently\n // lost. Flushing here, while the listeners are still attached, hands\n // those saves (with `storeError` in place of the links) to `onSave`\n // first. A no-op when nothing is pending, including StrictMode's\n // simulated unmount at mount time.\n //\n // Guarded: when an older CDN bundle registered the tag first, `el` is\n // that bundle's class and lacks the method — every other new-API use\n // degrades silently via property assignment, and unmount must not be\n // the one path that throws.\n if (typeof el.flushPendingSaves === 'function') {\n el.flushPendingSaves('widget removed before the rendering copy completed')\n }\n for (const [name, listener] of subs) el.removeEventListener(name, listener)\n }\n }, [])\n\n // eslint-disable-next-line react-hooks/refs -- ref is forwarded as a prop, not read during render\n return createElement('sfx-template-builder', { ref, class: className, style })\n})\n"],"names":["TemplateBuilder","forwardRef","props","forwardedRef","className","style","onReady","onOpen","onClose","onSave","onError","onDirtyChange","config","ref","useRef","useImperativeHandle","useLayoutEffect","el","handlers","subs","on","name","pick","listener","e","h","saveListener","onSaveNow","result","err","createElement"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"react.cjs","sources":["../src/react.ts"],"sourcesContent":["import {\n createElement,\n forwardRef,\n useImperativeHandle,\n useLayoutEffect,\n useRef,\n type CSSProperties,\n type ReactElement,\n} from 'react'\nimport './define'\nimport type { SfxTemplateBuilder } from './template-builder'\nimport type { TemplateBuilderSaveDetail } from './template-builder'\nimport type {\n BuilderDirtyData,\n BuilderErrorData,\n BuilderTheme,\n CustomMetadataField,\n} from './protocol'\n\n/**\n * Hub session — the full-featured credential.\n */\nexport interface TemplateBuilderSessionAuth {\n sassKey: string\n sessionUuid: string\n companyUuid?: string\n projectUuid?: string\n secTemplate?: never\n}\n\n/**\n * Filerobot security template — a guest credential for hosts that have no Hub\n * session to hand over. `stateless` is required rather than merely implied:\n * the app takes a security template on the stateless embed route only, so the\n * combination is a compile-time error instead of a runtime one.\n *\n * `companyUuid` / `projectUuid` are absent by design — they name a Hub project\n * that cannot be looked up without a session.\n */\nexport interface TemplateBuilderSecTemplateAuth {\n secTemplate: string\n stateless: true\n sassKey?: never\n sessionUuid?: never\n companyUuid?: never\n projectUuid?: never\n}\n\nexport interface TemplateBuilderBaseProps {\n baseUrl: string\n token: string\n templateId?: string\n mode?: 'inline' | 'modal'\n /** Hand the template in and take it back out instead of using the DAM. */\n stateless?: boolean\n /** Stateless mode: the template to edit, as `.fdt` XML. */\n content?: string\n /**\n * Stateless mode: open on a new, empty template instead of supplying\n * `content`. The widget provides the blank document, the user picks the\n * canvas size in the editor, and `onSave` receives a complete `.fdt` to\n * store. Ignored when `content` is set.\n */\n newTemplate?: boolean\n /** Stateless mode: display name for the editor header. */\n templateName?: string\n /**\n * Stateless mode: the `template_query` to open on — the value handed back on\n * save. Reopens the template on the same layout and variable values; empty\n * falls back to the XML's own `default=` attributes.\n */\n templateQuery?: string\n /** Accent colour for the editor chrome, as `#rgb` / `#rrggbb`. */\n brandColor?: string\n /** Colour scheme for the editor chrome. */\n theme?: BuilderTheme\n /**\n * Metadata model offered in the editor as the \"Custom metadata\" source type\n * — names only, no values. Omit it and the source is not offered.\n *\n * Compared by identity, like every other prop here, so a freshly built array\n * counts as a change. Nothing is re-sent to the editor over it — the element\n * de-dupes by value — but hoisting or memoising the array avoids the churn.\n */\n customMetadata?: CustomMetadataField[]\n /**\n * Display name for the custom-metadata source type in the editor's UI —\n * e.g. \"External metadata\". Wording only: the stored template is unaffected.\n * Empty uses the editor's default, \"Custom metadata\".\n */\n customMetadataLabel?: string\n /**\n * Stateless only: store each save in Filerobot too, so the CDN can render\n * it. `onSave`'s detail then carries `stored: { uuid, url }` next to the raw\n * `content` — or `storeError` when the copy failed.\n */\n damStore?: boolean\n /**\n * Folder new templates land in under `damStore` when the template id names\n * no existing DAM file (an existing file's own folder always wins).\n */\n storeFolder?: string\n /**\n * `damStore`: the `stored.uuid` a previous session's save reported for THIS\n * document, so re-saves after a reload resolve to (and version) the copy\n * that already exists instead of erroring on unchanged content or starting\n * a fresh file. Per-document — pass it with the content it belongs to.\n */\n storedUuid?: string\n readyTimeout?: number\n className?: string\n style?: CSSProperties\n onReady?: () => void\n onOpen?: () => void\n onClose?: () => void\n /**\n * Fired on save. In stateless mode the detail carries the edited `content`\n * for you to persist; otherwise it reports the uuid the app uploaded to.\n *\n * In stateless mode the outcome is reported back to the editor: return (or\n * resolve to) `false`, or throw, and the editor restores its unsaved-changes\n * flag and tells the user the save failed. Anything else counts as persisted.\n */\n onSave?: (\n data: TemplateBuilderSaveDetail,\n ) => void | boolean | Promise<void | boolean>\n onError?: (data: BuilderErrorData) => void\n /**\n * Stateless mode: unsaved-changes flag changed. Use it to prompt before\n * swapping `content`, which discards in-progress edits.\n */\n onDirtyChange?: (data: BuilderDirtyData) => void\n}\n\nexport type TemplateBuilderProps = TemplateBuilderBaseProps &\n (TemplateBuilderSessionAuth | TemplateBuilderSecTemplateAuth)\n\n/**\n * React wrapper around `<sfx-template-builder>`. Props are assigned as\n * element properties via ref (works on React 18 and 19 alike); callbacks\n * subscribe to the element's CustomEvents.\n *\n * Forwards a ref to the underlying element, which is the only way to reach the\n * imperative API — `open()` in particular, without which `mode=\"modal\"` can\n * never be shown:\n *\n * ```tsx\n * const builder = useRef<SfxTemplateBuilder>(null)\n * <TemplateBuilder ref={builder} mode=\"modal\" … />\n * <button onClick={() => builder.current?.open('tpl-1')}>Edit</button>\n * ```\n *\n * `forwardRef` rather than a plain `ref` prop: React 19 accepts the latter for\n * function components, React 18 does not, and both are supported peers.\n */\nexport const TemplateBuilder = forwardRef<\n SfxTemplateBuilder,\n TemplateBuilderProps\n>(function TemplateBuilder(props, forwardedRef): ReactElement {\n const {\n className,\n style,\n onReady,\n onOpen,\n onClose,\n onSave,\n onError,\n onDirtyChange,\n ...config\n } = props\n const ref = useRef<SfxTemplateBuilder>(null)\n\n // Hand the same element out to the caller without giving up the internal ref\n // the effects below rely on.\n useImperativeHandle(forwardedRef, () => ref.current as SfxTemplateBuilder, [])\n\n // Assign config before paint so the iframe doesn't first mount with defaults.\n useLayoutEffect(() => {\n const el = ref.current\n if (!el) return\n el.baseUrl = config.baseUrl\n el.token = config.token\n el.sassKey = config.sassKey ?? ''\n el.sessionUuid = config.sessionUuid ?? ''\n el.secTemplate = config.secTemplate ?? ''\n el.companyUuid = config.companyUuid ?? ''\n el.projectUuid = config.projectUuid ?? ''\n el.templateId = config.templateId ?? ''\n el.mode = config.mode ?? 'inline'\n el.stateless = config.stateless ?? false\n el.templateName = config.templateName ?? ''\n el.templateQuery = config.templateQuery ?? ''\n el.brandColor = config.brandColor ?? ''\n el.theme = config.theme ?? ''\n el.newTemplate = config.newTemplate ?? false\n el.customMetadata = config.customMetadata ?? []\n el.customMetadataLabel = config.customMetadataLabel ?? ''\n el.damStore = config.damStore ?? false\n el.storeFolder = config.storeFolder ?? '/'\n el.storedUuid = config.storedUuid ?? ''\n // Assigned last: the element sends content to the app as soon as it has\n // both a request and a value, so the id, name and query must already be\n // set — all four ship as one message.\n el.content = config.content ?? ''\n if (config.readyTimeout !== undefined) el.readyTimeout = config.readyTimeout\n }, [\n config.baseUrl,\n config.token,\n config.sassKey,\n config.sessionUuid,\n config.secTemplate,\n config.companyUuid,\n config.projectUuid,\n config.templateId,\n config.mode,\n config.stateless,\n config.content,\n config.newTemplate,\n config.templateName,\n config.templateQuery,\n config.brandColor,\n config.theme,\n config.customMetadata,\n config.customMetadataLabel,\n config.damStore,\n config.storeFolder,\n config.storedUuid,\n config.readyTimeout,\n ])\n\n // The callbacks the listeners read at event time. A ref rather than effect\n // dependencies: listeners are attached once per element (below), so a parent\n // re-render swapping handler identities costs nothing — and, decisively, the\n // listeners are still attached during the element's disconnect-time flush of\n // pending dam-store saves, which a resubscribe-per-change cleanup would have\n // already torn down.\n const handlers = useRef({\n onReady,\n onOpen,\n onClose,\n onSave,\n onError,\n onDirtyChange,\n })\n useLayoutEffect(() => {\n handlers.current = { onReady, onOpen, onClose, onSave, onError, onDirtyChange }\n })\n\n // Layout effect, not passive: the element reports config errors (e.g.\n // `invalid-base-url`) in a microtask queued during this same commit, and a\n // passive effect would subscribe only after that microtask has fired —\n // making a mount-time error unobservable from React. This effect is declared\n // after the config one, so it still runs once the config is assigned.\n useLayoutEffect(() => {\n const el = ref.current\n if (!el) return\n const subs: Array<[string, EventListener]> = []\n const on = (\n name: string,\n pick: (h: typeof handlers.current) => ((detail: never) => void) | undefined,\n ) => {\n const listener = ((e: CustomEvent) =>\n pick(handlers.current)?.(e.detail as never)) as EventListener\n el.addEventListener(name, listener)\n subs.push([name, listener])\n }\n on('ready', (h) => h.onReady)\n on('open', (h) => h.onOpen)\n on('close', (h) => h.onClose)\n on('error', (h) => h.onError)\n on('dirtychange', (h) => h.onDirtyChange)\n\n // `save` is not just re-emitted: in stateless mode the handler's outcome\n // is acked back, so a failed write on the host side doesn't leave the\n // editor showing the template as saved. `confirmSave` no-ops in DAM mode.\n const saveListener = ((e: CustomEvent<TemplateBuilderSaveDetail>) => {\n const onSaveNow = handlers.current.onSave\n if (!onSaveNow) return\n // Wrapped in a promise so a synchronous throw is handled like a\n // rejection, and a sync `false` like a resolved one.\n Promise.resolve()\n .then(() => onSaveNow(e.detail))\n .then((result) => el.confirmSave(result !== false))\n .catch((err) => {\n // No message: an internal error string is not something to put in\n // front of the end user. The editor uses its own wording.\n console.error('[sfx-template-builder] onSave failed:', err)\n el.confirmSave(false)\n })\n }) as EventListener\n el.addEventListener('save', saveListener)\n subs.push(['save', saveListener])\n\n return () => {\n // React runs this cleanup BEFORE it detaches the node, so the element's\n // own disconnect-time flush of in-flight dam-store saves would fire\n // after every listener is gone — and the raw save would be silently\n // lost. Flushing here, while the listeners are still attached, hands\n // those saves (with `storeError` in place of the links) to `onSave`\n // first. A no-op when nothing is pending, including StrictMode's\n // simulated unmount at mount time.\n //\n // Guarded: when an older CDN bundle registered the tag first, `el` is\n // that bundle's class and lacks the method — every other new-API use\n // degrades silently via property assignment, and unmount must not be\n // the one path that throws.\n if (typeof el.flushPendingSaves === 'function') {\n el.flushPendingSaves('widget removed before the rendering copy completed')\n }\n for (const [name, listener] of subs) el.removeEventListener(name, listener)\n }\n }, [])\n\n // eslint-disable-next-line react-hooks/refs -- ref is forwarded as a prop, not read during render\n return createElement('sfx-template-builder', { ref, class: className, style })\n})\n"],"names":["TemplateBuilder","forwardRef","props","forwardedRef","className","style","onReady","onOpen","onClose","onSave","onError","onDirtyChange","config","ref","useRef","useImperativeHandle","useLayoutEffect","el","handlers","subs","on","name","pick","listener","e","h","saveListener","onSaveNow","result","err","createElement"],"mappings":"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"}
|
package/dist/react.d.ts
CHANGED
|
@@ -59,7 +59,7 @@ export interface TemplateBuilderBaseProps {
|
|
|
59
59
|
/** Colour scheme for the editor chrome. */
|
|
60
60
|
theme?: BuilderTheme;
|
|
61
61
|
/**
|
|
62
|
-
* Metadata model offered in the editor as the "Custom metadata"
|
|
62
|
+
* Metadata model offered in the editor as the "Custom metadata" source type
|
|
63
63
|
* — names only, no values. Omit it and the source is not offered.
|
|
64
64
|
*
|
|
65
65
|
* Compared by identity, like every other prop here, so a freshly built array
|
|
@@ -68,7 +68,7 @@ export interface TemplateBuilderBaseProps {
|
|
|
68
68
|
*/
|
|
69
69
|
customMetadata?: CustomMetadataField[];
|
|
70
70
|
/**
|
|
71
|
-
* Display name for the custom-metadata
|
|
71
|
+
* Display name for the custom-metadata source type in the editor's UI —
|
|
72
72
|
* e.g. "External metadata". Wording only: the stored template is unaffected.
|
|
73
73
|
* Empty uses the editor's default, "Custom metadata".
|
|
74
74
|
*/
|
package/dist/react.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"react.js","sources":["../src/react.ts"],"sourcesContent":["import {\n createElement,\n forwardRef,\n useImperativeHandle,\n useLayoutEffect,\n useRef,\n type CSSProperties,\n type ReactElement,\n} from 'react'\nimport './define'\nimport type { SfxTemplateBuilder } from './template-builder'\nimport type { TemplateBuilderSaveDetail } from './template-builder'\nimport type {\n BuilderDirtyData,\n BuilderErrorData,\n BuilderTheme,\n CustomMetadataField,\n} from './protocol'\n\n/**\n * Hub session — the full-featured credential.\n */\nexport interface TemplateBuilderSessionAuth {\n sassKey: string\n sessionUuid: string\n companyUuid?: string\n projectUuid?: string\n secTemplate?: never\n}\n\n/**\n * Filerobot security template — a guest credential for hosts that have no Hub\n * session to hand over. `stateless` is required rather than merely implied:\n * the app takes a security template on the stateless embed route only, so the\n * combination is a compile-time error instead of a runtime one.\n *\n * `companyUuid` / `projectUuid` are absent by design — they name a Hub project\n * that cannot be looked up without a session.\n */\nexport interface TemplateBuilderSecTemplateAuth {\n secTemplate: string\n stateless: true\n sassKey?: never\n sessionUuid?: never\n companyUuid?: never\n projectUuid?: never\n}\n\nexport interface TemplateBuilderBaseProps {\n baseUrl: string\n token: string\n templateId?: string\n mode?: 'inline' | 'modal'\n /** Hand the template in and take it back out instead of using the DAM. */\n stateless?: boolean\n /** Stateless mode: the template to edit, as `.fdt` XML. */\n content?: string\n /**\n * Stateless mode: open on a new, empty template instead of supplying\n * `content`. The widget provides the blank document, the user picks the\n * canvas size in the editor, and `onSave` receives a complete `.fdt` to\n * store. Ignored when `content` is set.\n */\n newTemplate?: boolean\n /** Stateless mode: display name for the editor header. */\n templateName?: string\n /**\n * Stateless mode: the `template_query` to open on — the value handed back on\n * save. Reopens the template on the same layout and variable values; empty\n * falls back to the XML's own `default=` attributes.\n */\n templateQuery?: string\n /** Accent colour for the editor chrome, as `#rgb` / `#rrggbb`. */\n brandColor?: string\n /** Colour scheme for the editor chrome. */\n theme?: BuilderTheme\n /**\n * Metadata model offered in the editor as the \"Custom metadata\" value source\n * — names only, no values. Omit it and the source is not offered.\n *\n * Compared by identity, like every other prop here, so a freshly built array\n * counts as a change. Nothing is re-sent to the editor over it — the element\n * de-dupes by value — but hoisting or memoising the array avoids the churn.\n */\n customMetadata?: CustomMetadataField[]\n /**\n * Display name for the custom-metadata value source in the editor's UI —\n * e.g. \"External metadata\". Wording only: the stored template is unaffected.\n * Empty uses the editor's default, \"Custom metadata\".\n */\n customMetadataLabel?: string\n /**\n * Stateless only: store each save in Filerobot too, so the CDN can render\n * it. `onSave`'s detail then carries `stored: { uuid, url }` next to the raw\n * `content` — or `storeError` when the copy failed.\n */\n damStore?: boolean\n /**\n * Folder new templates land in under `damStore` when the template id names\n * no existing DAM file (an existing file's own folder always wins).\n */\n storeFolder?: string\n /**\n * `damStore`: the `stored.uuid` a previous session's save reported for THIS\n * document, so re-saves after a reload resolve to (and version) the copy\n * that already exists instead of erroring on unchanged content or starting\n * a fresh file. Per-document — pass it with the content it belongs to.\n */\n storedUuid?: string\n readyTimeout?: number\n className?: string\n style?: CSSProperties\n onReady?: () => void\n onOpen?: () => void\n onClose?: () => void\n /**\n * Fired on save. In stateless mode the detail carries the edited `content`\n * for you to persist; otherwise it reports the uuid the app uploaded to.\n *\n * In stateless mode the outcome is reported back to the editor: return (or\n * resolve to) `false`, or throw, and the editor restores its unsaved-changes\n * flag and tells the user the save failed. Anything else counts as persisted.\n */\n onSave?: (\n data: TemplateBuilderSaveDetail,\n ) => void | boolean | Promise<void | boolean>\n onError?: (data: BuilderErrorData) => void\n /**\n * Stateless mode: unsaved-changes flag changed. Use it to prompt before\n * swapping `content`, which discards in-progress edits.\n */\n onDirtyChange?: (data: BuilderDirtyData) => void\n}\n\nexport type TemplateBuilderProps = TemplateBuilderBaseProps &\n (TemplateBuilderSessionAuth | TemplateBuilderSecTemplateAuth)\n\n/**\n * React wrapper around `<sfx-template-builder>`. Props are assigned as\n * element properties via ref (works on React 18 and 19 alike); callbacks\n * subscribe to the element's CustomEvents.\n *\n * Forwards a ref to the underlying element, which is the only way to reach the\n * imperative API — `open()` in particular, without which `mode=\"modal\"` can\n * never be shown:\n *\n * ```tsx\n * const builder = useRef<SfxTemplateBuilder>(null)\n * <TemplateBuilder ref={builder} mode=\"modal\" … />\n * <button onClick={() => builder.current?.open('tpl-1')}>Edit</button>\n * ```\n *\n * `forwardRef` rather than a plain `ref` prop: React 19 accepts the latter for\n * function components, React 18 does not, and both are supported peers.\n */\nexport const TemplateBuilder = forwardRef<\n SfxTemplateBuilder,\n TemplateBuilderProps\n>(function TemplateBuilder(props, forwardedRef): ReactElement {\n const {\n className,\n style,\n onReady,\n onOpen,\n onClose,\n onSave,\n onError,\n onDirtyChange,\n ...config\n } = props\n const ref = useRef<SfxTemplateBuilder>(null)\n\n // Hand the same element out to the caller without giving up the internal ref\n // the effects below rely on.\n useImperativeHandle(forwardedRef, () => ref.current as SfxTemplateBuilder, [])\n\n // Assign config before paint so the iframe doesn't first mount with defaults.\n useLayoutEffect(() => {\n const el = ref.current\n if (!el) return\n el.baseUrl = config.baseUrl\n el.token = config.token\n el.sassKey = config.sassKey ?? ''\n el.sessionUuid = config.sessionUuid ?? ''\n el.secTemplate = config.secTemplate ?? ''\n el.companyUuid = config.companyUuid ?? ''\n el.projectUuid = config.projectUuid ?? ''\n el.templateId = config.templateId ?? ''\n el.mode = config.mode ?? 'inline'\n el.stateless = config.stateless ?? false\n el.templateName = config.templateName ?? ''\n el.templateQuery = config.templateQuery ?? ''\n el.brandColor = config.brandColor ?? ''\n el.theme = config.theme ?? ''\n el.newTemplate = config.newTemplate ?? false\n el.customMetadata = config.customMetadata ?? []\n el.customMetadataLabel = config.customMetadataLabel ?? ''\n el.damStore = config.damStore ?? false\n el.storeFolder = config.storeFolder ?? '/'\n el.storedUuid = config.storedUuid ?? ''\n // Assigned last: the element sends content to the app as soon as it has\n // both a request and a value, so the id, name and query must already be\n // set — all four ship as one message.\n el.content = config.content ?? ''\n if (config.readyTimeout !== undefined) el.readyTimeout = config.readyTimeout\n }, [\n config.baseUrl,\n config.token,\n config.sassKey,\n config.sessionUuid,\n config.secTemplate,\n config.companyUuid,\n config.projectUuid,\n config.templateId,\n config.mode,\n config.stateless,\n config.content,\n config.newTemplate,\n config.templateName,\n config.templateQuery,\n config.brandColor,\n config.theme,\n config.customMetadata,\n config.customMetadataLabel,\n config.damStore,\n config.storeFolder,\n config.storedUuid,\n config.readyTimeout,\n ])\n\n // The callbacks the listeners read at event time. A ref rather than effect\n // dependencies: listeners are attached once per element (below), so a parent\n // re-render swapping handler identities costs nothing — and, decisively, the\n // listeners are still attached during the element's disconnect-time flush of\n // pending dam-store saves, which a resubscribe-per-change cleanup would have\n // already torn down.\n const handlers = useRef({\n onReady,\n onOpen,\n onClose,\n onSave,\n onError,\n onDirtyChange,\n })\n useLayoutEffect(() => {\n handlers.current = { onReady, onOpen, onClose, onSave, onError, onDirtyChange }\n })\n\n // Layout effect, not passive: the element reports config errors (e.g.\n // `invalid-base-url`) in a microtask queued during this same commit, and a\n // passive effect would subscribe only after that microtask has fired —\n // making a mount-time error unobservable from React. This effect is declared\n // after the config one, so it still runs once the config is assigned.\n useLayoutEffect(() => {\n const el = ref.current\n if (!el) return\n const subs: Array<[string, EventListener]> = []\n const on = (\n name: string,\n pick: (h: typeof handlers.current) => ((detail: never) => void) | undefined,\n ) => {\n const listener = ((e: CustomEvent) =>\n pick(handlers.current)?.(e.detail as never)) as EventListener\n el.addEventListener(name, listener)\n subs.push([name, listener])\n }\n on('ready', (h) => h.onReady)\n on('open', (h) => h.onOpen)\n on('close', (h) => h.onClose)\n on('error', (h) => h.onError)\n on('dirtychange', (h) => h.onDirtyChange)\n\n // `save` is not just re-emitted: in stateless mode the handler's outcome\n // is acked back, so a failed write on the host side doesn't leave the\n // editor showing the template as saved. `confirmSave` no-ops in DAM mode.\n const saveListener = ((e: CustomEvent<TemplateBuilderSaveDetail>) => {\n const onSaveNow = handlers.current.onSave\n if (!onSaveNow) return\n // Wrapped in a promise so a synchronous throw is handled like a\n // rejection, and a sync `false` like a resolved one.\n Promise.resolve()\n .then(() => onSaveNow(e.detail))\n .then((result) => el.confirmSave(result !== false))\n .catch((err) => {\n // No message: an internal error string is not something to put in\n // front of the end user. The editor uses its own wording.\n console.error('[sfx-template-builder] onSave failed:', err)\n el.confirmSave(false)\n })\n }) as EventListener\n el.addEventListener('save', saveListener)\n subs.push(['save', saveListener])\n\n return () => {\n // React runs this cleanup BEFORE it detaches the node, so the element's\n // own disconnect-time flush of in-flight dam-store saves would fire\n // after every listener is gone — and the raw save would be silently\n // lost. Flushing here, while the listeners are still attached, hands\n // those saves (with `storeError` in place of the links) to `onSave`\n // first. A no-op when nothing is pending, including StrictMode's\n // simulated unmount at mount time.\n //\n // Guarded: when an older CDN bundle registered the tag first, `el` is\n // that bundle's class and lacks the method — every other new-API use\n // degrades silently via property assignment, and unmount must not be\n // the one path that throws.\n if (typeof el.flushPendingSaves === 'function') {\n el.flushPendingSaves('widget removed before the rendering copy completed')\n }\n for (const [name, listener] of subs) el.removeEventListener(name, listener)\n }\n }, [])\n\n // eslint-disable-next-line react-hooks/refs -- ref is forwarded as a prop, not read during render\n return createElement('sfx-template-builder', { ref, class: className, style })\n})\n"],"names":["TemplateBuilder","forwardRef","props","forwardedRef","className","style","onReady","onOpen","onClose","onSave","onError","onDirtyChange","config","ref","useRef","useImperativeHandle","useLayoutEffect","el","handlers","subs","on","name","pick","listener","e","h","saveListener","onSaveNow","result","err","createElement"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"react.js","sources":["../src/react.ts"],"sourcesContent":["import {\n createElement,\n forwardRef,\n useImperativeHandle,\n useLayoutEffect,\n useRef,\n type CSSProperties,\n type ReactElement,\n} from 'react'\nimport './define'\nimport type { SfxTemplateBuilder } from './template-builder'\nimport type { TemplateBuilderSaveDetail } from './template-builder'\nimport type {\n BuilderDirtyData,\n BuilderErrorData,\n BuilderTheme,\n CustomMetadataField,\n} from './protocol'\n\n/**\n * Hub session — the full-featured credential.\n */\nexport interface TemplateBuilderSessionAuth {\n sassKey: string\n sessionUuid: string\n companyUuid?: string\n projectUuid?: string\n secTemplate?: never\n}\n\n/**\n * Filerobot security template — a guest credential for hosts that have no Hub\n * session to hand over. `stateless` is required rather than merely implied:\n * the app takes a security template on the stateless embed route only, so the\n * combination is a compile-time error instead of a runtime one.\n *\n * `companyUuid` / `projectUuid` are absent by design — they name a Hub project\n * that cannot be looked up without a session.\n */\nexport interface TemplateBuilderSecTemplateAuth {\n secTemplate: string\n stateless: true\n sassKey?: never\n sessionUuid?: never\n companyUuid?: never\n projectUuid?: never\n}\n\nexport interface TemplateBuilderBaseProps {\n baseUrl: string\n token: string\n templateId?: string\n mode?: 'inline' | 'modal'\n /** Hand the template in and take it back out instead of using the DAM. */\n stateless?: boolean\n /** Stateless mode: the template to edit, as `.fdt` XML. */\n content?: string\n /**\n * Stateless mode: open on a new, empty template instead of supplying\n * `content`. The widget provides the blank document, the user picks the\n * canvas size in the editor, and `onSave` receives a complete `.fdt` to\n * store. Ignored when `content` is set.\n */\n newTemplate?: boolean\n /** Stateless mode: display name for the editor header. */\n templateName?: string\n /**\n * Stateless mode: the `template_query` to open on — the value handed back on\n * save. Reopens the template on the same layout and variable values; empty\n * falls back to the XML's own `default=` attributes.\n */\n templateQuery?: string\n /** Accent colour for the editor chrome, as `#rgb` / `#rrggbb`. */\n brandColor?: string\n /** Colour scheme for the editor chrome. */\n theme?: BuilderTheme\n /**\n * Metadata model offered in the editor as the \"Custom metadata\" source type\n * — names only, no values. Omit it and the source is not offered.\n *\n * Compared by identity, like every other prop here, so a freshly built array\n * counts as a change. Nothing is re-sent to the editor over it — the element\n * de-dupes by value — but hoisting or memoising the array avoids the churn.\n */\n customMetadata?: CustomMetadataField[]\n /**\n * Display name for the custom-metadata source type in the editor's UI —\n * e.g. \"External metadata\". Wording only: the stored template is unaffected.\n * Empty uses the editor's default, \"Custom metadata\".\n */\n customMetadataLabel?: string\n /**\n * Stateless only: store each save in Filerobot too, so the CDN can render\n * it. `onSave`'s detail then carries `stored: { uuid, url }` next to the raw\n * `content` — or `storeError` when the copy failed.\n */\n damStore?: boolean\n /**\n * Folder new templates land in under `damStore` when the template id names\n * no existing DAM file (an existing file's own folder always wins).\n */\n storeFolder?: string\n /**\n * `damStore`: the `stored.uuid` a previous session's save reported for THIS\n * document, so re-saves after a reload resolve to (and version) the copy\n * that already exists instead of erroring on unchanged content or starting\n * a fresh file. Per-document — pass it with the content it belongs to.\n */\n storedUuid?: string\n readyTimeout?: number\n className?: string\n style?: CSSProperties\n onReady?: () => void\n onOpen?: () => void\n onClose?: () => void\n /**\n * Fired on save. In stateless mode the detail carries the edited `content`\n * for you to persist; otherwise it reports the uuid the app uploaded to.\n *\n * In stateless mode the outcome is reported back to the editor: return (or\n * resolve to) `false`, or throw, and the editor restores its unsaved-changes\n * flag and tells the user the save failed. Anything else counts as persisted.\n */\n onSave?: (\n data: TemplateBuilderSaveDetail,\n ) => void | boolean | Promise<void | boolean>\n onError?: (data: BuilderErrorData) => void\n /**\n * Stateless mode: unsaved-changes flag changed. Use it to prompt before\n * swapping `content`, which discards in-progress edits.\n */\n onDirtyChange?: (data: BuilderDirtyData) => void\n}\n\nexport type TemplateBuilderProps = TemplateBuilderBaseProps &\n (TemplateBuilderSessionAuth | TemplateBuilderSecTemplateAuth)\n\n/**\n * React wrapper around `<sfx-template-builder>`. Props are assigned as\n * element properties via ref (works on React 18 and 19 alike); callbacks\n * subscribe to the element's CustomEvents.\n *\n * Forwards a ref to the underlying element, which is the only way to reach the\n * imperative API — `open()` in particular, without which `mode=\"modal\"` can\n * never be shown:\n *\n * ```tsx\n * const builder = useRef<SfxTemplateBuilder>(null)\n * <TemplateBuilder ref={builder} mode=\"modal\" … />\n * <button onClick={() => builder.current?.open('tpl-1')}>Edit</button>\n * ```\n *\n * `forwardRef` rather than a plain `ref` prop: React 19 accepts the latter for\n * function components, React 18 does not, and both are supported peers.\n */\nexport const TemplateBuilder = forwardRef<\n SfxTemplateBuilder,\n TemplateBuilderProps\n>(function TemplateBuilder(props, forwardedRef): ReactElement {\n const {\n className,\n style,\n onReady,\n onOpen,\n onClose,\n onSave,\n onError,\n onDirtyChange,\n ...config\n } = props\n const ref = useRef<SfxTemplateBuilder>(null)\n\n // Hand the same element out to the caller without giving up the internal ref\n // the effects below rely on.\n useImperativeHandle(forwardedRef, () => ref.current as SfxTemplateBuilder, [])\n\n // Assign config before paint so the iframe doesn't first mount with defaults.\n useLayoutEffect(() => {\n const el = ref.current\n if (!el) return\n el.baseUrl = config.baseUrl\n el.token = config.token\n el.sassKey = config.sassKey ?? ''\n el.sessionUuid = config.sessionUuid ?? ''\n el.secTemplate = config.secTemplate ?? ''\n el.companyUuid = config.companyUuid ?? ''\n el.projectUuid = config.projectUuid ?? ''\n el.templateId = config.templateId ?? ''\n el.mode = config.mode ?? 'inline'\n el.stateless = config.stateless ?? false\n el.templateName = config.templateName ?? ''\n el.templateQuery = config.templateQuery ?? ''\n el.brandColor = config.brandColor ?? ''\n el.theme = config.theme ?? ''\n el.newTemplate = config.newTemplate ?? false\n el.customMetadata = config.customMetadata ?? []\n el.customMetadataLabel = config.customMetadataLabel ?? ''\n el.damStore = config.damStore ?? false\n el.storeFolder = config.storeFolder ?? '/'\n el.storedUuid = config.storedUuid ?? ''\n // Assigned last: the element sends content to the app as soon as it has\n // both a request and a value, so the id, name and query must already be\n // set — all four ship as one message.\n el.content = config.content ?? ''\n if (config.readyTimeout !== undefined) el.readyTimeout = config.readyTimeout\n }, [\n config.baseUrl,\n config.token,\n config.sassKey,\n config.sessionUuid,\n config.secTemplate,\n config.companyUuid,\n config.projectUuid,\n config.templateId,\n config.mode,\n config.stateless,\n config.content,\n config.newTemplate,\n config.templateName,\n config.templateQuery,\n config.brandColor,\n config.theme,\n config.customMetadata,\n config.customMetadataLabel,\n config.damStore,\n config.storeFolder,\n config.storedUuid,\n config.readyTimeout,\n ])\n\n // The callbacks the listeners read at event time. A ref rather than effect\n // dependencies: listeners are attached once per element (below), so a parent\n // re-render swapping handler identities costs nothing — and, decisively, the\n // listeners are still attached during the element's disconnect-time flush of\n // pending dam-store saves, which a resubscribe-per-change cleanup would have\n // already torn down.\n const handlers = useRef({\n onReady,\n onOpen,\n onClose,\n onSave,\n onError,\n onDirtyChange,\n })\n useLayoutEffect(() => {\n handlers.current = { onReady, onOpen, onClose, onSave, onError, onDirtyChange }\n })\n\n // Layout effect, not passive: the element reports config errors (e.g.\n // `invalid-base-url`) in a microtask queued during this same commit, and a\n // passive effect would subscribe only after that microtask has fired —\n // making a mount-time error unobservable from React. This effect is declared\n // after the config one, so it still runs once the config is assigned.\n useLayoutEffect(() => {\n const el = ref.current\n if (!el) return\n const subs: Array<[string, EventListener]> = []\n const on = (\n name: string,\n pick: (h: typeof handlers.current) => ((detail: never) => void) | undefined,\n ) => {\n const listener = ((e: CustomEvent) =>\n pick(handlers.current)?.(e.detail as never)) as EventListener\n el.addEventListener(name, listener)\n subs.push([name, listener])\n }\n on('ready', (h) => h.onReady)\n on('open', (h) => h.onOpen)\n on('close', (h) => h.onClose)\n on('error', (h) => h.onError)\n on('dirtychange', (h) => h.onDirtyChange)\n\n // `save` is not just re-emitted: in stateless mode the handler's outcome\n // is acked back, so a failed write on the host side doesn't leave the\n // editor showing the template as saved. `confirmSave` no-ops in DAM mode.\n const saveListener = ((e: CustomEvent<TemplateBuilderSaveDetail>) => {\n const onSaveNow = handlers.current.onSave\n if (!onSaveNow) return\n // Wrapped in a promise so a synchronous throw is handled like a\n // rejection, and a sync `false` like a resolved one.\n Promise.resolve()\n .then(() => onSaveNow(e.detail))\n .then((result) => el.confirmSave(result !== false))\n .catch((err) => {\n // No message: an internal error string is not something to put in\n // front of the end user. The editor uses its own wording.\n console.error('[sfx-template-builder] onSave failed:', err)\n el.confirmSave(false)\n })\n }) as EventListener\n el.addEventListener('save', saveListener)\n subs.push(['save', saveListener])\n\n return () => {\n // React runs this cleanup BEFORE it detaches the node, so the element's\n // own disconnect-time flush of in-flight dam-store saves would fire\n // after every listener is gone — and the raw save would be silently\n // lost. Flushing here, while the listeners are still attached, hands\n // those saves (with `storeError` in place of the links) to `onSave`\n // first. A no-op when nothing is pending, including StrictMode's\n // simulated unmount at mount time.\n //\n // Guarded: when an older CDN bundle registered the tag first, `el` is\n // that bundle's class and lacks the method — every other new-API use\n // degrades silently via property assignment, and unmount must not be\n // the one path that throws.\n if (typeof el.flushPendingSaves === 'function') {\n el.flushPendingSaves('widget removed before the rendering copy completed')\n }\n for (const [name, listener] of subs) el.removeEventListener(name, listener)\n }\n }, [])\n\n // eslint-disable-next-line react-hooks/refs -- ref is forwarded as a prop, not read during render\n return createElement('sfx-template-builder', { ref, class: className, style })\n})\n"],"names":["TemplateBuilder","forwardRef","props","forwardedRef","className","style","onReady","onOpen","onClose","onSave","onError","onDirtyChange","config","ref","useRef","useImperativeHandle","useLayoutEffect","el","handlers","subs","on","name","pick","listener","e","h","saveListener","onSaveNow","result","err","createElement"],"mappings":"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"}
|