@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.
- package/.claude/skills/integrate-template-builder/SKILL.md +79 -24
- package/CHANGELOG.md +190 -4
- package/README.md +452 -66
- package/dist/dam-store.d.ts +92 -0
- package/dist/define.cjs +1 -1
- package/dist/define.js +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +16 -14
- package/dist/protocol.d.ts +84 -2
- package/dist/react.cjs +1 -1
- package/dist/react.cjs.map +1 -1
- package/dist/react.d.ts +41 -1
- package/dist/react.js +45 -29
- package/dist/react.js.map +1 -1
- package/dist/template-builder-B9Cwo_Q-.js +651 -0
- package/dist/template-builder-B9Cwo_Q-.js.map +1 -0
- package/dist/template-builder-Byqg1q93.cjs +53 -0
- package/dist/template-builder-Byqg1q93.cjs.map +1 -0
- package/dist/template-builder.d.ts +201 -6
- package/package.json +3 -1
- package/src/dam-store.ts +388 -0
- package/src/index.ts +2 -0
- package/src/protocol.ts +92 -2
- package/src/react.ts +120 -27
- package/src/template-builder.ts +467 -11
- package/dist/template-builder-CSyPZni9.cjs +0 -52
- package/dist/template-builder-CSyPZni9.cjs.map +0 -1
- package/dist/template-builder-S33H_d5T.js +0 -354
- package/dist/template-builder-S33H_d5T.js.map +0 -1
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">
|
|
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
|
|
|
@@ -30,13 +30,17 @@
|
|
|
30
30
|
- [Quick Start](#quick-start)
|
|
31
31
|
- [Vanilla JS / Web Component](#vanilla-js--web-component)
|
|
32
32
|
- [React](#react)
|
|
33
|
+
- [Hub session (internal)](#hub-session-internal)
|
|
33
34
|
- [Modes](#modes)
|
|
34
35
|
- [DAM-backed](#dam-backed-default)
|
|
35
36
|
- [Stateless](#stateless)
|
|
37
|
+
- [Storing a rendering copy (dam-store)](#storing-a-rendering-copy-dam-store)
|
|
38
|
+
- [Starting a template from scratch](#starting-a-template-from-scratch)
|
|
36
39
|
- [Reporting a failed save](#reporting-a-failed-save)
|
|
37
40
|
- [Configuration](#configuration)
|
|
38
41
|
- [Attributes & properties](#attributes--properties)
|
|
39
42
|
- [Authentication](#authentication)
|
|
43
|
+
- [Custom metadata fields](#custom-metadata-fields)
|
|
40
44
|
- [Origin registration](#origin-registration)
|
|
41
45
|
- [Cookies](#cookies)
|
|
42
46
|
- [Public Methods](#public-methods)
|
|
@@ -56,11 +60,11 @@
|
|
|
56
60
|
|
|
57
61
|
## Overview
|
|
58
62
|
|
|
59
|
-
`<sfx-template-builder>` embeds the
|
|
60
|
-
on any origin. The element
|
|
61
|
-
deployment, hands over the credentials, and
|
|
62
|
-
|
|
63
|
-
|
|
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.
|
|
64
68
|
|
|
65
69
|
All the heavy lifting — server-side text rendering, font resolution, asset
|
|
66
70
|
browsing, template XML — stays inside the app deployment. The widget is a
|
|
@@ -70,30 +74,33 @@ protocol adapter.
|
|
|
70
74
|
|
|
71
75
|
- **Framework-agnostic** — a custom element; works in vanilla JS, React, Vue,
|
|
72
76
|
Angular, Svelte. A thin React wrapper ships in the box.
|
|
73
|
-
- **Two storage modes** — let
|
|
77
|
+
- **Two storage modes** — let Scaleflex own the template, or keep the document
|
|
74
78
|
entirely on your side ([Stateless](#stateless)) and never map your users onto
|
|
75
79
|
Scaleflex identities.
|
|
76
|
-
- **Two ways in** — a Hub session, or a
|
|
80
|
+
- **Two ways in** — a Hub session, or a Scaleflex [security
|
|
77
81
|
template](#security-template-guest-auth) when you have no Hub account to hand
|
|
78
82
|
over per user.
|
|
79
83
|
- **Inline or modal** — fill a box in your layout, or cover the viewport.
|
|
84
|
+
- **New templates without the format** — [`new-template`](#starting-a-template-from-scratch)
|
|
85
|
+
starts an empty document for the user to build; you only ever store what
|
|
86
|
+
comes back.
|
|
80
87
|
- **Themeable** — one [brand colour](#brand-color) drives the editor's whole
|
|
81
88
|
accent ramp; light, dark, or follow the OS.
|
|
82
89
|
- **Origin-checked both ways** — the widget only accepts messages from the app
|
|
83
90
|
origin; the app only accepts a template from the origin pinned when the
|
|
84
91
|
session was handed over.
|
|
85
92
|
- **Diagnosable failures** — auth, blocked cookies, bad content and handshake
|
|
86
|
-
timeouts all surface as an `error` event instead of a stuck
|
|
93
|
+
timeouts all surface as an `error` event instead of a stuck editor.
|
|
87
94
|
|
|
88
95
|
## Requirements
|
|
89
96
|
|
|
90
|
-
- A [
|
|
97
|
+
- A [Scaleflex](https://www.scaleflex.com/page/digital-asset-management) account
|
|
91
98
|
and a running `design-templates-app` deployment to point `base-url` at
|
|
92
99
|
- Credentials minted server-side — either a Hub session (`session-uuid` +
|
|
93
|
-
**SASS key** + **
|
|
100
|
+
**SASS key** + **Scaleflex token**) or a **security template** + token for
|
|
94
101
|
guest access; see [Authentication](#authentication)
|
|
95
|
-
- Your page's origin registered in the deployment's
|
|
96
|
-
|
|
102
|
+
- Your page's origin registered in the deployment's embedding allowlist — see
|
|
103
|
+
[Origin registration](#origin-registration)
|
|
97
104
|
- Modern browser with Custom Elements v1 support (see
|
|
98
105
|
[Browser Support](#browser-support))
|
|
99
106
|
|
|
@@ -108,7 +115,7 @@ npm i @scaleflex/template-builder
|
|
|
108
115
|
### CDN
|
|
109
116
|
|
|
110
117
|
```html
|
|
111
|
-
<script type="module" src="https://cdn.
|
|
118
|
+
<script type="module" src="https://cdn.cloudimage.io/template-builder/0.4.0/template-builder.min.js"></script>
|
|
112
119
|
```
|
|
113
120
|
|
|
114
121
|
The CDN bundle is self-registering — it defines `<sfx-template-builder>` on
|
|
@@ -134,6 +141,15 @@ load, with Lit bundled in. Pin the major version.
|
|
|
134
141
|
|
|
135
142
|
## Quick Start
|
|
136
143
|
|
|
144
|
+
You need two things from your Scaleflex project: its **token**, and a
|
|
145
|
+
**security template** key — a named, permission-scoped credential you define
|
|
146
|
+
once, the same guest-auth mechanism the other Scaleflex widgets use. No Hub
|
|
147
|
+
account, and no user of yours ever needs a Scaleflex identity. See
|
|
148
|
+
[Authentication](#authentication) for how to scope one.
|
|
149
|
+
|
|
150
|
+
The template document stays on your side: you hand the widget its XML, and the
|
|
151
|
+
edit comes back to you on save. That is [stateless](#stateless) mode.
|
|
152
|
+
|
|
137
153
|
### Vanilla JS / Web Component
|
|
138
154
|
|
|
139
155
|
```html
|
|
@@ -143,16 +159,26 @@ load, with Lit bundled in. Pin the major version.
|
|
|
143
159
|
|
|
144
160
|
<sfx-template-builder
|
|
145
161
|
base-url="https://<your-design-templates-deployment>"
|
|
146
|
-
token="
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
template-id="TEMPLATE_UUID"
|
|
162
|
+
token="PROJECT_TOKEN"
|
|
163
|
+
sec-template="SEC_TEMPLATE_KEY"
|
|
164
|
+
stateless
|
|
150
165
|
style="display:block;height:800px"
|
|
151
166
|
></sfx-template-builder>
|
|
152
167
|
|
|
153
168
|
<script>
|
|
154
169
|
const builder = document.querySelector('sfx-template-builder')
|
|
155
|
-
|
|
170
|
+
|
|
171
|
+
// 1 — hand it the template to edit
|
|
172
|
+
const { content, name } = await fetch(`/api/templates/${id}`).then((r) => r.json())
|
|
173
|
+
builder.load({ templateId: id, name, content })
|
|
174
|
+
|
|
175
|
+
// 2 — take the edit back and store it
|
|
176
|
+
builder.addEventListener('save', async (e) => {
|
|
177
|
+
const { templateId, content, name, templateQuery } = e.detail
|
|
178
|
+
const ok = await saveToYourApi(templateId, { content, name, templateQuery })
|
|
179
|
+
builder.confirmSave(ok) // false → the editor keeps its unsaved-changes warning
|
|
180
|
+
})
|
|
181
|
+
|
|
156
182
|
builder.addEventListener('error', (e) => console.error(e.detail)) // { code, message }
|
|
157
183
|
</script>
|
|
158
184
|
```
|
|
@@ -164,6 +190,38 @@ Size the element yourself — in `inline` mode it fills the box you give it.
|
|
|
164
190
|
```tsx
|
|
165
191
|
import { TemplateBuilder } from '@scaleflex/template-builder/react'
|
|
166
192
|
|
|
193
|
+
<TemplateBuilder
|
|
194
|
+
stateless
|
|
195
|
+
baseUrl="https://<deployment>"
|
|
196
|
+
token={projectToken}
|
|
197
|
+
secTemplate={secTemplateKey}
|
|
198
|
+
templateId={id}
|
|
199
|
+
name={name}
|
|
200
|
+
content={xml}
|
|
201
|
+
style={{ height: 800 }}
|
|
202
|
+
onSave={async (data) => (await saveToYourApi(data)).ok}
|
|
203
|
+
/>
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
<!-- internal:start -->
|
|
207
|
+
### Hub session (internal)
|
|
208
|
+
|
|
209
|
+
Scaleflex-side embeds inside the Hub authenticate with a session instead of a
|
|
210
|
+
security template, which unlocks DAM-backed storage and Hub-project features.
|
|
211
|
+
Mint the session server-side; never put a long-lived credential in client code.
|
|
212
|
+
|
|
213
|
+
```html
|
|
214
|
+
<sfx-template-builder
|
|
215
|
+
base-url="https://<your-design-templates-deployment>"
|
|
216
|
+
token="PROJECT_TOKEN"
|
|
217
|
+
sass-key="SASS_KEY"
|
|
218
|
+
session-uuid="SESSION_UUID"
|
|
219
|
+
template-id="TEMPLATE_UUID"
|
|
220
|
+
style="display:block;height:800px"
|
|
221
|
+
></sfx-template-builder>
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
```tsx
|
|
167
225
|
<TemplateBuilder
|
|
168
226
|
baseUrl="https://<deployment>"
|
|
169
227
|
token={token}
|
|
@@ -174,6 +232,7 @@ import { TemplateBuilder } from '@scaleflex/template-builder/react'
|
|
|
174
232
|
onSave={(data) => console.log(data)}
|
|
175
233
|
/>
|
|
176
234
|
```
|
|
235
|
+
<!-- internal:end -->
|
|
177
236
|
|
|
178
237
|
---
|
|
179
238
|
|
|
@@ -181,7 +240,7 @@ import { TemplateBuilder } from '@scaleflex/template-builder/react'
|
|
|
181
240
|
|
|
182
241
|
### DAM-backed (default)
|
|
183
242
|
|
|
184
|
-
`template-id` is a
|
|
243
|
+
`template-id` is a DAM file uuid. The app loads the template itself, and
|
|
185
244
|
Save uploads a new version and reports the resulting uuid on the `save` event.
|
|
186
245
|
Leave `template-id` empty to open the new-template flow.
|
|
187
246
|
|
|
@@ -189,7 +248,9 @@ Leave `template-id` empty to open the new-template flow.
|
|
|
189
248
|
|
|
190
249
|
Set `stateless` to keep the document entirely on your side: you pass the
|
|
191
250
|
template in, the user edits it, and you get the edited template back. Nothing is
|
|
192
|
-
stored on the Scaleflex side
|
|
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,
|
|
193
254
|
tenancy and access control, and `template-id` becomes an opaque string that is
|
|
194
255
|
echoed back untouched.
|
|
195
256
|
|
|
@@ -203,9 +264,8 @@ point.
|
|
|
203
264
|
<sfx-template-builder
|
|
204
265
|
stateless
|
|
205
266
|
base-url="https://<deployment>"
|
|
206
|
-
token="
|
|
207
|
-
|
|
208
|
-
session-uuid="SESSION_UUID"
|
|
267
|
+
token="PROJECT_TOKEN"
|
|
268
|
+
sec-template="SEC_TEMPLATE_KEY"
|
|
209
269
|
style="display:block;height:800px"
|
|
210
270
|
></sfx-template-builder>
|
|
211
271
|
|
|
@@ -218,8 +278,8 @@ point.
|
|
|
218
278
|
const { content, name, templateQuery } = await res.json()
|
|
219
279
|
|
|
220
280
|
// 2 — pass it in. `content` is a property, never an attribute: templates
|
|
221
|
-
// routinely exceed what fits in markup or a URL,
|
|
222
|
-
//
|
|
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.
|
|
223
283
|
// `templateQuery` is what you stored on the last save; it reopens the
|
|
224
284
|
// template on the same layout and variable values.
|
|
225
285
|
builder.load({ templateId: id, name, content, templateQuery })
|
|
@@ -245,7 +305,7 @@ point.
|
|
|
245
305
|
</script>
|
|
246
306
|
```
|
|
247
307
|
|
|
248
|
-
`templateId` is **your** identifier, not a
|
|
308
|
+
`templateId` is **your** identifier, not a DAM uuid. The app never
|
|
249
309
|
resolves it against anything — it is carried alongside the content and handed
|
|
250
310
|
straight back on save, so use whatever key maps to your own record.
|
|
251
311
|
|
|
@@ -270,9 +330,8 @@ function TemplateEditor({ id }: { id: string }) {
|
|
|
270
330
|
<TemplateBuilder
|
|
271
331
|
stateless
|
|
272
332
|
baseUrl="https://<deployment>"
|
|
273
|
-
token={
|
|
274
|
-
|
|
275
|
-
sessionUuid={sessionUuid}
|
|
333
|
+
token={projectToken}
|
|
334
|
+
secTemplate={secTemplateKey}
|
|
276
335
|
// 2 — pass it in.
|
|
277
336
|
templateId={id}
|
|
278
337
|
content={tpl.content}
|
|
@@ -355,9 +414,10 @@ you navigated away from.
|
|
|
355
414
|
#### What stateless does *not* remove
|
|
356
415
|
|
|
357
416
|
Statelessness applies to the **document**, not to the infrastructure. The editor
|
|
358
|
-
still needs an authenticated
|
|
417
|
+
still needs an authenticated Scaleflex tenant for:
|
|
359
418
|
|
|
360
|
-
- **text rendering** — text layers are rasterized server-side
|
|
419
|
+
- **text rendering** — text and shape layers are rasterized server-side (see
|
|
420
|
+
[the render round-trip](#why-the-editor-still-calls-a-server) below),
|
|
361
421
|
- **fonts** — custom fonts are served from the tenant's `.studio/fonts/`,
|
|
362
422
|
- **asset browsing and upload** — image layers are picked from the DAM,
|
|
363
423
|
- **metadata variables and regional settings**.
|
|
@@ -370,12 +430,198 @@ Images referenced by a template may live on your own CDN, but the render
|
|
|
370
430
|
service only fetches from allowlisted hosts — add yours to the deployment's
|
|
371
431
|
`RENDER_ALLOWED_HOSTS_EXTRA`.
|
|
372
432
|
|
|
433
|
+
#### Why the editor still calls a server
|
|
434
|
+
|
|
435
|
+
This is the reason a stateless embed still needs a credential, so it is worth
|
|
436
|
+
being concrete about.
|
|
437
|
+
|
|
438
|
+
A `.fdt` template is not an image. The image only exists once someone requests
|
|
439
|
+
the template's CDN URL, and it is Scaleflex that renders it there — server-side,
|
|
440
|
+
with ImageMagick:
|
|
441
|
+
|
|
442
|
+
```
|
|
443
|
+
https://<tenant>.filerobot.com/<path>/<template>.fdt?<templateQuery>&force_format=png
|
|
444
|
+
```
|
|
445
|
+
|
|
446
|
+
The editor's contract is that what you see while editing is what that URL will
|
|
447
|
+
return. That rules out drawing the text in the browser. Line breaking, kerning,
|
|
448
|
+
letter spacing, baseline placement, shrink-to-fit and antialiasing are FreeType
|
|
449
|
+
and ImageMagick behaviours; canvas `fillText` and DOM text go through the
|
|
450
|
+
browser's own shaping and hinting instead, so the same layer lands differently
|
|
451
|
+
in Chrome, Safari and Firefox — and differently from the export in all three. A
|
|
452
|
+
few pixels of drift is enough to move a headline off a product shot. There is
|
|
453
|
+
no JS library that reimplements that layout either: the only faithful
|
|
454
|
+
implementation of ImageMagick's text rendering is ImageMagick.
|
|
455
|
+
|
|
456
|
+
So the editor does not approximate the export — it runs the same engine.
|
|
457
|
+
Text and shape layers are rasterized by an ImageMagick 7 build hosted in your
|
|
458
|
+
`base-url` deployment (the export pipeline drives ImageMagick 7 from PHP; the
|
|
459
|
+
editor drives a WebAssembly build of it), and each layer comes back as a
|
|
460
|
+
transparent PNG that the canvas positions with CSS:
|
|
461
|
+
|
|
462
|
+
```
|
|
463
|
+
browser — the widget app deployment (base-url)
|
|
464
|
+
──────────────────── ─────────────────────────
|
|
465
|
+
edit a text layer
|
|
466
|
+
│ batched across layers, debounced
|
|
467
|
+
├────── POST /api/render-layers ─────▶ ImageMagick (WASM)
|
|
468
|
+
│ layers + fonts + variables ├─ resolve fonts: bundled,
|
|
469
|
+
│ │ then tenant /.studio/fonts
|
|
470
|
+
│ ├─ draw at 3×, downscale
|
|
471
|
+
◀────── transparent PNG per layer ─────┘
|
|
472
|
+
│
|
|
473
|
+
└─ position / rotate / fade with CSS — no round trip
|
|
474
|
+
```
|
|
475
|
+
|
|
476
|
+
Consequences you can observe from the outside:
|
|
477
|
+
|
|
478
|
+
- **Content edits cost a round trip; placement edits do not.** Text, font,
|
|
479
|
+
weight, colour, alignment, letter spacing and box size re-render. Dragging,
|
|
480
|
+
rotating and opacity are CSS transforms on the PNG already in the page, so
|
|
481
|
+
they stay at pointer speed.
|
|
482
|
+
- **Bursts collapse.** Requests are debounced (~300 ms) and batched across
|
|
483
|
+
layers, and an in-flight batch is aborted when you keep typing — so a
|
|
484
|
+
sentence typed at speed costs one render, not one per keystroke.
|
|
485
|
+
- **Layers are drawn at 3× and downscaled**, so preview antialiasing matches
|
|
486
|
+
the export rather than the browser's rasterizer.
|
|
487
|
+
- **Fonts are resolved server-side** against the tenant's `/.studio/fonts`
|
|
488
|
+
folder and cached there, so the page never downloads a rendering engine or a
|
|
489
|
+
font binary per weight. Image layers are the exception — they are plain
|
|
490
|
+
`<img>` elements, drawn by the browser.
|
|
491
|
+
|
|
492
|
+
**That endpoint is authenticated, and it has to be.** It fetches fonts and
|
|
493
|
+
images by URL on the server's behalf, so it is not open to anonymous callers:
|
|
494
|
+
`POST /api/render-layers` requires the credential the widget handed over, and
|
|
495
|
+
without a valid one it answers `401` and text and shape layers simply never
|
|
496
|
+
appear. The same credential authorizes the font list/upload calls and the asset
|
|
497
|
+
picker.
|
|
498
|
+
|
|
499
|
+
In DAM-backed mode a Hub session covers that. A stateless embed has no Hub
|
|
500
|
+
session to hand over — and that is exactly the gap a
|
|
501
|
+
[security template](#security-template-guest-auth) fills: a permission-scoped,
|
|
502
|
+
project-level guest credential that authorizes rendering, fonts and asset
|
|
503
|
+
browsing without authenticating any particular user, and without your users
|
|
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`.
|
|
562
|
+
|
|
563
|
+
### Starting a template from scratch
|
|
564
|
+
|
|
565
|
+
A template your user has not created yet has no XML to pass in, and you should
|
|
566
|
+
not have to author one. In [stateless](#stateless) mode, set `new-template`
|
|
567
|
+
instead of `content` and the widget supplies the empty document itself.
|
|
568
|
+
(DAM-backed mode has its own new-template flow — leave `template-id` empty.)
|
|
569
|
+
|
|
570
|
+
```html
|
|
571
|
+
<sfx-template-builder
|
|
572
|
+
stateless
|
|
573
|
+
new-template
|
|
574
|
+
template-name="Untitled"
|
|
575
|
+
base-url="https://<deployment>"
|
|
576
|
+
token="PROJECT_TOKEN"
|
|
577
|
+
sec-template="SEC_TEMPLATE_KEY"
|
|
578
|
+
style="display:block;height:800px"
|
|
579
|
+
></sfx-template-builder>
|
|
580
|
+
```
|
|
581
|
+
|
|
582
|
+
```js
|
|
583
|
+
// Or imperatively, on an element that is already showing something else.
|
|
584
|
+
builder.createNew({ templateId: 'your-own-id-43', name: 'Untitled' })
|
|
585
|
+
```
|
|
586
|
+
|
|
587
|
+
```tsx
|
|
588
|
+
<TemplateBuilder stateless newTemplate templateName="Untitled" … />
|
|
589
|
+
```
|
|
590
|
+
|
|
591
|
+
The editor opens on its empty state — *"No layouts yet. Click + Add to create
|
|
592
|
+
one."* — and the user picks the canvas size, background and preset there. Save
|
|
593
|
+
is refused until at least one layout exists, so the first `save` you receive
|
|
594
|
+
already carries a complete, well-formed `.fdt` document; store it as `content`
|
|
595
|
+
and every later open is the ordinary [load flow](#the-template-comes-from-your-api).
|
|
596
|
+
|
|
597
|
+
- **`templateId` is optional.** Pass one if your record already exists and you
|
|
598
|
+
want it echoed back; otherwise the `save` payload simply arrives without an
|
|
599
|
+
id and you allocate one when you store it.
|
|
600
|
+
- **`templateQuery` stays empty.** A new document has no layouts and no
|
|
601
|
+
variables, so there is no render for a query to select. You get one back on
|
|
602
|
+
the first save — persist it then.
|
|
603
|
+
- **`content` wins when both are set,** so a host that renders one element for
|
|
604
|
+
both cases can simply pass the XML when it has one.
|
|
605
|
+
- **Empty `content` on its own does not start a blank template.** It means "the
|
|
606
|
+
host has nothing yet" — the editor keeps waiting, which is what lets you
|
|
607
|
+
mount the builder while your fetch is still in flight. Only `new-template`
|
|
608
|
+
turns that wait into a document.
|
|
609
|
+
|
|
610
|
+
Calling `createNew()` again while the blank template is already open does
|
|
611
|
+
nothing: resending would discard whatever the user has built since. Close and
|
|
612
|
+
reopen the editor to genuinely start over.
|
|
613
|
+
|
|
614
|
+
If you would rather ship your own starting point — a house style, a standard
|
|
615
|
+
canvas size, a locked logo layer — pass it as ordinary `content`. A starter
|
|
616
|
+
template is just a template, and `BLANK_TEMPLATE_XML` is exported from
|
|
617
|
+
`@scaleflex/template-builder/protocol` if you want the empty document as a base.
|
|
618
|
+
|
|
373
619
|
### Reporting a failed save
|
|
374
620
|
|
|
375
|
-
The editor clears its unsaved-changes state as soon as it
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
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:
|
|
379
625
|
|
|
380
626
|
```js
|
|
381
627
|
builder.addEventListener('save', async (e) => {
|
|
@@ -404,17 +650,23 @@ without it a failed write is invisible to the user.
|
|
|
404
650
|
| Attribute / property | Required | Description |
|
|
405
651
|
| --- | --- | --- |
|
|
406
652
|
| `base-url` / `baseUrl` | yes | Origin of the design-templates-app deployment |
|
|
407
|
-
| `token` | yes |
|
|
653
|
+
| `token` | yes | Scaleflex token (`ftoken`) |
|
|
408
654
|
| `sass-key` / `sassKey` | session auth | Project sass key |
|
|
409
655
|
| `session-uuid` / `sessionUuid` | session auth | Hub session uuid |
|
|
410
|
-
| `sec-template` / `secTemplate` | guest auth |
|
|
656
|
+
| `sec-template` / `secTemplate` | guest auth | Scaleflex security-template key, *instead of* `sass-key` + `session-uuid`. Stateless only — see [Authentication](#authentication) |
|
|
411
657
|
| `company-uuid`, `project-uuid` | no | Company / project scoping (session auth only) |
|
|
412
|
-
| `template-id` / `templateId` | no | DAM-backed:
|
|
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` |
|
|
413
659
|
| `mode` | no | `inline` (default; size the element) or `modal` (fullscreen overlay, starts closed — call `open()`) |
|
|
414
|
-
| `stateless` | no | Pass the template in and take it back out instead of using the DAM (see [Stateless](#stateless)). Requires `content` |
|
|
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` |
|
|
415
661
|
| `content` (property only) | stateless | The template to edit, as `.fdt` XML. Assigning a new value loads it into a running editor |
|
|
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) |
|
|
416
663
|
| `template-name` / `templateName` | no | Stateless: header title |
|
|
417
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 |
|
|
418
670
|
| `brand-color` / `brandColor` | no | Accent colour for the editor chrome, `#rgb` / `#rrggbb` |
|
|
419
671
|
| `theme` | no | `light`, `dark` or `auto` |
|
|
420
672
|
| `ready-timeout` / `readyTimeout` | no | Ms to wait for the app handshake before `error` (default 20000, 0 disables) |
|
|
@@ -432,18 +684,27 @@ something to hardcode in a public bundle.
|
|
|
432
684
|
| Metadata fields, regional variants, project branding | yes | **empty** |
|
|
433
685
|
| Rendering, fonts, asset picker | yes | yes, within the template's scope |
|
|
434
686
|
|
|
687
|
+
<!-- internal:start -->
|
|
435
688
|
#### Hub session
|
|
436
689
|
|
|
437
690
|
Mint the Hub session **server-side** and inject `session-uuid` / `sass-key` /
|
|
438
691
|
`token` into your page. Issue short-lived per-user sessions; never embed a
|
|
439
692
|
long-lived master credential in client-side code.
|
|
693
|
+
<!-- internal:end -->
|
|
440
694
|
|
|
441
695
|
#### Security template (guest auth)
|
|
442
696
|
|
|
443
|
-
A
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
all
|
|
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
|
|
700
|
+
all.
|
|
701
|
+
|
|
702
|
+
It is what makes a stateless embed work without Hub accounts. Even when the
|
|
703
|
+
document never leaves your side, the editor rasterizes every text and shape
|
|
704
|
+
layer on the server to stay pixel-identical to the CDN render, and resolves
|
|
705
|
+
fonts and assets from your tenant — all of it authenticated. See
|
|
706
|
+
[why the editor still calls a server](#why-the-editor-still-calls-a-server) for
|
|
707
|
+
what those calls are.
|
|
447
708
|
|
|
448
709
|
```html
|
|
449
710
|
<sfx-template-builder
|
|
@@ -469,30 +730,108 @@ all:
|
|
|
469
730
|
The app exchanges the key for a short-lived access key itself and renews it when
|
|
470
731
|
it expires, so the embed does not die mid-session.
|
|
471
732
|
|
|
733
|
+
**Scoping the template.** Grant it `LIST` on the folders you want browsable,
|
|
734
|
+
plus `LIST` + `UPLOAD` on `/.studio/fonts*` if users are to see or add custom
|
|
735
|
+
fonts — anything the template cannot reach simply isn't there. Prefer a short
|
|
736
|
+
TTL: the app re-exchanges the key when it expires, so a short-lived template
|
|
737
|
+
costs you nothing and limits the blast radius if one leaks.
|
|
738
|
+
|
|
739
|
+
If the key is rejected — revoked, wrong project token, typo — the widget emits
|
|
740
|
+
`error` with code `auth`.
|
|
741
|
+
|
|
742
|
+
<!-- internal:start -->
|
|
472
743
|
**What it costs.** A security template authenticates *nobody in particular*: no
|
|
473
744
|
user identity, no Hub project behind it. That has consequences worth knowing
|
|
474
745
|
before you pick it:
|
|
475
746
|
|
|
476
747
|
- **Stateless only.** Setting `sec-template` without `stateless` is a config
|
|
477
748
|
error — the widget reports `error` with code `invalid-config` and never mounts
|
|
478
|
-
the
|
|
749
|
+
the editor. The dashboard and the DAM-backed editor keep requiring a session.
|
|
479
750
|
- **Hub-project features come back empty** — metadata fields, regional variants
|
|
480
751
|
and dynamic fields have no model to read, and project branding does not apply
|
|
481
752
|
(theme the chrome with `brand-color` / `theme` instead).
|
|
482
|
-
|
|
483
|
-
want browsable, plus `LIST` + `UPLOAD` on `/.studio/fonts*` if users are to
|
|
484
|
-
see or add custom fonts. Anything it cannot reach simply isn't there.
|
|
485
|
-
- **Prefer short TTLs.** The app re-exchanges on expiry, so a short-lived
|
|
486
|
-
template costs you nothing but limits the blast radius of a leaked key.
|
|
753
|
+
<!-- internal:end -->
|
|
487
754
|
|
|
488
|
-
|
|
489
|
-
|
|
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.
|
|
490
822
|
|
|
491
823
|
### Origin registration
|
|
492
824
|
|
|
493
|
-
Your page's origin must be in the deployment's
|
|
494
|
-
(`NEXT_PUBLIC_TRUSTED_HUB_ORIGINS`), otherwise the browser refuses to
|
|
495
|
-
|
|
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
|
|
828
|
+
widget reports `handshake-timeout`.
|
|
829
|
+
|
|
830
|
+
A deployment allows `'self'`, `https://*.scaleflex.com`,
|
|
831
|
+
`https://*.filerobot.com` and `http://localhost:5173` (the demo's dev server)
|
|
832
|
+
out of the box, plus whatever its `NEXT_PUBLIC_TRUSTED_HUB_ORIGINS` names. Your
|
|
833
|
+
own domain has to be added there — the list is baked in at build time, so it
|
|
834
|
+
takes a rebuild of the app, not just a restart.
|
|
496
835
|
|
|
497
836
|
### Cookies
|
|
498
837
|
|
|
@@ -506,9 +845,11 @@ without CHIPS support that block third-party cookies will fail with `auth` or
|
|
|
506
845
|
|
|
507
846
|
| Method | Description |
|
|
508
847
|
| --- | --- |
|
|
509
|
-
| `open(templateId?)` | Open the editor, loading
|
|
510
|
-
| `close()` | Close the editor and unload
|
|
511
|
-
| `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. |
|
|
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). |
|
|
512
853
|
| `confirmSave(ok, message?)` | Stateless: report whether you persisted the content. See [Reporting a failed save](#reporting-a-failed-save). |
|
|
513
854
|
|
|
514
855
|
**Read-only properties:** `status` (`idle` \| `loading` \| `ready` \| `error`),
|
|
@@ -524,7 +865,7 @@ All events are `CustomEvent`s; the payload is in `detail`.
|
|
|
524
865
|
| --- | --- | --- |
|
|
525
866
|
| `ready` | — | The editor mounted and auth validated. Clears the handshake timeout. |
|
|
526
867
|
| `open` | — | The editor UI opened. |
|
|
527
|
-
| `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. |
|
|
528
869
|
| `dirtychange` | `{ isDirty }` | Stateless: the unsaved-changes flag flipped. |
|
|
529
870
|
| `close` | — | The user left the editor, or it unmounted. |
|
|
530
871
|
| `error` | `{ code, message? }` | See below. |
|
|
@@ -533,10 +874,10 @@ All events are `CustomEvent`s; the payload is in `detail`.
|
|
|
533
874
|
|
|
534
875
|
| Code | Means |
|
|
535
876
|
| --- | --- |
|
|
536
|
-
| `auth` | The app could not authenticate — blocked cookies, an expired session, or a security template the
|
|
877
|
+
| `auth` | The app could not authenticate — blocked cookies, an expired session, or a security template the Scaleflex API refused. |
|
|
537
878
|
| `invalid-content` | Stateless: the `content` you sent is not a parseable `.fdt` document. |
|
|
538
|
-
| `invalid-config` | Attributes that contradict each other, e.g. `sec-template` without `stateless`. The
|
|
539
|
-
| `handshake-timeout` | No ready signal in time — usually a missing
|
|
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. |
|
|
540
881
|
| `invalid-base-url` | `base-url` is not a URL. |
|
|
541
882
|
| `unknown` | Anything the app reported that this version does not name. |
|
|
542
883
|
|
|
@@ -544,6 +885,13 @@ All events are `CustomEvent`s; the payload is in `detail`.
|
|
|
544
885
|
`modal` mode the element tears its overlay down; in `inline` mode it is yours to
|
|
545
886
|
act on — the editor never navigates itself anywhere.
|
|
546
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
|
+
|
|
547
895
|
## React API
|
|
548
896
|
|
|
549
897
|
Props mirror the attributes in camelCase, plus `className` and `style`.
|
|
@@ -557,6 +905,10 @@ The credential props are a discriminated union, so the two
|
|
|
557
905
|
`onSave` may return `false` or a promise; see
|
|
558
906
|
[Reporting a failed save](#reporting-a-failed-save).
|
|
559
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
|
+
|
|
560
912
|
The component forwards a ref to the underlying element, which is how you reach
|
|
561
913
|
the [imperative API](#public-methods) — required for `mode="modal"`, which
|
|
562
914
|
renders nothing until `open()` is called:
|
|
@@ -636,10 +988,12 @@ blocks third-party cookies the editor cannot authenticate and the widget reports
|
|
|
636
988
|
|
|
637
989
|
---
|
|
638
990
|
|
|
991
|
+
<!-- internal:start -->
|
|
639
992
|
## Development
|
|
640
993
|
|
|
641
994
|
```bash
|
|
642
|
-
yarn dev:demo # demo site (
|
|
995
|
+
yarn dev:demo # demo site (defaults to the deployed app; point Base URL at
|
|
996
|
+
# http://localhost:3000 to drive a local one)
|
|
643
997
|
yarn test # vitest
|
|
644
998
|
yarn typecheck # tsc --noEmit
|
|
645
999
|
yarn build # dist/ — npm artifact (ESM + CJS + types)
|
|
@@ -666,7 +1020,18 @@ the README reloads the dev server.
|
|
|
666
1020
|
the folder can be zipped and handed to a client, or dropped behind any static
|
|
667
1021
|
host at any path — GitHub Pages, S3, a subdirectory of an existing site. The
|
|
668
1022
|
only thing it needs at runtime is a reachable `design-templates-app` for the
|
|
669
|
-
demo page's Base URL field to point at
|
|
1023
|
+
demo page's Base URL field to point at; it defaults to
|
|
1024
|
+
`https://design-templates.scaleflex.com`.
|
|
1025
|
+
|
|
1026
|
+
That default loads successfully only from an origin that deployment allows —
|
|
1027
|
+
`'self'`, `*.scaleflex.com`, `*.filerobot.com` and `http://localhost:5173`,
|
|
1028
|
+
which covers both the demo as published to the CDN and `yarn dev:demo`. Serving
|
|
1029
|
+
it anywhere else — another port (`yarn preview:demo` uses 4173), or a copy on
|
|
1030
|
+
your own domain — gets "refused to connect" until that origin joins the
|
|
1031
|
+
deployment's allowlist, which is baked in at build time and so needs a rebuild
|
|
1032
|
+
(see [Origin registration](#origin-registration)). Driving a *locally* running
|
|
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.
|
|
670
1035
|
|
|
671
1036
|
The demo page takes its XML from a URL, from a paste, or from a picker listing
|
|
672
1037
|
the `.fdt` files in the project its credentials point at, which fills the id,
|
|
@@ -676,6 +1041,15 @@ whatever that template's scope allows. The picker is demo scaffolding standing
|
|
|
676
1041
|
in for a host's own template store — the widget itself only ever sees the XML
|
|
677
1042
|
the page hands it.
|
|
678
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
|
+
|
|
679
1053
|
End-to-end cover for the embed boundary lives in the app repo at
|
|
680
1054
|
`e2e/embed-widget.spec.ts` — a cross-origin host page loads the built CDN
|
|
681
1055
|
bundle, pulls template XML from its own API and gets the edit back. It needs the
|
|
@@ -688,7 +1062,7 @@ yarn build && yarn start
|
|
|
688
1062
|
yarn test:e2e:embed
|
|
689
1063
|
```
|
|
690
1064
|
|
|
691
|
-
The
|
|
1065
|
+
The host↔app protocol lives in `src/protocol.ts` and is shared with the app
|
|
692
1066
|
via the `./protocol` export, so the two sides cannot drift. Message *values* are
|
|
693
1067
|
wire format: never change an existing string, only add new messages, so an older
|
|
694
1068
|
widget keeps working against a newer app deployment and vice versa.
|
|
@@ -707,11 +1081,23 @@ npm publish, git commit + tag + push. It needs a `.env.local` in this package:
|
|
|
707
1081
|
```env
|
|
708
1082
|
FILEROBOT_CDN_TOKEN=scaleflex
|
|
709
1083
|
FILEROBOT_CDN_SECU=<secu key>
|
|
710
|
-
FILEROBOT_CDN_FOLDER=/plugins/
|
|
1084
|
+
FILEROBOT_CDN_FOLDER=/plugins/cloudimage/template-builder/{version}/
|
|
711
1085
|
```
|
|
712
1086
|
|
|
713
1087
|
Update [CHANGELOG.md](./CHANGELOG.md) before releasing.
|
|
714
1088
|
|
|
1089
|
+
`yarn release:demo` publishes the demo site into `<that folder>/demo/`, next to
|
|
1090
|
+
the bundle it demonstrates. `yarn release:demo:probe` sends a single file first,
|
|
1091
|
+
reporting the content-type the CDN serves it as. The pages are built as
|
|
1092
|
+
self-contained HTML with their JS and CSS inlined, because the CDN project
|
|
1093
|
+
refuses `.js` uploads.
|
|
1094
|
+
|
|
1095
|
+
Note the CDN caches for 24h: re-uploading over a path that has already been
|
|
1096
|
+
fetched keeps serving the old copy until it expires. Version folders are the
|
|
1097
|
+
way around it — don't overwrite a published one.
|
|
1098
|
+
|
|
1099
|
+
<!-- internal:end -->
|
|
1100
|
+
|
|
715
1101
|
---
|
|
716
1102
|
|
|
717
1103
|
## Claude Code Integration
|