@scaleflex/template-builder 0.1.1 → 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 +79 -24
- package/CHANGELOG.md +190 -4
- package/README.md +452 -66
- 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 +16 -14
- package/dist/protocol.d.ts +84 -2
- package/dist/react.cjs +1 -1
- package/dist/react.cjs.map +1 -1
- package/dist/react.d.ts +41 -1
- package/dist/react.js +45 -29
- 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 +201 -6
- package/package.json +3 -1
- package/src/dam-store.ts +388 -0
- package/src/index.ts +2 -0
- package/src/protocol.ts +92 -2
- package/src/react.ts +120 -27
- package/src/template-builder.ts +467 -11
- package/dist/template-builder-CSyPZni9.cjs +0 -52
- package/dist/template-builder-CSyPZni9.cjs.map +0 -1
- package/dist/template-builder-S33H_d5T.js +0 -354
- package/dist/template-builder-S33H_d5T.js.map +0 -1
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `dam-store` save path: upload an edited template to Filerobot so the
|
|
3
|
+
* CDN can render it.
|
|
4
|
+
*
|
|
5
|
+
* A stateless save hands the host raw XML — but the CDN renders only stored
|
|
6
|
+
* files, so a host that wants render URLs (previews, production banners) needs
|
|
7
|
+
* a copy in the DAM too. With `dam-store` the element makes that copy itself,
|
|
8
|
+
* with the same multipart upload the DAM-backed editor uses, and the `save`
|
|
9
|
+
* event carries the stored file's links next to the raw data.
|
|
10
|
+
*
|
|
11
|
+
* The raw `content` remains the host's copy of record: nothing here changes
|
|
12
|
+
* what the save event has always carried.
|
|
13
|
+
*/
|
|
14
|
+
import type { BuilderContentData } from './protocol';
|
|
15
|
+
export declare const FILEROBOT_API = "https://api.filerobot.com";
|
|
16
|
+
/** Credentials the element already holds; one of sassKey / secTemplate. */
|
|
17
|
+
export interface DamStoreAuth {
|
|
18
|
+
token: string;
|
|
19
|
+
sassKey?: string;
|
|
20
|
+
secTemplate?: string;
|
|
21
|
+
sessionUuid?: string;
|
|
22
|
+
companyUuid?: string;
|
|
23
|
+
projectUuid?: string;
|
|
24
|
+
}
|
|
25
|
+
/** The stored copy's links, carried on the `save` event as `detail.stored`. */
|
|
26
|
+
export interface StoredTemplate {
|
|
27
|
+
/** DAM file uuid of the stored `.fdt`. */
|
|
28
|
+
uuid: string;
|
|
29
|
+
/**
|
|
30
|
+
* CDN URL of the stored file, with its current `?vh=` cache key — append a
|
|
31
|
+
* template query to render it. Empty when the file record could not be read
|
|
32
|
+
* back after the upload (the file is stored regardless).
|
|
33
|
+
*/
|
|
34
|
+
url: string;
|
|
35
|
+
}
|
|
36
|
+
export interface FileRecord {
|
|
37
|
+
uuid?: string;
|
|
38
|
+
name?: string;
|
|
39
|
+
folder?: {
|
|
40
|
+
name?: string;
|
|
41
|
+
};
|
|
42
|
+
url?: {
|
|
43
|
+
cdn?: string;
|
|
44
|
+
public?: string;
|
|
45
|
+
path?: string;
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* A security template is not a key — it is exchanged for a short-lived sass
|
|
50
|
+
* key first, the template authenticating its own exchange. Same call the app
|
|
51
|
+
* and the asset picker make.
|
|
52
|
+
*
|
|
53
|
+
* Exported (with `apiHeaders` / `getFileRecord`) for the demo page, which
|
|
54
|
+
* plays the host half of the same API conversation — one implementation of
|
|
55
|
+
* the auth rules, not two drifting copies.
|
|
56
|
+
*/
|
|
57
|
+
export declare function resolveKey(auth: DamStoreAuth): Promise<string>;
|
|
58
|
+
/**
|
|
59
|
+
* Session scope only: a minted key carries its own, and pairing it with a
|
|
60
|
+
* session's uuids would mix one mode's key with the other mode's scope.
|
|
61
|
+
*/
|
|
62
|
+
export declare function apiHeaders(auth: DamStoreAuth, key: string): Record<string, string>;
|
|
63
|
+
/**
|
|
64
|
+
* Whether a template id plausibly names a DAM file (hex-and-dashes uuid).
|
|
65
|
+
* Opaque host ids ('demo-1', 'sample-spring-banner') never do — looking them
|
|
66
|
+
* up would waste a round trip per save and couple every save to whichever
|
|
67
|
+
* status the API happens to answer a malformed id with.
|
|
68
|
+
*/
|
|
69
|
+
export declare function looksLikeDamFileUuid(id: string): boolean;
|
|
70
|
+
/**
|
|
71
|
+
* One file's record. `null` means the identifier names nothing — a 404/gone,
|
|
72
|
+
* a 4xx rejecting the id itself, or the API's not-found envelope — all normal
|
|
73
|
+
* answers here. What THROWS is a failure to answer (auth, rate limit, 5xx,
|
|
74
|
+
* network): collapsing those into null would make a transient blip read as
|
|
75
|
+
* "file gone", and the callers act on that — re-homing an existing template
|
|
76
|
+
* into the fallback folder as a duplicate, or failing an unchanged re-save.
|
|
77
|
+
*/
|
|
78
|
+
export declare function getFileRecord(auth: DamStoreAuth, key: string, uuid: string): Promise<FileRecord | null>;
|
|
79
|
+
/**
|
|
80
|
+
* Store one save in the DAM and return the stored copy's links.
|
|
81
|
+
*
|
|
82
|
+
* Uploads into the folder the file this template is already stored as lives
|
|
83
|
+
* in — the host's `templateId` when it names a DAM file, else `knownUuid`
|
|
84
|
+
* (the copy a previous save in this session made; hosts persist `stored.uuid`
|
|
85
|
+
* rather than echoing it into `template-id`, which would reload the editor) —
|
|
86
|
+
* so same name + folder makes the backend version the template in place. New
|
|
87
|
+
* templates land in `fallbackFolder`. Unchanged content resolves to the
|
|
88
|
+
* already-stored file rather than failing. Throws with a human-readable
|
|
89
|
+
* message when the copy could not be made; the caller decides what a save
|
|
90
|
+
* without a stored copy means.
|
|
91
|
+
*/
|
|
92
|
+
export declare function storeTemplateInDam(data: BuilderContentData, auth: DamStoreAuth, fallbackFolder: string, knownUuid?: string): Promise<StoredTemplate>;
|
package/dist/define.cjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const e=require("./template-builder-
|
|
1
|
+
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const e=require("./template-builder-Byqg1q93.cjs");typeof customElements<"u"&&!customElements.get("sfx-template-builder")&&customElements.define("sfx-template-builder",e.SfxTemplateBuilder);exports.SfxTemplateBuilder=e.SfxTemplateBuilder;
|
|
2
2
|
//# sourceMappingURL=define.cjs.map
|
package/dist/define.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { S as e } from "./template-builder-
|
|
1
|
+
import { S as e } from "./template-builder-B9Cwo_Q-.js";
|
|
2
2
|
typeof customElements < "u" && !customElements.get("sfx-template-builder") && customElements.define("sfx-template-builder", e);
|
|
3
3
|
export {
|
|
4
4
|
e as SfxTemplateBuilder
|
package/dist/index.cjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const E=require("./template-builder-
|
|
1
|
+
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const E=require("./template-builder-Byqg1q93.cjs");exports.AUTH_MODES=E.AUTH_MODES;exports.BLANK_TEMPLATE_XML=E.BLANK_TEMPLATE_XML;exports.BRAND_COLOR_PATTERN=E.BRAND_COLOR_PATTERN;exports.BUILDER_CLOSE=E.BUILDER_CLOSE;exports.BUILDER_CONTENT=E.BUILDER_CONTENT;exports.BUILDER_CONTENT_REQUEST=E.BUILDER_CONTENT_REQUEST;exports.BUILDER_DIRTY=E.BUILDER_DIRTY;exports.BUILDER_ERROR=E.BUILDER_ERROR;exports.BUILDER_OPEN=E.BUILDER_OPEN;exports.BUILDER_READY=E.BUILDER_READY;exports.BUILDER_SAVE=E.BUILDER_SAVE;exports.EMBED_PARAMS=E.EMBED_PARAMS;exports.EMBED_ROUTE=E.EMBED_ROUTE;exports.HOST_CONFIG=E.HOST_CONFIG;exports.HOST_LOAD=E.HOST_LOAD;exports.HOST_SAVED=E.HOST_SAVED;exports.PROTOCOL_VERSION=E.PROTOCOL_VERSION;exports.SfxTemplateBuilder=E.SfxTemplateBuilder;exports.builderRoute=E.builderRoute;
|
|
2
2
|
//# sourceMappingURL=index.cjs.map
|
package/dist/index.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export { SfxTemplateBuilder, type TemplateBuilderStatus, type TemplateBuilderEventMap, type TemplateBuilderSaveDetail, } from './template-builder';
|
|
1
|
+
export { SfxTemplateBuilder, type TemplateBuilderStatus, type TemplateBuilderEventMap, type TemplateBuilderSaveDetail, type StoredTemplate, type DamStoreAuth, } from './template-builder';
|
|
2
2
|
export * from './protocol';
|
package/dist/index.js
CHANGED
|
@@ -1,21 +1,23 @@
|
|
|
1
|
-
import { A as a, B as
|
|
1
|
+
import { A as a, B as _, a as s, b as O, c as T, d as D, e as L, f as B, g as A, h as S, i as I, E as U, j as N, H as e, k as C, l as M, P, S as H, m as l } from "./template-builder-B9Cwo_Q-.js";
|
|
2
2
|
export {
|
|
3
3
|
a as AUTH_MODES,
|
|
4
|
+
_ as BLANK_TEMPLATE_XML,
|
|
4
5
|
s as BRAND_COLOR_PATTERN,
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
O as BUILDER_CLOSE,
|
|
7
|
+
T as BUILDER_CONTENT,
|
|
8
|
+
D as BUILDER_CONTENT_REQUEST,
|
|
9
|
+
L as BUILDER_DIRTY,
|
|
9
10
|
B as BUILDER_ERROR,
|
|
10
|
-
|
|
11
|
+
A as BUILDER_OPEN,
|
|
11
12
|
S as BUILDER_READY,
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
P as
|
|
19
|
-
|
|
13
|
+
I as BUILDER_SAVE,
|
|
14
|
+
U as EMBED_PARAMS,
|
|
15
|
+
N as EMBED_ROUTE,
|
|
16
|
+
e as HOST_CONFIG,
|
|
17
|
+
C as HOST_LOAD,
|
|
18
|
+
M as HOST_SAVED,
|
|
19
|
+
P as PROTOCOL_VERSION,
|
|
20
|
+
H as SfxTemplateBuilder,
|
|
21
|
+
l as builderRoute
|
|
20
22
|
};
|
|
21
23
|
//# sourceMappingURL=index.js.map
|
package/dist/protocol.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export declare const PROTOCOL_VERSION =
|
|
1
|
+
export declare const PROTOCOL_VERSION = 3;
|
|
2
2
|
/** Editor mounted with valid auth — the embed handshake succeeded. */
|
|
3
3
|
export declare const BUILDER_READY = "design-templates:builder:ready";
|
|
4
4
|
/** Editor UI opened (kept for Hub backwards compatibility; implies ready). */
|
|
@@ -150,6 +150,88 @@ export interface HostLoadMessage {
|
|
|
150
150
|
type: typeof HOST_LOAD;
|
|
151
151
|
data: HostLoadData;
|
|
152
152
|
}
|
|
153
|
+
/**
|
|
154
|
+
* An empty `.fdt` document: no layouts, no layers, no variables. What the
|
|
155
|
+
* widget sends as `HOST_LOAD` content when the host asked for a new template
|
|
156
|
+
* (`new-template`) instead of supplying one, so starting from scratch costs a
|
|
157
|
+
* host no knowledge of the template format.
|
|
158
|
+
*
|
|
159
|
+
* The editor opens on its empty state — "No layouts yet. Click + Add to create
|
|
160
|
+
* one." — and the user picks the canvas size there. Save is refused until a
|
|
161
|
+
* layout exists, and hands back a fully-formed document serialized by the app,
|
|
162
|
+
* not this skeleton.
|
|
163
|
+
*
|
|
164
|
+
* Sent as ordinary `HOST_LOAD` content rather than a new message so it works
|
|
165
|
+
* against app deployments that predate this widget version: the document is
|
|
166
|
+
* the whole signal, and every app that can parse a template can parse this.
|
|
167
|
+
*
|
|
168
|
+
* `version` tracks the app's `TEMPLATE_VERSION` for the benefit of whoever
|
|
169
|
+
* reads this next: nothing consumes it. The parser never looks at it, and the
|
|
170
|
+
* backend never sees this document — the app refuses to save a template with
|
|
171
|
+
* no layouts, so what reaches the render pipeline was re-serialized by the app
|
|
172
|
+
* with a layout present. A widget lagging the app by a version still loads.
|
|
173
|
+
* `design-templates` pins the pair in
|
|
174
|
+
* `src/lib/xml/__tests__/blank-template.test.ts`.
|
|
175
|
+
*/
|
|
176
|
+
export declare const BLANK_TEMPLATE_XML: string;
|
|
177
|
+
/**
|
|
178
|
+
* Host-supplied editor configuration (protocol v3). Sent once the app reports
|
|
179
|
+
* `BUILDER_READY`, and again whenever the host changes it.
|
|
180
|
+
*
|
|
181
|
+
* Separate from `HOST_LOAD` because it is not per-template and because it must
|
|
182
|
+
* also reach DAM-backed embeds, which never receive a `HOST_LOAD` at all — the
|
|
183
|
+
* app loads those templates itself.
|
|
184
|
+
*
|
|
185
|
+
* Purely additive: an app deployment that predates this message ignores it and
|
|
186
|
+
* behaves exactly as before, so a newer widget stays compatible with an older
|
|
187
|
+
* app. Held to the same origin bar as `HOST_LOAD`.
|
|
188
|
+
*/
|
|
189
|
+
export declare const HOST_CONFIG = "design-templates:host:config";
|
|
190
|
+
/**
|
|
191
|
+
* One field of a host-supplied metadata model, offered in the editor as the
|
|
192
|
+
* "Custom metadata" value source.
|
|
193
|
+
*
|
|
194
|
+
* The model is a vocabulary, not data: it names the fields the host can fill at
|
|
195
|
+
* render time, so an author can bind a variable to `sku` rather than having to
|
|
196
|
+
* remember that the variable's slug happens to mean the SKU. No value travels
|
|
197
|
+
* with it — the host substitutes one by putting `$slug=value` in the render
|
|
198
|
+
* query, exactly as it would for a free-text variable.
|
|
199
|
+
*
|
|
200
|
+
* This is what makes named fields workable in `secTemplate` / stateless embeds,
|
|
201
|
+
* where the Hub project model (and with it the "File metadata" source) is
|
|
202
|
+
* unavailable.
|
|
203
|
+
*/
|
|
204
|
+
export interface CustomMetadataField {
|
|
205
|
+
/**
|
|
206
|
+
* Stable identifier stored in the template as `custom_ckey`. The host's own
|
|
207
|
+
* key for the field — the app never resolves it against anything.
|
|
208
|
+
*/
|
|
209
|
+
key: string;
|
|
210
|
+
/** Label shown in the editor's field picker. Falls back to `key` when empty. */
|
|
211
|
+
title?: string;
|
|
212
|
+
/** Optional section header, used to group fields in the picker. */
|
|
213
|
+
group?: string;
|
|
214
|
+
}
|
|
215
|
+
export interface HostConfigData {
|
|
216
|
+
/**
|
|
217
|
+
* Metadata model offered as the "Custom metadata" value source. Omitted or
|
|
218
|
+
* empty hides that source in the editor, so a host that sends nothing sees
|
|
219
|
+
* the two sources it always had.
|
|
220
|
+
*/
|
|
221
|
+
customMetadata?: CustomMetadataField[];
|
|
222
|
+
/**
|
|
223
|
+
* Display name for the custom-metadata value source in the editor's UI
|
|
224
|
+
* (source dropdowns, properties-panel section). Defaults to "Custom
|
|
225
|
+
* metadata"; a host can rename it after its own domain — e.g. "External
|
|
226
|
+
* metadata" or "Product attributes". Pure wording: the stored template is
|
|
227
|
+
* unaffected.
|
|
228
|
+
*/
|
|
229
|
+
customMetadataLabel?: string;
|
|
230
|
+
}
|
|
231
|
+
export interface HostConfigMessage {
|
|
232
|
+
type: typeof HOST_CONFIG;
|
|
233
|
+
data: HostConfigData;
|
|
234
|
+
}
|
|
153
235
|
/**
|
|
154
236
|
* Stateless mode only (protocol v2). Reports whether the host managed to
|
|
155
237
|
* persist the content it received in `BUILDER_CONTENT`.
|
|
@@ -171,7 +253,7 @@ export interface HostSavedMessage {
|
|
|
171
253
|
type: typeof HOST_SAVED;
|
|
172
254
|
data: HostSavedData;
|
|
173
255
|
}
|
|
174
|
-
export type HostMessage = HostLoadMessage | HostSavedMessage;
|
|
256
|
+
export type HostMessage = HostLoadMessage | HostSavedMessage | HostConfigMessage;
|
|
175
257
|
/**
|
|
176
258
|
* Query params the app's proxy middleware (`src/proxy.ts`) converts into auth
|
|
177
259
|
* cookies on first navigation. Names are wire format.
|
package/dist/react.cjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const
|
|
1
|
+
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const a=require("react");require("./define.cjs");const g=a.forwardRef(function(b,h){const{className:U,style:T,onReady:i,onOpen:c,onClose:m,onSave:f,onError:p,onDirtyChange:y,...e}=b,l=a.useRef(null);a.useImperativeHandle(h,()=>l.current,[]),a.useLayoutEffect(()=>{const t=l.current;t&&(t.baseUrl=e.baseUrl,t.token=e.token,t.sassKey=e.sassKey??"",t.sessionUuid=e.sessionUuid??"",t.secTemplate=e.secTemplate??"",t.companyUuid=e.companyUuid??"",t.projectUuid=e.projectUuid??"",t.templateId=e.templateId??"",t.mode=e.mode??"inline",t.stateless=e.stateless??!1,t.templateName=e.templateName??"",t.templateQuery=e.templateQuery??"",t.brandColor=e.brandColor??"",t.theme=e.theme??"",t.newTemplate=e.newTemplate??!1,t.customMetadata=e.customMetadata??[],t.customMetadataLabel=e.customMetadataLabel??"",t.damStore=e.damStore??!1,t.storeFolder=e.storeFolder??"/",t.storedUuid=e.storedUuid??"",t.content=e.content??"",e.readyTimeout!==void 0&&(t.readyTimeout=e.readyTimeout))},[e.baseUrl,e.token,e.sassKey,e.sessionUuid,e.secTemplate,e.companyUuid,e.projectUuid,e.templateId,e.mode,e.stateless,e.content,e.newTemplate,e.templateName,e.templateQuery,e.brandColor,e.theme,e.customMetadata,e.customMetadataLabel,e.damStore,e.storeFolder,e.storedUuid,e.readyTimeout]);const d=a.useRef({onReady:i,onOpen:c,onClose:m,onSave:f,onError:p,onDirtyChange:y});return a.useLayoutEffect(()=>{d.current={onReady:i,onOpen:c,onClose:m,onSave:f,onError:p,onDirtyChange:y}}),a.useLayoutEffect(()=>{const t=l.current;if(!t)return;const u=[],n=(o,r)=>{const s=(S=>r(d.current)?.(S.detail));t.addEventListener(o,s),u.push([o,s])};n("ready",o=>o.onReady),n("open",o=>o.onOpen),n("close",o=>o.onClose),n("error",o=>o.onError),n("dirtychange",o=>o.onDirtyChange);const v=(o=>{const r=d.current.onSave;r&&Promise.resolve().then(()=>r(o.detail)).then(s=>t.confirmSave(s!==!1)).catch(s=>{console.error("[sfx-template-builder] onSave failed:",s),t.confirmSave(!1)})});return t.addEventListener("save",v),u.push(["save",v]),()=>{typeof t.flushPendingSaves=="function"&&t.flushPendingSaves("widget removed before the rendering copy completed");for(const[o,r]of u)t.removeEventListener(o,r)}},[]),a.createElement("sfx-template-builder",{ref:l,class:U,style:T})});exports.TemplateBuilder=g;
|
|
2
2
|
//# sourceMappingURL=react.cjs.map
|
package/dist/react.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"react.cjs","sources":["../src/react.ts"],"sourcesContent":["import {\n createElement,\n forwardRef,\n useImperativeHandle,\n useLayoutEffect,\n useRef,\n type CSSProperties,\n type ReactElement,\n} from 'react'\nimport './define'\nimport type { SfxTemplateBuilder } from './template-builder'\nimport type { TemplateBuilderSaveDetail } from './template-builder'\nimport type { BuilderDirtyData, BuilderErrorData, BuilderTheme } from './protocol'\n\n/**\n * Hub session — the full-featured credential.\n */\nexport interface TemplateBuilderSessionAuth {\n sassKey: string\n sessionUuid: string\n companyUuid?: string\n projectUuid?: string\n secTemplate?: never\n}\n\n/**\n * Filerobot security template — a guest credential for hosts that have no Hub\n * session to hand over. `stateless` is required rather than merely implied:\n * the app takes a security template on the stateless embed route only, so the\n * combination is a compile-time error instead of a runtime one.\n *\n * `companyUuid` / `projectUuid` are absent by design — they name a Hub project\n * that cannot be looked up without a session.\n */\nexport interface TemplateBuilderSecTemplateAuth {\n secTemplate: string\n stateless: true\n sassKey?: never\n sessionUuid?: never\n companyUuid?: never\n projectUuid?: never\n}\n\nexport interface TemplateBuilderBaseProps {\n baseUrl: string\n token: string\n templateId?: string\n mode?: 'inline' | 'modal'\n /** Hand the template in and take it back out instead of using the DAM. */\n stateless?: boolean\n /** Stateless mode: the template to edit, as `.fdt` XML. */\n content?: string\n /** Stateless mode: display name for the editor header. */\n templateName?: string\n /**\n * Stateless mode: the `template_query` to open on — the value handed back on\n * save. Reopens the template on the same layout and variable values; empty\n * falls back to the XML's own `default=` attributes.\n */\n templateQuery?: string\n /** Accent colour for the editor chrome, as `#rgb` / `#rrggbb`. */\n brandColor?: string\n /** Colour scheme for the editor chrome. */\n theme?: BuilderTheme\n readyTimeout?: number\n className?: string\n style?: CSSProperties\n onReady?: () => void\n onOpen?: () => void\n onClose?: () => void\n /**\n * Fired on save. In stateless mode the detail carries the edited `content`\n * for you to persist; otherwise it reports the uuid the app uploaded to.\n *\n * In stateless mode the outcome is reported back to the editor: return (or\n * resolve to) `false`, or throw, and the editor restores its unsaved-changes\n * flag and tells the user the save failed. Anything else counts as persisted.\n */\n onSave?: (\n data: TemplateBuilderSaveDetail,\n ) => void | boolean | Promise<void | boolean>\n onError?: (data: BuilderErrorData) => void\n /**\n * Stateless mode: unsaved-changes flag changed. Use it to prompt before\n * swapping `content`, which discards in-progress edits.\n */\n onDirtyChange?: (data: BuilderDirtyData) => void\n}\n\nexport type TemplateBuilderProps = TemplateBuilderBaseProps &\n (TemplateBuilderSessionAuth | TemplateBuilderSecTemplateAuth)\n\n/**\n * React wrapper around `<sfx-template-builder>`. Props are assigned as\n * element properties via ref (works on React 18 and 19 alike); callbacks\n * subscribe to the element's CustomEvents.\n *\n * Forwards a ref to the underlying element, which is the only way to reach the\n * imperative API — `open()` in particular, without which `mode=\"modal\"` can\n * never be shown:\n *\n * ```tsx\n * const builder = useRef<SfxTemplateBuilder>(null)\n * <TemplateBuilder ref={builder} mode=\"modal\" … />\n * <button onClick={() => builder.current?.open('tpl-1')}>Edit</button>\n * ```\n *\n * `forwardRef` rather than a plain `ref` prop: React 19 accepts the latter for\n * function components, React 18 does not, and both are supported peers.\n */\nexport const TemplateBuilder = forwardRef<\n SfxTemplateBuilder,\n TemplateBuilderProps\n>(function TemplateBuilder(props, forwardedRef): ReactElement {\n const {\n className,\n style,\n onReady,\n onOpen,\n onClose,\n onSave,\n onError,\n onDirtyChange,\n ...config\n } = props\n const ref = useRef<SfxTemplateBuilder>(null)\n\n // Hand the same element out to the caller without giving up the internal ref\n // the effects below rely on.\n useImperativeHandle(forwardedRef, () => ref.current as SfxTemplateBuilder, [])\n\n // Assign config before paint so the iframe doesn't first mount with defaults.\n useLayoutEffect(() => {\n const el = ref.current\n if (!el) return\n el.baseUrl = config.baseUrl\n el.token = config.token\n el.sassKey = config.sassKey ?? ''\n el.sessionUuid = config.sessionUuid ?? ''\n el.secTemplate = config.secTemplate ?? ''\n el.companyUuid = config.companyUuid ?? ''\n el.projectUuid = config.projectUuid ?? ''\n el.templateId = config.templateId ?? ''\n el.mode = config.mode ?? 'inline'\n el.stateless = config.stateless ?? false\n el.templateName = config.templateName ?? ''\n el.templateQuery = config.templateQuery ?? ''\n el.brandColor = config.brandColor ?? ''\n el.theme = config.theme ?? ''\n // Assigned last: the element sends content to the app as soon as it has\n // both a request and a value, so the id, name and query must already be\n // set — all four ship as one message.\n el.content = config.content ?? ''\n if (config.readyTimeout !== undefined) el.readyTimeout = config.readyTimeout\n }, [\n config.baseUrl,\n config.token,\n config.sassKey,\n config.sessionUuid,\n config.secTemplate,\n config.companyUuid,\n config.projectUuid,\n config.templateId,\n config.mode,\n config.stateless,\n config.content,\n config.templateName,\n config.templateQuery,\n config.brandColor,\n config.theme,\n config.readyTimeout,\n ])\n\n // Layout effect, not passive: the element reports config errors (e.g.\n // `invalid-base-url`) in a microtask queued during this same commit, and a\n // passive effect would subscribe only after that microtask has fired —\n // making a mount-time error unobservable from React. This effect is declared\n // after the config one, so it still runs once the config is assigned.\n useLayoutEffect(() => {\n const el = ref.current\n if (!el) return\n const subs: Array<[string, EventListener]> = []\n const on = (name: string, handler?: (detail: never) => void) => {\n if (!handler) return\n const listener = ((e: CustomEvent) => handler(e.detail as never)) as EventListener\n el.addEventListener(name, listener)\n subs.push([name, listener])\n }\n on('ready', onReady)\n on('open', onOpen)\n on('close', onClose)\n on('error', onError)\n on('dirtychange', onDirtyChange)\n\n // `save` is not just re-emitted: in stateless mode the handler's outcome\n // is acked back, so a failed write on the host side doesn't leave the\n // editor showing the template as saved. `confirmSave` no-ops in DAM mode.\n if (onSave) {\n const listener = ((e: CustomEvent<TemplateBuilderSaveDetail>) => {\n // Wrapped in a promise so a synchronous throw is handled like a\n // rejection, and a sync `false` like a resolved one.\n Promise.resolve()\n .then(() => onSave(e.detail))\n .then((result) => el.confirmSave(result !== false))\n .catch((err) => {\n // No message: an internal error string is not something to put in\n // front of the end user. The editor uses its own wording.\n console.error('[sfx-template-builder] onSave failed:', err)\n el.confirmSave(false)\n })\n }) as EventListener\n el.addEventListener('save', listener)\n subs.push(['save', listener])\n }\n\n return () => {\n for (const [name, listener] of subs) el.removeEventListener(name, listener)\n }\n }, [onReady, onOpen, onClose, onSave, onError, onDirtyChange])\n\n // eslint-disable-next-line react-hooks/refs -- ref is forwarded as a prop, not read during render\n return createElement('sfx-template-builder', { ref, class: className, style })\n})\n"],"names":["TemplateBuilder","forwardRef","props","forwardedRef","className","style","onReady","onOpen","onClose","onSave","onError","onDirtyChange","config","ref","useRef","useImperativeHandle","useLayoutEffect","el","subs","on","name","handler","listener","e","result","err","createElement"],"mappings":"iIA8GO,MAAMA,EAAkBC,EAAAA,WAG7B,SAAyBC,EAAOC,EAA4B,CAC5D,KAAM,CACJ,UAAAC,EACA,MAAAC,EACA,QAAAC,EACA,OAAAC,EACA,QAAAC,EACA,OAAAC,EACA,QAAAC,EACA,cAAAC,EACA,GAAGC,CAAA,EACDV,EACEW,EAAMC,EAAAA,OAA2B,IAAI,EAI3CC,OAAAA,EAAAA,oBAAoBZ,EAAc,IAAMU,EAAI,QAA+B,CAAA,CAAE,EAG7EG,EAAAA,gBAAgB,IAAM,CACpB,MAAMC,EAAKJ,EAAI,QACVI,IACLA,EAAG,QAAUL,EAAO,QACpBK,EAAG,MAAQL,EAAO,MAClBK,EAAG,QAAUL,EAAO,SAAW,GAC/BK,EAAG,YAAcL,EAAO,aAAe,GACvCK,EAAG,YAAcL,EAAO,aAAe,GACvCK,EAAG,YAAcL,EAAO,aAAe,GACvCK,EAAG,YAAcL,EAAO,aAAe,GACvCK,EAAG,WAAaL,EAAO,YAAc,GACrCK,EAAG,KAAOL,EAAO,MAAQ,SACzBK,EAAG,UAAYL,EAAO,WAAa,GACnCK,EAAG,aAAeL,EAAO,cAAgB,GACzCK,EAAG,cAAgBL,EAAO,eAAiB,GAC3CK,EAAG,WAAaL,EAAO,YAAc,GACrCK,EAAG,MAAQL,EAAO,OAAS,GAI3BK,EAAG,QAAUL,EAAO,SAAW,GAC3BA,EAAO,eAAiB,SAAWK,EAAG,aAAeL,EAAO,cAClE,EAAG,CACDA,EAAO,QACPA,EAAO,MACPA,EAAO,QACPA,EAAO,YACPA,EAAO,YACPA,EAAO,YACPA,EAAO,YACPA,EAAO,WACPA,EAAO,KACPA,EAAO,UACPA,EAAO,QACPA,EAAO,aACPA,EAAO,cACPA,EAAO,WACPA,EAAO,MACPA,EAAO,YAAA,CACR,EAODI,EAAAA,gBAAgB,IAAM,CACpB,MAAMC,EAAKJ,EAAI,QACf,GAAI,CAACI,EAAI,OACT,MAAMC,EAAuC,CAAA,EACvCC,EAAK,CAACC,EAAcC,IAAsC,CAC9D,GAAI,CAACA,EAAS,OACd,MAAMC,GAAaC,GAAmBF,EAAQE,EAAE,MAAe,GAC/DN,EAAG,iBAAiBG,EAAME,CAAQ,EAClCJ,EAAK,KAAK,CAACE,EAAME,CAAQ,CAAC,CAC5B,EAUA,GATAH,EAAG,QAASb,CAAO,EACnBa,EAAG,OAAQZ,CAAM,EACjBY,EAAG,QAASX,CAAO,EACnBW,EAAG,QAAST,CAAO,EACnBS,EAAG,cAAeR,CAAa,EAK3BF,EAAQ,CACV,MAAMa,GAAaC,GAA8C,CAG/D,QAAQ,QAAA,EACL,KAAK,IAAMd,EAAOc,EAAE,MAAM,CAAC,EAC3B,KAAMC,GAAWP,EAAG,YAAYO,IAAW,EAAK,CAAC,EACjD,MAAOC,GAAQ,CAGd,QAAQ,MAAM,wCAAyCA,CAAG,EAC1DR,EAAG,YAAY,EAAK,CACtB,CAAC,CACL,GACAA,EAAG,iBAAiB,OAAQK,CAAQ,EACpCJ,EAAK,KAAK,CAAC,OAAQI,CAAQ,CAAC,CAC9B,CAEA,MAAO,IAAM,CACX,SAAW,CAACF,EAAME,CAAQ,IAAKJ,EAAMD,EAAG,oBAAoBG,EAAME,CAAQ,CAC5E,CACF,EAAG,CAAChB,EAASC,EAAQC,EAASC,EAAQC,EAASC,CAAa,CAAC,EAGtDe,EAAAA,cAAc,uBAAwB,CAAE,IAAAb,EAAK,MAAOT,EAAW,MAAAC,EAAO,CAC/E,CAAC"}
|
|
1
|
+
{"version":3,"file":"react.cjs","sources":["../src/react.ts"],"sourcesContent":["import {\n createElement,\n forwardRef,\n useImperativeHandle,\n useLayoutEffect,\n useRef,\n type CSSProperties,\n type ReactElement,\n} from 'react'\nimport './define'\nimport type { SfxTemplateBuilder } from './template-builder'\nimport type { TemplateBuilderSaveDetail } from './template-builder'\nimport type {\n BuilderDirtyData,\n BuilderErrorData,\n BuilderTheme,\n CustomMetadataField,\n} from './protocol'\n\n/**\n * Hub session — the full-featured credential.\n */\nexport interface TemplateBuilderSessionAuth {\n sassKey: string\n sessionUuid: string\n companyUuid?: string\n projectUuid?: string\n secTemplate?: never\n}\n\n/**\n * Filerobot security template — a guest credential for hosts that have no Hub\n * session to hand over. `stateless` is required rather than merely implied:\n * the app takes a security template on the stateless embed route only, so the\n * combination is a compile-time error instead of a runtime one.\n *\n * `companyUuid` / `projectUuid` are absent by design — they name a Hub project\n * that cannot be looked up without a session.\n */\nexport interface TemplateBuilderSecTemplateAuth {\n secTemplate: string\n stateless: true\n sassKey?: never\n sessionUuid?: never\n companyUuid?: never\n projectUuid?: never\n}\n\nexport interface TemplateBuilderBaseProps {\n baseUrl: string\n token: string\n templateId?: string\n mode?: 'inline' | 'modal'\n /** Hand the template in and take it back out instead of using the DAM. */\n stateless?: boolean\n /** Stateless mode: the template to edit, as `.fdt` XML. */\n content?: string\n /**\n * Stateless mode: open on a new, empty template instead of supplying\n * `content`. The widget provides the blank document, the user picks the\n * canvas size in the editor, and `onSave` receives a complete `.fdt` to\n * store. Ignored when `content` is set.\n */\n newTemplate?: boolean\n /** Stateless mode: display name for the editor header. */\n templateName?: string\n /**\n * Stateless mode: the `template_query` to open on — the value handed back on\n * save. Reopens the template on the same layout and variable values; empty\n * falls back to the XML's own `default=` attributes.\n */\n templateQuery?: string\n /** Accent colour for the editor chrome, as `#rgb` / `#rrggbb`. */\n brandColor?: string\n /** Colour scheme for the editor chrome. */\n theme?: BuilderTheme\n /**\n * Metadata model offered in the editor as the \"Custom metadata\" value source\n * — names only, no values. Omit it and the source is not offered.\n *\n * Compared by identity, like every other prop here, so a freshly built array\n * counts as a change. Nothing is re-sent to the editor over it — the element\n * de-dupes by value — but hoisting or memoising the array avoids the churn.\n */\n customMetadata?: CustomMetadataField[]\n /**\n * Display name for the custom-metadata value source in the editor's UI —\n * e.g. \"External metadata\". Wording only: the stored template is unaffected.\n * Empty uses the editor's default, \"Custom metadata\".\n */\n customMetadataLabel?: string\n /**\n * Stateless only: store each save in Filerobot too, so the CDN can render\n * it. `onSave`'s detail then carries `stored: { uuid, url }` next to the raw\n * `content` — or `storeError` when the copy failed.\n */\n damStore?: boolean\n /**\n * Folder new templates land in under `damStore` when the template id names\n * no existing DAM file (an existing file's own folder always wins).\n */\n storeFolder?: string\n /**\n * `damStore`: the `stored.uuid` a previous session's save reported for THIS\n * document, so re-saves after a reload resolve to (and version) the copy\n * that already exists instead of erroring on unchanged content or starting\n * a fresh file. Per-document — pass it with the content it belongs to.\n */\n storedUuid?: string\n readyTimeout?: number\n className?: string\n style?: CSSProperties\n onReady?: () => void\n onOpen?: () => void\n onClose?: () => void\n /**\n * Fired on save. In stateless mode the detail carries the edited `content`\n * for you to persist; otherwise it reports the uuid the app uploaded to.\n *\n * In stateless mode the outcome is reported back to the editor: return (or\n * resolve to) `false`, or throw, and the editor restores its unsaved-changes\n * flag and tells the user the save failed. Anything else counts as persisted.\n */\n onSave?: (\n data: TemplateBuilderSaveDetail,\n ) => void | boolean | Promise<void | boolean>\n onError?: (data: BuilderErrorData) => void\n /**\n * Stateless mode: unsaved-changes flag changed. Use it to prompt before\n * swapping `content`, which discards in-progress edits.\n */\n onDirtyChange?: (data: BuilderDirtyData) => void\n}\n\nexport type TemplateBuilderProps = TemplateBuilderBaseProps &\n (TemplateBuilderSessionAuth | TemplateBuilderSecTemplateAuth)\n\n/**\n * React wrapper around `<sfx-template-builder>`. Props are assigned as\n * element properties via ref (works on React 18 and 19 alike); callbacks\n * subscribe to the element's CustomEvents.\n *\n * Forwards a ref to the underlying element, which is the only way to reach the\n * imperative API — `open()` in particular, without which `mode=\"modal\"` can\n * never be shown:\n *\n * ```tsx\n * const builder = useRef<SfxTemplateBuilder>(null)\n * <TemplateBuilder ref={builder} mode=\"modal\" … />\n * <button onClick={() => builder.current?.open('tpl-1')}>Edit</button>\n * ```\n *\n * `forwardRef` rather than a plain `ref` prop: React 19 accepts the latter for\n * function components, React 18 does not, and both are supported peers.\n */\nexport const TemplateBuilder = forwardRef<\n SfxTemplateBuilder,\n TemplateBuilderProps\n>(function TemplateBuilder(props, forwardedRef): ReactElement {\n const {\n className,\n style,\n onReady,\n onOpen,\n onClose,\n onSave,\n onError,\n onDirtyChange,\n ...config\n } = props\n const ref = useRef<SfxTemplateBuilder>(null)\n\n // Hand the same element out to the caller without giving up the internal ref\n // the effects below rely on.\n useImperativeHandle(forwardedRef, () => ref.current as SfxTemplateBuilder, [])\n\n // Assign config before paint so the iframe doesn't first mount with defaults.\n useLayoutEffect(() => {\n const el = ref.current\n if (!el) return\n el.baseUrl = config.baseUrl\n el.token = config.token\n el.sassKey = config.sassKey ?? ''\n el.sessionUuid = config.sessionUuid ?? ''\n el.secTemplate = config.secTemplate ?? ''\n el.companyUuid = config.companyUuid ?? ''\n el.projectUuid = config.projectUuid ?? ''\n el.templateId = config.templateId ?? ''\n el.mode = config.mode ?? 'inline'\n el.stateless = config.stateless ?? false\n el.templateName = config.templateName ?? ''\n el.templateQuery = config.templateQuery ?? ''\n el.brandColor = config.brandColor ?? ''\n el.theme = config.theme ?? ''\n el.newTemplate = config.newTemplate ?? false\n el.customMetadata = config.customMetadata ?? []\n el.customMetadataLabel = config.customMetadataLabel ?? ''\n el.damStore = config.damStore ?? false\n el.storeFolder = config.storeFolder ?? '/'\n el.storedUuid = config.storedUuid ?? ''\n // Assigned last: the element sends content to the app as soon as it has\n // both a request and a value, so the id, name and query must already be\n // set — all four ship as one message.\n el.content = config.content ?? ''\n if (config.readyTimeout !== undefined) el.readyTimeout = config.readyTimeout\n }, [\n config.baseUrl,\n config.token,\n config.sassKey,\n config.sessionUuid,\n config.secTemplate,\n config.companyUuid,\n config.projectUuid,\n config.templateId,\n config.mode,\n config.stateless,\n config.content,\n config.newTemplate,\n config.templateName,\n config.templateQuery,\n config.brandColor,\n config.theme,\n config.customMetadata,\n config.customMetadataLabel,\n config.damStore,\n config.storeFolder,\n config.storedUuid,\n config.readyTimeout,\n ])\n\n // The callbacks the listeners read at event time. A ref rather than effect\n // dependencies: listeners are attached once per element (below), so a parent\n // re-render swapping handler identities costs nothing — and, decisively, the\n // listeners are still attached during the element's disconnect-time flush of\n // pending dam-store saves, which a resubscribe-per-change cleanup would have\n // already torn down.\n const handlers = useRef({\n onReady,\n onOpen,\n onClose,\n onSave,\n onError,\n onDirtyChange,\n })\n useLayoutEffect(() => {\n handlers.current = { onReady, onOpen, onClose, onSave, onError, onDirtyChange }\n })\n\n // Layout effect, not passive: the element reports config errors (e.g.\n // `invalid-base-url`) in a microtask queued during this same commit, and a\n // passive effect would subscribe only after that microtask has fired —\n // making a mount-time error unobservable from React. This effect is declared\n // after the config one, so it still runs once the config is assigned.\n useLayoutEffect(() => {\n const el = ref.current\n if (!el) return\n const subs: Array<[string, EventListener]> = []\n const on = (\n name: string,\n pick: (h: typeof handlers.current) => ((detail: never) => void) | undefined,\n ) => {\n const listener = ((e: CustomEvent) =>\n pick(handlers.current)?.(e.detail as never)) as EventListener\n el.addEventListener(name, listener)\n subs.push([name, listener])\n }\n on('ready', (h) => h.onReady)\n on('open', (h) => h.onOpen)\n on('close', (h) => h.onClose)\n on('error', (h) => h.onError)\n on('dirtychange', (h) => h.onDirtyChange)\n\n // `save` is not just re-emitted: in stateless mode the handler's outcome\n // is acked back, so a failed write on the host side doesn't leave the\n // editor showing the template as saved. `confirmSave` no-ops in DAM mode.\n const saveListener = ((e: CustomEvent<TemplateBuilderSaveDetail>) => {\n const onSaveNow = handlers.current.onSave\n if (!onSaveNow) return\n // Wrapped in a promise so a synchronous throw is handled like a\n // rejection, and a sync `false` like a resolved one.\n Promise.resolve()\n .then(() => onSaveNow(e.detail))\n .then((result) => el.confirmSave(result !== false))\n .catch((err) => {\n // No message: an internal error string is not something to put in\n // front of the end user. The editor uses its own wording.\n console.error('[sfx-template-builder] onSave failed:', err)\n el.confirmSave(false)\n })\n }) as EventListener\n el.addEventListener('save', saveListener)\n subs.push(['save', saveListener])\n\n return () => {\n // React runs this cleanup BEFORE it detaches the node, so the element's\n // own disconnect-time flush of in-flight dam-store saves would fire\n // after every listener is gone — and the raw save would be silently\n // lost. Flushing here, while the listeners are still attached, hands\n // those saves (with `storeError` in place of the links) to `onSave`\n // first. A no-op when nothing is pending, including StrictMode's\n // simulated unmount at mount time.\n //\n // Guarded: when an older CDN bundle registered the tag first, `el` is\n // that bundle's class and lacks the method — every other new-API use\n // degrades silently via property assignment, and unmount must not be\n // the one path that throws.\n if (typeof el.flushPendingSaves === 'function') {\n el.flushPendingSaves('widget removed before the rendering copy completed')\n }\n for (const [name, listener] of subs) el.removeEventListener(name, listener)\n }\n }, [])\n\n // eslint-disable-next-line react-hooks/refs -- ref is forwarded as a prop, not read during render\n return createElement('sfx-template-builder', { ref, class: className, style })\n})\n"],"names":["TemplateBuilder","forwardRef","props","forwardedRef","className","style","onReady","onOpen","onClose","onSave","onError","onDirtyChange","config","ref","useRef","useImperativeHandle","useLayoutEffect","el","handlers","subs","on","name","pick","listener","e","h","saveListener","onSaveNow","result","err","createElement"],"mappings":"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"}
|
package/dist/react.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { type CSSProperties } from 'react';
|
|
|
2
2
|
import './define';
|
|
3
3
|
import type { SfxTemplateBuilder } from './template-builder';
|
|
4
4
|
import type { TemplateBuilderSaveDetail } from './template-builder';
|
|
5
|
-
import type { BuilderDirtyData, BuilderErrorData, BuilderTheme } from './protocol';
|
|
5
|
+
import type { BuilderDirtyData, BuilderErrorData, BuilderTheme, CustomMetadataField } from './protocol';
|
|
6
6
|
/**
|
|
7
7
|
* Hub session — the full-featured credential.
|
|
8
8
|
*/
|
|
@@ -39,6 +39,13 @@ export interface TemplateBuilderBaseProps {
|
|
|
39
39
|
stateless?: boolean;
|
|
40
40
|
/** Stateless mode: the template to edit, as `.fdt` XML. */
|
|
41
41
|
content?: string;
|
|
42
|
+
/**
|
|
43
|
+
* Stateless mode: open on a new, empty template instead of supplying
|
|
44
|
+
* `content`. The widget provides the blank document, the user picks the
|
|
45
|
+
* canvas size in the editor, and `onSave` receives a complete `.fdt` to
|
|
46
|
+
* store. Ignored when `content` is set.
|
|
47
|
+
*/
|
|
48
|
+
newTemplate?: boolean;
|
|
42
49
|
/** Stateless mode: display name for the editor header. */
|
|
43
50
|
templateName?: string;
|
|
44
51
|
/**
|
|
@@ -51,6 +58,39 @@ export interface TemplateBuilderBaseProps {
|
|
|
51
58
|
brandColor?: string;
|
|
52
59
|
/** Colour scheme for the editor chrome. */
|
|
53
60
|
theme?: BuilderTheme;
|
|
61
|
+
/**
|
|
62
|
+
* Metadata model offered in the editor as the "Custom metadata" value source
|
|
63
|
+
* — names only, no values. Omit it and the source is not offered.
|
|
64
|
+
*
|
|
65
|
+
* Compared by identity, like every other prop here, so a freshly built array
|
|
66
|
+
* counts as a change. Nothing is re-sent to the editor over it — the element
|
|
67
|
+
* de-dupes by value — but hoisting or memoising the array avoids the churn.
|
|
68
|
+
*/
|
|
69
|
+
customMetadata?: CustomMetadataField[];
|
|
70
|
+
/**
|
|
71
|
+
* Display name for the custom-metadata value source in the editor's UI —
|
|
72
|
+
* e.g. "External metadata". Wording only: the stored template is unaffected.
|
|
73
|
+
* Empty uses the editor's default, "Custom metadata".
|
|
74
|
+
*/
|
|
75
|
+
customMetadataLabel?: string;
|
|
76
|
+
/**
|
|
77
|
+
* Stateless only: store each save in Filerobot too, so the CDN can render
|
|
78
|
+
* it. `onSave`'s detail then carries `stored: { uuid, url }` next to the raw
|
|
79
|
+
* `content` — or `storeError` when the copy failed.
|
|
80
|
+
*/
|
|
81
|
+
damStore?: boolean;
|
|
82
|
+
/**
|
|
83
|
+
* Folder new templates land in under `damStore` when the template id names
|
|
84
|
+
* no existing DAM file (an existing file's own folder always wins).
|
|
85
|
+
*/
|
|
86
|
+
storeFolder?: string;
|
|
87
|
+
/**
|
|
88
|
+
* `damStore`: the `stored.uuid` a previous session's save reported for THIS
|
|
89
|
+
* document, so re-saves after a reload resolve to (and version) the copy
|
|
90
|
+
* that already exists instead of erroring on unchanged content or starting
|
|
91
|
+
* a fresh file. Per-document — pass it with the content it belongs to.
|
|
92
|
+
*/
|
|
93
|
+
storedUuid?: string;
|
|
54
94
|
readyTimeout?: number;
|
|
55
95
|
className?: string;
|
|
56
96
|
style?: CSSProperties;
|
package/dist/react.js
CHANGED
|
@@ -1,20 +1,20 @@
|
|
|
1
|
-
import { forwardRef as
|
|
1
|
+
import { forwardRef as L, useRef as h, useImperativeHandle as C, useLayoutEffect as i, createElement as E } from "react";
|
|
2
2
|
import "./define.js";
|
|
3
|
-
const
|
|
3
|
+
const I = L(function(U, b) {
|
|
4
4
|
const {
|
|
5
|
-
className:
|
|
6
|
-
style:
|
|
5
|
+
className: T,
|
|
6
|
+
style: S,
|
|
7
7
|
onReady: m,
|
|
8
8
|
onOpen: c,
|
|
9
|
-
onClose:
|
|
10
|
-
onSave:
|
|
11
|
-
onError:
|
|
12
|
-
onDirtyChange:
|
|
9
|
+
onClose: u,
|
|
10
|
+
onSave: p,
|
|
11
|
+
onError: f,
|
|
12
|
+
onDirtyChange: y,
|
|
13
13
|
...e
|
|
14
|
-
} =
|
|
15
|
-
|
|
16
|
-
const t =
|
|
17
|
-
t && (t.baseUrl = e.baseUrl, t.token = e.token, t.sassKey = e.sassKey ?? "", t.sessionUuid = e.sessionUuid ?? "", t.secTemplate = e.secTemplate ?? "", t.companyUuid = e.companyUuid ?? "", t.projectUuid = e.projectUuid ?? "", t.templateId = e.templateId ?? "", t.mode = e.mode ?? "inline", t.stateless = e.stateless ?? !1, t.templateName = e.templateName ?? "", t.templateQuery = e.templateQuery ?? "", t.brandColor = e.brandColor ?? "", t.theme = e.theme ?? "", t.content = e.content ?? "", e.readyTimeout !== void 0 && (t.readyTimeout = e.readyTimeout));
|
|
14
|
+
} = U, s = h(null);
|
|
15
|
+
C(b, () => s.current, []), i(() => {
|
|
16
|
+
const t = s.current;
|
|
17
|
+
t && (t.baseUrl = e.baseUrl, t.token = e.token, t.sassKey = e.sassKey ?? "", t.sessionUuid = e.sessionUuid ?? "", t.secTemplate = e.secTemplate ?? "", t.companyUuid = e.companyUuid ?? "", t.projectUuid = e.projectUuid ?? "", t.templateId = e.templateId ?? "", t.mode = e.mode ?? "inline", t.stateless = e.stateless ?? !1, t.templateName = e.templateName ?? "", t.templateQuery = e.templateQuery ?? "", t.brandColor = e.brandColor ?? "", t.theme = e.theme ?? "", t.newTemplate = e.newTemplate ?? !1, t.customMetadata = e.customMetadata ?? [], t.customMetadataLabel = e.customMetadataLabel ?? "", t.damStore = e.damStore ?? !1, t.storeFolder = e.storeFolder ?? "/", t.storedUuid = e.storedUuid ?? "", t.content = e.content ?? "", e.readyTimeout !== void 0 && (t.readyTimeout = e.readyTimeout));
|
|
18
18
|
}, [
|
|
19
19
|
e.baseUrl,
|
|
20
20
|
e.token,
|
|
@@ -27,33 +27,49 @@ const g = T(function(y, v) {
|
|
|
27
27
|
e.mode,
|
|
28
28
|
e.stateless,
|
|
29
29
|
e.content,
|
|
30
|
+
e.newTemplate,
|
|
30
31
|
e.templateName,
|
|
31
32
|
e.templateQuery,
|
|
32
33
|
e.brandColor,
|
|
33
34
|
e.theme,
|
|
35
|
+
e.customMetadata,
|
|
36
|
+
e.customMetadataLabel,
|
|
37
|
+
e.damStore,
|
|
38
|
+
e.storeFolder,
|
|
39
|
+
e.storedUuid,
|
|
34
40
|
e.readyTimeout
|
|
35
|
-
])
|
|
36
|
-
|
|
41
|
+
]);
|
|
42
|
+
const l = h({
|
|
43
|
+
onReady: m,
|
|
44
|
+
onOpen: c,
|
|
45
|
+
onClose: u,
|
|
46
|
+
onSave: p,
|
|
47
|
+
onError: f,
|
|
48
|
+
onDirtyChange: y
|
|
49
|
+
});
|
|
50
|
+
return i(() => {
|
|
51
|
+
l.current = { onReady: m, onOpen: c, onClose: u, onSave: p, onError: f, onDirtyChange: y };
|
|
52
|
+
}), i(() => {
|
|
53
|
+
const t = s.current;
|
|
37
54
|
if (!t) return;
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
t.addEventListener(s, o), i.push([s, o]);
|
|
55
|
+
const d = [], n = (o, a) => {
|
|
56
|
+
const r = ((g) => a(l.current)?.(g.detail));
|
|
57
|
+
t.addEventListener(o, r), d.push([o, r]);
|
|
42
58
|
};
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
59
|
+
n("ready", (o) => o.onReady), n("open", (o) => o.onOpen), n("close", (o) => o.onClose), n("error", (o) => o.onError), n("dirtychange", (o) => o.onDirtyChange);
|
|
60
|
+
const v = ((o) => {
|
|
61
|
+
const a = l.current.onSave;
|
|
62
|
+
a && Promise.resolve().then(() => a(o.detail)).then((r) => t.confirmSave(r !== !1)).catch((r) => {
|
|
63
|
+
console.error("[sfx-template-builder] onSave failed:", r), t.confirmSave(!1);
|
|
48
64
|
});
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
for (const [
|
|
65
|
+
});
|
|
66
|
+
return t.addEventListener("save", v), d.push(["save", v]), () => {
|
|
67
|
+
typeof t.flushPendingSaves == "function" && t.flushPendingSaves("widget removed before the rendering copy completed");
|
|
68
|
+
for (const [o, a] of d) t.removeEventListener(o, a);
|
|
53
69
|
};
|
|
54
|
-
}, [
|
|
70
|
+
}, []), E("sfx-template-builder", { ref: s, class: T, style: S });
|
|
55
71
|
});
|
|
56
72
|
export {
|
|
57
|
-
|
|
73
|
+
I as TemplateBuilder
|
|
58
74
|
};
|
|
59
75
|
//# sourceMappingURL=react.js.map
|
package/dist/react.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"react.js","sources":["../src/react.ts"],"sourcesContent":["import {\n createElement,\n forwardRef,\n useImperativeHandle,\n useLayoutEffect,\n useRef,\n type CSSProperties,\n type ReactElement,\n} from 'react'\nimport './define'\nimport type { SfxTemplateBuilder } from './template-builder'\nimport type { TemplateBuilderSaveDetail } from './template-builder'\nimport type { BuilderDirtyData, BuilderErrorData, BuilderTheme } from './protocol'\n\n/**\n * Hub session — the full-featured credential.\n */\nexport interface TemplateBuilderSessionAuth {\n sassKey: string\n sessionUuid: string\n companyUuid?: string\n projectUuid?: string\n secTemplate?: never\n}\n\n/**\n * Filerobot security template — a guest credential for hosts that have no Hub\n * session to hand over. `stateless` is required rather than merely implied:\n * the app takes a security template on the stateless embed route only, so the\n * combination is a compile-time error instead of a runtime one.\n *\n * `companyUuid` / `projectUuid` are absent by design — they name a Hub project\n * that cannot be looked up without a session.\n */\nexport interface TemplateBuilderSecTemplateAuth {\n secTemplate: string\n stateless: true\n sassKey?: never\n sessionUuid?: never\n companyUuid?: never\n projectUuid?: never\n}\n\nexport interface TemplateBuilderBaseProps {\n baseUrl: string\n token: string\n templateId?: string\n mode?: 'inline' | 'modal'\n /** Hand the template in and take it back out instead of using the DAM. */\n stateless?: boolean\n /** Stateless mode: the template to edit, as `.fdt` XML. */\n content?: string\n /** Stateless mode: display name for the editor header. */\n templateName?: string\n /**\n * Stateless mode: the `template_query` to open on — the value handed back on\n * save. Reopens the template on the same layout and variable values; empty\n * falls back to the XML's own `default=` attributes.\n */\n templateQuery?: string\n /** Accent colour for the editor chrome, as `#rgb` / `#rrggbb`. */\n brandColor?: string\n /** Colour scheme for the editor chrome. */\n theme?: BuilderTheme\n readyTimeout?: number\n className?: string\n style?: CSSProperties\n onReady?: () => void\n onOpen?: () => void\n onClose?: () => void\n /**\n * Fired on save. In stateless mode the detail carries the edited `content`\n * for you to persist; otherwise it reports the uuid the app uploaded to.\n *\n * In stateless mode the outcome is reported back to the editor: return (or\n * resolve to) `false`, or throw, and the editor restores its unsaved-changes\n * flag and tells the user the save failed. Anything else counts as persisted.\n */\n onSave?: (\n data: TemplateBuilderSaveDetail,\n ) => void | boolean | Promise<void | boolean>\n onError?: (data: BuilderErrorData) => void\n /**\n * Stateless mode: unsaved-changes flag changed. Use it to prompt before\n * swapping `content`, which discards in-progress edits.\n */\n onDirtyChange?: (data: BuilderDirtyData) => void\n}\n\nexport type TemplateBuilderProps = TemplateBuilderBaseProps &\n (TemplateBuilderSessionAuth | TemplateBuilderSecTemplateAuth)\n\n/**\n * React wrapper around `<sfx-template-builder>`. Props are assigned as\n * element properties via ref (works on React 18 and 19 alike); callbacks\n * subscribe to the element's CustomEvents.\n *\n * Forwards a ref to the underlying element, which is the only way to reach the\n * imperative API — `open()` in particular, without which `mode=\"modal\"` can\n * never be shown:\n *\n * ```tsx\n * const builder = useRef<SfxTemplateBuilder>(null)\n * <TemplateBuilder ref={builder} mode=\"modal\" … />\n * <button onClick={() => builder.current?.open('tpl-1')}>Edit</button>\n * ```\n *\n * `forwardRef` rather than a plain `ref` prop: React 19 accepts the latter for\n * function components, React 18 does not, and both are supported peers.\n */\nexport const TemplateBuilder = forwardRef<\n SfxTemplateBuilder,\n TemplateBuilderProps\n>(function TemplateBuilder(props, forwardedRef): ReactElement {\n const {\n className,\n style,\n onReady,\n onOpen,\n onClose,\n onSave,\n onError,\n onDirtyChange,\n ...config\n } = props\n const ref = useRef<SfxTemplateBuilder>(null)\n\n // Hand the same element out to the caller without giving up the internal ref\n // the effects below rely on.\n useImperativeHandle(forwardedRef, () => ref.current as SfxTemplateBuilder, [])\n\n // Assign config before paint so the iframe doesn't first mount with defaults.\n useLayoutEffect(() => {\n const el = ref.current\n if (!el) return\n el.baseUrl = config.baseUrl\n el.token = config.token\n el.sassKey = config.sassKey ?? ''\n el.sessionUuid = config.sessionUuid ?? ''\n el.secTemplate = config.secTemplate ?? ''\n el.companyUuid = config.companyUuid ?? ''\n el.projectUuid = config.projectUuid ?? ''\n el.templateId = config.templateId ?? ''\n el.mode = config.mode ?? 'inline'\n el.stateless = config.stateless ?? false\n el.templateName = config.templateName ?? ''\n el.templateQuery = config.templateQuery ?? ''\n el.brandColor = config.brandColor ?? ''\n el.theme = config.theme ?? ''\n // Assigned last: the element sends content to the app as soon as it has\n // both a request and a value, so the id, name and query must already be\n // set — all four ship as one message.\n el.content = config.content ?? ''\n if (config.readyTimeout !== undefined) el.readyTimeout = config.readyTimeout\n }, [\n config.baseUrl,\n config.token,\n config.sassKey,\n config.sessionUuid,\n config.secTemplate,\n config.companyUuid,\n config.projectUuid,\n config.templateId,\n config.mode,\n config.stateless,\n config.content,\n config.templateName,\n config.templateQuery,\n config.brandColor,\n config.theme,\n config.readyTimeout,\n ])\n\n // Layout effect, not passive: the element reports config errors (e.g.\n // `invalid-base-url`) in a microtask queued during this same commit, and a\n // passive effect would subscribe only after that microtask has fired —\n // making a mount-time error unobservable from React. This effect is declared\n // after the config one, so it still runs once the config is assigned.\n useLayoutEffect(() => {\n const el = ref.current\n if (!el) return\n const subs: Array<[string, EventListener]> = []\n const on = (name: string, handler?: (detail: never) => void) => {\n if (!handler) return\n const listener = ((e: CustomEvent) => handler(e.detail as never)) as EventListener\n el.addEventListener(name, listener)\n subs.push([name, listener])\n }\n on('ready', onReady)\n on('open', onOpen)\n on('close', onClose)\n on('error', onError)\n on('dirtychange', onDirtyChange)\n\n // `save` is not just re-emitted: in stateless mode the handler's outcome\n // is acked back, so a failed write on the host side doesn't leave the\n // editor showing the template as saved. `confirmSave` no-ops in DAM mode.\n if (onSave) {\n const listener = ((e: CustomEvent<TemplateBuilderSaveDetail>) => {\n // Wrapped in a promise so a synchronous throw is handled like a\n // rejection, and a sync `false` like a resolved one.\n Promise.resolve()\n .then(() => onSave(e.detail))\n .then((result) => el.confirmSave(result !== false))\n .catch((err) => {\n // No message: an internal error string is not something to put in\n // front of the end user. The editor uses its own wording.\n console.error('[sfx-template-builder] onSave failed:', err)\n el.confirmSave(false)\n })\n }) as EventListener\n el.addEventListener('save', listener)\n subs.push(['save', listener])\n }\n\n return () => {\n for (const [name, listener] of subs) el.removeEventListener(name, listener)\n }\n }, [onReady, onOpen, onClose, onSave, onError, onDirtyChange])\n\n // eslint-disable-next-line react-hooks/refs -- ref is forwarded as a prop, not read during render\n return createElement('sfx-template-builder', { ref, class: className, style })\n})\n"],"names":["TemplateBuilder","forwardRef","props","forwardedRef","className","style","onReady","onOpen","onClose","onSave","onError","onDirtyChange","config","ref","useRef","useImperativeHandle","useLayoutEffect","el","subs","on","name","handler","listener","e","result","err","createElement"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"react.js","sources":["../src/react.ts"],"sourcesContent":["import {\n createElement,\n forwardRef,\n useImperativeHandle,\n useLayoutEffect,\n useRef,\n type CSSProperties,\n type ReactElement,\n} from 'react'\nimport './define'\nimport type { SfxTemplateBuilder } from './template-builder'\nimport type { TemplateBuilderSaveDetail } from './template-builder'\nimport type {\n BuilderDirtyData,\n BuilderErrorData,\n BuilderTheme,\n CustomMetadataField,\n} from './protocol'\n\n/**\n * Hub session — the full-featured credential.\n */\nexport interface TemplateBuilderSessionAuth {\n sassKey: string\n sessionUuid: string\n companyUuid?: string\n projectUuid?: string\n secTemplate?: never\n}\n\n/**\n * Filerobot security template — a guest credential for hosts that have no Hub\n * session to hand over. `stateless` is required rather than merely implied:\n * the app takes a security template on the stateless embed route only, so the\n * combination is a compile-time error instead of a runtime one.\n *\n * `companyUuid` / `projectUuid` are absent by design — they name a Hub project\n * that cannot be looked up without a session.\n */\nexport interface TemplateBuilderSecTemplateAuth {\n secTemplate: string\n stateless: true\n sassKey?: never\n sessionUuid?: never\n companyUuid?: never\n projectUuid?: never\n}\n\nexport interface TemplateBuilderBaseProps {\n baseUrl: string\n token: string\n templateId?: string\n mode?: 'inline' | 'modal'\n /** Hand the template in and take it back out instead of using the DAM. */\n stateless?: boolean\n /** Stateless mode: the template to edit, as `.fdt` XML. */\n content?: string\n /**\n * Stateless mode: open on a new, empty template instead of supplying\n * `content`. The widget provides the blank document, the user picks the\n * canvas size in the editor, and `onSave` receives a complete `.fdt` to\n * store. Ignored when `content` is set.\n */\n newTemplate?: boolean\n /** Stateless mode: display name for the editor header. */\n templateName?: string\n /**\n * Stateless mode: the `template_query` to open on — the value handed back on\n * save. Reopens the template on the same layout and variable values; empty\n * falls back to the XML's own `default=` attributes.\n */\n templateQuery?: string\n /** Accent colour for the editor chrome, as `#rgb` / `#rrggbb`. */\n brandColor?: string\n /** Colour scheme for the editor chrome. */\n theme?: BuilderTheme\n /**\n * Metadata model offered in the editor as the \"Custom metadata\" value source\n * — names only, no values. Omit it and the source is not offered.\n *\n * Compared by identity, like every other prop here, so a freshly built array\n * counts as a change. Nothing is re-sent to the editor over it — the element\n * de-dupes by value — but hoisting or memoising the array avoids the churn.\n */\n customMetadata?: CustomMetadataField[]\n /**\n * Display name for the custom-metadata value source in the editor's UI —\n * e.g. \"External metadata\". Wording only: the stored template is unaffected.\n * Empty uses the editor's default, \"Custom metadata\".\n */\n customMetadataLabel?: string\n /**\n * Stateless only: store each save in Filerobot too, so the CDN can render\n * it. `onSave`'s detail then carries `stored: { uuid, url }` next to the raw\n * `content` — or `storeError` when the copy failed.\n */\n damStore?: boolean\n /**\n * Folder new templates land in under `damStore` when the template id names\n * no existing DAM file (an existing file's own folder always wins).\n */\n storeFolder?: string\n /**\n * `damStore`: the `stored.uuid` a previous session's save reported for THIS\n * document, so re-saves after a reload resolve to (and version) the copy\n * that already exists instead of erroring on unchanged content or starting\n * a fresh file. Per-document — pass it with the content it belongs to.\n */\n storedUuid?: string\n readyTimeout?: number\n className?: string\n style?: CSSProperties\n onReady?: () => void\n onOpen?: () => void\n onClose?: () => void\n /**\n * Fired on save. In stateless mode the detail carries the edited `content`\n * for you to persist; otherwise it reports the uuid the app uploaded to.\n *\n * In stateless mode the outcome is reported back to the editor: return (or\n * resolve to) `false`, or throw, and the editor restores its unsaved-changes\n * flag and tells the user the save failed. Anything else counts as persisted.\n */\n onSave?: (\n data: TemplateBuilderSaveDetail,\n ) => void | boolean | Promise<void | boolean>\n onError?: (data: BuilderErrorData) => void\n /**\n * Stateless mode: unsaved-changes flag changed. Use it to prompt before\n * swapping `content`, which discards in-progress edits.\n */\n onDirtyChange?: (data: BuilderDirtyData) => void\n}\n\nexport type TemplateBuilderProps = TemplateBuilderBaseProps &\n (TemplateBuilderSessionAuth | TemplateBuilderSecTemplateAuth)\n\n/**\n * React wrapper around `<sfx-template-builder>`. Props are assigned as\n * element properties via ref (works on React 18 and 19 alike); callbacks\n * subscribe to the element's CustomEvents.\n *\n * Forwards a ref to the underlying element, which is the only way to reach the\n * imperative API — `open()` in particular, without which `mode=\"modal\"` can\n * never be shown:\n *\n * ```tsx\n * const builder = useRef<SfxTemplateBuilder>(null)\n * <TemplateBuilder ref={builder} mode=\"modal\" … />\n * <button onClick={() => builder.current?.open('tpl-1')}>Edit</button>\n * ```\n *\n * `forwardRef` rather than a plain `ref` prop: React 19 accepts the latter for\n * function components, React 18 does not, and both are supported peers.\n */\nexport const TemplateBuilder = forwardRef<\n SfxTemplateBuilder,\n TemplateBuilderProps\n>(function TemplateBuilder(props, forwardedRef): ReactElement {\n const {\n className,\n style,\n onReady,\n onOpen,\n onClose,\n onSave,\n onError,\n onDirtyChange,\n ...config\n } = props\n const ref = useRef<SfxTemplateBuilder>(null)\n\n // Hand the same element out to the caller without giving up the internal ref\n // the effects below rely on.\n useImperativeHandle(forwardedRef, () => ref.current as SfxTemplateBuilder, [])\n\n // Assign config before paint so the iframe doesn't first mount with defaults.\n useLayoutEffect(() => {\n const el = ref.current\n if (!el) return\n el.baseUrl = config.baseUrl\n el.token = config.token\n el.sassKey = config.sassKey ?? ''\n el.sessionUuid = config.sessionUuid ?? ''\n el.secTemplate = config.secTemplate ?? ''\n el.companyUuid = config.companyUuid ?? ''\n el.projectUuid = config.projectUuid ?? ''\n el.templateId = config.templateId ?? ''\n el.mode = config.mode ?? 'inline'\n el.stateless = config.stateless ?? false\n el.templateName = config.templateName ?? ''\n el.templateQuery = config.templateQuery ?? ''\n el.brandColor = config.brandColor ?? ''\n el.theme = config.theme ?? ''\n el.newTemplate = config.newTemplate ?? false\n el.customMetadata = config.customMetadata ?? []\n el.customMetadataLabel = config.customMetadataLabel ?? ''\n el.damStore = config.damStore ?? false\n el.storeFolder = config.storeFolder ?? '/'\n el.storedUuid = config.storedUuid ?? ''\n // Assigned last: the element sends content to the app as soon as it has\n // both a request and a value, so the id, name and query must already be\n // set — all four ship as one message.\n el.content = config.content ?? ''\n if (config.readyTimeout !== undefined) el.readyTimeout = config.readyTimeout\n }, [\n config.baseUrl,\n config.token,\n config.sassKey,\n config.sessionUuid,\n config.secTemplate,\n config.companyUuid,\n config.projectUuid,\n config.templateId,\n config.mode,\n config.stateless,\n config.content,\n config.newTemplate,\n config.templateName,\n config.templateQuery,\n config.brandColor,\n config.theme,\n config.customMetadata,\n config.customMetadataLabel,\n config.damStore,\n config.storeFolder,\n config.storedUuid,\n config.readyTimeout,\n ])\n\n // The callbacks the listeners read at event time. A ref rather than effect\n // dependencies: listeners are attached once per element (below), so a parent\n // re-render swapping handler identities costs nothing — and, decisively, the\n // listeners are still attached during the element's disconnect-time flush of\n // pending dam-store saves, which a resubscribe-per-change cleanup would have\n // already torn down.\n const handlers = useRef({\n onReady,\n onOpen,\n onClose,\n onSave,\n onError,\n onDirtyChange,\n })\n useLayoutEffect(() => {\n handlers.current = { onReady, onOpen, onClose, onSave, onError, onDirtyChange }\n })\n\n // Layout effect, not passive: the element reports config errors (e.g.\n // `invalid-base-url`) in a microtask queued during this same commit, and a\n // passive effect would subscribe only after that microtask has fired —\n // making a mount-time error unobservable from React. This effect is declared\n // after the config one, so it still runs once the config is assigned.\n useLayoutEffect(() => {\n const el = ref.current\n if (!el) return\n const subs: Array<[string, EventListener]> = []\n const on = (\n name: string,\n pick: (h: typeof handlers.current) => ((detail: never) => void) | undefined,\n ) => {\n const listener = ((e: CustomEvent) =>\n pick(handlers.current)?.(e.detail as never)) as EventListener\n el.addEventListener(name, listener)\n subs.push([name, listener])\n }\n on('ready', (h) => h.onReady)\n on('open', (h) => h.onOpen)\n on('close', (h) => h.onClose)\n on('error', (h) => h.onError)\n on('dirtychange', (h) => h.onDirtyChange)\n\n // `save` is not just re-emitted: in stateless mode the handler's outcome\n // is acked back, so a failed write on the host side doesn't leave the\n // editor showing the template as saved. `confirmSave` no-ops in DAM mode.\n const saveListener = ((e: CustomEvent<TemplateBuilderSaveDetail>) => {\n const onSaveNow = handlers.current.onSave\n if (!onSaveNow) return\n // Wrapped in a promise so a synchronous throw is handled like a\n // rejection, and a sync `false` like a resolved one.\n Promise.resolve()\n .then(() => onSaveNow(e.detail))\n .then((result) => el.confirmSave(result !== false))\n .catch((err) => {\n // No message: an internal error string is not something to put in\n // front of the end user. The editor uses its own wording.\n console.error('[sfx-template-builder] onSave failed:', err)\n el.confirmSave(false)\n })\n }) as EventListener\n el.addEventListener('save', saveListener)\n subs.push(['save', saveListener])\n\n return () => {\n // React runs this cleanup BEFORE it detaches the node, so the element's\n // own disconnect-time flush of in-flight dam-store saves would fire\n // after every listener is gone — and the raw save would be silently\n // lost. Flushing here, while the listeners are still attached, hands\n // those saves (with `storeError` in place of the links) to `onSave`\n // first. A no-op when nothing is pending, including StrictMode's\n // simulated unmount at mount time.\n //\n // Guarded: when an older CDN bundle registered the tag first, `el` is\n // that bundle's class and lacks the method — every other new-API use\n // degrades silently via property assignment, and unmount must not be\n // the one path that throws.\n if (typeof el.flushPendingSaves === 'function') {\n el.flushPendingSaves('widget removed before the rendering copy completed')\n }\n for (const [name, listener] of subs) el.removeEventListener(name, listener)\n }\n }, [])\n\n // eslint-disable-next-line react-hooks/refs -- ref is forwarded as a prop, not read during render\n return createElement('sfx-template-builder', { ref, class: className, style })\n})\n"],"names":["TemplateBuilder","forwardRef","props","forwardedRef","className","style","onReady","onOpen","onClose","onSave","onError","onDirtyChange","config","ref","useRef","useImperativeHandle","useLayoutEffect","el","handlers","subs","on","name","pick","listener","e","h","saveListener","onSaveNow","result","err","createElement"],"mappings":";;AA2JO,MAAMA,IAAkBC,EAG7B,SAAyBC,GAAOC,GAA4B;AAC5D,QAAM;AAAA,IACJ,WAAAC;AAAA,IACA,OAAAC;AAAA,IACA,SAAAC;AAAA,IACA,QAAAC;AAAA,IACA,SAAAC;AAAA,IACA,QAAAC;AAAA,IACA,SAAAC;AAAA,IACA,eAAAC;AAAA,IACA,GAAGC;AAAA,EAAA,IACDV,GACEW,IAAMC,EAA2B,IAAI;AAI3C,EAAAC,EAAoBZ,GAAc,MAAMU,EAAI,SAA+B,CAAA,CAAE,GAG7EG,EAAgB,MAAM;AACpB,UAAMC,IAAKJ,EAAI;AACf,IAAKI,MACLA,EAAG,UAAUL,EAAO,SACpBK,EAAG,QAAQL,EAAO,OAClBK,EAAG,UAAUL,EAAO,WAAW,IAC/BK,EAAG,cAAcL,EAAO,eAAe,IACvCK,EAAG,cAAcL,EAAO,eAAe,IACvCK,EAAG,cAAcL,EAAO,eAAe,IACvCK,EAAG,cAAcL,EAAO,eAAe,IACvCK,EAAG,aAAaL,EAAO,cAAc,IACrCK,EAAG,OAAOL,EAAO,QAAQ,UACzBK,EAAG,YAAYL,EAAO,aAAa,IACnCK,EAAG,eAAeL,EAAO,gBAAgB,IACzCK,EAAG,gBAAgBL,EAAO,iBAAiB,IAC3CK,EAAG,aAAaL,EAAO,cAAc,IACrCK,EAAG,QAAQL,EAAO,SAAS,IAC3BK,EAAG,cAAcL,EAAO,eAAe,IACvCK,EAAG,iBAAiBL,EAAO,kBAAkB,CAAA,GAC7CK,EAAG,sBAAsBL,EAAO,uBAAuB,IACvDK,EAAG,WAAWL,EAAO,YAAY,IACjCK,EAAG,cAAcL,EAAO,eAAe,KACvCK,EAAG,aAAaL,EAAO,cAAc,IAIrCK,EAAG,UAAUL,EAAO,WAAW,IAC3BA,EAAO,iBAAiB,WAAWK,EAAG,eAAeL,EAAO;AAAA,EAClE,GAAG;AAAA,IACDA,EAAO;AAAA,IACPA,EAAO;AAAA,IACPA,EAAO;AAAA,IACPA,EAAO;AAAA,IACPA,EAAO;AAAA,IACPA,EAAO;AAAA,IACPA,EAAO;AAAA,IACPA,EAAO;AAAA,IACPA,EAAO;AAAA,IACPA,EAAO;AAAA,IACPA,EAAO;AAAA,IACPA,EAAO;AAAA,IACPA,EAAO;AAAA,IACPA,EAAO;AAAA,IACPA,EAAO;AAAA,IACPA,EAAO;AAAA,IACPA,EAAO;AAAA,IACPA,EAAO;AAAA,IACPA,EAAO;AAAA,IACPA,EAAO;AAAA,IACPA,EAAO;AAAA,IACPA,EAAO;AAAA,EAAA,CACR;AAQD,QAAMM,IAAWJ,EAAO;AAAA,IACtB,SAAAR;AAAA,IACA,QAAAC;AAAA,IACA,SAAAC;AAAA,IACA,QAAAC;AAAA,IACA,SAAAC;AAAA,IACA,eAAAC;AAAA,EAAA,CACD;AACD,SAAAK,EAAgB,MAAM;AACpB,IAAAE,EAAS,UAAU,EAAE,SAAAZ,GAAS,QAAAC,GAAQ,SAAAC,GAAS,QAAAC,GAAQ,SAAAC,GAAS,eAAAC,EAAA;AAAA,EAClE,CAAC,GAODK,EAAgB,MAAM;AACpB,UAAMC,IAAKJ,EAAI;AACf,QAAI,CAACI,EAAI;AACT,UAAME,IAAuC,CAAA,GACvCC,IAAK,CACTC,GACAC,MACG;AACH,YAAMC,KAAY,CAACC,MACjBF,EAAKJ,EAAS,OAAO,IAAIM,EAAE,MAAe;AAC5C,MAAAP,EAAG,iBAAiBI,GAAME,CAAQ,GAClCJ,EAAK,KAAK,CAACE,GAAME,CAAQ,CAAC;AAAA,IAC5B;AACA,IAAAH,EAAG,SAAS,CAACK,MAAMA,EAAE,OAAO,GAC5BL,EAAG,QAAQ,CAACK,MAAMA,EAAE,MAAM,GAC1BL,EAAG,SAAS,CAACK,MAAMA,EAAE,OAAO,GAC5BL,EAAG,SAAS,CAACK,MAAMA,EAAE,OAAO,GAC5BL,EAAG,eAAe,CAACK,MAAMA,EAAE,aAAa;AAKxC,UAAMC,KAAgB,CAACF,MAA8C;AACnE,YAAMG,IAAYT,EAAS,QAAQ;AACnC,MAAKS,KAGL,QAAQ,QAAA,EACL,KAAK,MAAMA,EAAUH,EAAE,MAAM,CAAC,EAC9B,KAAK,CAACI,MAAWX,EAAG,YAAYW,MAAW,EAAK,CAAC,EACjD,MAAM,CAACC,MAAQ;AAGd,gBAAQ,MAAM,yCAAyCA,CAAG,GAC1DZ,EAAG,YAAY,EAAK;AAAA,MACtB,CAAC;AAAA,IACL;AACA,WAAAA,EAAG,iBAAiB,QAAQS,CAAY,GACxCP,EAAK,KAAK,CAAC,QAAQO,CAAY,CAAC,GAEzB,MAAM;AAaX,MAAI,OAAOT,EAAG,qBAAsB,cAClCA,EAAG,kBAAkB,oDAAoD;AAE3E,iBAAW,CAACI,GAAME,CAAQ,KAAKJ,EAAM,CAAAF,EAAG,oBAAoBI,GAAME,CAAQ;AAAA,IAC5E;AAAA,EACF,GAAG,CAAA,CAAE,GAGEO,EAAc,wBAAwB,EAAE,KAAAjB,GAAK,OAAOT,GAAW,OAAAC,GAAO;AAC/E,CAAC;"}
|