@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.
@@ -12,7 +12,7 @@ metadata:
12
12
  - design-templates
13
13
  - web-component
14
14
  status: ready
15
- version: 1
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 7).
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.4.1/template-builder.min.js"></script>
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 — Guard against losing edits
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 7 — Authentication
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 8 — Register the embedding origin (required)
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 9 — Theming (optional)
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 7) |
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
- | `brand-color`, `theme` | no | See Step 9 |
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
- Post-0.3.0 additions (check the CHANGELOG against your pinned bundle):
342
- `custom-metadata-label` renames the "Custom metadata" value source in the
343
- editor's UI (wording only); `dam-store` makes the element store each stateless
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()`, `load({ content, templateId?, name? })`,
351
- `createNew({ templateId?, name? })`, `confirmSave(ok, message?)`. Read-only:
352
- `status`, `isDirty`.
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 8), or third-party cookies blocked |
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 7) |
400
- | Metadata / regional / branding panels empty | Expected under `sec-template` — those come from the Hub project (Step 7) |
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
- ## [Unreleased]
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.4.1/template-builder.min.js"></script>
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** value source: `[{ 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** value source in the editor's UI (e.g. `External metadata`). Wording only — the stored template is unaffected. Empty uses the default |
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 (**Free text**),
758
- or from the source asset's DAM metadata (**File metadata**, which needs a
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
@@ -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" value source.
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 "File metadata" source) is
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" value source. Omitted or
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 value source in the editor's UI
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
@@ -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" value source
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 value source in the editor's UI —
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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"}