@scaleflex/template-builder 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/skills/integrate-template-builder/SKILL.md +356 -0
- package/CHANGELOG.md +66 -0
- package/LICENSE +50 -0
- package/README.md +775 -0
- package/dist/define.cjs +2 -0
- package/dist/define.cjs.map +1 -0
- package/dist/define.d.ts +2 -0
- package/dist/define.js +6 -0
- package/dist/define.js.map +1 -0
- package/dist/index.cjs +2 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +21 -0
- package/dist/index.js.map +1 -0
- package/dist/protocol.d.ts +237 -0
- package/dist/react.cjs +2 -0
- package/dist/react.cjs.map +1 -0
- package/dist/react.d.ts +95 -0
- package/dist/react.js +59 -0
- package/dist/react.js.map +1 -0
- package/dist/template-builder-CSyPZni9.cjs +52 -0
- package/dist/template-builder-CSyPZni9.cjs.map +1 -0
- package/dist/template-builder-S33H_d5T.js +354 -0
- package/dist/template-builder-S33H_d5T.js.map +1 -0
- package/dist/template-builder.d.ts +206 -0
- package/package.json +75 -0
- package/src/define.ts +10 -0
- package/src/index.ts +9 -0
- package/src/protocol.ts +298 -0
- package/src/react.ts +223 -0
- package/src/template-builder.ts +599 -0
package/dist/define.cjs
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const e=require("./template-builder-CSyPZni9.cjs");typeof customElements<"u"&&!customElements.get("sfx-template-builder")&&customElements.define("sfx-template-builder",e.SfxTemplateBuilder);exports.SfxTemplateBuilder=e.SfxTemplateBuilder;
|
|
2
|
+
//# sourceMappingURL=define.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"define.cjs","sources":["../src/define.ts"],"sourcesContent":["import { SfxTemplateBuilder } from './template-builder'\n\nif (\n typeof customElements !== 'undefined' &&\n !customElements.get('sfx-template-builder')\n) {\n customElements.define('sfx-template-builder', SfxTemplateBuilder)\n}\n\nexport { SfxTemplateBuilder }\n"],"names":["SfxTemplateBuilder"],"mappings":"mIAGE,OAAO,eAAmB,KAC1B,CAAC,eAAe,IAAI,sBAAsB,GAE1C,eAAe,OAAO,uBAAwBA,oBAAkB"}
|
package/dist/define.d.ts
ADDED
package/dist/define.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"define.js","sources":["../src/define.ts"],"sourcesContent":["import { SfxTemplateBuilder } from './template-builder'\n\nif (\n typeof customElements !== 'undefined' &&\n !customElements.get('sfx-template-builder')\n) {\n customElements.define('sfx-template-builder', SfxTemplateBuilder)\n}\n\nexport { SfxTemplateBuilder }\n"],"names":["SfxTemplateBuilder"],"mappings":";AAGE,OAAO,iBAAmB,OAC1B,CAAC,eAAe,IAAI,sBAAsB,KAE1C,eAAe,OAAO,wBAAwBA,CAAkB;"}
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const E=require("./template-builder-CSyPZni9.cjs");exports.AUTH_MODES=E.AUTH_MODES;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_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
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.cjs","sources":[],"sourcesContent":[],"names":[],"mappings":""}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { A as a, B as s, a as _, b as D, c as O, d as T, e as B, f as L, g as S, h as U, E as A, i as I, H as N, j as e, P as C, S as P, k as r } from "./template-builder-S33H_d5T.js";
|
|
2
|
+
export {
|
|
3
|
+
a as AUTH_MODES,
|
|
4
|
+
s as BRAND_COLOR_PATTERN,
|
|
5
|
+
_ as BUILDER_CLOSE,
|
|
6
|
+
D as BUILDER_CONTENT,
|
|
7
|
+
O as BUILDER_CONTENT_REQUEST,
|
|
8
|
+
T as BUILDER_DIRTY,
|
|
9
|
+
B as BUILDER_ERROR,
|
|
10
|
+
L as BUILDER_OPEN,
|
|
11
|
+
S as BUILDER_READY,
|
|
12
|
+
U as BUILDER_SAVE,
|
|
13
|
+
A as EMBED_PARAMS,
|
|
14
|
+
I as EMBED_ROUTE,
|
|
15
|
+
N as HOST_LOAD,
|
|
16
|
+
e as HOST_SAVED,
|
|
17
|
+
C as PROTOCOL_VERSION,
|
|
18
|
+
P as SfxTemplateBuilder,
|
|
19
|
+
r as builderRoute
|
|
20
|
+
};
|
|
21
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":";"}
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
export declare const PROTOCOL_VERSION = 2;
|
|
2
|
+
/** Editor mounted with valid auth — the embed handshake succeeded. */
|
|
3
|
+
export declare const BUILDER_READY = "design-templates:builder:ready";
|
|
4
|
+
/** Editor UI opened (kept for Hub backwards compatibility; implies ready). */
|
|
5
|
+
export declare const BUILDER_OPEN = "design-templates:builder:open";
|
|
6
|
+
/** Editor UI closed / unmounted. */
|
|
7
|
+
export declare const BUILDER_CLOSE = "design-templates:builder:close";
|
|
8
|
+
/** Template saved. `data` is absent on app deployments older than protocol v1. */
|
|
9
|
+
export declare const BUILDER_SAVE = "design-templates:builder:save";
|
|
10
|
+
/** The app cannot start (e.g. auth cookies missing/blocked). */
|
|
11
|
+
export declare const BUILDER_ERROR = "design-templates:builder:error";
|
|
12
|
+
/**
|
|
13
|
+
* Stateless mode only (protocol v2). The editor mounted without a template and
|
|
14
|
+
* is waiting for the host to send `HOST_LOAD`. Re-sent on nothing — the host
|
|
15
|
+
* may answer late; the app keeps waiting until content arrives.
|
|
16
|
+
*/
|
|
17
|
+
export declare const BUILDER_CONTENT_REQUEST = "design-templates:builder:content-request";
|
|
18
|
+
/**
|
|
19
|
+
* Stateless mode only (protocol v2). The user saved and the app is handing the
|
|
20
|
+
* edited template back instead of uploading it. This is the stateless
|
|
21
|
+
* counterpart to `BUILDER_SAVE` — a separate message so that embedders written
|
|
22
|
+
* against v1 (which read `data.uuid` from a save they assume was persisted)
|
|
23
|
+
* never receive a payload that was not, in fact, stored anywhere.
|
|
24
|
+
*/
|
|
25
|
+
export declare const BUILDER_CONTENT = "design-templates:builder:content";
|
|
26
|
+
/**
|
|
27
|
+
* Stateless mode only (protocol v2). The unsaved-changes flag flipped. Sent so
|
|
28
|
+
* a host can ask the user before swapping the template out from under them —
|
|
29
|
+
* `HOST_LOAD` is honoured unconditionally and discards whatever was in
|
|
30
|
+
* progress, and without this the host has no way to know there was anything to
|
|
31
|
+
* lose.
|
|
32
|
+
*/
|
|
33
|
+
export declare const BUILDER_DIRTY = "design-templates:builder:dirty";
|
|
34
|
+
export interface BuilderReadyMessage {
|
|
35
|
+
type: typeof BUILDER_READY;
|
|
36
|
+
}
|
|
37
|
+
export interface BuilderOpenMessage {
|
|
38
|
+
type: typeof BUILDER_OPEN;
|
|
39
|
+
}
|
|
40
|
+
export interface BuilderCloseMessage {
|
|
41
|
+
type: typeof BUILDER_CLOSE;
|
|
42
|
+
}
|
|
43
|
+
export interface BuilderSaveData {
|
|
44
|
+
uuid: string;
|
|
45
|
+
name?: string;
|
|
46
|
+
}
|
|
47
|
+
export interface BuilderSaveMessage {
|
|
48
|
+
type: typeof BUILDER_SAVE;
|
|
49
|
+
data?: BuilderSaveData;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* `auth` and `invalid-content` are the codes the app itself sends. The widget
|
|
53
|
+
* adds `handshake-timeout` (no ready signal — typically blocked third-party
|
|
54
|
+
* cookies or a missing frame-ancestors entry), `invalid-base-url` and
|
|
55
|
+
* `invalid-config`.
|
|
56
|
+
*/
|
|
57
|
+
export type BuilderErrorCode =
|
|
58
|
+
/**
|
|
59
|
+
* The app could not authenticate. In `secTemplate` mode this also covers a
|
|
60
|
+
* security-template key the Filerobot API refused to exchange for a sass key.
|
|
61
|
+
*/
|
|
62
|
+
'auth'
|
|
63
|
+
/** Stateless mode: the `HOST_LOAD` content could not be parsed as a template. */
|
|
64
|
+
| 'invalid-content' | 'handshake-timeout' | 'invalid-base-url'
|
|
65
|
+
/** Attributes that contradict each other, e.g. `sec-template` without `stateless`. */
|
|
66
|
+
| 'invalid-config' | 'unknown';
|
|
67
|
+
export interface BuilderErrorData {
|
|
68
|
+
code: BuilderErrorCode;
|
|
69
|
+
message?: string;
|
|
70
|
+
}
|
|
71
|
+
export interface BuilderErrorMessage {
|
|
72
|
+
type: typeof BUILDER_ERROR;
|
|
73
|
+
data: BuilderErrorData;
|
|
74
|
+
}
|
|
75
|
+
export interface BuilderContentRequestMessage {
|
|
76
|
+
type: typeof BUILDER_CONTENT_REQUEST;
|
|
77
|
+
}
|
|
78
|
+
export interface BuilderContentData {
|
|
79
|
+
/**
|
|
80
|
+
* The id the host supplied in `HOST_LOAD`, echoed back verbatim. Absent when
|
|
81
|
+
* the host sent content without one.
|
|
82
|
+
*/
|
|
83
|
+
templateId?: string;
|
|
84
|
+
/** The edited template, serialized as `.fdt` XML. */
|
|
85
|
+
content: string;
|
|
86
|
+
/** Display name the host supplied, echoed back. */
|
|
87
|
+
name?: string;
|
|
88
|
+
/**
|
|
89
|
+
* `template_query` for the default render — layout, variable values and
|
|
90
|
+
* locale. In DAM-backed mode this is stored as file metadata; a stateless
|
|
91
|
+
* host must persist it alongside `content` or renders will fall back to
|
|
92
|
+
* whatever defaults the XML alone implies.
|
|
93
|
+
*/
|
|
94
|
+
templateQuery?: string;
|
|
95
|
+
}
|
|
96
|
+
export interface BuilderContentMessage {
|
|
97
|
+
type: typeof BUILDER_CONTENT;
|
|
98
|
+
data: BuilderContentData;
|
|
99
|
+
}
|
|
100
|
+
export interface BuilderDirtyData {
|
|
101
|
+
/** True when the editor holds edits that have not been handed back. */
|
|
102
|
+
isDirty: boolean;
|
|
103
|
+
}
|
|
104
|
+
export interface BuilderDirtyMessage {
|
|
105
|
+
type: typeof BUILDER_DIRTY;
|
|
106
|
+
data: BuilderDirtyData;
|
|
107
|
+
}
|
|
108
|
+
export type BuilderMessage = BuilderReadyMessage | BuilderOpenMessage | BuilderCloseMessage | BuilderSaveMessage | BuilderErrorMessage | BuilderContentRequestMessage | BuilderContentMessage | BuilderDirtyMessage;
|
|
109
|
+
/**
|
|
110
|
+
* Stateless mode only (protocol v2). Hands the app a template to edit. Sent in
|
|
111
|
+
* answer to `BUILDER_CONTENT_REQUEST`, and again whenever the host swaps the
|
|
112
|
+
* template without remounting the iframe.
|
|
113
|
+
*
|
|
114
|
+
* Content travels by postMessage rather than a URL param because template XML
|
|
115
|
+
* routinely exceeds practical URL length limits.
|
|
116
|
+
*
|
|
117
|
+
* The app accepts this message only from the origin pinned as `embedOrigin`
|
|
118
|
+
* when the session credentials were handed over, so an unrelated framing page
|
|
119
|
+
* cannot inject a template into someone else's session.
|
|
120
|
+
*/
|
|
121
|
+
export declare const HOST_LOAD = "design-templates:host:load";
|
|
122
|
+
export interface HostLoadData {
|
|
123
|
+
/**
|
|
124
|
+
* Opaque host-side identifier, echoed back on save. It is never used to
|
|
125
|
+
* fetch anything, so it need not be a Filerobot uuid — any string the host
|
|
126
|
+
* can map back to its own record works.
|
|
127
|
+
*/
|
|
128
|
+
templateId?: string;
|
|
129
|
+
/** Template to edit, as `.fdt` XML. */
|
|
130
|
+
content: string;
|
|
131
|
+
/** Display name for the editor header. */
|
|
132
|
+
name?: string;
|
|
133
|
+
/**
|
|
134
|
+
* `template_query` describing the render to open on — layout and variable
|
|
135
|
+
* values, in the same `$key=value&$key2=value2` form the editor hands back
|
|
136
|
+
* in `BuilderContentData.templateQuery`.
|
|
137
|
+
*
|
|
138
|
+
* Round-trips that value: a host that stored it on save and passes it back
|
|
139
|
+
* here reopens the template exactly as it was left. Omitting it falls back
|
|
140
|
+
* to the `default=` attributes in the XML, which is a different render
|
|
141
|
+
* whenever the query overrode any of them.
|
|
142
|
+
*
|
|
143
|
+
* Applied as display state, not as an edit — it selects the layout and fills
|
|
144
|
+
* variable values without marking the document dirty, so opening a template
|
|
145
|
+
* and closing it again does not look like an unsaved change.
|
|
146
|
+
*/
|
|
147
|
+
templateQuery?: string;
|
|
148
|
+
}
|
|
149
|
+
export interface HostLoadMessage {
|
|
150
|
+
type: typeof HOST_LOAD;
|
|
151
|
+
data: HostLoadData;
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Stateless mode only (protocol v2). Reports whether the host managed to
|
|
155
|
+
* persist the content it received in `BUILDER_CONTENT`.
|
|
156
|
+
*
|
|
157
|
+
* Optional by design. The editor clears its unsaved-changes flag optimistically
|
|
158
|
+
* when it posts `BUILDER_CONTENT`, so a host that never acks behaves exactly as
|
|
159
|
+
* before. Sending `ok: false` is what buys something: the editor restores the
|
|
160
|
+
* dirty flag and tells the user, instead of leaving a failed write looking
|
|
161
|
+
* saved.
|
|
162
|
+
*/
|
|
163
|
+
export declare const HOST_SAVED = "design-templates:host:saved";
|
|
164
|
+
export interface HostSavedData {
|
|
165
|
+
/** False when the host could not persist the content. */
|
|
166
|
+
ok: boolean;
|
|
167
|
+
/** Shown to the user when `ok` is false. */
|
|
168
|
+
message?: string;
|
|
169
|
+
}
|
|
170
|
+
export interface HostSavedMessage {
|
|
171
|
+
type: typeof HOST_SAVED;
|
|
172
|
+
data: HostSavedData;
|
|
173
|
+
}
|
|
174
|
+
export type HostMessage = HostLoadMessage | HostSavedMessage;
|
|
175
|
+
/**
|
|
176
|
+
* Query params the app's proxy middleware (`src/proxy.ts`) converts into auth
|
|
177
|
+
* cookies on first navigation. Names are wire format.
|
|
178
|
+
*/
|
|
179
|
+
export declare const EMBED_PARAMS: {
|
|
180
|
+
readonly SESSION_UUID: "suuid";
|
|
181
|
+
readonly COMPANY_UUID: "cuuid";
|
|
182
|
+
readonly PROJECT_UUID: "puuid";
|
|
183
|
+
readonly SASS_KEY: "sassKey";
|
|
184
|
+
readonly FILEROBOT_TOKEN: "ftoken";
|
|
185
|
+
/**
|
|
186
|
+
* Filerobot security-template key, the alternative to a Hub session. The app
|
|
187
|
+
* exchanges it for a sass key itself and runs in a reduced mode — see
|
|
188
|
+
* `AUTH_MODES`. Sent *instead of* `sassKey` + `suuid`, never alongside them.
|
|
189
|
+
*/
|
|
190
|
+
readonly SEC_TEMPLATE: "secTemplate";
|
|
191
|
+
readonly IFRAME: "iframe";
|
|
192
|
+
/** Origin of the embedding page; the app uses it as postMessage targetOrigin. */
|
|
193
|
+
readonly EMBED_ORIGIN: "embedOrigin";
|
|
194
|
+
/**
|
|
195
|
+
* Accent colour for the editor chrome, as `#rgb` / `#rrggbb`. The app derives
|
|
196
|
+
* its whole accent ramp from it. Rejected server-side if it doesn't match
|
|
197
|
+
* that shape — it ends up inside a stylesheet.
|
|
198
|
+
*/
|
|
199
|
+
readonly BRAND_COLOR: "brandColor";
|
|
200
|
+
/** Colour scheme for the editor chrome: `light` | `dark` | `auto`. */
|
|
201
|
+
readonly THEME: "theme";
|
|
202
|
+
};
|
|
203
|
+
/** Values the `theme` param accepts. */
|
|
204
|
+
export type BuilderTheme = 'light' | 'dark' | 'auto';
|
|
205
|
+
/**
|
|
206
|
+
* How the embedder authenticated.
|
|
207
|
+
*
|
|
208
|
+
* - `session` — a Hub session (`suuid` + `sassKey` + `ftoken`). Full features.
|
|
209
|
+
* - `secTemplate` — a Filerobot security-template key (`secTemplate` +
|
|
210
|
+
* `ftoken`). A guest credential: no user identity and no Hub project, so the
|
|
211
|
+
* app accepts it on {@link EMBED_ROUTE} only, and everything that reads the
|
|
212
|
+
* Hub project model (metadata fields, regional variants, project branding)
|
|
213
|
+
* comes back empty. Rendering, fonts and asset browsing work, scoped by
|
|
214
|
+
* whatever the security template grants.
|
|
215
|
+
*
|
|
216
|
+
* Derived by the app from the params it received; named here so both sides use
|
|
217
|
+
* the same vocabulary.
|
|
218
|
+
*/
|
|
219
|
+
export declare const AUTH_MODES: {
|
|
220
|
+
readonly SESSION: "session";
|
|
221
|
+
readonly SEC_TEMPLATE: "secTemplate";
|
|
222
|
+
};
|
|
223
|
+
export type AuthMode = (typeof AUTH_MODES)[keyof typeof AUTH_MODES];
|
|
224
|
+
/**
|
|
225
|
+
* Shape the app requires of `brandColor`. Hex only: the value is interpolated
|
|
226
|
+
* into a `:root { … }` rule, so anything that could carry CSS syntax is
|
|
227
|
+
* refused rather than escaped. Mirrored in the app's proxy — keep in sync.
|
|
228
|
+
*/
|
|
229
|
+
export declare const BRAND_COLOR_PATTERN: RegExp;
|
|
230
|
+
/** Editor route for an existing template, or the new-template route. */
|
|
231
|
+
export declare function builderRoute(templateId?: string): string;
|
|
232
|
+
/**
|
|
233
|
+
* Stateless editor route. Takes no template id — the id is an opaque host
|
|
234
|
+
* value that arrives with the content over `HOST_LOAD`, not something the app
|
|
235
|
+
* resolves against Filerobot, so it has no place in the URL.
|
|
236
|
+
*/
|
|
237
|
+
export declare const EMBED_ROUTE = "/templates/embed";
|
package/dist/react.cjs
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const o=require("react");require("./define.cjs");const h=o.forwardRef(function(y,v){const{className:U,style:b,onReady:c,onOpen:d,onClose:m,onSave:i,onError:p,onDirtyChange:f,...e}=y,l=o.useRef(null);return o.useImperativeHandle(v,()=>l.current,[]),o.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.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.templateName,e.templateQuery,e.brandColor,e.theme,e.readyTimeout]),o.useLayoutEffect(()=>{const t=l.current;if(!t)return;const u=[],a=(r,s)=>{if(!s)return;const n=(T=>s(T.detail));t.addEventListener(r,n),u.push([r,n])};if(a("ready",c),a("open",d),a("close",m),a("error",p),a("dirtychange",f),i){const r=(s=>{Promise.resolve().then(()=>i(s.detail)).then(n=>t.confirmSave(n!==!1)).catch(n=>{console.error("[sfx-template-builder] onSave failed:",n),t.confirmSave(!1)})});t.addEventListener("save",r),u.push(["save",r])}return()=>{for(const[r,s]of u)t.removeEventListener(r,s)}},[c,d,m,i,p,f]),o.createElement("sfx-template-builder",{ref:l,class:U,style:b})});exports.TemplateBuilder=h;
|
|
2
|
+
//# sourceMappingURL=react.cjs.map
|
|
@@ -0,0 +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"}
|
package/dist/react.d.ts
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { type CSSProperties } from 'react';
|
|
2
|
+
import './define';
|
|
3
|
+
import type { SfxTemplateBuilder } from './template-builder';
|
|
4
|
+
import type { TemplateBuilderSaveDetail } from './template-builder';
|
|
5
|
+
import type { BuilderDirtyData, BuilderErrorData, BuilderTheme } from './protocol';
|
|
6
|
+
/**
|
|
7
|
+
* Hub session — the full-featured credential.
|
|
8
|
+
*/
|
|
9
|
+
export interface TemplateBuilderSessionAuth {
|
|
10
|
+
sassKey: string;
|
|
11
|
+
sessionUuid: string;
|
|
12
|
+
companyUuid?: string;
|
|
13
|
+
projectUuid?: string;
|
|
14
|
+
secTemplate?: never;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Filerobot security template — a guest credential for hosts that have no Hub
|
|
18
|
+
* session to hand over. `stateless` is required rather than merely implied:
|
|
19
|
+
* the app takes a security template on the stateless embed route only, so the
|
|
20
|
+
* combination is a compile-time error instead of a runtime one.
|
|
21
|
+
*
|
|
22
|
+
* `companyUuid` / `projectUuid` are absent by design — they name a Hub project
|
|
23
|
+
* that cannot be looked up without a session.
|
|
24
|
+
*/
|
|
25
|
+
export interface TemplateBuilderSecTemplateAuth {
|
|
26
|
+
secTemplate: string;
|
|
27
|
+
stateless: true;
|
|
28
|
+
sassKey?: never;
|
|
29
|
+
sessionUuid?: never;
|
|
30
|
+
companyUuid?: never;
|
|
31
|
+
projectUuid?: never;
|
|
32
|
+
}
|
|
33
|
+
export interface TemplateBuilderBaseProps {
|
|
34
|
+
baseUrl: string;
|
|
35
|
+
token: string;
|
|
36
|
+
templateId?: string;
|
|
37
|
+
mode?: 'inline' | 'modal';
|
|
38
|
+
/** Hand the template in and take it back out instead of using the DAM. */
|
|
39
|
+
stateless?: boolean;
|
|
40
|
+
/** Stateless mode: the template to edit, as `.fdt` XML. */
|
|
41
|
+
content?: string;
|
|
42
|
+
/** Stateless mode: display name for the editor header. */
|
|
43
|
+
templateName?: string;
|
|
44
|
+
/**
|
|
45
|
+
* Stateless mode: the `template_query` to open on — the value handed back on
|
|
46
|
+
* save. Reopens the template on the same layout and variable values; empty
|
|
47
|
+
* falls back to the XML's own `default=` attributes.
|
|
48
|
+
*/
|
|
49
|
+
templateQuery?: string;
|
|
50
|
+
/** Accent colour for the editor chrome, as `#rgb` / `#rrggbb`. */
|
|
51
|
+
brandColor?: string;
|
|
52
|
+
/** Colour scheme for the editor chrome. */
|
|
53
|
+
theme?: BuilderTheme;
|
|
54
|
+
readyTimeout?: number;
|
|
55
|
+
className?: string;
|
|
56
|
+
style?: CSSProperties;
|
|
57
|
+
onReady?: () => void;
|
|
58
|
+
onOpen?: () => void;
|
|
59
|
+
onClose?: () => void;
|
|
60
|
+
/**
|
|
61
|
+
* Fired on save. In stateless mode the detail carries the edited `content`
|
|
62
|
+
* for you to persist; otherwise it reports the uuid the app uploaded to.
|
|
63
|
+
*
|
|
64
|
+
* In stateless mode the outcome is reported back to the editor: return (or
|
|
65
|
+
* resolve to) `false`, or throw, and the editor restores its unsaved-changes
|
|
66
|
+
* flag and tells the user the save failed. Anything else counts as persisted.
|
|
67
|
+
*/
|
|
68
|
+
onSave?: (data: TemplateBuilderSaveDetail) => void | boolean | Promise<void | boolean>;
|
|
69
|
+
onError?: (data: BuilderErrorData) => void;
|
|
70
|
+
/**
|
|
71
|
+
* Stateless mode: unsaved-changes flag changed. Use it to prompt before
|
|
72
|
+
* swapping `content`, which discards in-progress edits.
|
|
73
|
+
*/
|
|
74
|
+
onDirtyChange?: (data: BuilderDirtyData) => void;
|
|
75
|
+
}
|
|
76
|
+
export type TemplateBuilderProps = TemplateBuilderBaseProps & (TemplateBuilderSessionAuth | TemplateBuilderSecTemplateAuth);
|
|
77
|
+
/**
|
|
78
|
+
* React wrapper around `<sfx-template-builder>`. Props are assigned as
|
|
79
|
+
* element properties via ref (works on React 18 and 19 alike); callbacks
|
|
80
|
+
* subscribe to the element's CustomEvents.
|
|
81
|
+
*
|
|
82
|
+
* Forwards a ref to the underlying element, which is the only way to reach the
|
|
83
|
+
* imperative API — `open()` in particular, without which `mode="modal"` can
|
|
84
|
+
* never be shown:
|
|
85
|
+
*
|
|
86
|
+
* ```tsx
|
|
87
|
+
* const builder = useRef<SfxTemplateBuilder>(null)
|
|
88
|
+
* <TemplateBuilder ref={builder} mode="modal" … />
|
|
89
|
+
* <button onClick={() => builder.current?.open('tpl-1')}>Edit</button>
|
|
90
|
+
* ```
|
|
91
|
+
*
|
|
92
|
+
* `forwardRef` rather than a plain `ref` prop: React 19 accepts the latter for
|
|
93
|
+
* function components, React 18 does not, and both are supported peers.
|
|
94
|
+
*/
|
|
95
|
+
export declare const TemplateBuilder: import("react").ForwardRefExoticComponent<TemplateBuilderProps & import("react").RefAttributes<SfxTemplateBuilder>>;
|
package/dist/react.js
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { forwardRef as T, useRef as E, useImperativeHandle as C, useLayoutEffect as f, createElement as I } from "react";
|
|
2
|
+
import "./define.js";
|
|
3
|
+
const g = T(function(y, v) {
|
|
4
|
+
const {
|
|
5
|
+
className: U,
|
|
6
|
+
style: b,
|
|
7
|
+
onReady: m,
|
|
8
|
+
onOpen: c,
|
|
9
|
+
onClose: d,
|
|
10
|
+
onSave: l,
|
|
11
|
+
onError: u,
|
|
12
|
+
onDirtyChange: p,
|
|
13
|
+
...e
|
|
14
|
+
} = y, a = E(null);
|
|
15
|
+
return C(v, () => a.current, []), f(() => {
|
|
16
|
+
const t = a.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.content = e.content ?? "", e.readyTimeout !== void 0 && (t.readyTimeout = e.readyTimeout));
|
|
18
|
+
}, [
|
|
19
|
+
e.baseUrl,
|
|
20
|
+
e.token,
|
|
21
|
+
e.sassKey,
|
|
22
|
+
e.sessionUuid,
|
|
23
|
+
e.secTemplate,
|
|
24
|
+
e.companyUuid,
|
|
25
|
+
e.projectUuid,
|
|
26
|
+
e.templateId,
|
|
27
|
+
e.mode,
|
|
28
|
+
e.stateless,
|
|
29
|
+
e.content,
|
|
30
|
+
e.templateName,
|
|
31
|
+
e.templateQuery,
|
|
32
|
+
e.brandColor,
|
|
33
|
+
e.theme,
|
|
34
|
+
e.readyTimeout
|
|
35
|
+
]), f(() => {
|
|
36
|
+
const t = a.current;
|
|
37
|
+
if (!t) return;
|
|
38
|
+
const i = [], n = (s, r) => {
|
|
39
|
+
if (!r) return;
|
|
40
|
+
const o = ((h) => r(h.detail));
|
|
41
|
+
t.addEventListener(s, o), i.push([s, o]);
|
|
42
|
+
};
|
|
43
|
+
if (n("ready", m), n("open", c), n("close", d), n("error", u), n("dirtychange", p), l) {
|
|
44
|
+
const s = ((r) => {
|
|
45
|
+
Promise.resolve().then(() => l(r.detail)).then((o) => t.confirmSave(o !== !1)).catch((o) => {
|
|
46
|
+
console.error("[sfx-template-builder] onSave failed:", o), t.confirmSave(!1);
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
t.addEventListener("save", s), i.push(["save", s]);
|
|
50
|
+
}
|
|
51
|
+
return () => {
|
|
52
|
+
for (const [s, r] of i) t.removeEventListener(s, r);
|
|
53
|
+
};
|
|
54
|
+
}, [m, c, d, l, u, p]), I("sfx-template-builder", { ref: a, class: U, style: b });
|
|
55
|
+
});
|
|
56
|
+
export {
|
|
57
|
+
g as TemplateBuilder
|
|
58
|
+
};
|
|
59
|
+
//# sourceMappingURL=react.js.map
|
|
@@ -0,0 +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":";;AA8GO,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,SAAAC,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,IAI3BK,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,EAAA,CACR,GAODI,EAAgB,MAAM;AACpB,UAAMC,IAAKJ,EAAI;AACf,QAAI,CAACI,EAAI;AACT,UAAMC,IAAuC,CAAA,GACvCC,IAAK,CAACC,GAAcC,MAAsC;AAC9D,UAAI,CAACA,EAAS;AACd,YAAMC,KAAY,CAACC,MAAmBF,EAAQE,EAAE,MAAe;AAC/D,MAAAN,EAAG,iBAAiBG,GAAME,CAAQ,GAClCJ,EAAK,KAAK,CAACE,GAAME,CAAQ,CAAC;AAAA,IAC5B;AAUA,QATAH,EAAG,SAASb,CAAO,GACnBa,EAAG,QAAQZ,CAAM,GACjBY,EAAG,SAASX,CAAO,GACnBW,EAAG,SAAST,CAAO,GACnBS,EAAG,eAAeR,CAAa,GAK3BF,GAAQ;AACV,YAAMa,KAAY,CAACC,MAA8C;AAG/D,gBAAQ,QAAA,EACL,KAAK,MAAMd,EAAOc,EAAE,MAAM,CAAC,EAC3B,KAAK,CAACC,MAAWP,EAAG,YAAYO,MAAW,EAAK,CAAC,EACjD,MAAM,CAACC,MAAQ;AAGd,kBAAQ,MAAM,yCAAyCA,CAAG,GAC1DR,EAAG,YAAY,EAAK;AAAA,QACtB,CAAC;AAAA,MACL;AACA,MAAAA,EAAG,iBAAiB,QAAQK,CAAQ,GACpCJ,EAAK,KAAK,CAAC,QAAQI,CAAQ,CAAC;AAAA,IAC9B;AAEA,WAAO,MAAM;AACX,iBAAW,CAACF,GAAME,CAAQ,KAAKJ,EAAM,CAAAD,EAAG,oBAAoBG,GAAME,CAAQ;AAAA,IAC5E;AAAA,EACF,GAAG,CAAChB,GAASC,GAAQC,GAASC,GAAQC,GAASC,CAAa,CAAC,GAGtDe,EAAc,wBAAwB,EAAE,KAAAb,GAAK,OAAOT,GAAW,OAAAC,GAAO;AAC/E,CAAC;"}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"use strict";const p=require("lit"),i=require("lit/decorators.js"),g=2,m="design-templates:builder:ready",l="design-templates:builder:open",_="design-templates:builder:close",y="design-templates:builder:save",f="design-templates:builder:error",E="design-templates:builder:content-request",b="design-templates:builder:content",R="design-templates:builder:dirty",T="design-templates:host:load",U="design-templates:host:saved",n={SESSION_UUID:"suuid",COMPANY_UUID:"cuuid",PROJECT_UUID:"puuid",SASS_KEY:"sassKey",FILEROBOT_TOKEN:"ftoken",SEC_TEMPLATE:"secTemplate",IFRAME:"iframe",EMBED_ORIGIN:"embedOrigin",BRAND_COLOR:"brandColor",THEME:"theme"},S={SESSION:"session",SEC_TEMPLATE:"secTemplate"},I=/^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/;function O(h){return h?`/templates/${encodeURIComponent(h)}/edit`:"/templates/new"}const D="/templates/embed";var v=Object.defineProperty,r=(h,t,e,a)=>{for(var o=void 0,d=h.length-1,u;d>=0;d--)(u=h[d])&&(o=u(t,e,o)||o);return o&&v(t,e,o),o};const c=class c extends p.LitElement{constructor(){super(...arguments),this.baseUrl="",this.token="",this.sassKey="",this.sessionUuid="",this.secTemplate="",this.companyUuid="",this.projectUuid="",this.templateId="",this.mode="inline",this.stateless=!1,this.content="",this.templateName="",this.templateQuery="",this.brandColor="",this.theme="",this.readyTimeout=2e4,this._status="idle",this._open=!1,this._src="",this._contentRequested=!1,this._reportedStatelessRequired=!1,this._isDirty=!1,this._autoOpenDecided=!1,this._onMessage=t=>{if(!this._open||!t.origin||t.origin!==this._appOrigin)return;const e=this.shadowRoot?.querySelector("iframe");if(!e||t.source!==e.contentWindow)return;const a=t.data;if(!(!a||typeof a.type!="string"))switch(a.type){case m:case l:this._clearHandshakeTimer(),this._status!=="ready"&&(this._status="ready",this._emit("ready")),a.type===l&&this._emit("open");break;case y:this._emit("save",a.data);break;case E:this._contentRequested=!0,this._sentKey=void 0,this._maybeSendContent();break;case b:{const o=a.data;this._sentKey=this._contentKey(o.content),this._emit("save",o);break}case R:{const o=a.data;this._isDirty=!!o?.isDirty,this._emit("dirtychange",{isDirty:this._isDirty});break}case _:this._emit("close"),this.mode==="modal"&&(this._open=!1);break;case f:this._fail(a.data??{code:"unknown"});break}}}get status(){return this._status}get isDirty(){return this._isDirty}open(t){t!==void 0&&(this.templateId=t),this._open=!0}close(){this._open=!1}load({content:t,templateId:e,name:a,templateQuery:o}){e!==void 0&&(this.templateId=e),a!==void 0&&(this.templateName=a),o!==void 0&&(this.templateQuery=o),this.content=t,this._open=!0}confirmSave(t,e){this.stateless&&this._postToApp({type:U,data:{ok:t,message:e}})}connectedCallback(){super.connectedCallback(),window.addEventListener("message",this._onMessage)}disconnectedCallback(){super.disconnectedCallback(),window.removeEventListener("message",this._onMessage),this._clearHandshakeTimer()}willUpdate(t){super.willUpdate(t),this._autoOpenDecided||(this._autoOpenDecided=!0,this.mode==="inline"&&(this._open=!0));const e=this._computeSrc();e!==this._src&&(this._src=e,this._status=e?"loading":"idle")}updated(t){t.has("_src")&&(this._clearHandshakeTimer(),this._contentRequested=!1,this._sentKey=void 0,this._isDirty&&(this._isDirty=!1,this._emit("dirtychange",{isDirty:!1})),this._src&&this._startHandshakeTimer()),(t.has("content")||t.has("templateId")||t.has("templateName")||t.has("templateQuery"))&&this._maybeSendContent()}render(){const t=this._src?p.html`<iframe
|
|
2
|
+
part="iframe"
|
|
3
|
+
title="Template builder"
|
|
4
|
+
src=${this._src}
|
|
5
|
+
allow="clipboard-read; clipboard-write"
|
|
6
|
+
></iframe>`:p.nothing,e=this._status==="loading"?p.html`<div class="spinner" part="spinner"></div>`:p.nothing;return this.mode==="modal"?this._open?p.html`<div class="overlay" part="overlay">
|
|
7
|
+
<div class="stage">${t}${e}</div>
|
|
8
|
+
</div>`:p.nothing:p.html`${t}${e}`}_computeSrc(){if(!this._open||!this.baseUrl||!this.token)return"";if(this.secTemplate){if(!this.stateless)return this._reportedStatelessRequired||(this._reportedStatelessRequired=!0,queueMicrotask(()=>this._fail({code:"invalid-config",message:"sec-template requires stateless mode — the app accepts a security template on the stateless embed route only."}))),"";this._reportedStatelessRequired=!1}else if(!this.sassKey||!this.sessionUuid)return"";let t;try{const e=this.stateless?D:O(this.templateId||void 0),a=this.baseUrl.endsWith("/")?this.baseUrl:`${this.baseUrl}/`;t=new URL(e.replace(/^\//,""),a)}catch{return this._reportedBadBaseUrl!==this.baseUrl&&(this._reportedBadBaseUrl=this.baseUrl,queueMicrotask(()=>this._fail({code:"invalid-base-url",message:`base-url is not a valid URL: ${this.baseUrl}`}))),""}return this._reportedBadBaseUrl=void 0,t.searchParams.set(n.FILEROBOT_TOKEN,this.token),this.secTemplate?t.searchParams.set(n.SEC_TEMPLATE,this.secTemplate):(t.searchParams.set(n.SASS_KEY,this.sassKey),t.searchParams.set(n.SESSION_UUID,this.sessionUuid),this.companyUuid&&t.searchParams.set(n.COMPANY_UUID,this.companyUuid),this.projectUuid&&t.searchParams.set(n.PROJECT_UUID,this.projectUuid)),this.brandColor&&t.searchParams.set(n.BRAND_COLOR,this.brandColor),this.theme&&t.searchParams.set(n.THEME,this.theme),t.searchParams.set(n.IFRAME,"1"),t.searchParams.set(n.EMBED_ORIGIN,window.location.origin),t.toString()}get _appOrigin(){try{return new URL(this.baseUrl).origin}catch{return null}}_maybeSendContent(){if(!this.stateless||!this._contentRequested||!this.content)return;const t={templateId:this.templateId||void 0,content:this.content,name:this.templateName||void 0,templateQuery:this.templateQuery||void 0},e=this._contentKey(this.content);e!==this._sentKey&&this._postToApp({type:T,data:t})&&(this._sentKey=e)}_contentKey(t){return JSON.stringify({templateId:this.templateId||void 0,content:t,name:this.templateName||void 0,templateQuery:this.templateQuery||void 0})}_postToApp(t){const e=this.shadowRoot?.querySelector("iframe")?.contentWindow,a=this._appOrigin;return!e||!a?!1:(e.postMessage(t,a),!0)}_startHandshakeTimer(){this.readyTimeout<=0||(this._handshakeTimer=window.setTimeout(()=>{this._fail({code:"handshake-timeout",message:`No ready signal from ${this.baseUrl} within ${this.readyTimeout}ms. Check that this origin is in the app's frame-ancestors allowlist and that third-party cookies are not blocked.`})},this.readyTimeout))}_clearHandshakeTimer(){this._handshakeTimer!==void 0&&(window.clearTimeout(this._handshakeTimer),this._handshakeTimer=void 0)}_fail(t){this._clearHandshakeTimer(),this._status="error",this._emit("error",t)}_emit(t,e){this.dispatchEvent(new CustomEvent(t,{detail:e,bubbles:!0,composed:!0}))}};c.styles=p.css`
|
|
9
|
+
:host {
|
|
10
|
+
display: block;
|
|
11
|
+
position: relative;
|
|
12
|
+
}
|
|
13
|
+
:host([mode='modal']) {
|
|
14
|
+
display: contents;
|
|
15
|
+
}
|
|
16
|
+
.overlay {
|
|
17
|
+
position: fixed;
|
|
18
|
+
inset: 0;
|
|
19
|
+
z-index: 2147483000;
|
|
20
|
+
background: rgba(0, 0, 0, 0.55);
|
|
21
|
+
display: flex;
|
|
22
|
+
}
|
|
23
|
+
.stage {
|
|
24
|
+
position: relative;
|
|
25
|
+
flex: 1;
|
|
26
|
+
display: flex;
|
|
27
|
+
}
|
|
28
|
+
iframe {
|
|
29
|
+
border: 0;
|
|
30
|
+
flex: 1;
|
|
31
|
+
width: 100%;
|
|
32
|
+
height: 100%;
|
|
33
|
+
}
|
|
34
|
+
.spinner {
|
|
35
|
+
position: absolute;
|
|
36
|
+
inset: 0;
|
|
37
|
+
margin: auto;
|
|
38
|
+
width: 32px;
|
|
39
|
+
height: 32px;
|
|
40
|
+
border: 3px solid rgba(128, 128, 128, 0.3);
|
|
41
|
+
border-top-color: currentColor;
|
|
42
|
+
border-radius: 50%;
|
|
43
|
+
animation: sfx-tb-spin 0.8s linear infinite;
|
|
44
|
+
pointer-events: none;
|
|
45
|
+
}
|
|
46
|
+
@keyframes sfx-tb-spin {
|
|
47
|
+
to {
|
|
48
|
+
transform: rotate(360deg);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
`;let s=c;r([i.property({attribute:"base-url"})],s.prototype,"baseUrl");r([i.property()],s.prototype,"token");r([i.property({attribute:"sass-key"})],s.prototype,"sassKey");r([i.property({attribute:"session-uuid"})],s.prototype,"sessionUuid");r([i.property({attribute:"sec-template"})],s.prototype,"secTemplate");r([i.property({attribute:"company-uuid"})],s.prototype,"companyUuid");r([i.property({attribute:"project-uuid"})],s.prototype,"projectUuid");r([i.property({attribute:"template-id"})],s.prototype,"templateId");r([i.property({reflect:!0})],s.prototype,"mode");r([i.property({type:Boolean,reflect:!0})],s.prototype,"stateless");r([i.property({attribute:!1})],s.prototype,"content");r([i.property({attribute:"template-name"})],s.prototype,"templateName");r([i.property({attribute:"template-query"})],s.prototype,"templateQuery");r([i.property({attribute:"brand-color"})],s.prototype,"brandColor");r([i.property()],s.prototype,"theme");r([i.property({type:Number,attribute:"ready-timeout"})],s.prototype,"readyTimeout");r([i.state()],s.prototype,"_status");r([i.state()],s.prototype,"_open");r([i.state()],s.prototype,"_src");r([i.state()],s.prototype,"_isDirty");exports.AUTH_MODES=S;exports.BRAND_COLOR_PATTERN=I;exports.BUILDER_CLOSE=_;exports.BUILDER_CONTENT=b;exports.BUILDER_CONTENT_REQUEST=E;exports.BUILDER_DIRTY=R;exports.BUILDER_ERROR=f;exports.BUILDER_OPEN=l;exports.BUILDER_READY=m;exports.BUILDER_SAVE=y;exports.EMBED_PARAMS=n;exports.EMBED_ROUTE=D;exports.HOST_LOAD=T;exports.HOST_SAVED=U;exports.PROTOCOL_VERSION=g;exports.SfxTemplateBuilder=s;exports.builderRoute=O;
|
|
52
|
+
//# sourceMappingURL=template-builder-CSyPZni9.cjs.map
|