@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/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** &mdash; All Rights Reserved.
756
+
757
+ Copyright &copy; 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>