@scaleflex/template-builder 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/skills/integrate-template-builder/SKILL.md +356 -0
- package/CHANGELOG.md +66 -0
- package/LICENSE +50 -0
- package/README.md +775 -0
- package/dist/define.cjs +2 -0
- package/dist/define.cjs.map +1 -0
- package/dist/define.d.ts +2 -0
- package/dist/define.js +6 -0
- package/dist/define.js.map +1 -0
- package/dist/index.cjs +2 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +21 -0
- package/dist/index.js.map +1 -0
- package/dist/protocol.d.ts +237 -0
- package/dist/react.cjs +2 -0
- package/dist/react.cjs.map +1 -0
- package/dist/react.d.ts +95 -0
- package/dist/react.js +59 -0
- package/dist/react.js.map +1 -0
- package/dist/template-builder-CSyPZni9.cjs +52 -0
- package/dist/template-builder-CSyPZni9.cjs.map +1 -0
- package/dist/template-builder-S33H_d5T.js +354 -0
- package/dist/template-builder-S33H_d5T.js.map +1 -0
- package/dist/template-builder.d.ts +206 -0
- package/package.json +75 -0
- package/src/define.ts +10 -0
- package/src/index.ts +9 -0
- package/src/protocol.ts +298 -0
- package/src/react.ts +223 -0
- package/src/template-builder.ts +599 -0
package/README.md
ADDED
|
@@ -0,0 +1,775 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<a href="https://www.scaleflex.com">
|
|
3
|
+
<img src="https://scaleflex.cloudimg.io/v7/plugins/scaleflex/logo.png?vh=b0a502&radius=25&w=700" alt="Scaleflex" width="350">
|
|
4
|
+
</a>
|
|
5
|
+
</p>
|
|
6
|
+
|
|
7
|
+
<h1 align="center">@scaleflex/template-builder</h1>
|
|
8
|
+
|
|
9
|
+
<p align="center">
|
|
10
|
+
Framework-agnostic Design Template Builder Web Component for <a href="https://www.scaleflex.com/page/digital-asset-management">Filerobot</a>.<br>
|
|
11
|
+
Drop a full multi-layout template editor into any page, in any frontend stack.
|
|
12
|
+
</p>
|
|
13
|
+
|
|
14
|
+
<p align="center">
|
|
15
|
+
<a href="https://www.npmjs.com/package/@scaleflex/template-builder"><img src="https://img.shields.io/npm/v/@scaleflex/template-builder.svg?style=flat-square" alt="npm version"></a>
|
|
16
|
+
<img src="https://img.shields.io/badge/license-proprietary-red?style=flat-square" alt="license">
|
|
17
|
+
</p>
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## Table of Contents
|
|
22
|
+
|
|
23
|
+
- [Overview](#overview)
|
|
24
|
+
- [Features](#features)
|
|
25
|
+
- [Requirements](#requirements)
|
|
26
|
+
- [Installation](#installation)
|
|
27
|
+
- [npm / yarn / pnpm](#npm--yarn--pnpm)
|
|
28
|
+
- [CDN](#cdn)
|
|
29
|
+
- [Package exports](#package-exports)
|
|
30
|
+
- [Quick Start](#quick-start)
|
|
31
|
+
- [Vanilla JS / Web Component](#vanilla-js--web-component)
|
|
32
|
+
- [React](#react)
|
|
33
|
+
- [Modes](#modes)
|
|
34
|
+
- [DAM-backed](#dam-backed-default)
|
|
35
|
+
- [Stateless](#stateless)
|
|
36
|
+
- [Reporting a failed save](#reporting-a-failed-save)
|
|
37
|
+
- [Configuration](#configuration)
|
|
38
|
+
- [Attributes & properties](#attributes--properties)
|
|
39
|
+
- [Authentication](#authentication)
|
|
40
|
+
- [Origin registration](#origin-registration)
|
|
41
|
+
- [Cookies](#cookies)
|
|
42
|
+
- [Public Methods](#public-methods)
|
|
43
|
+
- [Events](#events)
|
|
44
|
+
- [React API](#react-api)
|
|
45
|
+
- [Theming](#theming)
|
|
46
|
+
- [Brand Color](#brand-color)
|
|
47
|
+
- [Colour scheme](#colour-scheme)
|
|
48
|
+
- [Browser Support](#browser-support)
|
|
49
|
+
- [Claude Code Integration](#claude-code-integration)
|
|
50
|
+
- [Development](#development)
|
|
51
|
+
- [The demo site](#the-demo-site)
|
|
52
|
+
- [Release](#release)
|
|
53
|
+
- [License](#license)
|
|
54
|
+
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
## Overview
|
|
58
|
+
|
|
59
|
+
`<sfx-template-builder>` embeds the Filerobot design-templates builder in a page
|
|
60
|
+
on any origin. The element owns an iframe pointed at a `design-templates-app`
|
|
61
|
+
deployment, hands over the credentials, and translates the app's
|
|
62
|
+
postMessage protocol into ordinary DOM `CustomEvent`s — so from your side it
|
|
63
|
+
behaves like any other component.
|
|
64
|
+
|
|
65
|
+
All the heavy lifting — server-side text rendering, font resolution, asset
|
|
66
|
+
browsing, template XML — stays inside the app deployment. The widget is a
|
|
67
|
+
protocol adapter.
|
|
68
|
+
|
|
69
|
+
## Features
|
|
70
|
+
|
|
71
|
+
- **Framework-agnostic** — a custom element; works in vanilla JS, React, Vue,
|
|
72
|
+
Angular, Svelte. A thin React wrapper ships in the box.
|
|
73
|
+
- **Two storage modes** — let Filerobot own the template, or keep the document
|
|
74
|
+
entirely on your side ([Stateless](#stateless)) and never map your users onto
|
|
75
|
+
Scaleflex identities.
|
|
76
|
+
- **Two ways in** — a Hub session, or a Filerobot [security
|
|
77
|
+
template](#security-template-guest-auth) when you have no Hub account to hand
|
|
78
|
+
over per user.
|
|
79
|
+
- **Inline or modal** — fill a box in your layout, or cover the viewport.
|
|
80
|
+
- **Themeable** — one [brand colour](#brand-color) drives the editor's whole
|
|
81
|
+
accent ramp; light, dark, or follow the OS.
|
|
82
|
+
- **Origin-checked both ways** — the widget only accepts messages from the app
|
|
83
|
+
origin; the app only accepts a template from the origin pinned when the
|
|
84
|
+
session was handed over.
|
|
85
|
+
- **Diagnosable failures** — auth, blocked cookies, bad content and handshake
|
|
86
|
+
timeouts all surface as an `error` event instead of a stuck iframe.
|
|
87
|
+
|
|
88
|
+
## Requirements
|
|
89
|
+
|
|
90
|
+
- A [Filerobot](https://www.scaleflex.com/page/digital-asset-management) account
|
|
91
|
+
and a running `design-templates-app` deployment to point `base-url` at
|
|
92
|
+
- Credentials minted server-side — either a Hub session (`session-uuid` +
|
|
93
|
+
**SASS key** + **Filerobot token**) or a **security template** + token for
|
|
94
|
+
guest access; see [Authentication](#authentication)
|
|
95
|
+
- Your page's origin registered in the deployment's `frame-ancestors` allowlist
|
|
96
|
+
— see [Origin registration](#origin-registration)
|
|
97
|
+
- Modern browser with Custom Elements v1 support (see
|
|
98
|
+
[Browser Support](#browser-support))
|
|
99
|
+
|
|
100
|
+
## Installation
|
|
101
|
+
|
|
102
|
+
### npm / yarn / pnpm
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
npm i @scaleflex/template-builder
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### CDN
|
|
109
|
+
|
|
110
|
+
```html
|
|
111
|
+
<script type="module" src="https://cdn.scaleflex.com/design-template-builder/0.1.1/template-builder.min.js"></script>
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
The CDN bundle is self-registering — it defines `<sfx-template-builder>` on
|
|
115
|
+
load, with Lit bundled in. Pin the major version.
|
|
116
|
+
|
|
117
|
+
### Package exports
|
|
118
|
+
|
|
119
|
+
| Entry | Contents |
|
|
120
|
+
|---|---|
|
|
121
|
+
| `@scaleflex/template-builder` | The element class and every protocol constant / type. Does **not** register the tag. |
|
|
122
|
+
| `@scaleflex/template-builder/define` | Side-effect import that registers `<sfx-template-builder>`. |
|
|
123
|
+
| `@scaleflex/template-builder/react` | The `<TemplateBuilder>` React wrapper (also registers the tag). |
|
|
124
|
+
|
|
125
|
+
> **Client-only:** the element extends `HTMLElement`, so importing `.`,
|
|
126
|
+
> `./define`, or `./react` in a server-rendered module will throw. In SSR
|
|
127
|
+
> frameworks, import dynamically on the client (e.g. Next.js `dynamic(...,
|
|
128
|
+
> { ssr: false })` or a `useEffect` import).
|
|
129
|
+
|
|
130
|
+
> **Protocol constants:** there is also a `./protocol` subpath, but it resolves
|
|
131
|
+
> to TypeScript source and exists for the `design-templates-app` workspace,
|
|
132
|
+
> which compiles it. External consumers should take the same constants from the
|
|
133
|
+
> package root, which is compiled.
|
|
134
|
+
|
|
135
|
+
## Quick Start
|
|
136
|
+
|
|
137
|
+
### Vanilla JS / Web Component
|
|
138
|
+
|
|
139
|
+
```html
|
|
140
|
+
<script type="module">
|
|
141
|
+
import '@scaleflex/template-builder/define' // registers <sfx-template-builder>
|
|
142
|
+
</script>
|
|
143
|
+
|
|
144
|
+
<sfx-template-builder
|
|
145
|
+
base-url="https://<your-design-templates-deployment>"
|
|
146
|
+
token="FILEROBOT_TOKEN"
|
|
147
|
+
sass-key="SASS_KEY"
|
|
148
|
+
session-uuid="SESSION_UUID"
|
|
149
|
+
template-id="TEMPLATE_UUID"
|
|
150
|
+
style="display:block;height:800px"
|
|
151
|
+
></sfx-template-builder>
|
|
152
|
+
|
|
153
|
+
<script>
|
|
154
|
+
const builder = document.querySelector('sfx-template-builder')
|
|
155
|
+
builder.addEventListener('save', (e) => console.log('saved', e.detail)) // { uuid, name }
|
|
156
|
+
builder.addEventListener('error', (e) => console.error(e.detail)) // { code, message }
|
|
157
|
+
</script>
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
Size the element yourself — in `inline` mode it fills the box you give it.
|
|
161
|
+
|
|
162
|
+
### React
|
|
163
|
+
|
|
164
|
+
```tsx
|
|
165
|
+
import { TemplateBuilder } from '@scaleflex/template-builder/react'
|
|
166
|
+
|
|
167
|
+
<TemplateBuilder
|
|
168
|
+
baseUrl="https://<deployment>"
|
|
169
|
+
token={token}
|
|
170
|
+
sassKey={sassKey}
|
|
171
|
+
sessionUuid={sessionUuid}
|
|
172
|
+
templateId={templateId}
|
|
173
|
+
style={{ height: 800 }}
|
|
174
|
+
onSave={(data) => console.log(data)}
|
|
175
|
+
/>
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
---
|
|
179
|
+
|
|
180
|
+
## Modes
|
|
181
|
+
|
|
182
|
+
### DAM-backed (default)
|
|
183
|
+
|
|
184
|
+
`template-id` is a Filerobot file uuid. The app loads the template itself, and
|
|
185
|
+
Save uploads a new version and reports the resulting uuid on the `save` event.
|
|
186
|
+
Leave `template-id` empty to open the new-template flow.
|
|
187
|
+
|
|
188
|
+
### Stateless
|
|
189
|
+
|
|
190
|
+
Set `stateless` to keep the document entirely on your side: you pass the
|
|
191
|
+
template in, the user edits it, and you get the edited template back. Nothing is
|
|
192
|
+
stored on the Scaleflex side, so your app keeps its own storage, versioning,
|
|
193
|
+
tenancy and access control, and `template-id` becomes an opaque string that is
|
|
194
|
+
echoed back untouched.
|
|
195
|
+
|
|
196
|
+
#### The template comes from your API
|
|
197
|
+
|
|
198
|
+
The whole flow is three steps: **fetch the XML from your endpoint, pass it in,
|
|
199
|
+
take the edited XML back out.** Nothing is stored on the Scaleflex side at any
|
|
200
|
+
point.
|
|
201
|
+
|
|
202
|
+
```html
|
|
203
|
+
<sfx-template-builder
|
|
204
|
+
stateless
|
|
205
|
+
base-url="https://<deployment>"
|
|
206
|
+
token="FILEROBOT_TOKEN"
|
|
207
|
+
sass-key="SASS_KEY"
|
|
208
|
+
session-uuid="SESSION_UUID"
|
|
209
|
+
style="display:block;height:800px"
|
|
210
|
+
></sfx-template-builder>
|
|
211
|
+
|
|
212
|
+
<script type="module">
|
|
213
|
+
const builder = document.querySelector('sfx-template-builder')
|
|
214
|
+
const id = 'your-own-id-42'
|
|
215
|
+
|
|
216
|
+
// 1 — get the template XML from your API.
|
|
217
|
+
const res = await fetch(`/api/templates/${id}`)
|
|
218
|
+
const { content, name, templateQuery } = await res.json()
|
|
219
|
+
|
|
220
|
+
// 2 — pass it in. `content` is a property, never an attribute: templates
|
|
221
|
+
// routinely exceed what fits in markup or a URL, which is also why the
|
|
222
|
+
// widget delivers it over postMessage rather than the iframe src.
|
|
223
|
+
// `templateQuery` is what you stored on the last save; it reopens the
|
|
224
|
+
// template on the same layout and variable values.
|
|
225
|
+
builder.load({ templateId: id, name, content, templateQuery })
|
|
226
|
+
|
|
227
|
+
// 3 — take the edited template back out and store it yourself.
|
|
228
|
+
builder.addEventListener('save', async (e) => {
|
|
229
|
+
const { templateId, content, name, templateQuery } = e.detail
|
|
230
|
+
|
|
231
|
+
const ok = await fetch(`/api/templates/${templateId}`, {
|
|
232
|
+
method: 'PUT',
|
|
233
|
+
headers: { 'content-type': 'application/json' },
|
|
234
|
+
body: JSON.stringify({ content, name, templateQuery }),
|
|
235
|
+
}).then((r) => r.ok)
|
|
236
|
+
|
|
237
|
+
// Tell the editor whether it landed — see Reporting a failed save.
|
|
238
|
+
builder.confirmSave(ok)
|
|
239
|
+
})
|
|
240
|
+
|
|
241
|
+
// Loading a different template discards unsaved edits — check before you do.
|
|
242
|
+
builder.addEventListener('dirtychange', (e) => {
|
|
243
|
+
unsavedBanner.hidden = !e.detail.isDirty
|
|
244
|
+
})
|
|
245
|
+
</script>
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
`templateId` is **your** identifier, not a Filerobot uuid. The app never
|
|
249
|
+
resolves it against anything — it is carried alongside the content and handed
|
|
250
|
+
straight back on save, so use whatever key maps to your own record.
|
|
251
|
+
|
|
252
|
+
The [demo page](./demo) runs exactly this against a real HTTP endpoint and logs
|
|
253
|
+
every call, so you can watch the XML cross the boundary in both directions.
|
|
254
|
+
|
|
255
|
+
React — `content` is a prop, and the outcome of `onSave` is reported back
|
|
256
|
+
automatically:
|
|
257
|
+
|
|
258
|
+
```tsx
|
|
259
|
+
function TemplateEditor({ id }: { id: string }) {
|
|
260
|
+
const [tpl, setTpl] = useState<{ content: string; name: string } | null>(null)
|
|
261
|
+
|
|
262
|
+
// 1 — get the template XML from your API.
|
|
263
|
+
useEffect(() => {
|
|
264
|
+
fetch(`/api/templates/${id}`).then((r) => r.json()).then(setTpl)
|
|
265
|
+
}, [id])
|
|
266
|
+
|
|
267
|
+
if (!tpl) return null
|
|
268
|
+
|
|
269
|
+
return (
|
|
270
|
+
<TemplateBuilder
|
|
271
|
+
stateless
|
|
272
|
+
baseUrl="https://<deployment>"
|
|
273
|
+
token={token}
|
|
274
|
+
sassKey={sassKey}
|
|
275
|
+
sessionUuid={sessionUuid}
|
|
276
|
+
// 2 — pass it in.
|
|
277
|
+
templateId={id}
|
|
278
|
+
content={tpl.content}
|
|
279
|
+
templateName={tpl.name}
|
|
280
|
+
style={{ height: 800 }}
|
|
281
|
+
// 3 — take it back out. Returning false (or throwing) tells the editor
|
|
282
|
+
// the save failed, and it restores its unsaved-changes flag.
|
|
283
|
+
onSave={async (data) => {
|
|
284
|
+
const res = await fetch(`/api/templates/${data.templateId}`, {
|
|
285
|
+
method: 'PUT',
|
|
286
|
+
headers: { 'content-type': 'application/json' },
|
|
287
|
+
body: JSON.stringify(data),
|
|
288
|
+
})
|
|
289
|
+
return res.ok
|
|
290
|
+
}}
|
|
291
|
+
onDirtyChange={({ isDirty }) => setHasUnsavedEdits(isDirty)}
|
|
292
|
+
/>
|
|
293
|
+
)
|
|
294
|
+
}
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
#### About `templateQuery`
|
|
298
|
+
|
|
299
|
+
`save` returns a `templateQuery` next to the content. It is the query string
|
|
300
|
+
that renders the template at its defaults — layout, variable values, locale —
|
|
301
|
+
and you append it to the template's CDN URL when you want an image out:
|
|
302
|
+
|
|
303
|
+
```
|
|
304
|
+
https://<tenant>.filerobot.com/<path>/<template>.fdt?<templateQuery>&force_format=png
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
**Persist it alongside `content`.** DAM-backed saves store it as file metadata;
|
|
308
|
+
a stateless host that drops it gets renders that fall back to whatever the XML
|
|
309
|
+
alone implies.
|
|
310
|
+
|
|
311
|
+
**Pass it back in.** `load()` takes a `templateQuery` too, so the query you
|
|
312
|
+
stored reopens the template on the render it was left at:
|
|
313
|
+
|
|
314
|
+
```js
|
|
315
|
+
builder.load({ templateId, name, content, templateQuery })
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
Omit it and the editor falls back to the `default=` attributes in the XML —
|
|
319
|
+
a different render whenever the query overrode any of them, which is the usual
|
|
320
|
+
case for a template driven by metadata or URL variables.
|
|
321
|
+
|
|
322
|
+
It is applied as display state, not as an edit: it selects the layout and fills
|
|
323
|
+
variable values without marking the document dirty, so opening a template and
|
|
324
|
+
closing it again is not an unsaved change. Entries naming a variable or layout
|
|
325
|
+
the document no longer defines are ignored rather than treated as an error, so
|
|
326
|
+
a stale query still opens the template.
|
|
327
|
+
|
|
328
|
+
Known limits on the way in — all of them cases where the editor's preview can
|
|
329
|
+
differ from what the same query renders on the CDN:
|
|
330
|
+
|
|
331
|
+
- **`$locale` is not applied.** Regional selection is seeded from Hub project
|
|
332
|
+
info, which a guest embed never receives.
|
|
333
|
+
- **An explicitly empty value (`$headline=`) does not clear a variable.** The
|
|
334
|
+
editor resolves `value || default`, so it falls back to the XML default,
|
|
335
|
+
while the backend treats explicit-empty as empty. Omit the key instead of
|
|
336
|
+
sending it blank.
|
|
337
|
+
- **Metadata-sourced variables behave differently by session.** Under a guest
|
|
338
|
+
(security-template) session the query's value stands. Under a Hub session the
|
|
339
|
+
editor re-resolves them from the linked asset shortly after load, overwriting
|
|
340
|
+
it — that automation is the DAM behaviour and takes precedence.
|
|
341
|
+
- **`$layout_color` is ignored**; the layout's own colour is used. Queries this
|
|
342
|
+
editor produced always agree, so this only bites a hand-built one.
|
|
343
|
+
- **`layout`, `locale` and `force_format` are reserved.** A variable whose slug
|
|
344
|
+
is one of those cannot be addressed by a query.
|
|
345
|
+
|
|
346
|
+
**Swapping templates.** Assigning a new `content`, `templateId`, `templateName`
|
|
347
|
+
or `templateQuery` reloads the editor and **discards unsaved edits without
|
|
348
|
+
prompting** — the host is treated as authoritative. Watch `dirtychange` (or read
|
|
349
|
+
the `isDirty` property) and ask the user first. Re-assigning an identical
|
|
350
|
+
template is a no-op, so a host re-render can't destroy work by accident. Note
|
|
351
|
+
that the id is part of that identity check: moving between two records whose
|
|
352
|
+
content is byte-identical does reload, so a save can never land on the record
|
|
353
|
+
you navigated away from.
|
|
354
|
+
|
|
355
|
+
#### What stateless does *not* remove
|
|
356
|
+
|
|
357
|
+
Statelessness applies to the **document**, not to the infrastructure. The editor
|
|
358
|
+
still needs an authenticated Filerobot tenant for:
|
|
359
|
+
|
|
360
|
+
- **text rendering** — text layers are rasterized server-side,
|
|
361
|
+
- **fonts** — custom fonts are served from the tenant's `.studio/fonts/`,
|
|
362
|
+
- **asset browsing and upload** — image layers are picked from the DAM,
|
|
363
|
+
- **metadata variables and regional settings**.
|
|
364
|
+
|
|
365
|
+
What it does let you avoid is mapping *your users* onto Scaleflex identities:
|
|
366
|
+
point every embed at one service tenant with a single machine credential minted
|
|
367
|
+
server-side, and do all per-user permission work yourself.
|
|
368
|
+
|
|
369
|
+
Images referenced by a template may live on your own CDN, but the render
|
|
370
|
+
service only fetches from allowlisted hosts — add yours to the deployment's
|
|
371
|
+
`RENDER_ALLOWED_HOSTS_EXTRA`.
|
|
372
|
+
|
|
373
|
+
### Reporting a failed save
|
|
374
|
+
|
|
375
|
+
The editor clears its unsaved-changes state as soon as it posts `save` —
|
|
376
|
+
delivery of a postMessage says nothing about whether you stored anything. Tell
|
|
377
|
+
it when you didn't, and it restores the dirty flag and warns the user instead of
|
|
378
|
+
showing a failed write as saved:
|
|
379
|
+
|
|
380
|
+
```js
|
|
381
|
+
builder.addEventListener('save', async (e) => {
|
|
382
|
+
try {
|
|
383
|
+
await yourApi.saveTemplateXml(e.detail.templateId, e.detail.content)
|
|
384
|
+
builder.confirmSave(true)
|
|
385
|
+
} catch (err) {
|
|
386
|
+
builder.confirmSave(false, 'Could not save — please try again.')
|
|
387
|
+
}
|
|
388
|
+
})
|
|
389
|
+
```
|
|
390
|
+
|
|
391
|
+
The React wrapper does this for you: return (or resolve to) `false` from
|
|
392
|
+
`onSave`, or throw, and the failure is reported automatically.
|
|
393
|
+
|
|
394
|
+
Acking is optional. A host that never calls `confirmSave` keeps the optimistic
|
|
395
|
+
behaviour, so this is additive — but only `ok: false` carries information, and
|
|
396
|
+
without it a failed write is invisible to the user.
|
|
397
|
+
|
|
398
|
+
---
|
|
399
|
+
|
|
400
|
+
## Configuration
|
|
401
|
+
|
|
402
|
+
### Attributes & properties
|
|
403
|
+
|
|
404
|
+
| Attribute / property | Required | Description |
|
|
405
|
+
| --- | --- | --- |
|
|
406
|
+
| `base-url` / `baseUrl` | yes | Origin of the design-templates-app deployment |
|
|
407
|
+
| `token` | yes | Filerobot token (`ftoken`) |
|
|
408
|
+
| `sass-key` / `sassKey` | session auth | Project sass key |
|
|
409
|
+
| `session-uuid` / `sessionUuid` | session auth | Hub session uuid |
|
|
410
|
+
| `sec-template` / `secTemplate` | guest auth | Filerobot security-template key, *instead of* `sass-key` + `session-uuid`. Stateless only — see [Authentication](#authentication) |
|
|
411
|
+
| `company-uuid`, `project-uuid` | no | Company / project scoping (session auth only) |
|
|
412
|
+
| `template-id` / `templateId` | no | DAM-backed: Filerobot uuid to edit, empty opens the new-template flow. Stateless: opaque id echoed back on `save` |
|
|
413
|
+
| `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` |
|
|
415
|
+
| `content` (property only) | stateless | The template to edit, as `.fdt` XML. Assigning a new value loads it into a running editor |
|
|
416
|
+
| `template-name` / `templateName` | no | Stateless: header title |
|
|
417
|
+
| `template-query` / `templateQuery` | no | Stateless: the render to open on — the `templateQuery` from the last save. Empty uses the XML's `default=` values. |
|
|
418
|
+
| `brand-color` / `brandColor` | no | Accent colour for the editor chrome, `#rgb` / `#rrggbb` |
|
|
419
|
+
| `theme` | no | `light`, `dark` or `auto` |
|
|
420
|
+
| `ready-timeout` / `readyTimeout` | no | Ms to wait for the app handshake before `error` (default 20000, 0 disables) |
|
|
421
|
+
|
|
422
|
+
### Authentication
|
|
423
|
+
|
|
424
|
+
Two credentials get you in. Both go into the page from your server; neither is
|
|
425
|
+
something to hardcode in a public bundle.
|
|
426
|
+
|
|
427
|
+
| | Hub session | Security template |
|
|
428
|
+
| --- | --- | --- |
|
|
429
|
+
| Attributes | `token` + `sass-key` + `session-uuid` | `token` + `sec-template` |
|
|
430
|
+
| Needs a Hub account per embed | yes | **no** |
|
|
431
|
+
| Storage modes | DAM-backed and stateless | **stateless only** |
|
|
432
|
+
| Metadata fields, regional variants, project branding | yes | **empty** |
|
|
433
|
+
| Rendering, fonts, asset picker | yes | yes, within the template's scope |
|
|
434
|
+
|
|
435
|
+
#### Hub session
|
|
436
|
+
|
|
437
|
+
Mint the Hub session **server-side** and inject `session-uuid` / `sass-key` /
|
|
438
|
+
`token` into your page. Issue short-lived per-user sessions; never embed a
|
|
439
|
+
long-lived master credential in client-side code.
|
|
440
|
+
|
|
441
|
+
#### Security template (guest auth)
|
|
442
|
+
|
|
443
|
+
A Filerobot **security template** is a named, permission-scoped credential you
|
|
444
|
+
define once in your Filerobot project — the same guest-auth mechanism the other
|
|
445
|
+
Scaleflex widgets use. Hand one to the widget and no Hub session is involved at
|
|
446
|
+
all:
|
|
447
|
+
|
|
448
|
+
```html
|
|
449
|
+
<sfx-template-builder
|
|
450
|
+
base-url="https://templates.example.com"
|
|
451
|
+
token="PROJECT_TOKEN"
|
|
452
|
+
sec-template="SEC_TEMPLATE_KEY"
|
|
453
|
+
stateless
|
|
454
|
+
></sfx-template-builder>
|
|
455
|
+
```
|
|
456
|
+
|
|
457
|
+
```jsx
|
|
458
|
+
<TemplateBuilder
|
|
459
|
+
stateless
|
|
460
|
+
baseUrl="https://templates.example.com"
|
|
461
|
+
token={projectToken}
|
|
462
|
+
secTemplate={secTemplateKey}
|
|
463
|
+
templateId={id}
|
|
464
|
+
content={xml}
|
|
465
|
+
onSave={async (data) => (await saveToYourApi(data)).ok}
|
|
466
|
+
/>
|
|
467
|
+
```
|
|
468
|
+
|
|
469
|
+
The app exchanges the key for a short-lived access key itself and renews it when
|
|
470
|
+
it expires, so the embed does not die mid-session.
|
|
471
|
+
|
|
472
|
+
**What it costs.** A security template authenticates *nobody in particular*: no
|
|
473
|
+
user identity, no Hub project behind it. That has consequences worth knowing
|
|
474
|
+
before you pick it:
|
|
475
|
+
|
|
476
|
+
- **Stateless only.** Setting `sec-template` without `stateless` is a config
|
|
477
|
+
error — the widget reports `error` with code `invalid-config` and never mounts
|
|
478
|
+
the iframe. The dashboard and the DAM-backed editor keep requiring a session.
|
|
479
|
+
- **Hub-project features come back empty** — metadata fields, regional variants
|
|
480
|
+
and dynamic fields have no model to read, and project branding does not apply
|
|
481
|
+
(theme the chrome with `brand-color` / `theme` instead).
|
|
482
|
+
- **Its scope is the app's scope.** Grant the template `LIST` on the folders you
|
|
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`.
|
|
490
|
+
|
|
491
|
+
### Origin registration
|
|
492
|
+
|
|
493
|
+
Your page's origin must be in the deployment's `frame-ancestors` allowlist
|
|
494
|
+
(`NEXT_PUBLIC_TRUSTED_HUB_ORIGINS`), otherwise the browser refuses to render the
|
|
495
|
+
iframe and the widget reports `handshake-timeout`.
|
|
496
|
+
|
|
497
|
+
### Cookies
|
|
498
|
+
|
|
499
|
+
The app stores auth in partitioned (CHIPS) cookies scoped to your site. Browsers
|
|
500
|
+
without CHIPS support that block third-party cookies will fail with `auth` or
|
|
501
|
+
`handshake-timeout`.
|
|
502
|
+
|
|
503
|
+
---
|
|
504
|
+
|
|
505
|
+
## Public Methods
|
|
506
|
+
|
|
507
|
+
| Method | Description |
|
|
508
|
+
| --- | --- |
|
|
509
|
+
| `open(templateId?)` | Open the editor, loading the iframe. Optionally switch template first. |
|
|
510
|
+
| `close()` | Close the editor and unload the iframe. Does not emit `close`. |
|
|
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). |
|
|
512
|
+
| `confirmSave(ok, message?)` | Stateless: report whether you persisted the content. See [Reporting a failed save](#reporting-a-failed-save). |
|
|
513
|
+
|
|
514
|
+
**Read-only properties:** `status` (`idle` \| `loading` \| `ready` \| `error`),
|
|
515
|
+
`isDirty` (stateless; unsaved edits pending).
|
|
516
|
+
|
|
517
|
+
In React these are reached through a ref — see [React API](#react-api).
|
|
518
|
+
|
|
519
|
+
## Events
|
|
520
|
+
|
|
521
|
+
All events are `CustomEvent`s; the payload is in `detail`.
|
|
522
|
+
|
|
523
|
+
| Event | `detail` | Fired when |
|
|
524
|
+
| --- | --- | --- |
|
|
525
|
+
| `ready` | — | The editor mounted and auth validated. Clears the handshake timeout. |
|
|
526
|
+
| `open` | — | The editor UI opened. |
|
|
527
|
+
| `save` | `{ uuid, name }` (DAM-backed) or `{ templateId, content, name, templateQuery }` (stateless) | The user saved. |
|
|
528
|
+
| `dirtychange` | `{ isDirty }` | Stateless: the unsaved-changes flag flipped. |
|
|
529
|
+
| `close` | — | The user left the editor, or it unmounted. |
|
|
530
|
+
| `error` | `{ code, message? }` | See below. |
|
|
531
|
+
|
|
532
|
+
`error` codes:
|
|
533
|
+
|
|
534
|
+
| Code | Means |
|
|
535
|
+
| --- | --- |
|
|
536
|
+
| `auth` | The app could not authenticate — blocked cookies, an expired session, or a security template the Filerobot API refused. |
|
|
537
|
+
| `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 iframe never mounts. |
|
|
539
|
+
| `handshake-timeout` | No ready signal in time — usually a missing `frame-ancestors` entry or blocked third-party cookies. |
|
|
540
|
+
| `invalid-base-url` | `base-url` is not a URL. |
|
|
541
|
+
| `unknown` | Anything the app reported that this version does not name. |
|
|
542
|
+
|
|
543
|
+
`close` fires when the user leaves the editor as well as when it unmounts. In
|
|
544
|
+
`modal` mode the element tears its overlay down; in `inline` mode it is yours to
|
|
545
|
+
act on — the editor never navigates itself anywhere.
|
|
546
|
+
|
|
547
|
+
## React API
|
|
548
|
+
|
|
549
|
+
Props mirror the attributes in camelCase, plus `className` and `style`.
|
|
550
|
+
Callbacks: `onReady`, `onOpen`, `onSave`, `onDirtyChange`, `onClose`, `onError`.
|
|
551
|
+
|
|
552
|
+
The credential props are a discriminated union, so the two
|
|
553
|
+
[auth modes](#authentication) are enforced at compile time: `sassKey` +
|
|
554
|
+
`sessionUuid`, or `secTemplate` with `stateless` — mixing them, or passing
|
|
555
|
+
`secTemplate` without `stateless`, is a type error rather than a runtime one.
|
|
556
|
+
|
|
557
|
+
`onSave` may return `false` or a promise; see
|
|
558
|
+
[Reporting a failed save](#reporting-a-failed-save).
|
|
559
|
+
|
|
560
|
+
The component forwards a ref to the underlying element, which is how you reach
|
|
561
|
+
the [imperative API](#public-methods) — required for `mode="modal"`, which
|
|
562
|
+
renders nothing until `open()` is called:
|
|
563
|
+
|
|
564
|
+
```tsx
|
|
565
|
+
import { useRef } from 'react'
|
|
566
|
+
import { TemplateBuilder } from '@scaleflex/template-builder/react'
|
|
567
|
+
import type { SfxTemplateBuilder } from '@scaleflex/template-builder'
|
|
568
|
+
|
|
569
|
+
const builder = useRef<SfxTemplateBuilder>(null)
|
|
570
|
+
|
|
571
|
+
<TemplateBuilder ref={builder} mode="modal" baseUrl={...} {...auth} />
|
|
572
|
+
<button onClick={() => builder.current?.open('tpl-1')}>Edit template</button>
|
|
573
|
+
```
|
|
574
|
+
|
|
575
|
+
`react` and `react-dom` (>= 18) are optional peer dependencies — the package
|
|
576
|
+
works without React installed.
|
|
577
|
+
|
|
578
|
+
---
|
|
579
|
+
|
|
580
|
+
## Theming
|
|
581
|
+
|
|
582
|
+
### Brand Color
|
|
583
|
+
|
|
584
|
+
`brand-color` restyles the editor chrome — buttons, focus rings, highlights,
|
|
585
|
+
selected states — from a single accent colour.
|
|
586
|
+
|
|
587
|
+
```html
|
|
588
|
+
<sfx-template-builder brand-color="#FF6600" ...></sfx-template-builder>
|
|
589
|
+
```
|
|
590
|
+
|
|
591
|
+
- Must be `#rgb` or `#rrggbb`. Anything else is rejected by the app and the
|
|
592
|
+
default Scaleflex accent is kept — the value ends up inside a stylesheet, so
|
|
593
|
+
the shape is enforced rather than escaped.
|
|
594
|
+
- Text drawn *on top of* the brand colour (primary button labels) is chosen for
|
|
595
|
+
you, white or near-black, by contrast ratio. A pale brand colour gets dark
|
|
596
|
+
labels rather than invisible ones.
|
|
597
|
+
- Surfaces, borders and body text keep the design system's neutrals; only the
|
|
598
|
+
accent ramp follows your colour.
|
|
599
|
+
|
|
600
|
+
**Pick a colour that is readable on white.** The design system uses one accent
|
|
601
|
+
token for both filled surfaces and link text, so a very pale brand colour gives
|
|
602
|
+
you a good-looking button and low-contrast links. Tinted accents are darkened
|
|
603
|
+
automatically where they are unambiguously text, but a link rendered in the
|
|
604
|
+
accent colour itself cannot be — darkening it would mean not showing your brand
|
|
605
|
+
colour on the button either. Mid-tone colours (roughly, anything that passes
|
|
606
|
+
4.5:1 on white) avoid the trade-off entirely.
|
|
607
|
+
|
|
608
|
+
### Colour scheme
|
|
609
|
+
|
|
610
|
+
`theme` is `light`, `dark` or `auto` (follows the viewer's OS setting). It
|
|
611
|
+
overrides the user's own stored preference, which in an embed lives in
|
|
612
|
+
partitioned storage your page cannot reach.
|
|
613
|
+
|
|
614
|
+
**Theming applies to the editor UI, not the template.** Colours in the design
|
|
615
|
+
itself live in the document and are edited through the builder — a brand colour
|
|
616
|
+
never changes what gets rendered or exported.
|
|
617
|
+
|
|
618
|
+
---
|
|
619
|
+
|
|
620
|
+
## Browser Support
|
|
621
|
+
|
|
622
|
+
| Browser | Minimum version |
|
|
623
|
+
|---|---|
|
|
624
|
+
| Chrome | 114+ |
|
|
625
|
+
| Firefox | 131+ |
|
|
626
|
+
| Safari | 18.4+ |
|
|
627
|
+
| Edge (Chromium) | 114+ |
|
|
628
|
+
|
|
629
|
+
Requires native support for Custom Elements v1, Shadow DOM, and ES2020+.
|
|
630
|
+
Internet Explorer is **not** supported.
|
|
631
|
+
|
|
632
|
+
The floors are higher than a plain web component would need because cross-site
|
|
633
|
+
embedding depends on **partitioned cookies (CHIPS)**. On an older browser that
|
|
634
|
+
blocks third-party cookies the editor cannot authenticate and the widget reports
|
|
635
|
+
`auth` / `handshake-timeout`. Same-site embeds work further back.
|
|
636
|
+
|
|
637
|
+
---
|
|
638
|
+
|
|
639
|
+
## Development
|
|
640
|
+
|
|
641
|
+
```bash
|
|
642
|
+
yarn dev:demo # demo site (expects the app on http://localhost:3000)
|
|
643
|
+
yarn test # vitest
|
|
644
|
+
yarn typecheck # tsc --noEmit
|
|
645
|
+
yarn build # dist/ — npm artifact (ESM + CJS + types)
|
|
646
|
+
yarn build:cdn # dist-cdn/template-builder.min.js — self-registering bundle
|
|
647
|
+
yarn build:demo # demo-dist/ — the static demo site
|
|
648
|
+
yarn preview:demo # serve demo-dist/ as a client would
|
|
649
|
+
yarn build:all # build + build:cdn
|
|
650
|
+
```
|
|
651
|
+
|
|
652
|
+
From the repo root, `yarn widget <script>` runs any of these, and the root
|
|
653
|
+
`yarn test` / `yarn typecheck` include this package.
|
|
654
|
+
|
|
655
|
+
### The demo site
|
|
656
|
+
|
|
657
|
+
`demo/` is two pages: `index.html`, the live widget with a configuration panel
|
|
658
|
+
and a wire log, and `docs.html`, this README. `scripts/vite-plugin-docs.mjs`
|
|
659
|
+
renders the markdown at build time — the shipped page is static HTML with no
|
|
660
|
+
markdown runtime, and this file stays the only place the documentation is
|
|
661
|
+
written. Headings get GitHub's anchor slugs so the links above keep resolving,
|
|
662
|
+
the "Table of Contents" section is replaced by a generated sidebar, and editing
|
|
663
|
+
the README reloads the dev server.
|
|
664
|
+
|
|
665
|
+
`yarn build:demo` emits the pair to `demo-dist/` with relative asset paths, so
|
|
666
|
+
the folder can be zipped and handed to a client, or dropped behind any static
|
|
667
|
+
host at any path — GitHub Pages, S3, a subdirectory of an existing site. The
|
|
668
|
+
only thing it needs at runtime is a reachable `design-templates-app` for the
|
|
669
|
+
demo page's Base URL field to point at.
|
|
670
|
+
|
|
671
|
+
The demo page takes its XML from a URL, from a paste, or from a picker listing
|
|
672
|
+
the `.fdt` files in the project its credentials point at, which fills the id,
|
|
673
|
+
name and template query from the file you choose. Both auth modes list: a
|
|
674
|
+
security template is exchanged for a sass key first, so the listing sees
|
|
675
|
+
whatever that template's scope allows. The picker is demo scaffolding standing
|
|
676
|
+
in for a host's own template store — the widget itself only ever sees the XML
|
|
677
|
+
the page hands it.
|
|
678
|
+
|
|
679
|
+
End-to-end cover for the embed boundary lives in the app repo at
|
|
680
|
+
`e2e/embed-widget.spec.ts` — a cross-origin host page loads the built CDN
|
|
681
|
+
bundle, pulls template XML from its own API and gets the edit back. It needs the
|
|
682
|
+
app running with the fixture origin allowlisted:
|
|
683
|
+
|
|
684
|
+
```bash
|
|
685
|
+
yarn build:widget
|
|
686
|
+
export NEXT_PUBLIC_TRUSTED_HUB_ORIGINS=http://127.0.0.1:4321
|
|
687
|
+
yarn build && yarn start
|
|
688
|
+
yarn test:e2e:embed
|
|
689
|
+
```
|
|
690
|
+
|
|
691
|
+
The postMessage protocol lives in `src/protocol.ts` and is shared with the app
|
|
692
|
+
via the `./protocol` export, so the two sides cannot drift. Message *values* are
|
|
693
|
+
wire format: never change an existing string, only add new messages, so an older
|
|
694
|
+
widget keeps working against a newer app deployment and vice versa.
|
|
695
|
+
|
|
696
|
+
## Release
|
|
697
|
+
|
|
698
|
+
```bash
|
|
699
|
+
yarn release # patch bump (0.1.0 → 0.1.1)
|
|
700
|
+
yarn release -- minor # minor bump (0.1.0 → 0.2.0)
|
|
701
|
+
yarn release -- major # major bump (0.1.0 → 1.0.0)
|
|
702
|
+
```
|
|
703
|
+
|
|
704
|
+
This handles the full pipeline: version bump, CDN build + upload, library build,
|
|
705
|
+
npm publish, git commit + tag + push. It needs a `.env.local` in this package:
|
|
706
|
+
|
|
707
|
+
```env
|
|
708
|
+
FILEROBOT_CDN_TOKEN=scaleflex
|
|
709
|
+
FILEROBOT_CDN_SECU=<secu key>
|
|
710
|
+
FILEROBOT_CDN_FOLDER=/plugins/scaleflex/template-builder/{version}/
|
|
711
|
+
```
|
|
712
|
+
|
|
713
|
+
Update [CHANGELOG.md](./CHANGELOG.md) before releasing.
|
|
714
|
+
|
|
715
|
+
---
|
|
716
|
+
|
|
717
|
+
## Claude Code Integration
|
|
718
|
+
|
|
719
|
+
If you use [Claude Code](https://docs.anthropic.com/en/docs/claude-code), this
|
|
720
|
+
package ships a ready-made skill that walks Claude through adding the builder to
|
|
721
|
+
your project — choosing DAM-backed vs stateless storage, wiring the save round
|
|
722
|
+
trip, theming, and registering your embedding origin.
|
|
723
|
+
|
|
724
|
+
### Option 1: Project-level (recommended)
|
|
725
|
+
|
|
726
|
+
Copy the skill into your project so everyone on the team gets it:
|
|
727
|
+
|
|
728
|
+
```bash
|
|
729
|
+
mkdir -p .claude/skills/integrate-template-builder
|
|
730
|
+
cp node_modules/@scaleflex/template-builder/.claude/skills/integrate-template-builder/SKILL.md \
|
|
731
|
+
.claude/skills/integrate-template-builder/SKILL.md
|
|
732
|
+
```
|
|
733
|
+
|
|
734
|
+
Commit the `.claude/skills/` directory to version control.
|
|
735
|
+
|
|
736
|
+
### Option 2: Global (personal)
|
|
737
|
+
|
|
738
|
+
Install it once for all your projects:
|
|
739
|
+
|
|
740
|
+
```bash
|
|
741
|
+
mkdir -p ~/.claude/skills/integrate-template-builder
|
|
742
|
+
cp node_modules/@scaleflex/template-builder/.claude/skills/integrate-template-builder/SKILL.md \
|
|
743
|
+
~/.claude/skills/integrate-template-builder/SKILL.md
|
|
744
|
+
```
|
|
745
|
+
|
|
746
|
+
### Usage
|
|
747
|
+
|
|
748
|
+
Type `/integrate-template-builder` in Claude Code and it will take you through
|
|
749
|
+
the whole integration, tailored to your stack.
|
|
750
|
+
|
|
751
|
+
---
|
|
752
|
+
|
|
753
|
+
## License
|
|
754
|
+
|
|
755
|
+
**PROPRIETARY** — All Rights Reserved.
|
|
756
|
+
|
|
757
|
+
Copyright © 2025 [Scaleflex SAS](https://www.scaleflex.com).
|
|
758
|
+
|
|
759
|
+
This software and associated documentation are the exclusive property of
|
|
760
|
+
Scaleflex SAS. No part of this software may be copied, modified, distributed,
|
|
761
|
+
sublicensed, sold, or otherwise made available to any third party without prior
|
|
762
|
+
written permission from Scaleflex SAS.
|
|
763
|
+
|
|
764
|
+
This package is distributed via npm solely for the convenience of licensed
|
|
765
|
+
customers. Installing or using this package does not grant any licence to use
|
|
766
|
+
the software. Use is permitted only under a separate written licence agreement
|
|
767
|
+
with Scaleflex SAS.
|
|
768
|
+
|
|
769
|
+
For licensing enquiries, contact [sales@scaleflex.com](mailto:sales@scaleflex.com).
|
|
770
|
+
|
|
771
|
+
---
|
|
772
|
+
|
|
773
|
+
<p align="center">
|
|
774
|
+
Made with care by <a href="https://www.scaleflex.com">Scaleflex</a>
|
|
775
|
+
</p>
|