@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
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"template-builder-CSyPZni9.cjs","sources":["../src/protocol.ts","../src/template-builder.ts"],"sourcesContent":["// ---------------------------------------------------------------------------\n// postMessage protocol between the design-templates app (inside the iframe)\n// and its embedder (the <sfx-template-builder> widget or the Hub).\n//\n// This module is the single source of truth for both sides: the app imports\n// it via `@scaleflex/template-builder/protocol` (workspace TS-source export),\n// the widget bundles it. Message *values* are wire format — never change an\n// existing string; add new messages instead. All changes must stay additive\n// so older widgets keep working against newer app deployments and vice versa.\n// ---------------------------------------------------------------------------\n\nexport const PROTOCOL_VERSION = 2\n\n// App → embedder ------------------------------------------------------------\n\n/** Editor mounted with valid auth — the embed handshake succeeded. */\nexport const BUILDER_READY = 'design-templates:builder:ready'\n/** Editor UI opened (kept for Hub backwards compatibility; implies ready). */\nexport const BUILDER_OPEN = 'design-templates:builder:open'\n/** Editor UI closed / unmounted. */\nexport const BUILDER_CLOSE = 'design-templates:builder:close'\n/** Template saved. `data` is absent on app deployments older than protocol v1. */\nexport const BUILDER_SAVE = 'design-templates:builder:save'\n/** The app cannot start (e.g. auth cookies missing/blocked). */\nexport const BUILDER_ERROR = 'design-templates:builder:error'\n/**\n * Stateless mode only (protocol v2). The editor mounted without a template and\n * is waiting for the host to send `HOST_LOAD`. Re-sent on nothing — the host\n * may answer late; the app keeps waiting until content arrives.\n */\nexport const BUILDER_CONTENT_REQUEST = 'design-templates:builder:content-request'\n/**\n * Stateless mode only (protocol v2). The user saved and the app is handing the\n * edited template back instead of uploading it. This is the stateless\n * counterpart to `BUILDER_SAVE` — a separate message so that embedders written\n * against v1 (which read `data.uuid` from a save they assume was persisted)\n * never receive a payload that was not, in fact, stored anywhere.\n */\nexport const BUILDER_CONTENT = 'design-templates:builder:content'\n/**\n * Stateless mode only (protocol v2). The unsaved-changes flag flipped. Sent so\n * a host can ask the user before swapping the template out from under them —\n * `HOST_LOAD` is honoured unconditionally and discards whatever was in\n * progress, and without this the host has no way to know there was anything to\n * lose.\n */\nexport const BUILDER_DIRTY = 'design-templates:builder:dirty'\n\nexport interface BuilderReadyMessage {\n type: typeof BUILDER_READY\n}\n\nexport interface BuilderOpenMessage {\n type: typeof BUILDER_OPEN\n}\n\nexport interface BuilderCloseMessage {\n type: typeof BUILDER_CLOSE\n}\n\nexport interface BuilderSaveData {\n uuid: string\n name?: string\n}\n\nexport interface BuilderSaveMessage {\n type: typeof BUILDER_SAVE\n data?: BuilderSaveData\n}\n\n/**\n * `auth` and `invalid-content` are the codes the app itself sends. The widget\n * adds `handshake-timeout` (no ready signal — typically blocked third-party\n * cookies or a missing frame-ancestors entry), `invalid-base-url` and\n * `invalid-config`.\n */\nexport type BuilderErrorCode =\n /**\n * The app could not authenticate. In `secTemplate` mode this also covers a\n * security-template key the Filerobot API refused to exchange for a sass key.\n */\n | 'auth'\n /** Stateless mode: the `HOST_LOAD` content could not be parsed as a template. */\n | 'invalid-content'\n | 'handshake-timeout'\n | 'invalid-base-url'\n /** Attributes that contradict each other, e.g. `sec-template` without `stateless`. */\n | 'invalid-config'\n | 'unknown'\n\nexport interface BuilderErrorData {\n code: BuilderErrorCode\n message?: string\n}\n\nexport interface BuilderErrorMessage {\n type: typeof BUILDER_ERROR\n data: BuilderErrorData\n}\n\nexport interface BuilderContentRequestMessage {\n type: typeof BUILDER_CONTENT_REQUEST\n}\n\nexport interface BuilderContentData {\n /**\n * The id the host supplied in `HOST_LOAD`, echoed back verbatim. Absent when\n * the host sent content without one.\n */\n templateId?: string\n /** The edited template, serialized as `.fdt` XML. */\n content: string\n /** Display name the host supplied, echoed back. */\n name?: string\n /**\n * `template_query` for the default render — layout, variable values and\n * locale. In DAM-backed mode this is stored as file metadata; a stateless\n * host must persist it alongside `content` or renders will fall back to\n * whatever defaults the XML alone implies.\n */\n templateQuery?: string\n}\n\nexport interface BuilderContentMessage {\n type: typeof BUILDER_CONTENT\n data: BuilderContentData\n}\n\nexport interface BuilderDirtyData {\n /** True when the editor holds edits that have not been handed back. */\n isDirty: boolean\n}\n\nexport interface BuilderDirtyMessage {\n type: typeof BUILDER_DIRTY\n data: BuilderDirtyData\n}\n\nexport type BuilderMessage =\n | BuilderReadyMessage\n | BuilderOpenMessage\n | BuilderCloseMessage\n | BuilderSaveMessage\n | BuilderErrorMessage\n | BuilderContentRequestMessage\n | BuilderContentMessage\n | BuilderDirtyMessage\n\n// Embedder → app --------------------------------------------------------------\n\n/**\n * Stateless mode only (protocol v2). Hands the app a template to edit. Sent in\n * answer to `BUILDER_CONTENT_REQUEST`, and again whenever the host swaps the\n * template without remounting the iframe.\n *\n * Content travels by postMessage rather than a URL param because template XML\n * routinely exceeds practical URL length limits.\n *\n * The app accepts this message only from the origin pinned as `embedOrigin`\n * when the session credentials were handed over, so an unrelated framing page\n * cannot inject a template into someone else's session.\n */\nexport const HOST_LOAD = 'design-templates:host:load'\n\nexport interface HostLoadData {\n /**\n * Opaque host-side identifier, echoed back on save. It is never used to\n * fetch anything, so it need not be a Filerobot uuid — any string the host\n * can map back to its own record works.\n */\n templateId?: string\n /** Template to edit, as `.fdt` XML. */\n content: string\n /** Display name for the editor header. */\n name?: string\n /**\n * `template_query` describing the render to open on — layout and variable\n * values, in the same `$key=value&$key2=value2` form the editor hands back\n * in `BuilderContentData.templateQuery`.\n *\n * Round-trips that value: a host that stored it on save and passes it back\n * here reopens the template exactly as it was left. Omitting it falls back\n * to the `default=` attributes in the XML, which is a different render\n * whenever the query overrode any of them.\n *\n * Applied as display state, not as an edit — it selects the layout and fills\n * variable values without marking the document dirty, so opening a template\n * and closing it again does not look like an unsaved change.\n */\n templateQuery?: string\n}\n\nexport interface HostLoadMessage {\n type: typeof HOST_LOAD\n data: HostLoadData\n}\n\n/**\n * Stateless mode only (protocol v2). Reports whether the host managed to\n * persist the content it received in `BUILDER_CONTENT`.\n *\n * Optional by design. The editor clears its unsaved-changes flag optimistically\n * when it posts `BUILDER_CONTENT`, so a host that never acks behaves exactly as\n * before. Sending `ok: false` is what buys something: the editor restores the\n * dirty flag and tells the user, instead of leaving a failed write looking\n * saved.\n */\nexport const HOST_SAVED = 'design-templates:host:saved'\n\nexport interface HostSavedData {\n /** False when the host could not persist the content. */\n ok: boolean\n /** Shown to the user when `ok` is false. */\n message?: string\n}\n\nexport interface HostSavedMessage {\n type: typeof HOST_SAVED\n data: HostSavedData\n}\n\nexport type HostMessage = HostLoadMessage | HostSavedMessage\n\n// Embed URL contract ----------------------------------------------------------\n\n/**\n * Query params the app's proxy middleware (`src/proxy.ts`) converts into auth\n * cookies on first navigation. Names are wire format.\n */\nexport const EMBED_PARAMS = {\n SESSION_UUID: 'suuid',\n COMPANY_UUID: 'cuuid',\n PROJECT_UUID: 'puuid',\n SASS_KEY: 'sassKey',\n FILEROBOT_TOKEN: 'ftoken',\n /**\n * Filerobot security-template key, the alternative to a Hub session. The app\n * exchanges it for a sass key itself and runs in a reduced mode — see\n * `AUTH_MODES`. Sent *instead of* `sassKey` + `suuid`, never alongside them.\n */\n SEC_TEMPLATE: 'secTemplate',\n IFRAME: 'iframe',\n /** Origin of the embedding page; the app uses it as postMessage targetOrigin. */\n EMBED_ORIGIN: 'embedOrigin',\n /**\n * Accent colour for the editor chrome, as `#rgb` / `#rrggbb`. The app derives\n * its whole accent ramp from it. Rejected server-side if it doesn't match\n * that shape — it ends up inside a stylesheet.\n */\n BRAND_COLOR: 'brandColor',\n /** Colour scheme for the editor chrome: `light` | `dark` | `auto`. */\n THEME: 'theme',\n} as const\n\n/** Values the `theme` param accepts. */\nexport type BuilderTheme = 'light' | 'dark' | 'auto'\n\n/**\n * How the embedder authenticated.\n *\n * - `session` — a Hub session (`suuid` + `sassKey` + `ftoken`). Full features.\n * - `secTemplate` — a Filerobot security-template key (`secTemplate` +\n * `ftoken`). A guest credential: no user identity and no Hub project, so the\n * app accepts it on {@link EMBED_ROUTE} only, and everything that reads the\n * Hub project model (metadata fields, regional variants, project branding)\n * comes back empty. Rendering, fonts and asset browsing work, scoped by\n * whatever the security template grants.\n *\n * Derived by the app from the params it received; named here so both sides use\n * the same vocabulary.\n */\nexport const AUTH_MODES = {\n SESSION: 'session',\n SEC_TEMPLATE: 'secTemplate',\n} as const\n\nexport type AuthMode = (typeof AUTH_MODES)[keyof typeof AUTH_MODES]\n\n/**\n * Shape the app requires of `brandColor`. Hex only: the value is interpolated\n * into a `:root { … }` rule, so anything that could carry CSS syntax is\n * refused rather than escaped. Mirrored in the app's proxy — keep in sync.\n */\nexport const BRAND_COLOR_PATTERN = /^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/\n\n/** Editor route for an existing template, or the new-template route. */\nexport function builderRoute(templateId?: string): string {\n return templateId\n ? `/templates/${encodeURIComponent(templateId)}/edit`\n : '/templates/new'\n}\n\n/**\n * Stateless editor route. Takes no template id — the id is an opaque host\n * value that arrives with the content over `HOST_LOAD`, not something the app\n * resolves against Filerobot, so it has no place in the URL.\n */\nexport const EMBED_ROUTE = '/templates/embed'\n","import { LitElement, html, css, nothing, type PropertyValues } from 'lit'\nimport { property, state } from 'lit/decorators.js'\nimport {\n BUILDER_CLOSE,\n BUILDER_CONTENT,\n BUILDER_CONTENT_REQUEST,\n BUILDER_DIRTY,\n BUILDER_ERROR,\n BUILDER_OPEN,\n BUILDER_READY,\n BUILDER_SAVE,\n EMBED_PARAMS,\n EMBED_ROUTE,\n HOST_LOAD,\n HOST_SAVED,\n builderRoute,\n type BuilderContentData,\n type BuilderContentMessage,\n type BuilderDirtyData,\n type BuilderDirtyMessage,\n type BuilderErrorData,\n type BuilderErrorMessage,\n type BuilderSaveData,\n type BuilderSaveMessage,\n type BuilderTheme,\n} from './protocol'\n\nexport type TemplateBuilderStatus = 'idle' | 'loading' | 'ready' | 'error'\n\n/**\n * `save` payload. Which variant arrives follows the mode the element was\n * configured in:\n * - DAM-backed (default) — `BuilderSaveData`; the app uploaded the template and\n * reports the resulting `uuid`.\n * - `stateless` — `BuilderContentData`; nothing was stored, and `content` is\n * the edited template for the host to persist.\n */\nexport type TemplateBuilderSaveDetail =\n | BuilderSaveData\n | BuilderContentData\n | undefined\n\nexport interface TemplateBuilderEventMap {\n ready: CustomEvent<void>\n open: CustomEvent<void>\n close: CustomEvent<void>\n save: CustomEvent<TemplateBuilderSaveDetail>\n error: CustomEvent<BuilderErrorData>\n dirtychange: CustomEvent<BuilderDirtyData>\n}\n\n/**\n * `<sfx-template-builder>` — embeds the Filerobot design-templates builder.\n *\n * The element owns an iframe pointed at a design-templates-app deployment,\n * passes auth via URL params (converted to cookies by the app's proxy), and\n * translates the app's postMessage protocol into DOM CustomEvents:\n * `ready`, `open`, `close`, `save`, `error`.\n *\n * Required: `base-url`, `token`, and one of two credentials:\n * - `sass-key` + `session-uuid` — a Hub session. Full features.\n * - `sec-template` — a Filerobot security-template key. No Hub session needed,\n * but it only works with `stateless`, and Hub-project features (metadata\n * fields, regional variants, project branding) come back empty.\n *\n * In `inline` mode the editor loads as soon as config is complete and fills\n * the host element (size it explicitly). In `modal` mode nothing renders\n * until `open()` is called; the editor then covers the viewport.\n *\n * Two ways to supply the template:\n * - **DAM-backed** (default) — set `template-id` to a Filerobot file uuid. The\n * app loads and saves it itself, and `save` reports the new uuid.\n * - **Stateless** — set `stateless` and assign `content`. The element sends the\n * template into the editor over postMessage and `save` returns the edited\n * document; nothing is stored on the Scaleflex side, and `template-id` is\n * just an opaque string echoed back. Rendering, fonts and asset browsing\n * still use the session's Filerobot tenant.\n *\n * `brand-color` and `theme` restyle the editor chrome to match the host page.\n * They do not touch the rendered template — its colours live in the document.\n */\nexport class SfxTemplateBuilder extends LitElement {\n static styles = css`\n :host {\n display: block;\n position: relative;\n }\n :host([mode='modal']) {\n display: contents;\n }\n .overlay {\n position: fixed;\n inset: 0;\n z-index: 2147483000;\n background: rgba(0, 0, 0, 0.55);\n display: flex;\n }\n .stage {\n position: relative;\n flex: 1;\n display: flex;\n }\n iframe {\n border: 0;\n flex: 1;\n width: 100%;\n height: 100%;\n }\n .spinner {\n position: absolute;\n inset: 0;\n margin: auto;\n width: 32px;\n height: 32px;\n border: 3px solid rgba(128, 128, 128, 0.3);\n border-top-color: currentColor;\n border-radius: 50%;\n animation: sfx-tb-spin 0.8s linear infinite;\n pointer-events: none;\n }\n @keyframes sfx-tb-spin {\n to {\n transform: rotate(360deg);\n }\n }\n `\n\n /** Origin + optional path prefix of the design-templates-app deployment. */\n @property({ attribute: 'base-url' }) baseUrl = ''\n /** Filerobot token (`ftoken`). */\n @property() token = ''\n @property({ attribute: 'sass-key' }) sassKey = ''\n @property({ attribute: 'session-uuid' }) sessionUuid = ''\n /**\n * Filerobot security-template key — the alternative to `sass-key` +\n * `session-uuid` for hosts with no Hub session to hand over. Requires\n * `stateless`, and degrades the features that come from the Hub project\n * model (metadata fields, regional variants, project branding). When set it\n * wins: neither `sass-key` nor `session-uuid` is passed to the app.\n */\n @property({ attribute: 'sec-template' }) secTemplate = ''\n @property({ attribute: 'company-uuid' }) companyUuid = ''\n @property({ attribute: 'project-uuid' }) projectUuid = ''\n /**\n * DAM-backed mode: the Filerobot uuid to load; empty opens the new-template\n * flow. Stateless mode: an opaque host id, echoed back on `save`.\n */\n @property({ attribute: 'template-id' }) templateId = ''\n @property({ reflect: true }) mode: 'inline' | 'modal' = 'inline'\n /**\n * Hand the template in and take it back out instead of letting the app read\n * and write Filerobot. Requires `content`.\n */\n @property({ type: Boolean, reflect: true }) stateless = false\n /**\n * Stateless mode: the template to edit, as `.fdt` XML. Property only — templates\n * routinely exceed practical attribute/URL sizes, so it is never reflected.\n * Assigning a different value while open loads it into the running editor.\n */\n @property({ attribute: false }) content = ''\n /** Stateless mode: display name for the editor header. */\n @property({ attribute: 'template-name' }) templateName = ''\n /**\n * Stateless mode: the `template_query` to open on — the value handed back in\n * the `save` payload. Pass back what you stored and the editor reopens on the\n * same layout and variable values; leave it empty and the render falls back\n * to the XML's own `default=` attributes.\n */\n @property({ attribute: 'template-query' }) templateQuery = ''\n /**\n * Accent colour for the editor chrome, as `#rgb` / `#rrggbb`. The app derives\n * buttons, focus rings and highlights from it. Empty keeps the Scaleflex\n * default. Themes the editor UI only — never the rendered template, whose\n * colours live in the document.\n */\n @property({ attribute: 'brand-color' }) brandColor = ''\n /** Colour scheme for the editor chrome. Empty leaves the app's own default. */\n @property() theme: BuilderTheme | '' = ''\n /** Ms to wait for the app's ready signal before emitting `error`. 0 disables. */\n @property({ type: Number, attribute: 'ready-timeout' }) readyTimeout = 20000\n\n @state() private _status: TemplateBuilderStatus = 'idle'\n @state() private _open = false\n @state() private _src = ''\n\n private _handshakeTimer?: number\n /**\n * The app asked for content. Tracked because the request and the `content`\n * assignment race: whichever lands second triggers the send.\n */\n private _contentRequested = false\n /**\n * Identity of the template already delivered, so an unrelated re-render does\n * not resend it and discard the user's edits. Covers the id and name too, not\n * just the content: two host records can hold byte-identical templates, and\n * resending only on content change would leave the app echoing a stale id\n * back on save.\n */\n private _sentKey?: string\n /**\n * The `baseUrl` value already reported as unparseable. `_computeSrc()` runs\n * on every update cycle, so without this a bad URL re-emits `error` forever —\n * once per render, since the error status it sets is already in place after\n * the first.\n */\n private _reportedBadBaseUrl?: string\n /**\n * Whether the sec-template-without-stateless mistake has been reported. Same\n * reason as `_reportedBadBaseUrl`: `_computeSrc()` runs every update cycle\n * and the error status it sets is already in place after the first pass.\n */\n private _reportedStatelessRequired = false\n\n @state() private _isDirty = false\n\n get status(): TemplateBuilderStatus {\n return this._status\n }\n\n /**\n * Stateless mode: whether the editor holds edits that have not been handed\n * back yet. Check this before calling `load()` — a swap discards them.\n * Always false in DAM-backed mode, where the app owns saving.\n */\n get isDirty(): boolean {\n return this._isDirty\n }\n\n /** Open the editor (loads the iframe). Optionally switch template first. */\n open(templateId?: string): void {\n if (templateId !== undefined) this.templateId = templateId\n this._open = true\n }\n\n /** Close the editor and unload the iframe. Does not emit `close`. */\n close(): void {\n this._open = false\n }\n\n /**\n * Stateless mode: load a template, opening the editor if needed. Equivalent\n * to assigning `templateId` / `content` / `templateName` and calling `open()`.\n */\n load({\n content,\n templateId,\n name,\n templateQuery,\n }: {\n content: string\n templateId?: string\n name?: string\n templateQuery?: string\n }): void {\n if (templateId !== undefined) this.templateId = templateId\n if (name !== undefined) this.templateName = name\n // Assigned before `content`: all four ship as one HOST_LOAD, and leaving a\n // previous template's query in place while the new content goes out would\n // open the new document on the old layout and values.\n if (templateQuery !== undefined) this.templateQuery = templateQuery\n this.content = content\n this._open = true\n }\n\n /**\n * Stateless mode: report back whether a `save` was persisted on your side.\n *\n * Optional. The editor clears its unsaved-changes flag as soon as it hands\n * the content over, so not calling this leaves the previous behaviour intact.\n * Calling it with `false` is what earns something: the editor restores the\n * dirty flag and tells the user, rather than showing a failed write as saved.\n *\n * No-op outside stateless mode, where the app did the saving and has nothing\n * to hear back about.\n */\n confirmSave(ok: boolean, message?: string): void {\n if (!this.stateless) return\n this._postToApp({ type: HOST_SAVED, data: { ok, message } })\n }\n\n /**\n * Whether the inline-implies-open decision has been made. It cannot be made\n * in `connectedCallback`: frameworks insert the element first and assign\n * properties afterwards in the same task (the React wrapper does), so at\n * connect time `mode` may still hold its `'inline'` default — deciding there\n * flashes a modal's full-viewport overlay open on mount. By the first update\n * cycle the real value has settled.\n */\n private _autoOpenDecided = false\n\n connectedCallback(): void {\n super.connectedCallback()\n window.addEventListener('message', this._onMessage)\n }\n\n disconnectedCallback(): void {\n super.disconnectedCallback()\n window.removeEventListener('message', this._onMessage)\n this._clearHandshakeTimer()\n }\n\n protected willUpdate(changed: PropertyValues): void {\n super.willUpdate(changed)\n if (!this._autoOpenDecided) {\n this._autoOpenDecided = true\n if (this.mode === 'inline') this._open = true\n }\n const src = this._computeSrc()\n if (src !== this._src) {\n this._src = src\n this._status = src ? 'loading' : 'idle'\n }\n }\n\n protected updated(changed: PropertyValues): void {\n if (changed.has('_src')) {\n this._clearHandshakeTimer()\n // A new document means a new app instance: it has not asked for content\n // yet, and nothing has been delivered to it.\n this._contentRequested = false\n this._sentKey = undefined\n if (this._isDirty) {\n this._isDirty = false\n this._emit('dirtychange', { isDirty: false })\n }\n if (this._src) this._startHandshakeTimer()\n }\n // Swapping any part of the template on a running editor reloads it. The id\n // matters as much as the content: a host moving between two identical\n // templates must not leave the app saving under the previous id.\n if (\n changed.has('content') ||\n changed.has('templateId') ||\n changed.has('templateName') ||\n changed.has('templateQuery')\n ) {\n this._maybeSendContent()\n }\n }\n\n render() {\n const frame = this._src\n ? html`<iframe\n part=\"iframe\"\n title=\"Template builder\"\n src=${this._src}\n allow=\"clipboard-read; clipboard-write\"\n ></iframe>`\n : nothing\n const spinner =\n this._status === 'loading'\n ? html`<div class=\"spinner\" part=\"spinner\"></div>`\n : nothing\n\n if (this.mode === 'modal') {\n return this._open\n ? html`<div class=\"overlay\" part=\"overlay\">\n <div class=\"stage\">${frame}${spinner}</div>\n </div>`\n : nothing\n }\n return html`${frame}${spinner}`\n }\n\n private _computeSrc(): string {\n if (!this._open) return ''\n if (!this.baseUrl || !this.token) return ''\n if (this.secTemplate) {\n // A security template is a guest credential with no user identity behind\n // it, so the app takes it on the stateless route only. Saying so here\n // turns a config mistake into a message instead of a login redirect the\n // host sees as `handshake-timeout`.\n if (!this.stateless) {\n if (!this._reportedStatelessRequired) {\n this._reportedStatelessRequired = true\n queueMicrotask(() =>\n this._fail({\n code: 'invalid-config',\n message:\n 'sec-template requires stateless mode — the app accepts a ' +\n 'security template on the stateless embed route only.',\n }),\n )\n }\n return ''\n }\n // Cleared on a valid pass so a host that fixes the combination and later\n // breaks it again is told again, matching `_reportedBadBaseUrl`.\n this._reportedStatelessRequired = false\n } else if (!this.sassKey || !this.sessionUuid) {\n return ''\n }\n let url: URL\n try {\n // Stateless mode keeps the id out of the URL — it is a host-side value\n // the app never resolves, and the content arrives by postMessage.\n const route = this.stateless\n ? EMBED_ROUTE\n : builderRoute(this.templateId || undefined)\n // Resolve relative to the base, not the origin: routes are absolute\n // paths, and `new URL('/x', 'https://host/app')` would silently drop\n // the documented path prefix, 404 on subpath deployments, and surface\n // only as a handshake-timeout.\n const base = this.baseUrl.endsWith('/') ? this.baseUrl : `${this.baseUrl}/`\n url = new URL(route.replace(/^\\//, ''), base)\n } catch {\n // Report each bad value once. This runs on every update cycle, and\n // `_fail` sets a status that is already 'error' by the second pass, so\n // there is no state change to fall out of the loop on.\n if (this._reportedBadBaseUrl !== this.baseUrl) {\n this._reportedBadBaseUrl = this.baseUrl\n // Emitted from a state-compute path; defer so consumers attached after\n // this update cycle still receive it.\n queueMicrotask(() =>\n this._fail({\n code: 'invalid-base-url',\n message: `base-url is not a valid URL: ${this.baseUrl}`,\n }),\n )\n }\n return ''\n }\n this._reportedBadBaseUrl = undefined\n url.searchParams.set(EMBED_PARAMS.FILEROBOT_TOKEN, this.token)\n if (this.secTemplate) {\n // Exclusive with the session credentials: the app reads the mode off\n // which of the two arrived, and the Hub uuids below name a project it\n // cannot look up without a session anyway.\n url.searchParams.set(EMBED_PARAMS.SEC_TEMPLATE, this.secTemplate)\n } else {\n url.searchParams.set(EMBED_PARAMS.SASS_KEY, this.sassKey)\n url.searchParams.set(EMBED_PARAMS.SESSION_UUID, this.sessionUuid)\n if (this.companyUuid) {\n url.searchParams.set(EMBED_PARAMS.COMPANY_UUID, this.companyUuid)\n }\n if (this.projectUuid) {\n url.searchParams.set(EMBED_PARAMS.PROJECT_UUID, this.projectUuid)\n }\n }\n if (this.brandColor) {\n url.searchParams.set(EMBED_PARAMS.BRAND_COLOR, this.brandColor)\n }\n if (this.theme) {\n url.searchParams.set(EMBED_PARAMS.THEME, this.theme)\n }\n url.searchParams.set(EMBED_PARAMS.IFRAME, '1')\n url.searchParams.set(EMBED_PARAMS.EMBED_ORIGIN, window.location.origin)\n return url.toString()\n }\n\n private get _appOrigin(): string | null {\n try {\n return new URL(this.baseUrl).origin\n } catch {\n return null\n }\n }\n\n private _onMessage = (event: MessageEvent): void => {\n if (!this._open) return\n if (!event.origin || event.origin !== this._appOrigin) return\n const iframe = this.shadowRoot?.querySelector('iframe')\n if (!iframe) return\n // Ignore messages from other frames of the same app origin. Strict: a\n // missing source is not given the benefit of the doubt — the app side\n // (`readHostLoadMessage`) applies the same rule.\n if (event.source !== iframe.contentWindow) return\n\n const msg = event.data as { type?: unknown } | null\n if (!msg || typeof msg.type !== 'string') return\n\n switch (msg.type) {\n case BUILDER_READY:\n case BUILDER_OPEN:\n this._clearHandshakeTimer()\n if (this._status !== 'ready') {\n this._status = 'ready'\n this._emit('ready')\n }\n if (msg.type === BUILDER_OPEN) this._emit('open')\n break\n case BUILDER_SAVE:\n this._emit('save', (msg as BuilderSaveMessage).data)\n break\n case BUILDER_CONTENT_REQUEST:\n this._contentRequested = true\n // A fresh request is authoritative: the app is saying it holds no\n // template. It may have remounted or reloaded inside an unchanged\n // iframe, so resend even if this content went out already — otherwise\n // the editor waits on a skeleton forever.\n this._sentKey = undefined\n this._maybeSendContent()\n break\n case BUILDER_CONTENT: {\n // Stateless save. Surfaced as `save` so hosts have one event to bind\n // regardless of mode; the detail shape follows the mode they chose.\n const data = (msg as BuilderContentMessage).data\n // The saved document is what the editor now holds. A host that stores\n // it and echoes it back into `content` — the natural controlled\n // pattern — must not trigger a HOST_LOAD reload that wipes the\n // editor's undo history behind a skeleton flash.\n this._sentKey = this._contentKey(data.content)\n this._emit('save', data)\n break\n }\n case BUILDER_DIRTY: {\n const data = (msg as BuilderDirtyMessage).data\n this._isDirty = !!data?.isDirty\n this._emit('dirtychange', { isDirty: this._isDirty })\n break\n }\n case BUILDER_CLOSE:\n this._emit('close')\n if (this.mode === 'modal') this._open = false\n break\n case BUILDER_ERROR:\n this._fail((msg as BuilderErrorMessage).data ?? { code: 'unknown' })\n break\n }\n }\n\n /**\n * Deliver `content` to the app once both sides are ready: it has asked, and\n * we have something new to give it. Skips a re-send of identical content so\n * an unrelated re-render can't discard the user's in-progress edits.\n */\n private _maybeSendContent(): void {\n if (!this.stateless || !this._contentRequested || !this.content) return\n\n const data = {\n templateId: this.templateId || undefined,\n content: this.content,\n name: this.templateName || undefined,\n templateQuery: this.templateQuery || undefined,\n }\n const key = this._contentKey(this.content)\n if (key === this._sentKey) return\n\n if (!this._postToApp({ type: HOST_LOAD, data })) return\n this._sentKey = key\n }\n\n /** Identity of a delivered template, as compared against `_sentKey`. */\n private _contentKey(content: string): string {\n return JSON.stringify({\n templateId: this.templateId || undefined,\n content,\n name: this.templateName || undefined,\n templateQuery: this.templateQuery || undefined,\n })\n }\n\n /** Post into the iframe, targeted at the app origin. False if not mounted. */\n private _postToApp(message: unknown): boolean {\n const target = this.shadowRoot?.querySelector('iframe')?.contentWindow\n const appOrigin = this._appOrigin\n if (!target || !appOrigin) return false\n target.postMessage(message, appOrigin)\n return true\n }\n\n private _startHandshakeTimer(): void {\n if (this.readyTimeout <= 0) return\n this._handshakeTimer = window.setTimeout(() => {\n this._fail({\n code: 'handshake-timeout',\n message:\n `No ready signal from ${this.baseUrl} within ${this.readyTimeout}ms. ` +\n 'Check that this origin is in the app\\'s frame-ancestors allowlist ' +\n 'and that third-party cookies are not blocked.',\n })\n }, this.readyTimeout)\n }\n\n private _clearHandshakeTimer(): void {\n if (this._handshakeTimer !== undefined) {\n window.clearTimeout(this._handshakeTimer)\n this._handshakeTimer = undefined\n }\n }\n\n private _fail(data: BuilderErrorData): void {\n this._clearHandshakeTimer()\n this._status = 'error'\n this._emit('error', data)\n }\n\n private _emit<T>(name: string, detail?: T): void {\n this.dispatchEvent(\n new CustomEvent(name, { detail, bubbles: true, composed: true }),\n )\n }\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n 'sfx-template-builder': SfxTemplateBuilder\n }\n}\n"],"names":["PROTOCOL_VERSION","BUILDER_READY","BUILDER_OPEN","BUILDER_CLOSE","BUILDER_SAVE","BUILDER_ERROR","BUILDER_CONTENT_REQUEST","BUILDER_CONTENT","BUILDER_DIRTY","HOST_LOAD","HOST_SAVED","EMBED_PARAMS","AUTH_MODES","BRAND_COLOR_PATTERN","builderRoute","templateId","EMBED_ROUTE","_SfxTemplateBuilder","LitElement","event","iframe","msg","data","content","name","templateQuery","ok","message","changed","src","frame","html","nothing","spinner","url","route","base","key","target","appOrigin","detail","css","SfxTemplateBuilder","__decorateClass","property","state"],"mappings":"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"}
|
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
import { LitElement as y, css as f, html as d, nothing as c } from "lit";
|
|
2
|
+
import { property as r, state as l } from "lit/decorators.js";
|
|
3
|
+
const N = 2, b = "design-templates:builder:ready", _ = "design-templates:builder:open", E = "design-templates:builder:close", U = "design-templates:builder:save", T = "design-templates:builder:error", g = "design-templates:builder:content-request", R = "design-templates:builder:content", O = "design-templates:builder:dirty", v = "design-templates:host:load", k = "design-templates:host:saved", n = {
|
|
4
|
+
SESSION_UUID: "suuid",
|
|
5
|
+
COMPANY_UUID: "cuuid",
|
|
6
|
+
PROJECT_UUID: "puuid",
|
|
7
|
+
SASS_KEY: "sassKey",
|
|
8
|
+
FILEROBOT_TOKEN: "ftoken",
|
|
9
|
+
/**
|
|
10
|
+
* Filerobot security-template key, the alternative to a Hub session. The app
|
|
11
|
+
* exchanges it for a sass key itself and runs in a reduced mode — see
|
|
12
|
+
* `AUTH_MODES`. Sent *instead of* `sassKey` + `suuid`, never alongside them.
|
|
13
|
+
*/
|
|
14
|
+
SEC_TEMPLATE: "secTemplate",
|
|
15
|
+
IFRAME: "iframe",
|
|
16
|
+
/** Origin of the embedding page; the app uses it as postMessage targetOrigin. */
|
|
17
|
+
EMBED_ORIGIN: "embedOrigin",
|
|
18
|
+
/**
|
|
19
|
+
* Accent colour for the editor chrome, as `#rgb` / `#rrggbb`. The app derives
|
|
20
|
+
* its whole accent ramp from it. Rejected server-side if it doesn't match
|
|
21
|
+
* that shape — it ends up inside a stylesheet.
|
|
22
|
+
*/
|
|
23
|
+
BRAND_COLOR: "brandColor",
|
|
24
|
+
/** Colour scheme for the editor chrome: `light` | `dark` | `auto`. */
|
|
25
|
+
THEME: "theme"
|
|
26
|
+
}, A = {
|
|
27
|
+
SESSION: "session",
|
|
28
|
+
SEC_TEMPLATE: "secTemplate"
|
|
29
|
+
}, L = /^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/;
|
|
30
|
+
function D(h) {
|
|
31
|
+
return h ? `/templates/${encodeURIComponent(h)}/edit` : "/templates/new";
|
|
32
|
+
}
|
|
33
|
+
const S = "/templates/embed";
|
|
34
|
+
var I = Object.defineProperty, i = (h, t, e, a) => {
|
|
35
|
+
for (var o = void 0, p = h.length - 1, m; p >= 0; p--)
|
|
36
|
+
(m = h[p]) && (o = m(t, e, o) || o);
|
|
37
|
+
return o && I(t, e, o), o;
|
|
38
|
+
};
|
|
39
|
+
const u = class u extends y {
|
|
40
|
+
constructor() {
|
|
41
|
+
super(...arguments), this.baseUrl = "", this.token = "", this.sassKey = "", this.sessionUuid = "", this.secTemplate = "", this.companyUuid = "", this.projectUuid = "", this.templateId = "", this.mode = "inline", this.stateless = !1, this.content = "", this.templateName = "", this.templateQuery = "", this.brandColor = "", this.theme = "", this.readyTimeout = 2e4, this._status = "idle", this._open = !1, this._src = "", this._contentRequested = !1, this._reportedStatelessRequired = !1, this._isDirty = !1, this._autoOpenDecided = !1, this._onMessage = (t) => {
|
|
42
|
+
if (!this._open || !t.origin || t.origin !== this._appOrigin) return;
|
|
43
|
+
const e = this.shadowRoot?.querySelector("iframe");
|
|
44
|
+
if (!e || t.source !== e.contentWindow) return;
|
|
45
|
+
const a = t.data;
|
|
46
|
+
if (!(!a || typeof a.type != "string"))
|
|
47
|
+
switch (a.type) {
|
|
48
|
+
case b:
|
|
49
|
+
case _:
|
|
50
|
+
this._clearHandshakeTimer(), this._status !== "ready" && (this._status = "ready", this._emit("ready")), a.type === _ && this._emit("open");
|
|
51
|
+
break;
|
|
52
|
+
case U:
|
|
53
|
+
this._emit("save", a.data);
|
|
54
|
+
break;
|
|
55
|
+
case g:
|
|
56
|
+
this._contentRequested = !0, this._sentKey = void 0, this._maybeSendContent();
|
|
57
|
+
break;
|
|
58
|
+
case R: {
|
|
59
|
+
const o = a.data;
|
|
60
|
+
this._sentKey = this._contentKey(o.content), this._emit("save", o);
|
|
61
|
+
break;
|
|
62
|
+
}
|
|
63
|
+
case O: {
|
|
64
|
+
const o = a.data;
|
|
65
|
+
this._isDirty = !!o?.isDirty, this._emit("dirtychange", { isDirty: this._isDirty });
|
|
66
|
+
break;
|
|
67
|
+
}
|
|
68
|
+
case E:
|
|
69
|
+
this._emit("close"), this.mode === "modal" && (this._open = !1);
|
|
70
|
+
break;
|
|
71
|
+
case T:
|
|
72
|
+
this._fail(a.data ?? { code: "unknown" });
|
|
73
|
+
break;
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
get status() {
|
|
78
|
+
return this._status;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Stateless mode: whether the editor holds edits that have not been handed
|
|
82
|
+
* back yet. Check this before calling `load()` — a swap discards them.
|
|
83
|
+
* Always false in DAM-backed mode, where the app owns saving.
|
|
84
|
+
*/
|
|
85
|
+
get isDirty() {
|
|
86
|
+
return this._isDirty;
|
|
87
|
+
}
|
|
88
|
+
/** Open the editor (loads the iframe). Optionally switch template first. */
|
|
89
|
+
open(t) {
|
|
90
|
+
t !== void 0 && (this.templateId = t), this._open = !0;
|
|
91
|
+
}
|
|
92
|
+
/** Close the editor and unload the iframe. Does not emit `close`. */
|
|
93
|
+
close() {
|
|
94
|
+
this._open = !1;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Stateless mode: load a template, opening the editor if needed. Equivalent
|
|
98
|
+
* to assigning `templateId` / `content` / `templateName` and calling `open()`.
|
|
99
|
+
*/
|
|
100
|
+
load({
|
|
101
|
+
content: t,
|
|
102
|
+
templateId: e,
|
|
103
|
+
name: a,
|
|
104
|
+
templateQuery: o
|
|
105
|
+
}) {
|
|
106
|
+
e !== void 0 && (this.templateId = e), a !== void 0 && (this.templateName = a), o !== void 0 && (this.templateQuery = o), this.content = t, this._open = !0;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Stateless mode: report back whether a `save` was persisted on your side.
|
|
110
|
+
*
|
|
111
|
+
* Optional. The editor clears its unsaved-changes flag as soon as it hands
|
|
112
|
+
* the content over, so not calling this leaves the previous behaviour intact.
|
|
113
|
+
* Calling it with `false` is what earns something: the editor restores the
|
|
114
|
+
* dirty flag and tells the user, rather than showing a failed write as saved.
|
|
115
|
+
*
|
|
116
|
+
* No-op outside stateless mode, where the app did the saving and has nothing
|
|
117
|
+
* to hear back about.
|
|
118
|
+
*/
|
|
119
|
+
confirmSave(t, e) {
|
|
120
|
+
this.stateless && this._postToApp({ type: k, data: { ok: t, message: e } });
|
|
121
|
+
}
|
|
122
|
+
connectedCallback() {
|
|
123
|
+
super.connectedCallback(), window.addEventListener("message", this._onMessage);
|
|
124
|
+
}
|
|
125
|
+
disconnectedCallback() {
|
|
126
|
+
super.disconnectedCallback(), window.removeEventListener("message", this._onMessage), this._clearHandshakeTimer();
|
|
127
|
+
}
|
|
128
|
+
willUpdate(t) {
|
|
129
|
+
super.willUpdate(t), this._autoOpenDecided || (this._autoOpenDecided = !0, this.mode === "inline" && (this._open = !0));
|
|
130
|
+
const e = this._computeSrc();
|
|
131
|
+
e !== this._src && (this._src = e, this._status = e ? "loading" : "idle");
|
|
132
|
+
}
|
|
133
|
+
updated(t) {
|
|
134
|
+
t.has("_src") && (this._clearHandshakeTimer(), this._contentRequested = !1, this._sentKey = void 0, this._isDirty && (this._isDirty = !1, this._emit("dirtychange", { isDirty: !1 })), this._src && this._startHandshakeTimer()), (t.has("content") || t.has("templateId") || t.has("templateName") || t.has("templateQuery")) && this._maybeSendContent();
|
|
135
|
+
}
|
|
136
|
+
render() {
|
|
137
|
+
const t = this._src ? d`<iframe
|
|
138
|
+
part="iframe"
|
|
139
|
+
title="Template builder"
|
|
140
|
+
src=${this._src}
|
|
141
|
+
allow="clipboard-read; clipboard-write"
|
|
142
|
+
></iframe>` : c, e = this._status === "loading" ? d`<div class="spinner" part="spinner"></div>` : c;
|
|
143
|
+
return this.mode === "modal" ? this._open ? d`<div class="overlay" part="overlay">
|
|
144
|
+
<div class="stage">${t}${e}</div>
|
|
145
|
+
</div>` : c : d`${t}${e}`;
|
|
146
|
+
}
|
|
147
|
+
_computeSrc() {
|
|
148
|
+
if (!this._open || !this.baseUrl || !this.token) return "";
|
|
149
|
+
if (this.secTemplate) {
|
|
150
|
+
if (!this.stateless)
|
|
151
|
+
return this._reportedStatelessRequired || (this._reportedStatelessRequired = !0, queueMicrotask(
|
|
152
|
+
() => this._fail({
|
|
153
|
+
code: "invalid-config",
|
|
154
|
+
message: "sec-template requires stateless mode — the app accepts a security template on the stateless embed route only."
|
|
155
|
+
})
|
|
156
|
+
)), "";
|
|
157
|
+
this._reportedStatelessRequired = !1;
|
|
158
|
+
} else if (!this.sassKey || !this.sessionUuid)
|
|
159
|
+
return "";
|
|
160
|
+
let t;
|
|
161
|
+
try {
|
|
162
|
+
const e = this.stateless ? S : D(this.templateId || void 0), a = this.baseUrl.endsWith("/") ? this.baseUrl : `${this.baseUrl}/`;
|
|
163
|
+
t = new URL(e.replace(/^\//, ""), a);
|
|
164
|
+
} catch {
|
|
165
|
+
return this._reportedBadBaseUrl !== this.baseUrl && (this._reportedBadBaseUrl = this.baseUrl, queueMicrotask(
|
|
166
|
+
() => this._fail({
|
|
167
|
+
code: "invalid-base-url",
|
|
168
|
+
message: `base-url is not a valid URL: ${this.baseUrl}`
|
|
169
|
+
})
|
|
170
|
+
)), "";
|
|
171
|
+
}
|
|
172
|
+
return this._reportedBadBaseUrl = void 0, t.searchParams.set(n.FILEROBOT_TOKEN, this.token), this.secTemplate ? t.searchParams.set(n.SEC_TEMPLATE, this.secTemplate) : (t.searchParams.set(n.SASS_KEY, this.sassKey), t.searchParams.set(n.SESSION_UUID, this.sessionUuid), this.companyUuid && t.searchParams.set(n.COMPANY_UUID, this.companyUuid), this.projectUuid && t.searchParams.set(n.PROJECT_UUID, this.projectUuid)), this.brandColor && t.searchParams.set(n.BRAND_COLOR, this.brandColor), this.theme && t.searchParams.set(n.THEME, this.theme), t.searchParams.set(n.IFRAME, "1"), t.searchParams.set(n.EMBED_ORIGIN, window.location.origin), t.toString();
|
|
173
|
+
}
|
|
174
|
+
get _appOrigin() {
|
|
175
|
+
try {
|
|
176
|
+
return new URL(this.baseUrl).origin;
|
|
177
|
+
} catch {
|
|
178
|
+
return null;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Deliver `content` to the app once both sides are ready: it has asked, and
|
|
183
|
+
* we have something new to give it. Skips a re-send of identical content so
|
|
184
|
+
* an unrelated re-render can't discard the user's in-progress edits.
|
|
185
|
+
*/
|
|
186
|
+
_maybeSendContent() {
|
|
187
|
+
if (!this.stateless || !this._contentRequested || !this.content) return;
|
|
188
|
+
const t = {
|
|
189
|
+
templateId: this.templateId || void 0,
|
|
190
|
+
content: this.content,
|
|
191
|
+
name: this.templateName || void 0,
|
|
192
|
+
templateQuery: this.templateQuery || void 0
|
|
193
|
+
}, e = this._contentKey(this.content);
|
|
194
|
+
e !== this._sentKey && this._postToApp({ type: v, data: t }) && (this._sentKey = e);
|
|
195
|
+
}
|
|
196
|
+
/** Identity of a delivered template, as compared against `_sentKey`. */
|
|
197
|
+
_contentKey(t) {
|
|
198
|
+
return JSON.stringify({
|
|
199
|
+
templateId: this.templateId || void 0,
|
|
200
|
+
content: t,
|
|
201
|
+
name: this.templateName || void 0,
|
|
202
|
+
templateQuery: this.templateQuery || void 0
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
/** Post into the iframe, targeted at the app origin. False if not mounted. */
|
|
206
|
+
_postToApp(t) {
|
|
207
|
+
const e = this.shadowRoot?.querySelector("iframe")?.contentWindow, a = this._appOrigin;
|
|
208
|
+
return !e || !a ? !1 : (e.postMessage(t, a), !0);
|
|
209
|
+
}
|
|
210
|
+
_startHandshakeTimer() {
|
|
211
|
+
this.readyTimeout <= 0 || (this._handshakeTimer = window.setTimeout(() => {
|
|
212
|
+
this._fail({
|
|
213
|
+
code: "handshake-timeout",
|
|
214
|
+
message: `No ready signal from ${this.baseUrl} within ${this.readyTimeout}ms. Check that this origin is in the app's frame-ancestors allowlist and that third-party cookies are not blocked.`
|
|
215
|
+
});
|
|
216
|
+
}, this.readyTimeout));
|
|
217
|
+
}
|
|
218
|
+
_clearHandshakeTimer() {
|
|
219
|
+
this._handshakeTimer !== void 0 && (window.clearTimeout(this._handshakeTimer), this._handshakeTimer = void 0);
|
|
220
|
+
}
|
|
221
|
+
_fail(t) {
|
|
222
|
+
this._clearHandshakeTimer(), this._status = "error", this._emit("error", t);
|
|
223
|
+
}
|
|
224
|
+
_emit(t, e) {
|
|
225
|
+
this.dispatchEvent(
|
|
226
|
+
new CustomEvent(t, { detail: e, bubbles: !0, composed: !0 })
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
};
|
|
230
|
+
u.styles = f`
|
|
231
|
+
:host {
|
|
232
|
+
display: block;
|
|
233
|
+
position: relative;
|
|
234
|
+
}
|
|
235
|
+
:host([mode='modal']) {
|
|
236
|
+
display: contents;
|
|
237
|
+
}
|
|
238
|
+
.overlay {
|
|
239
|
+
position: fixed;
|
|
240
|
+
inset: 0;
|
|
241
|
+
z-index: 2147483000;
|
|
242
|
+
background: rgba(0, 0, 0, 0.55);
|
|
243
|
+
display: flex;
|
|
244
|
+
}
|
|
245
|
+
.stage {
|
|
246
|
+
position: relative;
|
|
247
|
+
flex: 1;
|
|
248
|
+
display: flex;
|
|
249
|
+
}
|
|
250
|
+
iframe {
|
|
251
|
+
border: 0;
|
|
252
|
+
flex: 1;
|
|
253
|
+
width: 100%;
|
|
254
|
+
height: 100%;
|
|
255
|
+
}
|
|
256
|
+
.spinner {
|
|
257
|
+
position: absolute;
|
|
258
|
+
inset: 0;
|
|
259
|
+
margin: auto;
|
|
260
|
+
width: 32px;
|
|
261
|
+
height: 32px;
|
|
262
|
+
border: 3px solid rgba(128, 128, 128, 0.3);
|
|
263
|
+
border-top-color: currentColor;
|
|
264
|
+
border-radius: 50%;
|
|
265
|
+
animation: sfx-tb-spin 0.8s linear infinite;
|
|
266
|
+
pointer-events: none;
|
|
267
|
+
}
|
|
268
|
+
@keyframes sfx-tb-spin {
|
|
269
|
+
to {
|
|
270
|
+
transform: rotate(360deg);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
`;
|
|
274
|
+
let s = u;
|
|
275
|
+
i([
|
|
276
|
+
r({ attribute: "base-url" })
|
|
277
|
+
], s.prototype, "baseUrl");
|
|
278
|
+
i([
|
|
279
|
+
r()
|
|
280
|
+
], s.prototype, "token");
|
|
281
|
+
i([
|
|
282
|
+
r({ attribute: "sass-key" })
|
|
283
|
+
], s.prototype, "sassKey");
|
|
284
|
+
i([
|
|
285
|
+
r({ attribute: "session-uuid" })
|
|
286
|
+
], s.prototype, "sessionUuid");
|
|
287
|
+
i([
|
|
288
|
+
r({ attribute: "sec-template" })
|
|
289
|
+
], s.prototype, "secTemplate");
|
|
290
|
+
i([
|
|
291
|
+
r({ attribute: "company-uuid" })
|
|
292
|
+
], s.prototype, "companyUuid");
|
|
293
|
+
i([
|
|
294
|
+
r({ attribute: "project-uuid" })
|
|
295
|
+
], s.prototype, "projectUuid");
|
|
296
|
+
i([
|
|
297
|
+
r({ attribute: "template-id" })
|
|
298
|
+
], s.prototype, "templateId");
|
|
299
|
+
i([
|
|
300
|
+
r({ reflect: !0 })
|
|
301
|
+
], s.prototype, "mode");
|
|
302
|
+
i([
|
|
303
|
+
r({ type: Boolean, reflect: !0 })
|
|
304
|
+
], s.prototype, "stateless");
|
|
305
|
+
i([
|
|
306
|
+
r({ attribute: !1 })
|
|
307
|
+
], s.prototype, "content");
|
|
308
|
+
i([
|
|
309
|
+
r({ attribute: "template-name" })
|
|
310
|
+
], s.prototype, "templateName");
|
|
311
|
+
i([
|
|
312
|
+
r({ attribute: "template-query" })
|
|
313
|
+
], s.prototype, "templateQuery");
|
|
314
|
+
i([
|
|
315
|
+
r({ attribute: "brand-color" })
|
|
316
|
+
], s.prototype, "brandColor");
|
|
317
|
+
i([
|
|
318
|
+
r()
|
|
319
|
+
], s.prototype, "theme");
|
|
320
|
+
i([
|
|
321
|
+
r({ type: Number, attribute: "ready-timeout" })
|
|
322
|
+
], s.prototype, "readyTimeout");
|
|
323
|
+
i([
|
|
324
|
+
l()
|
|
325
|
+
], s.prototype, "_status");
|
|
326
|
+
i([
|
|
327
|
+
l()
|
|
328
|
+
], s.prototype, "_open");
|
|
329
|
+
i([
|
|
330
|
+
l()
|
|
331
|
+
], s.prototype, "_src");
|
|
332
|
+
i([
|
|
333
|
+
l()
|
|
334
|
+
], s.prototype, "_isDirty");
|
|
335
|
+
export {
|
|
336
|
+
A,
|
|
337
|
+
L as B,
|
|
338
|
+
n as E,
|
|
339
|
+
v as H,
|
|
340
|
+
N as P,
|
|
341
|
+
s as S,
|
|
342
|
+
E as a,
|
|
343
|
+
R as b,
|
|
344
|
+
g as c,
|
|
345
|
+
O as d,
|
|
346
|
+
T as e,
|
|
347
|
+
_ as f,
|
|
348
|
+
b as g,
|
|
349
|
+
U as h,
|
|
350
|
+
S as i,
|
|
351
|
+
k as j,
|
|
352
|
+
D as k
|
|
353
|
+
};
|
|
354
|
+
//# sourceMappingURL=template-builder-S33H_d5T.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"template-builder-S33H_d5T.js","sources":["../src/protocol.ts","../src/template-builder.ts"],"sourcesContent":["// ---------------------------------------------------------------------------\n// postMessage protocol between the design-templates app (inside the iframe)\n// and its embedder (the <sfx-template-builder> widget or the Hub).\n//\n// This module is the single source of truth for both sides: the app imports\n// it via `@scaleflex/template-builder/protocol` (workspace TS-source export),\n// the widget bundles it. Message *values* are wire format — never change an\n// existing string; add new messages instead. All changes must stay additive\n// so older widgets keep working against newer app deployments and vice versa.\n// ---------------------------------------------------------------------------\n\nexport const PROTOCOL_VERSION = 2\n\n// App → embedder ------------------------------------------------------------\n\n/** Editor mounted with valid auth — the embed handshake succeeded. */\nexport const BUILDER_READY = 'design-templates:builder:ready'\n/** Editor UI opened (kept for Hub backwards compatibility; implies ready). */\nexport const BUILDER_OPEN = 'design-templates:builder:open'\n/** Editor UI closed / unmounted. */\nexport const BUILDER_CLOSE = 'design-templates:builder:close'\n/** Template saved. `data` is absent on app deployments older than protocol v1. */\nexport const BUILDER_SAVE = 'design-templates:builder:save'\n/** The app cannot start (e.g. auth cookies missing/blocked). */\nexport const BUILDER_ERROR = 'design-templates:builder:error'\n/**\n * Stateless mode only (protocol v2). The editor mounted without a template and\n * is waiting for the host to send `HOST_LOAD`. Re-sent on nothing — the host\n * may answer late; the app keeps waiting until content arrives.\n */\nexport const BUILDER_CONTENT_REQUEST = 'design-templates:builder:content-request'\n/**\n * Stateless mode only (protocol v2). The user saved and the app is handing the\n * edited template back instead of uploading it. This is the stateless\n * counterpart to `BUILDER_SAVE` — a separate message so that embedders written\n * against v1 (which read `data.uuid` from a save they assume was persisted)\n * never receive a payload that was not, in fact, stored anywhere.\n */\nexport const BUILDER_CONTENT = 'design-templates:builder:content'\n/**\n * Stateless mode only (protocol v2). The unsaved-changes flag flipped. Sent so\n * a host can ask the user before swapping the template out from under them —\n * `HOST_LOAD` is honoured unconditionally and discards whatever was in\n * progress, and without this the host has no way to know there was anything to\n * lose.\n */\nexport const BUILDER_DIRTY = 'design-templates:builder:dirty'\n\nexport interface BuilderReadyMessage {\n type: typeof BUILDER_READY\n}\n\nexport interface BuilderOpenMessage {\n type: typeof BUILDER_OPEN\n}\n\nexport interface BuilderCloseMessage {\n type: typeof BUILDER_CLOSE\n}\n\nexport interface BuilderSaveData {\n uuid: string\n name?: string\n}\n\nexport interface BuilderSaveMessage {\n type: typeof BUILDER_SAVE\n data?: BuilderSaveData\n}\n\n/**\n * `auth` and `invalid-content` are the codes the app itself sends. The widget\n * adds `handshake-timeout` (no ready signal — typically blocked third-party\n * cookies or a missing frame-ancestors entry), `invalid-base-url` and\n * `invalid-config`.\n */\nexport type BuilderErrorCode =\n /**\n * The app could not authenticate. In `secTemplate` mode this also covers a\n * security-template key the Filerobot API refused to exchange for a sass key.\n */\n | 'auth'\n /** Stateless mode: the `HOST_LOAD` content could not be parsed as a template. */\n | 'invalid-content'\n | 'handshake-timeout'\n | 'invalid-base-url'\n /** Attributes that contradict each other, e.g. `sec-template` without `stateless`. */\n | 'invalid-config'\n | 'unknown'\n\nexport interface BuilderErrorData {\n code: BuilderErrorCode\n message?: string\n}\n\nexport interface BuilderErrorMessage {\n type: typeof BUILDER_ERROR\n data: BuilderErrorData\n}\n\nexport interface BuilderContentRequestMessage {\n type: typeof BUILDER_CONTENT_REQUEST\n}\n\nexport interface BuilderContentData {\n /**\n * The id the host supplied in `HOST_LOAD`, echoed back verbatim. Absent when\n * the host sent content without one.\n */\n templateId?: string\n /** The edited template, serialized as `.fdt` XML. */\n content: string\n /** Display name the host supplied, echoed back. */\n name?: string\n /**\n * `template_query` for the default render — layout, variable values and\n * locale. In DAM-backed mode this is stored as file metadata; a stateless\n * host must persist it alongside `content` or renders will fall back to\n * whatever defaults the XML alone implies.\n */\n templateQuery?: string\n}\n\nexport interface BuilderContentMessage {\n type: typeof BUILDER_CONTENT\n data: BuilderContentData\n}\n\nexport interface BuilderDirtyData {\n /** True when the editor holds edits that have not been handed back. */\n isDirty: boolean\n}\n\nexport interface BuilderDirtyMessage {\n type: typeof BUILDER_DIRTY\n data: BuilderDirtyData\n}\n\nexport type BuilderMessage =\n | BuilderReadyMessage\n | BuilderOpenMessage\n | BuilderCloseMessage\n | BuilderSaveMessage\n | BuilderErrorMessage\n | BuilderContentRequestMessage\n | BuilderContentMessage\n | BuilderDirtyMessage\n\n// Embedder → app --------------------------------------------------------------\n\n/**\n * Stateless mode only (protocol v2). Hands the app a template to edit. Sent in\n * answer to `BUILDER_CONTENT_REQUEST`, and again whenever the host swaps the\n * template without remounting the iframe.\n *\n * Content travels by postMessage rather than a URL param because template XML\n * routinely exceeds practical URL length limits.\n *\n * The app accepts this message only from the origin pinned as `embedOrigin`\n * when the session credentials were handed over, so an unrelated framing page\n * cannot inject a template into someone else's session.\n */\nexport const HOST_LOAD = 'design-templates:host:load'\n\nexport interface HostLoadData {\n /**\n * Opaque host-side identifier, echoed back on save. It is never used to\n * fetch anything, so it need not be a Filerobot uuid — any string the host\n * can map back to its own record works.\n */\n templateId?: string\n /** Template to edit, as `.fdt` XML. */\n content: string\n /** Display name for the editor header. */\n name?: string\n /**\n * `template_query` describing the render to open on — layout and variable\n * values, in the same `$key=value&$key2=value2` form the editor hands back\n * in `BuilderContentData.templateQuery`.\n *\n * Round-trips that value: a host that stored it on save and passes it back\n * here reopens the template exactly as it was left. Omitting it falls back\n * to the `default=` attributes in the XML, which is a different render\n * whenever the query overrode any of them.\n *\n * Applied as display state, not as an edit — it selects the layout and fills\n * variable values without marking the document dirty, so opening a template\n * and closing it again does not look like an unsaved change.\n */\n templateQuery?: string\n}\n\nexport interface HostLoadMessage {\n type: typeof HOST_LOAD\n data: HostLoadData\n}\n\n/**\n * Stateless mode only (protocol v2). Reports whether the host managed to\n * persist the content it received in `BUILDER_CONTENT`.\n *\n * Optional by design. The editor clears its unsaved-changes flag optimistically\n * when it posts `BUILDER_CONTENT`, so a host that never acks behaves exactly as\n * before. Sending `ok: false` is what buys something: the editor restores the\n * dirty flag and tells the user, instead of leaving a failed write looking\n * saved.\n */\nexport const HOST_SAVED = 'design-templates:host:saved'\n\nexport interface HostSavedData {\n /** False when the host could not persist the content. */\n ok: boolean\n /** Shown to the user when `ok` is false. */\n message?: string\n}\n\nexport interface HostSavedMessage {\n type: typeof HOST_SAVED\n data: HostSavedData\n}\n\nexport type HostMessage = HostLoadMessage | HostSavedMessage\n\n// Embed URL contract ----------------------------------------------------------\n\n/**\n * Query params the app's proxy middleware (`src/proxy.ts`) converts into auth\n * cookies on first navigation. Names are wire format.\n */\nexport const EMBED_PARAMS = {\n SESSION_UUID: 'suuid',\n COMPANY_UUID: 'cuuid',\n PROJECT_UUID: 'puuid',\n SASS_KEY: 'sassKey',\n FILEROBOT_TOKEN: 'ftoken',\n /**\n * Filerobot security-template key, the alternative to a Hub session. The app\n * exchanges it for a sass key itself and runs in a reduced mode — see\n * `AUTH_MODES`. Sent *instead of* `sassKey` + `suuid`, never alongside them.\n */\n SEC_TEMPLATE: 'secTemplate',\n IFRAME: 'iframe',\n /** Origin of the embedding page; the app uses it as postMessage targetOrigin. */\n EMBED_ORIGIN: 'embedOrigin',\n /**\n * Accent colour for the editor chrome, as `#rgb` / `#rrggbb`. The app derives\n * its whole accent ramp from it. Rejected server-side if it doesn't match\n * that shape — it ends up inside a stylesheet.\n */\n BRAND_COLOR: 'brandColor',\n /** Colour scheme for the editor chrome: `light` | `dark` | `auto`. */\n THEME: 'theme',\n} as const\n\n/** Values the `theme` param accepts. */\nexport type BuilderTheme = 'light' | 'dark' | 'auto'\n\n/**\n * How the embedder authenticated.\n *\n * - `session` — a Hub session (`suuid` + `sassKey` + `ftoken`). Full features.\n * - `secTemplate` — a Filerobot security-template key (`secTemplate` +\n * `ftoken`). A guest credential: no user identity and no Hub project, so the\n * app accepts it on {@link EMBED_ROUTE} only, and everything that reads the\n * Hub project model (metadata fields, regional variants, project branding)\n * comes back empty. Rendering, fonts and asset browsing work, scoped by\n * whatever the security template grants.\n *\n * Derived by the app from the params it received; named here so both sides use\n * the same vocabulary.\n */\nexport const AUTH_MODES = {\n SESSION: 'session',\n SEC_TEMPLATE: 'secTemplate',\n} as const\n\nexport type AuthMode = (typeof AUTH_MODES)[keyof typeof AUTH_MODES]\n\n/**\n * Shape the app requires of `brandColor`. Hex only: the value is interpolated\n * into a `:root { … }` rule, so anything that could carry CSS syntax is\n * refused rather than escaped. Mirrored in the app's proxy — keep in sync.\n */\nexport const BRAND_COLOR_PATTERN = /^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/\n\n/** Editor route for an existing template, or the new-template route. */\nexport function builderRoute(templateId?: string): string {\n return templateId\n ? `/templates/${encodeURIComponent(templateId)}/edit`\n : '/templates/new'\n}\n\n/**\n * Stateless editor route. Takes no template id — the id is an opaque host\n * value that arrives with the content over `HOST_LOAD`, not something the app\n * resolves against Filerobot, so it has no place in the URL.\n */\nexport const EMBED_ROUTE = '/templates/embed'\n","import { LitElement, html, css, nothing, type PropertyValues } from 'lit'\nimport { property, state } from 'lit/decorators.js'\nimport {\n BUILDER_CLOSE,\n BUILDER_CONTENT,\n BUILDER_CONTENT_REQUEST,\n BUILDER_DIRTY,\n BUILDER_ERROR,\n BUILDER_OPEN,\n BUILDER_READY,\n BUILDER_SAVE,\n EMBED_PARAMS,\n EMBED_ROUTE,\n HOST_LOAD,\n HOST_SAVED,\n builderRoute,\n type BuilderContentData,\n type BuilderContentMessage,\n type BuilderDirtyData,\n type BuilderDirtyMessage,\n type BuilderErrorData,\n type BuilderErrorMessage,\n type BuilderSaveData,\n type BuilderSaveMessage,\n type BuilderTheme,\n} from './protocol'\n\nexport type TemplateBuilderStatus = 'idle' | 'loading' | 'ready' | 'error'\n\n/**\n * `save` payload. Which variant arrives follows the mode the element was\n * configured in:\n * - DAM-backed (default) — `BuilderSaveData`; the app uploaded the template and\n * reports the resulting `uuid`.\n * - `stateless` — `BuilderContentData`; nothing was stored, and `content` is\n * the edited template for the host to persist.\n */\nexport type TemplateBuilderSaveDetail =\n | BuilderSaveData\n | BuilderContentData\n | undefined\n\nexport interface TemplateBuilderEventMap {\n ready: CustomEvent<void>\n open: CustomEvent<void>\n close: CustomEvent<void>\n save: CustomEvent<TemplateBuilderSaveDetail>\n error: CustomEvent<BuilderErrorData>\n dirtychange: CustomEvent<BuilderDirtyData>\n}\n\n/**\n * `<sfx-template-builder>` — embeds the Filerobot design-templates builder.\n *\n * The element owns an iframe pointed at a design-templates-app deployment,\n * passes auth via URL params (converted to cookies by the app's proxy), and\n * translates the app's postMessage protocol into DOM CustomEvents:\n * `ready`, `open`, `close`, `save`, `error`.\n *\n * Required: `base-url`, `token`, and one of two credentials:\n * - `sass-key` + `session-uuid` — a Hub session. Full features.\n * - `sec-template` — a Filerobot security-template key. No Hub session needed,\n * but it only works with `stateless`, and Hub-project features (metadata\n * fields, regional variants, project branding) come back empty.\n *\n * In `inline` mode the editor loads as soon as config is complete and fills\n * the host element (size it explicitly). In `modal` mode nothing renders\n * until `open()` is called; the editor then covers the viewport.\n *\n * Two ways to supply the template:\n * - **DAM-backed** (default) — set `template-id` to a Filerobot file uuid. The\n * app loads and saves it itself, and `save` reports the new uuid.\n * - **Stateless** — set `stateless` and assign `content`. The element sends the\n * template into the editor over postMessage and `save` returns the edited\n * document; nothing is stored on the Scaleflex side, and `template-id` is\n * just an opaque string echoed back. Rendering, fonts and asset browsing\n * still use the session's Filerobot tenant.\n *\n * `brand-color` and `theme` restyle the editor chrome to match the host page.\n * They do not touch the rendered template — its colours live in the document.\n */\nexport class SfxTemplateBuilder extends LitElement {\n static styles = css`\n :host {\n display: block;\n position: relative;\n }\n :host([mode='modal']) {\n display: contents;\n }\n .overlay {\n position: fixed;\n inset: 0;\n z-index: 2147483000;\n background: rgba(0, 0, 0, 0.55);\n display: flex;\n }\n .stage {\n position: relative;\n flex: 1;\n display: flex;\n }\n iframe {\n border: 0;\n flex: 1;\n width: 100%;\n height: 100%;\n }\n .spinner {\n position: absolute;\n inset: 0;\n margin: auto;\n width: 32px;\n height: 32px;\n border: 3px solid rgba(128, 128, 128, 0.3);\n border-top-color: currentColor;\n border-radius: 50%;\n animation: sfx-tb-spin 0.8s linear infinite;\n pointer-events: none;\n }\n @keyframes sfx-tb-spin {\n to {\n transform: rotate(360deg);\n }\n }\n `\n\n /** Origin + optional path prefix of the design-templates-app deployment. */\n @property({ attribute: 'base-url' }) baseUrl = ''\n /** Filerobot token (`ftoken`). */\n @property() token = ''\n @property({ attribute: 'sass-key' }) sassKey = ''\n @property({ attribute: 'session-uuid' }) sessionUuid = ''\n /**\n * Filerobot security-template key — the alternative to `sass-key` +\n * `session-uuid` for hosts with no Hub session to hand over. Requires\n * `stateless`, and degrades the features that come from the Hub project\n * model (metadata fields, regional variants, project branding). When set it\n * wins: neither `sass-key` nor `session-uuid` is passed to the app.\n */\n @property({ attribute: 'sec-template' }) secTemplate = ''\n @property({ attribute: 'company-uuid' }) companyUuid = ''\n @property({ attribute: 'project-uuid' }) projectUuid = ''\n /**\n * DAM-backed mode: the Filerobot uuid to load; empty opens the new-template\n * flow. Stateless mode: an opaque host id, echoed back on `save`.\n */\n @property({ attribute: 'template-id' }) templateId = ''\n @property({ reflect: true }) mode: 'inline' | 'modal' = 'inline'\n /**\n * Hand the template in and take it back out instead of letting the app read\n * and write Filerobot. Requires `content`.\n */\n @property({ type: Boolean, reflect: true }) stateless = false\n /**\n * Stateless mode: the template to edit, as `.fdt` XML. Property only — templates\n * routinely exceed practical attribute/URL sizes, so it is never reflected.\n * Assigning a different value while open loads it into the running editor.\n */\n @property({ attribute: false }) content = ''\n /** Stateless mode: display name for the editor header. */\n @property({ attribute: 'template-name' }) templateName = ''\n /**\n * Stateless mode: the `template_query` to open on — the value handed back in\n * the `save` payload. Pass back what you stored and the editor reopens on the\n * same layout and variable values; leave it empty and the render falls back\n * to the XML's own `default=` attributes.\n */\n @property({ attribute: 'template-query' }) templateQuery = ''\n /**\n * Accent colour for the editor chrome, as `#rgb` / `#rrggbb`. The app derives\n * buttons, focus rings and highlights from it. Empty keeps the Scaleflex\n * default. Themes the editor UI only — never the rendered template, whose\n * colours live in the document.\n */\n @property({ attribute: 'brand-color' }) brandColor = ''\n /** Colour scheme for the editor chrome. Empty leaves the app's own default. */\n @property() theme: BuilderTheme | '' = ''\n /** Ms to wait for the app's ready signal before emitting `error`. 0 disables. */\n @property({ type: Number, attribute: 'ready-timeout' }) readyTimeout = 20000\n\n @state() private _status: TemplateBuilderStatus = 'idle'\n @state() private _open = false\n @state() private _src = ''\n\n private _handshakeTimer?: number\n /**\n * The app asked for content. Tracked because the request and the `content`\n * assignment race: whichever lands second triggers the send.\n */\n private _contentRequested = false\n /**\n * Identity of the template already delivered, so an unrelated re-render does\n * not resend it and discard the user's edits. Covers the id and name too, not\n * just the content: two host records can hold byte-identical templates, and\n * resending only on content change would leave the app echoing a stale id\n * back on save.\n */\n private _sentKey?: string\n /**\n * The `baseUrl` value already reported as unparseable. `_computeSrc()` runs\n * on every update cycle, so without this a bad URL re-emits `error` forever —\n * once per render, since the error status it sets is already in place after\n * the first.\n */\n private _reportedBadBaseUrl?: string\n /**\n * Whether the sec-template-without-stateless mistake has been reported. Same\n * reason as `_reportedBadBaseUrl`: `_computeSrc()` runs every update cycle\n * and the error status it sets is already in place after the first pass.\n */\n private _reportedStatelessRequired = false\n\n @state() private _isDirty = false\n\n get status(): TemplateBuilderStatus {\n return this._status\n }\n\n /**\n * Stateless mode: whether the editor holds edits that have not been handed\n * back yet. Check this before calling `load()` — a swap discards them.\n * Always false in DAM-backed mode, where the app owns saving.\n */\n get isDirty(): boolean {\n return this._isDirty\n }\n\n /** Open the editor (loads the iframe). Optionally switch template first. */\n open(templateId?: string): void {\n if (templateId !== undefined) this.templateId = templateId\n this._open = true\n }\n\n /** Close the editor and unload the iframe. Does not emit `close`. */\n close(): void {\n this._open = false\n }\n\n /**\n * Stateless mode: load a template, opening the editor if needed. Equivalent\n * to assigning `templateId` / `content` / `templateName` and calling `open()`.\n */\n load({\n content,\n templateId,\n name,\n templateQuery,\n }: {\n content: string\n templateId?: string\n name?: string\n templateQuery?: string\n }): void {\n if (templateId !== undefined) this.templateId = templateId\n if (name !== undefined) this.templateName = name\n // Assigned before `content`: all four ship as one HOST_LOAD, and leaving a\n // previous template's query in place while the new content goes out would\n // open the new document on the old layout and values.\n if (templateQuery !== undefined) this.templateQuery = templateQuery\n this.content = content\n this._open = true\n }\n\n /**\n * Stateless mode: report back whether a `save` was persisted on your side.\n *\n * Optional. The editor clears its unsaved-changes flag as soon as it hands\n * the content over, so not calling this leaves the previous behaviour intact.\n * Calling it with `false` is what earns something: the editor restores the\n * dirty flag and tells the user, rather than showing a failed write as saved.\n *\n * No-op outside stateless mode, where the app did the saving and has nothing\n * to hear back about.\n */\n confirmSave(ok: boolean, message?: string): void {\n if (!this.stateless) return\n this._postToApp({ type: HOST_SAVED, data: { ok, message } })\n }\n\n /**\n * Whether the inline-implies-open decision has been made. It cannot be made\n * in `connectedCallback`: frameworks insert the element first and assign\n * properties afterwards in the same task (the React wrapper does), so at\n * connect time `mode` may still hold its `'inline'` default — deciding there\n * flashes a modal's full-viewport overlay open on mount. By the first update\n * cycle the real value has settled.\n */\n private _autoOpenDecided = false\n\n connectedCallback(): void {\n super.connectedCallback()\n window.addEventListener('message', this._onMessage)\n }\n\n disconnectedCallback(): void {\n super.disconnectedCallback()\n window.removeEventListener('message', this._onMessage)\n this._clearHandshakeTimer()\n }\n\n protected willUpdate(changed: PropertyValues): void {\n super.willUpdate(changed)\n if (!this._autoOpenDecided) {\n this._autoOpenDecided = true\n if (this.mode === 'inline') this._open = true\n }\n const src = this._computeSrc()\n if (src !== this._src) {\n this._src = src\n this._status = src ? 'loading' : 'idle'\n }\n }\n\n protected updated(changed: PropertyValues): void {\n if (changed.has('_src')) {\n this._clearHandshakeTimer()\n // A new document means a new app instance: it has not asked for content\n // yet, and nothing has been delivered to it.\n this._contentRequested = false\n this._sentKey = undefined\n if (this._isDirty) {\n this._isDirty = false\n this._emit('dirtychange', { isDirty: false })\n }\n if (this._src) this._startHandshakeTimer()\n }\n // Swapping any part of the template on a running editor reloads it. The id\n // matters as much as the content: a host moving between two identical\n // templates must not leave the app saving under the previous id.\n if (\n changed.has('content') ||\n changed.has('templateId') ||\n changed.has('templateName') ||\n changed.has('templateQuery')\n ) {\n this._maybeSendContent()\n }\n }\n\n render() {\n const frame = this._src\n ? html`<iframe\n part=\"iframe\"\n title=\"Template builder\"\n src=${this._src}\n allow=\"clipboard-read; clipboard-write\"\n ></iframe>`\n : nothing\n const spinner =\n this._status === 'loading'\n ? html`<div class=\"spinner\" part=\"spinner\"></div>`\n : nothing\n\n if (this.mode === 'modal') {\n return this._open\n ? html`<div class=\"overlay\" part=\"overlay\">\n <div class=\"stage\">${frame}${spinner}</div>\n </div>`\n : nothing\n }\n return html`${frame}${spinner}`\n }\n\n private _computeSrc(): string {\n if (!this._open) return ''\n if (!this.baseUrl || !this.token) return ''\n if (this.secTemplate) {\n // A security template is a guest credential with no user identity behind\n // it, so the app takes it on the stateless route only. Saying so here\n // turns a config mistake into a message instead of a login redirect the\n // host sees as `handshake-timeout`.\n if (!this.stateless) {\n if (!this._reportedStatelessRequired) {\n this._reportedStatelessRequired = true\n queueMicrotask(() =>\n this._fail({\n code: 'invalid-config',\n message:\n 'sec-template requires stateless mode — the app accepts a ' +\n 'security template on the stateless embed route only.',\n }),\n )\n }\n return ''\n }\n // Cleared on a valid pass so a host that fixes the combination and later\n // breaks it again is told again, matching `_reportedBadBaseUrl`.\n this._reportedStatelessRequired = false\n } else if (!this.sassKey || !this.sessionUuid) {\n return ''\n }\n let url: URL\n try {\n // Stateless mode keeps the id out of the URL — it is a host-side value\n // the app never resolves, and the content arrives by postMessage.\n const route = this.stateless\n ? EMBED_ROUTE\n : builderRoute(this.templateId || undefined)\n // Resolve relative to the base, not the origin: routes are absolute\n // paths, and `new URL('/x', 'https://host/app')` would silently drop\n // the documented path prefix, 404 on subpath deployments, and surface\n // only as a handshake-timeout.\n const base = this.baseUrl.endsWith('/') ? this.baseUrl : `${this.baseUrl}/`\n url = new URL(route.replace(/^\\//, ''), base)\n } catch {\n // Report each bad value once. This runs on every update cycle, and\n // `_fail` sets a status that is already 'error' by the second pass, so\n // there is no state change to fall out of the loop on.\n if (this._reportedBadBaseUrl !== this.baseUrl) {\n this._reportedBadBaseUrl = this.baseUrl\n // Emitted from a state-compute path; defer so consumers attached after\n // this update cycle still receive it.\n queueMicrotask(() =>\n this._fail({\n code: 'invalid-base-url',\n message: `base-url is not a valid URL: ${this.baseUrl}`,\n }),\n )\n }\n return ''\n }\n this._reportedBadBaseUrl = undefined\n url.searchParams.set(EMBED_PARAMS.FILEROBOT_TOKEN, this.token)\n if (this.secTemplate) {\n // Exclusive with the session credentials: the app reads the mode off\n // which of the two arrived, and the Hub uuids below name a project it\n // cannot look up without a session anyway.\n url.searchParams.set(EMBED_PARAMS.SEC_TEMPLATE, this.secTemplate)\n } else {\n url.searchParams.set(EMBED_PARAMS.SASS_KEY, this.sassKey)\n url.searchParams.set(EMBED_PARAMS.SESSION_UUID, this.sessionUuid)\n if (this.companyUuid) {\n url.searchParams.set(EMBED_PARAMS.COMPANY_UUID, this.companyUuid)\n }\n if (this.projectUuid) {\n url.searchParams.set(EMBED_PARAMS.PROJECT_UUID, this.projectUuid)\n }\n }\n if (this.brandColor) {\n url.searchParams.set(EMBED_PARAMS.BRAND_COLOR, this.brandColor)\n }\n if (this.theme) {\n url.searchParams.set(EMBED_PARAMS.THEME, this.theme)\n }\n url.searchParams.set(EMBED_PARAMS.IFRAME, '1')\n url.searchParams.set(EMBED_PARAMS.EMBED_ORIGIN, window.location.origin)\n return url.toString()\n }\n\n private get _appOrigin(): string | null {\n try {\n return new URL(this.baseUrl).origin\n } catch {\n return null\n }\n }\n\n private _onMessage = (event: MessageEvent): void => {\n if (!this._open) return\n if (!event.origin || event.origin !== this._appOrigin) return\n const iframe = this.shadowRoot?.querySelector('iframe')\n if (!iframe) return\n // Ignore messages from other frames of the same app origin. Strict: a\n // missing source is not given the benefit of the doubt — the app side\n // (`readHostLoadMessage`) applies the same rule.\n if (event.source !== iframe.contentWindow) return\n\n const msg = event.data as { type?: unknown } | null\n if (!msg || typeof msg.type !== 'string') return\n\n switch (msg.type) {\n case BUILDER_READY:\n case BUILDER_OPEN:\n this._clearHandshakeTimer()\n if (this._status !== 'ready') {\n this._status = 'ready'\n this._emit('ready')\n }\n if (msg.type === BUILDER_OPEN) this._emit('open')\n break\n case BUILDER_SAVE:\n this._emit('save', (msg as BuilderSaveMessage).data)\n break\n case BUILDER_CONTENT_REQUEST:\n this._contentRequested = true\n // A fresh request is authoritative: the app is saying it holds no\n // template. It may have remounted or reloaded inside an unchanged\n // iframe, so resend even if this content went out already — otherwise\n // the editor waits on a skeleton forever.\n this._sentKey = undefined\n this._maybeSendContent()\n break\n case BUILDER_CONTENT: {\n // Stateless save. Surfaced as `save` so hosts have one event to bind\n // regardless of mode; the detail shape follows the mode they chose.\n const data = (msg as BuilderContentMessage).data\n // The saved document is what the editor now holds. A host that stores\n // it and echoes it back into `content` — the natural controlled\n // pattern — must not trigger a HOST_LOAD reload that wipes the\n // editor's undo history behind a skeleton flash.\n this._sentKey = this._contentKey(data.content)\n this._emit('save', data)\n break\n }\n case BUILDER_DIRTY: {\n const data = (msg as BuilderDirtyMessage).data\n this._isDirty = !!data?.isDirty\n this._emit('dirtychange', { isDirty: this._isDirty })\n break\n }\n case BUILDER_CLOSE:\n this._emit('close')\n if (this.mode === 'modal') this._open = false\n break\n case BUILDER_ERROR:\n this._fail((msg as BuilderErrorMessage).data ?? { code: 'unknown' })\n break\n }\n }\n\n /**\n * Deliver `content` to the app once both sides are ready: it has asked, and\n * we have something new to give it. Skips a re-send of identical content so\n * an unrelated re-render can't discard the user's in-progress edits.\n */\n private _maybeSendContent(): void {\n if (!this.stateless || !this._contentRequested || !this.content) return\n\n const data = {\n templateId: this.templateId || undefined,\n content: this.content,\n name: this.templateName || undefined,\n templateQuery: this.templateQuery || undefined,\n }\n const key = this._contentKey(this.content)\n if (key === this._sentKey) return\n\n if (!this._postToApp({ type: HOST_LOAD, data })) return\n this._sentKey = key\n }\n\n /** Identity of a delivered template, as compared against `_sentKey`. */\n private _contentKey(content: string): string {\n return JSON.stringify({\n templateId: this.templateId || undefined,\n content,\n name: this.templateName || undefined,\n templateQuery: this.templateQuery || undefined,\n })\n }\n\n /** Post into the iframe, targeted at the app origin. False if not mounted. */\n private _postToApp(message: unknown): boolean {\n const target = this.shadowRoot?.querySelector('iframe')?.contentWindow\n const appOrigin = this._appOrigin\n if (!target || !appOrigin) return false\n target.postMessage(message, appOrigin)\n return true\n }\n\n private _startHandshakeTimer(): void {\n if (this.readyTimeout <= 0) return\n this._handshakeTimer = window.setTimeout(() => {\n this._fail({\n code: 'handshake-timeout',\n message:\n `No ready signal from ${this.baseUrl} within ${this.readyTimeout}ms. ` +\n 'Check that this origin is in the app\\'s frame-ancestors allowlist ' +\n 'and that third-party cookies are not blocked.',\n })\n }, this.readyTimeout)\n }\n\n private _clearHandshakeTimer(): void {\n if (this._handshakeTimer !== undefined) {\n window.clearTimeout(this._handshakeTimer)\n this._handshakeTimer = undefined\n }\n }\n\n private _fail(data: BuilderErrorData): void {\n this._clearHandshakeTimer()\n this._status = 'error'\n this._emit('error', data)\n }\n\n private _emit<T>(name: string, detail?: T): void {\n this.dispatchEvent(\n new CustomEvent(name, { detail, bubbles: true, composed: true }),\n )\n }\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n 'sfx-template-builder': SfxTemplateBuilder\n }\n}\n"],"names":["PROTOCOL_VERSION","BUILDER_READY","BUILDER_OPEN","BUILDER_CLOSE","BUILDER_SAVE","BUILDER_ERROR","BUILDER_CONTENT_REQUEST","BUILDER_CONTENT","BUILDER_DIRTY","HOST_LOAD","HOST_SAVED","EMBED_PARAMS","AUTH_MODES","BRAND_COLOR_PATTERN","builderRoute","templateId","EMBED_ROUTE","_SfxTemplateBuilder","LitElement","event","iframe","msg","data","content","name","templateQuery","ok","message","changed","src","frame","html","nothing","spinner","url","route","base","key","target","appOrigin","detail","css","SfxTemplateBuilder","__decorateClass","property","state"],"mappings":"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"}
|