@scaleflex/template-builder 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/skills/integrate-template-builder/SKILL.md +27 -21
- package/CHANGELOG.md +168 -4
- package/README.md +207 -52
- package/dist/dam-store.d.ts +92 -0
- package/dist/define.cjs +1 -1
- package/dist/define.js +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +9 -8
- package/dist/protocol.d.ts +60 -2
- package/dist/react.cjs +1 -1
- package/dist/react.cjs.map +1 -1
- package/dist/react.d.ts +34 -1
- package/dist/react.js +43 -28
- package/dist/react.js.map +1 -1
- package/dist/template-builder-B9Cwo_Q-.js +651 -0
- package/dist/template-builder-B9Cwo_Q-.js.map +1 -0
- package/dist/template-builder-Byqg1q93.cjs +53 -0
- package/dist/template-builder-Byqg1q93.cjs.map +1 -0
- package/dist/template-builder.d.ts +164 -4
- package/package.json +1 -1
- package/src/dam-store.ts +388 -0
- package/src/index.ts +2 -0
- package/src/protocol.ts +64 -2
- package/src/react.ts +111 -27
- package/src/template-builder.ts +405 -7
- package/dist/template-builder-CK2Zlo7E.cjs +0 -53
- package/dist/template-builder-CK2Zlo7E.cjs.map +0 -1
- package/dist/template-builder-De0hRO4s.js +0 -380
- package/dist/template-builder-De0hRO4s.js.map +0 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"template-builder-B9Cwo_Q-.js","sources":["../src/protocol.ts","../src/dam-store.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 = 3\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 * An empty `.fdt` document: no layouts, no layers, no variables. What the\n * widget sends as `HOST_LOAD` content when the host asked for a new template\n * (`new-template`) instead of supplying one, so starting from scratch costs a\n * host no knowledge of the template format.\n *\n * The editor opens on its empty state — \"No layouts yet. Click + Add to create\n * one.\" — and the user picks the canvas size there. Save is refused until a\n * layout exists, and hands back a fully-formed document serialized by the app,\n * not this skeleton.\n *\n * Sent as ordinary `HOST_LOAD` content rather than a new message so it works\n * against app deployments that predate this widget version: the document is\n * the whole signal, and every app that can parse a template can parse this.\n *\n * `version` tracks the app's `TEMPLATE_VERSION` for the benefit of whoever\n * reads this next: nothing consumes it. The parser never looks at it, and the\n * backend never sees this document — the app refuses to save a template with\n * no layouts, so what reaches the render pipeline was re-serialized by the app\n * with a layout present. A widget lagging the app by a version still loads.\n * `design-templates` pins the pair in\n * `src/lib/xml/__tests__/blank-template.test.ts`.\n */\nexport const BLANK_TEMPLATE_XML =\n '<?xml version=\"1.0\" encoding=\"UTF-8\"?>\\n' +\n '<template><templateInfo><version>0.3</version></templateInfo>' +\n '<rootStack/><layouts/><variables/></template>'\n\n/**\n * Host-supplied editor configuration (protocol v3). Sent once the app reports\n * `BUILDER_READY`, and again whenever the host changes it.\n *\n * Separate from `HOST_LOAD` because it is not per-template and because it must\n * also reach DAM-backed embeds, which never receive a `HOST_LOAD` at all — the\n * app loads those templates itself.\n *\n * Purely additive: an app deployment that predates this message ignores it and\n * behaves exactly as before, so a newer widget stays compatible with an older\n * app. Held to the same origin bar as `HOST_LOAD`.\n */\nexport const HOST_CONFIG = 'design-templates:host:config'\n\n/**\n * One field of a host-supplied metadata model, offered in the editor as the\n * \"Custom metadata\" value source.\n *\n * The model is a vocabulary, not data: it names the fields the host can fill at\n * render time, so an author can bind a variable to `sku` rather than having to\n * remember that the variable's slug happens to mean the SKU. No value travels\n * with it — the host substitutes one by putting `$slug=value` in the render\n * query, exactly as it would for a free-text variable.\n *\n * This is what makes named fields workable in `secTemplate` / stateless embeds,\n * where the Hub project model (and with it the \"File metadata\" source) is\n * unavailable.\n */\nexport interface CustomMetadataField {\n /**\n * Stable identifier stored in the template as `custom_ckey`. The host's own\n * key for the field — the app never resolves it against anything.\n */\n key: string\n /** Label shown in the editor's field picker. Falls back to `key` when empty. */\n title?: string\n /** Optional section header, used to group fields in the picker. */\n group?: string\n}\n\nexport interface HostConfigData {\n /**\n * Metadata model offered as the \"Custom metadata\" value source. Omitted or\n * empty hides that source in the editor, so a host that sends nothing sees\n * the two sources it always had.\n */\n customMetadata?: CustomMetadataField[]\n /**\n * Display name for the custom-metadata value source in the editor's UI\n * (source dropdowns, properties-panel section). Defaults to \"Custom\n * metadata\"; a host can rename it after its own domain — e.g. \"External\n * metadata\" or \"Product attributes\". Pure wording: the stored template is\n * unaffected.\n */\n customMetadataLabel?: string\n}\n\nexport interface HostConfigMessage {\n type: typeof HOST_CONFIG\n data: HostConfigData\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 | HostConfigMessage\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","/**\n * The `dam-store` save path: upload an edited template to Filerobot so the\n * CDN can render it.\n *\n * A stateless save hands the host raw XML — but the CDN renders only stored\n * files, so a host that wants render URLs (previews, production banners) needs\n * a copy in the DAM too. With `dam-store` the element makes that copy itself,\n * with the same multipart upload the DAM-backed editor uses, and the `save`\n * event carries the stored file's links next to the raw data.\n *\n * The raw `content` remains the host's copy of record: nothing here changes\n * what the save event has always carried.\n */\n\nimport type { BuilderContentData } from './protocol'\n\nexport const FILEROBOT_API = 'https://api.filerobot.com'\n\n/** Credentials the element already holds; one of sassKey / secTemplate. */\nexport interface DamStoreAuth {\n token: string\n sassKey?: string\n secTemplate?: string\n sessionUuid?: string\n companyUuid?: string\n projectUuid?: string\n}\n\n/** The stored copy's links, carried on the `save` event as `detail.stored`. */\nexport interface StoredTemplate {\n /** DAM file uuid of the stored `.fdt`. */\n uuid: string\n /**\n * CDN URL of the stored file, with its current `?vh=` cache key — append a\n * template query to render it. Empty when the file record could not be read\n * back after the upload (the file is stored regardless).\n */\n url: string\n}\n\nexport interface FileRecord {\n uuid?: string\n name?: string\n folder?: { name?: string }\n url?: { cdn?: string; public?: string; path?: string }\n}\n\n/**\n * A security template is not a key — it is exchanged for a short-lived sass\n * key first, the template authenticating its own exchange. Same call the app\n * and the asset picker make.\n *\n * Exported (with `apiHeaders` / `getFileRecord`) for the demo page, which\n * plays the host half of the same API conversation — one implementation of\n * the auth rules, not two drifting copies.\n */\nexport async function resolveKey(auth: DamStoreAuth): Promise<string> {\n if (!auth.secTemplate) return auth.sassKey ?? ''\n const res = await fetch(\n `${FILEROBOT_API}/${encodeURIComponent(auth.token)}/v5/key/${encodeURIComponent(auth.secTemplate)}`,\n { headers: { 'X-Filerobot-Key': auth.secTemplate } },\n )\n const body = (await res.json().catch(() => ({}))) as { key?: string; msg?: string }\n if (!res.ok || !body.key) {\n throw new Error(body.msg ?? `key exchange failed: ${res.status}`)\n }\n return body.key\n}\n\n/**\n * Session scope only: a minted key carries its own, and pairing it with a\n * session's uuids would mix one mode's key with the other mode's scope.\n */\nexport function apiHeaders(auth: DamStoreAuth, key: string): Record<string, string> {\n const headers: Record<string, string> = { 'X-Filerobot-Key': key }\n if (!auth.secTemplate) {\n if (auth.sessionUuid) headers['X-Session-Token'] = auth.sessionUuid\n if (auth.companyUuid) headers['X-Company-Token'] = auth.companyUuid\n if (auth.projectUuid) headers['X-Project-Token'] = auth.projectUuid\n }\n return headers\n}\n\n/**\n * Whether a template id plausibly names a DAM file (hex-and-dashes uuid).\n * Opaque host ids ('demo-1', 'sample-spring-banner') never do — looking them\n * up would waste a round trip per save and couple every save to whichever\n * status the API happens to answer a malformed id with.\n */\nexport function looksLikeDamFileUuid(id: string): boolean {\n return /^[0-9a-f][0-9a-f-]{18,}$/i.test(id)\n}\n\n/**\n * One file's record. `null` means the identifier names nothing — a 404/gone,\n * a 4xx rejecting the id itself, or the API's not-found envelope — all normal\n * answers here. What THROWS is a failure to answer (auth, rate limit, 5xx,\n * network): collapsing those into null would make a transient blip read as\n * \"file gone\", and the callers act on that — re-homing an existing template\n * into the fallback folder as a duplicate, or failing an unchanged re-save.\n */\nexport async function getFileRecord(\n auth: DamStoreAuth,\n key: string,\n uuid: string,\n): Promise<FileRecord | null> {\n let res: Response\n try {\n res = await fetch(\n `${FILEROBOT_API}/${encodeURIComponent(auth.token)}/v5/files/${encodeURIComponent(uuid)}`,\n { headers: apiHeaders(auth, key) },\n )\n } catch (err) {\n throw new Error(\n `file lookup failed: ${err instanceof Error ? err.message : String(err)}`,\n )\n }\n const body = (await res.json().catch(() => ({}))) as {\n status?: string\n msg?: string\n message?: string\n file?: FileRecord\n }\n // \"This identifier names nothing\" — not an error worth failing a save for.\n if ([400, 404, 410, 422].includes(res.status)) return null\n const message = body.msg ?? body.message ?? ''\n if (!res.ok || body.status === 'error') {\n if (/not[ _-]?found|does not exist|no such file/i.test(message)) return null\n throw new Error(message || `file lookup failed: ${res.status}`)\n }\n return body.file ?? null\n}\n\n/**\n * The newest file with exactly this name in a folder, or null. Used as a last\n * resort by the unchanged-content conflict path; best-effort by design.\n */\nasync function findByName(\n auth: DamStoreAuth,\n key: string,\n fileName: string,\n folder: string,\n): Promise<FileRecord | null> {\n const url = new URL(\n `${FILEROBOT_API}/${encodeURIComponent(auth.token)}/v5/files`,\n )\n url.searchParams.set('q', `name:\"${fileName}\"`)\n url.searchParams.set('folder', folder)\n url.searchParams.set('sort', 'modified_at:desc')\n url.searchParams.set('limit', '5')\n const res = await fetch(url, { headers: apiHeaders(auth, key) })\n const body = (await res.json().catch(() => ({}))) as {\n status?: string\n files?: FileRecord[]\n }\n if (!res.ok || body.status === 'error') return null\n return body.files?.find((f) => f.name === fileName) ?? null\n}\n\n/**\n * FNV-1a 32-bit of the host's template id — the deterministic filename\n * suffix that keeps one document on one DAM file across page reloads.\n */\nfunction hashId(id: string): string {\n let h = 0x811c9dc5\n for (let i = 0; i < id.length; i++) {\n h ^= id.charCodeAt(i)\n h = Math.imul(h, 0x01000193)\n }\n return (h >>> 0).toString(36)\n}\n\n/** The folder half of a file's URL path (mirrors the app's folderPathFromFile). */\nfunction folderFromPath(rawPath: string): string {\n let path: string\n try {\n path = decodeURIComponent(rawPath)\n } catch {\n path = rawPath\n }\n const segments = path.split('/').filter(Boolean)\n segments.pop()\n return '/' + segments.join('/')\n}\n\n/**\n * The `$slug=value` pairs of custom-metadata-bound variables, dropped from the\n * query stored as the file's `template_query` metadata. The stored default has\n * to be record-agnostic: the editor hands back whatever values the host opened\n * it with, and storing those would pin one record's data as the template's own\n * default. The binding lives in the XML (`custom_ckey`); the value belongs to\n * the record and goes back in at render time.\n */\nfunction stripBoundValues(templateQuery: string, content: string): string {\n if (!templateQuery) return templateQuery\n let bound: NodeListOf<Element>\n try {\n const doc = new DOMParser().parseFromString(content, 'application/xml')\n if (doc.querySelector('parsererror')) return templateQuery\n bound = doc.querySelectorAll(\n 'variable[custom_ckey][source=\"URL\"][type=\"text_placeholder\"]',\n )\n } catch {\n return templateQuery\n }\n if (bound.length === 0) return templateQuery\n const slugs = new Set([...bound].map((el) => el.getAttribute('name') ?? ''))\n return templateQuery\n .split('&')\n .filter(Boolean)\n .filter((part) => !slugs.has(part.split('=')[0].replace(/^(\\$|%24)/, '')))\n .join('&')\n}\n\n/**\n * \"Content unchanged\" — the stored file already IS this save. Filerobot\n * reports it inconsistently: with a 2xx or an error status alike, at the top\n * level or inside a per-file `files.failed[]` entry.\n */\n/** Uniquifies first-save filenames within a page (Date.now can collide). */\nlet uploadSeq = 0\n\nconst UNCHANGED_UPLOAD_CODES = new Set([\n 'ERROR_SHA1_CONFLICT',\n 'SAME_ASSET_EXISTS_SKIP_UPLOAD',\n])\n\ninterface UploadResponseBody {\n status?: string\n code?: string\n msg?: string\n message?: string\n file?: FileRecord\n files?: {\n uploaded?: FileRecord[]\n failed?: Array<{ code?: string }>\n }\n}\n\n/**\n * Store one save in the DAM and return the stored copy's links.\n *\n * Uploads into the folder the file this template is already stored as lives\n * in — the host's `templateId` when it names a DAM file, else `knownUuid`\n * (the copy a previous save in this session made; hosts persist `stored.uuid`\n * rather than echoing it into `template-id`, which would reload the editor) —\n * so same name + folder makes the backend version the template in place. New\n * templates land in `fallbackFolder`. Unchanged content resolves to the\n * already-stored file rather than failing. Throws with a human-readable\n * message when the copy could not be made; the caller decides what a save\n * without a stored copy means.\n */\nexport async function storeTemplateInDam(\n data: BuilderContentData,\n auth: DamStoreAuth,\n fallbackFolder: string,\n knownUuid?: string,\n): Promise<StoredTemplate> {\n if (!auth.token) throw new Error('dam-store needs a token')\n const key = await resolveKey(auth)\n if (!key) throw new Error('dam-store needs a sass key or a security template')\n\n // Two candidate records with different jobs. The host's `templateId` file\n // anchors the FOLDER (that is where the template lives) — looked up only\n // when the id is actually uuid-shaped; an opaque host id names nothing in\n // the DAM by definition. The copy this session last made (`knownUuid`) is\n // the freshest row and is what an unchanged-content conflict must resolve\n // to — under a VERSION policy the `templateId` row can be an older version\n // whose uuid/`?vh=` would hand the host a regressive pointer.\n const wantFromId = !!data.templateId && looksLikeDamFileUuid(data.templateId)\n const wantOwnCopy = !!knownUuid && knownUuid !== data.templateId\n // Independent lookups — in parallel, not a round trip each. The failure\n // rules differ by role: the templateId anchor is required (proceeding on a\n // transient failure would re-home the file into the fallback folder), while\n // the knownUuid lookup is an optional freshness upgrade whose transient\n // failure must not fail a save the other anchor can carry — unless it was\n // the only tie to the existing file.\n const [fromRes, ownRes] = await Promise.allSettled([\n wantFromId\n ? getFileRecord(auth, key, data.templateId as string)\n : Promise.resolve<FileRecord | null>(null),\n wantOwnCopy\n ? getFileRecord(auth, key, knownUuid as string)\n : Promise.resolve<FileRecord | null>(null),\n ])\n if (fromRes.status === 'rejected') throw fromRes.reason\n const fromId = fromRes.value\n let ownCopy: FileRecord | null = null\n if (ownRes.status === 'fulfilled') ownCopy = ownRes.value\n else if (!fromId) throw ownRes.reason\n\n const anchor = fromId ?? ownCopy\n const folder = anchor?.url?.path\n ? folderFromPath(anchor.url.path)\n : fallbackFolder || '/'\n\n // The DAM filename. Versioning-in-place matches on name + folder, so the\n // name must be STABLE per document and DISTINCT between documents:\n // - a known copy's own filename wins — versioning keeps matching across\n // renames and sessions;\n // - else, with a host template id, the name carries a hash of that id —\n // deterministic, so a reload without the stored-uuid seed still lands on\n // the same file, while two \"Untitled\"s with different ids never collide;\n // - else (no id at all) a per-page unique suffix — the one case where\n // nothing identifies the document across sessions.\n const display = (data.name ?? '').trim().replace(/\\.fdt$/i, '') || 'template'\n const fileName = anchor?.name\n ? anchor.name\n : data.templateId\n ? `${display}_${hashId(data.templateId)}.fdt`\n : `${display}_${Date.now().toString(36)}-${++uploadSeq}.fdt`\n\n const body = new FormData()\n // The same MIME the app's own template uploads declare\n // (TEMPLATE_MIME_TYPE in the design-templates-app) — plain text/xml would\n // risk the copy not being classified as a template_fdt asset.\n body.append(\n 'files[]',\n new Blob([data.content], { type: 'text/xml+sfxtemplate' }),\n fileName,\n )\n const storedQuery = stripBoundValues(data.templateQuery ?? '', data.content)\n if (storedQuery) {\n body.append(\n 'info[files[]]',\n JSON.stringify({ custom: { template_query: storedQuery } }),\n )\n }\n\n const res = await fetch(\n `${FILEROBOT_API}/${encodeURIComponent(auth.token)}/v4/files?folder=${encodeURIComponent(folder)}`,\n {\n method: 'POST',\n headers: { ...apiHeaders(auth, key), 'X-Filerobot-Template': 'true' },\n body,\n },\n )\n const parsed = (await res.json().catch(() => ({}))) as UploadResponseBody\n\n const code = parsed.code ?? parsed.files?.failed?.[0]?.code\n if (code && UNCHANGED_UPLOAD_CODES.has(code)) {\n let resolved = ownCopy ?? fromId\n if (!resolved?.uuid) {\n // No anchor in hand — typically the first save after a page reload\n // without the stored-uuid seed. In that case the file this conflict\n // points at is this document's own earlier copy, which carries exactly\n // the (deterministic) filename we just tried to upload — find it.\n resolved = await findByName(auth, key, fileName, folder).catch(() => null)\n }\n if (resolved?.uuid) {\n return {\n uuid: resolved.uuid,\n url: resolved.url?.cdn ?? resolved.url?.public ?? '',\n }\n }\n // A genuinely foreign copy: the project's dedupe policy already stores\n // these exact bytes as some other document's file.\n throw new Error(\n `this project's deduplication policy already stores this exact content as another file (${code}) — ` +\n 'edit the content, or pass that file\\'s uuid as stored-uuid so the save can resolve to it',\n )\n }\n if (!res.ok || parsed.status === 'error') {\n throw new Error(parsed.msg ?? parsed.message ?? `upload failed: ${res.status}`)\n }\n\n const uuid = parsed.file?.uuid ?? parsed.files?.uploaded?.[0]?.uuid\n if (!uuid) throw new Error('upload succeeded but no file uuid in the response')\n\n // Read the record back rather than trusting the upload response, whose shape\n // varies — this is also what yields the fresh `?vh=` cache key. Best-effort:\n // the file IS stored by now, so a failed read must not turn the save into a\n // storeError — the url just falls back to what the upload response carried.\n let record: FileRecord | null = null\n try {\n record = await getFileRecord(auth, key, uuid)\n } catch {\n // Tolerated — see above.\n }\n return {\n uuid,\n url:\n record?.url?.cdn ??\n record?.url?.public ??\n parsed.file?.url?.cdn ??\n '',\n }\n}\n","import { LitElement, html, css, nothing, type PropertyValues } from 'lit'\nimport { property, state } from 'lit/decorators.js'\nimport {\n BLANK_TEMPLATE_XML,\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_CONFIG,\n HOST_LOAD,\n HOST_SAVED,\n builderRoute,\n type CustomMetadataField,\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'\nimport {\n storeTemplateInDam,\n type DamStoreAuth,\n type StoredTemplate,\n} from './dam-store'\n\nexport type { StoredTemplate, DamStoreAuth } from './dam-store'\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. With `dam-store` the element\n * uploads a rendering copy to Filerobot first, and the detail additionally\n * carries `stored` (the copy's uuid + CDN URL) — or `storeError` when that\n * copy could not be made; the raw `content` arrives either way.\n */\nexport type TemplateBuilderSaveDetail =\n | BuilderSaveData\n | (BuilderContentData & { stored?: StoredTemplate; storeError?: string })\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. To start a template from\n * scratch, set `new-template` (or call `createNew()`) instead of `content`\n * — the widget supplies the blank document.\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`, or `new-template` to start from\n * scratch.\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 /**\n * Stateless mode: open on a new, empty template rather than one of yours.\n * The widget supplies the blank document ({@link BLANK_TEMPLATE_XML}), so\n * nothing here needs to know the template format — the user picks the canvas\n * size in the editor, and `save` hands back a complete document to store.\n *\n * `content` wins when both are set: a host with a real template to edit is\n * not asking for a blank one.\n *\n * A flag rather than an inferred meaning for empty `content`, because empty\n * already means \"the host is still fetching\" — the editor waits on a spinner\n * for it, and blanking that case would flash an empty document in front of\n * every host that mounts the builder before its request resolves.\n */\n @property({ type: Boolean, attribute: 'new-template' }) newTemplate = false\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 /**\n * Metadata model to offer in the editor as the **Custom metadata** value\n * source: `[{ key, title?, group? }]`. Authors then bind a text variable to\n * one of your field names instead of to a bare slug.\n *\n * Names only — no values travel with the model, and the app resolves nothing\n * against it. The bound key is stored in the template as `custom_ckey`, and\n * the variable renders like any free-text one: your pipeline puts\n * `$slug=value` in the render query. Read the key back from the saved `.fdt`\n * to know which of your fields each variable expects.\n *\n * Leave it empty and the source is not offered at all, so hosts that send\n * nothing see the editor they always had. Its main use is `sec-template` /\n * stateless embeds, where the Hub project model — and with it the \"File\n * metadata\" source — is unavailable.\n *\n * Settable as a property (an array) or as a `custom-metadata` attribute\n * holding that array as JSON. Neither is trusted to be well-formed:\n * unparseable JSON is warned about, a value that is not an array is treated as\n * no model, and individual fields the editor cannot use are dropped there —\n * a config typo costs the field, not the editor.\n *\n * Lit compares by identity: assign a new array to change the model, don't\n * mutate the one you passed.\n */\n @property({\n attribute: 'custom-metadata',\n converter: {\n fromAttribute: (value: string | null): CustomMetadataField[] => {\n if (!value) return []\n try {\n const parsed: unknown = JSON.parse(value)\n return Array.isArray(parsed) ? (parsed as CustomMetadataField[]) : []\n } catch {\n console.warn('[sfx-template-builder] custom-metadata is not valid JSON — ignoring.')\n return []\n }\n },\n toAttribute: (value: CustomMetadataField[]): string => JSON.stringify(value ?? []),\n },\n })\n customMetadata: CustomMetadataField[] = []\n /**\n * Display name for the custom-metadata value source in the editor's UI.\n * Empty means the editor's default (\"Custom metadata\"); a host can rename\n * it after its own domain — e.g. \"External metadata\". Pure wording: the\n * stored template is unaffected.\n */\n @property({ attribute: 'custom-metadata-label' }) customMetadataLabel = ''\n /**\n * Stateless only: store each save in Filerobot too, so the CDN can render\n * it. The `save` event then carries `stored: { uuid, url }` next to the raw\n * `content` — or `storeError` when the copy failed (the raw data arrives\n * either way; whether that fails the save is the host's call via the ack).\n * Uses the element's own credentials; a security template needs a scope\n * that allows uploads.\n */\n @property({ type: Boolean, attribute: 'dam-store' }) damStore = false\n /**\n * Folder new templates are stored into when `dam-store` is on and the\n * template id names no existing DAM file (an existing file's own folder\n * always wins, so same name + folder versions it in place).\n */\n @property({ attribute: 'store-folder' }) storeFolder = '/'\n /**\n * `dam-store`: the uuid of the rendering copy this document already has —\n * the `stored.uuid` a previous session's save reported, passed back in by\n * the host alongside the content. Without it the element only remembers\n * copies it made itself, so after a reload an unchanged re-save cannot find\n * its own file and reports a spurious `storeError`, and a changed one starts\n * a fresh file instead of versioning the existing one. Set it when reopening\n * a stored template; it belongs to the document, so hosts that swap\n * documents must swap (or clear) it too — `load()` does this for you.\n */\n @property({ attribute: 'stored-uuid' }) storedUuid = ''\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 * Identity of the last SAVE the app handed back, in the same shape as\n * `_sentKey`. A host echoing the full save detail into the props (content,\n * name, templateQuery) matches this key rather than `_sentKey`, whose name\n * and query still describe what the template was loaded with — without it\n * the echo would post a HOST_LOAD that reloads the editor and wipes undo\n * history on nearly every save (a save almost always changes the query).\n */\n private _sentSaveKey?: string\n /**\n * Identity of the config already delivered, so re-renders don't re-post it.\n * Undefined means the app has not been told anything yet — set back to that\n * whenever a new app instance loads.\n */\n private _sentConfigKey?: 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 storedUuid,\n }: {\n content: string\n templateId?: string\n name?: string\n templateQuery?: string\n storedUuid?: 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 // Omission clears rather than keeps: the seed names the DOCUMENT's stored\n // copy, and carrying one document's uuid into the next would anchor its\n // saves to the wrong file. \"Unknown\" is the safe reading of not saying.\n this.storedUuid = storedUuid ?? ''\n // A template of your own is not the blank one: clearing the flag lets a\n // host alternate between `createNew()` and `load()` on one element.\n this.newTemplate = false\n this.content = content\n this._open = true\n }\n\n /**\n * Stateless mode: open the editor on a new, empty template, opening it if\n * needed. Equivalent to setting `new-template` and calling `open()`.\n *\n * The user chooses the canvas size from the editor's empty state; `save`\n * then hands back a complete `.fdt` document — the first one you store for\n * this record. Pass `templateId` if you have already allocated one; leave it\n * out and the `save` payload simply comes back without an id.\n *\n * Calling it again on an editor that is already showing the blank document\n * does nothing — resending would discard whatever the user has built since.\n * To genuinely start over, `close()` and reopen.\n */\n createNew({\n templateId,\n name,\n }: { templateId?: string; name?: string } = {}): void {\n if (templateId !== undefined) this.templateId = templateId\n if (name !== undefined) this.templateName = name\n // A new document has no layouts and no variables, so there is no render\n // for a query to select — and a leftover one would name layouts and\n // variables of the previous template. Same for the stored-copy seed: a\n // blank document has no copy, and a stale one would anchor the first save\n // to the previous document's file.\n this.templateQuery = ''\n this.storedUuid = ''\n this.newTemplate = true\n this.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 // Element-level listeners still fire on a detached element, so a vanilla\n // host that removes the widget mid-upload still gets its raw save. (A\n // framework wrapper unsubscribes its listeners before the node detaches,\n // which is why the public `flushPendingSaves()` exists — the React wrapper\n // calls it from its cleanup, while its listeners still hear the event.)\n //\n // Deferred a task: REPARENTING (appendChild into another container)\n // detaches and reattaches synchronously, and flushing on the detach half\n // would report a false storeError for an upload that lands fine. Only a\n // detach that is still detached a tick later is a real removal.\n setTimeout(() => {\n if (!this.isConnected) {\n this._flushPendingSaves('widget removed before the rendering copy completed')\n }\n }, 0)\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 // Delivery state only — this is a new APP instance, not a new document.\n // `_lastDocKey` / `_docEpoch` / `_storeMemory` describe the document and\n // survive: resetting them here made every modal close or theme swap\n // forget the stored copy and fork the file on the next save.\n this._contentRequested = false\n this._sentKey = undefined\n this._sentSaveKey = undefined\n this._sentConfigKey = 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('newTemplate') ||\n changed.has('templateId') ||\n changed.has('templateName') ||\n changed.has('templateQuery')\n ) {\n this._maybeSendContent()\n }\n // Config is independent of the template: it also has to reach DAM-backed\n // embeds, which never send a content request. Only once the app is ready,\n // though — before that there is nothing listening, and the ready signal\n // sends whatever the latest value is anyway.\n if (\n (changed.has('customMetadata') || changed.has('customMetadataLabel')) &&\n this._status === 'ready'\n ) {\n this._maybeSendConfig()\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 // Config rides the ready signal: the app has its listener attached by\n // the time it announces, and a remount inside an unchanged iframe\n // re-announces — so this is also the resend point after the editor\n // reloads and forgets what it was told.\n //\n // Keyed on READY alone, because a single mount announces READY *and*\n // OPEN and resetting on both would post the same config twice. OPEN\n // still calls in, without the reset: after a READY that is a no-op, and\n // it is the only signal an app deployment older than protocol v1 sends.\n if (msg.type === BUILDER_READY) this._sentConfigKey = undefined\n this._maybeSendConfig()\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._sentSaveKey = 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 the props — the natural controlled\n // pattern — must not trigger a HOST_LOAD reload that wipes the\n // editor's undo history behind a skeleton flash. TWO keys are\n // recorded because hosts echo different subsets: the prop-based key\n // covers \"content only, other props untouched\", and the detail-based\n // key covers \"the whole save detail\" — whose name and templateQuery\n // (a save nearly always changes the query) differ from the props the\n // template was loaded with.\n this._sentKey = this._contentKey(data.content)\n this._sentSaveKey = this._detailKey(data)\n if (this.damStore) {\n // Serialized, not fired in parallel: the app discards a failure ack\n // that has newer saves still outstanding as superseded, which is\n // only sound while acks come back in post order — and ack order\n // follows save-event order. Two racing uploads could swap it.\n this._pendingSaves.add(data)\n // Epoch, folder and credentials are captured now, not when the\n // queued upload runs: the save belongs to the document — and the\n // configuration — the app posted it under, and the host may have\n // swapped both by the time the queue reaches it. Only the\n // known-uuid seed stays live (validated against the epoch), because\n // an earlier queued save of the SAME document must be able to hand\n // its uuid to the next one.\n const session = this._docEpoch\n const job = {\n auth: {\n token: this.token,\n sassKey: this.sassKey,\n secTemplate: this.secTemplate,\n sessionUuid: this.sessionUuid,\n companyUuid: this.companyUuid,\n projectUuid: this.projectUuid,\n },\n storeFolder: this.storeFolder || '/',\n }\n this._storeQueue = this._storeQueue.then(() =>\n this._storeAndEmitSave(data, session, job),\n )\n } else {\n this._emit('save', data)\n }\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 // Pending `dam-store` uploads are deliberately NOT flushed here: the\n // element outlives a close (inline keeps rendering, modal just drops\n // its overlay), so a save still inside its upload window emits with\n // its real outcome moments later — flushing would hand the host a\n // `storeError` for a copy that lands fine. The paths where waiting\n // genuinely loses the event — element removal — are covered by\n // `disconnectedCallback` and the React wrapper's cleanup flush.\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 * Pending `dam-store` uploads, chained so saves emit in the order the app\n * posted them. `_storeAndEmitSave` never rejects (its catch emits\n * `storeError`), so the chain cannot wedge.\n */\n private _storeQueue: Promise<void> = Promise.resolve()\n\n /**\n * Saves handed over by the app whose `save` event has not fired yet — the\n * upload window. Insertion-ordered; membership is what makes a flush and a\n * completing upload not double-emit the same save.\n */\n private _pendingSaves = new Set<BuilderContentData>()\n\n /**\n * Identity (id, content, name — not query) of the last document posted to\n * the app — updated on every ship, and on an accepted save echo (the echoed\n * document IS the current one; leaving the pre-save key here would make a\n * later content-request redelivery look like a new document and wipe the\n * store memory below).\n */\n private _lastDocKey?: string\n\n /**\n * Monotonic id of the current DOCUMENT. Bumped in exactly one place: when\n * `_maybeSendContent` ships a genuinely different document (docKey change).\n * Deliberately NOT bumped on iframe/src changes — a modal close, a theme\n * swap or an in-place remount is the same document, and treating it as new\n * forked the file on every such boundary.\n */\n private _docEpoch = 0\n\n /**\n * The copy this element last stored, tagged with the epoch of the document\n * it belongs to. Written when an upload lands (with the SAVE's epoch, so a\n * late-landing upload can never masquerade as another document's copy) and\n * validated at read time — there is no eager reset to get wrong.\n */\n private _storeMemory?: { epoch: number; uuid: string }\n\n /**\n * Emit every not-yet-emitted `dam-store` save immediately, raw content with\n * `storeError` in place of the links. Called when waiting any longer risks\n * the event finding no listener; a still-running upload for a flushed save\n * is left to finish (the copy usually lands) but will not emit again.\n *\n * Public for framework wrappers: one that unsubscribes its listeners before\n * unmounting must call this first, while they are still attached — the\n * element's own disconnect-time flush fires only after the wrapper has\n * stopped listening, and the raw save would be lost. The React wrapper does\n * this; a vanilla host never needs to call it.\n */\n flushPendingSaves(\n reason = 'widget removed before the rendering copy completed',\n ): void {\n this._flushPendingSaves(reason)\n }\n\n private _flushPendingSaves(reason: string): void {\n for (const data of this._pendingSaves) {\n this._pendingSaves.delete(data)\n this._emit('save', { ...data, storeError: reason })\n }\n }\n\n /**\n * The `dam-store` save path: upload the edited template to Filerobot, then\n * emit `save` with the stored copy's links on the detail. The upload is the\n * render side of the save — the CDN renders only stored files — while the\n * raw `content` stays the host's copy exactly as without the flag.\n *\n * A failed upload still emits `save` (the raw data must reach the host\n * either way), with `storeError` in place of `stored`; whether a save\n * without a rendering copy counts as saved is the host's decision, made\n * where it always is — the save ack.\n */\n private async _storeAndEmitSave(\n data: BuilderContentData,\n session: number,\n job: { auth: DamStoreAuth; storeFolder: string },\n ): Promise<void> {\n if (!this._pendingSaves.has(data)) return\n // The seeds are validated against the save's own document epoch. The\n // element's memory carries the epoch it was recorded under; the host's\n // `storedUuid` property is live and describes the CURRENT document, so it\n // only applies while the save's epoch is still the current one — anchoring\n // a stale save's upload to it would put the old document into the new\n // one's file. Folder and credentials come from the job captured at\n // enqueue time, for the same reason.\n const knownUuid =\n (this._storeMemory?.epoch === session\n ? this._storeMemory.uuid\n : undefined) ||\n (session === this._docEpoch ? this.storedUuid || undefined : undefined)\n let stored: StoredTemplate | undefined\n let storeError: string | undefined\n try {\n stored = await storeTemplateInDam(data, job.auth, job.storeFolder, knownUuid)\n } catch (err) {\n storeError = err instanceof Error ? err.message : String(err)\n }\n // A flush mid-upload already delivered this save; the copy (if it landed)\n // is simply not reported. Emitting again would double the host's write —\n // and recording the uuid would poison the session memory of whatever\n // document has loaded since (the flush usually precedes a swap), pointing\n // its next save at this document's file.\n if (!this._pendingSaves.delete(data)) return\n // Recorded under the SAVE's epoch, unconditionally: a copy always belongs\n // to the document it was saved from. A later document's saves carry a\n // higher epoch and simply never match this record — no current-vs-then\n // comparison to get wrong. (Uploads complete in queue order, so the last\n // write is always the newest save.)\n if (stored) this._storeMemory = { epoch: session, uuid: stored.uuid }\n this._emit('save', stored ? { ...data, stored } : { ...data, storeError })\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) return\n\n // Empty `content` means the host has nothing to give yet — usually a fetch\n // in flight — so the editor keeps waiting. Only `new-template` turns that\n // into a document, and only until the host does supply one.\n const content =\n this.content || (this.newTemplate ? BLANK_TEMPLATE_XML : '')\n if (!content) return\n\n const data = {\n templateId: this.templateId || undefined,\n content,\n name: this.templateName || undefined,\n templateQuery: this.templateQuery || undefined,\n }\n const key = this._contentKey(content)\n // The document's identity — id, content, name; NOT the query. A\n // query-only resend (\"same XML, different render\") and a content-request\n // redelivery after an in-place remount are the same document, and\n // treating them as new would orphan the stored copy.\n const docKey = JSON.stringify({\n templateId: this.templateId || undefined,\n content,\n name: this.templateName || undefined,\n })\n // `_sentSaveKey` counts as delivered too: matching it means the host has\n // echoed the last save back into the props, and the editor already holds\n // exactly that document — reloading would wipe its undo history. The doc\n // key is still refreshed: the echoed document IS the current one, and\n // leaving the pre-save key in place would make the next redelivery of\n // this same content look like a document change and orphan the copy.\n if (key === this._sentKey || key === this._sentSaveKey) {\n this._lastDocKey = docKey\n return\n }\n\n if (!this._postToApp({ type: HOST_LOAD, data })) return\n // A genuinely different document starts a new epoch — the file the\n // previous one made must not resolve this one's folder or unchanged\n // re-saves.\n if (docKey !== this._lastDocKey) this._docEpoch++\n this._lastDocKey = docKey\n this._sentKey = key\n // A new document shipped: the previous save's echo key no longer names\n // what the editor holds, and matching it later would wrongly skip a load.\n this._sentSaveKey = undefined\n }\n\n /**\n * Deliver host config to the app. Unlike content this is not requested — the\n * app has no way to know a host means to send any — so it goes out on the\n * ready signal and on every later change.\n *\n * Sending an empty model is meaningful: it is how a host clears one it set\n * before. What is skipped is only a *repeat* of what the app already holds,\n * and the very first send when there was never anything to say.\n */\n private _maybeSendConfig(): void {\n // Hosts are plain JS as often as not, and the property has no converter to\n // vet what lands on it the way the attribute does. Anything that is not a\n // list of fields is treated as no model rather than posted onward as\n // something the app would have to make sense of.\n const customMetadata = Array.isArray(this.customMetadata)\n ? this.customMetadata\n : []\n const customMetadataLabel =\n typeof this.customMetadataLabel === 'string'\n ? this.customMetadataLabel.trim()\n : ''\n const key = JSON.stringify({ customMetadata, customMetadataLabel })\n if (key === this._sentConfigKey) return\n if (\n this._sentConfigKey === undefined &&\n customMetadata.length === 0 &&\n customMetadataLabel === ''\n ) {\n return\n }\n\n if (\n !this._postToApp({\n type: HOST_CONFIG,\n data: {\n customMetadata,\n ...(customMetadataLabel ? { customMetadataLabel } : {}),\n },\n })\n ) {\n return\n }\n this._sentConfigKey = 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 /**\n * Identity of a save the app handed back — the same shape as\n * `_contentKey`, but built from the save detail rather than the props, so\n * a host echoing the detail (whose name/query the save changed) matches.\n */\n private _detailKey(data: BuilderContentData): string {\n return JSON.stringify({\n templateId: data.templateId || undefined,\n content: data.content,\n name: data.name || undefined,\n templateQuery: data.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","BLANK_TEMPLATE_XML","HOST_CONFIG","HOST_SAVED","EMBED_PARAMS","AUTH_MODES","BRAND_COLOR_PATTERN","builderRoute","templateId","EMBED_ROUTE","FILEROBOT_API","resolveKey","auth","res","body","apiHeaders","key","headers","looksLikeDamFileUuid","id","getFileRecord","uuid","err","message","findByName","fileName","folder","url","f","hashId","h","i","folderFromPath","rawPath","path","segments","stripBoundValues","templateQuery","content","bound","doc","slugs","el","part","uploadSeq","UNCHANGED_UPLOAD_CODES","storeTemplateInDam","data","fallbackFolder","knownUuid","wantFromId","wantOwnCopy","fromRes","ownRes","fromId","ownCopy","anchor","display","storedQuery","parsed","code","resolved","record","_SfxTemplateBuilder","LitElement","event","iframe","msg","session","job","name","storedUuid","ok","changed","src","frame","html","nothing","spinner","route","base","reason","stored","storeError","docKey","customMetadata","customMetadataLabel","target","appOrigin","detail","css","SfxTemplateBuilder","__decorateClass","property","value","state"],"mappings":";;AAWO,MAAMA,KAAmB,GAKnBC,IAAgB,kCAEhBC,IAAe,iCAEfC,IAAgB,kCAEhBC,IAAe,iCAEfC,IAAgB,kCAMhBC,IAA0B,4CAQ1BC,IAAkB,oCAQlBC,IAAgB,kCAoHhBC,IAAY,8BA0DZC,IACX;AAAA,6GAgBWC,IAAc,gCA4DdC,IAAa,+BAsBbC,IAAe;AAAA,EAC1B,cAAc;AAAA,EACd,cAAc;AAAA,EACd,cAAc;AAAA,EACd,UAAU;AAAA,EACV,iBAAiB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAMjB,cAAc;AAAA,EACd,QAAQ;AAAA;AAAA,EAER,cAAc;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAMd,aAAa;AAAA;AAAA,EAEb,OAAO;AACT,GAmBaC,KAAa;AAAA,EACxB,SAAS;AAAA,EACT,cAAc;AAChB,GASaC,KAAsB;AAG5B,SAASC,EAAaC,GAA6B;AACxD,SAAOA,IACH,cAAc,mBAAmBA,CAAU,CAAC,UAC5C;AACN;AAOO,MAAMC,IAAc,oBCnXdC,IAAgB;AAwC7B,eAAsBC,EAAWC,GAAqC;AACpE,MAAI,CAACA,EAAK,YAAa,QAAOA,EAAK,WAAW;AAC9C,QAAMC,IAAM,MAAM;AAAA,IAChB,GAAGH,CAAa,IAAI,mBAAmBE,EAAK,KAAK,CAAC,WAAW,mBAAmBA,EAAK,WAAW,CAAC;AAAA,IACjG,EAAE,SAAS,EAAE,mBAAmBA,EAAK,cAAY;AAAA,EAAE,GAE/CE,IAAQ,MAAMD,EAAI,KAAA,EAAO,MAAM,OAAO,CAAA,EAAG;AAC/C,MAAI,CAACA,EAAI,MAAM,CAACC,EAAK;AACnB,UAAM,IAAI,MAAMA,EAAK,OAAO,wBAAwBD,EAAI,MAAM,EAAE;AAElE,SAAOC,EAAK;AACd;AAMO,SAASC,EAAWH,GAAoBI,GAAqC;AAClF,QAAMC,IAAkC,EAAE,mBAAmBD,EAAA;AAC7D,SAAKJ,EAAK,gBACJA,EAAK,gBAAaK,EAAQ,iBAAiB,IAAIL,EAAK,cACpDA,EAAK,gBAAaK,EAAQ,iBAAiB,IAAIL,EAAK,cACpDA,EAAK,gBAAaK,EAAQ,iBAAiB,IAAIL,EAAK,eAEnDK;AACT;AAQO,SAASC,EAAqBC,GAAqB;AACxD,SAAO,4BAA4B,KAAKA,CAAE;AAC5C;AAUA,eAAsBC,EACpBR,GACAI,GACAK,GAC4B;AAC5B,MAAIR;AACJ,MAAI;AACF,IAAAA,IAAM,MAAM;AAAA,MACV,GAAGH,CAAa,IAAI,mBAAmBE,EAAK,KAAK,CAAC,aAAa,mBAAmBS,CAAI,CAAC;AAAA,MACvF,EAAE,SAASN,EAAWH,GAAMI,CAAG,EAAA;AAAA,IAAE;AAAA,EAErC,SAASM,GAAK;AACZ,UAAM,IAAI;AAAA,MACR,uBAAuBA,aAAe,QAAQA,EAAI,UAAU,OAAOA,CAAG,CAAC;AAAA,IAAA;AAAA,EAE3E;AACA,QAAMR,IAAQ,MAAMD,EAAI,KAAA,EAAO,MAAM,OAAO,CAAA,EAAG;AAO/C,MAAI,CAAC,KAAK,KAAK,KAAK,GAAG,EAAE,SAASA,EAAI,MAAM,EAAG,QAAO;AACtD,QAAMU,IAAUT,EAAK,OAAOA,EAAK,WAAW;AAC5C,MAAI,CAACD,EAAI,MAAMC,EAAK,WAAW,SAAS;AACtC,QAAI,8CAA8C,KAAKS,CAAO,EAAG,QAAO;AACxE,UAAM,IAAI,MAAMA,KAAW,uBAAuBV,EAAI,MAAM,EAAE;AAAA,EAChE;AACA,SAAOC,EAAK,QAAQ;AACtB;AAMA,eAAeU,EACbZ,GACAI,GACAS,GACAC,GAC4B;AAC5B,QAAMC,IAAM,IAAI;AAAA,IACd,GAAGjB,CAAa,IAAI,mBAAmBE,EAAK,KAAK,CAAC;AAAA,EAAA;AAEpD,EAAAe,EAAI,aAAa,IAAI,KAAK,SAASF,CAAQ,GAAG,GAC9CE,EAAI,aAAa,IAAI,UAAUD,CAAM,GACrCC,EAAI,aAAa,IAAI,QAAQ,kBAAkB,GAC/CA,EAAI,aAAa,IAAI,SAAS,GAAG;AACjC,QAAMd,IAAM,MAAM,MAAMc,GAAK,EAAE,SAASZ,EAAWH,GAAMI,CAAG,GAAG,GACzDF,IAAQ,MAAMD,EAAI,KAAA,EAAO,MAAM,OAAO,CAAA,EAAG;AAI/C,SAAI,CAACA,EAAI,MAAMC,EAAK,WAAW,UAAgB,OACxCA,EAAK,OAAO,KAAK,CAACc,MAAMA,EAAE,SAASH,CAAQ,KAAK;AACzD;AAMA,SAASI,EAAOV,GAAoB;AAClC,MAAIW,IAAI;AACR,WAASC,IAAI,GAAGA,IAAIZ,EAAG,QAAQY;AAC7B,IAAAD,KAAKX,EAAG,WAAWY,CAAC,GACpBD,IAAI,KAAK,KAAKA,GAAG,QAAU;AAE7B,UAAQA,MAAM,GAAG,SAAS,EAAE;AAC9B;AAGA,SAASE,GAAeC,GAAyB;AAC/C,MAAIC;AACJ,MAAI;AACF,IAAAA,IAAO,mBAAmBD,CAAO;AAAA,EACnC,QAAQ;AACN,IAAAC,IAAOD;AAAA,EACT;AACA,QAAME,IAAWD,EAAK,MAAM,GAAG,EAAE,OAAO,OAAO;AAC/C,SAAAC,EAAS,IAAA,GACF,MAAMA,EAAS,KAAK,GAAG;AAChC;AAUA,SAASC,GAAiBC,GAAuBC,GAAyB;AACxE,MAAI,CAACD,EAAe,QAAOA;AAC3B,MAAIE;AACJ,MAAI;AACF,UAAMC,IAAM,IAAI,UAAA,EAAY,gBAAgBF,GAAS,iBAAiB;AACtE,QAAIE,EAAI,cAAc,aAAa,EAAG,QAAOH;AAC7C,IAAAE,IAAQC,EAAI;AAAA,MACV;AAAA,IAAA;AAAA,EAEJ,QAAQ;AACN,WAAOH;AAAA,EACT;AACA,MAAIE,EAAM,WAAW,EAAG,QAAOF;AAC/B,QAAMI,IAAQ,IAAI,IAAI,CAAC,GAAGF,CAAK,EAAE,IAAI,CAACG,MAAOA,EAAG,aAAa,MAAM,KAAK,EAAE,CAAC;AAC3E,SAAOL,EACJ,MAAM,GAAG,EACT,OAAO,OAAO,EACd,OAAO,CAACM,MAAS,CAACF,EAAM,IAAIE,EAAK,MAAM,GAAG,EAAE,CAAC,EAAE,QAAQ,aAAa,EAAE,CAAC,CAAC,EACxE,KAAK,GAAG;AACb;AAQA,IAAIC,KAAY;AAEhB,MAAMC,yBAA6B,IAAI;AAAA,EACrC;AAAA,EACA;AACF,CAAC;AA2BD,eAAsBC,GACpBC,GACAnC,GACAoC,GACAC,GACyB;AACzB,MAAI,CAACrC,EAAK,MAAO,OAAM,IAAI,MAAM,yBAAyB;AAC1D,QAAMI,IAAM,MAAML,EAAWC,CAAI;AACjC,MAAI,CAACI,EAAK,OAAM,IAAI,MAAM,mDAAmD;AAS7E,QAAMkC,IAAa,CAAC,CAACH,EAAK,cAAc7B,EAAqB6B,EAAK,UAAU,GACtEI,IAAc,CAAC,CAACF,KAAaA,MAAcF,EAAK,YAOhD,CAACK,GAASC,CAAM,IAAI,MAAM,QAAQ,WAAW;AAAA,IACjDH,IACI9B,EAAcR,GAAMI,GAAK+B,EAAK,UAAoB,IAClD,QAAQ,QAA2B,IAAI;AAAA,IAC3CI,IACI/B,EAAcR,GAAMI,GAAKiC,CAAmB,IAC5C,QAAQ,QAA2B,IAAI;AAAA,EAAA,CAC5C;AACD,MAAIG,EAAQ,WAAW,WAAY,OAAMA,EAAQ;AACjD,QAAME,IAASF,EAAQ;AACvB,MAAIG,IAA6B;AACjC,MAAIF,EAAO,WAAW,YAAa,CAAAE,IAAUF,EAAO;AAAA,WAC3C,CAACC,EAAQ,OAAMD,EAAO;AAE/B,QAAMG,IAASF,KAAUC,GACnB7B,IAAS8B,GAAQ,KAAK,OACxBxB,GAAewB,EAAO,IAAI,IAAI,IAC9BR,KAAkB,KAWhBS,KAAWV,EAAK,QAAQ,IAAI,OAAO,QAAQ,WAAW,EAAE,KAAK,YAC7DtB,IAAW+B,GAAQ,OACrBA,EAAO,OACPT,EAAK,aACH,GAAGU,CAAO,IAAI5B,EAAOkB,EAAK,UAAU,CAAC,SACrC,GAAGU,CAAO,IAAI,KAAK,MAAM,SAAS,EAAE,CAAC,IAAI,EAAEb,EAAS,QAEpD9B,IAAO,IAAI,SAAA;AAIjB,EAAAA,EAAK;AAAA,IACH;AAAA,IACA,IAAI,KAAK,CAACiC,EAAK,OAAO,GAAG,EAAE,MAAM,wBAAwB;AAAA,IACzDtB;AAAA,EAAA;AAEF,QAAMiC,IAActB,GAAiBW,EAAK,iBAAiB,IAAIA,EAAK,OAAO;AAC3E,EAAIW,KACF5C,EAAK;AAAA,IACH;AAAA,IACA,KAAK,UAAU,EAAE,QAAQ,EAAE,gBAAgB4C,EAAA,GAAe;AAAA,EAAA;AAI9D,QAAM7C,IAAM,MAAM;AAAA,IAChB,GAAGH,CAAa,IAAI,mBAAmBE,EAAK,KAAK,CAAC,oBAAoB,mBAAmBc,CAAM,CAAC;AAAA,IAChG;AAAA,MACE,QAAQ;AAAA,MACR,SAAS,EAAE,GAAGX,EAAWH,GAAMI,CAAG,GAAG,wBAAwB,OAAA;AAAA,MAC7D,MAAAF;AAAA,IAAA;AAAA,EACF,GAEI6C,IAAU,MAAM9C,EAAI,KAAA,EAAO,MAAM,OAAO,CAAA,EAAG,GAE3C+C,IAAOD,EAAO,QAAQA,EAAO,OAAO,SAAS,CAAC,GAAG;AACvD,MAAIC,KAAQf,GAAuB,IAAIe,CAAI,GAAG;AAC5C,QAAIC,IAAWN,KAAWD;AAQ1B,QAPKO,GAAU,SAKbA,IAAW,MAAMrC,EAAWZ,GAAMI,GAAKS,GAAUC,CAAM,EAAE,MAAM,MAAM,IAAI,IAEvEmC,GAAU;AACZ,aAAO;AAAA,QACL,MAAMA,EAAS;AAAA,QACf,KAAKA,EAAS,KAAK,OAAOA,EAAS,KAAK,UAAU;AAAA,MAAA;AAKtD,UAAM,IAAI;AAAA,MACR,0FAA0FD,CAAI;AAAA,IAAA;AAAA,EAGlG;AACA,MAAI,CAAC/C,EAAI,MAAM8C,EAAO,WAAW;AAC/B,UAAM,IAAI,MAAMA,EAAO,OAAOA,EAAO,WAAW,kBAAkB9C,EAAI,MAAM,EAAE;AAGhF,QAAMQ,IAAOsC,EAAO,MAAM,QAAQA,EAAO,OAAO,WAAW,CAAC,GAAG;AAC/D,MAAI,CAACtC,EAAM,OAAM,IAAI,MAAM,mDAAmD;AAM9E,MAAIyC,IAA4B;AAChC,MAAI;AACF,IAAAA,IAAS,MAAM1C,EAAcR,GAAMI,GAAKK,CAAI;AAAA,EAC9C,QAAQ;AAAA,EAER;AACA,SAAO;AAAA,IACL,MAAAA;AAAA,IACA,KACEyC,GAAQ,KAAK,OACbA,GAAQ,KAAK,UACbH,EAAO,MAAM,KAAK,OAClB;AAAA,EAAA;AAEN;;;;;;ACnSO,MAAMI,IAAN,MAAMA,UAA2BC,EAAW;AAAA,EAA5C,cAAA;AAAA,UAAA,GAAA,SAAA,GA+CgC,KAAA,UAAU,IAEnC,KAAA,QAAQ,IACiB,KAAA,UAAU,IACN,KAAA,cAAc,IAQd,KAAA,cAAc,IACd,KAAA,cAAc,IACd,KAAA,cAAc,IAKf,KAAA,aAAa,IACxB,KAAA,OAA2B,UAMZ,KAAA,YAAY,IAMxB,KAAA,UAAU,IAec,KAAA,cAAc,IAE5B,KAAA,eAAe,IAOd,KAAA,gBAAgB,IAOnB,KAAA,aAAa,IAEzC,KAAA,QAA2B,IA0CvC,KAAA,iBAAwC,CAAA,GAOU,KAAA,sBAAsB,IASnB,KAAA,WAAW,IAMvB,KAAA,cAAc,KAWf,KAAA,aAAa,IAEG,KAAA,eAAe,KAE9D,KAAQ,UAAiC,QACzC,KAAQ,QAAQ,IAChB,KAAQ,OAAO,IAOxB,KAAQ,oBAAoB,IAoC5B,KAAQ,6BAA6B,IAE5B,KAAQ,WAAW,IAmH5B,KAAQ,mBAAmB,IA0M3B,KAAQ,aAAa,CAACC,MAA8B;AAElD,UADI,CAAC,KAAK,SACN,CAACA,EAAM,UAAUA,EAAM,WAAW,KAAK,WAAY;AACvD,YAAMC,IAAS,KAAK,YAAY,cAAc,QAAQ;AAKtD,UAJI,CAACA,KAIDD,EAAM,WAAWC,EAAO,cAAe;AAE3C,YAAMC,IAAMF,EAAM;AAClB,UAAI,GAACE,KAAO,OAAOA,EAAI,QAAS;AAEhC,gBAAQA,EAAI,MAAA;AAAA,UACV,KAAK3E;AAAA,UACL,KAAKC;AACH,iBAAK,qBAAA,GACD,KAAK,YAAY,YACnB,KAAK,UAAU,SACf,KAAK,MAAM,OAAO,IAWhB0E,EAAI,SAAS3E,MAAe,KAAK,iBAAiB,SACtD,KAAK,iBAAA,GACD2E,EAAI,SAAS1E,KAAc,KAAK,MAAM,MAAM;AAChD;AAAA,UACF,KAAKE;AACH,iBAAK,MAAM,QAASwE,EAA2B,IAAI;AACnD;AAAA,UACF,KAAKtE;AACH,iBAAK,oBAAoB,IAKzB,KAAK,WAAW,QAChB,KAAK,eAAe,QACpB,KAAK,kBAAA;AACL;AAAA,UACF,KAAKC,GAAiB;AAGpB,kBAAMiD,IAAQoB,EAA8B;AAY5C,gBAFA,KAAK,WAAW,KAAK,YAAYpB,EAAK,OAAO,GAC7C,KAAK,eAAe,KAAK,WAAWA,CAAI,GACpC,KAAK,UAAU;AAKjB,mBAAK,cAAc,IAAIA,CAAI;AAQ3B,oBAAMqB,IAAU,KAAK,WACfC,IAAM;AAAA,gBACV,MAAM;AAAA,kBACJ,OAAO,KAAK;AAAA,kBACZ,SAAS,KAAK;AAAA,kBACd,aAAa,KAAK;AAAA,kBAClB,aAAa,KAAK;AAAA,kBAClB,aAAa,KAAK;AAAA,kBAClB,aAAa,KAAK;AAAA,gBAAA;AAAA,gBAEpB,aAAa,KAAK,eAAe;AAAA,cAAA;AAEnC,mBAAK,cAAc,KAAK,YAAY;AAAA,gBAAK,MACvC,KAAK,kBAAkBtB,GAAMqB,GAASC,CAAG;AAAA,cAAA;AAAA,YAE7C;AACE,mBAAK,MAAM,QAAQtB,CAAI;AAEzB;AAAA,UACF;AAAA,UACA,KAAKhD,GAAe;AAClB,kBAAMgD,IAAQoB,EAA4B;AAC1C,iBAAK,WAAW,CAAC,CAACpB,GAAM,SACxB,KAAK,MAAM,eAAe,EAAE,SAAS,KAAK,UAAU;AACpD;AAAA,UACF;AAAA,UACA,KAAKrD;AAQH,iBAAK,MAAM,OAAO,GACd,KAAK,SAAS,YAAS,KAAK,QAAQ;AACxC;AAAA,UACF,KAAKE;AACH,iBAAK,MAAOuE,EAA4B,QAAQ,EAAE,MAAM,WAAW;AACnE;AAAA,QAAA;AAAA,IAEN,GAOA,KAAQ,cAA6B,QAAQ,QAAA,GAO7C,KAAQ,oCAAoB,IAAA,GAkB5B,KAAQ,YAAY;AAAA,EAAA;AAAA,EA/cpB,IAAI,SAAgC;AAClC,WAAO,KAAK;AAAA,EACd;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,IAAI,UAAmB;AACrB,WAAO,KAAK;AAAA,EACd;AAAA;AAAA,EAGA,KAAK3D,GAA2B;AAC9B,IAAIA,MAAe,WAAW,KAAK,aAAaA,IAChD,KAAK,QAAQ;AAAA,EACf;AAAA;AAAA,EAGA,QAAc;AACZ,SAAK,QAAQ;AAAA,EACf;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,KAAK;AAAA,IACH,SAAA8B;AAAA,IACA,YAAA9B;AAAA,IACA,MAAA8D;AAAA,IACA,eAAAjC;AAAA,IACA,YAAAkC;AAAA,EAAA,GAOO;AACP,IAAI/D,MAAe,WAAW,KAAK,aAAaA,IAC5C8D,MAAS,WAAW,KAAK,eAAeA,IAIxCjC,MAAkB,WAAW,KAAK,gBAAgBA,IAItD,KAAK,aAAakC,KAAc,IAGhC,KAAK,cAAc,IACnB,KAAK,UAAUjC,GACf,KAAK,QAAQ;AAAA,EACf;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAeA,UAAU;AAAA,IACR,YAAA9B;AAAA,IACA,MAAA8D;AAAA,EAAA,IAC0C,IAAU;AACpD,IAAI9D,MAAe,WAAW,KAAK,aAAaA,IAC5C8D,MAAS,WAAW,KAAK,eAAeA,IAM5C,KAAK,gBAAgB,IACrB,KAAK,aAAa,IAClB,KAAK,cAAc,IACnB,KAAK,UAAU,IACf,KAAK,QAAQ;AAAA,EACf;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAaA,YAAYE,GAAajD,GAAwB;AAC/C,IAAK,KAAK,aACV,KAAK,WAAW,EAAE,MAAMpB,GAAY,MAAM,EAAE,IAAAqE,GAAI,SAAAjD,EAAA,GAAW;AAAA,EAC7D;AAAA,EAYA,oBAA0B;AACxB,UAAM,kBAAA,GACN,OAAO,iBAAiB,WAAW,KAAK,UAAU;AAAA,EACpD;AAAA,EAEA,uBAA6B;AAC3B,UAAM,qBAAA,GACN,OAAO,oBAAoB,WAAW,KAAK,UAAU,GACrD,KAAK,qBAAA,GAWL,WAAW,MAAM;AACf,MAAK,KAAK,eACR,KAAK,mBAAmB,oDAAoD;AAAA,IAEhF,GAAG,CAAC;AAAA,EACN;AAAA,EAEU,WAAWkD,GAA+B;AAClD,UAAM,WAAWA,CAAO,GACnB,KAAK,qBACR,KAAK,mBAAmB,IACpB,KAAK,SAAS,aAAU,KAAK,QAAQ;AAE3C,UAAMC,IAAM,KAAK,YAAA;AACjB,IAAIA,MAAQ,KAAK,SACf,KAAK,OAAOA,GACZ,KAAK,UAAUA,IAAM,YAAY;AAAA,EAErC;AAAA,EAEU,QAAQD,GAA+B;AAC/C,IAAIA,EAAQ,IAAI,MAAM,MACpB,KAAK,qBAAA,GAOL,KAAK,oBAAoB,IACzB,KAAK,WAAW,QAChB,KAAK,eAAe,QACpB,KAAK,iBAAiB,QAClB,KAAK,aACP,KAAK,WAAW,IAChB,KAAK,MAAM,eAAe,EAAE,SAAS,IAAO,IAE1C,KAAK,QAAM,KAAK,qBAAA,KAMpBA,EAAQ,IAAI,SAAS,KACrBA,EAAQ,IAAI,aAAa,KACzBA,EAAQ,IAAI,YAAY,KACxBA,EAAQ,IAAI,cAAc,KAC1BA,EAAQ,IAAI,eAAe,MAE3B,KAAK,kBAAA,IAOJA,EAAQ,IAAI,gBAAgB,KAAKA,EAAQ,IAAI,qBAAqB,MACnE,KAAK,YAAY,WAEjB,KAAK,iBAAA;AAAA,EAET;AAAA,EAEA,SAAS;AACP,UAAME,IAAQ,KAAK,OACfC;AAAA;AAAA;AAAA,gBAGQ,KAAK,IAAI;AAAA;AAAA,sBAGjBC,GACEC,IACJ,KAAK,YAAY,YACbF,gDACAC;AAEN,WAAI,KAAK,SAAS,UACT,KAAK,QACRD;AAAA,iCACuBD,CAAK,GAAGG,CAAO;AAAA,oBAEtCD,IAECD,IAAOD,CAAK,GAAGG,CAAO;AAAA,EAC/B;AAAA,EAEQ,cAAsB;AAE5B,QADI,CAAC,KAAK,SACN,CAAC,KAAK,WAAW,CAAC,KAAK,MAAO,QAAO;AACzC,QAAI,KAAK,aAAa;AAKpB,UAAI,CAAC,KAAK;AACR,eAAK,KAAK,+BACR,KAAK,6BAA6B,IAClC;AAAA,UAAe,MACb,KAAK,MAAM;AAAA,YACT,MAAM;AAAA,YACN,SACE;AAAA,UAAA,CAEH;AAAA,QAAA,IAGE;AAIT,WAAK,6BAA6B;AAAA,IACpC,WAAW,CAAC,KAAK,WAAW,CAAC,KAAK;AAChC,aAAO;AAET,QAAInD;AACJ,QAAI;AAGF,YAAMoD,IAAQ,KAAK,YACftE,IACAF,EAAa,KAAK,cAAc,MAAS,GAKvCyE,IAAO,KAAK,QAAQ,SAAS,GAAG,IAAI,KAAK,UAAU,GAAG,KAAK,OAAO;AACxE,MAAArD,IAAM,IAAI,IAAIoD,EAAM,QAAQ,OAAO,EAAE,GAAGC,CAAI;AAAA,IAC9C,QAAQ;AAIN,aAAI,KAAK,wBAAwB,KAAK,YACpC,KAAK,sBAAsB,KAAK,SAGhC;AAAA,QAAe,MACb,KAAK,MAAM;AAAA,UACT,MAAM;AAAA,UACN,SAAS,gCAAgC,KAAK,OAAO;AAAA,QAAA,CACtD;AAAA,MAAA,IAGE;AAAA,IACT;AACA,gBAAK,sBAAsB,QAC3BrD,EAAI,aAAa,IAAIvB,EAAa,iBAAiB,KAAK,KAAK,GACzD,KAAK,cAIPuB,EAAI,aAAa,IAAIvB,EAAa,cAAc,KAAK,WAAW,KAEhEuB,EAAI,aAAa,IAAIvB,EAAa,UAAU,KAAK,OAAO,GACxDuB,EAAI,aAAa,IAAIvB,EAAa,cAAc,KAAK,WAAW,GAC5D,KAAK,eACPuB,EAAI,aAAa,IAAIvB,EAAa,cAAc,KAAK,WAAW,GAE9D,KAAK,eACPuB,EAAI,aAAa,IAAIvB,EAAa,cAAc,KAAK,WAAW,IAGhE,KAAK,cACPuB,EAAI,aAAa,IAAIvB,EAAa,aAAa,KAAK,UAAU,GAE5D,KAAK,SACPuB,EAAI,aAAa,IAAIvB,EAAa,OAAO,KAAK,KAAK,GAErDuB,EAAI,aAAa,IAAIvB,EAAa,QAAQ,GAAG,GAC7CuB,EAAI,aAAa,IAAIvB,EAAa,cAAc,OAAO,SAAS,MAAM,GAC/DuB,EAAI,SAAA;AAAA,EACb;AAAA,EAEA,IAAY,aAA4B;AACtC,QAAI;AACF,aAAO,IAAI,IAAI,KAAK,OAAO,EAAE;AAAA,IAC/B,QAAQ;AACN,aAAO;AAAA,IACT;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EA4KA,kBACEsD,IAAS,sDACH;AACN,SAAK,mBAAmBA,CAAM;AAAA,EAChC;AAAA,EAEQ,mBAAmBA,GAAsB;AAC/C,eAAWlC,KAAQ,KAAK;AACtB,WAAK,cAAc,OAAOA,CAAI,GAC9B,KAAK,MAAM,QAAQ,EAAE,GAAGA,GAAM,YAAYkC,GAAQ;AAAA,EAEtD;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAaA,MAAc,kBACZlC,GACAqB,GACAC,GACe;AACf,QAAI,CAAC,KAAK,cAAc,IAAItB,CAAI,EAAG;AAQnC,UAAME,KACH,KAAK,cAAc,UAAUmB,IAC1B,KAAK,aAAa,OAClB,WACHA,MAAY,KAAK,aAAY,KAAK,cAAc;AACnD,QAAIc,GACAC;AACJ,QAAI;AACF,MAAAD,IAAS,MAAMpC,GAAmBC,GAAMsB,EAAI,MAAMA,EAAI,aAAapB,CAAS;AAAA,IAC9E,SAAS3B,GAAK;AACZ,MAAA6D,IAAa7D,aAAe,QAAQA,EAAI,UAAU,OAAOA,CAAG;AAAA,IAC9D;AAMA,IAAK,KAAK,cAAc,OAAOyB,CAAI,MAM/BmC,WAAa,eAAe,EAAE,OAAOd,GAAS,MAAMc,EAAO,KAAA,IAC/D,KAAK,MAAM,QAAQA,IAAS,EAAE,GAAGnC,GAAM,QAAAmC,MAAW,EAAE,GAAGnC,GAAM,YAAAoC,EAAA,CAAY;AAAA,EAC3E;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOQ,oBAA0B;AAChC,QAAI,CAAC,KAAK,aAAa,CAAC,KAAK,kBAAmB;AAKhD,UAAM7C,IACJ,KAAK,YAAY,KAAK,cAAcrC,IAAqB;AAC3D,QAAI,CAACqC,EAAS;AAEd,UAAMS,IAAO;AAAA,MACX,YAAY,KAAK,cAAc;AAAA,MAC/B,SAAAT;AAAA,MACA,MAAM,KAAK,gBAAgB;AAAA,MAC3B,eAAe,KAAK,iBAAiB;AAAA,IAAA,GAEjCtB,IAAM,KAAK,YAAYsB,CAAO,GAK9B8C,IAAS,KAAK,UAAU;AAAA,MAC5B,YAAY,KAAK,cAAc;AAAA,MAC/B,SAAA9C;AAAA,MACA,MAAM,KAAK,gBAAgB;AAAA,IAAA,CAC5B;AAOD,QAAItB,MAAQ,KAAK,YAAYA,MAAQ,KAAK,cAAc;AACtD,WAAK,cAAcoE;AACnB;AAAA,IACF;AAEA,IAAK,KAAK,WAAW,EAAE,MAAMpF,GAAW,MAAA+C,EAAA,CAAM,MAI1CqC,MAAW,KAAK,eAAa,KAAK,aACtC,KAAK,cAAcA,GACnB,KAAK,WAAWpE,GAGhB,KAAK,eAAe;AAAA,EACtB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAWQ,mBAAyB;AAK/B,UAAMqE,IAAiB,MAAM,QAAQ,KAAK,cAAc,IACpD,KAAK,iBACL,CAAA,GACEC,IACJ,OAAO,KAAK,uBAAwB,WAChC,KAAK,oBAAoB,SACzB,IACAtE,IAAM,KAAK,UAAU,EAAE,gBAAAqE,GAAgB,qBAAAC,GAAqB;AAClE,IAAItE,MAAQ,KAAK,mBAEf,KAAK,mBAAmB,UACxBqE,EAAe,WAAW,KAC1BC,MAAwB,MAMvB,KAAK,WAAW;AAAA,MACf,MAAMpF;AAAA,MACN,MAAM;AAAA,QACJ,gBAAAmF;AAAA,QACA,GAAIC,IAAsB,EAAE,qBAAAA,MAAwB,CAAA;AAAA,MAAC;AAAA,IACvD,CACD,MAIH,KAAK,iBAAiBtE;AAAA,EACxB;AAAA;AAAA,EAGQ,YAAYsB,GAAyB;AAC3C,WAAO,KAAK,UAAU;AAAA,MACpB,YAAY,KAAK,cAAc;AAAA,MAC/B,SAAAA;AAAA,MACA,MAAM,KAAK,gBAAgB;AAAA,MAC3B,eAAe,KAAK,iBAAiB;AAAA,IAAA,CACtC;AAAA,EACH;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOQ,WAAWS,GAAkC;AACnD,WAAO,KAAK,UAAU;AAAA,MACpB,YAAYA,EAAK,cAAc;AAAA,MAC/B,SAASA,EAAK;AAAA,MACd,MAAMA,EAAK,QAAQ;AAAA,MACnB,eAAeA,EAAK,iBAAiB;AAAA,IAAA,CACtC;AAAA,EACH;AAAA;AAAA,EAGQ,WAAWxB,GAA2B;AAC5C,UAAMgE,IAAS,KAAK,YAAY,cAAc,QAAQ,GAAG,eACnDC,IAAY,KAAK;AACvB,WAAI,CAACD,KAAU,CAACC,IAAkB,MAClCD,EAAO,YAAYhE,GAASiE,CAAS,GAC9B;AAAA,EACT;AAAA,EAEQ,uBAA6B;AACnC,IAAI,KAAK,gBAAgB,MACzB,KAAK,kBAAkB,OAAO,WAAW,MAAM;AAC7C,WAAK,MAAM;AAAA,QACT,MAAM;AAAA,QACN,SACE,wBAAwB,KAAK,OAAO,WAAW,KAAK,YAAY;AAAA,MAAA,CAGnE;AAAA,IACH,GAAG,KAAK,YAAY;AAAA,EACtB;AAAA,EAEQ,uBAA6B;AACnC,IAAI,KAAK,oBAAoB,WAC3B,OAAO,aAAa,KAAK,eAAe,GACxC,KAAK,kBAAkB;AAAA,EAE3B;AAAA,EAEQ,MAAMzC,GAA8B;AAC1C,SAAK,qBAAA,GACL,KAAK,UAAU,SACf,KAAK,MAAM,SAASA,CAAI;AAAA,EAC1B;AAAA,EAEQ,MAASuB,GAAcmB,GAAkB;AAC/C,SAAK;AAAA,MACH,IAAI,YAAYnB,GAAM,EAAE,QAAAmB,GAAQ,SAAS,IAAM,UAAU,GAAA,CAAM;AAAA,IAAA;AAAA,EAEnE;AACF;AAv7BE1B,EAAO,SAAS2B;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AADX,IAAMC,IAAN5B;AA+CgC6B,EAAA;AAAA,EAApCC,EAAS,EAAE,WAAW,WAAA,CAAY;AAAA,GA/CxBF,EA+C0B,WAAA,SAAA;AAEzBC,EAAA;AAAA,EAAXC,EAAA;AAAS,GAjDCF,EAiDC,WAAA,OAAA;AACyBC,EAAA;AAAA,EAApCC,EAAS,EAAE,WAAW,WAAA,CAAY;AAAA,GAlDxBF,EAkD0B,WAAA,SAAA;AACIC,EAAA;AAAA,EAAxCC,EAAS,EAAE,WAAW,eAAA,CAAgB;AAAA,GAnD5BF,EAmD8B,WAAA,aAAA;AAQAC,EAAA;AAAA,EAAxCC,EAAS,EAAE,WAAW,eAAA,CAAgB;AAAA,GA3D5BF,EA2D8B,WAAA,aAAA;AACAC,EAAA;AAAA,EAAxCC,EAAS,EAAE,WAAW,eAAA,CAAgB;AAAA,GA5D5BF,EA4D8B,WAAA,aAAA;AACAC,EAAA;AAAA,EAAxCC,EAAS,EAAE,WAAW,eAAA,CAAgB;AAAA,GA7D5BF,EA6D8B,WAAA,aAAA;AAKDC,EAAA;AAAA,EAAvCC,EAAS,EAAE,WAAW,cAAA,CAAe;AAAA,GAlE3BF,EAkE6B,WAAA,YAAA;AACXC,EAAA;AAAA,EAA5BC,EAAS,EAAE,SAAS,GAAA,CAAM;AAAA,GAnEhBF,EAmEkB,WAAA,MAAA;AAMeC,EAAA;AAAA,EAA3CC,EAAS,EAAE,MAAM,SAAS,SAAS,IAAM;AAAA,GAzE/BF,EAyEiC,WAAA,WAAA;AAMZC,EAAA;AAAA,EAA/BC,EAAS,EAAE,WAAW,GAAA,CAAO;AAAA,GA/EnBF,EA+EqB,WAAA,SAAA;AAewBC,EAAA;AAAA,EAAvDC,EAAS,EAAE,MAAM,SAAS,WAAW,gBAAgB;AAAA,GA9F3CF,EA8F6C,WAAA,aAAA;AAEdC,EAAA;AAAA,EAAzCC,EAAS,EAAE,WAAW,gBAAA,CAAiB;AAAA,GAhG7BF,EAgG+B,WAAA,cAAA;AAOCC,EAAA;AAAA,EAA1CC,EAAS,EAAE,WAAW,iBAAA,CAAkB;AAAA,GAvG9BF,EAuGgC,WAAA,eAAA;AAOHC,EAAA;AAAA,EAAvCC,EAAS,EAAE,WAAW,cAAA,CAAe;AAAA,GA9G3BF,EA8G6B,WAAA,YAAA;AAE5BC,EAAA;AAAA,EAAXC,EAAA;AAAS,GAhHCF,EAgHC,WAAA,OAAA;AA0CZC,EAAA;AAAA,EAhBCC,EAAS;AAAA,IACR,WAAW;AAAA,IACX,WAAW;AAAA,MACT,eAAe,CAACC,MAAgD;AAC9D,YAAI,CAACA,EAAO,QAAO,CAAA;AACnB,YAAI;AACF,gBAAMnC,IAAkB,KAAK,MAAMmC,CAAK;AACxC,iBAAO,MAAM,QAAQnC,CAAM,IAAKA,IAAmC,CAAA;AAAA,QACrE,QAAQ;AACN,yBAAQ,KAAK,sEAAsE,GAC5E,CAAA;AAAA,QACT;AAAA,MACF;AAAA,MACA,aAAa,CAACmC,MAAyC,KAAK,UAAUA,KAAS,CAAA,CAAE;AAAA,IAAA;AAAA,EACnF,CACD;AAAA,GAzJUH,EA0JX,WAAA,gBAAA;AAOkDC,EAAA;AAAA,EAAjDC,EAAS,EAAE,WAAW,wBAAA,CAAyB;AAAA,GAjKrCF,EAiKuC,WAAA,qBAAA;AASGC,EAAA;AAAA,EAApDC,EAAS,EAAE,MAAM,SAAS,WAAW,aAAa;AAAA,GA1KxCF,EA0K0C,WAAA,UAAA;AAMZC,EAAA;AAAA,EAAxCC,EAAS,EAAE,WAAW,eAAA,CAAgB;AAAA,GAhL5BF,EAgL8B,WAAA,aAAA;AAWDC,EAAA;AAAA,EAAvCC,EAAS,EAAE,WAAW,cAAA,CAAe;AAAA,GA3L3BF,EA2L6B,WAAA,YAAA;AAEgBC,EAAA;AAAA,EAAvDC,EAAS,EAAE,MAAM,QAAQ,WAAW,iBAAiB;AAAA,GA7L3CF,EA6L6C,WAAA,cAAA;AAEvCC,EAAA;AAAA,EAAhBG,EAAA;AAAM,GA/LIJ,EA+LM,WAAA,SAAA;AACAC,EAAA;AAAA,EAAhBG,EAAA;AAAM,GAhMIJ,EAgMM,WAAA,OAAA;AACAC,EAAA;AAAA,EAAhBG,EAAA;AAAM,GAjMIJ,EAiMM,WAAA,MAAA;AA6CAC,EAAA;AAAA,EAAhBG,EAAA;AAAM,GA9OIJ,EA8OM,WAAA,UAAA;"}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"use strict";const h=require("lit"),a=require("lit/decorators.js"),J=3,O="design-templates:builder:ready",w="design-templates:builder:open",N="design-templates:builder:close",P="design-templates:builder:save",M="design-templates:builder:error",B="design-templates:builder:content-request",K="design-templates:builder:content",$="design-templates:builder:dirty",F="design-templates:host:load",q=`<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<template><templateInfo><version>0.3</version></templateInfo><rootStack/><layouts/><variables/></template>`,x="design-templates:host:config",j="design-templates:host:saved",p={SESSION_UUID:"suuid",COMPANY_UUID:"cuuid",PROJECT_UUID:"puuid",SASS_KEY:"sassKey",FILEROBOT_TOKEN:"ftoken",SEC_TEMPLATE:"secTemplate",IFRAME:"iframe",EMBED_ORIGIN:"embedOrigin",BRAND_COLOR:"brandColor",THEME:"theme"},X={SESSION:"session",SEC_TEMPLATE:"secTemplate"},Y=/^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/;function H(s){return s?`/templates/${encodeURIComponent(s)}/edit`:"/templates/new"}const Q="/templates/embed",y="https://api.filerobot.com";async function V(s){if(!s.secTemplate)return s.sassKey??"";const e=await fetch(`${y}/${encodeURIComponent(s.token)}/v5/key/${encodeURIComponent(s.secTemplate)}`,{headers:{"X-Filerobot-Key":s.secTemplate}}),t=await e.json().catch(()=>({}));if(!e.ok||!t.key)throw new Error(t.msg??`key exchange failed: ${e.status}`);return t.key}function I(s,e){const t={"X-Filerobot-Key":e};return s.secTemplate||(s.sessionUuid&&(t["X-Session-Token"]=s.sessionUuid),s.companyUuid&&(t["X-Company-Token"]=s.companyUuid),s.projectUuid&&(t["X-Project-Token"]=s.projectUuid)),t}function G(s){return/^[0-9a-f][0-9a-f-]{18,}$/i.test(s)}async function R(s,e,t){let r;try{r=await fetch(`${y}/${encodeURIComponent(s.token)}/v5/files/${encodeURIComponent(t)}`,{headers:I(s,e)})}catch(d){throw new Error(`file lookup failed: ${d instanceof Error?d.message:String(d)}`)}const i=await r.json().catch(()=>({}));if([400,404,410,422].includes(r.status))return null;const l=i.msg??i.message??"";if(!r.ok||i.status==="error"){if(/not[ _-]?found|does not exist|no such file/i.test(l))return null;throw new Error(l||`file lookup failed: ${r.status}`)}return i.file??null}async function W(s,e,t,r){const i=new URL(`${y}/${encodeURIComponent(s.token)}/v5/files`);i.searchParams.set("q",`name:"${t}"`),i.searchParams.set("folder",r),i.searchParams.set("sort","modified_at:desc"),i.searchParams.set("limit","5");const l=await fetch(i,{headers:I(s,e)}),d=await l.json().catch(()=>({}));return!l.ok||d.status==="error"?null:d.files?.find(c=>c.name===t)??null}function z(s){let e=2166136261;for(let t=0;t<s.length;t++)e^=s.charCodeAt(t),e=Math.imul(e,16777619);return(e>>>0).toString(36)}function Z(s){let e;try{e=decodeURIComponent(s)}catch{e=s}const t=e.split("/").filter(Boolean);return t.pop(),"/"+t.join("/")}function ee(s,e){if(!s)return s;let t;try{const i=new DOMParser().parseFromString(e,"application/xml");if(i.querySelector("parsererror"))return s;t=i.querySelectorAll('variable[custom_ckey][source="URL"][type="text_placeholder"]')}catch{return s}if(t.length===0)return s;const r=new Set([...t].map(i=>i.getAttribute("name")??""));return s.split("&").filter(Boolean).filter(i=>!r.has(i.split("=")[0].replace(/^(\$|%24)/,""))).join("&")}let te=0;const se=new Set(["ERROR_SHA1_CONFLICT","SAME_ASSET_EXISTS_SKIP_UPLOAD"]);async function ie(s,e,t,r){if(!e.token)throw new Error("dam-store needs a token");const i=await V(e);if(!i)throw new Error("dam-store needs a sass key or a security template");const l=!!s.templateId&&G(s.templateId),d=!!r&&r!==s.templateId,[c,_]=await Promise.allSettled([l?R(e,i,s.templateId):Promise.resolve(null),d?R(e,i,r):Promise.resolve(null)]);if(c.status==="rejected")throw c.reason;const b=c.value;let E=null;if(_.status==="fulfilled")E=_.value;else if(!b)throw _.reason;const f=b??E,k=f?.url?.path?Z(f.url.path):t||"/",C=(s.name??"").trim().replace(/\.fdt$/i,"")||"template",A=f?.name?f.name:s.templateId?`${C}_${z(s.templateId)}.fdt`:`${C}_${Date.now().toString(36)}-${++te}.fdt`,S=new FormData;S.append("files[]",new Blob([s.content],{type:"text/xml+sfxtemplate"}),A);const L=ee(s.templateQuery??"",s.content);L&&S.append("info[files[]]",JSON.stringify({custom:{template_query:L}}));const g=await fetch(`${y}/${encodeURIComponent(e.token)}/v4/files?folder=${encodeURIComponent(k)}`,{method:"POST",headers:{...I(e,i),"X-Filerobot-Template":"true"},body:S}),u=await g.json().catch(()=>({})),v=u.code??u.files?.failed?.[0]?.code;if(v&&se.has(v)){let m=E??b;if(m?.uuid||(m=await W(e,i,A,k).catch(()=>null)),m?.uuid)return{uuid:m.uuid,url:m.url?.cdn??m.url?.public??""};throw new Error(`this project's deduplication policy already stores this exact content as another file (${v}) — edit the content, or pass that file's uuid as stored-uuid so the save can resolve to it`)}if(!g.ok||u.status==="error")throw new Error(u.msg??u.message??`upload failed: ${g.status}`);const T=u.file?.uuid??u.files?.uploaded?.[0]?.uuid;if(!T)throw new Error("upload succeeded but no file uuid in the response");let U=null;try{U=await R(e,i,T)}catch{}return{uuid:T,url:U?.url?.cdn??U?.url?.public??u.file?.url?.cdn??""}}var re=Object.defineProperty,n=(s,e,t,r)=>{for(var i=void 0,l=s.length-1,d;l>=0;l--)(d=s[l])&&(i=d(e,t,i)||i);return i&&re(e,t,i),i};const D=class D extends h.LitElement{constructor(){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.newTemplate=!1,this.templateName="",this.templateQuery="",this.brandColor="",this.theme="",this.customMetadata=[],this.customMetadataLabel="",this.damStore=!1,this.storeFolder="/",this.storedUuid="",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=e=>{if(!this._open||!e.origin||e.origin!==this._appOrigin)return;const t=this.shadowRoot?.querySelector("iframe");if(!t||e.source!==t.contentWindow)return;const r=e.data;if(!(!r||typeof r.type!="string"))switch(r.type){case O:case w:this._clearHandshakeTimer(),this._status!=="ready"&&(this._status="ready",this._emit("ready")),r.type===O&&(this._sentConfigKey=void 0),this._maybeSendConfig(),r.type===w&&this._emit("open");break;case P:this._emit("save",r.data);break;case B:this._contentRequested=!0,this._sentKey=void 0,this._sentSaveKey=void 0,this._maybeSendContent();break;case K:{const i=r.data;if(this._sentKey=this._contentKey(i.content),this._sentSaveKey=this._detailKey(i),this.damStore){this._pendingSaves.add(i);const l=this._docEpoch,d={auth:{token:this.token,sassKey:this.sassKey,secTemplate:this.secTemplate,sessionUuid:this.sessionUuid,companyUuid:this.companyUuid,projectUuid:this.projectUuid},storeFolder:this.storeFolder||"/"};this._storeQueue=this._storeQueue.then(()=>this._storeAndEmitSave(i,l,d))}else this._emit("save",i);break}case $:{const i=r.data;this._isDirty=!!i?.isDirty,this._emit("dirtychange",{isDirty:this._isDirty});break}case N:this._emit("close"),this.mode==="modal"&&(this._open=!1);break;case M:this._fail(r.data??{code:"unknown"});break}},this._storeQueue=Promise.resolve(),this._pendingSaves=new Set,this._docEpoch=0}get status(){return this._status}get isDirty(){return this._isDirty}open(e){e!==void 0&&(this.templateId=e),this._open=!0}close(){this._open=!1}load({content:e,templateId:t,name:r,templateQuery:i,storedUuid:l}){t!==void 0&&(this.templateId=t),r!==void 0&&(this.templateName=r),i!==void 0&&(this.templateQuery=i),this.storedUuid=l??"",this.newTemplate=!1,this.content=e,this._open=!0}createNew({templateId:e,name:t}={}){e!==void 0&&(this.templateId=e),t!==void 0&&(this.templateName=t),this.templateQuery="",this.storedUuid="",this.newTemplate=!0,this.content="",this._open=!0}confirmSave(e,t){this.stateless&&this._postToApp({type:j,data:{ok:e,message:t}})}connectedCallback(){super.connectedCallback(),window.addEventListener("message",this._onMessage)}disconnectedCallback(){super.disconnectedCallback(),window.removeEventListener("message",this._onMessage),this._clearHandshakeTimer(),setTimeout(()=>{this.isConnected||this._flushPendingSaves("widget removed before the rendering copy completed")},0)}willUpdate(e){super.willUpdate(e),this._autoOpenDecided||(this._autoOpenDecided=!0,this.mode==="inline"&&(this._open=!0));const t=this._computeSrc();t!==this._src&&(this._src=t,this._status=t?"loading":"idle")}updated(e){e.has("_src")&&(this._clearHandshakeTimer(),this._contentRequested=!1,this._sentKey=void 0,this._sentSaveKey=void 0,this._sentConfigKey=void 0,this._isDirty&&(this._isDirty=!1,this._emit("dirtychange",{isDirty:!1})),this._src&&this._startHandshakeTimer()),(e.has("content")||e.has("newTemplate")||e.has("templateId")||e.has("templateName")||e.has("templateQuery"))&&this._maybeSendContent(),(e.has("customMetadata")||e.has("customMetadataLabel"))&&this._status==="ready"&&this._maybeSendConfig()}render(){const e=this._src?h.html`<iframe
|
|
3
|
+
part="iframe"
|
|
4
|
+
title="Template builder"
|
|
5
|
+
src=${this._src}
|
|
6
|
+
allow="clipboard-read; clipboard-write"
|
|
7
|
+
></iframe>`:h.nothing,t=this._status==="loading"?h.html`<div class="spinner" part="spinner"></div>`:h.nothing;return this.mode==="modal"?this._open?h.html`<div class="overlay" part="overlay">
|
|
8
|
+
<div class="stage">${e}${t}</div>
|
|
9
|
+
</div>`:h.nothing:h.html`${e}${t}`}_computeSrc(){if(!this._open||!this.baseUrl||!this.token)return"";if(this.secTemplate){if(!this.stateless)return this._reportedStatelessRequired||(this._reportedStatelessRequired=!0,queueMicrotask(()=>this._fail({code:"invalid-config",message:"sec-template requires stateless mode — the app accepts a security template on the stateless embed route only."}))),"";this._reportedStatelessRequired=!1}else if(!this.sassKey||!this.sessionUuid)return"";let e;try{const t=this.stateless?Q:H(this.templateId||void 0),r=this.baseUrl.endsWith("/")?this.baseUrl:`${this.baseUrl}/`;e=new URL(t.replace(/^\//,""),r)}catch{return this._reportedBadBaseUrl!==this.baseUrl&&(this._reportedBadBaseUrl=this.baseUrl,queueMicrotask(()=>this._fail({code:"invalid-base-url",message:`base-url is not a valid URL: ${this.baseUrl}`}))),""}return this._reportedBadBaseUrl=void 0,e.searchParams.set(p.FILEROBOT_TOKEN,this.token),this.secTemplate?e.searchParams.set(p.SEC_TEMPLATE,this.secTemplate):(e.searchParams.set(p.SASS_KEY,this.sassKey),e.searchParams.set(p.SESSION_UUID,this.sessionUuid),this.companyUuid&&e.searchParams.set(p.COMPANY_UUID,this.companyUuid),this.projectUuid&&e.searchParams.set(p.PROJECT_UUID,this.projectUuid)),this.brandColor&&e.searchParams.set(p.BRAND_COLOR,this.brandColor),this.theme&&e.searchParams.set(p.THEME,this.theme),e.searchParams.set(p.IFRAME,"1"),e.searchParams.set(p.EMBED_ORIGIN,window.location.origin),e.toString()}get _appOrigin(){try{return new URL(this.baseUrl).origin}catch{return null}}flushPendingSaves(e="widget removed before the rendering copy completed"){this._flushPendingSaves(e)}_flushPendingSaves(e){for(const t of this._pendingSaves)this._pendingSaves.delete(t),this._emit("save",{...t,storeError:e})}async _storeAndEmitSave(e,t,r){if(!this._pendingSaves.has(e))return;const i=(this._storeMemory?.epoch===t?this._storeMemory.uuid:void 0)||t===this._docEpoch&&this.storedUuid||void 0;let l,d;try{l=await ie(e,r.auth,r.storeFolder,i)}catch(c){d=c instanceof Error?c.message:String(c)}this._pendingSaves.delete(e)&&(l&&(this._storeMemory={epoch:t,uuid:l.uuid}),this._emit("save",l?{...e,stored:l}:{...e,storeError:d}))}_maybeSendContent(){if(!this.stateless||!this._contentRequested)return;const e=this.content||(this.newTemplate?q:"");if(!e)return;const t={templateId:this.templateId||void 0,content:e,name:this.templateName||void 0,templateQuery:this.templateQuery||void 0},r=this._contentKey(e),i=JSON.stringify({templateId:this.templateId||void 0,content:e,name:this.templateName||void 0});if(r===this._sentKey||r===this._sentSaveKey){this._lastDocKey=i;return}this._postToApp({type:F,data:t})&&(i!==this._lastDocKey&&this._docEpoch++,this._lastDocKey=i,this._sentKey=r,this._sentSaveKey=void 0)}_maybeSendConfig(){const e=Array.isArray(this.customMetadata)?this.customMetadata:[],t=typeof this.customMetadataLabel=="string"?this.customMetadataLabel.trim():"",r=JSON.stringify({customMetadata:e,customMetadataLabel:t});r!==this._sentConfigKey&&(this._sentConfigKey===void 0&&e.length===0&&t===""||this._postToApp({type:x,data:{customMetadata:e,...t?{customMetadataLabel:t}:{}}})&&(this._sentConfigKey=r))}_contentKey(e){return JSON.stringify({templateId:this.templateId||void 0,content:e,name:this.templateName||void 0,templateQuery:this.templateQuery||void 0})}_detailKey(e){return JSON.stringify({templateId:e.templateId||void 0,content:e.content,name:e.name||void 0,templateQuery:e.templateQuery||void 0})}_postToApp(e){const t=this.shadowRoot?.querySelector("iframe")?.contentWindow,r=this._appOrigin;return!t||!r?!1:(t.postMessage(e,r),!0)}_startHandshakeTimer(){this.readyTimeout<=0||(this._handshakeTimer=window.setTimeout(()=>{this._fail({code:"handshake-timeout",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.`})},this.readyTimeout))}_clearHandshakeTimer(){this._handshakeTimer!==void 0&&(window.clearTimeout(this._handshakeTimer),this._handshakeTimer=void 0)}_fail(e){this._clearHandshakeTimer(),this._status="error",this._emit("error",e)}_emit(e,t){this.dispatchEvent(new CustomEvent(e,{detail:t,bubbles:!0,composed:!0}))}};D.styles=h.css`
|
|
10
|
+
:host {
|
|
11
|
+
display: block;
|
|
12
|
+
position: relative;
|
|
13
|
+
}
|
|
14
|
+
:host([mode='modal']) {
|
|
15
|
+
display: contents;
|
|
16
|
+
}
|
|
17
|
+
.overlay {
|
|
18
|
+
position: fixed;
|
|
19
|
+
inset: 0;
|
|
20
|
+
z-index: 2147483000;
|
|
21
|
+
background: rgba(0, 0, 0, 0.55);
|
|
22
|
+
display: flex;
|
|
23
|
+
}
|
|
24
|
+
.stage {
|
|
25
|
+
position: relative;
|
|
26
|
+
flex: 1;
|
|
27
|
+
display: flex;
|
|
28
|
+
}
|
|
29
|
+
iframe {
|
|
30
|
+
border: 0;
|
|
31
|
+
flex: 1;
|
|
32
|
+
width: 100%;
|
|
33
|
+
height: 100%;
|
|
34
|
+
}
|
|
35
|
+
.spinner {
|
|
36
|
+
position: absolute;
|
|
37
|
+
inset: 0;
|
|
38
|
+
margin: auto;
|
|
39
|
+
width: 32px;
|
|
40
|
+
height: 32px;
|
|
41
|
+
border: 3px solid rgba(128, 128, 128, 0.3);
|
|
42
|
+
border-top-color: currentColor;
|
|
43
|
+
border-radius: 50%;
|
|
44
|
+
animation: sfx-tb-spin 0.8s linear infinite;
|
|
45
|
+
pointer-events: none;
|
|
46
|
+
}
|
|
47
|
+
@keyframes sfx-tb-spin {
|
|
48
|
+
to {
|
|
49
|
+
transform: rotate(360deg);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
`;let o=D;n([a.property({attribute:"base-url"})],o.prototype,"baseUrl");n([a.property()],o.prototype,"token");n([a.property({attribute:"sass-key"})],o.prototype,"sassKey");n([a.property({attribute:"session-uuid"})],o.prototype,"sessionUuid");n([a.property({attribute:"sec-template"})],o.prototype,"secTemplate");n([a.property({attribute:"company-uuid"})],o.prototype,"companyUuid");n([a.property({attribute:"project-uuid"})],o.prototype,"projectUuid");n([a.property({attribute:"template-id"})],o.prototype,"templateId");n([a.property({reflect:!0})],o.prototype,"mode");n([a.property({type:Boolean,reflect:!0})],o.prototype,"stateless");n([a.property({attribute:!1})],o.prototype,"content");n([a.property({type:Boolean,attribute:"new-template"})],o.prototype,"newTemplate");n([a.property({attribute:"template-name"})],o.prototype,"templateName");n([a.property({attribute:"template-query"})],o.prototype,"templateQuery");n([a.property({attribute:"brand-color"})],o.prototype,"brandColor");n([a.property()],o.prototype,"theme");n([a.property({attribute:"custom-metadata",converter:{fromAttribute:s=>{if(!s)return[];try{const e=JSON.parse(s);return Array.isArray(e)?e:[]}catch{return console.warn("[sfx-template-builder] custom-metadata is not valid JSON — ignoring."),[]}},toAttribute:s=>JSON.stringify(s??[])}})],o.prototype,"customMetadata");n([a.property({attribute:"custom-metadata-label"})],o.prototype,"customMetadataLabel");n([a.property({type:Boolean,attribute:"dam-store"})],o.prototype,"damStore");n([a.property({attribute:"store-folder"})],o.prototype,"storeFolder");n([a.property({attribute:"stored-uuid"})],o.prototype,"storedUuid");n([a.property({type:Number,attribute:"ready-timeout"})],o.prototype,"readyTimeout");n([a.state()],o.prototype,"_status");n([a.state()],o.prototype,"_open");n([a.state()],o.prototype,"_src");n([a.state()],o.prototype,"_isDirty");exports.AUTH_MODES=X;exports.BLANK_TEMPLATE_XML=q;exports.BRAND_COLOR_PATTERN=Y;exports.BUILDER_CLOSE=N;exports.BUILDER_CONTENT=K;exports.BUILDER_CONTENT_REQUEST=B;exports.BUILDER_DIRTY=$;exports.BUILDER_ERROR=M;exports.BUILDER_OPEN=w;exports.BUILDER_READY=O;exports.BUILDER_SAVE=P;exports.EMBED_PARAMS=p;exports.EMBED_ROUTE=Q;exports.HOST_CONFIG=x;exports.HOST_LOAD=F;exports.HOST_SAVED=j;exports.PROTOCOL_VERSION=J;exports.SfxTemplateBuilder=o;exports.builderRoute=H;
|
|
53
|
+
//# sourceMappingURL=template-builder-Byqg1q93.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"template-builder-Byqg1q93.cjs","sources":["../src/protocol.ts","../src/dam-store.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 = 3\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 * An empty `.fdt` document: no layouts, no layers, no variables. What the\n * widget sends as `HOST_LOAD` content when the host asked for a new template\n * (`new-template`) instead of supplying one, so starting from scratch costs a\n * host no knowledge of the template format.\n *\n * The editor opens on its empty state — \"No layouts yet. Click + Add to create\n * one.\" — and the user picks the canvas size there. Save is refused until a\n * layout exists, and hands back a fully-formed document serialized by the app,\n * not this skeleton.\n *\n * Sent as ordinary `HOST_LOAD` content rather than a new message so it works\n * against app deployments that predate this widget version: the document is\n * the whole signal, and every app that can parse a template can parse this.\n *\n * `version` tracks the app's `TEMPLATE_VERSION` for the benefit of whoever\n * reads this next: nothing consumes it. The parser never looks at it, and the\n * backend never sees this document — the app refuses to save a template with\n * no layouts, so what reaches the render pipeline was re-serialized by the app\n * with a layout present. A widget lagging the app by a version still loads.\n * `design-templates` pins the pair in\n * `src/lib/xml/__tests__/blank-template.test.ts`.\n */\nexport const BLANK_TEMPLATE_XML =\n '<?xml version=\"1.0\" encoding=\"UTF-8\"?>\\n' +\n '<template><templateInfo><version>0.3</version></templateInfo>' +\n '<rootStack/><layouts/><variables/></template>'\n\n/**\n * Host-supplied editor configuration (protocol v3). Sent once the app reports\n * `BUILDER_READY`, and again whenever the host changes it.\n *\n * Separate from `HOST_LOAD` because it is not per-template and because it must\n * also reach DAM-backed embeds, which never receive a `HOST_LOAD` at all — the\n * app loads those templates itself.\n *\n * Purely additive: an app deployment that predates this message ignores it and\n * behaves exactly as before, so a newer widget stays compatible with an older\n * app. Held to the same origin bar as `HOST_LOAD`.\n */\nexport const HOST_CONFIG = 'design-templates:host:config'\n\n/**\n * One field of a host-supplied metadata model, offered in the editor as the\n * \"Custom metadata\" value source.\n *\n * The model is a vocabulary, not data: it names the fields the host can fill at\n * render time, so an author can bind a variable to `sku` rather than having to\n * remember that the variable's slug happens to mean the SKU. No value travels\n * with it — the host substitutes one by putting `$slug=value` in the render\n * query, exactly as it would for a free-text variable.\n *\n * This is what makes named fields workable in `secTemplate` / stateless embeds,\n * where the Hub project model (and with it the \"File metadata\" source) is\n * unavailable.\n */\nexport interface CustomMetadataField {\n /**\n * Stable identifier stored in the template as `custom_ckey`. The host's own\n * key for the field — the app never resolves it against anything.\n */\n key: string\n /** Label shown in the editor's field picker. Falls back to `key` when empty. */\n title?: string\n /** Optional section header, used to group fields in the picker. */\n group?: string\n}\n\nexport interface HostConfigData {\n /**\n * Metadata model offered as the \"Custom metadata\" value source. Omitted or\n * empty hides that source in the editor, so a host that sends nothing sees\n * the two sources it always had.\n */\n customMetadata?: CustomMetadataField[]\n /**\n * Display name for the custom-metadata value source in the editor's UI\n * (source dropdowns, properties-panel section). Defaults to \"Custom\n * metadata\"; a host can rename it after its own domain — e.g. \"External\n * metadata\" or \"Product attributes\". Pure wording: the stored template is\n * unaffected.\n */\n customMetadataLabel?: string\n}\n\nexport interface HostConfigMessage {\n type: typeof HOST_CONFIG\n data: HostConfigData\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 | HostConfigMessage\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","/**\n * The `dam-store` save path: upload an edited template to Filerobot so the\n * CDN can render it.\n *\n * A stateless save hands the host raw XML — but the CDN renders only stored\n * files, so a host that wants render URLs (previews, production banners) needs\n * a copy in the DAM too. With `dam-store` the element makes that copy itself,\n * with the same multipart upload the DAM-backed editor uses, and the `save`\n * event carries the stored file's links next to the raw data.\n *\n * The raw `content` remains the host's copy of record: nothing here changes\n * what the save event has always carried.\n */\n\nimport type { BuilderContentData } from './protocol'\n\nexport const FILEROBOT_API = 'https://api.filerobot.com'\n\n/** Credentials the element already holds; one of sassKey / secTemplate. */\nexport interface DamStoreAuth {\n token: string\n sassKey?: string\n secTemplate?: string\n sessionUuid?: string\n companyUuid?: string\n projectUuid?: string\n}\n\n/** The stored copy's links, carried on the `save` event as `detail.stored`. */\nexport interface StoredTemplate {\n /** DAM file uuid of the stored `.fdt`. */\n uuid: string\n /**\n * CDN URL of the stored file, with its current `?vh=` cache key — append a\n * template query to render it. Empty when the file record could not be read\n * back after the upload (the file is stored regardless).\n */\n url: string\n}\n\nexport interface FileRecord {\n uuid?: string\n name?: string\n folder?: { name?: string }\n url?: { cdn?: string; public?: string; path?: string }\n}\n\n/**\n * A security template is not a key — it is exchanged for a short-lived sass\n * key first, the template authenticating its own exchange. Same call the app\n * and the asset picker make.\n *\n * Exported (with `apiHeaders` / `getFileRecord`) for the demo page, which\n * plays the host half of the same API conversation — one implementation of\n * the auth rules, not two drifting copies.\n */\nexport async function resolveKey(auth: DamStoreAuth): Promise<string> {\n if (!auth.secTemplate) return auth.sassKey ?? ''\n const res = await fetch(\n `${FILEROBOT_API}/${encodeURIComponent(auth.token)}/v5/key/${encodeURIComponent(auth.secTemplate)}`,\n { headers: { 'X-Filerobot-Key': auth.secTemplate } },\n )\n const body = (await res.json().catch(() => ({}))) as { key?: string; msg?: string }\n if (!res.ok || !body.key) {\n throw new Error(body.msg ?? `key exchange failed: ${res.status}`)\n }\n return body.key\n}\n\n/**\n * Session scope only: a minted key carries its own, and pairing it with a\n * session's uuids would mix one mode's key with the other mode's scope.\n */\nexport function apiHeaders(auth: DamStoreAuth, key: string): Record<string, string> {\n const headers: Record<string, string> = { 'X-Filerobot-Key': key }\n if (!auth.secTemplate) {\n if (auth.sessionUuid) headers['X-Session-Token'] = auth.sessionUuid\n if (auth.companyUuid) headers['X-Company-Token'] = auth.companyUuid\n if (auth.projectUuid) headers['X-Project-Token'] = auth.projectUuid\n }\n return headers\n}\n\n/**\n * Whether a template id plausibly names a DAM file (hex-and-dashes uuid).\n * Opaque host ids ('demo-1', 'sample-spring-banner') never do — looking them\n * up would waste a round trip per save and couple every save to whichever\n * status the API happens to answer a malformed id with.\n */\nexport function looksLikeDamFileUuid(id: string): boolean {\n return /^[0-9a-f][0-9a-f-]{18,}$/i.test(id)\n}\n\n/**\n * One file's record. `null` means the identifier names nothing — a 404/gone,\n * a 4xx rejecting the id itself, or the API's not-found envelope — all normal\n * answers here. What THROWS is a failure to answer (auth, rate limit, 5xx,\n * network): collapsing those into null would make a transient blip read as\n * \"file gone\", and the callers act on that — re-homing an existing template\n * into the fallback folder as a duplicate, or failing an unchanged re-save.\n */\nexport async function getFileRecord(\n auth: DamStoreAuth,\n key: string,\n uuid: string,\n): Promise<FileRecord | null> {\n let res: Response\n try {\n res = await fetch(\n `${FILEROBOT_API}/${encodeURIComponent(auth.token)}/v5/files/${encodeURIComponent(uuid)}`,\n { headers: apiHeaders(auth, key) },\n )\n } catch (err) {\n throw new Error(\n `file lookup failed: ${err instanceof Error ? err.message : String(err)}`,\n )\n }\n const body = (await res.json().catch(() => ({}))) as {\n status?: string\n msg?: string\n message?: string\n file?: FileRecord\n }\n // \"This identifier names nothing\" — not an error worth failing a save for.\n if ([400, 404, 410, 422].includes(res.status)) return null\n const message = body.msg ?? body.message ?? ''\n if (!res.ok || body.status === 'error') {\n if (/not[ _-]?found|does not exist|no such file/i.test(message)) return null\n throw new Error(message || `file lookup failed: ${res.status}`)\n }\n return body.file ?? null\n}\n\n/**\n * The newest file with exactly this name in a folder, or null. Used as a last\n * resort by the unchanged-content conflict path; best-effort by design.\n */\nasync function findByName(\n auth: DamStoreAuth,\n key: string,\n fileName: string,\n folder: string,\n): Promise<FileRecord | null> {\n const url = new URL(\n `${FILEROBOT_API}/${encodeURIComponent(auth.token)}/v5/files`,\n )\n url.searchParams.set('q', `name:\"${fileName}\"`)\n url.searchParams.set('folder', folder)\n url.searchParams.set('sort', 'modified_at:desc')\n url.searchParams.set('limit', '5')\n const res = await fetch(url, { headers: apiHeaders(auth, key) })\n const body = (await res.json().catch(() => ({}))) as {\n status?: string\n files?: FileRecord[]\n }\n if (!res.ok || body.status === 'error') return null\n return body.files?.find((f) => f.name === fileName) ?? null\n}\n\n/**\n * FNV-1a 32-bit of the host's template id — the deterministic filename\n * suffix that keeps one document on one DAM file across page reloads.\n */\nfunction hashId(id: string): string {\n let h = 0x811c9dc5\n for (let i = 0; i < id.length; i++) {\n h ^= id.charCodeAt(i)\n h = Math.imul(h, 0x01000193)\n }\n return (h >>> 0).toString(36)\n}\n\n/** The folder half of a file's URL path (mirrors the app's folderPathFromFile). */\nfunction folderFromPath(rawPath: string): string {\n let path: string\n try {\n path = decodeURIComponent(rawPath)\n } catch {\n path = rawPath\n }\n const segments = path.split('/').filter(Boolean)\n segments.pop()\n return '/' + segments.join('/')\n}\n\n/**\n * The `$slug=value` pairs of custom-metadata-bound variables, dropped from the\n * query stored as the file's `template_query` metadata. The stored default has\n * to be record-agnostic: the editor hands back whatever values the host opened\n * it with, and storing those would pin one record's data as the template's own\n * default. The binding lives in the XML (`custom_ckey`); the value belongs to\n * the record and goes back in at render time.\n */\nfunction stripBoundValues(templateQuery: string, content: string): string {\n if (!templateQuery) return templateQuery\n let bound: NodeListOf<Element>\n try {\n const doc = new DOMParser().parseFromString(content, 'application/xml')\n if (doc.querySelector('parsererror')) return templateQuery\n bound = doc.querySelectorAll(\n 'variable[custom_ckey][source=\"URL\"][type=\"text_placeholder\"]',\n )\n } catch {\n return templateQuery\n }\n if (bound.length === 0) return templateQuery\n const slugs = new Set([...bound].map((el) => el.getAttribute('name') ?? ''))\n return templateQuery\n .split('&')\n .filter(Boolean)\n .filter((part) => !slugs.has(part.split('=')[0].replace(/^(\\$|%24)/, '')))\n .join('&')\n}\n\n/**\n * \"Content unchanged\" — the stored file already IS this save. Filerobot\n * reports it inconsistently: with a 2xx or an error status alike, at the top\n * level or inside a per-file `files.failed[]` entry.\n */\n/** Uniquifies first-save filenames within a page (Date.now can collide). */\nlet uploadSeq = 0\n\nconst UNCHANGED_UPLOAD_CODES = new Set([\n 'ERROR_SHA1_CONFLICT',\n 'SAME_ASSET_EXISTS_SKIP_UPLOAD',\n])\n\ninterface UploadResponseBody {\n status?: string\n code?: string\n msg?: string\n message?: string\n file?: FileRecord\n files?: {\n uploaded?: FileRecord[]\n failed?: Array<{ code?: string }>\n }\n}\n\n/**\n * Store one save in the DAM and return the stored copy's links.\n *\n * Uploads into the folder the file this template is already stored as lives\n * in — the host's `templateId` when it names a DAM file, else `knownUuid`\n * (the copy a previous save in this session made; hosts persist `stored.uuid`\n * rather than echoing it into `template-id`, which would reload the editor) —\n * so same name + folder makes the backend version the template in place. New\n * templates land in `fallbackFolder`. Unchanged content resolves to the\n * already-stored file rather than failing. Throws with a human-readable\n * message when the copy could not be made; the caller decides what a save\n * without a stored copy means.\n */\nexport async function storeTemplateInDam(\n data: BuilderContentData,\n auth: DamStoreAuth,\n fallbackFolder: string,\n knownUuid?: string,\n): Promise<StoredTemplate> {\n if (!auth.token) throw new Error('dam-store needs a token')\n const key = await resolveKey(auth)\n if (!key) throw new Error('dam-store needs a sass key or a security template')\n\n // Two candidate records with different jobs. The host's `templateId` file\n // anchors the FOLDER (that is where the template lives) — looked up only\n // when the id is actually uuid-shaped; an opaque host id names nothing in\n // the DAM by definition. The copy this session last made (`knownUuid`) is\n // the freshest row and is what an unchanged-content conflict must resolve\n // to — under a VERSION policy the `templateId` row can be an older version\n // whose uuid/`?vh=` would hand the host a regressive pointer.\n const wantFromId = !!data.templateId && looksLikeDamFileUuid(data.templateId)\n const wantOwnCopy = !!knownUuid && knownUuid !== data.templateId\n // Independent lookups — in parallel, not a round trip each. The failure\n // rules differ by role: the templateId anchor is required (proceeding on a\n // transient failure would re-home the file into the fallback folder), while\n // the knownUuid lookup is an optional freshness upgrade whose transient\n // failure must not fail a save the other anchor can carry — unless it was\n // the only tie to the existing file.\n const [fromRes, ownRes] = await Promise.allSettled([\n wantFromId\n ? getFileRecord(auth, key, data.templateId as string)\n : Promise.resolve<FileRecord | null>(null),\n wantOwnCopy\n ? getFileRecord(auth, key, knownUuid as string)\n : Promise.resolve<FileRecord | null>(null),\n ])\n if (fromRes.status === 'rejected') throw fromRes.reason\n const fromId = fromRes.value\n let ownCopy: FileRecord | null = null\n if (ownRes.status === 'fulfilled') ownCopy = ownRes.value\n else if (!fromId) throw ownRes.reason\n\n const anchor = fromId ?? ownCopy\n const folder = anchor?.url?.path\n ? folderFromPath(anchor.url.path)\n : fallbackFolder || '/'\n\n // The DAM filename. Versioning-in-place matches on name + folder, so the\n // name must be STABLE per document and DISTINCT between documents:\n // - a known copy's own filename wins — versioning keeps matching across\n // renames and sessions;\n // - else, with a host template id, the name carries a hash of that id —\n // deterministic, so a reload without the stored-uuid seed still lands on\n // the same file, while two \"Untitled\"s with different ids never collide;\n // - else (no id at all) a per-page unique suffix — the one case where\n // nothing identifies the document across sessions.\n const display = (data.name ?? '').trim().replace(/\\.fdt$/i, '') || 'template'\n const fileName = anchor?.name\n ? anchor.name\n : data.templateId\n ? `${display}_${hashId(data.templateId)}.fdt`\n : `${display}_${Date.now().toString(36)}-${++uploadSeq}.fdt`\n\n const body = new FormData()\n // The same MIME the app's own template uploads declare\n // (TEMPLATE_MIME_TYPE in the design-templates-app) — plain text/xml would\n // risk the copy not being classified as a template_fdt asset.\n body.append(\n 'files[]',\n new Blob([data.content], { type: 'text/xml+sfxtemplate' }),\n fileName,\n )\n const storedQuery = stripBoundValues(data.templateQuery ?? '', data.content)\n if (storedQuery) {\n body.append(\n 'info[files[]]',\n JSON.stringify({ custom: { template_query: storedQuery } }),\n )\n }\n\n const res = await fetch(\n `${FILEROBOT_API}/${encodeURIComponent(auth.token)}/v4/files?folder=${encodeURIComponent(folder)}`,\n {\n method: 'POST',\n headers: { ...apiHeaders(auth, key), 'X-Filerobot-Template': 'true' },\n body,\n },\n )\n const parsed = (await res.json().catch(() => ({}))) as UploadResponseBody\n\n const code = parsed.code ?? parsed.files?.failed?.[0]?.code\n if (code && UNCHANGED_UPLOAD_CODES.has(code)) {\n let resolved = ownCopy ?? fromId\n if (!resolved?.uuid) {\n // No anchor in hand — typically the first save after a page reload\n // without the stored-uuid seed. In that case the file this conflict\n // points at is this document's own earlier copy, which carries exactly\n // the (deterministic) filename we just tried to upload — find it.\n resolved = await findByName(auth, key, fileName, folder).catch(() => null)\n }\n if (resolved?.uuid) {\n return {\n uuid: resolved.uuid,\n url: resolved.url?.cdn ?? resolved.url?.public ?? '',\n }\n }\n // A genuinely foreign copy: the project's dedupe policy already stores\n // these exact bytes as some other document's file.\n throw new Error(\n `this project's deduplication policy already stores this exact content as another file (${code}) — ` +\n 'edit the content, or pass that file\\'s uuid as stored-uuid so the save can resolve to it',\n )\n }\n if (!res.ok || parsed.status === 'error') {\n throw new Error(parsed.msg ?? parsed.message ?? `upload failed: ${res.status}`)\n }\n\n const uuid = parsed.file?.uuid ?? parsed.files?.uploaded?.[0]?.uuid\n if (!uuid) throw new Error('upload succeeded but no file uuid in the response')\n\n // Read the record back rather than trusting the upload response, whose shape\n // varies — this is also what yields the fresh `?vh=` cache key. Best-effort:\n // the file IS stored by now, so a failed read must not turn the save into a\n // storeError — the url just falls back to what the upload response carried.\n let record: FileRecord | null = null\n try {\n record = await getFileRecord(auth, key, uuid)\n } catch {\n // Tolerated — see above.\n }\n return {\n uuid,\n url:\n record?.url?.cdn ??\n record?.url?.public ??\n parsed.file?.url?.cdn ??\n '',\n }\n}\n","import { LitElement, html, css, nothing, type PropertyValues } from 'lit'\nimport { property, state } from 'lit/decorators.js'\nimport {\n BLANK_TEMPLATE_XML,\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_CONFIG,\n HOST_LOAD,\n HOST_SAVED,\n builderRoute,\n type CustomMetadataField,\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'\nimport {\n storeTemplateInDam,\n type DamStoreAuth,\n type StoredTemplate,\n} from './dam-store'\n\nexport type { StoredTemplate, DamStoreAuth } from './dam-store'\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. With `dam-store` the element\n * uploads a rendering copy to Filerobot first, and the detail additionally\n * carries `stored` (the copy's uuid + CDN URL) — or `storeError` when that\n * copy could not be made; the raw `content` arrives either way.\n */\nexport type TemplateBuilderSaveDetail =\n | BuilderSaveData\n | (BuilderContentData & { stored?: StoredTemplate; storeError?: string })\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. To start a template from\n * scratch, set `new-template` (or call `createNew()`) instead of `content`\n * — the widget supplies the blank document.\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`, or `new-template` to start from\n * scratch.\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 /**\n * Stateless mode: open on a new, empty template rather than one of yours.\n * The widget supplies the blank document ({@link BLANK_TEMPLATE_XML}), so\n * nothing here needs to know the template format — the user picks the canvas\n * size in the editor, and `save` hands back a complete document to store.\n *\n * `content` wins when both are set: a host with a real template to edit is\n * not asking for a blank one.\n *\n * A flag rather than an inferred meaning for empty `content`, because empty\n * already means \"the host is still fetching\" — the editor waits on a spinner\n * for it, and blanking that case would flash an empty document in front of\n * every host that mounts the builder before its request resolves.\n */\n @property({ type: Boolean, attribute: 'new-template' }) newTemplate = false\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 /**\n * Metadata model to offer in the editor as the **Custom metadata** value\n * source: `[{ key, title?, group? }]`. Authors then bind a text variable to\n * one of your field names instead of to a bare slug.\n *\n * Names only — no values travel with the model, and the app resolves nothing\n * against it. The bound key is stored in the template as `custom_ckey`, and\n * the variable renders like any free-text one: your pipeline puts\n * `$slug=value` in the render query. Read the key back from the saved `.fdt`\n * to know which of your fields each variable expects.\n *\n * Leave it empty and the source is not offered at all, so hosts that send\n * nothing see the editor they always had. Its main use is `sec-template` /\n * stateless embeds, where the Hub project model — and with it the \"File\n * metadata\" source — is unavailable.\n *\n * Settable as a property (an array) or as a `custom-metadata` attribute\n * holding that array as JSON. Neither is trusted to be well-formed:\n * unparseable JSON is warned about, a value that is not an array is treated as\n * no model, and individual fields the editor cannot use are dropped there —\n * a config typo costs the field, not the editor.\n *\n * Lit compares by identity: assign a new array to change the model, don't\n * mutate the one you passed.\n */\n @property({\n attribute: 'custom-metadata',\n converter: {\n fromAttribute: (value: string | null): CustomMetadataField[] => {\n if (!value) return []\n try {\n const parsed: unknown = JSON.parse(value)\n return Array.isArray(parsed) ? (parsed as CustomMetadataField[]) : []\n } catch {\n console.warn('[sfx-template-builder] custom-metadata is not valid JSON — ignoring.')\n return []\n }\n },\n toAttribute: (value: CustomMetadataField[]): string => JSON.stringify(value ?? []),\n },\n })\n customMetadata: CustomMetadataField[] = []\n /**\n * Display name for the custom-metadata value source in the editor's UI.\n * Empty means the editor's default (\"Custom metadata\"); a host can rename\n * it after its own domain — e.g. \"External metadata\". Pure wording: the\n * stored template is unaffected.\n */\n @property({ attribute: 'custom-metadata-label' }) customMetadataLabel = ''\n /**\n * Stateless only: store each save in Filerobot too, so the CDN can render\n * it. The `save` event then carries `stored: { uuid, url }` next to the raw\n * `content` — or `storeError` when the copy failed (the raw data arrives\n * either way; whether that fails the save is the host's call via the ack).\n * Uses the element's own credentials; a security template needs a scope\n * that allows uploads.\n */\n @property({ type: Boolean, attribute: 'dam-store' }) damStore = false\n /**\n * Folder new templates are stored into when `dam-store` is on and the\n * template id names no existing DAM file (an existing file's own folder\n * always wins, so same name + folder versions it in place).\n */\n @property({ attribute: 'store-folder' }) storeFolder = '/'\n /**\n * `dam-store`: the uuid of the rendering copy this document already has —\n * the `stored.uuid` a previous session's save reported, passed back in by\n * the host alongside the content. Without it the element only remembers\n * copies it made itself, so after a reload an unchanged re-save cannot find\n * its own file and reports a spurious `storeError`, and a changed one starts\n * a fresh file instead of versioning the existing one. Set it when reopening\n * a stored template; it belongs to the document, so hosts that swap\n * documents must swap (or clear) it too — `load()` does this for you.\n */\n @property({ attribute: 'stored-uuid' }) storedUuid = ''\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 * Identity of the last SAVE the app handed back, in the same shape as\n * `_sentKey`. A host echoing the full save detail into the props (content,\n * name, templateQuery) matches this key rather than `_sentKey`, whose name\n * and query still describe what the template was loaded with — without it\n * the echo would post a HOST_LOAD that reloads the editor and wipes undo\n * history on nearly every save (a save almost always changes the query).\n */\n private _sentSaveKey?: string\n /**\n * Identity of the config already delivered, so re-renders don't re-post it.\n * Undefined means the app has not been told anything yet — set back to that\n * whenever a new app instance loads.\n */\n private _sentConfigKey?: 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 storedUuid,\n }: {\n content: string\n templateId?: string\n name?: string\n templateQuery?: string\n storedUuid?: 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 // Omission clears rather than keeps: the seed names the DOCUMENT's stored\n // copy, and carrying one document's uuid into the next would anchor its\n // saves to the wrong file. \"Unknown\" is the safe reading of not saying.\n this.storedUuid = storedUuid ?? ''\n // A template of your own is not the blank one: clearing the flag lets a\n // host alternate between `createNew()` and `load()` on one element.\n this.newTemplate = false\n this.content = content\n this._open = true\n }\n\n /**\n * Stateless mode: open the editor on a new, empty template, opening it if\n * needed. Equivalent to setting `new-template` and calling `open()`.\n *\n * The user chooses the canvas size from the editor's empty state; `save`\n * then hands back a complete `.fdt` document — the first one you store for\n * this record. Pass `templateId` if you have already allocated one; leave it\n * out and the `save` payload simply comes back without an id.\n *\n * Calling it again on an editor that is already showing the blank document\n * does nothing — resending would discard whatever the user has built since.\n * To genuinely start over, `close()` and reopen.\n */\n createNew({\n templateId,\n name,\n }: { templateId?: string; name?: string } = {}): void {\n if (templateId !== undefined) this.templateId = templateId\n if (name !== undefined) this.templateName = name\n // A new document has no layouts and no variables, so there is no render\n // for a query to select — and a leftover one would name layouts and\n // variables of the previous template. Same for the stored-copy seed: a\n // blank document has no copy, and a stale one would anchor the first save\n // to the previous document's file.\n this.templateQuery = ''\n this.storedUuid = ''\n this.newTemplate = true\n this.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 // Element-level listeners still fire on a detached element, so a vanilla\n // host that removes the widget mid-upload still gets its raw save. (A\n // framework wrapper unsubscribes its listeners before the node detaches,\n // which is why the public `flushPendingSaves()` exists — the React wrapper\n // calls it from its cleanup, while its listeners still hear the event.)\n //\n // Deferred a task: REPARENTING (appendChild into another container)\n // detaches and reattaches synchronously, and flushing on the detach half\n // would report a false storeError for an upload that lands fine. Only a\n // detach that is still detached a tick later is a real removal.\n setTimeout(() => {\n if (!this.isConnected) {\n this._flushPendingSaves('widget removed before the rendering copy completed')\n }\n }, 0)\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 // Delivery state only — this is a new APP instance, not a new document.\n // `_lastDocKey` / `_docEpoch` / `_storeMemory` describe the document and\n // survive: resetting them here made every modal close or theme swap\n // forget the stored copy and fork the file on the next save.\n this._contentRequested = false\n this._sentKey = undefined\n this._sentSaveKey = undefined\n this._sentConfigKey = 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('newTemplate') ||\n changed.has('templateId') ||\n changed.has('templateName') ||\n changed.has('templateQuery')\n ) {\n this._maybeSendContent()\n }\n // Config is independent of the template: it also has to reach DAM-backed\n // embeds, which never send a content request. Only once the app is ready,\n // though — before that there is nothing listening, and the ready signal\n // sends whatever the latest value is anyway.\n if (\n (changed.has('customMetadata') || changed.has('customMetadataLabel')) &&\n this._status === 'ready'\n ) {\n this._maybeSendConfig()\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 // Config rides the ready signal: the app has its listener attached by\n // the time it announces, and a remount inside an unchanged iframe\n // re-announces — so this is also the resend point after the editor\n // reloads and forgets what it was told.\n //\n // Keyed on READY alone, because a single mount announces READY *and*\n // OPEN and resetting on both would post the same config twice. OPEN\n // still calls in, without the reset: after a READY that is a no-op, and\n // it is the only signal an app deployment older than protocol v1 sends.\n if (msg.type === BUILDER_READY) this._sentConfigKey = undefined\n this._maybeSendConfig()\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._sentSaveKey = 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 the props — the natural controlled\n // pattern — must not trigger a HOST_LOAD reload that wipes the\n // editor's undo history behind a skeleton flash. TWO keys are\n // recorded because hosts echo different subsets: the prop-based key\n // covers \"content only, other props untouched\", and the detail-based\n // key covers \"the whole save detail\" — whose name and templateQuery\n // (a save nearly always changes the query) differ from the props the\n // template was loaded with.\n this._sentKey = this._contentKey(data.content)\n this._sentSaveKey = this._detailKey(data)\n if (this.damStore) {\n // Serialized, not fired in parallel: the app discards a failure ack\n // that has newer saves still outstanding as superseded, which is\n // only sound while acks come back in post order — and ack order\n // follows save-event order. Two racing uploads could swap it.\n this._pendingSaves.add(data)\n // Epoch, folder and credentials are captured now, not when the\n // queued upload runs: the save belongs to the document — and the\n // configuration — the app posted it under, and the host may have\n // swapped both by the time the queue reaches it. Only the\n // known-uuid seed stays live (validated against the epoch), because\n // an earlier queued save of the SAME document must be able to hand\n // its uuid to the next one.\n const session = this._docEpoch\n const job = {\n auth: {\n token: this.token,\n sassKey: this.sassKey,\n secTemplate: this.secTemplate,\n sessionUuid: this.sessionUuid,\n companyUuid: this.companyUuid,\n projectUuid: this.projectUuid,\n },\n storeFolder: this.storeFolder || '/',\n }\n this._storeQueue = this._storeQueue.then(() =>\n this._storeAndEmitSave(data, session, job),\n )\n } else {\n this._emit('save', data)\n }\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 // Pending `dam-store` uploads are deliberately NOT flushed here: the\n // element outlives a close (inline keeps rendering, modal just drops\n // its overlay), so a save still inside its upload window emits with\n // its real outcome moments later — flushing would hand the host a\n // `storeError` for a copy that lands fine. The paths where waiting\n // genuinely loses the event — element removal — are covered by\n // `disconnectedCallback` and the React wrapper's cleanup flush.\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 * Pending `dam-store` uploads, chained so saves emit in the order the app\n * posted them. `_storeAndEmitSave` never rejects (its catch emits\n * `storeError`), so the chain cannot wedge.\n */\n private _storeQueue: Promise<void> = Promise.resolve()\n\n /**\n * Saves handed over by the app whose `save` event has not fired yet — the\n * upload window. Insertion-ordered; membership is what makes a flush and a\n * completing upload not double-emit the same save.\n */\n private _pendingSaves = new Set<BuilderContentData>()\n\n /**\n * Identity (id, content, name — not query) of the last document posted to\n * the app — updated on every ship, and on an accepted save echo (the echoed\n * document IS the current one; leaving the pre-save key here would make a\n * later content-request redelivery look like a new document and wipe the\n * store memory below).\n */\n private _lastDocKey?: string\n\n /**\n * Monotonic id of the current DOCUMENT. Bumped in exactly one place: when\n * `_maybeSendContent` ships a genuinely different document (docKey change).\n * Deliberately NOT bumped on iframe/src changes — a modal close, a theme\n * swap or an in-place remount is the same document, and treating it as new\n * forked the file on every such boundary.\n */\n private _docEpoch = 0\n\n /**\n * The copy this element last stored, tagged with the epoch of the document\n * it belongs to. Written when an upload lands (with the SAVE's epoch, so a\n * late-landing upload can never masquerade as another document's copy) and\n * validated at read time — there is no eager reset to get wrong.\n */\n private _storeMemory?: { epoch: number; uuid: string }\n\n /**\n * Emit every not-yet-emitted `dam-store` save immediately, raw content with\n * `storeError` in place of the links. Called when waiting any longer risks\n * the event finding no listener; a still-running upload for a flushed save\n * is left to finish (the copy usually lands) but will not emit again.\n *\n * Public for framework wrappers: one that unsubscribes its listeners before\n * unmounting must call this first, while they are still attached — the\n * element's own disconnect-time flush fires only after the wrapper has\n * stopped listening, and the raw save would be lost. The React wrapper does\n * this; a vanilla host never needs to call it.\n */\n flushPendingSaves(\n reason = 'widget removed before the rendering copy completed',\n ): void {\n this._flushPendingSaves(reason)\n }\n\n private _flushPendingSaves(reason: string): void {\n for (const data of this._pendingSaves) {\n this._pendingSaves.delete(data)\n this._emit('save', { ...data, storeError: reason })\n }\n }\n\n /**\n * The `dam-store` save path: upload the edited template to Filerobot, then\n * emit `save` with the stored copy's links on the detail. The upload is the\n * render side of the save — the CDN renders only stored files — while the\n * raw `content` stays the host's copy exactly as without the flag.\n *\n * A failed upload still emits `save` (the raw data must reach the host\n * either way), with `storeError` in place of `stored`; whether a save\n * without a rendering copy counts as saved is the host's decision, made\n * where it always is — the save ack.\n */\n private async _storeAndEmitSave(\n data: BuilderContentData,\n session: number,\n job: { auth: DamStoreAuth; storeFolder: string },\n ): Promise<void> {\n if (!this._pendingSaves.has(data)) return\n // The seeds are validated against the save's own document epoch. The\n // element's memory carries the epoch it was recorded under; the host's\n // `storedUuid` property is live and describes the CURRENT document, so it\n // only applies while the save's epoch is still the current one — anchoring\n // a stale save's upload to it would put the old document into the new\n // one's file. Folder and credentials come from the job captured at\n // enqueue time, for the same reason.\n const knownUuid =\n (this._storeMemory?.epoch === session\n ? this._storeMemory.uuid\n : undefined) ||\n (session === this._docEpoch ? this.storedUuid || undefined : undefined)\n let stored: StoredTemplate | undefined\n let storeError: string | undefined\n try {\n stored = await storeTemplateInDam(data, job.auth, job.storeFolder, knownUuid)\n } catch (err) {\n storeError = err instanceof Error ? err.message : String(err)\n }\n // A flush mid-upload already delivered this save; the copy (if it landed)\n // is simply not reported. Emitting again would double the host's write —\n // and recording the uuid would poison the session memory of whatever\n // document has loaded since (the flush usually precedes a swap), pointing\n // its next save at this document's file.\n if (!this._pendingSaves.delete(data)) return\n // Recorded under the SAVE's epoch, unconditionally: a copy always belongs\n // to the document it was saved from. A later document's saves carry a\n // higher epoch and simply never match this record — no current-vs-then\n // comparison to get wrong. (Uploads complete in queue order, so the last\n // write is always the newest save.)\n if (stored) this._storeMemory = { epoch: session, uuid: stored.uuid }\n this._emit('save', stored ? { ...data, stored } : { ...data, storeError })\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) return\n\n // Empty `content` means the host has nothing to give yet — usually a fetch\n // in flight — so the editor keeps waiting. Only `new-template` turns that\n // into a document, and only until the host does supply one.\n const content =\n this.content || (this.newTemplate ? BLANK_TEMPLATE_XML : '')\n if (!content) return\n\n const data = {\n templateId: this.templateId || undefined,\n content,\n name: this.templateName || undefined,\n templateQuery: this.templateQuery || undefined,\n }\n const key = this._contentKey(content)\n // The document's identity — id, content, name; NOT the query. A\n // query-only resend (\"same XML, different render\") and a content-request\n // redelivery after an in-place remount are the same document, and\n // treating them as new would orphan the stored copy.\n const docKey = JSON.stringify({\n templateId: this.templateId || undefined,\n content,\n name: this.templateName || undefined,\n })\n // `_sentSaveKey` counts as delivered too: matching it means the host has\n // echoed the last save back into the props, and the editor already holds\n // exactly that document — reloading would wipe its undo history. The doc\n // key is still refreshed: the echoed document IS the current one, and\n // leaving the pre-save key in place would make the next redelivery of\n // this same content look like a document change and orphan the copy.\n if (key === this._sentKey || key === this._sentSaveKey) {\n this._lastDocKey = docKey\n return\n }\n\n if (!this._postToApp({ type: HOST_LOAD, data })) return\n // A genuinely different document starts a new epoch — the file the\n // previous one made must not resolve this one's folder or unchanged\n // re-saves.\n if (docKey !== this._lastDocKey) this._docEpoch++\n this._lastDocKey = docKey\n this._sentKey = key\n // A new document shipped: the previous save's echo key no longer names\n // what the editor holds, and matching it later would wrongly skip a load.\n this._sentSaveKey = undefined\n }\n\n /**\n * Deliver host config to the app. Unlike content this is not requested — the\n * app has no way to know a host means to send any — so it goes out on the\n * ready signal and on every later change.\n *\n * Sending an empty model is meaningful: it is how a host clears one it set\n * before. What is skipped is only a *repeat* of what the app already holds,\n * and the very first send when there was never anything to say.\n */\n private _maybeSendConfig(): void {\n // Hosts are plain JS as often as not, and the property has no converter to\n // vet what lands on it the way the attribute does. Anything that is not a\n // list of fields is treated as no model rather than posted onward as\n // something the app would have to make sense of.\n const customMetadata = Array.isArray(this.customMetadata)\n ? this.customMetadata\n : []\n const customMetadataLabel =\n typeof this.customMetadataLabel === 'string'\n ? this.customMetadataLabel.trim()\n : ''\n const key = JSON.stringify({ customMetadata, customMetadataLabel })\n if (key === this._sentConfigKey) return\n if (\n this._sentConfigKey === undefined &&\n customMetadata.length === 0 &&\n customMetadataLabel === ''\n ) {\n return\n }\n\n if (\n !this._postToApp({\n type: HOST_CONFIG,\n data: {\n customMetadata,\n ...(customMetadataLabel ? { customMetadataLabel } : {}),\n },\n })\n ) {\n return\n }\n this._sentConfigKey = 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 /**\n * Identity of a save the app handed back — the same shape as\n * `_contentKey`, but built from the save detail rather than the props, so\n * a host echoing the detail (whose name/query the save changed) matches.\n */\n private _detailKey(data: BuilderContentData): string {\n return JSON.stringify({\n templateId: data.templateId || undefined,\n content: data.content,\n name: data.name || undefined,\n templateQuery: data.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","BLANK_TEMPLATE_XML","HOST_CONFIG","HOST_SAVED","EMBED_PARAMS","AUTH_MODES","BRAND_COLOR_PATTERN","builderRoute","templateId","EMBED_ROUTE","FILEROBOT_API","resolveKey","auth","res","body","apiHeaders","key","headers","looksLikeDamFileUuid","id","getFileRecord","uuid","err","message","findByName","fileName","folder","url","f","hashId","h","i","folderFromPath","rawPath","path","segments","stripBoundValues","templateQuery","content","bound","doc","slugs","el","part","uploadSeq","UNCHANGED_UPLOAD_CODES","storeTemplateInDam","data","fallbackFolder","knownUuid","wantFromId","wantOwnCopy","fromRes","ownRes","fromId","ownCopy","anchor","display","storedQuery","parsed","code","resolved","record","_SfxTemplateBuilder","LitElement","event","iframe","msg","session","job","name","storedUuid","ok","changed","src","frame","html","nothing","spinner","route","base","reason","stored","storeError","docKey","customMetadata","customMetadataLabel","target","appOrigin","detail","css","SfxTemplateBuilder","__decorateClass","property","value","state"],"mappings":"mEAWaA,EAAmB,EAKnBC,EAAgB,iCAEhBC,EAAe,gCAEfC,EAAgB,iCAEhBC,EAAe,gCAEfC,EAAgB,iCAMhBC,EAA0B,2CAQ1BC,EAAkB,mCAQlBC,EAAgB,iCAoHhBC,EAAY,6BA0DZC,EACX;AAAA,4GAgBWC,EAAc,+BA4DdC,EAAa,8BAsBbC,EAAe,CAC1B,aAAc,QACd,aAAc,QACd,aAAc,QACd,SAAU,UACV,gBAAiB,SAMjB,aAAc,cACd,OAAQ,SAER,aAAc,cAMd,YAAa,aAEb,MAAO,OACT,EAmBaC,EAAa,CACxB,QAAS,UACT,aAAc,aAChB,EASaC,EAAsB,uCAG5B,SAASC,EAAaC,EAA6B,CACxD,OAAOA,EACH,cAAc,mBAAmBA,CAAU,CAAC,QAC5C,gBACN,CAOO,MAAMC,EAAc,mBCnXdC,EAAgB,4BAwC7B,eAAsBC,EAAWC,EAAqC,CACpE,GAAI,CAACA,EAAK,YAAa,OAAOA,EAAK,SAAW,GAC9C,MAAMC,EAAM,MAAM,MAChB,GAAGH,CAAa,IAAI,mBAAmBE,EAAK,KAAK,CAAC,WAAW,mBAAmBA,EAAK,WAAW,CAAC,GACjG,CAAE,QAAS,CAAE,kBAAmBA,EAAK,YAAY,CAAE,EAE/CE,EAAQ,MAAMD,EAAI,KAAA,EAAO,MAAM,KAAO,CAAA,EAAG,EAC/C,GAAI,CAACA,EAAI,IAAM,CAACC,EAAK,IACnB,MAAM,IAAI,MAAMA,EAAK,KAAO,wBAAwBD,EAAI,MAAM,EAAE,EAElE,OAAOC,EAAK,GACd,CAMO,SAASC,EAAWH,EAAoBI,EAAqC,CAClF,MAAMC,EAAkC,CAAE,kBAAmBD,CAAA,EAC7D,OAAKJ,EAAK,cACJA,EAAK,cAAaK,EAAQ,iBAAiB,EAAIL,EAAK,aACpDA,EAAK,cAAaK,EAAQ,iBAAiB,EAAIL,EAAK,aACpDA,EAAK,cAAaK,EAAQ,iBAAiB,EAAIL,EAAK,cAEnDK,CACT,CAQO,SAASC,EAAqBC,EAAqB,CACxD,MAAO,4BAA4B,KAAKA,CAAE,CAC5C,CAUA,eAAsBC,EACpBR,EACAI,EACAK,EAC4B,CAC5B,IAAIR,EACJ,GAAI,CACFA,EAAM,MAAM,MACV,GAAGH,CAAa,IAAI,mBAAmBE,EAAK,KAAK,CAAC,aAAa,mBAAmBS,CAAI,CAAC,GACvF,CAAE,QAASN,EAAWH,EAAMI,CAAG,CAAA,CAAE,CAErC,OAASM,EAAK,CACZ,MAAM,IAAI,MACR,uBAAuBA,aAAe,MAAQA,EAAI,QAAU,OAAOA,CAAG,CAAC,EAAA,CAE3E,CACA,MAAMR,EAAQ,MAAMD,EAAI,KAAA,EAAO,MAAM,KAAO,CAAA,EAAG,EAO/C,GAAI,CAAC,IAAK,IAAK,IAAK,GAAG,EAAE,SAASA,EAAI,MAAM,EAAG,OAAO,KACtD,MAAMU,EAAUT,EAAK,KAAOA,EAAK,SAAW,GAC5C,GAAI,CAACD,EAAI,IAAMC,EAAK,SAAW,QAAS,CACtC,GAAI,8CAA8C,KAAKS,CAAO,EAAG,OAAO,KACxE,MAAM,IAAI,MAAMA,GAAW,uBAAuBV,EAAI,MAAM,EAAE,CAChE,CACA,OAAOC,EAAK,MAAQ,IACtB,CAMA,eAAeU,EACbZ,EACAI,EACAS,EACAC,EAC4B,CAC5B,MAAMC,EAAM,IAAI,IACd,GAAGjB,CAAa,IAAI,mBAAmBE,EAAK,KAAK,CAAC,WAAA,EAEpDe,EAAI,aAAa,IAAI,IAAK,SAASF,CAAQ,GAAG,EAC9CE,EAAI,aAAa,IAAI,SAAUD,CAAM,EACrCC,EAAI,aAAa,IAAI,OAAQ,kBAAkB,EAC/CA,EAAI,aAAa,IAAI,QAAS,GAAG,EACjC,MAAMd,EAAM,MAAM,MAAMc,EAAK,CAAE,QAASZ,EAAWH,EAAMI,CAAG,EAAG,EACzDF,EAAQ,MAAMD,EAAI,KAAA,EAAO,MAAM,KAAO,CAAA,EAAG,EAI/C,MAAI,CAACA,EAAI,IAAMC,EAAK,SAAW,QAAgB,KACxCA,EAAK,OAAO,KAAMc,GAAMA,EAAE,OAASH,CAAQ,GAAK,IACzD,CAMA,SAASI,EAAOV,EAAoB,CAClC,IAAIW,EAAI,WACR,QAASC,EAAI,EAAGA,EAAIZ,EAAG,OAAQY,IAC7BD,GAAKX,EAAG,WAAWY,CAAC,EACpBD,EAAI,KAAK,KAAKA,EAAG,QAAU,EAE7B,OAAQA,IAAM,GAAG,SAAS,EAAE,CAC9B,CAGA,SAASE,EAAeC,EAAyB,CAC/C,IAAIC,EACJ,GAAI,CACFA,EAAO,mBAAmBD,CAAO,CACnC,MAAQ,CACNC,EAAOD,CACT,CACA,MAAME,EAAWD,EAAK,MAAM,GAAG,EAAE,OAAO,OAAO,EAC/C,OAAAC,EAAS,IAAA,EACF,IAAMA,EAAS,KAAK,GAAG,CAChC,CAUA,SAASC,GAAiBC,EAAuBC,EAAyB,CACxE,GAAI,CAACD,EAAe,OAAOA,EAC3B,IAAIE,EACJ,GAAI,CACF,MAAMC,EAAM,IAAI,UAAA,EAAY,gBAAgBF,EAAS,iBAAiB,EACtE,GAAIE,EAAI,cAAc,aAAa,EAAG,OAAOH,EAC7CE,EAAQC,EAAI,iBACV,8DAAA,CAEJ,MAAQ,CACN,OAAOH,CACT,CACA,GAAIE,EAAM,SAAW,EAAG,OAAOF,EAC/B,MAAMI,EAAQ,IAAI,IAAI,CAAC,GAAGF,CAAK,EAAE,IAAKG,GAAOA,EAAG,aAAa,MAAM,GAAK,EAAE,CAAC,EAC3E,OAAOL,EACJ,MAAM,GAAG,EACT,OAAO,OAAO,EACd,OAAQM,GAAS,CAACF,EAAM,IAAIE,EAAK,MAAM,GAAG,EAAE,CAAC,EAAE,QAAQ,YAAa,EAAE,CAAC,CAAC,EACxE,KAAK,GAAG,CACb,CAQA,IAAIC,GAAY,EAEhB,MAAMC,OAA6B,IAAI,CACrC,sBACA,+BACF,CAAC,EA2BD,eAAsBC,GACpBC,EACAnC,EACAoC,EACAC,EACyB,CACzB,GAAI,CAACrC,EAAK,MAAO,MAAM,IAAI,MAAM,yBAAyB,EAC1D,MAAMI,EAAM,MAAML,EAAWC,CAAI,EACjC,GAAI,CAACI,EAAK,MAAM,IAAI,MAAM,mDAAmD,EAS7E,MAAMkC,EAAa,CAAC,CAACH,EAAK,YAAc7B,EAAqB6B,EAAK,UAAU,EACtEI,EAAc,CAAC,CAACF,GAAaA,IAAcF,EAAK,WAOhD,CAACK,EAASC,CAAM,EAAI,MAAM,QAAQ,WAAW,CACjDH,EACI9B,EAAcR,EAAMI,EAAK+B,EAAK,UAAoB,EAClD,QAAQ,QAA2B,IAAI,EAC3CI,EACI/B,EAAcR,EAAMI,EAAKiC,CAAmB,EAC5C,QAAQ,QAA2B,IAAI,CAAA,CAC5C,EACD,GAAIG,EAAQ,SAAW,WAAY,MAAMA,EAAQ,OACjD,MAAME,EAASF,EAAQ,MACvB,IAAIG,EAA6B,KACjC,GAAIF,EAAO,SAAW,YAAaE,EAAUF,EAAO,cAC3C,CAACC,EAAQ,MAAMD,EAAO,OAE/B,MAAMG,EAASF,GAAUC,EACnB7B,EAAS8B,GAAQ,KAAK,KACxBxB,EAAewB,EAAO,IAAI,IAAI,EAC9BR,GAAkB,IAWhBS,GAAWV,EAAK,MAAQ,IAAI,OAAO,QAAQ,UAAW,EAAE,GAAK,WAC7DtB,EAAW+B,GAAQ,KACrBA,EAAO,KACPT,EAAK,WACH,GAAGU,CAAO,IAAI5B,EAAOkB,EAAK,UAAU,CAAC,OACrC,GAAGU,CAAO,IAAI,KAAK,MAAM,SAAS,EAAE,CAAC,IAAI,EAAEb,EAAS,OAEpD9B,EAAO,IAAI,SAIjBA,EAAK,OACH,UACA,IAAI,KAAK,CAACiC,EAAK,OAAO,EAAG,CAAE,KAAM,uBAAwB,EACzDtB,CAAA,EAEF,MAAMiC,EAActB,GAAiBW,EAAK,eAAiB,GAAIA,EAAK,OAAO,EACvEW,GACF5C,EAAK,OACH,gBACA,KAAK,UAAU,CAAE,OAAQ,CAAE,eAAgB4C,CAAA,EAAe,CAAA,EAI9D,MAAM7C,EAAM,MAAM,MAChB,GAAGH,CAAa,IAAI,mBAAmBE,EAAK,KAAK,CAAC,oBAAoB,mBAAmBc,CAAM,CAAC,GAChG,CACE,OAAQ,OACR,QAAS,CAAE,GAAGX,EAAWH,EAAMI,CAAG,EAAG,uBAAwB,MAAA,EAC7D,KAAAF,CAAA,CACF,EAEI6C,EAAU,MAAM9C,EAAI,KAAA,EAAO,MAAM,KAAO,CAAA,EAAG,EAE3C+C,EAAOD,EAAO,MAAQA,EAAO,OAAO,SAAS,CAAC,GAAG,KACvD,GAAIC,GAAQf,GAAuB,IAAIe,CAAI,EAAG,CAC5C,IAAIC,EAAWN,GAAWD,EAQ1B,GAPKO,GAAU,OAKbA,EAAW,MAAMrC,EAAWZ,EAAMI,EAAKS,EAAUC,CAAM,EAAE,MAAM,IAAM,IAAI,GAEvEmC,GAAU,KACZ,MAAO,CACL,KAAMA,EAAS,KACf,IAAKA,EAAS,KAAK,KAAOA,EAAS,KAAK,QAAU,EAAA,EAKtD,MAAM,IAAI,MACR,0FAA0FD,CAAI,6FAAA,CAGlG,CACA,GAAI,CAAC/C,EAAI,IAAM8C,EAAO,SAAW,QAC/B,MAAM,IAAI,MAAMA,EAAO,KAAOA,EAAO,SAAW,kBAAkB9C,EAAI,MAAM,EAAE,EAGhF,MAAMQ,EAAOsC,EAAO,MAAM,MAAQA,EAAO,OAAO,WAAW,CAAC,GAAG,KAC/D,GAAI,CAACtC,EAAM,MAAM,IAAI,MAAM,mDAAmD,EAM9E,IAAIyC,EAA4B,KAChC,GAAI,CACFA,EAAS,MAAM1C,EAAcR,EAAMI,EAAKK,CAAI,CAC9C,MAAQ,CAER,CACA,MAAO,CACL,KAAAA,EACA,IACEyC,GAAQ,KAAK,KACbA,GAAQ,KAAK,QACbH,EAAO,MAAM,KAAK,KAClB,EAAA,CAEN,sICnSO,MAAMI,EAAN,MAAMA,UAA2BC,EAAAA,UAAW,CAA5C,aAAA,CAAA,MAAA,GAAA,SAAA,EA+CgC,KAAA,QAAU,GAEnC,KAAA,MAAQ,GACiB,KAAA,QAAU,GACN,KAAA,YAAc,GAQd,KAAA,YAAc,GACd,KAAA,YAAc,GACd,KAAA,YAAc,GAKf,KAAA,WAAa,GACxB,KAAA,KAA2B,SAMZ,KAAA,UAAY,GAMxB,KAAA,QAAU,GAec,KAAA,YAAc,GAE5B,KAAA,aAAe,GAOd,KAAA,cAAgB,GAOnB,KAAA,WAAa,GAEzC,KAAA,MAA2B,GA0CvC,KAAA,eAAwC,CAAA,EAOU,KAAA,oBAAsB,GASnB,KAAA,SAAW,GAMvB,KAAA,YAAc,IAWf,KAAA,WAAa,GAEG,KAAA,aAAe,IAE9D,KAAQ,QAAiC,OACzC,KAAQ,MAAQ,GAChB,KAAQ,KAAO,GAOxB,KAAQ,kBAAoB,GAoC5B,KAAQ,2BAA6B,GAE5B,KAAQ,SAAW,GAmH5B,KAAQ,iBAAmB,GA0M3B,KAAQ,WAAcC,GAA8B,CAElD,GADI,CAAC,KAAK,OACN,CAACA,EAAM,QAAUA,EAAM,SAAW,KAAK,WAAY,OACvD,MAAMC,EAAS,KAAK,YAAY,cAAc,QAAQ,EAKtD,GAJI,CAACA,GAIDD,EAAM,SAAWC,EAAO,cAAe,OAE3C,MAAMC,EAAMF,EAAM,KAClB,GAAI,GAACE,GAAO,OAAOA,EAAI,MAAS,UAEhC,OAAQA,EAAI,KAAA,CACV,KAAK3E,EACL,KAAKC,EACH,KAAK,qBAAA,EACD,KAAK,UAAY,UACnB,KAAK,QAAU,QACf,KAAK,MAAM,OAAO,GAWhB0E,EAAI,OAAS3E,IAAe,KAAK,eAAiB,QACtD,KAAK,iBAAA,EACD2E,EAAI,OAAS1E,GAAc,KAAK,MAAM,MAAM,EAChD,MACF,KAAKE,EACH,KAAK,MAAM,OAASwE,EAA2B,IAAI,EACnD,MACF,KAAKtE,EACH,KAAK,kBAAoB,GAKzB,KAAK,SAAW,OAChB,KAAK,aAAe,OACpB,KAAK,kBAAA,EACL,MACF,KAAKC,EAAiB,CAGpB,MAAMiD,EAAQoB,EAA8B,KAY5C,GAFA,KAAK,SAAW,KAAK,YAAYpB,EAAK,OAAO,EAC7C,KAAK,aAAe,KAAK,WAAWA,CAAI,EACpC,KAAK,SAAU,CAKjB,KAAK,cAAc,IAAIA,CAAI,EAQ3B,MAAMqB,EAAU,KAAK,UACfC,EAAM,CACV,KAAM,CACJ,MAAO,KAAK,MACZ,QAAS,KAAK,QACd,YAAa,KAAK,YAClB,YAAa,KAAK,YAClB,YAAa,KAAK,YAClB,YAAa,KAAK,WAAA,EAEpB,YAAa,KAAK,aAAe,GAAA,EAEnC,KAAK,YAAc,KAAK,YAAY,KAAK,IACvC,KAAK,kBAAkBtB,EAAMqB,EAASC,CAAG,CAAA,CAE7C,MACE,KAAK,MAAM,OAAQtB,CAAI,EAEzB,KACF,CACA,KAAKhD,EAAe,CAClB,MAAMgD,EAAQoB,EAA4B,KAC1C,KAAK,SAAW,CAAC,CAACpB,GAAM,QACxB,KAAK,MAAM,cAAe,CAAE,QAAS,KAAK,SAAU,EACpD,KACF,CACA,KAAKrD,EAQH,KAAK,MAAM,OAAO,EACd,KAAK,OAAS,UAAS,KAAK,MAAQ,IACxC,MACF,KAAKE,EACH,KAAK,MAAOuE,EAA4B,MAAQ,CAAE,KAAM,UAAW,EACnE,KAAA,CAEN,EAOA,KAAQ,YAA6B,QAAQ,QAAA,EAO7C,KAAQ,kBAAoB,IAkB5B,KAAQ,UAAY,CAAA,CA/cpB,IAAI,QAAgC,CAClC,OAAO,KAAK,OACd,CAOA,IAAI,SAAmB,CACrB,OAAO,KAAK,QACd,CAGA,KAAK3D,EAA2B,CAC1BA,IAAe,SAAW,KAAK,WAAaA,GAChD,KAAK,MAAQ,EACf,CAGA,OAAc,CACZ,KAAK,MAAQ,EACf,CAMA,KAAK,CACH,QAAA8B,EACA,WAAA9B,EACA,KAAA8D,EACA,cAAAjC,EACA,WAAAkC,CAAA,EAOO,CACH/D,IAAe,SAAW,KAAK,WAAaA,GAC5C8D,IAAS,SAAW,KAAK,aAAeA,GAIxCjC,IAAkB,SAAW,KAAK,cAAgBA,GAItD,KAAK,WAAakC,GAAc,GAGhC,KAAK,YAAc,GACnB,KAAK,QAAUjC,EACf,KAAK,MAAQ,EACf,CAeA,UAAU,CACR,WAAA9B,EACA,KAAA8D,CAAA,EAC0C,GAAU,CAChD9D,IAAe,SAAW,KAAK,WAAaA,GAC5C8D,IAAS,SAAW,KAAK,aAAeA,GAM5C,KAAK,cAAgB,GACrB,KAAK,WAAa,GAClB,KAAK,YAAc,GACnB,KAAK,QAAU,GACf,KAAK,MAAQ,EACf,CAaA,YAAYE,EAAajD,EAAwB,CAC1C,KAAK,WACV,KAAK,WAAW,CAAE,KAAMpB,EAAY,KAAM,CAAE,GAAAqE,EAAI,QAAAjD,CAAA,EAAW,CAC7D,CAYA,mBAA0B,CACxB,MAAM,kBAAA,EACN,OAAO,iBAAiB,UAAW,KAAK,UAAU,CACpD,CAEA,sBAA6B,CAC3B,MAAM,qBAAA,EACN,OAAO,oBAAoB,UAAW,KAAK,UAAU,EACrD,KAAK,qBAAA,EAWL,WAAW,IAAM,CACV,KAAK,aACR,KAAK,mBAAmB,oDAAoD,CAEhF,EAAG,CAAC,CACN,CAEU,WAAWkD,EAA+B,CAClD,MAAM,WAAWA,CAAO,EACnB,KAAK,mBACR,KAAK,iBAAmB,GACpB,KAAK,OAAS,WAAU,KAAK,MAAQ,KAE3C,MAAMC,EAAM,KAAK,YAAA,EACbA,IAAQ,KAAK,OACf,KAAK,KAAOA,EACZ,KAAK,QAAUA,EAAM,UAAY,OAErC,CAEU,QAAQD,EAA+B,CAC3CA,EAAQ,IAAI,MAAM,IACpB,KAAK,qBAAA,EAOL,KAAK,kBAAoB,GACzB,KAAK,SAAW,OAChB,KAAK,aAAe,OACpB,KAAK,eAAiB,OAClB,KAAK,WACP,KAAK,SAAW,GAChB,KAAK,MAAM,cAAe,CAAE,QAAS,GAAO,GAE1C,KAAK,MAAM,KAAK,qBAAA,IAMpBA,EAAQ,IAAI,SAAS,GACrBA,EAAQ,IAAI,aAAa,GACzBA,EAAQ,IAAI,YAAY,GACxBA,EAAQ,IAAI,cAAc,GAC1BA,EAAQ,IAAI,eAAe,IAE3B,KAAK,kBAAA,GAOJA,EAAQ,IAAI,gBAAgB,GAAKA,EAAQ,IAAI,qBAAqB,IACnE,KAAK,UAAY,SAEjB,KAAK,iBAAA,CAET,CAEA,QAAS,CACP,MAAME,EAAQ,KAAK,KACfC;;;gBAGQ,KAAK,IAAI;AAAA;AAAA,oBAGjBC,EAAAA,QACEC,EACJ,KAAK,UAAY,UACbF,EAAAA,iDACAC,EAAAA,QAEN,OAAI,KAAK,OAAS,QACT,KAAK,MACRD;iCACuBD,CAAK,GAAGG,CAAO;AAAA,kBAEtCD,EAAAA,QAECD,SAAOD,CAAK,GAAGG,CAAO,EAC/B,CAEQ,aAAsB,CAE5B,GADI,CAAC,KAAK,OACN,CAAC,KAAK,SAAW,CAAC,KAAK,MAAO,MAAO,GACzC,GAAI,KAAK,YAAa,CAKpB,GAAI,CAAC,KAAK,UACR,OAAK,KAAK,6BACR,KAAK,2BAA6B,GAClC,eAAe,IACb,KAAK,MAAM,CACT,KAAM,iBACN,QACE,+GAAA,CAEH,CAAA,GAGE,GAIT,KAAK,2BAA6B,EACpC,SAAW,CAAC,KAAK,SAAW,CAAC,KAAK,YAChC,MAAO,GAET,IAAInD,EACJ,GAAI,CAGF,MAAMoD,EAAQ,KAAK,UACftE,EACAF,EAAa,KAAK,YAAc,MAAS,EAKvCyE,EAAO,KAAK,QAAQ,SAAS,GAAG,EAAI,KAAK,QAAU,GAAG,KAAK,OAAO,IACxErD,EAAM,IAAI,IAAIoD,EAAM,QAAQ,MAAO,EAAE,EAAGC,CAAI,CAC9C,MAAQ,CAIN,OAAI,KAAK,sBAAwB,KAAK,UACpC,KAAK,oBAAsB,KAAK,QAGhC,eAAe,IACb,KAAK,MAAM,CACT,KAAM,mBACN,QAAS,gCAAgC,KAAK,OAAO,EAAA,CACtD,CAAA,GAGE,EACT,CACA,YAAK,oBAAsB,OAC3BrD,EAAI,aAAa,IAAIvB,EAAa,gBAAiB,KAAK,KAAK,EACzD,KAAK,YAIPuB,EAAI,aAAa,IAAIvB,EAAa,aAAc,KAAK,WAAW,GAEhEuB,EAAI,aAAa,IAAIvB,EAAa,SAAU,KAAK,OAAO,EACxDuB,EAAI,aAAa,IAAIvB,EAAa,aAAc,KAAK,WAAW,EAC5D,KAAK,aACPuB,EAAI,aAAa,IAAIvB,EAAa,aAAc,KAAK,WAAW,EAE9D,KAAK,aACPuB,EAAI,aAAa,IAAIvB,EAAa,aAAc,KAAK,WAAW,GAGhE,KAAK,YACPuB,EAAI,aAAa,IAAIvB,EAAa,YAAa,KAAK,UAAU,EAE5D,KAAK,OACPuB,EAAI,aAAa,IAAIvB,EAAa,MAAO,KAAK,KAAK,EAErDuB,EAAI,aAAa,IAAIvB,EAAa,OAAQ,GAAG,EAC7CuB,EAAI,aAAa,IAAIvB,EAAa,aAAc,OAAO,SAAS,MAAM,EAC/DuB,EAAI,SAAA,CACb,CAEA,IAAY,YAA4B,CACtC,GAAI,CACF,OAAO,IAAI,IAAI,KAAK,OAAO,EAAE,MAC/B,MAAQ,CACN,OAAO,IACT,CACF,CA4KA,kBACEsD,EAAS,qDACH,CACN,KAAK,mBAAmBA,CAAM,CAChC,CAEQ,mBAAmBA,EAAsB,CAC/C,UAAWlC,KAAQ,KAAK,cACtB,KAAK,cAAc,OAAOA,CAAI,EAC9B,KAAK,MAAM,OAAQ,CAAE,GAAGA,EAAM,WAAYkC,EAAQ,CAEtD,CAaA,MAAc,kBACZlC,EACAqB,EACAC,EACe,CACf,GAAI,CAAC,KAAK,cAAc,IAAItB,CAAI,EAAG,OAQnC,MAAME,GACH,KAAK,cAAc,QAAUmB,EAC1B,KAAK,aAAa,KAClB,SACHA,IAAY,KAAK,WAAY,KAAK,YAAc,OACnD,IAAIc,EACAC,EACJ,GAAI,CACFD,EAAS,MAAMpC,GAAmBC,EAAMsB,EAAI,KAAMA,EAAI,YAAapB,CAAS,CAC9E,OAAS3B,EAAK,CACZ6D,EAAa7D,aAAe,MAAQA,EAAI,QAAU,OAAOA,CAAG,CAC9D,CAMK,KAAK,cAAc,OAAOyB,CAAI,IAM/BmC,SAAa,aAAe,CAAE,MAAOd,EAAS,KAAMc,EAAO,IAAA,GAC/D,KAAK,MAAM,OAAQA,EAAS,CAAE,GAAGnC,EAAM,OAAAmC,GAAW,CAAE,GAAGnC,EAAM,WAAAoC,CAAA,CAAY,EAC3E,CAOQ,mBAA0B,CAChC,GAAI,CAAC,KAAK,WAAa,CAAC,KAAK,kBAAmB,OAKhD,MAAM7C,EACJ,KAAK,UAAY,KAAK,YAAcrC,EAAqB,IAC3D,GAAI,CAACqC,EAAS,OAEd,MAAMS,EAAO,CACX,WAAY,KAAK,YAAc,OAC/B,QAAAT,EACA,KAAM,KAAK,cAAgB,OAC3B,cAAe,KAAK,eAAiB,MAAA,EAEjCtB,EAAM,KAAK,YAAYsB,CAAO,EAK9B8C,EAAS,KAAK,UAAU,CAC5B,WAAY,KAAK,YAAc,OAC/B,QAAA9C,EACA,KAAM,KAAK,cAAgB,MAAA,CAC5B,EAOD,GAAItB,IAAQ,KAAK,UAAYA,IAAQ,KAAK,aAAc,CACtD,KAAK,YAAcoE,EACnB,MACF,CAEK,KAAK,WAAW,CAAE,KAAMpF,EAAW,KAAA+C,CAAA,CAAM,IAI1CqC,IAAW,KAAK,aAAa,KAAK,YACtC,KAAK,YAAcA,EACnB,KAAK,SAAWpE,EAGhB,KAAK,aAAe,OACtB,CAWQ,kBAAyB,CAK/B,MAAMqE,EAAiB,MAAM,QAAQ,KAAK,cAAc,EACpD,KAAK,eACL,CAAA,EACEC,EACJ,OAAO,KAAK,qBAAwB,SAChC,KAAK,oBAAoB,OACzB,GACAtE,EAAM,KAAK,UAAU,CAAE,eAAAqE,EAAgB,oBAAAC,EAAqB,EAC9DtE,IAAQ,KAAK,iBAEf,KAAK,iBAAmB,QACxBqE,EAAe,SAAW,GAC1BC,IAAwB,IAMvB,KAAK,WAAW,CACf,KAAMpF,EACN,KAAM,CACJ,eAAAmF,EACA,GAAIC,EAAsB,CAAE,oBAAAA,GAAwB,CAAA,CAAC,CACvD,CACD,IAIH,KAAK,eAAiBtE,GACxB,CAGQ,YAAYsB,EAAyB,CAC3C,OAAO,KAAK,UAAU,CACpB,WAAY,KAAK,YAAc,OAC/B,QAAAA,EACA,KAAM,KAAK,cAAgB,OAC3B,cAAe,KAAK,eAAiB,MAAA,CACtC,CACH,CAOQ,WAAWS,EAAkC,CACnD,OAAO,KAAK,UAAU,CACpB,WAAYA,EAAK,YAAc,OAC/B,QAASA,EAAK,QACd,KAAMA,EAAK,MAAQ,OACnB,cAAeA,EAAK,eAAiB,MAAA,CACtC,CACH,CAGQ,WAAWxB,EAA2B,CAC5C,MAAMgE,EAAS,KAAK,YAAY,cAAc,QAAQ,GAAG,cACnDC,EAAY,KAAK,WACvB,MAAI,CAACD,GAAU,CAACC,EAAkB,IAClCD,EAAO,YAAYhE,EAASiE,CAAS,EAC9B,GACT,CAEQ,sBAA6B,CAC/B,KAAK,cAAgB,IACzB,KAAK,gBAAkB,OAAO,WAAW,IAAM,CAC7C,KAAK,MAAM,CACT,KAAM,oBACN,QACE,wBAAwB,KAAK,OAAO,WAAW,KAAK,YAAY,oHAAA,CAGnE,CACH,EAAG,KAAK,YAAY,EACtB,CAEQ,sBAA6B,CAC/B,KAAK,kBAAoB,SAC3B,OAAO,aAAa,KAAK,eAAe,EACxC,KAAK,gBAAkB,OAE3B,CAEQ,MAAMzC,EAA8B,CAC1C,KAAK,qBAAA,EACL,KAAK,QAAU,QACf,KAAK,MAAM,QAASA,CAAI,CAC1B,CAEQ,MAASuB,EAAcmB,EAAkB,CAC/C,KAAK,cACH,IAAI,YAAYnB,EAAM,CAAE,OAAAmB,EAAQ,QAAS,GAAM,SAAU,EAAA,CAAM,CAAA,CAEnE,CACF,EAv7BE1B,EAAO,OAAS2B,EAAAA;AAAAA;AAAAA;AAAAA;AAAAA;AAAAA;AAAAA;AAAAA;AAAAA;AAAAA;AAAAA;AAAAA;AAAAA;AAAAA;AAAAA;AAAAA;AAAAA;AAAAA;AAAAA;AAAAA;AAAAA;AAAAA;AAAAA;AAAAA;AAAAA;AAAAA;AAAAA;AAAAA;AAAAA;AAAAA;AAAAA;AAAAA;AAAAA;AAAAA;AAAAA;AAAAA;AAAAA;AAAAA;AAAAA;AAAAA;AAAAA;AAAAA;AAAAA;AAAAA,IADX,IAAMC,EAAN5B,EA+CgC6B,EAAA,CAApCC,WAAS,CAAE,UAAW,UAAA,CAAY,CAAA,EA/CxBF,EA+C0B,UAAA,SAAA,EAEzBC,EAAA,CAAXC,EAAAA,SAAA,CAAS,EAjDCF,EAiDC,UAAA,OAAA,EACyBC,EAAA,CAApCC,WAAS,CAAE,UAAW,UAAA,CAAY,CAAA,EAlDxBF,EAkD0B,UAAA,SAAA,EACIC,EAAA,CAAxCC,WAAS,CAAE,UAAW,cAAA,CAAgB,CAAA,EAnD5BF,EAmD8B,UAAA,aAAA,EAQAC,EAAA,CAAxCC,WAAS,CAAE,UAAW,cAAA,CAAgB,CAAA,EA3D5BF,EA2D8B,UAAA,aAAA,EACAC,EAAA,CAAxCC,WAAS,CAAE,UAAW,cAAA,CAAgB,CAAA,EA5D5BF,EA4D8B,UAAA,aAAA,EACAC,EAAA,CAAxCC,WAAS,CAAE,UAAW,cAAA,CAAgB,CAAA,EA7D5BF,EA6D8B,UAAA,aAAA,EAKDC,EAAA,CAAvCC,WAAS,CAAE,UAAW,aAAA,CAAe,CAAA,EAlE3BF,EAkE6B,UAAA,YAAA,EACXC,EAAA,CAA5BC,WAAS,CAAE,QAAS,EAAA,CAAM,CAAA,EAnEhBF,EAmEkB,UAAA,MAAA,EAMeC,EAAA,CAA3CC,EAAAA,SAAS,CAAE,KAAM,QAAS,QAAS,GAAM,CAAA,EAzE/BF,EAyEiC,UAAA,WAAA,EAMZC,EAAA,CAA/BC,WAAS,CAAE,UAAW,EAAA,CAAO,CAAA,EA/EnBF,EA+EqB,UAAA,SAAA,EAewBC,EAAA,CAAvDC,EAAAA,SAAS,CAAE,KAAM,QAAS,UAAW,eAAgB,CAAA,EA9F3CF,EA8F6C,UAAA,aAAA,EAEdC,EAAA,CAAzCC,WAAS,CAAE,UAAW,eAAA,CAAiB,CAAA,EAhG7BF,EAgG+B,UAAA,cAAA,EAOCC,EAAA,CAA1CC,WAAS,CAAE,UAAW,gBAAA,CAAkB,CAAA,EAvG9BF,EAuGgC,UAAA,eAAA,EAOHC,EAAA,CAAvCC,WAAS,CAAE,UAAW,aAAA,CAAe,CAAA,EA9G3BF,EA8G6B,UAAA,YAAA,EAE5BC,EAAA,CAAXC,EAAAA,SAAA,CAAS,EAhHCF,EAgHC,UAAA,OAAA,EA0CZC,EAAA,CAhBCC,WAAS,CACR,UAAW,kBACX,UAAW,CACT,cAAgBC,GAAgD,CAC9D,GAAI,CAACA,EAAO,MAAO,CAAA,EACnB,GAAI,CACF,MAAMnC,EAAkB,KAAK,MAAMmC,CAAK,EACxC,OAAO,MAAM,QAAQnC,CAAM,EAAKA,EAAmC,CAAA,CACrE,MAAQ,CACN,eAAQ,KAAK,sEAAsE,EAC5E,CAAA,CACT,CACF,EACA,YAAcmC,GAAyC,KAAK,UAAUA,GAAS,CAAA,CAAE,CAAA,CACnF,CACD,CAAA,EAzJUH,EA0JX,UAAA,gBAAA,EAOkDC,EAAA,CAAjDC,WAAS,CAAE,UAAW,uBAAA,CAAyB,CAAA,EAjKrCF,EAiKuC,UAAA,qBAAA,EASGC,EAAA,CAApDC,EAAAA,SAAS,CAAE,KAAM,QAAS,UAAW,YAAa,CAAA,EA1KxCF,EA0K0C,UAAA,UAAA,EAMZC,EAAA,CAAxCC,WAAS,CAAE,UAAW,cAAA,CAAgB,CAAA,EAhL5BF,EAgL8B,UAAA,aAAA,EAWDC,EAAA,CAAvCC,WAAS,CAAE,UAAW,aAAA,CAAe,CAAA,EA3L3BF,EA2L6B,UAAA,YAAA,EAEgBC,EAAA,CAAvDC,EAAAA,SAAS,CAAE,KAAM,OAAQ,UAAW,gBAAiB,CAAA,EA7L3CF,EA6L6C,UAAA,cAAA,EAEvCC,EAAA,CAAhBG,EAAAA,MAAA,CAAM,EA/LIJ,EA+LM,UAAA,SAAA,EACAC,EAAA,CAAhBG,EAAAA,MAAA,CAAM,EAhMIJ,EAgMM,UAAA,OAAA,EACAC,EAAA,CAAhBG,EAAAA,MAAA,CAAM,EAjMIJ,EAiMM,UAAA,MAAA,EA6CAC,EAAA,CAAhBG,EAAAA,MAAA,CAAM,EA9OIJ,EA8OM,UAAA,UAAA"}
|