@scaleflex/template-builder 0.2.0 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,651 @@
1
+ import { LitElement as L, css as M, html as f, nothing as k } from "lit";
2
+ import { property as n, state as y } from "lit/decorators.js";
3
+ const de = 3, P = "design-templates:builder:ready", K = "design-templates:builder:open", $ = "design-templates:builder:close", B = "design-templates:builder:save", F = "design-templates:builder:error", x = "design-templates:builder:content-request", j = "design-templates:builder:content", q = "design-templates:builder:dirty", H = "design-templates:host:load", Q = `<?xml version="1.0" encoding="UTF-8"?>
4
+ <template><templateInfo><version>0.3</version></templateInfo><rootStack/><layouts/><variables/></template>`, J = "design-templates:host:config", X = "design-templates:host:saved", c = {
5
+ SESSION_UUID: "suuid",
6
+ COMPANY_UUID: "cuuid",
7
+ PROJECT_UUID: "puuid",
8
+ SASS_KEY: "sassKey",
9
+ FILEROBOT_TOKEN: "ftoken",
10
+ /**
11
+ * Filerobot security-template key, the alternative to a Hub session. The app
12
+ * exchanges it for a sass key itself and runs in a reduced mode — see
13
+ * `AUTH_MODES`. Sent *instead of* `sassKey` + `suuid`, never alongside them.
14
+ */
15
+ SEC_TEMPLATE: "secTemplate",
16
+ IFRAME: "iframe",
17
+ /** Origin of the embedding page; the app uses it as postMessage targetOrigin. */
18
+ EMBED_ORIGIN: "embedOrigin",
19
+ /**
20
+ * Accent colour for the editor chrome, as `#rgb` / `#rrggbb`. The app derives
21
+ * its whole accent ramp from it. Rejected server-side if it doesn't match
22
+ * that shape — it ends up inside a stylesheet.
23
+ */
24
+ BRAND_COLOR: "brandColor",
25
+ /** Colour scheme for the editor chrome: `light` | `dark` | `auto`. */
26
+ THEME: "theme"
27
+ }, le = {
28
+ SESSION: "session",
29
+ SEC_TEMPLATE: "secTemplate"
30
+ }, ce = /^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/;
31
+ function Y(s) {
32
+ return s ? `/templates/${encodeURIComponent(s)}/edit` : "/templates/new";
33
+ }
34
+ const G = "/templates/embed", _ = "https://api.filerobot.com";
35
+ async function V(s) {
36
+ if (!s.secTemplate) return s.sassKey ?? "";
37
+ const e = await fetch(
38
+ `${_}/${encodeURIComponent(s.token)}/v5/key/${encodeURIComponent(s.secTemplate)}`,
39
+ { headers: { "X-Filerobot-Key": s.secTemplate } }
40
+ ), t = await e.json().catch(() => ({}));
41
+ if (!e.ok || !t.key)
42
+ throw new Error(t.msg ?? `key exchange failed: ${e.status}`);
43
+ return t.key;
44
+ }
45
+ function R(s, e) {
46
+ const t = { "X-Filerobot-Key": e };
47
+ 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;
48
+ }
49
+ function W(s) {
50
+ return /^[0-9a-f][0-9a-f-]{18,}$/i.test(s);
51
+ }
52
+ async function I(s, e, t) {
53
+ let o;
54
+ try {
55
+ o = await fetch(
56
+ `${_}/${encodeURIComponent(s.token)}/v5/files/${encodeURIComponent(t)}`,
57
+ { headers: R(s, e) }
58
+ );
59
+ } catch (l) {
60
+ throw new Error(
61
+ `file lookup failed: ${l instanceof Error ? l.message : String(l)}`
62
+ );
63
+ }
64
+ const i = await o.json().catch(() => ({}));
65
+ if ([400, 404, 410, 422].includes(o.status)) return null;
66
+ const d = i.msg ?? i.message ?? "";
67
+ if (!o.ok || i.status === "error") {
68
+ if (/not[ _-]?found|does not exist|no such file/i.test(d)) return null;
69
+ throw new Error(d || `file lookup failed: ${o.status}`);
70
+ }
71
+ return i.file ?? null;
72
+ }
73
+ async function z(s, e, t, o) {
74
+ const i = new URL(
75
+ `${_}/${encodeURIComponent(s.token)}/v5/files`
76
+ );
77
+ i.searchParams.set("q", `name:"${t}"`), i.searchParams.set("folder", o), i.searchParams.set("sort", "modified_at:desc"), i.searchParams.set("limit", "5");
78
+ const d = await fetch(i, { headers: R(s, e) }), l = await d.json().catch(() => ({}));
79
+ return !d.ok || l.status === "error" ? null : l.files?.find((h) => h.name === t) ?? null;
80
+ }
81
+ function Z(s) {
82
+ let e = 2166136261;
83
+ for (let t = 0; t < s.length; t++)
84
+ e ^= s.charCodeAt(t), e = Math.imul(e, 16777619);
85
+ return (e >>> 0).toString(36);
86
+ }
87
+ function ee(s) {
88
+ let e;
89
+ try {
90
+ e = decodeURIComponent(s);
91
+ } catch {
92
+ e = s;
93
+ }
94
+ const t = e.split("/").filter(Boolean);
95
+ return t.pop(), "/" + t.join("/");
96
+ }
97
+ function te(s, e) {
98
+ if (!s) return s;
99
+ let t;
100
+ try {
101
+ const i = new DOMParser().parseFromString(e, "application/xml");
102
+ if (i.querySelector("parsererror")) return s;
103
+ t = i.querySelectorAll(
104
+ 'variable[custom_ckey][source="URL"][type="text_placeholder"]'
105
+ );
106
+ } catch {
107
+ return s;
108
+ }
109
+ if (t.length === 0) return s;
110
+ const o = new Set([...t].map((i) => i.getAttribute("name") ?? ""));
111
+ return s.split("&").filter(Boolean).filter((i) => !o.has(i.split("=")[0].replace(/^(\$|%24)/, ""))).join("&");
112
+ }
113
+ let se = 0;
114
+ const ie = /* @__PURE__ */ new Set([
115
+ "ERROR_SHA1_CONFLICT",
116
+ "SAME_ASSET_EXISTS_SKIP_UPLOAD"
117
+ ]);
118
+ async function oe(s, e, t, o) {
119
+ if (!e.token) throw new Error("dam-store needs a token");
120
+ const i = await V(e);
121
+ if (!i) throw new Error("dam-store needs a sass key or a security template");
122
+ const d = !!s.templateId && W(s.templateId), l = !!o && o !== s.templateId, [h, b] = await Promise.allSettled([
123
+ d ? I(e, i, s.templateId) : Promise.resolve(null),
124
+ l ? I(e, i, o) : Promise.resolve(null)
125
+ ]);
126
+ if (h.status === "rejected") throw h.reason;
127
+ const g = h.value;
128
+ let v = null;
129
+ if (b.status === "fulfilled") v = b.value;
130
+ else if (!g) throw b.reason;
131
+ const m = g ?? v, C = m?.url?.path ? ee(m.url.path) : t || "/", D = (s.name ?? "").trim().replace(/\.fdt$/i, "") || "template", A = m?.name ? m.name : s.templateId ? `${D}_${Z(s.templateId)}.fdt` : `${D}_${Date.now().toString(36)}-${++se}.fdt`, S = new FormData();
132
+ S.append(
133
+ "files[]",
134
+ new Blob([s.content], { type: "text/xml+sfxtemplate" }),
135
+ A
136
+ );
137
+ const N = te(s.templateQuery ?? "", s.content);
138
+ N && S.append(
139
+ "info[files[]]",
140
+ JSON.stringify({ custom: { template_query: N } })
141
+ );
142
+ const E = await fetch(
143
+ `${_}/${encodeURIComponent(e.token)}/v4/files?folder=${encodeURIComponent(C)}`,
144
+ {
145
+ method: "POST",
146
+ headers: { ...R(e, i), "X-Filerobot-Template": "true" },
147
+ body: S
148
+ }
149
+ ), p = await E.json().catch(() => ({})), U = p.code ?? p.files?.failed?.[0]?.code;
150
+ if (U && ie.has(U)) {
151
+ let u = v ?? g;
152
+ if (u?.uuid || (u = await z(e, i, A, C).catch(() => null)), u?.uuid)
153
+ return {
154
+ uuid: u.uuid,
155
+ url: u.url?.cdn ?? u.url?.public ?? ""
156
+ };
157
+ throw new Error(
158
+ `this project's deduplication policy already stores this exact content as another file (${U}) — edit the content, or pass that file's uuid as stored-uuid so the save can resolve to it`
159
+ );
160
+ }
161
+ if (!E.ok || p.status === "error")
162
+ throw new Error(p.msg ?? p.message ?? `upload failed: ${E.status}`);
163
+ const T = p.file?.uuid ?? p.files?.uploaded?.[0]?.uuid;
164
+ if (!T) throw new Error("upload succeeded but no file uuid in the response");
165
+ let w = null;
166
+ try {
167
+ w = await I(e, i, T);
168
+ } catch {
169
+ }
170
+ return {
171
+ uuid: T,
172
+ url: w?.url?.cdn ?? w?.url?.public ?? p.file?.url?.cdn ?? ""
173
+ };
174
+ }
175
+ var re = Object.defineProperty, a = (s, e, t, o) => {
176
+ for (var i = void 0, d = s.length - 1, l; d >= 0; d--)
177
+ (l = s[d]) && (i = l(e, t, i) || i);
178
+ return i && re(e, t, i), i;
179
+ };
180
+ const O = class O extends L {
181
+ constructor() {
182
+ 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) => {
183
+ if (!this._open || !e.origin || e.origin !== this._appOrigin) return;
184
+ const t = this.shadowRoot?.querySelector("iframe");
185
+ if (!t || e.source !== t.contentWindow) return;
186
+ const o = e.data;
187
+ if (!(!o || typeof o.type != "string"))
188
+ switch (o.type) {
189
+ case P:
190
+ case K:
191
+ this._clearHandshakeTimer(), this._status !== "ready" && (this._status = "ready", this._emit("ready")), o.type === P && (this._sentConfigKey = void 0), this._maybeSendConfig(), o.type === K && this._emit("open");
192
+ break;
193
+ case B:
194
+ this._emit("save", o.data);
195
+ break;
196
+ case x:
197
+ this._contentRequested = !0, this._sentKey = void 0, this._sentSaveKey = void 0, this._maybeSendContent();
198
+ break;
199
+ case j: {
200
+ const i = o.data;
201
+ if (this._sentKey = this._contentKey(i.content), this._sentSaveKey = this._detailKey(i), this.damStore) {
202
+ this._pendingSaves.add(i);
203
+ const d = this._docEpoch, l = {
204
+ auth: {
205
+ token: this.token,
206
+ sassKey: this.sassKey,
207
+ secTemplate: this.secTemplate,
208
+ sessionUuid: this.sessionUuid,
209
+ companyUuid: this.companyUuid,
210
+ projectUuid: this.projectUuid
211
+ },
212
+ storeFolder: this.storeFolder || "/"
213
+ };
214
+ this._storeQueue = this._storeQueue.then(
215
+ () => this._storeAndEmitSave(i, d, l)
216
+ );
217
+ } else
218
+ this._emit("save", i);
219
+ break;
220
+ }
221
+ case q: {
222
+ const i = o.data;
223
+ this._isDirty = !!i?.isDirty, this._emit("dirtychange", { isDirty: this._isDirty });
224
+ break;
225
+ }
226
+ case $:
227
+ this._emit("close"), this.mode === "modal" && (this._open = !1);
228
+ break;
229
+ case F:
230
+ this._fail(o.data ?? { code: "unknown" });
231
+ break;
232
+ }
233
+ }, this._storeQueue = Promise.resolve(), this._pendingSaves = /* @__PURE__ */ new Set(), this._docEpoch = 0;
234
+ }
235
+ get status() {
236
+ return this._status;
237
+ }
238
+ /**
239
+ * Stateless mode: whether the editor holds edits that have not been handed
240
+ * back yet. Check this before calling `load()` — a swap discards them.
241
+ * Always false in DAM-backed mode, where the app owns saving.
242
+ */
243
+ get isDirty() {
244
+ return this._isDirty;
245
+ }
246
+ /** Open the editor (loads the iframe). Optionally switch template first. */
247
+ open(e) {
248
+ e !== void 0 && (this.templateId = e), this._open = !0;
249
+ }
250
+ /** Close the editor and unload the iframe. Does not emit `close`. */
251
+ close() {
252
+ this._open = !1;
253
+ }
254
+ /**
255
+ * Stateless mode: load a template, opening the editor if needed. Equivalent
256
+ * to assigning `templateId` / `content` / `templateName` and calling `open()`.
257
+ */
258
+ load({
259
+ content: e,
260
+ templateId: t,
261
+ name: o,
262
+ templateQuery: i,
263
+ storedUuid: d
264
+ }) {
265
+ t !== void 0 && (this.templateId = t), o !== void 0 && (this.templateName = o), i !== void 0 && (this.templateQuery = i), this.storedUuid = d ?? "", this.newTemplate = !1, this.content = e, this._open = !0;
266
+ }
267
+ /**
268
+ * Stateless mode: open the editor on a new, empty template, opening it if
269
+ * needed. Equivalent to setting `new-template` and calling `open()`.
270
+ *
271
+ * The user chooses the canvas size from the editor's empty state; `save`
272
+ * then hands back a complete `.fdt` document — the first one you store for
273
+ * this record. Pass `templateId` if you have already allocated one; leave it
274
+ * out and the `save` payload simply comes back without an id.
275
+ *
276
+ * Calling it again on an editor that is already showing the blank document
277
+ * does nothing — resending would discard whatever the user has built since.
278
+ * To genuinely start over, `close()` and reopen.
279
+ */
280
+ createNew({
281
+ templateId: e,
282
+ name: t
283
+ } = {}) {
284
+ e !== void 0 && (this.templateId = e), t !== void 0 && (this.templateName = t), this.templateQuery = "", this.storedUuid = "", this.newTemplate = !0, this.content = "", this._open = !0;
285
+ }
286
+ /**
287
+ * Stateless mode: report back whether a `save` was persisted on your side.
288
+ *
289
+ * Optional. The editor clears its unsaved-changes flag as soon as it hands
290
+ * the content over, so not calling this leaves the previous behaviour intact.
291
+ * Calling it with `false` is what earns something: the editor restores the
292
+ * dirty flag and tells the user, rather than showing a failed write as saved.
293
+ *
294
+ * No-op outside stateless mode, where the app did the saving and has nothing
295
+ * to hear back about.
296
+ */
297
+ confirmSave(e, t) {
298
+ this.stateless && this._postToApp({ type: X, data: { ok: e, message: t } });
299
+ }
300
+ connectedCallback() {
301
+ super.connectedCallback(), window.addEventListener("message", this._onMessage);
302
+ }
303
+ disconnectedCallback() {
304
+ super.disconnectedCallback(), window.removeEventListener("message", this._onMessage), this._clearHandshakeTimer(), setTimeout(() => {
305
+ this.isConnected || this._flushPendingSaves("widget removed before the rendering copy completed");
306
+ }, 0);
307
+ }
308
+ willUpdate(e) {
309
+ super.willUpdate(e), this._autoOpenDecided || (this._autoOpenDecided = !0, this.mode === "inline" && (this._open = !0));
310
+ const t = this._computeSrc();
311
+ t !== this._src && (this._src = t, this._status = t ? "loading" : "idle");
312
+ }
313
+ updated(e) {
314
+ 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();
315
+ }
316
+ render() {
317
+ const e = this._src ? f`<iframe
318
+ part="iframe"
319
+ title="Template builder"
320
+ src=${this._src}
321
+ allow="clipboard-read; clipboard-write"
322
+ ></iframe>` : k, t = this._status === "loading" ? f`<div class="spinner" part="spinner"></div>` : k;
323
+ return this.mode === "modal" ? this._open ? f`<div class="overlay" part="overlay">
324
+ <div class="stage">${e}${t}</div>
325
+ </div>` : k : f`${e}${t}`;
326
+ }
327
+ _computeSrc() {
328
+ if (!this._open || !this.baseUrl || !this.token) return "";
329
+ if (this.secTemplate) {
330
+ if (!this.stateless)
331
+ return this._reportedStatelessRequired || (this._reportedStatelessRequired = !0, queueMicrotask(
332
+ () => this._fail({
333
+ code: "invalid-config",
334
+ message: "sec-template requires stateless mode — the app accepts a security template on the stateless embed route only."
335
+ })
336
+ )), "";
337
+ this._reportedStatelessRequired = !1;
338
+ } else if (!this.sassKey || !this.sessionUuid)
339
+ return "";
340
+ let e;
341
+ try {
342
+ const t = this.stateless ? G : Y(this.templateId || void 0), o = this.baseUrl.endsWith("/") ? this.baseUrl : `${this.baseUrl}/`;
343
+ e = new URL(t.replace(/^\//, ""), o);
344
+ } catch {
345
+ return this._reportedBadBaseUrl !== this.baseUrl && (this._reportedBadBaseUrl = this.baseUrl, queueMicrotask(
346
+ () => this._fail({
347
+ code: "invalid-base-url",
348
+ message: `base-url is not a valid URL: ${this.baseUrl}`
349
+ })
350
+ )), "";
351
+ }
352
+ return this._reportedBadBaseUrl = void 0, e.searchParams.set(c.FILEROBOT_TOKEN, this.token), this.secTemplate ? e.searchParams.set(c.SEC_TEMPLATE, this.secTemplate) : (e.searchParams.set(c.SASS_KEY, this.sassKey), e.searchParams.set(c.SESSION_UUID, this.sessionUuid), this.companyUuid && e.searchParams.set(c.COMPANY_UUID, this.companyUuid), this.projectUuid && e.searchParams.set(c.PROJECT_UUID, this.projectUuid)), this.brandColor && e.searchParams.set(c.BRAND_COLOR, this.brandColor), this.theme && e.searchParams.set(c.THEME, this.theme), e.searchParams.set(c.IFRAME, "1"), e.searchParams.set(c.EMBED_ORIGIN, window.location.origin), e.toString();
353
+ }
354
+ get _appOrigin() {
355
+ try {
356
+ return new URL(this.baseUrl).origin;
357
+ } catch {
358
+ return null;
359
+ }
360
+ }
361
+ /**
362
+ * Emit every not-yet-emitted `dam-store` save immediately, raw content with
363
+ * `storeError` in place of the links. Called when waiting any longer risks
364
+ * the event finding no listener; a still-running upload for a flushed save
365
+ * is left to finish (the copy usually lands) but will not emit again.
366
+ *
367
+ * Public for framework wrappers: one that unsubscribes its listeners before
368
+ * unmounting must call this first, while they are still attached — the
369
+ * element's own disconnect-time flush fires only after the wrapper has
370
+ * stopped listening, and the raw save would be lost. The React wrapper does
371
+ * this; a vanilla host never needs to call it.
372
+ */
373
+ flushPendingSaves(e = "widget removed before the rendering copy completed") {
374
+ this._flushPendingSaves(e);
375
+ }
376
+ _flushPendingSaves(e) {
377
+ for (const t of this._pendingSaves)
378
+ this._pendingSaves.delete(t), this._emit("save", { ...t, storeError: e });
379
+ }
380
+ /**
381
+ * The `dam-store` save path: upload the edited template to Filerobot, then
382
+ * emit `save` with the stored copy's links on the detail. The upload is the
383
+ * render side of the save — the CDN renders only stored files — while the
384
+ * raw `content` stays the host's copy exactly as without the flag.
385
+ *
386
+ * A failed upload still emits `save` (the raw data must reach the host
387
+ * either way), with `storeError` in place of `stored`; whether a save
388
+ * without a rendering copy counts as saved is the host's decision, made
389
+ * where it always is — the save ack.
390
+ */
391
+ async _storeAndEmitSave(e, t, o) {
392
+ if (!this._pendingSaves.has(e)) return;
393
+ const i = (this._storeMemory?.epoch === t ? this._storeMemory.uuid : void 0) || t === this._docEpoch && this.storedUuid || void 0;
394
+ let d, l;
395
+ try {
396
+ d = await oe(e, o.auth, o.storeFolder, i);
397
+ } catch (h) {
398
+ l = h instanceof Error ? h.message : String(h);
399
+ }
400
+ this._pendingSaves.delete(e) && (d && (this._storeMemory = { epoch: t, uuid: d.uuid }), this._emit("save", d ? { ...e, stored: d } : { ...e, storeError: l }));
401
+ }
402
+ /**
403
+ * Deliver `content` to the app once both sides are ready: it has asked, and
404
+ * we have something new to give it. Skips a re-send of identical content so
405
+ * an unrelated re-render can't discard the user's in-progress edits.
406
+ */
407
+ _maybeSendContent() {
408
+ if (!this.stateless || !this._contentRequested) return;
409
+ const e = this.content || (this.newTemplate ? Q : "");
410
+ if (!e) return;
411
+ const t = {
412
+ templateId: this.templateId || void 0,
413
+ content: e,
414
+ name: this.templateName || void 0,
415
+ templateQuery: this.templateQuery || void 0
416
+ }, o = this._contentKey(e), i = JSON.stringify({
417
+ templateId: this.templateId || void 0,
418
+ content: e,
419
+ name: this.templateName || void 0
420
+ });
421
+ if (o === this._sentKey || o === this._sentSaveKey) {
422
+ this._lastDocKey = i;
423
+ return;
424
+ }
425
+ this._postToApp({ type: H, data: t }) && (i !== this._lastDocKey && this._docEpoch++, this._lastDocKey = i, this._sentKey = o, this._sentSaveKey = void 0);
426
+ }
427
+ /**
428
+ * Deliver host config to the app. Unlike content this is not requested — the
429
+ * app has no way to know a host means to send any — so it goes out on the
430
+ * ready signal and on every later change.
431
+ *
432
+ * Sending an empty model is meaningful: it is how a host clears one it set
433
+ * before. What is skipped is only a *repeat* of what the app already holds,
434
+ * and the very first send when there was never anything to say.
435
+ */
436
+ _maybeSendConfig() {
437
+ const e = Array.isArray(this.customMetadata) ? this.customMetadata : [], t = typeof this.customMetadataLabel == "string" ? this.customMetadataLabel.trim() : "", o = JSON.stringify({ customMetadata: e, customMetadataLabel: t });
438
+ o !== this._sentConfigKey && (this._sentConfigKey === void 0 && e.length === 0 && t === "" || this._postToApp({
439
+ type: J,
440
+ data: {
441
+ customMetadata: e,
442
+ ...t ? { customMetadataLabel: t } : {}
443
+ }
444
+ }) && (this._sentConfigKey = o));
445
+ }
446
+ /** Identity of a delivered template, as compared against `_sentKey`. */
447
+ _contentKey(e) {
448
+ return JSON.stringify({
449
+ templateId: this.templateId || void 0,
450
+ content: e,
451
+ name: this.templateName || void 0,
452
+ templateQuery: this.templateQuery || void 0
453
+ });
454
+ }
455
+ /**
456
+ * Identity of a save the app handed back — the same shape as
457
+ * `_contentKey`, but built from the save detail rather than the props, so
458
+ * a host echoing the detail (whose name/query the save changed) matches.
459
+ */
460
+ _detailKey(e) {
461
+ return JSON.stringify({
462
+ templateId: e.templateId || void 0,
463
+ content: e.content,
464
+ name: e.name || void 0,
465
+ templateQuery: e.templateQuery || void 0
466
+ });
467
+ }
468
+ /** Post into the iframe, targeted at the app origin. False if not mounted. */
469
+ _postToApp(e) {
470
+ const t = this.shadowRoot?.querySelector("iframe")?.contentWindow, o = this._appOrigin;
471
+ return !t || !o ? !1 : (t.postMessage(e, o), !0);
472
+ }
473
+ _startHandshakeTimer() {
474
+ this.readyTimeout <= 0 || (this._handshakeTimer = window.setTimeout(() => {
475
+ this._fail({
476
+ code: "handshake-timeout",
477
+ 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.`
478
+ });
479
+ }, this.readyTimeout));
480
+ }
481
+ _clearHandshakeTimer() {
482
+ this._handshakeTimer !== void 0 && (window.clearTimeout(this._handshakeTimer), this._handshakeTimer = void 0);
483
+ }
484
+ _fail(e) {
485
+ this._clearHandshakeTimer(), this._status = "error", this._emit("error", e);
486
+ }
487
+ _emit(e, t) {
488
+ this.dispatchEvent(
489
+ new CustomEvent(e, { detail: t, bubbles: !0, composed: !0 })
490
+ );
491
+ }
492
+ };
493
+ O.styles = M`
494
+ :host {
495
+ display: block;
496
+ position: relative;
497
+ }
498
+ :host([mode='modal']) {
499
+ display: contents;
500
+ }
501
+ .overlay {
502
+ position: fixed;
503
+ inset: 0;
504
+ z-index: 2147483000;
505
+ background: rgba(0, 0, 0, 0.55);
506
+ display: flex;
507
+ }
508
+ .stage {
509
+ position: relative;
510
+ flex: 1;
511
+ display: flex;
512
+ }
513
+ iframe {
514
+ border: 0;
515
+ flex: 1;
516
+ width: 100%;
517
+ height: 100%;
518
+ }
519
+ .spinner {
520
+ position: absolute;
521
+ inset: 0;
522
+ margin: auto;
523
+ width: 32px;
524
+ height: 32px;
525
+ border: 3px solid rgba(128, 128, 128, 0.3);
526
+ border-top-color: currentColor;
527
+ border-radius: 50%;
528
+ animation: sfx-tb-spin 0.8s linear infinite;
529
+ pointer-events: none;
530
+ }
531
+ @keyframes sfx-tb-spin {
532
+ to {
533
+ transform: rotate(360deg);
534
+ }
535
+ }
536
+ `;
537
+ let r = O;
538
+ a([
539
+ n({ attribute: "base-url" })
540
+ ], r.prototype, "baseUrl");
541
+ a([
542
+ n()
543
+ ], r.prototype, "token");
544
+ a([
545
+ n({ attribute: "sass-key" })
546
+ ], r.prototype, "sassKey");
547
+ a([
548
+ n({ attribute: "session-uuid" })
549
+ ], r.prototype, "sessionUuid");
550
+ a([
551
+ n({ attribute: "sec-template" })
552
+ ], r.prototype, "secTemplate");
553
+ a([
554
+ n({ attribute: "company-uuid" })
555
+ ], r.prototype, "companyUuid");
556
+ a([
557
+ n({ attribute: "project-uuid" })
558
+ ], r.prototype, "projectUuid");
559
+ a([
560
+ n({ attribute: "template-id" })
561
+ ], r.prototype, "templateId");
562
+ a([
563
+ n({ reflect: !0 })
564
+ ], r.prototype, "mode");
565
+ a([
566
+ n({ type: Boolean, reflect: !0 })
567
+ ], r.prototype, "stateless");
568
+ a([
569
+ n({ attribute: !1 })
570
+ ], r.prototype, "content");
571
+ a([
572
+ n({ type: Boolean, attribute: "new-template" })
573
+ ], r.prototype, "newTemplate");
574
+ a([
575
+ n({ attribute: "template-name" })
576
+ ], r.prototype, "templateName");
577
+ a([
578
+ n({ attribute: "template-query" })
579
+ ], r.prototype, "templateQuery");
580
+ a([
581
+ n({ attribute: "brand-color" })
582
+ ], r.prototype, "brandColor");
583
+ a([
584
+ n()
585
+ ], r.prototype, "theme");
586
+ a([
587
+ n({
588
+ attribute: "custom-metadata",
589
+ converter: {
590
+ fromAttribute: (s) => {
591
+ if (!s) return [];
592
+ try {
593
+ const e = JSON.parse(s);
594
+ return Array.isArray(e) ? e : [];
595
+ } catch {
596
+ return console.warn("[sfx-template-builder] custom-metadata is not valid JSON — ignoring."), [];
597
+ }
598
+ },
599
+ toAttribute: (s) => JSON.stringify(s ?? [])
600
+ }
601
+ })
602
+ ], r.prototype, "customMetadata");
603
+ a([
604
+ n({ attribute: "custom-metadata-label" })
605
+ ], r.prototype, "customMetadataLabel");
606
+ a([
607
+ n({ type: Boolean, attribute: "dam-store" })
608
+ ], r.prototype, "damStore");
609
+ a([
610
+ n({ attribute: "store-folder" })
611
+ ], r.prototype, "storeFolder");
612
+ a([
613
+ n({ attribute: "stored-uuid" })
614
+ ], r.prototype, "storedUuid");
615
+ a([
616
+ n({ type: Number, attribute: "ready-timeout" })
617
+ ], r.prototype, "readyTimeout");
618
+ a([
619
+ y()
620
+ ], r.prototype, "_status");
621
+ a([
622
+ y()
623
+ ], r.prototype, "_open");
624
+ a([
625
+ y()
626
+ ], r.prototype, "_src");
627
+ a([
628
+ y()
629
+ ], r.prototype, "_isDirty");
630
+ export {
631
+ le as A,
632
+ Q as B,
633
+ c as E,
634
+ J as H,
635
+ de as P,
636
+ r as S,
637
+ ce as a,
638
+ $ as b,
639
+ j as c,
640
+ x as d,
641
+ q as e,
642
+ F as f,
643
+ K as g,
644
+ P as h,
645
+ B as i,
646
+ G as j,
647
+ H as k,
648
+ X as l,
649
+ Y as m
650
+ };
651
+ //# sourceMappingURL=template-builder-B9Cwo_Q-.js.map