@scaleflex/template-builder 0.1.1 → 0.2.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 +52 -3
- package/CHANGELOG.md +22 -0
- package/README.md +258 -27
- package/dist/define.cjs +1 -1
- package/dist/define.js +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.js +14 -13
- package/dist/protocol.d.ts +24 -0
- package/dist/react.cjs +1 -1
- package/dist/react.cjs.map +1 -1
- package/dist/react.d.ts +7 -0
- package/dist/react.js +17 -16
- package/dist/react.js.map +1 -1
- package/dist/template-builder-CK2Zlo7E.cjs +53 -0
- package/dist/template-builder-CK2Zlo7E.cjs.map +1 -0
- package/dist/{template-builder-S33H_d5T.js → template-builder-De0hRO4s.js} +103 -77
- package/dist/template-builder-De0hRO4s.js.map +1 -0
- package/dist/template-builder.d.ts +37 -2
- package/package.json +3 -1
- package/src/protocol.ts +28 -0
- package/src/react.ts +9 -0
- package/src/template-builder.ts +63 -5
- package/dist/template-builder-CSyPZni9.cjs +0 -52
- package/dist/template-builder-CSyPZni9.cjs.map +0 -1
- package/dist/template-builder-S33H_d5T.js.map +0 -1
|
@@ -43,7 +43,8 @@ storage and does not want to map them onto Scaleflex tenants. Pick
|
|
|
43
43
|
|
|
44
44
|
Statelessness applies to the **document only**. The editor still needs an
|
|
45
45
|
authenticated Filerobot tenant for server-side text rendering, custom fonts,
|
|
46
|
-
asset browsing, and metadata variables
|
|
46
|
+
asset browsing, and metadata variables — so a stateless embed still needs a
|
|
47
|
+
credential (Step 7).
|
|
47
48
|
|
|
48
49
|
## Step 2 — Detect the target framework
|
|
49
50
|
|
|
@@ -144,6 +145,39 @@ import { TemplateBuilder } from '@scaleflex/template-builder/react'
|
|
|
144
145
|
/>
|
|
145
146
|
```
|
|
146
147
|
|
|
148
|
+
### Stateless — a template that does not exist yet
|
|
149
|
+
|
|
150
|
+
A host whose user is creating their first template has no XML to pass in, and
|
|
151
|
+
should not have to author any. Set `new-template` instead of `content` and the
|
|
152
|
+
widget supplies the empty document:
|
|
153
|
+
|
|
154
|
+
```html
|
|
155
|
+
<sfx-template-builder stateless new-template template-name="Untitled" …>
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
```js
|
|
159
|
+
builder.createNew({ templateId: 'your-own-id-43', name: 'Untitled' })
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
```tsx
|
|
163
|
+
<TemplateBuilder stateless newTemplate templateName="Untitled" … />
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
The editor opens on its empty state, the user adds the first layout (canvas
|
|
167
|
+
size, background, preset), and the first `save` hands back a complete `.fdt`
|
|
168
|
+
document — store that and every later open is the ordinary load flow. Save is
|
|
169
|
+
refused until a layout exists.
|
|
170
|
+
|
|
171
|
+
- `templateId` is optional; without one the `save` payload just arrives without
|
|
172
|
+
an id, and the host allocates one when storing.
|
|
173
|
+
- `templateQuery` stays empty — a new document has no layouts or variables for
|
|
174
|
+
a query to select. It comes back on the first save.
|
|
175
|
+
- `content` wins when both are set, so one element can serve both cases.
|
|
176
|
+
- Empty `content` alone does **not** start a blank template: it means the host
|
|
177
|
+
is still fetching, and the editor keeps waiting. Only the flag changes that.
|
|
178
|
+
- For a house-style starting point (standard canvas, locked logo layer), pass
|
|
179
|
+
it as ordinary `content` — a starter template is just a template.
|
|
180
|
+
|
|
147
181
|
### DAM-backed
|
|
148
182
|
|
|
149
183
|
`template-id` is a Filerobot file uuid; omit it to open the new-template flow.
|
|
@@ -232,6 +266,19 @@ What it costs:
|
|
|
232
266
|
In stateless mode a single service tenant is usually right either way: one
|
|
233
267
|
machine credential, with all per-user permission logic staying in the host app.
|
|
234
268
|
|
|
269
|
+
**If asked why a stateless embed needs a credential at all** — because the
|
|
270
|
+
preview is not drawn by the browser. A `.fdt` is rendered server-side with
|
|
271
|
+
ImageMagick when its CDN URL is requested, and browser text APIs cannot
|
|
272
|
+
reproduce that layout (kerning, letter spacing, wrapping, baselines,
|
|
273
|
+
antialiasing differ per browser and from the export), so the editor rasterizes
|
|
274
|
+
every text and shape layer through the same engine via
|
|
275
|
+
`POST /api/render-layers` on the `base-url` deployment — batched, debounced,
|
|
276
|
+
supersampled 3×, composited with CSS. That endpoint fetches fonts and images
|
|
277
|
+
server-side, so it authenticates on the handed-over credential and returns
|
|
278
|
+
`401` without one: no credential, no visible text or shape layers. The security
|
|
279
|
+
template is what authorizes it when there is no Hub session. Full explanation:
|
|
280
|
+
*Why the editor still calls a server* in the README.
|
|
281
|
+
|
|
235
282
|
## Step 8 — Register the embedding origin (required)
|
|
236
283
|
|
|
237
284
|
The host page's origin must be in the deployment's `frame-ancestors` allowlist,
|
|
@@ -270,8 +317,9 @@ template document.
|
|
|
270
317
|
| `company-uuid`, `project-uuid` | no | Company / project scoping (session auth only) |
|
|
271
318
|
| `template-id` / `templateId` | no | DAM: Filerobot uuid. Stateless: opaque host id |
|
|
272
319
|
| `mode` | no | `inline` (default) or `modal` (starts closed — call `open()`) |
|
|
273
|
-
| `stateless` | no | Host owns the document; requires `content` |
|
|
320
|
+
| `stateless` | no | Host owns the document; requires `content`, or `new-template` |
|
|
274
321
|
| `content` (property only) | stateless | Template as `.fdt` XML |
|
|
322
|
+
| `new-template` / `newTemplate` | no | Stateless: open on a new, empty template — the widget supplies the blank document. Ignored when `content` is set |
|
|
275
323
|
| `template-name` / `templateName` | no | Stateless: header title |
|
|
276
324
|
| `template-query` / `templateQuery` | no | Stateless: the render to open on — the `templateQuery` from the last save. Empty uses the XML's `default=` values. |
|
|
277
325
|
| `brand-color`, `theme` | no | See Step 9 |
|
|
@@ -294,7 +342,8 @@ template document.
|
|
|
294
342
|
## Public Methods
|
|
295
343
|
|
|
296
344
|
`open(templateId?)`, `close()`, `load({ content, templateId?, name? })`,
|
|
297
|
-
`confirmSave(ok, message?)`. Read-only:
|
|
345
|
+
`createNew({ templateId?, name? })`, `confirmSave(ok, message?)`. Read-only:
|
|
346
|
+
`status`, `isDirty`.
|
|
298
347
|
|
|
299
348
|
In React these are reached through a forwarded ref — **required for
|
|
300
349
|
`mode="modal"`**, which renders nothing until `open()` is called:
|
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,28 @@ 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
|
+
## [0.2.0] - 2026-08-11
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
- `new-template` attribute / `newTemplate` prop and `createNew({ templateId?,
|
|
16
|
+
name? })` — start a template from scratch in stateless mode. The widget
|
|
17
|
+
supplies the empty document (`BLANK_TEMPLATE_XML`, exported from
|
|
18
|
+
`./protocol`), so a host with nothing stored yet needs no knowledge of the
|
|
19
|
+
`.fdt` format: the editor opens on its empty state, the user adds the first
|
|
20
|
+
layout, and the first `save` hands back a complete document to store.
|
|
21
|
+
`content` wins when both are set. Empty `content` still means "the host is
|
|
22
|
+
still fetching" and keeps the editor waiting — only the flag turns that into
|
|
23
|
+
a blank document.
|
|
24
|
+
|
|
25
|
+
### Changed
|
|
26
|
+
|
|
27
|
+
- The demo's Base URL defaults to `https://design-templates.scaleflex.com`
|
|
28
|
+
instead of `http://localhost:3000`, so the published page is usable without
|
|
29
|
+
filling a field first. It also logs the full `.fdt` and `templateQuery` to the
|
|
30
|
+
console at both crossings — going into the editor, and coming back on save —
|
|
31
|
+
while the on-page panel keeps summarizing.
|
|
32
|
+
|
|
11
33
|
## [0.1.1] - 2026-08-10
|
|
12
34
|
|
|
13
35
|
First published release. `<sfx-template-builder>` custom element, `./react`
|
package/README.md
CHANGED
|
@@ -30,9 +30,11 @@
|
|
|
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
|
+
- [Starting a template from scratch](#starting-a-template-from-scratch)
|
|
36
38
|
- [Reporting a failed save](#reporting-a-failed-save)
|
|
37
39
|
- [Configuration](#configuration)
|
|
38
40
|
- [Attributes & properties](#attributes--properties)
|
|
@@ -77,6 +79,9 @@ protocol adapter.
|
|
|
77
79
|
template](#security-template-guest-auth) when you have no Hub account to hand
|
|
78
80
|
over per user.
|
|
79
81
|
- **Inline or modal** — fill a box in your layout, or cover the viewport.
|
|
82
|
+
- **New templates without the format** — [`new-template`](#starting-a-template-from-scratch)
|
|
83
|
+
starts an empty document for the user to build; you only ever store what
|
|
84
|
+
comes back.
|
|
80
85
|
- **Themeable** — one [brand colour](#brand-color) drives the editor's whole
|
|
81
86
|
accent ramp; light, dark, or follow the OS.
|
|
82
87
|
- **Origin-checked both ways** — the widget only accepts messages from the app
|
|
@@ -108,7 +113,7 @@ npm i @scaleflex/template-builder
|
|
|
108
113
|
### CDN
|
|
109
114
|
|
|
110
115
|
```html
|
|
111
|
-
<script type="module" src="https://cdn.scaleflex.com/design-template-builder/0.
|
|
116
|
+
<script type="module" src="https://cdn.scaleflex.com/design-template-builder/0.2.0/template-builder.min.js"></script>
|
|
112
117
|
```
|
|
113
118
|
|
|
114
119
|
The CDN bundle is self-registering — it defines `<sfx-template-builder>` on
|
|
@@ -134,6 +139,15 @@ load, with Lit bundled in. Pin the major version.
|
|
|
134
139
|
|
|
135
140
|
## Quick Start
|
|
136
141
|
|
|
142
|
+
You need two things from your Filerobot project: its **token**, and a
|
|
143
|
+
**security template** key — a named, permission-scoped credential you define
|
|
144
|
+
once, the same guest-auth mechanism the other Scaleflex widgets use. No Hub
|
|
145
|
+
account, and no user of yours ever needs a Scaleflex identity. See
|
|
146
|
+
[Authentication](#authentication) for how to scope one.
|
|
147
|
+
|
|
148
|
+
The template document stays on your side: you hand the widget its XML, and the
|
|
149
|
+
edit comes back to you on save. That is [stateless](#stateless) mode.
|
|
150
|
+
|
|
137
151
|
### Vanilla JS / Web Component
|
|
138
152
|
|
|
139
153
|
```html
|
|
@@ -143,16 +157,26 @@ load, with Lit bundled in. Pin the major version.
|
|
|
143
157
|
|
|
144
158
|
<sfx-template-builder
|
|
145
159
|
base-url="https://<your-design-templates-deployment>"
|
|
146
|
-
token="
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
template-id="TEMPLATE_UUID"
|
|
160
|
+
token="PROJECT_TOKEN"
|
|
161
|
+
sec-template="SEC_TEMPLATE_KEY"
|
|
162
|
+
stateless
|
|
150
163
|
style="display:block;height:800px"
|
|
151
164
|
></sfx-template-builder>
|
|
152
165
|
|
|
153
166
|
<script>
|
|
154
167
|
const builder = document.querySelector('sfx-template-builder')
|
|
155
|
-
|
|
168
|
+
|
|
169
|
+
// 1 — hand it the template to edit
|
|
170
|
+
const { content, name } = await fetch(`/api/templates/${id}`).then((r) => r.json())
|
|
171
|
+
builder.load({ templateId: id, name, content })
|
|
172
|
+
|
|
173
|
+
// 2 — take the edit back and store it
|
|
174
|
+
builder.addEventListener('save', async (e) => {
|
|
175
|
+
const { templateId, content, name, templateQuery } = e.detail
|
|
176
|
+
const ok = await saveToYourApi(templateId, { content, name, templateQuery })
|
|
177
|
+
builder.confirmSave(ok) // false → the editor keeps its unsaved-changes warning
|
|
178
|
+
})
|
|
179
|
+
|
|
156
180
|
builder.addEventListener('error', (e) => console.error(e.detail)) // { code, message }
|
|
157
181
|
</script>
|
|
158
182
|
```
|
|
@@ -164,6 +188,38 @@ Size the element yourself — in `inline` mode it fills the box you give it.
|
|
|
164
188
|
```tsx
|
|
165
189
|
import { TemplateBuilder } from '@scaleflex/template-builder/react'
|
|
166
190
|
|
|
191
|
+
<TemplateBuilder
|
|
192
|
+
stateless
|
|
193
|
+
baseUrl="https://<deployment>"
|
|
194
|
+
token={projectToken}
|
|
195
|
+
secTemplate={secTemplateKey}
|
|
196
|
+
templateId={id}
|
|
197
|
+
name={name}
|
|
198
|
+
content={xml}
|
|
199
|
+
style={{ height: 800 }}
|
|
200
|
+
onSave={async (data) => (await saveToYourApi(data)).ok}
|
|
201
|
+
/>
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
<!-- internal:start -->
|
|
205
|
+
### Hub session (internal)
|
|
206
|
+
|
|
207
|
+
Scaleflex-side embeds inside the Hub authenticate with a session instead of a
|
|
208
|
+
security template, which unlocks DAM-backed storage and Hub-project features.
|
|
209
|
+
Mint the session server-side; never put a long-lived credential in client code.
|
|
210
|
+
|
|
211
|
+
```html
|
|
212
|
+
<sfx-template-builder
|
|
213
|
+
base-url="https://<your-design-templates-deployment>"
|
|
214
|
+
token="FILEROBOT_TOKEN"
|
|
215
|
+
sass-key="SASS_KEY"
|
|
216
|
+
session-uuid="SESSION_UUID"
|
|
217
|
+
template-id="TEMPLATE_UUID"
|
|
218
|
+
style="display:block;height:800px"
|
|
219
|
+
></sfx-template-builder>
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
```tsx
|
|
167
223
|
<TemplateBuilder
|
|
168
224
|
baseUrl="https://<deployment>"
|
|
169
225
|
token={token}
|
|
@@ -174,6 +230,7 @@ import { TemplateBuilder } from '@scaleflex/template-builder/react'
|
|
|
174
230
|
onSave={(data) => console.log(data)}
|
|
175
231
|
/>
|
|
176
232
|
```
|
|
233
|
+
<!-- internal:end -->
|
|
177
234
|
|
|
178
235
|
---
|
|
179
236
|
|
|
@@ -203,9 +260,8 @@ point.
|
|
|
203
260
|
<sfx-template-builder
|
|
204
261
|
stateless
|
|
205
262
|
base-url="https://<deployment>"
|
|
206
|
-
token="
|
|
207
|
-
|
|
208
|
-
session-uuid="SESSION_UUID"
|
|
263
|
+
token="PROJECT_TOKEN"
|
|
264
|
+
sec-template="SEC_TEMPLATE_KEY"
|
|
209
265
|
style="display:block;height:800px"
|
|
210
266
|
></sfx-template-builder>
|
|
211
267
|
|
|
@@ -270,9 +326,8 @@ function TemplateEditor({ id }: { id: string }) {
|
|
|
270
326
|
<TemplateBuilder
|
|
271
327
|
stateless
|
|
272
328
|
baseUrl="https://<deployment>"
|
|
273
|
-
token={
|
|
274
|
-
|
|
275
|
-
sessionUuid={sessionUuid}
|
|
329
|
+
token={projectToken}
|
|
330
|
+
secTemplate={secTemplateKey}
|
|
276
331
|
// 2 — pass it in.
|
|
277
332
|
templateId={id}
|
|
278
333
|
content={tpl.content}
|
|
@@ -357,7 +412,8 @@ you navigated away from.
|
|
|
357
412
|
Statelessness applies to the **document**, not to the infrastructure. The editor
|
|
358
413
|
still needs an authenticated Filerobot tenant for:
|
|
359
414
|
|
|
360
|
-
- **text rendering** — text layers are rasterized server-side
|
|
415
|
+
- **text rendering** — text and shape layers are rasterized server-side (see
|
|
416
|
+
[the render round-trip](#why-the-editor-still-calls-a-server) below),
|
|
361
417
|
- **fonts** — custom fonts are served from the tenant's `.studio/fonts/`,
|
|
362
418
|
- **asset browsing and upload** — image layers are picked from the DAM,
|
|
363
419
|
- **metadata variables and regional settings**.
|
|
@@ -370,6 +426,135 @@ Images referenced by a template may live on your own CDN, but the render
|
|
|
370
426
|
service only fetches from allowlisted hosts — add yours to the deployment's
|
|
371
427
|
`RENDER_ALLOWED_HOSTS_EXTRA`.
|
|
372
428
|
|
|
429
|
+
#### Why the editor still calls a server
|
|
430
|
+
|
|
431
|
+
This is the reason a stateless embed still needs a credential, so it is worth
|
|
432
|
+
being concrete about.
|
|
433
|
+
|
|
434
|
+
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,
|
|
436
|
+
with ImageMagick:
|
|
437
|
+
|
|
438
|
+
```
|
|
439
|
+
https://<tenant>.filerobot.com/<path>/<template>.fdt?<templateQuery>&force_format=png
|
|
440
|
+
```
|
|
441
|
+
|
|
442
|
+
The editor's contract is that what you see while editing is what that URL will
|
|
443
|
+
return. That rules out drawing the text in the browser. Line breaking, kerning,
|
|
444
|
+
letter spacing, baseline placement, shrink-to-fit and antialiasing are FreeType
|
|
445
|
+
and ImageMagick behaviours; canvas `fillText` and DOM text go through the
|
|
446
|
+
browser's own shaping and hinting instead, so the same layer lands differently
|
|
447
|
+
in Chrome, Safari and Firefox — and differently from the export in all three. A
|
|
448
|
+
few pixels of drift is enough to move a headline off a product shot. There is
|
|
449
|
+
no JS library that reimplements that layout either: the only faithful
|
|
450
|
+
implementation of ImageMagick's text rendering is ImageMagick.
|
|
451
|
+
|
|
452
|
+
So the editor does not approximate the export — it runs the same engine.
|
|
453
|
+
Text and shape layers are rasterized by an ImageMagick 7 build hosted in your
|
|
454
|
+
`base-url` deployment (the export pipeline drives ImageMagick 7 from PHP; the
|
|
455
|
+
editor drives a WebAssembly build of it), and each layer comes back as a
|
|
456
|
+
transparent PNG that the canvas positions with CSS:
|
|
457
|
+
|
|
458
|
+
```
|
|
459
|
+
browser — widget iframe app deployment (base-url)
|
|
460
|
+
─────────────────────── ─────────────────────────
|
|
461
|
+
edit a text layer
|
|
462
|
+
│ batched across layers, debounced
|
|
463
|
+
├────── POST /api/render-layers ─────▶ ImageMagick (WASM)
|
|
464
|
+
│ layers + fonts + variables ├─ resolve fonts: bundled,
|
|
465
|
+
│ │ then tenant /.studio/fonts
|
|
466
|
+
│ ├─ draw at 3×, downscale
|
|
467
|
+
◀────── transparent PNG per layer ─────┘
|
|
468
|
+
│
|
|
469
|
+
└─ position / rotate / fade with CSS — no round trip
|
|
470
|
+
```
|
|
471
|
+
|
|
472
|
+
Consequences you can observe from the outside:
|
|
473
|
+
|
|
474
|
+
- **Content edits cost a round trip; placement edits do not.** Text, font,
|
|
475
|
+
weight, colour, alignment, letter spacing and box size re-render. Dragging,
|
|
476
|
+
rotating and opacity are CSS transforms on the PNG already in the page, so
|
|
477
|
+
they stay at pointer speed.
|
|
478
|
+
- **Bursts collapse.** Requests are debounced (~300 ms) and batched across
|
|
479
|
+
layers, and an in-flight batch is aborted when you keep typing — so a
|
|
480
|
+
sentence typed at speed costs one render, not one per keystroke.
|
|
481
|
+
- **Layers are drawn at 3× and downscaled**, so preview antialiasing matches
|
|
482
|
+
the export rather than the browser's rasterizer.
|
|
483
|
+
- **Fonts are resolved server-side** against the tenant's `/.studio/fonts`
|
|
484
|
+
folder and cached there, so the page never downloads a rendering engine or a
|
|
485
|
+
font binary per weight. Image layers are the exception — they are plain
|
|
486
|
+
`<img>` elements, drawn by the browser.
|
|
487
|
+
|
|
488
|
+
**That endpoint is authenticated, and it has to be.** It fetches fonts and
|
|
489
|
+
images by URL on the server's behalf, so it is not open to anonymous callers:
|
|
490
|
+
`POST /api/render-layers` requires the credential the widget handed over, and
|
|
491
|
+
without a valid one it answers `401` and text and shape layers simply never
|
|
492
|
+
appear. The same credential authorizes the font list/upload calls and the asset
|
|
493
|
+
picker.
|
|
494
|
+
|
|
495
|
+
In DAM-backed mode a Hub session covers that. A stateless embed has no Hub
|
|
496
|
+
session to hand over — and that is exactly the gap a
|
|
497
|
+
[security template](#security-template-guest-auth) fills: a permission-scoped,
|
|
498
|
+
project-level guest credential that authorizes rendering, fonts and asset
|
|
499
|
+
browsing without authenticating any particular user, and without your users
|
|
500
|
+
existing in Filerobot at all.
|
|
501
|
+
|
|
502
|
+
### Starting a template from scratch
|
|
503
|
+
|
|
504
|
+
A template your user has not created yet has no XML to pass in, and you should
|
|
505
|
+
not have to author one. In [stateless](#stateless) mode, set `new-template`
|
|
506
|
+
instead of `content` and the widget supplies the empty document itself.
|
|
507
|
+
(DAM-backed mode has its own new-template flow — leave `template-id` empty.)
|
|
508
|
+
|
|
509
|
+
```html
|
|
510
|
+
<sfx-template-builder
|
|
511
|
+
stateless
|
|
512
|
+
new-template
|
|
513
|
+
template-name="Untitled"
|
|
514
|
+
base-url="https://<deployment>"
|
|
515
|
+
token="PROJECT_TOKEN"
|
|
516
|
+
sec-template="SEC_TEMPLATE_KEY"
|
|
517
|
+
style="display:block;height:800px"
|
|
518
|
+
></sfx-template-builder>
|
|
519
|
+
```
|
|
520
|
+
|
|
521
|
+
```js
|
|
522
|
+
// Or imperatively, on an element that is already showing something else.
|
|
523
|
+
builder.createNew({ templateId: 'your-own-id-43', name: 'Untitled' })
|
|
524
|
+
```
|
|
525
|
+
|
|
526
|
+
```tsx
|
|
527
|
+
<TemplateBuilder stateless newTemplate templateName="Untitled" … />
|
|
528
|
+
```
|
|
529
|
+
|
|
530
|
+
The editor opens on its empty state — *"No layouts yet. Click + Add to create
|
|
531
|
+
one."* — and the user picks the canvas size, background and preset there. Save
|
|
532
|
+
is refused until at least one layout exists, so the first `save` you receive
|
|
533
|
+
already carries a complete, well-formed `.fdt` document; store it as `content`
|
|
534
|
+
and every later open is the ordinary [load flow](#the-template-comes-from-your-api).
|
|
535
|
+
|
|
536
|
+
- **`templateId` is optional.** Pass one if your record already exists and you
|
|
537
|
+
want it echoed back; otherwise the `save` payload simply arrives without an
|
|
538
|
+
id and you allocate one when you store it.
|
|
539
|
+
- **`templateQuery` stays empty.** A new document has no layouts and no
|
|
540
|
+
variables, so there is no render for a query to select. You get one back on
|
|
541
|
+
the first save — persist it then.
|
|
542
|
+
- **`content` wins when both are set,** so a host that renders one element for
|
|
543
|
+
both cases can simply pass the XML when it has one.
|
|
544
|
+
- **Empty `content` on its own does not start a blank template.** It means "the
|
|
545
|
+
host has nothing yet" — the editor keeps waiting, which is what lets you
|
|
546
|
+
mount the builder while your fetch is still in flight. Only `new-template`
|
|
547
|
+
turns that wait into a document.
|
|
548
|
+
|
|
549
|
+
Calling `createNew()` again while the blank template is already open does
|
|
550
|
+
nothing: resending would discard whatever the user has built since. Close and
|
|
551
|
+
reopen the editor to genuinely start over.
|
|
552
|
+
|
|
553
|
+
If you would rather ship your own starting point — a house style, a standard
|
|
554
|
+
canvas size, a locked logo layer — pass it as ordinary `content`. A starter
|
|
555
|
+
template is just a template, and `BLANK_TEMPLATE_XML` is exported from
|
|
556
|
+
`@scaleflex/template-builder/protocol` if you want the empty document as a base.
|
|
557
|
+
|
|
373
558
|
### Reporting a failed save
|
|
374
559
|
|
|
375
560
|
The editor clears its unsaved-changes state as soon as it posts `save` —
|
|
@@ -411,8 +596,9 @@ without it a failed write is invisible to the user.
|
|
|
411
596
|
| `company-uuid`, `project-uuid` | no | Company / project scoping (session auth only) |
|
|
412
597
|
| `template-id` / `templateId` | no | DAM-backed: Filerobot uuid to edit, empty opens the new-template flow. Stateless: opaque id echoed back on `save` |
|
|
413
598
|
| `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` |
|
|
599
|
+
| `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
600
|
| `content` (property only) | stateless | The template to edit, as `.fdt` XML. Assigning a new value loads it into a running editor |
|
|
601
|
+
| `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
602
|
| `template-name` / `templateName` | no | Stateless: header title |
|
|
417
603
|
| `template-query` / `templateQuery` | no | Stateless: the render to open on — the `templateQuery` from the last save. Empty uses the XML's `default=` values. |
|
|
418
604
|
| `brand-color` / `brandColor` | no | Accent colour for the editor chrome, `#rgb` / `#rrggbb` |
|
|
@@ -432,18 +618,27 @@ something to hardcode in a public bundle.
|
|
|
432
618
|
| Metadata fields, regional variants, project branding | yes | **empty** |
|
|
433
619
|
| Rendering, fonts, asset picker | yes | yes, within the template's scope |
|
|
434
620
|
|
|
621
|
+
<!-- internal:start -->
|
|
435
622
|
#### Hub session
|
|
436
623
|
|
|
437
624
|
Mint the Hub session **server-side** and inject `session-uuid` / `sass-key` /
|
|
438
625
|
`token` into your page. Issue short-lived per-user sessions; never embed a
|
|
439
626
|
long-lived master credential in client-side code.
|
|
627
|
+
<!-- internal:end -->
|
|
440
628
|
|
|
441
629
|
#### Security template (guest auth)
|
|
442
630
|
|
|
443
631
|
A Filerobot **security template** is a named, permission-scoped credential you
|
|
444
632
|
define once in your Filerobot project — the same guest-auth mechanism the other
|
|
445
633
|
Scaleflex widgets use. Hand one to the widget and no Hub session is involved at
|
|
446
|
-
all
|
|
634
|
+
all.
|
|
635
|
+
|
|
636
|
+
It is what makes a stateless embed work without Hub accounts. Even when the
|
|
637
|
+
document never leaves your side, the editor rasterizes every text and shape
|
|
638
|
+
layer on the server to stay pixel-identical to the CDN render, and resolves
|
|
639
|
+
fonts and assets from your tenant — all of it authenticated. See
|
|
640
|
+
[why the editor still calls a server](#why-the-editor-still-calls-a-server) for
|
|
641
|
+
what those calls are.
|
|
447
642
|
|
|
448
643
|
```html
|
|
449
644
|
<sfx-template-builder
|
|
@@ -469,6 +664,16 @@ all:
|
|
|
469
664
|
The app exchanges the key for a short-lived access key itself and renews it when
|
|
470
665
|
it expires, so the embed does not die mid-session.
|
|
471
666
|
|
|
667
|
+
**Scoping the template.** Grant it `LIST` on the folders you want browsable,
|
|
668
|
+
plus `LIST` + `UPLOAD` on `/.studio/fonts*` if users are to see or add custom
|
|
669
|
+
fonts — anything the template cannot reach simply isn't there. Prefer a short
|
|
670
|
+
TTL: the app re-exchanges the key when it expires, so a short-lived template
|
|
671
|
+
costs you nothing and limits the blast radius if one leaks.
|
|
672
|
+
|
|
673
|
+
If the key is rejected — revoked, wrong project token, typo — the widget emits
|
|
674
|
+
`error` with code `auth`.
|
|
675
|
+
|
|
676
|
+
<!-- internal:start -->
|
|
472
677
|
**What it costs.** A security template authenticates *nobody in particular*: no
|
|
473
678
|
user identity, no Hub project behind it. That has consequences worth knowing
|
|
474
679
|
before you pick it:
|
|
@@ -479,20 +684,20 @@ before you pick it:
|
|
|
479
684
|
- **Hub-project features come back empty** — metadata fields, regional variants
|
|
480
685
|
and dynamic fields have no model to read, and project branding does not apply
|
|
481
686
|
(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.
|
|
487
|
-
|
|
488
|
-
If the key is rejected — revoked, wrong project token, typo — the widget emits
|
|
489
|
-
`error` with code `auth`.
|
|
687
|
+
<!-- internal:end -->
|
|
490
688
|
|
|
491
689
|
### Origin registration
|
|
492
690
|
|
|
493
691
|
Your page's origin must be in the deployment's `frame-ancestors` allowlist
|
|
494
692
|
(`NEXT_PUBLIC_TRUSTED_HUB_ORIGINS`), otherwise the browser refuses to render the
|
|
495
|
-
iframe
|
|
693
|
+
iframe — Chrome shows "refused to connect" in place of the editor, and the
|
|
694
|
+
widget reports `handshake-timeout`.
|
|
695
|
+
|
|
696
|
+
A deployment allows `'self'`, `https://*.scaleflex.com`,
|
|
697
|
+
`https://*.filerobot.com` and `http://localhost:5173` (the demo's dev server)
|
|
698
|
+
out of the box, plus whatever its `NEXT_PUBLIC_TRUSTED_HUB_ORIGINS` names. Your
|
|
699
|
+
own domain has to be added there — the list is baked in at build time, so it
|
|
700
|
+
takes a rebuild of the app, not just a restart.
|
|
496
701
|
|
|
497
702
|
### Cookies
|
|
498
703
|
|
|
@@ -509,6 +714,7 @@ without CHIPS support that block third-party cookies will fail with `auth` or
|
|
|
509
714
|
| `open(templateId?)` | Open the editor, loading the iframe. Optionally switch template first. |
|
|
510
715
|
| `close()` | Close the editor and unload the iframe. Does not emit `close`. |
|
|
511
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). |
|
|
717
|
+
| `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
718
|
| `confirmSave(ok, message?)` | Stateless: report whether you persisted the content. See [Reporting a failed save](#reporting-a-failed-save). |
|
|
513
719
|
|
|
514
720
|
**Read-only properties:** `status` (`idle` \| `loading` \| `ready` \| `error`),
|
|
@@ -636,10 +842,12 @@ blocks third-party cookies the editor cannot authenticate and the widget reports
|
|
|
636
842
|
|
|
637
843
|
---
|
|
638
844
|
|
|
845
|
+
<!-- internal:start -->
|
|
639
846
|
## Development
|
|
640
847
|
|
|
641
848
|
```bash
|
|
642
|
-
yarn dev:demo # demo site (
|
|
849
|
+
yarn dev:demo # demo site (defaults to the deployed app; point Base URL at
|
|
850
|
+
# http://localhost:3000 to drive a local one)
|
|
643
851
|
yarn test # vitest
|
|
644
852
|
yarn typecheck # tsc --noEmit
|
|
645
853
|
yarn build # dist/ — npm artifact (ESM + CJS + types)
|
|
@@ -666,7 +874,18 @@ the README reloads the dev server.
|
|
|
666
874
|
the folder can be zipped and handed to a client, or dropped behind any static
|
|
667
875
|
host at any path — GitHub Pages, S3, a subdirectory of an existing site. The
|
|
668
876
|
only thing it needs at runtime is a reachable `design-templates-app` for the
|
|
669
|
-
demo page's Base URL field to point at
|
|
877
|
+
demo page's Base URL field to point at; it defaults to
|
|
878
|
+
`https://design-templates.scaleflex.com`.
|
|
879
|
+
|
|
880
|
+
That default frames successfully only from an origin that deployment allows —
|
|
881
|
+
`'self'`, `*.scaleflex.com`, `*.filerobot.com` and `http://localhost:5173`,
|
|
882
|
+
which covers both the demo as published to the CDN and `yarn dev:demo`. Serving
|
|
883
|
+
it anywhere else — another port (`yarn preview:demo` uses 4173), or a copy on
|
|
884
|
+
your own domain — gets "refused to connect" until that origin joins the
|
|
885
|
+
deployment's allowlist, which is baked in at build time and so needs a rebuild
|
|
886
|
+
(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.
|
|
670
889
|
|
|
671
890
|
The demo page takes its XML from a URL, from a paste, or from a picker listing
|
|
672
891
|
the `.fdt` files in the project its credentials point at, which fills the id,
|
|
@@ -707,11 +926,23 @@ npm publish, git commit + tag + push. It needs a `.env.local` in this package:
|
|
|
707
926
|
```env
|
|
708
927
|
FILEROBOT_CDN_TOKEN=scaleflex
|
|
709
928
|
FILEROBOT_CDN_SECU=<secu key>
|
|
710
|
-
FILEROBOT_CDN_FOLDER=/plugins/scaleflex/template-builder/{version}/
|
|
929
|
+
FILEROBOT_CDN_FOLDER=/plugins/scaleflex/design-template-builder/{version}/
|
|
711
930
|
```
|
|
712
931
|
|
|
713
932
|
Update [CHANGELOG.md](./CHANGELOG.md) before releasing.
|
|
714
933
|
|
|
934
|
+
`yarn release:demo` publishes the demo site into `<that folder>/demo/`, next to
|
|
935
|
+
the bundle it demonstrates. `yarn release:demo:probe` sends a single file first,
|
|
936
|
+
reporting the content-type the CDN serves it as. The pages are built as
|
|
937
|
+
self-contained HTML with their JS and CSS inlined, because the CDN project
|
|
938
|
+
refuses `.js` uploads.
|
|
939
|
+
|
|
940
|
+
Note the CDN caches for 24h: re-uploading over a path that has already been
|
|
941
|
+
fetched keeps serving the old copy until it expires. Version folders are the
|
|
942
|
+
way around it — don't overwrite a published one.
|
|
943
|
+
|
|
944
|
+
<!-- internal:end -->
|
|
945
|
+
|
|
715
946
|
---
|
|
716
947
|
|
|
717
948
|
## Claude Code Integration
|
package/dist/define.cjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const e=require("./template-builder-
|
|
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;
|
|
2
2
|
//# sourceMappingURL=define.cjs.map
|
package/dist/define.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { S as e } from "./template-builder-
|
|
1
|
+
import { S as e } from "./template-builder-De0hRO4s.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-
|
|
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;
|
|
2
2
|
//# sourceMappingURL=index.cjs.map
|
package/dist/index.js
CHANGED
|
@@ -1,21 +1,22 @@
|
|
|
1
|
-
import { A as a, B as
|
|
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";
|
|
2
2
|
export {
|
|
3
3
|
a as AUTH_MODES,
|
|
4
|
+
_ as BLANK_TEMPLATE_XML,
|
|
4
5
|
s as BRAND_COLOR_PATTERN,
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
D as BUILDER_CLOSE,
|
|
7
|
+
O as BUILDER_CONTENT,
|
|
8
|
+
T as BUILDER_CONTENT_REQUEST,
|
|
9
|
+
L as BUILDER_DIRTY,
|
|
9
10
|
B as BUILDER_ERROR,
|
|
10
|
-
|
|
11
|
+
A as BUILDER_OPEN,
|
|
11
12
|
S as BUILDER_READY,
|
|
12
13
|
U as BUILDER_SAVE,
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
14
|
+
I as EMBED_PARAMS,
|
|
15
|
+
N as EMBED_ROUTE,
|
|
16
|
+
e as HOST_LOAD,
|
|
17
|
+
M as HOST_SAVED,
|
|
18
|
+
P as PROTOCOL_VERSION,
|
|
19
|
+
C as SfxTemplateBuilder,
|
|
20
|
+
l as builderRoute
|
|
20
21
|
};
|
|
21
22
|
//# sourceMappingURL=index.js.map
|
package/dist/protocol.d.ts
CHANGED
|
@@ -150,6 +150,30 @@ export interface HostLoadMessage {
|
|
|
150
150
|
type: typeof HOST_LOAD;
|
|
151
151
|
data: HostLoadData;
|
|
152
152
|
}
|
|
153
|
+
/**
|
|
154
|
+
* An empty `.fdt` document: no layouts, no layers, no variables. What the
|
|
155
|
+
* widget sends as `HOST_LOAD` content when the host asked for a new template
|
|
156
|
+
* (`new-template`) instead of supplying one, so starting from scratch costs a
|
|
157
|
+
* host no knowledge of the template format.
|
|
158
|
+
*
|
|
159
|
+
* The editor opens on its empty state — "No layouts yet. Click + Add to create
|
|
160
|
+
* one." — and the user picks the canvas size there. Save is refused until a
|
|
161
|
+
* layout exists, and hands back a fully-formed document serialized by the app,
|
|
162
|
+
* not this skeleton.
|
|
163
|
+
*
|
|
164
|
+
* Sent as ordinary `HOST_LOAD` content rather than a new message so it works
|
|
165
|
+
* against app deployments that predate this widget version: the document is
|
|
166
|
+
* the whole signal, and every app that can parse a template can parse this.
|
|
167
|
+
*
|
|
168
|
+
* `version` tracks the app's `TEMPLATE_VERSION` for the benefit of whoever
|
|
169
|
+
* reads this next: nothing consumes it. The parser never looks at it, and the
|
|
170
|
+
* backend never sees this document — the app refuses to save a template with
|
|
171
|
+
* no layouts, so what reaches the render pipeline was re-serialized by the app
|
|
172
|
+
* with a layout present. A widget lagging the app by a version still loads.
|
|
173
|
+
* `design-templates` pins the pair in
|
|
174
|
+
* `src/lib/xml/__tests__/blank-template.test.ts`.
|
|
175
|
+
*/
|
|
176
|
+
export declare const BLANK_TEMPLATE_XML: string;
|
|
153
177
|
/**
|
|
154
178
|
* Stateless mode only (protocol v2). Reports whether the host managed to
|
|
155
179
|
* persist the content it received in `BUILDER_CONTENT`.
|
package/dist/react.cjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const
|
|
1
|
+
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const a=require("react");require("./define.cjs");const h=a.forwardRef(function(y,v){const{className:T,style:U,onReady:m,onOpen:c,onClose:d,onSave:i,onError:p,onDirtyChange:f,...e}=y,l=a.useRef(null);return a.useImperativeHandle(v,()=>l.current,[]),a.useLayoutEffect(()=>{const t=l.current;t&&(t.baseUrl=e.baseUrl,t.token=e.token,t.sassKey=e.sassKey??"",t.sessionUuid=e.sessionUuid??"",t.secTemplate=e.secTemplate??"",t.companyUuid=e.companyUuid??"",t.projectUuid=e.projectUuid??"",t.templateId=e.templateId??"",t.mode=e.mode??"inline",t.stateless=e.stateless??!1,t.templateName=e.templateName??"",t.templateQuery=e.templateQuery??"",t.brandColor=e.brandColor??"",t.theme=e.theme??"",t.newTemplate=e.newTemplate??!1,t.content=e.content??"",e.readyTimeout!==void 0&&(t.readyTimeout=e.readyTimeout))},[e.baseUrl,e.token,e.sassKey,e.sessionUuid,e.secTemplate,e.companyUuid,e.projectUuid,e.templateId,e.mode,e.stateless,e.content,e.newTemplate,e.templateName,e.templateQuery,e.brandColor,e.theme,e.readyTimeout]),a.useLayoutEffect(()=>{const t=l.current;if(!t)return;const u=[],o=(s,r)=>{if(!r)return;const n=(b=>r(b.detail));t.addEventListener(s,n),u.push([s,n])};if(o("ready",m),o("open",c),o("close",d),o("error",p),o("dirtychange",f),i){const s=(r=>{Promise.resolve().then(()=>i(r.detail)).then(n=>t.confirmSave(n!==!1)).catch(n=>{console.error("[sfx-template-builder] onSave failed:",n),t.confirmSave(!1)})});t.addEventListener("save",s),u.push(["save",s])}return()=>{for(const[s,r]of u)t.removeEventListener(s,r)}},[m,c,d,i,p,f]),a.createElement("sfx-template-builder",{ref:l,class:T,style:U})});exports.TemplateBuilder=h;
|
|
2
2
|
//# sourceMappingURL=react.cjs.map
|