@scaleflex/template-builder 0.2.0 → 0.4.1

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/README.md CHANGED
@@ -7,7 +7,7 @@
7
7
  <h1 align="center">@scaleflex/template-builder</h1>
8
8
 
9
9
  <p align="center">
10
- Framework-agnostic Design Template Builder Web Component for <a href="https://www.scaleflex.com/page/digital-asset-management">Filerobot</a>.<br>
10
+ Framework-agnostic Design Template Builder Web Component for <a href="https://www.scaleflex.com/page/digital-asset-management">Scaleflex</a>.<br>
11
11
  Drop a full multi-layout template editor into any page, in any frontend stack.
12
12
  </p>
13
13
 
@@ -34,11 +34,13 @@
34
34
  - [Modes](#modes)
35
35
  - [DAM-backed](#dam-backed-default)
36
36
  - [Stateless](#stateless)
37
+ - [Storing a rendering copy (dam-store)](#storing-a-rendering-copy-dam-store)
37
38
  - [Starting a template from scratch](#starting-a-template-from-scratch)
38
39
  - [Reporting a failed save](#reporting-a-failed-save)
39
40
  - [Configuration](#configuration)
40
41
  - [Attributes & properties](#attributes--properties)
41
42
  - [Authentication](#authentication)
43
+ - [Custom metadata fields](#custom-metadata-fields)
42
44
  - [Origin registration](#origin-registration)
43
45
  - [Cookies](#cookies)
44
46
  - [Public Methods](#public-methods)
@@ -58,11 +60,11 @@
58
60
 
59
61
  ## Overview
60
62
 
61
- `<sfx-template-builder>` embeds the Filerobot design-templates builder in a page
62
- on any origin. The element owns an iframe pointed at a `design-templates-app`
63
- deployment, hands over the credentials, and translates the app's
64
- postMessage protocol into ordinary DOM `CustomEvent`s — so from your side it
65
- behaves like any other component.
63
+ `<sfx-template-builder>` embeds the Scaleflex design-templates builder in a page
64
+ on any origin. The element loads the editor from a `design-templates-app`
65
+ deployment, hands over the credentials, and surfaces everything the editor does
66
+ as ordinary DOM `CustomEvent`s — so from your side it behaves like any other
67
+ component.
66
68
 
67
69
  All the heavy lifting — server-side text rendering, font resolution, asset
68
70
  browsing, template XML — stays inside the app deployment. The widget is a
@@ -72,10 +74,10 @@ protocol adapter.
72
74
 
73
75
  - **Framework-agnostic** — a custom element; works in vanilla JS, React, Vue,
74
76
  Angular, Svelte. A thin React wrapper ships in the box.
75
- - **Two storage modes** — let Filerobot own the template, or keep the document
77
+ - **Two storage modes** — let Scaleflex own the template, or keep the document
76
78
  entirely on your side ([Stateless](#stateless)) and never map your users onto
77
79
  Scaleflex identities.
78
- - **Two ways in** — a Hub session, or a Filerobot [security
80
+ - **Two ways in** — a Hub session, or a Scaleflex [security
79
81
  template](#security-template-guest-auth) when you have no Hub account to hand
80
82
  over per user.
81
83
  - **Inline or modal** — fill a box in your layout, or cover the viewport.
@@ -88,17 +90,17 @@ protocol adapter.
88
90
  origin; the app only accepts a template from the origin pinned when the
89
91
  session was handed over.
90
92
  - **Diagnosable failures** — auth, blocked cookies, bad content and handshake
91
- timeouts all surface as an `error` event instead of a stuck iframe.
93
+ timeouts all surface as an `error` event instead of a stuck editor.
92
94
 
93
95
  ## Requirements
94
96
 
95
- - A [Filerobot](https://www.scaleflex.com/page/digital-asset-management) account
97
+ - A [Scaleflex](https://www.scaleflex.com/page/digital-asset-management) account
96
98
  and a running `design-templates-app` deployment to point `base-url` at
97
99
  - Credentials minted server-side — either a Hub session (`session-uuid` +
98
- **SASS key** + **Filerobot token**) or a **security template** + token for
100
+ **SASS key** + **Scaleflex token**) or a **security template** + token for
99
101
  guest access; see [Authentication](#authentication)
100
- - Your page's origin registered in the deployment's `frame-ancestors` allowlist
101
- — see [Origin registration](#origin-registration)
102
+ - Your page's origin registered in the deployment's embedding allowlist — see
103
+ [Origin registration](#origin-registration)
102
104
  - Modern browser with Custom Elements v1 support (see
103
105
  [Browser Support](#browser-support))
104
106
 
@@ -113,7 +115,7 @@ npm i @scaleflex/template-builder
113
115
  ### CDN
114
116
 
115
117
  ```html
116
- <script type="module" src="https://cdn.scaleflex.com/design-template-builder/0.2.0/template-builder.min.js"></script>
118
+ <script type="module" src="https://cdn.cloudimage.io/template-builder/0.4.1/template-builder.min.js"></script>
117
119
  ```
118
120
 
119
121
  The CDN bundle is self-registering — it defines `<sfx-template-builder>` on
@@ -139,7 +141,7 @@ load, with Lit bundled in. Pin the major version.
139
141
 
140
142
  ## Quick Start
141
143
 
142
- You need two things from your Filerobot project: its **token**, and a
144
+ You need two things from your Scaleflex project: its **token**, and a
143
145
  **security template** key — a named, permission-scoped credential you define
144
146
  once, the same guest-auth mechanism the other Scaleflex widgets use. No Hub
145
147
  account, and no user of yours ever needs a Scaleflex identity. See
@@ -211,7 +213,7 @@ Mint the session server-side; never put a long-lived credential in client code.
211
213
  ```html
212
214
  <sfx-template-builder
213
215
  base-url="https://<your-design-templates-deployment>"
214
- token="FILEROBOT_TOKEN"
216
+ token="PROJECT_TOKEN"
215
217
  sass-key="SASS_KEY"
216
218
  session-uuid="SESSION_UUID"
217
219
  template-id="TEMPLATE_UUID"
@@ -238,7 +240,7 @@ Mint the session server-side; never put a long-lived credential in client code.
238
240
 
239
241
  ### DAM-backed (default)
240
242
 
241
- `template-id` is a Filerobot file uuid. The app loads the template itself, and
243
+ `template-id` is a DAM file uuid. The app loads the template itself, and
242
244
  Save uploads a new version and reports the resulting uuid on the `save` event.
243
245
  Leave `template-id` empty to open the new-template flow.
244
246
 
@@ -246,7 +248,9 @@ Leave `template-id` empty to open the new-template flow.
246
248
 
247
249
  Set `stateless` to keep the document entirely on your side: you pass the
248
250
  template in, the user edits it, and you get the edited template back. Nothing is
249
- stored on the Scaleflex side, so your app keeps its own storage, versioning,
251
+ stored on the Scaleflex side (unless you opt into
252
+ [`dam-store`](#storing-a-rendering-copy-dam-store) for a rendering copy), so
253
+ your app keeps its own storage, versioning,
250
254
  tenancy and access control, and `template-id` becomes an opaque string that is
251
255
  echoed back untouched.
252
256
 
@@ -274,8 +278,8 @@ point.
274
278
  const { content, name, templateQuery } = await res.json()
275
279
 
276
280
  // 2 — pass it in. `content` is a property, never an attribute: templates
277
- // routinely exceed what fits in markup or a URL, which is also why the
278
- // widget delivers it over postMessage rather than the iframe src.
281
+ // routinely exceed what fits in markup or a URL, so the widget hands the
282
+ // document to the editor directly rather than through either.
279
283
  // `templateQuery` is what you stored on the last save; it reopens the
280
284
  // template on the same layout and variable values.
281
285
  builder.load({ templateId: id, name, content, templateQuery })
@@ -301,7 +305,7 @@ point.
301
305
  </script>
302
306
  ```
303
307
 
304
- `templateId` is **your** identifier, not a Filerobot uuid. The app never
308
+ `templateId` is **your** identifier, not a DAM uuid. The app never
305
309
  resolves it against anything — it is carried alongside the content and handed
306
310
  straight back on save, so use whatever key maps to your own record.
307
311
 
@@ -410,7 +414,7 @@ you navigated away from.
410
414
  #### What stateless does *not* remove
411
415
 
412
416
  Statelessness applies to the **document**, not to the infrastructure. The editor
413
- still needs an authenticated Filerobot tenant for:
417
+ still needs an authenticated Scaleflex tenant for:
414
418
 
415
419
  - **text rendering** — text and shape layers are rasterized server-side (see
416
420
  [the render round-trip](#why-the-editor-still-calls-a-server) below),
@@ -432,7 +436,7 @@ This is the reason a stateless embed still needs a credential, so it is worth
432
436
  being concrete about.
433
437
 
434
438
  A `.fdt` template is not an image. The image only exists once someone requests
435
- the template's CDN URL, and it is Filerobot that renders it there — server-side,
439
+ the template's CDN URL, and it is Scaleflex that renders it there — server-side,
436
440
  with ImageMagick:
437
441
 
438
442
  ```
@@ -456,8 +460,8 @@ editor drives a WebAssembly build of it), and each layer comes back as a
456
460
  transparent PNG that the canvas positions with CSS:
457
461
 
458
462
  ```
459
- browser — widget iframe app deployment (base-url)
460
- ─────────────────────── ─────────────────────────
463
+ browser — the widget app deployment (base-url)
464
+ ──────────────────── ─────────────────────────
461
465
  edit a text layer
462
466
  │ batched across layers, debounced
463
467
  ├────── POST /api/render-layers ─────▶ ImageMagick (WASM)
@@ -497,7 +501,64 @@ session to hand over — and that is exactly the gap a
497
501
  [security template](#security-template-guest-auth) fills: a permission-scoped,
498
502
  project-level guest credential that authorizes rendering, fonts and asset
499
503
  browsing without authenticating any particular user, and without your users
500
- existing in Filerobot at all.
504
+ existing in Scaleflex at all.
505
+
506
+ ### Storing a rendering copy (dam-store)
507
+
508
+ The raw XML in the `save` event is yours to keep — but the CDN renders only
509
+ files it stores, so previews and production banners need a copy in Filerobot
510
+ too. Set `dam-store` and the element makes that copy itself on every stateless
511
+ save, with the same multipart upload the DAM-backed editor uses, before the
512
+ `save` event fires. The detail then carries the links next to the raw data:
513
+
514
+ ```ts
515
+ builder.damStore = true
516
+ builder.addEventListener('save', (e) => {
517
+ const { content, templateQuery, stored, storeError } = e.detail
518
+ // stored = { uuid, url } — the DAM copy. `url` is the .fdt's CDN URL with
519
+ // its current ?vh= cache key; append templateQuery and it IS a render:
520
+ // <img src=`${stored.url}&${templateQuery}`>
521
+ // (`url` can rarely come back empty — the record read-back after the
522
+ // upload failed; the file is stored regardless, under stored.uuid. And
523
+ // join with '?' instead of '&' if your URL happens to carry no query.)
524
+ })
525
+ ```
526
+
527
+ Added after 0.3.0 — if your pinned CDN bundle predates it, the attribute is
528
+ silently ignored; check the [changelog](./CHANGELOG.md).
529
+
530
+ What to know:
531
+
532
+ - **The raw `content` arrives either way.** A failed upload replaces `stored`
533
+ with `storeError` (a message) — whether a save without a rendering copy
534
+ counts as saved is your call, made where it always is: the save ack
535
+ (`confirmSave(false)` / returning `false` from React's `onSave`).
536
+ - **The raw content never dies with the element.** Closing the editor changes
537
+ nothing: the element outlives a close, so a save still inside its upload
538
+ window simply emits moments later with its real outcome — possibly after
539
+ the `close` event. Only *removing* the element mid-upload flushes the
540
+ pending `save` immediately, raw content with a `storeError` note (the copy,
541
+ if it lands, goes unreported); the React wrapper does the same from its
542
+ unmount cleanup. And rapid saves are serialized, so `save` events always
543
+ arrive in the order the editor posted them.
544
+ - **Same name + folder versions the file in place.** When `template-id` names
545
+ an existing DAM file, its own folder is reused; otherwise new templates land
546
+ in `store-folder` (default `/`). Under a VERSION conflict policy each save is
547
+ its own file row — `stored.uuid` is always the current one, so persist it
548
+ (don't echo it into `template-id`: an id change deliberately reloads the
549
+ editor). The element remembers the copy it last made, so follow-up saves —
550
+ including unchanged re-saves, which resolve to that copy instead of
551
+ erroring — keep working against it even while your own id stays opaque.
552
+ That memory lives in the element instance: to carry it across a page
553
+ reload or remount, pass the persisted uuid back in as `stored-uuid` /
554
+ `storedUuid` (or the `storedUuid` field of `load()`) alongside the content.
555
+ - **The stored `template_query` is record-agnostic.** Values of
556
+ [custom-metadata-bound](#custom-metadata-fields) variables are stripped
557
+ before the query is attached as file metadata, so one record's data never
558
+ becomes the template's own default.
559
+ - **The credential needs upload rights.** A
560
+ [security template](#security-template-guest-auth) must have a scope that
561
+ allows uploads, or every save reports `storeError`.
501
562
 
502
563
  ### Starting a template from scratch
503
564
 
@@ -557,10 +618,10 @@ template is just a template, and `BLANK_TEMPLATE_XML` is exported from
557
618
 
558
619
  ### Reporting a failed save
559
620
 
560
- The editor clears its unsaved-changes state as soon as it posts `save` —
561
- delivery of a postMessage says nothing about whether you stored anything. Tell
562
- it when you didn't, and it restores the dirty flag and warns the user instead of
563
- showing a failed write as saved:
621
+ The editor clears its unsaved-changes state as soon as it emits `save` —
622
+ receiving the event says nothing about whether you stored anything. Tell it when
623
+ you didn't, and it restores the dirty flag and warns the user instead of showing
624
+ a failed write as saved:
564
625
 
565
626
  ```js
566
627
  builder.addEventListener('save', async (e) => {
@@ -589,18 +650,23 @@ without it a failed write is invisible to the user.
589
650
  | Attribute / property | Required | Description |
590
651
  | --- | --- | --- |
591
652
  | `base-url` / `baseUrl` | yes | Origin of the design-templates-app deployment |
592
- | `token` | yes | Filerobot token (`ftoken`) |
653
+ | `token` | yes | Scaleflex token (`ftoken`) |
593
654
  | `sass-key` / `sassKey` | session auth | Project sass key |
594
655
  | `session-uuid` / `sessionUuid` | session auth | Hub session uuid |
595
- | `sec-template` / `secTemplate` | guest auth | Filerobot security-template key, *instead of* `sass-key` + `session-uuid`. Stateless only — see [Authentication](#authentication) |
656
+ | `sec-template` / `secTemplate` | guest auth | Scaleflex security-template key, *instead of* `sass-key` + `session-uuid`. Stateless only — see [Authentication](#authentication) |
596
657
  | `company-uuid`, `project-uuid` | no | Company / project scoping (session auth only) |
597
- | `template-id` / `templateId` | no | DAM-backed: Filerobot uuid to edit, empty opens the new-template flow. Stateless: opaque id echoed back on `save` |
658
+ | `template-id` / `templateId` | no | DAM-backed: the file uuid to edit, empty opens the new-template flow. Stateless: opaque id echoed back on `save` |
598
659
  | `mode` | no | `inline` (default; size the element) or `modal` (fullscreen overlay, starts closed — call `open()`) |
599
660
  | `stateless` | no | Pass the template in and take it back out instead of using the DAM (see [Stateless](#stateless)). Requires `content`, or `new-template` |
600
661
  | `content` (property only) | stateless | The template to edit, as `.fdt` XML. Assigning a new value loads it into a running editor |
601
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) |
602
663
  | `template-name` / `templateName` | no | Stateless: header title |
603
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 |
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
+ | `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
+ | `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 |
604
670
  | `brand-color` / `brandColor` | no | Accent colour for the editor chrome, `#rgb` / `#rrggbb` |
605
671
  | `theme` | no | `light`, `dark` or `auto` |
606
672
  | `ready-timeout` / `readyTimeout` | no | Ms to wait for the app handshake before `error` (default 20000, 0 disables) |
@@ -628,9 +694,9 @@ long-lived master credential in client-side code.
628
694
 
629
695
  #### Security template (guest auth)
630
696
 
631
- A Filerobot **security template** is a named, permission-scoped credential you
632
- define once in your Filerobot project — the same guest-auth mechanism the other
633
- Scaleflex widgets use. Hand one to the widget and no Hub session is involved at
697
+ A **security template** is a named, permission-scoped credential you define once
698
+ in your Scaleflex project — the same guest-auth mechanism the other Scaleflex
699
+ widgets use. Hand one to the widget and no Hub session is involved at
634
700
  all.
635
701
 
636
702
  It is what makes a stateless embed work without Hub accounts. Even when the
@@ -680,17 +746,85 @@ before you pick it:
680
746
 
681
747
  - **Stateless only.** Setting `sec-template` without `stateless` is a config
682
748
  error — the widget reports `error` with code `invalid-config` and never mounts
683
- the iframe. The dashboard and the DAM-backed editor keep requiring a session.
749
+ the editor. The dashboard and the DAM-backed editor keep requiring a session.
684
750
  - **Hub-project features come back empty** — metadata fields, regional variants
685
751
  and dynamic fields have no model to read, and project branding does not apply
686
752
  (theme the chrome with `brand-color` / `theme` instead).
687
753
  <!-- internal:end -->
688
754
 
755
+ ### Custom metadata fields
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
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`
761
+ instead of having to remember which slug happens to mean the SKU.
762
+
763
+ ```html
764
+ <sfx-template-builder
765
+ base-url="https://templates.example.com"
766
+ token="…"
767
+ sec-template="…"
768
+ stateless
769
+ custom-metadata='[
770
+ { "key": "sku", "title": "SKU", "group": "Product" },
771
+ { "key": "price", "title": "Price", "group": "Product" },
772
+ { "key": "campaign", "title": "Campaign name" }
773
+ ]'
774
+ ></sfx-template-builder>
775
+ ```
776
+
777
+ ```ts
778
+ // Or as a property, which is nicer for anything built at runtime:
779
+ builder.customMetadata = fields.map((f) => ({ key: f.id, title: f.label }))
780
+ ```
781
+
782
+ Fields appear in the editor's picker in the order you declare them, grouped
783
+ under `group` where present, with `title` (or the bare `key`) as the label. Set
784
+ no model and the source is not offered at all.
785
+
786
+ The source is called **Custom metadata** in the editor by default;
787
+ `custom-metadata-label` renames it to fit your domain — `External metadata`,
788
+ `Product attributes`, whatever your authors know it as. Wording only: the saved
789
+ template carries the same `custom_ckey` either way. (The label is another
790
+ post-0.3.0 addition — an older pinned bundle ignores it.)
791
+
792
+ **Names only — no values travel with the model, and the editor resolves nothing
793
+ against it.** The binding is stored in the saved `.fdt` on the variable as
794
+ `custom_ckey`, and the variable renders exactly like a free-text one: your
795
+ pipeline substitutes the value by putting `$slug=value` in the render query.
796
+
797
+ ```xml
798
+ <variable annotation_id="text_1" display="Product code" name="code"
799
+ type="text_placeholder" source="URL" custom_ckey="sku"
800
+ default="AB-1234" />
801
+ ```
802
+
803
+ So the round trip is: read `custom_ckey` back from the template you stored, look
804
+ up that field in your own data, and render with `$code=<that value>`. Until you
805
+ do, the editor and any render show the variable's default value.
806
+
807
+ A few consequences worth knowing:
808
+
809
+ - **The model can change between sessions.** A variable bound to a key your
810
+ current model does not list keeps its binding — the editor shows it read-only
811
+ and says so — so opening a template with a narrower model never silently
812
+ rewrites it.
813
+ - **Nothing is validated against the model at render time.** A key you stop
814
+ sending simply stops being substituted, and the default shows instead.
815
+ - **Send it whenever you like.** The widget delivers the model as soon as the
816
+ editor is ready and re-sends it whenever you change it; assigning an equal
817
+ model is a no-op. It applies to DAM-backed embeds as well as stateless ones.
818
+ - **Malformed fields are dropped, not fatal.** An entry with no `key`, a
819
+ duplicate `key` (first one wins), or a `key` containing a character XML cannot
820
+ carry — a C0 control, an unpaired surrogate — is skipped; the rest of the model
821
+ still works. Ordinary text, punctuation, accents and emoji are all fine.
822
+
689
823
  ### Origin registration
690
824
 
691
- Your page's origin must be in the deployment's `frame-ancestors` allowlist
692
- (`NEXT_PUBLIC_TRUSTED_HUB_ORIGINS`), otherwise the browser refuses to render the
693
- iframe — Chrome shows "refused to connect" in place of the editor, and the
825
+ Your page's origin must be in the deployment's embedding allowlist
826
+ (`NEXT_PUBLIC_TRUSTED_HUB_ORIGINS`), otherwise the browser refuses to load the
827
+ editor on your page — Chrome shows "refused to connect" in its place, and the
694
828
  widget reports `handshake-timeout`.
695
829
 
696
830
  A deployment allows `'self'`, `https://*.scaleflex.com`,
@@ -711,9 +845,10 @@ without CHIPS support that block third-party cookies will fail with `auth` or
711
845
 
712
846
  | Method | Description |
713
847
  | --- | --- |
714
- | `open(templateId?)` | Open the editor, loading the iframe. Optionally switch template first. |
715
- | `close()` | Close the editor and unload the iframe. Does not emit `close`. |
716
- | `load({ content, templateId?, name?, templateQuery? })` | Stateless: load a template, opening the editor if needed. `templateQuery` picks the render to open on — see [About `templateQuery`](#about-templatequery). |
848
+ | `open(templateId?)` | Open the editor, loading it if it isn't loaded yet. Optionally switch template first. |
849
+ | `close()` | Close the editor and unload it. Does not emit `close`. |
850
+ | `load({ content, templateId?, name?, templateQuery?, storedUuid? })` | Stateless: load a template, opening the editor if needed. `templateQuery` picks the render to open on — see [About `templateQuery`](#about-templatequery). `storedUuid` seeds the [dam-store](#storing-a-rendering-copy-dam-store) memory; omitting it clears the seed. |
851
+ | `flushPendingSaves(reason?)` | Emit any `dam-store` saves still waiting on their upload, raw content with `storeError` in place of the links. Only for framework wrappers that unsubscribe listeners before removing the element — the React wrapper calls it for you. |
717
852
  | `createNew({ templateId?, name? })` | Stateless: open on a new, empty template — no XML needed. See [Starting a template from scratch](#starting-a-template-from-scratch). |
718
853
  | `confirmSave(ok, message?)` | Stateless: report whether you persisted the content. See [Reporting a failed save](#reporting-a-failed-save). |
719
854
 
@@ -730,7 +865,7 @@ All events are `CustomEvent`s; the payload is in `detail`.
730
865
  | --- | --- | --- |
731
866
  | `ready` | — | The editor mounted and auth validated. Clears the handshake timeout. |
732
867
  | `open` | — | The editor UI opened. |
733
- | `save` | `{ uuid, name }` (DAM-backed) or `{ templateId, content, name, templateQuery }` (stateless) | The user saved. |
868
+ | `save` | `{ uuid, name }` (DAM-backed) or `{ templateId, content, name, templateQuery }` (stateless; under [`dam-store`](#storing-a-rendering-copy-dam-store) also `stored: { uuid, url }` or `storeError`) | The user saved. |
734
869
  | `dirtychange` | `{ isDirty }` | Stateless: the unsaved-changes flag flipped. |
735
870
  | `close` | — | The user left the editor, or it unmounted. |
736
871
  | `error` | `{ code, message? }` | See below. |
@@ -739,10 +874,10 @@ All events are `CustomEvent`s; the payload is in `detail`.
739
874
 
740
875
  | Code | Means |
741
876
  | --- | --- |
742
- | `auth` | The app could not authenticate — blocked cookies, an expired session, or a security template the Filerobot API refused. |
877
+ | `auth` | The app could not authenticate — blocked cookies, an expired session, or a security template the Scaleflex API refused. |
743
878
  | `invalid-content` | Stateless: the `content` you sent is not a parseable `.fdt` document. |
744
- | `invalid-config` | Attributes that contradict each other, e.g. `sec-template` without `stateless`. The iframe never mounts. |
745
- | `handshake-timeout` | No ready signal in time — usually a missing `frame-ancestors` entry or blocked third-party cookies. |
879
+ | `invalid-config` | Attributes that contradict each other, e.g. `sec-template` without `stateless`. The editor never mounts. |
880
+ | `handshake-timeout` | No ready signal in time — usually a missing entry in the deployment's embedding allowlist, or blocked third-party cookies. |
746
881
  | `invalid-base-url` | `base-url` is not a URL. |
747
882
  | `unknown` | Anything the app reported that this version does not name. |
748
883
 
@@ -750,6 +885,13 @@ All events are `CustomEvent`s; the payload is in `detail`.
750
885
  `modal` mode the element tears its overlay down; in `inline` mode it is yours to
751
886
  act on — the editor never navigates itself anywhere.
752
887
 
888
+ Under [`dam-store`](#storing-a-rendering-copy-dam-store), a save whose upload
889
+ is still in flight emits **after** `close`, with its real outcome. Keep your
890
+ `save` listener attached until it arrives — or, if your close handler tears the
891
+ element down anyway, removing it from the DOM flushes the pending save to your
892
+ still-attached listeners; call `flushPendingSaves()` yourself only if you
893
+ detach listeners without removing the element.
894
+
753
895
  ## React API
754
896
 
755
897
  Props mirror the attributes in camelCase, plus `className` and `style`.
@@ -763,6 +905,10 @@ The credential props are a discriminated union, so the two
763
905
  `onSave` may return `false` or a promise; see
764
906
  [Reporting a failed save](#reporting-a-failed-save).
765
907
 
908
+ `customMetadata` is an array prop, compared by identity like the rest — hoist it
909
+ to module scope or memoise it, or every render counts as a change (harmless; the
910
+ element de-dupes by value before it says anything to the editor).
911
+
766
912
  The component forwards a ref to the underlying element, which is how you reach
767
913
  the [imperative API](#public-methods) — required for `mode="modal"`, which
768
914
  renders nothing until `open()` is called:
@@ -877,15 +1023,15 @@ only thing it needs at runtime is a reachable `design-templates-app` for the
877
1023
  demo page's Base URL field to point at; it defaults to
878
1024
  `https://design-templates.scaleflex.com`.
879
1025
 
880
- That default frames successfully only from an origin that deployment allows —
1026
+ That default loads successfully only from an origin that deployment allows —
881
1027
  `'self'`, `*.scaleflex.com`, `*.filerobot.com` and `http://localhost:5173`,
882
1028
  which covers both the demo as published to the CDN and `yarn dev:demo`. Serving
883
1029
  it anywhere else — another port (`yarn preview:demo` uses 4173), or a copy on
884
1030
  your own domain — gets "refused to connect" until that origin joins the
885
1031
  deployment's allowlist, which is baked in at build time and so needs a rebuild
886
1032
  (see [Origin registration](#origin-registration)). Driving a *locally* running
887
- app has the same requirement in reverse: `'self'` does not cover
888
- `localhost:5173` framing `localhost:3000`, but the default list now does.
1033
+ app has the same requirement in reverse: `'self'` does not cover a page on
1034
+ `localhost:5173` embedding `localhost:3000`, but the default list now does.
889
1035
 
890
1036
  The demo page takes its XML from a URL, from a paste, or from a picker listing
891
1037
  the `.fdt` files in the project its credentials point at, which fills the id,
@@ -895,6 +1041,15 @@ whatever that template's scope allows. The picker is demo scaffolding standing
895
1041
  in for a host's own template store — the widget itself only ever sees the XML
896
1042
  the page hands it.
897
1043
 
1044
+ It also plays the host half of
1045
+ [custom metadata](#custom-metadata-fields). The panel carries a switch and an
1046
+ editable JSON model — sample fields to start from, and add whatever of your own
1047
+ you like. Each field takes an optional `value`, which never reaches the editor:
1048
+ the page substitutes it into the render query for any variable bound to that
1049
+ field, on the way in and again on the way out, and logs each substitution. That
1050
+ is exactly the work a real integration does, so binding a variable to `sku` and
1051
+ reloading shows the host's SKU on the canvas rather than the variable's default.
1052
+
898
1053
  End-to-end cover for the embed boundary lives in the app repo at
899
1054
  `e2e/embed-widget.spec.ts` — a cross-origin host page loads the built CDN
900
1055
  bundle, pulls template XML from its own API and gets the edit back. It needs the
@@ -907,7 +1062,7 @@ yarn build && yarn start
907
1062
  yarn test:e2e:embed
908
1063
  ```
909
1064
 
910
- The postMessage protocol lives in `src/protocol.ts` and is shared with the app
1065
+ The host↔app protocol lives in `src/protocol.ts` and is shared with the app
911
1066
  via the `./protocol` export, so the two sides cannot drift. Message *values* are
912
1067
  wire format: never change an existing string, only add new messages, so an older
913
1068
  widget keeps working against a newer app deployment and vice versa.
@@ -926,7 +1081,7 @@ npm publish, git commit + tag + push. It needs a `.env.local` in this package:
926
1081
  ```env
927
1082
  FILEROBOT_CDN_TOKEN=scaleflex
928
1083
  FILEROBOT_CDN_SECU=<secu key>
929
- FILEROBOT_CDN_FOLDER=/plugins/scaleflex/design-template-builder/{version}/
1084
+ FILEROBOT_CDN_FOLDER=/plugins/cloudimage/template-builder/{version}/
930
1085
  ```
931
1086
 
932
1087
  Update [CHANGELOG.md](./CHANGELOG.md) before releasing.
@@ -0,0 +1,92 @@
1
+ /**
2
+ * The `dam-store` save path: upload an edited template to Filerobot so the
3
+ * CDN can render it.
4
+ *
5
+ * A stateless save hands the host raw XML — but the CDN renders only stored
6
+ * files, so a host that wants render URLs (previews, production banners) needs
7
+ * a copy in the DAM too. With `dam-store` the element makes that copy itself,
8
+ * with the same multipart upload the DAM-backed editor uses, and the `save`
9
+ * event carries the stored file's links next to the raw data.
10
+ *
11
+ * The raw `content` remains the host's copy of record: nothing here changes
12
+ * what the save event has always carried.
13
+ */
14
+ import type { BuilderContentData } from './protocol';
15
+ export declare const FILEROBOT_API = "https://api.filerobot.com";
16
+ /** Credentials the element already holds; one of sassKey / secTemplate. */
17
+ export interface DamStoreAuth {
18
+ token: string;
19
+ sassKey?: string;
20
+ secTemplate?: string;
21
+ sessionUuid?: string;
22
+ companyUuid?: string;
23
+ projectUuid?: string;
24
+ }
25
+ /** The stored copy's links, carried on the `save` event as `detail.stored`. */
26
+ export interface StoredTemplate {
27
+ /** DAM file uuid of the stored `.fdt`. */
28
+ uuid: string;
29
+ /**
30
+ * CDN URL of the stored file, with its current `?vh=` cache key — append a
31
+ * template query to render it. Empty when the file record could not be read
32
+ * back after the upload (the file is stored regardless).
33
+ */
34
+ url: string;
35
+ }
36
+ export interface FileRecord {
37
+ uuid?: string;
38
+ name?: string;
39
+ folder?: {
40
+ name?: string;
41
+ };
42
+ url?: {
43
+ cdn?: string;
44
+ public?: string;
45
+ path?: string;
46
+ };
47
+ }
48
+ /**
49
+ * A security template is not a key — it is exchanged for a short-lived sass
50
+ * key first, the template authenticating its own exchange. Same call the app
51
+ * and the asset picker make.
52
+ *
53
+ * Exported (with `apiHeaders` / `getFileRecord`) for the demo page, which
54
+ * plays the host half of the same API conversation — one implementation of
55
+ * the auth rules, not two drifting copies.
56
+ */
57
+ export declare function resolveKey(auth: DamStoreAuth): Promise<string>;
58
+ /**
59
+ * Session scope only: a minted key carries its own, and pairing it with a
60
+ * session's uuids would mix one mode's key with the other mode's scope.
61
+ */
62
+ export declare function apiHeaders(auth: DamStoreAuth, key: string): Record<string, string>;
63
+ /**
64
+ * Whether a template id plausibly names a DAM file (hex-and-dashes uuid).
65
+ * Opaque host ids ('demo-1', 'sample-spring-banner') never do — looking them
66
+ * up would waste a round trip per save and couple every save to whichever
67
+ * status the API happens to answer a malformed id with.
68
+ */
69
+ export declare function looksLikeDamFileUuid(id: string): boolean;
70
+ /**
71
+ * One file's record. `null` means the identifier names nothing — a 404/gone,
72
+ * a 4xx rejecting the id itself, or the API's not-found envelope — all normal
73
+ * answers here. What THROWS is a failure to answer (auth, rate limit, 5xx,
74
+ * network): collapsing those into null would make a transient blip read as
75
+ * "file gone", and the callers act on that — re-homing an existing template
76
+ * into the fallback folder as a duplicate, or failing an unchanged re-save.
77
+ */
78
+ export declare function getFileRecord(auth: DamStoreAuth, key: string, uuid: string): Promise<FileRecord | null>;
79
+ /**
80
+ * Store one save in the DAM and return the stored copy's links.
81
+ *
82
+ * Uploads into the folder the file this template is already stored as lives
83
+ * in — the host's `templateId` when it names a DAM file, else `knownUuid`
84
+ * (the copy a previous save in this session made; hosts persist `stored.uuid`
85
+ * rather than echoing it into `template-id`, which would reload the editor) —
86
+ * so same name + folder makes the backend version the template in place. New
87
+ * templates land in `fallbackFolder`. Unchanged content resolves to the
88
+ * already-stored file rather than failing. Throws with a human-readable
89
+ * message when the copy could not be made; the caller decides what a save
90
+ * without a stored copy means.
91
+ */
92
+ export declare function storeTemplateInDam(data: BuilderContentData, auth: DamStoreAuth, fallbackFolder: string, knownUuid?: string): Promise<StoredTemplate>;
package/dist/define.cjs CHANGED
@@ -1,2 +1,2 @@
1
- "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const e=require("./template-builder-CK2Zlo7E.cjs");typeof customElements<"u"&&!customElements.get("sfx-template-builder")&&customElements.define("sfx-template-builder",e.SfxTemplateBuilder);exports.SfxTemplateBuilder=e.SfxTemplateBuilder;
1
+ "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const e=require("./template-builder-Byqg1q93.cjs");typeof customElements<"u"&&!customElements.get("sfx-template-builder")&&customElements.define("sfx-template-builder",e.SfxTemplateBuilder);exports.SfxTemplateBuilder=e.SfxTemplateBuilder;
2
2
  //# sourceMappingURL=define.cjs.map
package/dist/define.js CHANGED
@@ -1,4 +1,4 @@
1
- import { S as e } from "./template-builder-De0hRO4s.js";
1
+ import { S as e } from "./template-builder-B9Cwo_Q-.js";
2
2
  typeof customElements < "u" && !customElements.get("sfx-template-builder") && customElements.define("sfx-template-builder", e);
3
3
  export {
4
4
  e as SfxTemplateBuilder
package/dist/index.cjs CHANGED
@@ -1,2 +1,2 @@
1
- "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const E=require("./template-builder-CK2Zlo7E.cjs");exports.AUTH_MODES=E.AUTH_MODES;exports.BLANK_TEMPLATE_XML=E.BLANK_TEMPLATE_XML;exports.BRAND_COLOR_PATTERN=E.BRAND_COLOR_PATTERN;exports.BUILDER_CLOSE=E.BUILDER_CLOSE;exports.BUILDER_CONTENT=E.BUILDER_CONTENT;exports.BUILDER_CONTENT_REQUEST=E.BUILDER_CONTENT_REQUEST;exports.BUILDER_DIRTY=E.BUILDER_DIRTY;exports.BUILDER_ERROR=E.BUILDER_ERROR;exports.BUILDER_OPEN=E.BUILDER_OPEN;exports.BUILDER_READY=E.BUILDER_READY;exports.BUILDER_SAVE=E.BUILDER_SAVE;exports.EMBED_PARAMS=E.EMBED_PARAMS;exports.EMBED_ROUTE=E.EMBED_ROUTE;exports.HOST_LOAD=E.HOST_LOAD;exports.HOST_SAVED=E.HOST_SAVED;exports.PROTOCOL_VERSION=E.PROTOCOL_VERSION;exports.SfxTemplateBuilder=E.SfxTemplateBuilder;exports.builderRoute=E.builderRoute;
1
+ "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const E=require("./template-builder-Byqg1q93.cjs");exports.AUTH_MODES=E.AUTH_MODES;exports.BLANK_TEMPLATE_XML=E.BLANK_TEMPLATE_XML;exports.BRAND_COLOR_PATTERN=E.BRAND_COLOR_PATTERN;exports.BUILDER_CLOSE=E.BUILDER_CLOSE;exports.BUILDER_CONTENT=E.BUILDER_CONTENT;exports.BUILDER_CONTENT_REQUEST=E.BUILDER_CONTENT_REQUEST;exports.BUILDER_DIRTY=E.BUILDER_DIRTY;exports.BUILDER_ERROR=E.BUILDER_ERROR;exports.BUILDER_OPEN=E.BUILDER_OPEN;exports.BUILDER_READY=E.BUILDER_READY;exports.BUILDER_SAVE=E.BUILDER_SAVE;exports.EMBED_PARAMS=E.EMBED_PARAMS;exports.EMBED_ROUTE=E.EMBED_ROUTE;exports.HOST_CONFIG=E.HOST_CONFIG;exports.HOST_LOAD=E.HOST_LOAD;exports.HOST_SAVED=E.HOST_SAVED;exports.PROTOCOL_VERSION=E.PROTOCOL_VERSION;exports.SfxTemplateBuilder=E.SfxTemplateBuilder;exports.builderRoute=E.builderRoute;
2
2
  //# sourceMappingURL=index.cjs.map
package/dist/index.d.ts CHANGED
@@ -1,2 +1,2 @@
1
- export { SfxTemplateBuilder, type TemplateBuilderStatus, type TemplateBuilderEventMap, type TemplateBuilderSaveDetail, } from './template-builder';
1
+ export { SfxTemplateBuilder, type TemplateBuilderStatus, type TemplateBuilderEventMap, type TemplateBuilderSaveDetail, type StoredTemplate, type DamStoreAuth, } from './template-builder';
2
2
  export * from './protocol';
package/dist/index.js CHANGED
@@ -1,22 +1,23 @@
1
- import { A as a, B as _, a as s, b as D, c as O, d as T, e as L, f as B, g as A, h as S, i as U, E as I, j as N, H as e, k as M, P, S as C, l } from "./template-builder-De0hRO4s.js";
1
+ import { A as a, B as _, a as s, b as O, c as T, d as D, e as L, f as B, g as A, h as S, i as I, E as U, j as N, H as e, k as C, l as M, P, S as H, m as l } from "./template-builder-B9Cwo_Q-.js";
2
2
  export {
3
3
  a as AUTH_MODES,
4
4
  _ as BLANK_TEMPLATE_XML,
5
5
  s as BRAND_COLOR_PATTERN,
6
- D as BUILDER_CLOSE,
7
- O as BUILDER_CONTENT,
8
- T as BUILDER_CONTENT_REQUEST,
6
+ O as BUILDER_CLOSE,
7
+ T as BUILDER_CONTENT,
8
+ D as BUILDER_CONTENT_REQUEST,
9
9
  L as BUILDER_DIRTY,
10
10
  B as BUILDER_ERROR,
11
11
  A as BUILDER_OPEN,
12
12
  S as BUILDER_READY,
13
- U as BUILDER_SAVE,
14
- I as EMBED_PARAMS,
13
+ I as BUILDER_SAVE,
14
+ U as EMBED_PARAMS,
15
15
  N as EMBED_ROUTE,
16
- e as HOST_LOAD,
16
+ e as HOST_CONFIG,
17
+ C as HOST_LOAD,
17
18
  M as HOST_SAVED,
18
19
  P as PROTOCOL_VERSION,
19
- C as SfxTemplateBuilder,
20
+ H as SfxTemplateBuilder,
20
21
  l as builderRoute
21
22
  };
22
23
  //# sourceMappingURL=index.js.map