@scaleflex/template-builder 0.1.1 → 0.4.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.
@@ -8,7 +8,6 @@ metadata:
8
8
  category: integration
9
9
  tags:
10
10
  - scaleflex
11
- - filerobot
12
11
  - template-builder
13
12
  - design-templates
14
13
  - web-component
@@ -23,7 +22,7 @@ metadata:
23
22
  - User says "add the template builder", "embed the design template editor",
24
23
  "let users edit templates in our app"
25
24
  - User asks how to use `@scaleflex/template-builder` in their project
26
- - User wants a banner / creative editor backed by Filerobot rendering
25
+ - User wants a banner / creative editor backed by Scaleflex rendering
27
26
 
28
27
  ## Step 1 — Decide who stores the template
29
28
 
@@ -32,18 +31,19 @@ not already clear from the project.
32
31
 
33
32
  | | **DAM-backed** (default) | **Stateless** |
34
33
  |---|---|---|
35
- | Template lives in | Filerobot, as a `.fdt` file | The host's own database |
36
- | `template-id` is | a Filerobot file uuid | any string the host chooses |
34
+ | Template lives in | Scaleflex, as a `.fdt` file | The host's own database |
35
+ | `template-id` is | a DAM file uuid | any string the host chooses |
37
36
  | Save does | uploads a new version, returns `{ uuid }` | hands back `{ content }` for the host to store |
38
37
  | Needs per-user Scaleflex identities | yes | no — one service tenant is enough |
39
38
 
40
39
  Pick **stateless** when the host already has its own users, permissions and
41
40
  storage and does not want to map them onto Scaleflex tenants. Pick
42
- **DAM-backed** when templates are a Filerobot asset like any other.
41
+ **DAM-backed** when templates are a Scaleflex asset like any other.
43
42
 
44
43
  Statelessness applies to the **document only**. The editor still needs an
45
- authenticated Filerobot tenant for server-side text rendering, custom fonts,
46
- asset browsing, and metadata variables.
44
+ authenticated Scaleflex tenant for server-side text rendering, custom fonts,
45
+ asset browsing, and metadata variables — so a stateless embed still needs a
46
+ credential (Step 7).
47
47
 
48
48
  ## Step 2 — Detect the target framework
49
49
 
@@ -61,7 +61,7 @@ npm i @scaleflex/template-builder
61
61
  Or via CDN (self-registering, Lit bundled in — pin the major):
62
62
 
63
63
  ```html
64
- <script type="module" src="https://cdn.scaleflex.com/plugins/scaleflex/template-builder/0.1.0/template-builder.min.js"></script>
64
+ <script type="module" src="https://cdn.cloudimage.io/template-builder/0.4.0/template-builder.min.js"></script>
65
65
  ```
66
66
 
67
67
  **SSR caution:** the element extends `HTMLElement`, so importing `.`,
@@ -82,7 +82,7 @@ in, take the edited XML back out.**
82
82
  <sfx-template-builder
83
83
  stateless
84
84
  base-url="https://<deployment>"
85
- token="FILEROBOT_TOKEN"
85
+ token="PROJECT_TOKEN"
86
86
  sass-key="SASS_KEY"
87
87
  session-uuid="SESSION_UUID"
88
88
  style="display:block;height:800px"
@@ -144,15 +144,48 @@ import { TemplateBuilder } from '@scaleflex/template-builder/react'
144
144
  />
145
145
  ```
146
146
 
147
+ ### Stateless — a template that does not exist yet
148
+
149
+ A host whose user is creating their first template has no XML to pass in, and
150
+ should not have to author any. Set `new-template` instead of `content` and the
151
+ widget supplies the empty document:
152
+
153
+ ```html
154
+ <sfx-template-builder stateless new-template template-name="Untitled" …>
155
+ ```
156
+
157
+ ```js
158
+ builder.createNew({ templateId: 'your-own-id-43', name: 'Untitled' })
159
+ ```
160
+
161
+ ```tsx
162
+ <TemplateBuilder stateless newTemplate templateName="Untitled" … />
163
+ ```
164
+
165
+ The editor opens on its empty state, the user adds the first layout (canvas
166
+ size, background, preset), and the first `save` hands back a complete `.fdt`
167
+ document — store that and every later open is the ordinary load flow. Save is
168
+ refused until a layout exists.
169
+
170
+ - `templateId` is optional; without one the `save` payload just arrives without
171
+ an id, and the host allocates one when storing.
172
+ - `templateQuery` stays empty — a new document has no layouts or variables for
173
+ a query to select. It comes back on the first save.
174
+ - `content` wins when both are set, so one element can serve both cases.
175
+ - Empty `content` alone does **not** start a blank template: it means the host
176
+ is still fetching, and the editor keeps waiting. Only the flag changes that.
177
+ - For a house-style starting point (standard canvas, locked logo layer), pass
178
+ it as ordinary `content` — a starter template is just a template.
179
+
147
180
  ### DAM-backed
148
181
 
149
- `template-id` is a Filerobot file uuid; omit it to open the new-template flow.
182
+ `template-id` is a DAM file uuid; omit it to open the new-template flow.
150
183
  The app loads and saves the file itself.
151
184
 
152
185
  ```html
153
186
  <sfx-template-builder
154
187
  base-url="https://<deployment>"
155
- token="FILEROBOT_TOKEN"
188
+ token="PROJECT_TOKEN"
156
189
  sass-key="SASS_KEY"
157
190
  session-uuid="SESSION_UUID"
158
191
  template-id="TEMPLATE_UUID"
@@ -164,8 +197,8 @@ The app loads and saves the file itself.
164
197
 
165
198
  ## Step 5 — Handle the save outcome (stateless only)
166
199
 
167
- The editor clears its unsaved-changes state as soon as it posts `save`, because
168
- a delivered postMessage says nothing about whether the host stored anything.
200
+ The editor clears its unsaved-changes state as soon as it emits `save`, because
201
+ receiving the event says nothing about whether the host stored anything.
169
202
  **Call `confirmSave(false)` when your write fails**, or a failed save silently
170
203
  looks successful and the user loses work:
171
204
 
@@ -211,7 +244,7 @@ it when the host has no Scaleflex identity to hand over per user:
211
244
  ```html
212
245
  <sfx-template-builder
213
246
  base-url="https://<deployment>"
214
- token="FILEROBOT_TOKEN"
247
+ token="PROJECT_TOKEN"
215
248
  sec-template="SEC_TEMPLATE_KEY"
216
249
  stateless
217
250
  ></sfx-template-builder>
@@ -232,15 +265,28 @@ What it costs:
232
265
  In stateless mode a single service tenant is usually right either way: one
233
266
  machine credential, with all per-user permission logic staying in the host app.
234
267
 
268
+ **If asked why a stateless embed needs a credential at all** — because the
269
+ preview is not drawn by the browser. A `.fdt` is rendered server-side with
270
+ ImageMagick when its CDN URL is requested, and browser text APIs cannot
271
+ reproduce that layout (kerning, letter spacing, wrapping, baselines,
272
+ antialiasing differ per browser and from the export), so the editor rasterizes
273
+ every text and shape layer through the same engine via
274
+ `POST /api/render-layers` on the `base-url` deployment — batched, debounced,
275
+ supersampled 3×, composited with CSS. That endpoint fetches fonts and images
276
+ server-side, so it authenticates on the handed-over credential and returns
277
+ `401` without one: no credential, no visible text or shape layers. The security
278
+ template is what authorizes it when there is no Hub session. Full explanation:
279
+ *Why the editor still calls a server* in the README.
280
+
235
281
  ## Step 8 — Register the embedding origin (required)
236
282
 
237
- The host page's origin must be in the deployment's `frame-ancestors` allowlist,
238
- via `NEXT_PUBLIC_TRUSTED_HUB_ORIGINS` on the `design-templates-app` deployment.
283
+ The host page's origin must be in the deployment's embedding allowlist, via
284
+ `NEXT_PUBLIC_TRUSTED_HUB_ORIGINS` on the `design-templates-app` deployment.
239
285
  This is baked in at build time, so it needs a redeploy.
240
286
 
241
- **If this is missed**, the browser refuses to render the iframe and the widget
242
- reports `error` with code `handshake-timeout`. That is the single most common
243
- first-integration failure — check it before anything else.
287
+ **If this is missed**, the browser refuses to load the editor on the host page
288
+ and the widget reports `error` with code `handshake-timeout`. That is the
289
+ single most common first-integration failure — check it before anything else.
244
290
 
245
291
  ## Step 9 — Theming (optional)
246
292
 
@@ -263,15 +309,16 @@ template document.
263
309
  | Attribute / property | Required | Description |
264
310
  |---|---|---|
265
311
  | `base-url` / `baseUrl` | yes | Origin of the design-templates-app deployment |
266
- | `token` | yes | Filerobot token (`ftoken`) |
312
+ | `token` | yes | Scaleflex token (`ftoken`) |
267
313
  | `sass-key` / `sassKey` | session auth | Project sass key |
268
314
  | `session-uuid` / `sessionUuid` | session auth | Hub session uuid |
269
315
  | `sec-template` / `secTemplate` | guest auth | Security-template key, instead of sass key + session uuid. Stateless only (Step 7) |
270
316
  | `company-uuid`, `project-uuid` | no | Company / project scoping (session auth only) |
271
- | `template-id` / `templateId` | no | DAM: Filerobot uuid. Stateless: opaque host id |
317
+ | `template-id` / `templateId` | no | DAM: the file uuid. Stateless: opaque host id |
272
318
  | `mode` | no | `inline` (default) or `modal` (starts closed — call `open()`) |
273
- | `stateless` | no | Host owns the document; requires `content` |
319
+ | `stateless` | no | Host owns the document; requires `content`, or `new-template` |
274
320
  | `content` (property only) | stateless | Template as `.fdt` XML |
321
+ | `new-template` / `newTemplate` | no | Stateless: open on a new, empty template — the widget supplies the blank document. Ignored when `content` is set |
275
322
  | `template-name` / `templateName` | no | Stateless: header title |
276
323
  | `template-query` / `templateQuery` | no | Stateless: the render to open on — the `templateQuery` from the last save. Empty uses the XML's `default=` values. |
277
324
  | `brand-color`, `theme` | no | See Step 9 |
@@ -291,10 +338,18 @@ template document.
291
338
  `error` codes: `auth`, `invalid-content`, `invalid-config`,
292
339
  `handshake-timeout`, `invalid-base-url`, `unknown`.
293
340
 
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.
347
+
294
348
  ## Public Methods
295
349
 
296
350
  `open(templateId?)`, `close()`, `load({ content, templateId?, name? })`,
297
- `confirmSave(ok, message?)`. Read-only: `status`, `isDirty`.
351
+ `createNew({ templateId?, name? })`, `confirmSave(ok, message?)`. Read-only:
352
+ `status`, `isDirty`.
298
353
 
299
354
  In React these are reached through a forwarded ref — **required for
300
355
  `mode="modal"`**, which renders nothing until `open()` is called:
@@ -338,7 +393,7 @@ ignored; and `layout` / `locale` / `force_format` are reserved slugs.
338
393
 
339
394
  | Symptom | Cause |
340
395
  |---|---|
341
- | `error` code `handshake-timeout`, blank frame | Host origin not in `frame-ancestors` (Step 8), or third-party cookies blocked |
396
+ | `error` code `handshake-timeout`, nothing rendered | Host origin not in the deployment's embedding allowlist (Step 8), or third-party cookies blocked |
342
397
  | `error` code `auth` | Bad/expired session credentials, a rejected security template, or cookies blocked |
343
398
  | `error` code `invalid-content` | The `content` handed over is not parseable `.fdt` XML |
344
399
  | `error` code `invalid-config` | `sec-template` without `stateless` (Step 7) |
package/CHANGELOG.md CHANGED
@@ -8,15 +8,201 @@ 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]
12
+
13
+ ## [0.4.0] - 2026-08-26
14
+
15
+ ### Added
16
+
17
+ - `stored-uuid` attribute / `storedUuid` prop (and a `storedUuid` field on
18
+ `load()`) — seeds the element's `dam-store` memory with the copy a document
19
+ already has, so re-saves after a page reload resolve to (and version) the
20
+ existing file instead of reporting a spurious `storeError` on unchanged
21
+ content or starting a fresh file. Per-document: `load()` without it clears
22
+ the seed, `createNew()` always does.
23
+ - `flushPendingSaves()` public method — emits any `dam-store` saves still
24
+ waiting on their upload, raw content with `storeError` in place of the
25
+ links. For framework wrappers that unsubscribe their listeners before the
26
+ element leaves the DOM; the React wrapper calls it from its cleanup, so a
27
+ host unmounting `<TemplateBuilder>` mid-upload no longer loses the save.
28
+
29
+ ### Changed
30
+
31
+ - Closing the editor no longer flushes a `dam-store` upload still in flight
32
+ into a `storeError`: the element outlives a close (inline keeps rendering,
33
+ modal drops only its overlay), so the save emits its real outcome when the
34
+ copy lands — possibly after the `close` event. Element removal still
35
+ flushes immediately.
36
+ - `dam-store` filenames: the element reuses its known copy's own filename so
37
+ versioning-in-place keeps matching; otherwise the name carries a
38
+ deterministic hash of the host's template id — the same document lands on
39
+ the same DAM file across page reloads (no fork per session, no stored-uuid
40
+ seed required), while two documents sharing a display name ("Untitled")
41
+ never collide. Only an id-less document falls back to a per-page unique
42
+ suffix.
43
+ - `dam-store` document memory is now epoch-based and validated at read time:
44
+ the document epoch advances only when a genuinely different document ships,
45
+ never on iframe changes — a modal close or theme swap mid-save no longer
46
+ forgets the stored copy, a host echo followed by an in-place remount no
47
+ longer orphans it, and a late-landing upload is recorded under its own
48
+ document's epoch so it can never masquerade as another document's copy.
49
+ - `dam-store` no longer looks up opaque (non-uuid-shaped) template ids
50
+ against the files API — one round trip saved per save, and the store no
51
+ longer depends on how the API answers a malformed id. The transient-failure
52
+ rule is role-aware: the required folder anchor still fails the copy, but a
53
+ transient failure of the optional freshness lookup no longer fails a save
54
+ the other anchor can carry.
55
+ - An unchanged-content conflict with no anchor in hand (first save after a
56
+ reload without the stored-uuid seed) now resolves itself by finding the
57
+ document's own earlier copy by its deterministic filename; a genuinely
58
+ foreign duplicate reports an actionable error naming `stored-uuid` as the
59
+ fix.
60
+ - Removing the element flushes pending saves one task later, skipped if the
61
+ element is reconnected — reparenting the mounted widget (e.g. a fullscreen
62
+ toggle) no longer reports a false `storeError` for an upload that lands
63
+ fine.
64
+ - Demo page: the Filerobot API helpers (key exchange, scope headers, file
65
+ lookup) are imported from `dam-store.ts` instead of being hand copies.
66
+
67
+ ### Fixed
68
+
69
+ - A host echoing the full save detail back into the props (content + name +
70
+ `templateQuery` — the documented controlled pattern) no longer triggers a
71
+ HOST_LOAD that reloads the editor and wipes undo history: the element now
72
+ also accepts the save's own identity, whose query nearly always differs
73
+ from the one the template was loaded with.
74
+ - A transient Filerobot failure (5xx, 429, auth, network) during the
75
+ folder lookup no longer reads as "file does not exist" — which silently
76
+ re-homed an existing template into the fallback folder as a brand-new
77
+ file, or failed an unchanged re-save with a spurious `storeError`. Only a
78
+ clean 404 means missing; everything else fails the copy explicitly, and
79
+ the post-upload record read-back stays best-effort.
80
+ - `dam-store` uploads now declare the template MIME type
81
+ (`text/xml+sfxtemplate`) the app's own uploads use, instead of plain
82
+ `text/xml`.
83
+ - A queued `dam-store` upload captures `store-folder` and the credentials at
84
+ save time — an in-place document swap while a save is still queued no
85
+ longer re-targets that save's upload to the new document's folder or
86
+ tenant.
87
+ - A `dam-store` upload finishing after the host swapped documents in place
88
+ (new `content`/`templateId` on the mounted element) no longer records the
89
+ old document's uuid into the new document's session memory — a follow-up
90
+ save can no longer version the wrong DAM file.
91
+ - React wrapper: event listeners are now attached once per element and read
92
+ the latest callbacks at event time, instead of being re-bound on every
93
+ handler identity change. The unmount-time flush is guarded, so a page
94
+ whose older CDN bundle registered the element first degrades silently
95
+ instead of throwing from the effect cleanup.
96
+ - Demo page: the save handler now detects a `dam-store` save from the event
97
+ detail itself (`stored`/`storeError` present) rather than the live
98
+ `damStore` property, which a "Load template" inside the upload window can
99
+ rewrite before the serialized save event arrives.
100
+ - Demo sample template: the `$headline$` placeholder now renders — the
101
+ variable holds its value in the `default=` attribute as
102
+ `type="text_placeholder"`, the shape the app's parser actually reads
103
+ (CDATA text on a `type="text"` variable is ignored, leaving the layer
104
+ blank).
105
+
106
+ - `dam-store` attribute / `damStore` prop (stateless only) — the element
107
+ stores each save in Filerobot itself, with the same multipart upload the
108
+ DAM-backed editor makes, before emitting `save`. The detail then carries
109
+ `stored: { uuid, url }` (the DAM copy's uuid and its CDN URL with a fresh
110
+ `?vh=`) next to the raw `content` — append the `templateQuery` to `url` and
111
+ it is a render. A failed copy emits `storeError` instead; the raw data
112
+ arrives either way, and the host decides via the save ack whether that
113
+ fails the save. `store-folder` / `storeFolder` names the folder for
114
+ templates whose id matches no existing DAM file (an existing file's own
115
+ folder always wins, so same-name saves version in place). The stored
116
+ `template_query` metadata has custom-metadata-bound values stripped —
117
+ record-agnostic, like the editor's own stores. Rapid saves are serialized
118
+ (events and acks keep post order); element removal mid-upload flushes the
119
+ pending save immediately — raw content with `storeError`, never a dropped
120
+ save (a mere close lets the upload finish and the save emit its real
121
+ outcome); and the element remembers the copy it last made, so unchanged
122
+ re-saves resolve to it instead of erroring even when the host's own
123
+ template id is opaque.
124
+ - Demo page: the store checkbox now drives `dam-store` (on by default) — the
125
+ page writes no upload code; it reads `stored` off the save event, logs the
126
+ links, and the Template id / .fdt URL fields follow the stored file so the
127
+ preview always renders what was just saved. A failed copy fails the save
128
+ (`confirmSave(false)`).
129
+ - Demo page: a **Show preview** button under the template-query field. It
130
+ renders the stored `.fdt` with the query applied and the host's
131
+ custom-metadata values substituted — the same image URL a host page would
132
+ embed. The file's CDN URL is filled in by a library pick, resolved from the
133
+ Template id via the API when the field is left empty (dam-store saves also
134
+ fill it), or pasted by hand. Renders the stored file, not unsaved local
135
+ edits.
136
+
137
+ - `custom-metadata-label` attribute / `customMetadataLabel` prop — renames the
138
+ **Custom metadata** value source in the editor's UI (source dropdowns and the
139
+ properties-panel section), e.g. to `External metadata`. Wording only: the
140
+ saved template carries the same `custom_ckey` binding either way, and an
141
+ empty value keeps the editor's default label. Requires an app deployment
142
+ that understands `customMetadataLabel` in `HOST_CONFIG`; an older app
143
+ ignores it.
144
+
145
+ ## [0.3.0] - 2026-08-18
146
+
147
+ ### Added
148
+
149
+ - `custom-metadata` attribute / `customMetadata` prop — a metadata model
150
+ (`[{ key, title?, group? }]`) offered in the editor as a third **Custom
151
+ metadata** value source, so authors bind a text variable to one of your field
152
+ names instead of to a bare slug. Names only: no values travel with the model
153
+ and the editor resolves nothing against it. The binding is saved on the
154
+ variable as `custom_ckey` while the variable stays `source="URL"`, so the
155
+ render pipeline is unchanged — you substitute the value with `$slug=value` as
156
+ you would for free text, and read `custom_ckey` back from the stored `.fdt` to
157
+ know which field each variable wants. Set no model and the source is not
158
+ offered. Its main use is `sec-template` / stateless embeds, where the Hub
159
+ project model — and with it the *File metadata* source — is unavailable.
160
+ Fields the editor cannot use are dropped rather than breaking the source: no
161
+ `key`, a duplicate `key`, or a `key` holding a character XML cannot represent
162
+ (a C0 control, an unpaired surrogate) — the last of which would otherwise make
163
+ the saved `.fdt` unparseable in its entirety.
164
+ - The demo page plays the host half of it: a switch, an editable JSON model to
165
+ start from, and an optional `value` per field that the page substitutes into
166
+ the render query for any variable bound to that field — on load and on save,
167
+ with each substitution logged. Values never reach the editor.
168
+ - `HOST_CONFIG` message and `CustomMetadataField` / `HostConfigData` protocol
169
+ types; `PROTOCOL_VERSION` is now 3. Additive: an app deployment that predates
170
+ it ignores the message, and a host that sends no model gets the previous
171
+ behaviour exactly. The widget delivers config on the editor's ready signal
172
+ (so it reaches DAM-backed embeds, which exchange no content messages) and
173
+ whenever the model changes.
174
+
175
+ ## [0.2.0] - 2026-08-11
176
+
177
+ ### Added
178
+
179
+ - `new-template` attribute / `newTemplate` prop and `createNew({ templateId?,
180
+ name? })` — start a template from scratch in stateless mode. The widget
181
+ supplies the empty document (`BLANK_TEMPLATE_XML`, exported from
182
+ `./protocol`), so a host with nothing stored yet needs no knowledge of the
183
+ `.fdt` format: the editor opens on its empty state, the user adds the first
184
+ layout, and the first `save` hands back a complete document to store.
185
+ `content` wins when both are set. Empty `content` still means "the host is
186
+ still fetching" and keeps the editor waiting — only the flag turns that into
187
+ a blank document.
188
+
189
+ ### Changed
190
+
191
+ - The demo's Base URL defaults to `https://design-templates.scaleflex.com`
192
+ instead of `http://localhost:3000`, so the published page is usable without
193
+ filling a field first. It also logs the full `.fdt` and `templateQuery` to the
194
+ console at both crossings — going into the editor, and coming back on save —
195
+ while the on-page panel keeps summarizing.
196
+
11
197
  ## [0.1.1] - 2026-08-10
12
198
 
13
199
  First published release. `<sfx-template-builder>` custom element, `./react`
14
- wrapper, `./define` registration, and the v2 postMessage protocol (DAM-backed
15
- and stateless modes). 0.1.0 was never published.
200
+ wrapper, `./define` registration, and the v2 host protocol (DAM-backed and
201
+ stateless modes). 0.1.0 was never published.
16
202
 
17
203
  ### Added
18
204
 
19
- - `sec-template` attribute — authenticate with a Filerobot security template
205
+ - `sec-template` attribute — authenticate with a Scaleflex security template
20
206
  instead of a Hub session. The app exchanges the key for an access key itself
21
207
  and renews it on expiry. Stateless mode only, and Hub-project features
22
208
  (metadata fields, regional variants, project branding) come back empty; see
@@ -36,7 +222,7 @@ and stateless modes). 0.1.0 was never published.
36
222
  parse, rather than leaving the host on a loading state indefinitely.
37
223
 
38
224
  - Buildable demo (`build:demo` → `demo-dist/`) and a `release` script wrapping
39
- the shared Filerobot CDN release pipeline. The demo fetches its template over
225
+ the shared Scaleflex CDN release pipeline. The demo fetches its template over
40
226
  HTTP from a bundled sample, so the "XML comes from your API, goes in, comes
41
227
  back out" round trip is visible end to end with no backend.
42
228
  - `LICENSE` (proprietary), shipped in the npm tarball.