@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.
@@ -0,0 +1,206 @@
1
+ import { LitElement, nothing, type PropertyValues } from 'lit';
2
+ import { type BuilderContentData, type BuilderDirtyData, type BuilderErrorData, type BuilderSaveData, type BuilderTheme } from './protocol';
3
+ export type TemplateBuilderStatus = 'idle' | 'loading' | 'ready' | 'error';
4
+ /**
5
+ * `save` payload. Which variant arrives follows the mode the element was
6
+ * configured in:
7
+ * - DAM-backed (default) — `BuilderSaveData`; the app uploaded the template and
8
+ * reports the resulting `uuid`.
9
+ * - `stateless` — `BuilderContentData`; nothing was stored, and `content` is
10
+ * the edited template for the host to persist.
11
+ */
12
+ export type TemplateBuilderSaveDetail = BuilderSaveData | BuilderContentData | undefined;
13
+ export interface TemplateBuilderEventMap {
14
+ ready: CustomEvent<void>;
15
+ open: CustomEvent<void>;
16
+ close: CustomEvent<void>;
17
+ save: CustomEvent<TemplateBuilderSaveDetail>;
18
+ error: CustomEvent<BuilderErrorData>;
19
+ dirtychange: CustomEvent<BuilderDirtyData>;
20
+ }
21
+ /**
22
+ * `<sfx-template-builder>` — embeds the Filerobot design-templates builder.
23
+ *
24
+ * The element owns an iframe pointed at a design-templates-app deployment,
25
+ * passes auth via URL params (converted to cookies by the app's proxy), and
26
+ * translates the app's postMessage protocol into DOM CustomEvents:
27
+ * `ready`, `open`, `close`, `save`, `error`.
28
+ *
29
+ * Required: `base-url`, `token`, and one of two credentials:
30
+ * - `sass-key` + `session-uuid` — a Hub session. Full features.
31
+ * - `sec-template` — a Filerobot security-template key. No Hub session needed,
32
+ * but it only works with `stateless`, and Hub-project features (metadata
33
+ * fields, regional variants, project branding) come back empty.
34
+ *
35
+ * In `inline` mode the editor loads as soon as config is complete and fills
36
+ * the host element (size it explicitly). In `modal` mode nothing renders
37
+ * until `open()` is called; the editor then covers the viewport.
38
+ *
39
+ * Two ways to supply the template:
40
+ * - **DAM-backed** (default) — set `template-id` to a Filerobot file uuid. The
41
+ * app loads and saves it itself, and `save` reports the new uuid.
42
+ * - **Stateless** — set `stateless` and assign `content`. The element sends the
43
+ * template into the editor over postMessage and `save` returns the edited
44
+ * document; nothing is stored on the Scaleflex side, and `template-id` is
45
+ * just an opaque string echoed back. Rendering, fonts and asset browsing
46
+ * still use the session's Filerobot tenant.
47
+ *
48
+ * `brand-color` and `theme` restyle the editor chrome to match the host page.
49
+ * They do not touch the rendered template — its colours live in the document.
50
+ */
51
+ export declare class SfxTemplateBuilder extends LitElement {
52
+ static styles: import("lit").CSSResult;
53
+ /** Origin + optional path prefix of the design-templates-app deployment. */
54
+ baseUrl: string;
55
+ /** Filerobot token (`ftoken`). */
56
+ token: string;
57
+ sassKey: string;
58
+ sessionUuid: string;
59
+ /**
60
+ * Filerobot security-template key — the alternative to `sass-key` +
61
+ * `session-uuid` for hosts with no Hub session to hand over. Requires
62
+ * `stateless`, and degrades the features that come from the Hub project
63
+ * model (metadata fields, regional variants, project branding). When set it
64
+ * wins: neither `sass-key` nor `session-uuid` is passed to the app.
65
+ */
66
+ secTemplate: string;
67
+ companyUuid: string;
68
+ projectUuid: string;
69
+ /**
70
+ * DAM-backed mode: the Filerobot uuid to load; empty opens the new-template
71
+ * flow. Stateless mode: an opaque host id, echoed back on `save`.
72
+ */
73
+ templateId: string;
74
+ mode: 'inline' | 'modal';
75
+ /**
76
+ * Hand the template in and take it back out instead of letting the app read
77
+ * and write Filerobot. Requires `content`.
78
+ */
79
+ stateless: boolean;
80
+ /**
81
+ * Stateless mode: the template to edit, as `.fdt` XML. Property only — templates
82
+ * routinely exceed practical attribute/URL sizes, so it is never reflected.
83
+ * Assigning a different value while open loads it into the running editor.
84
+ */
85
+ content: string;
86
+ /** Stateless mode: display name for the editor header. */
87
+ templateName: string;
88
+ /**
89
+ * Stateless mode: the `template_query` to open on — the value handed back in
90
+ * the `save` payload. Pass back what you stored and the editor reopens on the
91
+ * same layout and variable values; leave it empty and the render falls back
92
+ * to the XML's own `default=` attributes.
93
+ */
94
+ templateQuery: string;
95
+ /**
96
+ * Accent colour for the editor chrome, as `#rgb` / `#rrggbb`. The app derives
97
+ * buttons, focus rings and highlights from it. Empty keeps the Scaleflex
98
+ * default. Themes the editor UI only — never the rendered template, whose
99
+ * colours live in the document.
100
+ */
101
+ brandColor: string;
102
+ /** Colour scheme for the editor chrome. Empty leaves the app's own default. */
103
+ theme: BuilderTheme | '';
104
+ /** Ms to wait for the app's ready signal before emitting `error`. 0 disables. */
105
+ readyTimeout: number;
106
+ private _status;
107
+ private _open;
108
+ private _src;
109
+ private _handshakeTimer?;
110
+ /**
111
+ * The app asked for content. Tracked because the request and the `content`
112
+ * assignment race: whichever lands second triggers the send.
113
+ */
114
+ private _contentRequested;
115
+ /**
116
+ * Identity of the template already delivered, so an unrelated re-render does
117
+ * not resend it and discard the user's edits. Covers the id and name too, not
118
+ * just the content: two host records can hold byte-identical templates, and
119
+ * resending only on content change would leave the app echoing a stale id
120
+ * back on save.
121
+ */
122
+ private _sentKey?;
123
+ /**
124
+ * The `baseUrl` value already reported as unparseable. `_computeSrc()` runs
125
+ * on every update cycle, so without this a bad URL re-emits `error` forever —
126
+ * once per render, since the error status it sets is already in place after
127
+ * the first.
128
+ */
129
+ private _reportedBadBaseUrl?;
130
+ /**
131
+ * Whether the sec-template-without-stateless mistake has been reported. Same
132
+ * reason as `_reportedBadBaseUrl`: `_computeSrc()` runs every update cycle
133
+ * and the error status it sets is already in place after the first pass.
134
+ */
135
+ private _reportedStatelessRequired;
136
+ private _isDirty;
137
+ get status(): TemplateBuilderStatus;
138
+ /**
139
+ * Stateless mode: whether the editor holds edits that have not been handed
140
+ * back yet. Check this before calling `load()` — a swap discards them.
141
+ * Always false in DAM-backed mode, where the app owns saving.
142
+ */
143
+ get isDirty(): boolean;
144
+ /** Open the editor (loads the iframe). Optionally switch template first. */
145
+ open(templateId?: string): void;
146
+ /** Close the editor and unload the iframe. Does not emit `close`. */
147
+ close(): void;
148
+ /**
149
+ * Stateless mode: load a template, opening the editor if needed. Equivalent
150
+ * to assigning `templateId` / `content` / `templateName` and calling `open()`.
151
+ */
152
+ load({ content, templateId, name, templateQuery, }: {
153
+ content: string;
154
+ templateId?: string;
155
+ name?: string;
156
+ templateQuery?: string;
157
+ }): void;
158
+ /**
159
+ * Stateless mode: report back whether a `save` was persisted on your side.
160
+ *
161
+ * Optional. The editor clears its unsaved-changes flag as soon as it hands
162
+ * the content over, so not calling this leaves the previous behaviour intact.
163
+ * Calling it with `false` is what earns something: the editor restores the
164
+ * dirty flag and tells the user, rather than showing a failed write as saved.
165
+ *
166
+ * No-op outside stateless mode, where the app did the saving and has nothing
167
+ * to hear back about.
168
+ */
169
+ confirmSave(ok: boolean, message?: string): void;
170
+ /**
171
+ * Whether the inline-implies-open decision has been made. It cannot be made
172
+ * in `connectedCallback`: frameworks insert the element first and assign
173
+ * properties afterwards in the same task (the React wrapper does), so at
174
+ * connect time `mode` may still hold its `'inline'` default — deciding there
175
+ * flashes a modal's full-viewport overlay open on mount. By the first update
176
+ * cycle the real value has settled.
177
+ */
178
+ private _autoOpenDecided;
179
+ connectedCallback(): void;
180
+ disconnectedCallback(): void;
181
+ protected willUpdate(changed: PropertyValues): void;
182
+ protected updated(changed: PropertyValues): void;
183
+ render(): import("lit-html").TemplateResult<1> | typeof nothing;
184
+ private _computeSrc;
185
+ private get _appOrigin();
186
+ private _onMessage;
187
+ /**
188
+ * Deliver `content` to the app once both sides are ready: it has asked, and
189
+ * we have something new to give it. Skips a re-send of identical content so
190
+ * an unrelated re-render can't discard the user's in-progress edits.
191
+ */
192
+ private _maybeSendContent;
193
+ /** Identity of a delivered template, as compared against `_sentKey`. */
194
+ private _contentKey;
195
+ /** Post into the iframe, targeted at the app origin. False if not mounted. */
196
+ private _postToApp;
197
+ private _startHandshakeTimer;
198
+ private _clearHandshakeTimer;
199
+ private _fail;
200
+ private _emit;
201
+ }
202
+ declare global {
203
+ interface HTMLElementTagNameMap {
204
+ 'sfx-template-builder': SfxTemplateBuilder;
205
+ }
206
+ }
package/package.json ADDED
@@ -0,0 +1,75 @@
1
+ {
2
+ "name": "@scaleflex/template-builder",
3
+ "version": "0.1.1",
4
+ "description": "Embeddable web component (<sfx-template-builder>) for the Filerobot design-templates builder",
5
+ "license": "SEE LICENSE IN LICENSE",
6
+ "type": "module",
7
+ "main": "./dist/index.cjs",
8
+ "module": "./dist/index.js",
9
+ "types": "./dist/index.d.ts",
10
+ "exports": {
11
+ ".": {
12
+ "types": "./dist/index.d.ts",
13
+ "import": "./dist/index.js",
14
+ "require": "./dist/index.cjs"
15
+ },
16
+ "./define": {
17
+ "types": "./dist/define.d.ts",
18
+ "import": "./dist/define.js",
19
+ "require": "./dist/define.cjs"
20
+ },
21
+ "./react": {
22
+ "types": "./dist/react.d.ts",
23
+ "import": "./dist/react.js",
24
+ "require": "./dist/react.cjs"
25
+ },
26
+ "./protocol": {
27
+ "types": "./src/protocol.ts",
28
+ "default": "./src/protocol.ts"
29
+ }
30
+ },
31
+ "files": [
32
+ "dist",
33
+ "src/*.ts",
34
+ ".claude",
35
+ "README.md",
36
+ "CHANGELOG.md",
37
+ "LICENSE"
38
+ ],
39
+ "scripts": {
40
+ "dev": "vite --config vite.demo.config.ts",
41
+ "dev:demo": "vite --config vite.demo.config.ts",
42
+ "build": "vite build && tsc -p tsconfig.build.json",
43
+ "build:cdn": "vite build --config vite.cdn.config.ts",
44
+ "build:demo": "vite build --config vite.demo.config.ts",
45
+ "preview:demo": "vite preview --config vite.demo.config.ts",
46
+ "build:all": "yarn build && yarn build:cdn",
47
+ "release": "node scripts/release-cdn.mjs plugin",
48
+ "typecheck": "tsc --noEmit",
49
+ "test": "vitest run",
50
+ "test:watch": "vitest"
51
+ },
52
+ "dependencies": {
53
+ "lit": "^3.2.0"
54
+ },
55
+ "peerDependencies": {
56
+ "react": ">=18",
57
+ "react-dom": ">=18"
58
+ },
59
+ "peerDependenciesMeta": {
60
+ "react": {
61
+ "optional": true
62
+ },
63
+ "react-dom": {
64
+ "optional": true
65
+ }
66
+ },
67
+ "devDependencies": {
68
+ "@types/react": "^19",
69
+ "jsdom": "^29.0.1",
70
+ "markdown-it": "^14.0.0",
71
+ "typescript": "^5",
72
+ "vite": "^6.3.5",
73
+ "vitest": "^4.1.2"
74
+ }
75
+ }
package/src/define.ts ADDED
@@ -0,0 +1,10 @@
1
+ import { SfxTemplateBuilder } from './template-builder'
2
+
3
+ if (
4
+ typeof customElements !== 'undefined' &&
5
+ !customElements.get('sfx-template-builder')
6
+ ) {
7
+ customElements.define('sfx-template-builder', SfxTemplateBuilder)
8
+ }
9
+
10
+ export { SfxTemplateBuilder }
package/src/index.ts ADDED
@@ -0,0 +1,9 @@
1
+ // Core entry — exports the element class without registering the tag.
2
+ // Import '@scaleflex/template-builder/define' to register <sfx-template-builder>.
3
+ export {
4
+ SfxTemplateBuilder,
5
+ type TemplateBuilderStatus,
6
+ type TemplateBuilderEventMap,
7
+ type TemplateBuilderSaveDetail,
8
+ } from './template-builder'
9
+ export * from './protocol'
@@ -0,0 +1,298 @@
1
+ // ---------------------------------------------------------------------------
2
+ // postMessage protocol between the design-templates app (inside the iframe)
3
+ // and its embedder (the <sfx-template-builder> widget or the Hub).
4
+ //
5
+ // This module is the single source of truth for both sides: the app imports
6
+ // it via `@scaleflex/template-builder/protocol` (workspace TS-source export),
7
+ // the widget bundles it. Message *values* are wire format — never change an
8
+ // existing string; add new messages instead. All changes must stay additive
9
+ // so older widgets keep working against newer app deployments and vice versa.
10
+ // ---------------------------------------------------------------------------
11
+
12
+ export const PROTOCOL_VERSION = 2
13
+
14
+ // App → embedder ------------------------------------------------------------
15
+
16
+ /** Editor mounted with valid auth — the embed handshake succeeded. */
17
+ export const BUILDER_READY = 'design-templates:builder:ready'
18
+ /** Editor UI opened (kept for Hub backwards compatibility; implies ready). */
19
+ export const BUILDER_OPEN = 'design-templates:builder:open'
20
+ /** Editor UI closed / unmounted. */
21
+ export const BUILDER_CLOSE = 'design-templates:builder:close'
22
+ /** Template saved. `data` is absent on app deployments older than protocol v1. */
23
+ export const BUILDER_SAVE = 'design-templates:builder:save'
24
+ /** The app cannot start (e.g. auth cookies missing/blocked). */
25
+ export const BUILDER_ERROR = 'design-templates:builder:error'
26
+ /**
27
+ * Stateless mode only (protocol v2). The editor mounted without a template and
28
+ * is waiting for the host to send `HOST_LOAD`. Re-sent on nothing — the host
29
+ * may answer late; the app keeps waiting until content arrives.
30
+ */
31
+ export const BUILDER_CONTENT_REQUEST = 'design-templates:builder:content-request'
32
+ /**
33
+ * Stateless mode only (protocol v2). The user saved and the app is handing the
34
+ * edited template back instead of uploading it. This is the stateless
35
+ * counterpart to `BUILDER_SAVE` — a separate message so that embedders written
36
+ * against v1 (which read `data.uuid` from a save they assume was persisted)
37
+ * never receive a payload that was not, in fact, stored anywhere.
38
+ */
39
+ export const BUILDER_CONTENT = 'design-templates:builder:content'
40
+ /**
41
+ * Stateless mode only (protocol v2). The unsaved-changes flag flipped. Sent so
42
+ * a host can ask the user before swapping the template out from under them —
43
+ * `HOST_LOAD` is honoured unconditionally and discards whatever was in
44
+ * progress, and without this the host has no way to know there was anything to
45
+ * lose.
46
+ */
47
+ export const BUILDER_DIRTY = 'design-templates:builder:dirty'
48
+
49
+ export interface BuilderReadyMessage {
50
+ type: typeof BUILDER_READY
51
+ }
52
+
53
+ export interface BuilderOpenMessage {
54
+ type: typeof BUILDER_OPEN
55
+ }
56
+
57
+ export interface BuilderCloseMessage {
58
+ type: typeof BUILDER_CLOSE
59
+ }
60
+
61
+ export interface BuilderSaveData {
62
+ uuid: string
63
+ name?: string
64
+ }
65
+
66
+ export interface BuilderSaveMessage {
67
+ type: typeof BUILDER_SAVE
68
+ data?: BuilderSaveData
69
+ }
70
+
71
+ /**
72
+ * `auth` and `invalid-content` are the codes the app itself sends. The widget
73
+ * adds `handshake-timeout` (no ready signal — typically blocked third-party
74
+ * cookies or a missing frame-ancestors entry), `invalid-base-url` and
75
+ * `invalid-config`.
76
+ */
77
+ export type BuilderErrorCode =
78
+ /**
79
+ * The app could not authenticate. In `secTemplate` mode this also covers a
80
+ * security-template key the Filerobot API refused to exchange for a sass key.
81
+ */
82
+ | 'auth'
83
+ /** Stateless mode: the `HOST_LOAD` content could not be parsed as a template. */
84
+ | 'invalid-content'
85
+ | 'handshake-timeout'
86
+ | 'invalid-base-url'
87
+ /** Attributes that contradict each other, e.g. `sec-template` without `stateless`. */
88
+ | 'invalid-config'
89
+ | 'unknown'
90
+
91
+ export interface BuilderErrorData {
92
+ code: BuilderErrorCode
93
+ message?: string
94
+ }
95
+
96
+ export interface BuilderErrorMessage {
97
+ type: typeof BUILDER_ERROR
98
+ data: BuilderErrorData
99
+ }
100
+
101
+ export interface BuilderContentRequestMessage {
102
+ type: typeof BUILDER_CONTENT_REQUEST
103
+ }
104
+
105
+ export interface BuilderContentData {
106
+ /**
107
+ * The id the host supplied in `HOST_LOAD`, echoed back verbatim. Absent when
108
+ * the host sent content without one.
109
+ */
110
+ templateId?: string
111
+ /** The edited template, serialized as `.fdt` XML. */
112
+ content: string
113
+ /** Display name the host supplied, echoed back. */
114
+ name?: string
115
+ /**
116
+ * `template_query` for the default render — layout, variable values and
117
+ * locale. In DAM-backed mode this is stored as file metadata; a stateless
118
+ * host must persist it alongside `content` or renders will fall back to
119
+ * whatever defaults the XML alone implies.
120
+ */
121
+ templateQuery?: string
122
+ }
123
+
124
+ export interface BuilderContentMessage {
125
+ type: typeof BUILDER_CONTENT
126
+ data: BuilderContentData
127
+ }
128
+
129
+ export interface BuilderDirtyData {
130
+ /** True when the editor holds edits that have not been handed back. */
131
+ isDirty: boolean
132
+ }
133
+
134
+ export interface BuilderDirtyMessage {
135
+ type: typeof BUILDER_DIRTY
136
+ data: BuilderDirtyData
137
+ }
138
+
139
+ export type BuilderMessage =
140
+ | BuilderReadyMessage
141
+ | BuilderOpenMessage
142
+ | BuilderCloseMessage
143
+ | BuilderSaveMessage
144
+ | BuilderErrorMessage
145
+ | BuilderContentRequestMessage
146
+ | BuilderContentMessage
147
+ | BuilderDirtyMessage
148
+
149
+ // Embedder → app --------------------------------------------------------------
150
+
151
+ /**
152
+ * Stateless mode only (protocol v2). Hands the app a template to edit. Sent in
153
+ * answer to `BUILDER_CONTENT_REQUEST`, and again whenever the host swaps the
154
+ * template without remounting the iframe.
155
+ *
156
+ * Content travels by postMessage rather than a URL param because template XML
157
+ * routinely exceeds practical URL length limits.
158
+ *
159
+ * The app accepts this message only from the origin pinned as `embedOrigin`
160
+ * when the session credentials were handed over, so an unrelated framing page
161
+ * cannot inject a template into someone else's session.
162
+ */
163
+ export const HOST_LOAD = 'design-templates:host:load'
164
+
165
+ export interface HostLoadData {
166
+ /**
167
+ * Opaque host-side identifier, echoed back on save. It is never used to
168
+ * fetch anything, so it need not be a Filerobot uuid — any string the host
169
+ * can map back to its own record works.
170
+ */
171
+ templateId?: string
172
+ /** Template to edit, as `.fdt` XML. */
173
+ content: string
174
+ /** Display name for the editor header. */
175
+ name?: string
176
+ /**
177
+ * `template_query` describing the render to open on — layout and variable
178
+ * values, in the same `$key=value&$key2=value2` form the editor hands back
179
+ * in `BuilderContentData.templateQuery`.
180
+ *
181
+ * Round-trips that value: a host that stored it on save and passes it back
182
+ * here reopens the template exactly as it was left. Omitting it falls back
183
+ * to the `default=` attributes in the XML, which is a different render
184
+ * whenever the query overrode any of them.
185
+ *
186
+ * Applied as display state, not as an edit — it selects the layout and fills
187
+ * variable values without marking the document dirty, so opening a template
188
+ * and closing it again does not look like an unsaved change.
189
+ */
190
+ templateQuery?: string
191
+ }
192
+
193
+ export interface HostLoadMessage {
194
+ type: typeof HOST_LOAD
195
+ data: HostLoadData
196
+ }
197
+
198
+ /**
199
+ * Stateless mode only (protocol v2). Reports whether the host managed to
200
+ * persist the content it received in `BUILDER_CONTENT`.
201
+ *
202
+ * Optional by design. The editor clears its unsaved-changes flag optimistically
203
+ * when it posts `BUILDER_CONTENT`, so a host that never acks behaves exactly as
204
+ * before. Sending `ok: false` is what buys something: the editor restores the
205
+ * dirty flag and tells the user, instead of leaving a failed write looking
206
+ * saved.
207
+ */
208
+ export const HOST_SAVED = 'design-templates:host:saved'
209
+
210
+ export interface HostSavedData {
211
+ /** False when the host could not persist the content. */
212
+ ok: boolean
213
+ /** Shown to the user when `ok` is false. */
214
+ message?: string
215
+ }
216
+
217
+ export interface HostSavedMessage {
218
+ type: typeof HOST_SAVED
219
+ data: HostSavedData
220
+ }
221
+
222
+ export type HostMessage = HostLoadMessage | HostSavedMessage
223
+
224
+ // Embed URL contract ----------------------------------------------------------
225
+
226
+ /**
227
+ * Query params the app's proxy middleware (`src/proxy.ts`) converts into auth
228
+ * cookies on first navigation. Names are wire format.
229
+ */
230
+ export const EMBED_PARAMS = {
231
+ SESSION_UUID: 'suuid',
232
+ COMPANY_UUID: 'cuuid',
233
+ PROJECT_UUID: 'puuid',
234
+ SASS_KEY: 'sassKey',
235
+ FILEROBOT_TOKEN: 'ftoken',
236
+ /**
237
+ * Filerobot security-template key, the alternative to a Hub session. The app
238
+ * exchanges it for a sass key itself and runs in a reduced mode — see
239
+ * `AUTH_MODES`. Sent *instead of* `sassKey` + `suuid`, never alongside them.
240
+ */
241
+ SEC_TEMPLATE: 'secTemplate',
242
+ IFRAME: 'iframe',
243
+ /** Origin of the embedding page; the app uses it as postMessage targetOrigin. */
244
+ EMBED_ORIGIN: 'embedOrigin',
245
+ /**
246
+ * Accent colour for the editor chrome, as `#rgb` / `#rrggbb`. The app derives
247
+ * its whole accent ramp from it. Rejected server-side if it doesn't match
248
+ * that shape — it ends up inside a stylesheet.
249
+ */
250
+ BRAND_COLOR: 'brandColor',
251
+ /** Colour scheme for the editor chrome: `light` | `dark` | `auto`. */
252
+ THEME: 'theme',
253
+ } as const
254
+
255
+ /** Values the `theme` param accepts. */
256
+ export type BuilderTheme = 'light' | 'dark' | 'auto'
257
+
258
+ /**
259
+ * How the embedder authenticated.
260
+ *
261
+ * - `session` — a Hub session (`suuid` + `sassKey` + `ftoken`). Full features.
262
+ * - `secTemplate` — a Filerobot security-template key (`secTemplate` +
263
+ * `ftoken`). A guest credential: no user identity and no Hub project, so the
264
+ * app accepts it on {@link EMBED_ROUTE} only, and everything that reads the
265
+ * Hub project model (metadata fields, regional variants, project branding)
266
+ * comes back empty. Rendering, fonts and asset browsing work, scoped by
267
+ * whatever the security template grants.
268
+ *
269
+ * Derived by the app from the params it received; named here so both sides use
270
+ * the same vocabulary.
271
+ */
272
+ export const AUTH_MODES = {
273
+ SESSION: 'session',
274
+ SEC_TEMPLATE: 'secTemplate',
275
+ } as const
276
+
277
+ export type AuthMode = (typeof AUTH_MODES)[keyof typeof AUTH_MODES]
278
+
279
+ /**
280
+ * Shape the app requires of `brandColor`. Hex only: the value is interpolated
281
+ * into a `:root { … }` rule, so anything that could carry CSS syntax is
282
+ * refused rather than escaped. Mirrored in the app's proxy — keep in sync.
283
+ */
284
+ export const BRAND_COLOR_PATTERN = /^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/
285
+
286
+ /** Editor route for an existing template, or the new-template route. */
287
+ export function builderRoute(templateId?: string): string {
288
+ return templateId
289
+ ? `/templates/${encodeURIComponent(templateId)}/edit`
290
+ : '/templates/new'
291
+ }
292
+
293
+ /**
294
+ * Stateless editor route. Takes no template id — the id is an opaque host
295
+ * value that arrives with the content over `HOST_LOAD`, not something the app
296
+ * resolves against Filerobot, so it has no place in the URL.
297
+ */
298
+ export const EMBED_ROUTE = '/templates/embed'