@brftech/filex-app-ui 0.54.0 → 0.55.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/README.md +9 -4
- package/dist/filex-app-ui.iife.js +1 -1
- package/dist/filex-app-ui.iife.js.map +1 -1
- package/dist/filex-app-ui.js +147 -99
- package/dist/filex-app-ui.js.map +1 -1
- package/dist/index.d.ts +215 -2
- package/package.json +1 -1
- package/src/index.ts +66 -0
- package/src/protocol.ts +200 -2
package/dist/filex-app-ui.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const
|
|
1
|
+
const j = 1, S = "filex:hello", B = "filex:port", E = [
|
|
2
2
|
"session.get",
|
|
3
3
|
"file.read",
|
|
4
4
|
"file.save",
|
|
@@ -14,7 +14,16 @@ const k = 1, S = "filex:hello", E = "filex:port", L = [
|
|
|
14
14
|
"job.submit",
|
|
15
15
|
"state.get",
|
|
16
16
|
"state.set",
|
|
17
|
-
"license.get"
|
|
17
|
+
"license.get",
|
|
18
|
+
"ui.print",
|
|
19
|
+
"coedit.join",
|
|
20
|
+
"coedit.subscribe",
|
|
21
|
+
"coedit.append",
|
|
22
|
+
"coedit.lease",
|
|
23
|
+
"coedit.cursor",
|
|
24
|
+
"coedit.blob.put",
|
|
25
|
+
"coedit.blob.get",
|
|
26
|
+
"coedit.leave"
|
|
18
27
|
], x = {
|
|
19
28
|
/** `file.read({as: 'text'})` refuses a file larger than this; read a stream. */
|
|
20
29
|
maxTextBytes: 33554432,
|
|
@@ -25,50 +34,57 @@ const k = 1, S = "filex:hello", E = "filex:port", L = [
|
|
|
25
34
|
/** How long the SDK waits for the host's port. */
|
|
26
35
|
connectTimeoutMs: 1e4,
|
|
27
36
|
/** The largest file `ui.download` hands the person (streamed or not). */
|
|
28
|
-
maxDownloadBytes: 268435456
|
|
37
|
+
maxDownloadBytes: 268435456,
|
|
38
|
+
/**
|
|
39
|
+
* The largest PDF `ui.print` prints (filex 0.55). Lower than a download:
|
|
40
|
+
* a download can stream to disk, a print is held whole in the page (one
|
|
41
|
+
* Blob) until the print dialog has read it, and a document worth printing
|
|
42
|
+
* is far smaller.
|
|
43
|
+
*/
|
|
44
|
+
maxPrintBytes: 67108864
|
|
29
45
|
};
|
|
30
|
-
function
|
|
46
|
+
function k(n) {
|
|
31
47
|
return !!n && typeof n == "object" && n.type === S && typeof n.v == "number";
|
|
32
48
|
}
|
|
33
|
-
function
|
|
34
|
-
return !!n && typeof n == "object" && n.type ===
|
|
49
|
+
function L(n) {
|
|
50
|
+
return !!n && typeof n == "object" && n.type === B;
|
|
35
51
|
}
|
|
36
|
-
function
|
|
52
|
+
function A(n) {
|
|
37
53
|
return !!n && typeof n == "object" && typeof n.id == "number" && typeof n.method == "string";
|
|
38
54
|
}
|
|
39
|
-
function
|
|
40
|
-
return
|
|
55
|
+
function I(n) {
|
|
56
|
+
return E.includes(n);
|
|
41
57
|
}
|
|
42
|
-
class
|
|
43
|
-
constructor(
|
|
44
|
-
super(
|
|
58
|
+
class m extends Error {
|
|
59
|
+
constructor(u) {
|
|
60
|
+
super(u.message || u.code), this.name = "FilexError", this.code = u.code;
|
|
45
61
|
}
|
|
46
62
|
}
|
|
47
|
-
let
|
|
48
|
-
function
|
|
49
|
-
return
|
|
63
|
+
let v = null;
|
|
64
|
+
function P(n = {}) {
|
|
65
|
+
return v || (v = O(n)), v;
|
|
50
66
|
}
|
|
51
67
|
function O(n) {
|
|
52
|
-
return new Promise((
|
|
68
|
+
return new Promise((u, p) => {
|
|
53
69
|
if (typeof window > "u" || window.parent === window) {
|
|
54
|
-
|
|
70
|
+
p(new m({ code: "unavailable", message: "not running inside filex" }));
|
|
55
71
|
return;
|
|
56
72
|
}
|
|
57
|
-
const
|
|
58
|
-
window.removeEventListener("message", g),
|
|
73
|
+
const o = window.parent, l = R(), a = setTimeout(() => {
|
|
74
|
+
window.removeEventListener("message", g), p(new m({ code: "unavailable", message: "filex did not answer" }));
|
|
59
75
|
}, n.timeoutMs ?? x.connectTimeoutMs);
|
|
60
76
|
function g(y) {
|
|
61
|
-
if (y.source !==
|
|
77
|
+
if (y.source !== o || !L(y.data) || !y.ports[0] || l && y.origin !== l) return;
|
|
62
78
|
window.removeEventListener("message", g), clearTimeout(a);
|
|
63
|
-
const
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
const e =
|
|
67
|
-
|
|
68
|
-
})), c
|
|
69
|
-
}).catch(
|
|
79
|
+
const s = y.ports[0], c = T(s, n);
|
|
80
|
+
c.request("session.get").then((f) => {
|
|
81
|
+
c.session = f, n.applyTheme !== !1 && (h(f.theme, f.locale, f.dir), c.on("theme", (d) => h(d)), c.on("locale", (d) => {
|
|
82
|
+
const e = d;
|
|
83
|
+
h(void 0, e.locale, e.dir);
|
|
84
|
+
})), u(c);
|
|
85
|
+
}).catch(p);
|
|
70
86
|
}
|
|
71
|
-
window.addEventListener("message", g),
|
|
87
|
+
window.addEventListener("message", g), o.postMessage({ type: S, v: 1 }, "*");
|
|
72
88
|
});
|
|
73
89
|
}
|
|
74
90
|
function R() {
|
|
@@ -79,17 +95,17 @@ function R() {
|
|
|
79
95
|
}
|
|
80
96
|
return "";
|
|
81
97
|
}
|
|
82
|
-
function T(n,
|
|
83
|
-
let
|
|
84
|
-
const
|
|
98
|
+
function T(n, u) {
|
|
99
|
+
let p = 0;
|
|
100
|
+
const o = /* @__PURE__ */ new Map(), l = /* @__PURE__ */ new Map();
|
|
85
101
|
let a = null;
|
|
86
102
|
n.onmessage = (e) => {
|
|
87
103
|
const t = e.data;
|
|
88
104
|
if (!(!t || typeof t != "object")) {
|
|
89
105
|
if (typeof t.id == "number") {
|
|
90
|
-
const
|
|
91
|
-
if (!
|
|
92
|
-
|
|
106
|
+
const r = o.get(t.id);
|
|
107
|
+
if (!r) return;
|
|
108
|
+
o.delete(t.id), t.error ? r.reject(new m(t.error)) : r.resolve(t.result);
|
|
93
109
|
return;
|
|
94
110
|
}
|
|
95
111
|
if (typeof t.hid == "number") {
|
|
@@ -97,11 +113,11 @@ function T(n, c) {
|
|
|
97
113
|
return;
|
|
98
114
|
}
|
|
99
115
|
if (typeof t.event == "string")
|
|
100
|
-
for (const
|
|
116
|
+
for (const r of l.get(t.event) ?? [])
|
|
101
117
|
try {
|
|
102
|
-
|
|
103
|
-
} catch (
|
|
104
|
-
console.error("[filex-app-ui] listener failed",
|
|
118
|
+
r(t.data);
|
|
119
|
+
} catch (i) {
|
|
120
|
+
console.error("[filex-app-ui] listener failed", i);
|
|
105
121
|
}
|
|
106
122
|
}
|
|
107
123
|
};
|
|
@@ -115,8 +131,8 @@ function T(n, c) {
|
|
|
115
131
|
const t = await y();
|
|
116
132
|
n.postMessage({ hid: e.hid, result: t ?? { saved: !0 } });
|
|
117
133
|
} catch (t) {
|
|
118
|
-
const
|
|
119
|
-
n.postMessage({ hid: e.hid, error:
|
|
134
|
+
const r = t instanceof m ? { code: t.code, message: t.message } : { code: "failed", message: String(t?.message ?? t) };
|
|
135
|
+
n.postMessage({ hid: e.hid, error: r });
|
|
120
136
|
}
|
|
121
137
|
return;
|
|
122
138
|
}
|
|
@@ -125,23 +141,23 @@ function T(n, c) {
|
|
|
125
141
|
async function y() {
|
|
126
142
|
if (!a) return null;
|
|
127
143
|
const e = await a();
|
|
128
|
-
return e == null ? null :
|
|
144
|
+
return e == null ? null : d.save(e);
|
|
129
145
|
}
|
|
130
|
-
function
|
|
131
|
-
const
|
|
132
|
-
return new Promise((
|
|
133
|
-
|
|
146
|
+
function s(e, t, r = []) {
|
|
147
|
+
const i = ++p;
|
|
148
|
+
return new Promise((b, w) => {
|
|
149
|
+
o.set(i, { resolve: b, reject: w });
|
|
134
150
|
try {
|
|
135
|
-
n.postMessage({ id:
|
|
151
|
+
n.postMessage({ id: i, method: e, params: t }, r);
|
|
136
152
|
} catch (M) {
|
|
137
|
-
|
|
153
|
+
o.delete(i), w(M);
|
|
138
154
|
}
|
|
139
155
|
});
|
|
140
156
|
}
|
|
141
|
-
function
|
|
142
|
-
|
|
157
|
+
function c(e, t) {
|
|
158
|
+
s(e, t).catch((r) => console.warn("[filex-app-ui]", e, r));
|
|
143
159
|
}
|
|
144
|
-
async function
|
|
160
|
+
async function f(e) {
|
|
145
161
|
if (typeof e == "string") return { data: e, transfer: [] };
|
|
146
162
|
if (e instanceof ArrayBuffer) return { data: e, transfer: [e] };
|
|
147
163
|
if (ArrayBuffer.isView(e)) {
|
|
@@ -154,111 +170,143 @@ function T(n, c) {
|
|
|
154
170
|
}
|
|
155
171
|
if (typeof ReadableStream < "u" && e instanceof ReadableStream)
|
|
156
172
|
return { data: e, transfer: [e] };
|
|
157
|
-
throw new
|
|
173
|
+
throw new m({ code: "invalid", message: "save takes a string, a Blob, bytes or a ReadableStream" });
|
|
158
174
|
}
|
|
159
|
-
const
|
|
175
|
+
const d = {
|
|
160
176
|
session: void 0,
|
|
161
177
|
async open(e = 0) {
|
|
162
|
-
const t =
|
|
163
|
-
if (!t) throw new
|
|
164
|
-
const
|
|
178
|
+
const t = d.session?.files?.[e];
|
|
179
|
+
if (!t) throw new m({ code: "not_found", message: `no file at ${e}` });
|
|
180
|
+
const r = (i, b) => s("file.read", { index: e, as: i }).then((w) => w[b]);
|
|
165
181
|
return {
|
|
166
182
|
...t,
|
|
167
|
-
text: () =>
|
|
168
|
-
bytes: () =>
|
|
169
|
-
stream: () =>
|
|
170
|
-
save: (
|
|
183
|
+
text: () => r("text", "text"),
|
|
184
|
+
bytes: () => r("bytes", "bytes"),
|
|
185
|
+
stream: () => r("stream", "stream"),
|
|
186
|
+
save: (i, b) => d.save(i, { index: e, mime: b })
|
|
171
187
|
};
|
|
172
188
|
},
|
|
173
189
|
async save(e, t = {}) {
|
|
174
|
-
const
|
|
175
|
-
return
|
|
190
|
+
const r = await f(e);
|
|
191
|
+
return s("file.save", { index: t.index ?? 0, data: r.data, mime: t.mime }, r.transfer);
|
|
192
|
+
},
|
|
193
|
+
async saveAs(e, t, r) {
|
|
194
|
+
const i = await f(t);
|
|
195
|
+
return s("file.saveAs", { name: e, data: i.data, mime: r }, i.transfer);
|
|
176
196
|
},
|
|
177
|
-
async
|
|
178
|
-
const
|
|
179
|
-
return
|
|
197
|
+
async download(e, t, r) {
|
|
198
|
+
const i = await f(t);
|
|
199
|
+
return s("ui.download", { name: e, data: i.data, mime: r }, i.transfer);
|
|
180
200
|
},
|
|
181
|
-
async
|
|
182
|
-
const
|
|
183
|
-
return
|
|
201
|
+
async print(e, t) {
|
|
202
|
+
const r = await f(t);
|
|
203
|
+
return s("ui.print", { name: e, data: r.data, mime: "application/pdf" }, r.transfer);
|
|
204
|
+
},
|
|
205
|
+
coedit: {
|
|
206
|
+
join(e = 0) {
|
|
207
|
+
return s("coedit.join", { index: e });
|
|
208
|
+
},
|
|
209
|
+
subscribe(e) {
|
|
210
|
+
return s("coedit.subscribe", { from: e });
|
|
211
|
+
},
|
|
212
|
+
append(e, t) {
|
|
213
|
+
return s("coedit.append", { kind: e, body: t });
|
|
214
|
+
},
|
|
215
|
+
async lease(e, t) {
|
|
216
|
+
return !!(await s("coedit.lease", { op: e, changesSeen: t }))?.granted;
|
|
217
|
+
},
|
|
218
|
+
cursor(e) {
|
|
219
|
+
c("coedit.cursor", { cursor: e });
|
|
220
|
+
},
|
|
221
|
+
async putBlob(e, t) {
|
|
222
|
+
const r = await f(t);
|
|
223
|
+
if (!(r.data instanceof ArrayBuffer)) throw new m({ code: "invalid", message: "a blob is bytes" });
|
|
224
|
+
await s("coedit.blob.put", { name: e, data: r.data }, r.transfer);
|
|
225
|
+
},
|
|
226
|
+
async getBlob(e) {
|
|
227
|
+
return (await s("coedit.blob.get", { name: e })).bytes;
|
|
228
|
+
},
|
|
229
|
+
async leave() {
|
|
230
|
+
await s("coedit.leave");
|
|
231
|
+
}
|
|
184
232
|
},
|
|
185
233
|
dirty(e) {
|
|
186
|
-
|
|
234
|
+
c("ui.dirty", { dirty: !!e });
|
|
187
235
|
},
|
|
188
236
|
title(e) {
|
|
189
|
-
|
|
237
|
+
c("ui.title", { text: String(e).slice(0, x.maxLineChars) });
|
|
190
238
|
},
|
|
191
239
|
toast(e, t) {
|
|
192
|
-
|
|
240
|
+
c("ui.toast", { text: String(e).slice(0, x.maxLineChars), tone: t });
|
|
193
241
|
},
|
|
194
242
|
confirm(e) {
|
|
195
|
-
return
|
|
243
|
+
return s("ui.confirm", typeof e == "string" ? { text: e } : e);
|
|
196
244
|
},
|
|
197
245
|
close() {
|
|
198
|
-
|
|
246
|
+
c("ui.close");
|
|
199
247
|
},
|
|
200
248
|
copy(e) {
|
|
201
|
-
return
|
|
249
|
+
return s("clipboard.write", { text: String(e) });
|
|
202
250
|
},
|
|
203
251
|
call(e, t) {
|
|
204
|
-
return
|
|
252
|
+
return s("engine.call", { method: e, params: t });
|
|
205
253
|
},
|
|
206
254
|
submit(e, t) {
|
|
207
|
-
return
|
|
255
|
+
return s("job.submit", { action: e, params: t });
|
|
208
256
|
},
|
|
209
257
|
state: {
|
|
210
258
|
get(e) {
|
|
211
|
-
return
|
|
259
|
+
return s("state.get", { key: e });
|
|
212
260
|
},
|
|
213
261
|
set(e, t) {
|
|
214
|
-
return
|
|
262
|
+
return s("state.set", { key: e, value: t });
|
|
215
263
|
}
|
|
216
264
|
},
|
|
217
265
|
license: {
|
|
218
266
|
get() {
|
|
219
|
-
return
|
|
267
|
+
return s("license.get");
|
|
220
268
|
}
|
|
221
269
|
},
|
|
222
270
|
on(e, t) {
|
|
223
|
-
let
|
|
224
|
-
return
|
|
271
|
+
let r = l.get(e);
|
|
272
|
+
return r || l.set(e, r = /* @__PURE__ */ new Set()), r.add(t), () => r.delete(t);
|
|
225
273
|
},
|
|
226
274
|
onSave(e) {
|
|
227
275
|
return a = e, () => {
|
|
228
276
|
a === e && (a = null);
|
|
229
277
|
};
|
|
230
278
|
},
|
|
231
|
-
request:
|
|
279
|
+
request: s
|
|
232
280
|
};
|
|
233
|
-
return
|
|
281
|
+
return u.saveShortcut !== !1 && typeof document < "u" && document.addEventListener(
|
|
234
282
|
"keydown",
|
|
235
283
|
(e) => {
|
|
236
|
-
(e.ctrlKey || e.metaKey) && !e.altKey && !e.shiftKey && (e.key === "s" || e.key === "S") && a && (e.preventDefault(), y().catch((t) =>
|
|
284
|
+
(e.ctrlKey || e.metaKey) && !e.altKey && !e.shiftKey && (e.key === "s" || e.key === "S") && a && (e.preventDefault(), y().catch((t) => d.toast(String(t?.message ?? t), "error")));
|
|
237
285
|
},
|
|
238
286
|
!0
|
|
239
|
-
),
|
|
287
|
+
), d;
|
|
240
288
|
}
|
|
241
|
-
function
|
|
289
|
+
function h(n, u, p) {
|
|
242
290
|
if (typeof document > "u") return;
|
|
243
|
-
const
|
|
291
|
+
const o = document.documentElement;
|
|
244
292
|
if (n) {
|
|
245
|
-
|
|
246
|
-
for (const [
|
|
247
|
-
/^--fe-[a-z0-9-]+$/.test(
|
|
293
|
+
o.dataset.theme = n.mode, o.style.colorScheme = n.mode;
|
|
294
|
+
for (const [l, a] of Object.entries(n.tokens ?? {}))
|
|
295
|
+
/^--fe-[a-z0-9-]+$/.test(l) && typeof a == "string" && o.style.setProperty(l, a);
|
|
248
296
|
}
|
|
249
|
-
|
|
297
|
+
u && (o.lang = u), p && (o.dir = p);
|
|
250
298
|
}
|
|
251
299
|
export {
|
|
252
|
-
|
|
253
|
-
|
|
300
|
+
j as BRIDGE_VERSION,
|
|
301
|
+
m as FilexError,
|
|
254
302
|
S as HELLO,
|
|
255
303
|
x as LIMITS,
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
304
|
+
E as METHODS,
|
|
305
|
+
B as PORT,
|
|
306
|
+
P as connect,
|
|
307
|
+
k as isHello,
|
|
308
|
+
I as isKnownMethod,
|
|
309
|
+
L as isPortMessage,
|
|
310
|
+
A as isRequest
|
|
263
311
|
};
|
|
264
312
|
//# sourceMappingURL=filex-app-ui.js.map
|
package/dist/filex-app-ui.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"filex-app-ui.js","sources":["../src/protocol.ts","../src/index.ts"],"sourcesContent":["/**\n * The bridge between filex and an app's own interface — the ONE definition.\n *\n * An app's interface (the `ui` block of its filex-app.json) runs in a frame\n * filex draws with `sandbox=\"allow-scripts\"`: an opaque origin, no cookies, no\n * storage, no network (the server's CSP says `connect-src 'none'`). Everything\n * it may do goes through this bridge, and the host decides every call.\n *\n * Both halves import this file — the SDK the app bundles (`src/index.ts`) and\n * filex's own frame (`packages/core/src/components/plugin/AppFrame.vue`) — so a\n * method or an event added here is added on both sides at once.\n *\n * The wire, in order (docs/APP-PLUGINS-API.md → \"The interface bridge\"):\n *\n * 1. The app posts `{type: 'filex:hello', v: 1}` to its parent window.\n * 2. The host takes it ONLY when `event.source` is the frame it drew. The\n * origin says nothing here: a sandboxed frame's origin is always \"null\",\n * so any other sandboxed frame on the page would look the same (measured,\n * 2026-09-27, Chrome, Firefox and WebKit).\n * 3. The host answers with a MessagePort: `{type: 'filex:port', v: 1}` and\n * the port transferred. Once per load of the frame; a second hello is\n * ignored.\n * 4. The app takes the port ONLY when `event.source` is its parent window.\n * 5. From then on, only the port: requests `{id, method, params}`, answers\n * `{id, result}` or `{id, error}`, events `{event, data}`, and the host's\n * own requests `{hid, request, params}` answered `{hid, result|error}`.\n */\n\n/** The bridge generation both sides speak. */\nexport const BRIDGE_VERSION = 1;\n\n/** The app's first message, to `window.parent`. */\nexport const HELLO = 'filex:hello';\n/** The host's answer, carrying the port. */\nexport const PORT = 'filex:port';\n\n/** What the app may ask the host. Anything else is `unknown_method`. */\nexport type Method =\n | 'session.get'\n | 'file.read'\n | 'file.save'\n | 'file.saveAs'\n | 'ui.dirty'\n | 'ui.title'\n | 'ui.toast'\n | 'ui.confirm'\n | 'ui.close'\n | 'clipboard.write'\n | 'ui.download'\n | 'engine.call'\n | 'job.submit'\n | 'state.get'\n | 'state.set'\n /** 0.52.0: what the app's license says (a paid app from a store), `LicenseInfo`. */\n | 'license.get';\n\nexport const METHODS: readonly Method[] = [\n 'session.get',\n 'file.read',\n 'file.save',\n 'file.saveAs',\n 'ui.dirty',\n 'ui.title',\n 'ui.toast',\n 'ui.confirm',\n 'ui.close',\n 'clipboard.write',\n 'ui.download',\n 'engine.call',\n 'job.submit',\n 'state.get',\n 'state.set',\n 'license.get',\n];\n\n/** What the host tells the app, unasked. */\nexport type HostEvent =\n /** The interface's colours changed (light/dark, palette). `data: Theme`. */\n | 'theme'\n /** The reader's language changed. `data: {locale, dir}`. */\n | 'locale'\n /** The open file changed underneath (another tab, another person). */\n | 'file.changed'\n /** The person closed the frame's surroundings; last chance to say so. */\n | 'close.request'\n /** An administrator installed a new version of this app: reload to use it. */\n | 'app.updated';\n\n/** What the host may ask the app (and wait for). */\nexport type HostRequest =\n /** Save now (the host's Save button, a draft's \"Save to disk\"): the app\n * hands its document to `file.save` and answers when it is written. */\n 'save';\n\n/** Why a call was refused. */\nexport type ErrorCode =\n /** The app was not granted what the call needs (files:write for a save…). */\n | 'not_granted'\n /** No such file (an index past the opened files), no such key. */\n | 'not_found'\n /** The file cannot be written: a read-only storage, a view-only opening. */\n | 'read_only'\n /** The call's parameters are wrong. */\n | 'invalid'\n /** Over a limit (LIMITS). */\n | 'too_large'\n /** The server could not be reached or refused; `message` says what. */\n | 'failed'\n /** The person said no (a confirm, a save-as picker closed). */\n | 'cancelled'\n /** The host does not offer this here (no engine, no save handler). */\n | 'unavailable'\n | 'unknown_method';\n\nexport interface BridgeError {\n code: ErrorCode;\n message?: string;\n}\n\n/* ── messages ──────────────────────────────────────────────────────────── */\n\nexport interface HelloMessage {\n type: typeof HELLO;\n v: number;\n}\n\nexport interface PortMessage {\n type: typeof PORT;\n v: number;\n}\n\nexport interface RequestMessage {\n id: number;\n method: Method;\n params?: unknown;\n}\n\nexport interface ResponseMessage {\n id: number;\n result?: unknown;\n error?: BridgeError;\n}\n\nexport interface EventMessage {\n event: HostEvent;\n data?: unknown;\n}\n\nexport interface HostRequestMessage {\n hid: number;\n request: HostRequest;\n params?: unknown;\n}\n\nexport interface HostResponseMessage {\n hid: number;\n result?: unknown;\n error?: BridgeError;\n}\n\n/* ── shapes ────────────────────────────────────────────────────────────── */\n\n/** The interface's look: `mode` and filex's own `--fe-*` custom properties. */\nexport interface Theme {\n mode: 'light' | 'dark';\n /** `--fe-bg`, `--fe-text`, `--fe-accent`… — the SDK sets them on `<html>`. */\n tokens: Record<string, string>;\n}\n\n/** One file the interface was opened with. Never a storage path. */\nexport interface FileInfo {\n index: number;\n name: string;\n /** Lower-case, no dot. */\n ext: string;\n size: number;\n mime: string;\n /** It cannot be saved over: read-only storage, view-only opening, or the\n * app holds no `files:write`. */\n readOnly: boolean;\n}\n\n/** What `session.get` answers — what the app knows about where it runs. */\nexport interface Session {\n v: number;\n app: { name: string; version: string };\n view: { id: string; placement: 'modal' | 'page' | 'inspector' | 'home' | 'viewer' };\n /** The reader's language tag (`tr`, `pt-br`) and its direction. */\n locale: string;\n dir: 'ltr' | 'rtl';\n theme: Theme;\n /** The person's display name; never their e-mail, never a token. */\n user: { name: string };\n files: FileInfo[];\n /** The permissions this app was granted — so it can hide what it may not do. */\n grants: string[];\n /** The administrator's non-secret settings for this app (grant `settings`). */\n settings?: Record<string, string>;\n}\n\n/** `file.read` params. */\nexport interface ReadParams {\n index?: number;\n /** `stream` (a transferred ReadableStream<Uint8Array>), `bytes` (an\n * ArrayBuffer), or `text` (UTF-8, at most LIMITS.maxTextBytes). */\n as?: 'stream' | 'bytes' | 'text';\n}\n\nexport interface ReadResult {\n name: string;\n size: number;\n mime: string;\n stream?: ReadableStream<Uint8Array>;\n bytes?: ArrayBuffer;\n text?: string;\n}\n\n/** `file.save` params: the new content of the opened file. */\nexport interface SaveParams {\n index?: number;\n /** A transferred ReadableStream<Uint8Array>, an ArrayBuffer or a string. */\n data: ReadableStream<Uint8Array> | ArrayBuffer | string;\n mime?: string;\n}\n\nexport interface SaveResult {\n saved: true;\n size: number;\n}\n\n/**\n * `ui.download`: a file for the person's own disk. filex does it (a sandboxed\n * frame cannot download), with the app's `ui:download` grant, on a gesture in\n * the frame or the person's yes, never over LIMITS.maxDownloadBytes.\n */\nexport interface DownloadParams {\n /** The file name offered — a name, not a path. */\n name: string;\n /** A transferred ReadableStream<Uint8Array>, an ArrayBuffer or a string. */\n data: ReadableStream<Uint8Array> | ArrayBuffer | string;\n mime?: string;\n}\n\nexport interface DownloadResult {\n saved: true;\n size: number;\n}\n\n/**\n * `file.saveAs`: a NEW file, in a folder the person picks in filex's own\n * folder dialog (the one its Move to… uses), which opens in the opened\n * file's folder. Closing the dialog answers `cancelled`.\n */\nexport interface SaveAsParams {\n /** The file name — a name, not a path. */\n name: string;\n data: ReadableStream<Uint8Array> | ArrayBuffer | string;\n mime?: string;\n}\n\nexport interface SaveAsResult {\n saved: true;\n name: string;\n size: number;\n}\n\nexport interface ToastParams {\n text: string;\n tone?: 'info' | 'success' | 'warning' | 'error';\n}\n\nexport interface ConfirmParams {\n title?: string;\n text: string;\n confirm?: string;\n cancel?: string;\n danger?: boolean;\n}\n\n/**\n * `license.get` (filex 0.52.0): what the app's license says. A paid app is\n * installed from a store, which issues its license and is asked about it every\n * day; filex keeps the key and never hands it to the app. `status`:\n *\n * - `free`: not a paid app;\n * - `valid`: the store said valid at its last check;\n * - `grace`: valid, but the store could not be asked since - it holds until\n * the grace the store signed ends;\n * - anything else (`revoked`, `expired`, `invalid`, `seats_exhausted`,\n * `wrong_app`, `grace_expired`, `missing`, `unverified`): the app is held\n * and its interface is not served, so an app rarely reads one of these.\n *\n * `valid_until` / `updates_until` are there for a valid license (dates as\n * RFC 3339). Who holds the license is not the app's to read. filex 0.51.0\n * and older answer `unknown_method`.\n */\nexport interface LicenseInfo {\n status: string;\n valid_until?: string;\n updates_until?: string;\n}\n\n/** `engine.call`: the app's own module, `ui_call` export. */\nexport interface EngineCallParams {\n method: string;\n params?: unknown;\n}\n\n/** `job.submit`: queue one of the app's actions on the opened files. */\nexport interface JobSubmitParams {\n action: string;\n params?: Record<string, unknown>;\n}\n\n/* ── limits ────────────────────────────────────────────────────────────── */\n\nexport const LIMITS = {\n /** `file.read({as: 'text'})` refuses a file larger than this; read a stream. */\n maxTextBytes: 32 << 20,\n /** One `state.set` value, as JSON. */\n maxStateBytes: 8 << 10,\n /** A `ui.toast` / `ui.title` text, in characters. */\n maxLineChars: 300,\n /** How long the SDK waits for the host's port. */\n connectTimeoutMs: 10_000,\n /** The largest file `ui.download` hands the person (streamed or not). */\n maxDownloadBytes: 256 << 20,\n} as const;\n\n/* ── guards ────────────────────────────────────────────────────────────── */\n\nexport function isHello(d: unknown): d is HelloMessage {\n return !!d && typeof d === 'object' && (d as HelloMessage).type === HELLO && typeof (d as HelloMessage).v === 'number';\n}\n\nexport function isPortMessage(d: unknown): d is PortMessage {\n return !!d && typeof d === 'object' && (d as PortMessage).type === PORT;\n}\n\nexport function isRequest(d: unknown): d is RequestMessage {\n return (\n !!d &&\n typeof d === 'object' &&\n typeof (d as RequestMessage).id === 'number' &&\n typeof (d as RequestMessage).method === 'string'\n );\n}\n\nexport function isKnownMethod(m: string): m is Method {\n return (METHODS as readonly string[]).includes(m);\n}\n","/**\n * @brftech/filex-app-ui — the SDK an app's own interface bundles.\n *\n * ```js\n * import { connect } from '@brftech/filex-app-ui';\n *\n * const fx = await connect(); // the handshake with filex\n * const file = await fx.open(); // the file the app was opened with\n * editor.setValue(await file.text());\n * editor.onChange(() => fx.dirty(true));\n * fx.onSave(async () => editor.getValue()); // filex's Save, a draft's \"Save to disk\", Ctrl+S\n * ```\n *\n * No dependency, a few KB. The interface runs in a sandboxed frame with no\n * network and no storage of its own: every call below goes to filex over the\n * MessagePort the handshake set up, and filex decides it (docs/APP-PLUGINS.md\n * → \"An app's own interface\"). The protocol itself is `./protocol`.\n */\nimport {\n BRIDGE_VERSION,\n HELLO,\n LIMITS,\n isPortMessage,\n type BridgeError,\n type ConfirmParams,\n type ErrorCode,\n type FileInfo,\n type HostEvent,\n type HostRequestMessage,\n type Method,\n type ReadResult,\n type SaveAsResult,\n type SaveResult,\n type Session,\n type Theme,\n type ToastParams,\n type DownloadResult,\n type LicenseInfo,\n} from './protocol';\n\nexport * from './protocol';\n\n/** A call filex refused, with the reason as a code. */\nexport class FilexError extends Error {\n readonly code: ErrorCode;\n constructor(err: BridgeError) {\n super(err.message || err.code);\n this.name = 'FilexError';\n this.code = err.code;\n }\n}\n\n/** Content the app hands to a save. */\nexport type SaveData = string | Blob | ArrayBuffer | ArrayBufferView | ReadableStream<Uint8Array>;\n\n/** One opened file, read the way the app needs it. */\nexport interface OpenedFile extends FileInfo {\n text(): Promise<string>;\n bytes(): Promise<ArrayBuffer>;\n stream(): Promise<ReadableStream<Uint8Array>>;\n /** Save new content over this file (a new version, or the draft it is). */\n save(data: SaveData, mime?: string): Promise<SaveResult>;\n}\n\nexport interface ConnectOptions {\n /** How long to wait for filex (default 10 s). */\n timeoutMs?: number;\n /**\n * Put filex's colours (`--fe-*`), `lang`, `dir` and `data-theme` on\n * `<html>`, and keep them current (default true).\n */\n applyTheme?: boolean;\n /** Ctrl/Cmd+S inside the interface runs the save handler (default true). */\n saveShortcut?: boolean;\n}\n\nexport interface FilexApp {\n readonly session: Session;\n /** The file at `index` (default the first) the interface was opened with. */\n open(index?: number): Promise<OpenedFile>;\n /** Save new content over an opened file. */\n save(data: SaveData, opts?: { index?: number; mime?: string }): Promise<SaveResult>;\n /** Save a NEW file; filex asks the person where (its own folder picker). */\n saveAs(name: string, data: SaveData, mime?: string): Promise<SaveAsResult>;\n /** Unsaved changes, or not: filex asks before the person leaves them. */\n dirty(on: boolean): void;\n title(text: string): void;\n toast(text: string, tone?: ToastParams['tone']): void;\n confirm(opts: ConfirmParams | string): Promise<boolean>;\n /** Ask filex to close the interface (it asks first when there are changes). */\n close(): void;\n /** Put text on the clipboard (filex does it, the same way in every browser). */\n copy(text: string): Promise<void>;\n /**\n * Hand the person a file for their own disk (the app needs `ui.download`\n * in its manifest). Call it from a click or a key press: without one,\n * filex asks the person first. `cancelled` when they say no.\n */\n download(name: string, data: SaveData, mime?: string): Promise<DownloadResult>;\n /** Call the app's own module (`ui_call` export) with the opened files. */\n call<T = unknown>(method: string, params?: unknown): Promise<T>;\n /** Queue one of the app's actions on the opened files; answers the op. */\n submit(action: string, params?: Record<string, unknown>): Promise<{ op: unknown }>;\n /** This person's small store for this app (JSON values, 8 KiB each). */\n state: {\n get<T = unknown>(key: string): Promise<T | undefined>;\n set(key: string, value: unknown): Promise<void>;\n };\n /**\n * The app's license (filex 0.52.0; a paid app installed from a store):\n * `{status: \"free\"}` for a free app, `{status: \"valid\", valid_until,\n * updates_until}` for a licensed one. Never the key, never the licensee.\n */\n license: {\n get(): Promise<LicenseInfo>;\n };\n /** Listen to filex. Returns the unsubscribe. */\n on(event: HostEvent, handler: (data: unknown) => void): () => void;\n /**\n * What to save when filex asks (its Save button, a draft's \"Save to disk\",\n * Ctrl+S): return the document and the SDK saves it over the opened file;\n * return nothing if the handler saved by itself.\n */\n onSave(handler: () => SaveData | void | Promise<SaveData | void>): () => void;\n /** Raw call, for a method this SDK has no helper for. */\n request<T = unknown>(method: Method, params?: unknown, transfer?: Transferable[]): Promise<T>;\n}\n\nlet connecting: Promise<FilexApp> | null = null;\n\n/**\n * The handshake. Resolves once filex answered with its port and the session;\n * rejects outside filex or when filex did not answer in time. Calling it again\n * answers the same connection.\n */\nexport function connect(opts: ConnectOptions = {}): Promise<FilexApp> {\n if (!connecting) connecting = open(opts);\n return connecting;\n}\n\nfunction open(opts: ConnectOptions): Promise<FilexApp> {\n return new Promise<FilexApp>((resolve, reject) => {\n if (typeof window === 'undefined' || window.parent === window) {\n reject(new FilexError({ code: 'unavailable', message: 'not running inside filex' }));\n return;\n }\n const parent = window.parent;\n // Where filex runs, when the browser says so (Chrome, Safari). A frame\n // drawn with referrerpolicy=\"no-referrer\" has no document.referrer, and\n // Firefox has no ancestorOrigins: then `source === parent` is the check,\n // which is the one that matters (only the parent window IS the parent).\n const expected = ancestorOrigin();\n const timer = setTimeout(() => {\n window.removeEventListener('message', onMessage);\n reject(new FilexError({ code: 'unavailable', message: 'filex did not answer' }));\n }, opts.timeoutMs ?? LIMITS.connectTimeoutMs);\n\n function onMessage(ev: MessageEvent) {\n if (ev.source !== parent || !isPortMessage(ev.data) || !ev.ports[0]) return;\n if (expected && ev.origin !== expected) return;\n window.removeEventListener('message', onMessage);\n clearTimeout(timer);\n const port = ev.ports[0];\n const app = bridge(port, opts);\n app\n .request<Session>('session.get')\n .then((session) => {\n (app as { session: Session }).session = session;\n if (opts.applyTheme !== false) {\n applyLook(session.theme, session.locale, session.dir);\n app.on('theme', (t) => applyLook(t as Theme));\n app.on('locale', (l) => {\n const v = l as { locale?: string; dir?: 'ltr' | 'rtl' };\n applyLook(undefined, v.locale, v.dir);\n });\n }\n resolve(app);\n })\n .catch(reject);\n }\n window.addEventListener('message', onMessage);\n parent.postMessage({ type: HELLO, v: BRIDGE_VERSION }, '*');\n });\n}\n\nfunction ancestorOrigin(): string {\n try {\n const list = (window.location as Location & { ancestorOrigins?: DOMStringList }).ancestorOrigins;\n if (list && list.length > 0 && list[0] !== 'null') return list[0];\n } catch {\n /* no such API */\n }\n return '';\n}\n\nfunction bridge(port: MessagePort, opts: ConnectOptions): FilexApp {\n let seq = 0;\n const pending = new Map<number, { resolve: (v: unknown) => void; reject: (e: unknown) => void }>();\n const listeners = new Map<HostEvent, Set<(data: unknown) => void>>();\n let saveHandler: (() => SaveData | void | Promise<SaveData | void>) | null = null;\n\n port.onmessage = (ev: MessageEvent) => {\n const m = ev.data as Record<string, unknown> | null;\n if (!m || typeof m !== 'object') return;\n if (typeof m.id === 'number') {\n const p = pending.get(m.id);\n if (!p) return;\n pending.delete(m.id);\n if (m.error) p.reject(new FilexError(m.error as BridgeError));\n else p.resolve(m.result);\n return;\n }\n if (typeof m.hid === 'number') {\n void answerHost(m as unknown as HostRequestMessage);\n return;\n }\n if (typeof m.event === 'string') {\n for (const h of listeners.get(m.event as HostEvent) ?? []) {\n try {\n h(m.data);\n } catch (e) {\n console.error('[filex-app-ui] listener failed', e);\n }\n }\n }\n };\n\n async function answerHost(m: HostRequestMessage) {\n if (m.request === 'save') {\n if (!saveHandler) {\n port.postMessage({ hid: m.hid, error: { code: 'unavailable', message: 'this app has no save handler' } });\n return;\n }\n try {\n const res = await runSave();\n port.postMessage({ hid: m.hid, result: res ?? { saved: true } });\n } catch (e) {\n const err = e instanceof FilexError ? { code: e.code, message: e.message } : { code: 'failed', message: String((e as Error)?.message ?? e) };\n port.postMessage({ hid: m.hid, error: err });\n }\n return;\n }\n port.postMessage({ hid: m.hid, error: { code: 'unknown_method' } });\n }\n\n async function runSave(): Promise<SaveResult | null> {\n if (!saveHandler) return null;\n const data = await saveHandler();\n if (data === undefined || data === null) return null;\n return app.save(data as SaveData);\n }\n\n function request<T>(method: Method, params?: unknown, transfer: Transferable[] = []): Promise<T> {\n const id = ++seq;\n return new Promise<T>((resolve, reject) => {\n pending.set(id, { resolve: resolve as (v: unknown) => void, reject });\n try {\n port.postMessage({ id, method, params }, transfer);\n } catch (e) {\n pending.delete(id);\n reject(e);\n }\n });\n }\n\n function notify(method: Method, params?: unknown) {\n request(method, params).catch((e) => console.warn('[filex-app-ui]', method, e));\n }\n\n async function payload(data: SaveData): Promise<{ data: ReadableStream<Uint8Array> | ArrayBuffer | string; transfer: Transferable[] }> {\n if (typeof data === 'string') return { data, transfer: [] };\n if (data instanceof ArrayBuffer) return { data, transfer: [data] };\n if (ArrayBuffer.isView(data)) {\n const copy = data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength) as ArrayBuffer;\n return { data: copy, transfer: [copy] };\n }\n if (typeof Blob !== 'undefined' && data instanceof Blob) {\n const buf = await data.arrayBuffer();\n return { data: buf, transfer: [buf] };\n }\n if (typeof ReadableStream !== 'undefined' && data instanceof ReadableStream) {\n return { data, transfer: [data as unknown as Transferable] };\n }\n throw new FilexError({ code: 'invalid', message: 'save takes a string, a Blob, bytes or a ReadableStream' });\n }\n\n const app: FilexApp = {\n session: undefined as unknown as Session,\n async open(index = 0) {\n const info = app.session?.files?.[index];\n if (!info) throw new FilexError({ code: 'not_found', message: `no file at ${index}` });\n const read = <K extends keyof ReadResult>(as: 'stream' | 'bytes' | 'text', key: K) =>\n request<ReadResult>('file.read', { index, as }).then((r) => r[key] as NonNullable<ReadResult[K]>);\n return {\n ...info,\n text: () => read('text', 'text'),\n bytes: () => read('bytes', 'bytes'),\n stream: () => read('stream', 'stream'),\n save: (data: SaveData, mime?: string) => app.save(data, { index, mime }),\n };\n },\n async save(data, o = {}) {\n const p = await payload(data);\n return request<SaveResult>('file.save', { index: o.index ?? 0, data: p.data, mime: o.mime }, p.transfer);\n },\n async saveAs(name, data, mime) {\n const p = await payload(data);\n return request<SaveAsResult>('file.saveAs', { name, data: p.data, mime }, p.transfer);\n },\n async download(name, data, mime) {\n const p = await payload(data);\n return request<DownloadResult>('ui.download', { name, data: p.data, mime }, p.transfer);\n },\n dirty(on) {\n notify('ui.dirty', { dirty: !!on });\n },\n title(text) {\n notify('ui.title', { text: String(text).slice(0, LIMITS.maxLineChars) });\n },\n toast(text, tone) {\n notify('ui.toast', { text: String(text).slice(0, LIMITS.maxLineChars), tone });\n },\n confirm(o) {\n const params = typeof o === 'string' ? { text: o } : o;\n return request<boolean>('ui.confirm', params);\n },\n close() {\n notify('ui.close');\n },\n copy(text) {\n return request<void>('clipboard.write', { text: String(text) });\n },\n call<T>(method: string, params?: unknown) {\n return request<T>('engine.call', { method, params });\n },\n submit(action, params) {\n return request<{ op: unknown }>('job.submit', { action, params });\n },\n state: {\n get<T>(key: string) {\n return request<T | undefined>('state.get', { key });\n },\n set(key: string, value: unknown) {\n return request<void>('state.set', { key, value });\n },\n },\n license: {\n get() {\n return request<LicenseInfo>('license.get');\n },\n },\n on(event, handler) {\n let set = listeners.get(event);\n if (!set) listeners.set(event, (set = new Set()));\n set.add(handler);\n return () => set!.delete(handler);\n },\n onSave(handler) {\n saveHandler = handler;\n return () => {\n if (saveHandler === handler) saveHandler = null;\n };\n },\n request,\n };\n\n if (opts.saveShortcut !== false && typeof document !== 'undefined') {\n // ⚠ The keystroke is handled HERE and never forwarded: a frame that could\n // send key events to filex could press filex's own shortcuts. Ctrl+S runs\n // the app's own save handler, which saves through the bridge like any call.\n document.addEventListener(\n 'keydown',\n (e) => {\n if ((e.ctrlKey || e.metaKey) && !e.altKey && !e.shiftKey && (e.key === 's' || e.key === 'S') && saveHandler) {\n e.preventDefault();\n runSave().catch((err) => app.toast(String((err as Error)?.message ?? err), 'error'));\n }\n },\n true,\n );\n }\n return app;\n}\n\n/** Paint filex's look onto the interface's own document. */\nfunction applyLook(theme?: Theme, locale?: string, dir?: 'ltr' | 'rtl') {\n if (typeof document === 'undefined') return;\n const root = document.documentElement;\n if (theme) {\n root.dataset.theme = theme.mode;\n root.style.colorScheme = theme.mode;\n for (const [k, v] of Object.entries(theme.tokens ?? {})) {\n if (/^--fe-[a-z0-9-]+$/.test(k) && typeof v === 'string') root.style.setProperty(k, v);\n }\n }\n if (locale) root.lang = locale;\n if (dir) root.dir = dir;\n}\n"],"names":["BRIDGE_VERSION","HELLO","PORT","METHODS","LIMITS","isHello","d","isPortMessage","isRequest","isKnownMethod","m","FilexError","err","connecting","connect","opts","open","resolve","reject","parent","expected","ancestorOrigin","timer","onMessage","ev","port","app","bridge","session","applyLook","t","v","list","seq","pending","listeners","saveHandler","p","answerHost","h","e","res","runSave","data","request","method","params","transfer","id","notify","payload","copy","buf","index","info","read","as","key","r","mime","o","name","on","text","tone","action","value","event","handler","set","theme","locale","dir","root","k"],"mappings":"AA6BO,MAAMA,IAAiB,GAGjBC,IAAQ,eAERC,IAAO,cAsBPC,IAA6B;AAAA,EACxC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAmPaC,IAAS;AAAA;AAAA,EAEpB,cAAc;AAAA;AAAA,EAEd,eAAe;AAAA;AAAA,EAEf,cAAc;AAAA;AAAA,EAEd,kBAAkB;AAAA;AAAA,EAElB,kBAAkB;AACpB;AAIO,SAASC,EAAQC,GAA+B;AACrD,SAAO,CAAC,CAACA,KAAK,OAAOA,KAAM,YAAaA,EAAmB,SAASL,KAAS,OAAQK,EAAmB,KAAM;AAChH;AAEO,SAASC,EAAcD,GAA8B;AAC1D,SAAO,CAAC,CAACA,KAAK,OAAOA,KAAM,YAAaA,EAAkB,SAASJ;AACrE;AAEO,SAASM,EAAUF,GAAiC;AACzD,SACE,CAAC,CAACA,KACF,OAAOA,KAAM,YACb,OAAQA,EAAqB,MAAO,YACpC,OAAQA,EAAqB,UAAW;AAE5C;AAEO,SAASG,EAAcC,GAAwB;AACpD,SAAQP,EAA8B,SAASO,CAAC;AAClD;ACnTO,MAAMC,UAAmB,MAAM;AAAA,EAEpC,YAAYC,GAAkB;AAC5B,UAAMA,EAAI,WAAWA,EAAI,IAAI,GAC7B,KAAK,OAAO,cACZ,KAAK,OAAOA,EAAI;AAAA,EAClB;AACF;AA8EA,IAAIC,IAAuC;AAOpC,SAASC,EAAQC,IAAuB,IAAuB;AACpE,SAAKF,MAAYA,IAAaG,EAAKD,CAAI,IAChCF;AACT;AAEA,SAASG,EAAKD,GAAyC;AACrD,SAAO,IAAI,QAAkB,CAACE,GAASC,MAAW;AAChD,QAAI,OAAO,SAAW,OAAe,OAAO,WAAW,QAAQ;AAC7D,MAAAA,EAAO,IAAIP,EAAW,EAAE,MAAM,eAAe,SAAS,2BAAA,CAA4B,CAAC;AACnF;AAAA,IACF;AACA,UAAMQ,IAAS,OAAO,QAKhBC,IAAWC,EAAA,GACXC,IAAQ,WAAW,MAAM;AAC7B,aAAO,oBAAoB,WAAWC,CAAS,GAC/CL,EAAO,IAAIP,EAAW,EAAE,MAAM,eAAe,SAAS,uBAAA,CAAwB,CAAC;AAAA,IACjF,GAAGI,EAAK,aAAaX,EAAO,gBAAgB;AAE5C,aAASmB,EAAUC,GAAkB;AAEnC,UADIA,EAAG,WAAWL,KAAU,CAACZ,EAAciB,EAAG,IAAI,KAAK,CAACA,EAAG,MAAM,CAAC,KAC9DJ,KAAYI,EAAG,WAAWJ,EAAU;AACxC,aAAO,oBAAoB,WAAWG,CAAS,GAC/C,aAAaD,CAAK;AAClB,YAAMG,IAAOD,EAAG,MAAM,CAAC,GACjBE,IAAMC,EAAOF,GAAMV,CAAI;AAC7B,MAAAW,EACG,QAAiB,aAAa,EAC9B,KAAK,CAACE,MAAY;AAChB,QAAAF,EAA6B,UAAUE,GACpCb,EAAK,eAAe,OACtBc,EAAUD,EAAQ,OAAOA,EAAQ,QAAQA,EAAQ,GAAG,GACpDF,EAAI,GAAG,SAAS,CAACI,MAAMD,EAAUC,CAAU,CAAC,GAC5CJ,EAAI,GAAG,UAAU,CAAC,MAAM;AACtB,gBAAMK,IAAI;AACV,UAAAF,EAAU,QAAWE,EAAE,QAAQA,EAAE,GAAG;AAAA,QACtC,CAAC,IAEHd,EAAQS,CAAG;AAAA,MACb,CAAC,EACA,MAAMR,CAAM;AAAA,IACjB;AACA,WAAO,iBAAiB,WAAWK,CAAS,GAC5CJ,EAAO,YAAY,EAAE,MAAMlB,GAAO,GAAG,EAAA,GAAkB,GAAG;AAAA,EAC5D,CAAC;AACH;AAEA,SAASoB,IAAyB;AAChC,MAAI;AACF,UAAMW,IAAQ,OAAO,SAA4D;AACjF,QAAIA,KAAQA,EAAK,SAAS,KAAKA,EAAK,CAAC,MAAM,OAAQ,QAAOA,EAAK,CAAC;AAAA,EAClE,QAAQ;AAAA,EAER;AACA,SAAO;AACT;AAEA,SAASL,EAAOF,GAAmBV,GAAgC;AACjE,MAAIkB,IAAM;AACV,QAAMC,wBAAc,IAAA,GACdC,wBAAgB,IAAA;AACtB,MAAIC,IAAyE;AAE7E,EAAAX,EAAK,YAAY,CAACD,MAAqB;AACrC,UAAMd,IAAIc,EAAG;AACb,QAAI,GAACd,KAAK,OAAOA,KAAM,WACvB;AAAA,UAAI,OAAOA,EAAE,MAAO,UAAU;AAC5B,cAAM2B,IAAIH,EAAQ,IAAIxB,EAAE,EAAE;AAC1B,YAAI,CAAC2B,EAAG;AACR,QAAAH,EAAQ,OAAOxB,EAAE,EAAE,GACfA,EAAE,QAAO2B,EAAE,OAAO,IAAI1B,EAAWD,EAAE,KAAoB,CAAC,IACvD2B,EAAE,QAAQ3B,EAAE,MAAM;AACvB;AAAA,MACF;AACA,UAAI,OAAOA,EAAE,OAAQ,UAAU;AAC7B,QAAK4B,EAAW5B,CAAkC;AAClD;AAAA,MACF;AACA,UAAI,OAAOA,EAAE,SAAU;AACrB,mBAAW6B,KAAKJ,EAAU,IAAIzB,EAAE,KAAkB,KAAK;AACrD,cAAI;AACF,YAAA6B,EAAE7B,EAAE,IAAI;AAAA,UACV,SAAS8B,GAAG;AACV,oBAAQ,MAAM,kCAAkCA,CAAC;AAAA,UACnD;AAAA;AAAA,EAGN;AAEA,iBAAeF,EAAW5B,GAAuB;AAC/C,QAAIA,EAAE,YAAY,QAAQ;AACxB,UAAI,CAAC0B,GAAa;AAChB,QAAAX,EAAK,YAAY,EAAE,KAAKf,EAAE,KAAK,OAAO,EAAE,MAAM,eAAe,SAAS,+BAAA,EAA+B,CAAG;AACxG;AAAA,MACF;AACA,UAAI;AACF,cAAM+B,IAAM,MAAMC,EAAA;AAClB,QAAAjB,EAAK,YAAY,EAAE,KAAKf,EAAE,KAAK,QAAQ+B,KAAO,EAAE,OAAO,GAAA,EAAK,CAAG;AAAA,MACjE,SAASD,GAAG;AACV,cAAM5B,IAAM4B,aAAa7B,IAAa,EAAE,MAAM6B,EAAE,MAAM,SAASA,EAAE,YAAY,EAAE,MAAM,UAAU,SAAS,OAAQA,GAAa,WAAWA,CAAC,EAAA;AACzI,QAAAf,EAAK,YAAY,EAAE,KAAKf,EAAE,KAAK,OAAOE,GAAK;AAAA,MAC7C;AACA;AAAA,IACF;AACA,IAAAa,EAAK,YAAY,EAAE,KAAKf,EAAE,KAAK,OAAO,EAAE,MAAM,iBAAA,GAAoB;AAAA,EACpE;AAEA,iBAAegC,IAAsC;AACnD,QAAI,CAACN,EAAa,QAAO;AACzB,UAAMO,IAAO,MAAMP,EAAA;AACnB,WAA0BO,KAAS,OAAa,OACzCjB,EAAI,KAAKiB,CAAgB;AAAA,EAClC;AAEA,WAASC,EAAWC,GAAgBC,GAAkBC,IAA2B,CAAA,GAAgB;AAC/F,UAAMC,IAAK,EAAEf;AACb,WAAO,IAAI,QAAW,CAAChB,GAASC,MAAW;AACzC,MAAAgB,EAAQ,IAAIc,GAAI,EAAE,SAAA/B,GAA0C,QAAAC,GAAQ;AACpE,UAAI;AACF,QAAAO,EAAK,YAAY,EAAE,IAAAuB,GAAI,QAAAH,GAAQ,QAAAC,EAAA,GAAUC,CAAQ;AAAA,MACnD,SAASP,GAAG;AACV,QAAAN,EAAQ,OAAOc,CAAE,GACjB9B,EAAOsB,CAAC;AAAA,MACV;AAAA,IACF,CAAC;AAAA,EACH;AAEA,WAASS,EAAOJ,GAAgBC,GAAkB;AAChD,IAAAF,EAAQC,GAAQC,CAAM,EAAE,MAAM,CAACN,MAAM,QAAQ,KAAK,kBAAkBK,GAAQL,CAAC,CAAC;AAAA,EAChF;AAEA,iBAAeU,EAAQP,GAAgH;AACrI,QAAI,OAAOA,KAAS,SAAU,QAAO,EAAE,MAAAA,GAAM,UAAU,GAAC;AACxD,QAAIA,aAAgB,YAAa,QAAO,EAAE,MAAAA,GAAM,UAAU,CAACA,CAAI,EAAA;AAC/D,QAAI,YAAY,OAAOA,CAAI,GAAG;AAC5B,YAAMQ,IAAOR,EAAK,OAAO,MAAMA,EAAK,YAAYA,EAAK,aAAaA,EAAK,UAAU;AACjF,aAAO,EAAE,MAAMQ,GAAM,UAAU,CAACA,CAAI,EAAA;AAAA,IACtC;AACA,QAAI,OAAO,OAAS,OAAeR,aAAgB,MAAM;AACvD,YAAMS,IAAM,MAAMT,EAAK,YAAA;AACvB,aAAO,EAAE,MAAMS,GAAK,UAAU,CAACA,CAAG,EAAA;AAAA,IACpC;AACA,QAAI,OAAO,iBAAmB,OAAeT,aAAgB;AAC3D,aAAO,EAAE,MAAAA,GAAM,UAAU,CAACA,CAA+B,EAAA;AAE3D,UAAM,IAAIhC,EAAW,EAAE,MAAM,WAAW,SAAS,0DAA0D;AAAA,EAC7G;AAEA,QAAMe,IAAgB;AAAA,IACpB,SAAS;AAAA,IACT,MAAM,KAAK2B,IAAQ,GAAG;AACpB,YAAMC,IAAO5B,EAAI,SAAS,QAAQ2B,CAAK;AACvC,UAAI,CAACC,EAAM,OAAM,IAAI3C,EAAW,EAAE,MAAM,aAAa,SAAS,cAAc0C,CAAK,GAAA,CAAI;AACrF,YAAME,IAAO,CAA6BC,GAAiCC,MACzEb,EAAoB,aAAa,EAAE,OAAAS,GAAO,IAAAG,EAAA,CAAI,EAAE,KAAK,CAACE,MAAMA,EAAED,CAAG,CAA+B;AAClG,aAAO;AAAA,QACL,GAAGH;AAAA,QACH,MAAM,MAAMC,EAAK,QAAQ,MAAM;AAAA,QAC/B,OAAO,MAAMA,EAAK,SAAS,OAAO;AAAA,QAClC,QAAQ,MAAMA,EAAK,UAAU,QAAQ;AAAA,QACrC,MAAM,CAACZ,GAAgBgB,MAAkBjC,EAAI,KAAKiB,GAAM,EAAE,OAAAU,GAAO,MAAAM,EAAA,CAAM;AAAA,MAAA;AAAA,IAE3E;AAAA,IACA,MAAM,KAAKhB,GAAMiB,IAAI,IAAI;AACvB,YAAMvB,IAAI,MAAMa,EAAQP,CAAI;AAC5B,aAAOC,EAAoB,aAAa,EAAE,OAAOgB,EAAE,SAAS,GAAG,MAAMvB,EAAE,MAAM,MAAMuB,EAAE,KAAA,GAAQvB,EAAE,QAAQ;AAAA,IACzG;AAAA,IACA,MAAM,OAAOwB,GAAMlB,GAAMgB,GAAM;AAC7B,YAAMtB,IAAI,MAAMa,EAAQP,CAAI;AAC5B,aAAOC,EAAsB,eAAe,EAAE,MAAAiB,GAAM,MAAMxB,EAAE,MAAM,MAAAsB,EAAA,GAAQtB,EAAE,QAAQ;AAAA,IACtF;AAAA,IACA,MAAM,SAASwB,GAAMlB,GAAMgB,GAAM;AAC/B,YAAMtB,IAAI,MAAMa,EAAQP,CAAI;AAC5B,aAAOC,EAAwB,eAAe,EAAE,MAAAiB,GAAM,MAAMxB,EAAE,MAAM,MAAAsB,EAAA,GAAQtB,EAAE,QAAQ;AAAA,IACxF;AAAA,IACA,MAAMyB,GAAI;AACR,MAAAb,EAAO,YAAY,EAAE,OAAO,CAAC,CAACa,GAAI;AAAA,IACpC;AAAA,IACA,MAAMC,GAAM;AACV,MAAAd,EAAO,YAAY,EAAE,MAAM,OAAOc,CAAI,EAAE,MAAM,GAAG3D,EAAO,YAAY,EAAA,CAAG;AAAA,IACzE;AAAA,IACA,MAAM2D,GAAMC,GAAM;AAChB,MAAAf,EAAO,YAAY,EAAE,MAAM,OAAOc,CAAI,EAAE,MAAM,GAAG3D,EAAO,YAAY,GAAG,MAAA4D,EAAA,CAAM;AAAA,IAC/E;AAAA,IACA,QAAQJ,GAAG;AAET,aAAOhB,EAAiB,cADT,OAAOgB,KAAM,WAAW,EAAE,MAAMA,MAAMA,CACT;AAAA,IAC9C;AAAA,IACA,QAAQ;AACN,MAAAX,EAAO,UAAU;AAAA,IACnB;AAAA,IACA,KAAKc,GAAM;AACT,aAAOnB,EAAc,mBAAmB,EAAE,MAAM,OAAOmB,CAAI,GAAG;AAAA,IAChE;AAAA,IACA,KAAQlB,GAAgBC,GAAkB;AACxC,aAAOF,EAAW,eAAe,EAAE,QAAAC,GAAQ,QAAAC,GAAQ;AAAA,IACrD;AAAA,IACA,OAAOmB,GAAQnB,GAAQ;AACrB,aAAOF,EAAyB,cAAc,EAAE,QAAAqB,GAAQ,QAAAnB,GAAQ;AAAA,IAClE;AAAA,IACA,OAAO;AAAA,MACL,IAAOW,GAAa;AAClB,eAAOb,EAAuB,aAAa,EAAE,KAAAa,GAAK;AAAA,MACpD;AAAA,MACA,IAAIA,GAAaS,GAAgB;AAC/B,eAAOtB,EAAc,aAAa,EAAE,KAAAa,GAAK,OAAAS,GAAO;AAAA,MAClD;AAAA,IAAA;AAAA,IAEF,SAAS;AAAA,MACP,MAAM;AACJ,eAAOtB,EAAqB,aAAa;AAAA,MAC3C;AAAA,IAAA;AAAA,IAEF,GAAGuB,GAAOC,GAAS;AACjB,UAAIC,IAAMlC,EAAU,IAAIgC,CAAK;AAC7B,aAAKE,KAAKlC,EAAU,IAAIgC,GAAQE,IAAM,oBAAI,KAAM,GAChDA,EAAI,IAAID,CAAO,GACR,MAAMC,EAAK,OAAOD,CAAO;AAAA,IAClC;AAAA,IACA,OAAOA,GAAS;AACd,aAAAhC,IAAcgC,GACP,MAAM;AACX,QAAIhC,MAAgBgC,MAAShC,IAAc;AAAA,MAC7C;AAAA,IACF;AAAA,IACA,SAAAQ;AAAA,EAAA;AAGF,SAAI7B,EAAK,iBAAiB,MAAS,OAAO,WAAa,OAIrD,SAAS;AAAA,IACP;AAAA,IACA,CAAC,MAAM;AACL,OAAK,EAAE,WAAW,EAAE,YAAY,CAAC,EAAE,UAAU,CAAC,EAAE,aAAa,EAAE,QAAQ,OAAO,EAAE,QAAQ,QAAQqB,MAC9F,EAAE,eAAA,GACFM,IAAU,MAAM,CAAC9B,MAAQc,EAAI,MAAM,OAAQd,GAAe,WAAWA,CAAG,GAAG,OAAO,CAAC;AAAA,IAEvF;AAAA,IACA;AAAA,EAAA,GAGGc;AACT;AAGA,SAASG,EAAUyC,GAAeC,GAAiBC,GAAqB;AACtE,MAAI,OAAO,WAAa,IAAa;AACrC,QAAMC,IAAO,SAAS;AACtB,MAAIH,GAAO;AACT,IAAAG,EAAK,QAAQ,QAAQH,EAAM,MAC3BG,EAAK,MAAM,cAAcH,EAAM;AAC/B,eAAW,CAACI,GAAG3C,CAAC,KAAK,OAAO,QAAQuC,EAAM,UAAU,CAAA,CAAE;AACpD,MAAI,oBAAoB,KAAKI,CAAC,KAAK,OAAO3C,KAAM,YAAU0C,EAAK,MAAM,YAAYC,GAAG3C,CAAC;AAAA,EAEzF;AACA,EAAIwC,QAAa,OAAOA,IACpBC,QAAU,MAAMA;AACtB;"}
|
|
1
|
+
{"version":3,"file":"filex-app-ui.js","sources":["../src/protocol.ts","../src/index.ts"],"sourcesContent":["/**\n * The bridge between filex and an app's own interface — the ONE definition.\n *\n * An app's interface (the `ui` block of its filex-app.json) runs in a frame\n * filex draws with `sandbox=\"allow-scripts\"`: an opaque origin, no cookies, no\n * storage, no network (the server's CSP says `connect-src 'none'`). Everything\n * it may do goes through this bridge, and the host decides every call.\n *\n * Both halves import this file — the SDK the app bundles (`src/index.ts`) and\n * filex's own frame (`packages/core/src/components/plugin/AppFrame.vue`) — so a\n * method or an event added here is added on both sides at once.\n *\n * The wire, in order (docs/APP-PLUGINS-API.md → \"The interface bridge\"):\n *\n * 1. The app posts `{type: 'filex:hello', v: 1}` to its parent window.\n * 2. The host takes it ONLY when `event.source` is the frame it drew. The\n * origin says nothing here: a sandboxed frame's origin is always \"null\",\n * so any other sandboxed frame on the page would look the same (measured,\n * 2026-09-27, Chrome, Firefox and WebKit).\n * 3. The host answers with a MessagePort: `{type: 'filex:port', v: 1}` and\n * the port transferred. Once per load of the frame; a second hello is\n * ignored.\n * 4. The app takes the port ONLY when `event.source` is its parent window.\n * 5. From then on, only the port: requests `{id, method, params}`, answers\n * `{id, result}` or `{id, error}`, events `{event, data}`, and the host's\n * own requests `{hid, request, params}` answered `{hid, result|error}`.\n */\n\n/** The bridge generation both sides speak. */\nexport const BRIDGE_VERSION = 1;\n\n/** The app's first message, to `window.parent`. */\nexport const HELLO = 'filex:hello';\n/** The host's answer, carrying the port. */\nexport const PORT = 'filex:port';\n\n/** What the app may ask the host. Anything else is `unknown_method`. */\nexport type Method =\n | 'session.get'\n | 'file.read'\n | 'file.save'\n | 'file.saveAs'\n | 'ui.dirty'\n | 'ui.title'\n | 'ui.toast'\n | 'ui.confirm'\n | 'ui.close'\n | 'clipboard.write'\n | 'ui.download'\n | 'engine.call'\n | 'job.submit'\n | 'state.get'\n | 'state.set'\n /** 0.52.0: what the app's license says (a paid app from a store), `LicenseInfo`. */\n | 'license.get'\n /** 0.55.0: filex prints a PDF the app hands it (`PrintParams`, grant `ui:print`). */\n | 'ui.print'\n /**\n * 0.55.0, editing together (`coedit.*`, task #189): the session's log the\n * host seals, orders through filex's relay and opens again. Defined so an\n * app can be written against them; filex 0.55 offers no relay yet, so each\n * answers `unavailable` - edit alone then (see `CoEditHello`).\n */\n | 'coedit.join'\n | 'coedit.subscribe'\n | 'coedit.append'\n | 'coedit.lease'\n | 'coedit.cursor'\n | 'coedit.blob.put'\n | 'coedit.blob.get'\n | 'coedit.leave';\n\nexport const METHODS: readonly Method[] = [\n 'session.get',\n 'file.read',\n 'file.save',\n 'file.saveAs',\n 'ui.dirty',\n 'ui.title',\n 'ui.toast',\n 'ui.confirm',\n 'ui.close',\n 'clipboard.write',\n 'ui.download',\n 'engine.call',\n 'job.submit',\n 'state.get',\n 'state.set',\n 'license.get',\n 'ui.print',\n 'coedit.join',\n 'coedit.subscribe',\n 'coedit.append',\n 'coedit.lease',\n 'coedit.cursor',\n 'coedit.blob.put',\n 'coedit.blob.get',\n 'coedit.leave',\n];\n\n/** What the host tells the app, unasked. */\nexport type HostEvent =\n /** The interface's colours changed (light/dark, palette). `data: Theme`. */\n | 'theme'\n /** The reader's language changed. `data: {locale, dir}`. */\n | 'locale'\n /** The open file changed underneath (another tab, another person). */\n | 'file.changed'\n /** The person closed the frame's surroundings; last chance to say so. */\n | 'close.request'\n /** An administrator installed a new version of this app: reload to use it. */\n | 'app.updated'\n /** Editing together: the next entry of the session's log, opened. `data: CoEditEntry`. */\n | 'coedit.entry'\n /** Editing together: another member's cursor (not kept). `data: CoEditCursor`. */\n | 'coedit.cursor'\n /**\n * Editing together: the host stopped handing entries over (the app fell\n * behind, or the connection dropped). `data: {from}` - subscribe again\n * from the last entry the app holds.\n */\n | 'coedit.dropped';\n\n/** What the host may ask the app (and wait for). */\nexport type HostRequest =\n /** Save now (the host's Save button, a draft's \"Save to disk\"): the app\n * hands its document to `file.save` and answers when it is written. */\n 'save';\n\n/** Why a call was refused. */\nexport type ErrorCode =\n /** The app was not granted what the call needs (files:write for a save…). */\n | 'not_granted'\n /** No such file (an index past the opened files), no such key. */\n | 'not_found'\n /** The file cannot be written: a read-only storage, a view-only opening. */\n | 'read_only'\n /** The call's parameters are wrong. */\n | 'invalid'\n /** Over a limit (LIMITS). */\n | 'too_large'\n /** The server could not be reached or refused; `message` says what. */\n | 'failed'\n /** The person said no (a confirm, a save-as picker closed). */\n | 'cancelled'\n /** The host does not offer this here (no engine, no save handler). */\n | 'unavailable'\n | 'unknown_method';\n\nexport interface BridgeError {\n code: ErrorCode;\n message?: string;\n}\n\n/* ── messages ──────────────────────────────────────────────────────────── */\n\nexport interface HelloMessage {\n type: typeof HELLO;\n v: number;\n}\n\nexport interface PortMessage {\n type: typeof PORT;\n v: number;\n}\n\nexport interface RequestMessage {\n id: number;\n method: Method;\n params?: unknown;\n}\n\nexport interface ResponseMessage {\n id: number;\n result?: unknown;\n error?: BridgeError;\n}\n\nexport interface EventMessage {\n event: HostEvent;\n data?: unknown;\n}\n\nexport interface HostRequestMessage {\n hid: number;\n request: HostRequest;\n params?: unknown;\n}\n\nexport interface HostResponseMessage {\n hid: number;\n result?: unknown;\n error?: BridgeError;\n}\n\n/* ── shapes ────────────────────────────────────────────────────────────── */\n\n/** The interface's look: `mode` and filex's own `--fe-*` custom properties. */\nexport interface Theme {\n mode: 'light' | 'dark';\n /** `--fe-bg`, `--fe-text`, `--fe-accent`… — the SDK sets them on `<html>`. */\n tokens: Record<string, string>;\n}\n\n/** One file the interface was opened with. Never a storage path. */\nexport interface FileInfo {\n index: number;\n name: string;\n /** Lower-case, no dot. */\n ext: string;\n size: number;\n mime: string;\n /** It cannot be saved over: read-only storage, view-only opening, or the\n * app holds no `files:write`. */\n readOnly: boolean;\n /**\n * 0.55.0: the file is end-to-end encrypted - `folder` in an encrypted\n * folder, `vault` in a vault (where only one person edits at a time),\n * `file` for a single encrypted file (`.fxe`). As the server says it;\n * ABSENT for a file that is not encrypted.\n *\n * ⚠ filex 0.55 decrypts nothing for an app: the server holds such a file\n * only as ciphertext and has no key, so `file.read` is refused\n * (`failed` with `encrypted`), a save is refused, and `readOnly` is true.\n * The explorer offers no app an encrypted file in the first place; the\n * field is information, not a promise of plaintext.\n */\n encrypted?: 'folder' | 'vault' | 'file';\n}\n\n/** What `session.get` answers — what the app knows about where it runs. */\nexport interface Session {\n v: number;\n app: { name: string; version: string };\n view: { id: string; placement: 'modal' | 'page' | 'inspector' | 'home' | 'viewer' };\n /** The reader's language tag (`tr`, `pt-br`) and its direction. */\n locale: string;\n dir: 'ltr' | 'rtl';\n theme: Theme;\n /** The person's display name; never their e-mail, never a token. */\n user: { name: string };\n files: FileInfo[];\n /** The permissions this app was granted — so it can hide what it may not do. */\n grants: string[];\n /** The administrator's non-secret settings for this app (grant `settings`). */\n settings?: Record<string, string>;\n}\n\n/** `file.read` params. */\nexport interface ReadParams {\n index?: number;\n /** `stream` (a transferred ReadableStream<Uint8Array>), `bytes` (an\n * ArrayBuffer), or `text` (UTF-8, at most LIMITS.maxTextBytes). */\n as?: 'stream' | 'bytes' | 'text';\n}\n\nexport interface ReadResult {\n name: string;\n size: number;\n mime: string;\n stream?: ReadableStream<Uint8Array>;\n bytes?: ArrayBuffer;\n text?: string;\n}\n\n/** `file.save` params: the new content of the opened file. */\nexport interface SaveParams {\n index?: number;\n /** A transferred ReadableStream<Uint8Array>, an ArrayBuffer or a string. */\n data: ReadableStream<Uint8Array> | ArrayBuffer | string;\n mime?: string;\n /**\n * 0.55.0, editing together: the last log entry (`CoEditEntry.seq`) the\n * saved document holds, so the session knows what is saved. Ignored\n * outside a session.\n */\n through?: number;\n}\n\nexport interface SaveResult {\n saved: true;\n size: number;\n}\n\n/**\n * `ui.download`: a file for the person's own disk. filex does it (a sandboxed\n * frame cannot download), with the app's `ui:download` grant, on a gesture in\n * the frame or the person's yes, never over LIMITS.maxDownloadBytes.\n */\nexport interface DownloadParams {\n /** The file name offered — a name, not a path. */\n name: string;\n /** A transferred ReadableStream<Uint8Array>, an ArrayBuffer or a string. */\n data: ReadableStream<Uint8Array> | ArrayBuffer | string;\n mime?: string;\n}\n\nexport interface DownloadResult {\n saved: true;\n size: number;\n}\n\n/**\n * `ui.print` (filex 0.55.0): a PDF for the browser's print dialog. A\n * sandboxed frame may not open that dialog (measured: Chromium ignores\n * `print()` there and says so, Firefox ignores it), so filex prints the PDF\n * from a page of its own, with the app's `ui:print` grant, never over\n * LIMITS.maxPrintBytes. filex asks every time, above the frame, and the\n * dialog opens only on the person's click on filex's Allow. The print dialog\n * can save the PDF too, so `ui:print` is the same kind of grant as\n * `ui:download`. filex 0.54 and older answer `unknown_method`.\n */\nexport interface PrintParams {\n /** The document's name, for the question filex may ask - a name, not a path. */\n name: string;\n /** The PDF: a transferred ArrayBuffer or ReadableStream<Uint8Array>. */\n data: ReadableStream<Uint8Array> | ArrayBuffer;\n /** `application/pdf` when given; nothing else is printed. */\n mime?: string;\n}\n\nexport interface PrintResult {\n /** The browser's print dialog was opened with the PDF. */\n printed: true;\n size: number;\n}\n\n/* ── editing together (coedit.*, filex 0.55.0: defined, not offered) ────── */\n\n/** What a member appends: the editor's changes, a lock request, a release. */\nexport type CoEditKind = 'changes' | 'lock' | 'release';\n\n/** A member of the session. */\nexport interface CoEditMember {\n /** The relay's id for the connection. */\n client: string;\n /** An opaque id of the person, the same for each of their connections in\n * this session; never an account id. */\n user: string;\n /** The person's display name. */\n name: string;\n /** The editor's per-session user index: given once, never twice. */\n indexUser: number;\n /** May append changes (an editor role and the right to change the file). */\n canEdit: boolean;\n}\n\n/** `coedit.join` params: join (or start) the session of an opened file. */\nexport interface CoEditJoinParams {\n index?: number;\n}\n\n/**\n * What `coedit.join` answers: who the app is in the session and how far\n * the log has gone. The entries come as `coedit.entry` events once the app\n * subscribes (`coedit.subscribe` from 0 for the whole log). `unavailable`\n * when filex offers no editing together here (filex 0.55, a vault, a file\n * that cannot be shared): the app edits alone.\n */\nexport interface CoEditHello {\n session: string;\n me: CoEditMember;\n /** The last entry's seq (0: an empty log). */\n head: number;\n /** The last `changes` entry's seq. */\n changesHead: number;\n /** The last entry the file on the storage holds. */\n savedThrough: number;\n}\n\n/** `coedit.subscribe` params: entries after `from` arrive as `coedit.entry`. */\nexport interface CoEditSubscribeParams {\n from: number;\n}\n\n/** `coedit.append` params: the host seals it and the relay places it. */\nexport interface CoEditAppendParams {\n kind: CoEditKind;\n body: unknown;\n}\n\nexport interface CoEditAppendResult {\n /** Where it landed; it comes back as a `coedit.entry` too. */\n seq: number;\n}\n\n/** `coedit.lease`: the changes lease (one writer at a time, one that has\n * seen every change). */\nexport interface CoEditLeaseParams {\n op: 'acquire' | 'release';\n /** The seq of the last `changes` entry the app applied. */\n changesSeen: number;\n}\n\nexport interface CoEditLeaseResult {\n granted: boolean;\n}\n\n/** `coedit.cursor` params: passed to the other members, not kept. */\nexport interface CoEditCursorParams {\n cursor: unknown;\n}\n\n/** `coedit.blob.put` / `coedit.blob.get`: the session's sealed blobs (the\n * base document, images added while editing). A name, not a path. */\nexport interface CoEditBlobPutParams {\n name: string;\n data: ArrayBuffer;\n}\n\nexport interface CoEditBlobGetParams {\n name: string;\n}\n\nexport interface CoEditBlobGetResult {\n name: string;\n bytes: ArrayBuffer;\n}\n\ninterface CoEditEntryBase {\n seq: number;\n /** The relay's time, Unix ms - the same for every member. */\n at: number;\n client: string;\n}\n\n/** One entry of the session's log, opened and checked by filex. */\nexport type CoEditEntry =\n | (CoEditEntryBase & { kind: CoEditKind; body: unknown })\n | (CoEditEntryBase & { kind: 'join'; member: CoEditMember })\n | (CoEditEntryBase & { kind: 'leave' })\n | (CoEditEntryBase & { kind: 'saved'; through: number });\n\n/** `coedit.cursor` event data. */\nexport interface CoEditCursor {\n client: string;\n cursor: unknown;\n}\n\n/**\n * `file.saveAs`: a NEW file, in a folder the person picks in filex's own\n * folder dialog (the one its Move to… uses), which opens in the opened\n * file's folder. Closing the dialog answers `cancelled`.\n */\nexport interface SaveAsParams {\n /** The file name — a name, not a path. */\n name: string;\n data: ReadableStream<Uint8Array> | ArrayBuffer | string;\n mime?: string;\n}\n\nexport interface SaveAsResult {\n saved: true;\n name: string;\n size: number;\n}\n\nexport interface ToastParams {\n text: string;\n tone?: 'info' | 'success' | 'warning' | 'error';\n}\n\nexport interface ConfirmParams {\n title?: string;\n text: string;\n confirm?: string;\n cancel?: string;\n danger?: boolean;\n}\n\n/**\n * `license.get` (filex 0.52.0): what the app's license says. A paid app is\n * installed from a store, which issues its license and is asked about it every\n * day; filex keeps the key and never hands it to the app. `status`:\n *\n * - `free`: not a paid app;\n * - `valid`: the store said valid at its last check;\n * - `grace`: valid, but the store could not be asked since - it holds until\n * the grace the store signed ends;\n * - anything else (`revoked`, `expired`, `invalid`, `seats_exhausted`,\n * `wrong_app`, `grace_expired`, `missing`, `unverified`): the app is held\n * and its interface is not served, so an app rarely reads one of these.\n *\n * `valid_until` / `updates_until` are there for a valid license (dates as\n * RFC 3339). Who holds the license is not the app's to read. filex 0.51.0\n * and older answer `unknown_method`.\n */\nexport interface LicenseInfo {\n status: string;\n valid_until?: string;\n updates_until?: string;\n}\n\n/** `engine.call`: the app's own module, `ui_call` export. */\nexport interface EngineCallParams {\n method: string;\n params?: unknown;\n}\n\n/** `job.submit`: queue one of the app's actions on the opened files. */\nexport interface JobSubmitParams {\n action: string;\n params?: Record<string, unknown>;\n}\n\n/* ── limits ────────────────────────────────────────────────────────────── */\n\nexport const LIMITS = {\n /** `file.read({as: 'text'})` refuses a file larger than this; read a stream. */\n maxTextBytes: 32 << 20,\n /** One `state.set` value, as JSON. */\n maxStateBytes: 8 << 10,\n /** A `ui.toast` / `ui.title` text, in characters. */\n maxLineChars: 300,\n /** How long the SDK waits for the host's port. */\n connectTimeoutMs: 10_000,\n /** The largest file `ui.download` hands the person (streamed or not). */\n maxDownloadBytes: 256 << 20,\n /**\n * The largest PDF `ui.print` prints (filex 0.55). Lower than a download:\n * a download can stream to disk, a print is held whole in the page (one\n * Blob) until the print dialog has read it, and a document worth printing\n * is far smaller.\n */\n maxPrintBytes: 64 << 20,\n} as const;\n\n/* ── guards ────────────────────────────────────────────────────────────── */\n\nexport function isHello(d: unknown): d is HelloMessage {\n return !!d && typeof d === 'object' && (d as HelloMessage).type === HELLO && typeof (d as HelloMessage).v === 'number';\n}\n\nexport function isPortMessage(d: unknown): d is PortMessage {\n return !!d && typeof d === 'object' && (d as PortMessage).type === PORT;\n}\n\nexport function isRequest(d: unknown): d is RequestMessage {\n return (\n !!d &&\n typeof d === 'object' &&\n typeof (d as RequestMessage).id === 'number' &&\n typeof (d as RequestMessage).method === 'string'\n );\n}\n\nexport function isKnownMethod(m: string): m is Method {\n return (METHODS as readonly string[]).includes(m);\n}\n","/**\n * @brftech/filex-app-ui — the SDK an app's own interface bundles.\n *\n * ```js\n * import { connect } from '@brftech/filex-app-ui';\n *\n * const fx = await connect(); // the handshake with filex\n * const file = await fx.open(); // the file the app was opened with\n * editor.setValue(await file.text());\n * editor.onChange(() => fx.dirty(true));\n * fx.onSave(async () => editor.getValue()); // filex's Save, a draft's \"Save to disk\", Ctrl+S\n * ```\n *\n * No dependency, a few KB. The interface runs in a sandboxed frame with no\n * network and no storage of its own: every call below goes to filex over the\n * MessagePort the handshake set up, and filex decides it (docs/APP-PLUGINS.md\n * → \"An app's own interface\"). The protocol itself is `./protocol`.\n */\nimport {\n BRIDGE_VERSION,\n HELLO,\n LIMITS,\n isPortMessage,\n type BridgeError,\n type ConfirmParams,\n type ErrorCode,\n type FileInfo,\n type HostEvent,\n type HostRequestMessage,\n type Method,\n type ReadResult,\n type SaveAsResult,\n type SaveResult,\n type Session,\n type Theme,\n type ToastParams,\n type DownloadResult,\n type LicenseInfo,\n type PrintResult,\n type CoEditHello,\n type CoEditKind,\n type CoEditAppendResult,\n type CoEditLeaseResult,\n type CoEditBlobGetResult,\n} from './protocol';\n\nexport * from './protocol';\n\n/** A call filex refused, with the reason as a code. */\nexport class FilexError extends Error {\n readonly code: ErrorCode;\n constructor(err: BridgeError) {\n super(err.message || err.code);\n this.name = 'FilexError';\n this.code = err.code;\n }\n}\n\n/** Content the app hands to a save. */\nexport type SaveData = string | Blob | ArrayBuffer | ArrayBufferView | ReadableStream<Uint8Array>;\n\n/** One opened file, read the way the app needs it. */\nexport interface OpenedFile extends FileInfo {\n text(): Promise<string>;\n bytes(): Promise<ArrayBuffer>;\n stream(): Promise<ReadableStream<Uint8Array>>;\n /** Save new content over this file (a new version, or the draft it is). */\n save(data: SaveData, mime?: string): Promise<SaveResult>;\n}\n\nexport interface ConnectOptions {\n /** How long to wait for filex (default 10 s). */\n timeoutMs?: number;\n /**\n * Put filex's colours (`--fe-*`), `lang`, `dir` and `data-theme` on\n * `<html>`, and keep them current (default true).\n */\n applyTheme?: boolean;\n /** Ctrl/Cmd+S inside the interface runs the save handler (default true). */\n saveShortcut?: boolean;\n}\n\nexport interface FilexApp {\n readonly session: Session;\n /** The file at `index` (default the first) the interface was opened with. */\n open(index?: number): Promise<OpenedFile>;\n /** Save new content over an opened file. */\n save(data: SaveData, opts?: { index?: number; mime?: string }): Promise<SaveResult>;\n /** Save a NEW file; filex asks the person where (its own folder picker). */\n saveAs(name: string, data: SaveData, mime?: string): Promise<SaveAsResult>;\n /** Unsaved changes, or not: filex asks before the person leaves them. */\n dirty(on: boolean): void;\n title(text: string): void;\n toast(text: string, tone?: ToastParams['tone']): void;\n confirm(opts: ConfirmParams | string): Promise<boolean>;\n /** Ask filex to close the interface (it asks first when there are changes). */\n close(): void;\n /** Put text on the clipboard (filex does it, the same way in every browser). */\n copy(text: string): Promise<void>;\n /**\n * Hand the person a file for their own disk (the app needs `ui.download`\n * in its manifest). Call it from a click or a key press: without one,\n * filex asks the person first. `cancelled` when they say no.\n */\n download(name: string, data: SaveData, mime?: string): Promise<DownloadResult>;\n /**\n * Print a PDF (filex 0.55; the app needs `ui.print` in its manifest): a\n * sandboxed frame may not open the print dialog, so filex prints it from\n * its own page. filex asks the person every time, and the dialog opens on\n * their click on filex's Allow; at most LIMITS.maxPrintBytes. filex 0.54\n * and older answer `unknown_method`, and a host whose print page may not\n * be shown `unavailable`: hand the PDF over with `download` then.\n */\n print(name: string, pdf: Blob | ArrayBuffer | ArrayBufferView | ReadableStream<Uint8Array>): Promise<PrintResult>;\n /**\n * Editing together (filex 0.55: defined, not offered - every call answers\n * `unavailable`, and the app edits alone). filex seals what the app\n * appends, orders it through its relay and hands every member the same\n * entries in the same order (`coedit.entry` events after `subscribe`);\n * the app never holds a key.\n */\n coedit: {\n join(index?: number): Promise<CoEditHello>;\n subscribe(from: number): Promise<void>;\n append(kind: CoEditKind, body: unknown): Promise<CoEditAppendResult>;\n lease(op: 'acquire' | 'release', changesSeen: number): Promise<boolean>;\n cursor(cursor: unknown): void;\n putBlob(name: string, data: Blob | ArrayBuffer | ArrayBufferView): Promise<void>;\n getBlob(name: string): Promise<ArrayBuffer>;\n leave(): Promise<void>;\n };\n /** Call the app's own module (`ui_call` export) with the opened files. */\n call<T = unknown>(method: string, params?: unknown): Promise<T>;\n /** Queue one of the app's actions on the opened files; answers the op. */\n submit(action: string, params?: Record<string, unknown>): Promise<{ op: unknown }>;\n /** This person's small store for this app (JSON values, 8 KiB each). */\n state: {\n get<T = unknown>(key: string): Promise<T | undefined>;\n set(key: string, value: unknown): Promise<void>;\n };\n /**\n * The app's license (filex 0.52.0; a paid app installed from a store):\n * `{status: \"free\"}` for a free app, `{status: \"valid\", valid_until,\n * updates_until}` for a licensed one. Never the key, never the licensee.\n */\n license: {\n get(): Promise<LicenseInfo>;\n };\n /** Listen to filex. Returns the unsubscribe. */\n on(event: HostEvent, handler: (data: unknown) => void): () => void;\n /**\n * What to save when filex asks (its Save button, a draft's \"Save to disk\",\n * Ctrl+S): return the document and the SDK saves it over the opened file;\n * return nothing if the handler saved by itself.\n */\n onSave(handler: () => SaveData | void | Promise<SaveData | void>): () => void;\n /** Raw call, for a method this SDK has no helper for. */\n request<T = unknown>(method: Method, params?: unknown, transfer?: Transferable[]): Promise<T>;\n}\n\nlet connecting: Promise<FilexApp> | null = null;\n\n/**\n * The handshake. Resolves once filex answered with its port and the session;\n * rejects outside filex or when filex did not answer in time. Calling it again\n * answers the same connection.\n */\nexport function connect(opts: ConnectOptions = {}): Promise<FilexApp> {\n if (!connecting) connecting = open(opts);\n return connecting;\n}\n\nfunction open(opts: ConnectOptions): Promise<FilexApp> {\n return new Promise<FilexApp>((resolve, reject) => {\n if (typeof window === 'undefined' || window.parent === window) {\n reject(new FilexError({ code: 'unavailable', message: 'not running inside filex' }));\n return;\n }\n const parent = window.parent;\n // Where filex runs, when the browser says so (Chrome, Safari). A frame\n // drawn with referrerpolicy=\"no-referrer\" has no document.referrer, and\n // Firefox has no ancestorOrigins: then `source === parent` is the check,\n // which is the one that matters (only the parent window IS the parent).\n const expected = ancestorOrigin();\n const timer = setTimeout(() => {\n window.removeEventListener('message', onMessage);\n reject(new FilexError({ code: 'unavailable', message: 'filex did not answer' }));\n }, opts.timeoutMs ?? LIMITS.connectTimeoutMs);\n\n function onMessage(ev: MessageEvent) {\n if (ev.source !== parent || !isPortMessage(ev.data) || !ev.ports[0]) return;\n if (expected && ev.origin !== expected) return;\n window.removeEventListener('message', onMessage);\n clearTimeout(timer);\n const port = ev.ports[0];\n const app = bridge(port, opts);\n app\n .request<Session>('session.get')\n .then((session) => {\n (app as { session: Session }).session = session;\n if (opts.applyTheme !== false) {\n applyLook(session.theme, session.locale, session.dir);\n app.on('theme', (t) => applyLook(t as Theme));\n app.on('locale', (l) => {\n const v = l as { locale?: string; dir?: 'ltr' | 'rtl' };\n applyLook(undefined, v.locale, v.dir);\n });\n }\n resolve(app);\n })\n .catch(reject);\n }\n window.addEventListener('message', onMessage);\n parent.postMessage({ type: HELLO, v: BRIDGE_VERSION }, '*');\n });\n}\n\nfunction ancestorOrigin(): string {\n try {\n const list = (window.location as Location & { ancestorOrigins?: DOMStringList }).ancestorOrigins;\n if (list && list.length > 0 && list[0] !== 'null') return list[0];\n } catch {\n /* no such API */\n }\n return '';\n}\n\nfunction bridge(port: MessagePort, opts: ConnectOptions): FilexApp {\n let seq = 0;\n const pending = new Map<number, { resolve: (v: unknown) => void; reject: (e: unknown) => void }>();\n const listeners = new Map<HostEvent, Set<(data: unknown) => void>>();\n let saveHandler: (() => SaveData | void | Promise<SaveData | void>) | null = null;\n\n port.onmessage = (ev: MessageEvent) => {\n const m = ev.data as Record<string, unknown> | null;\n if (!m || typeof m !== 'object') return;\n if (typeof m.id === 'number') {\n const p = pending.get(m.id);\n if (!p) return;\n pending.delete(m.id);\n if (m.error) p.reject(new FilexError(m.error as BridgeError));\n else p.resolve(m.result);\n return;\n }\n if (typeof m.hid === 'number') {\n void answerHost(m as unknown as HostRequestMessage);\n return;\n }\n if (typeof m.event === 'string') {\n for (const h of listeners.get(m.event as HostEvent) ?? []) {\n try {\n h(m.data);\n } catch (e) {\n console.error('[filex-app-ui] listener failed', e);\n }\n }\n }\n };\n\n async function answerHost(m: HostRequestMessage) {\n if (m.request === 'save') {\n if (!saveHandler) {\n port.postMessage({ hid: m.hid, error: { code: 'unavailable', message: 'this app has no save handler' } });\n return;\n }\n try {\n const res = await runSave();\n port.postMessage({ hid: m.hid, result: res ?? { saved: true } });\n } catch (e) {\n const err = e instanceof FilexError ? { code: e.code, message: e.message } : { code: 'failed', message: String((e as Error)?.message ?? e) };\n port.postMessage({ hid: m.hid, error: err });\n }\n return;\n }\n port.postMessage({ hid: m.hid, error: { code: 'unknown_method' } });\n }\n\n async function runSave(): Promise<SaveResult | null> {\n if (!saveHandler) return null;\n const data = await saveHandler();\n if (data === undefined || data === null) return null;\n return app.save(data as SaveData);\n }\n\n function request<T>(method: Method, params?: unknown, transfer: Transferable[] = []): Promise<T> {\n const id = ++seq;\n return new Promise<T>((resolve, reject) => {\n pending.set(id, { resolve: resolve as (v: unknown) => void, reject });\n try {\n port.postMessage({ id, method, params }, transfer);\n } catch (e) {\n pending.delete(id);\n reject(e);\n }\n });\n }\n\n function notify(method: Method, params?: unknown) {\n request(method, params).catch((e) => console.warn('[filex-app-ui]', method, e));\n }\n\n async function payload(data: SaveData): Promise<{ data: ReadableStream<Uint8Array> | ArrayBuffer | string; transfer: Transferable[] }> {\n if (typeof data === 'string') return { data, transfer: [] };\n if (data instanceof ArrayBuffer) return { data, transfer: [data] };\n if (ArrayBuffer.isView(data)) {\n const copy = data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength) as ArrayBuffer;\n return { data: copy, transfer: [copy] };\n }\n if (typeof Blob !== 'undefined' && data instanceof Blob) {\n const buf = await data.arrayBuffer();\n return { data: buf, transfer: [buf] };\n }\n if (typeof ReadableStream !== 'undefined' && data instanceof ReadableStream) {\n return { data, transfer: [data as unknown as Transferable] };\n }\n throw new FilexError({ code: 'invalid', message: 'save takes a string, a Blob, bytes or a ReadableStream' });\n }\n\n const app: FilexApp = {\n session: undefined as unknown as Session,\n async open(index = 0) {\n const info = app.session?.files?.[index];\n if (!info) throw new FilexError({ code: 'not_found', message: `no file at ${index}` });\n const read = <K extends keyof ReadResult>(as: 'stream' | 'bytes' | 'text', key: K) =>\n request<ReadResult>('file.read', { index, as }).then((r) => r[key] as NonNullable<ReadResult[K]>);\n return {\n ...info,\n text: () => read('text', 'text'),\n bytes: () => read('bytes', 'bytes'),\n stream: () => read('stream', 'stream'),\n save: (data: SaveData, mime?: string) => app.save(data, { index, mime }),\n };\n },\n async save(data, o = {}) {\n const p = await payload(data);\n return request<SaveResult>('file.save', { index: o.index ?? 0, data: p.data, mime: o.mime }, p.transfer);\n },\n async saveAs(name, data, mime) {\n const p = await payload(data);\n return request<SaveAsResult>('file.saveAs', { name, data: p.data, mime }, p.transfer);\n },\n async download(name, data, mime) {\n const p = await payload(data);\n return request<DownloadResult>('ui.download', { name, data: p.data, mime }, p.transfer);\n },\n async print(name, pdf) {\n const p = await payload(pdf);\n return request<PrintResult>('ui.print', { name, data: p.data, mime: 'application/pdf' }, p.transfer);\n },\n coedit: {\n join(index = 0) {\n return request<CoEditHello>('coedit.join', { index });\n },\n subscribe(from: number) {\n return request<void>('coedit.subscribe', { from });\n },\n append(kind: CoEditKind, body: unknown) {\n return request<CoEditAppendResult>('coedit.append', { kind, body });\n },\n async lease(op: 'acquire' | 'release', changesSeen: number) {\n const r = await request<CoEditLeaseResult>('coedit.lease', { op, changesSeen });\n return !!r?.granted;\n },\n cursor(cursor: unknown) {\n notify('coedit.cursor', { cursor });\n },\n async putBlob(name: string, data: Blob | ArrayBuffer | ArrayBufferView) {\n const p = await payload(data);\n if (!(p.data instanceof ArrayBuffer)) throw new FilexError({ code: 'invalid', message: 'a blob is bytes' });\n await request<void>('coedit.blob.put', { name, data: p.data }, p.transfer);\n },\n async getBlob(name: string) {\n const r = await request<CoEditBlobGetResult>('coedit.blob.get', { name });\n return r.bytes;\n },\n async leave() {\n await request<void>('coedit.leave');\n },\n },\n dirty(on) {\n notify('ui.dirty', { dirty: !!on });\n },\n title(text) {\n notify('ui.title', { text: String(text).slice(0, LIMITS.maxLineChars) });\n },\n toast(text, tone) {\n notify('ui.toast', { text: String(text).slice(0, LIMITS.maxLineChars), tone });\n },\n confirm(o) {\n const params = typeof o === 'string' ? { text: o } : o;\n return request<boolean>('ui.confirm', params);\n },\n close() {\n notify('ui.close');\n },\n copy(text) {\n return request<void>('clipboard.write', { text: String(text) });\n },\n call<T>(method: string, params?: unknown) {\n return request<T>('engine.call', { method, params });\n },\n submit(action, params) {\n return request<{ op: unknown }>('job.submit', { action, params });\n },\n state: {\n get<T>(key: string) {\n return request<T | undefined>('state.get', { key });\n },\n set(key: string, value: unknown) {\n return request<void>('state.set', { key, value });\n },\n },\n license: {\n get() {\n return request<LicenseInfo>('license.get');\n },\n },\n on(event, handler) {\n let set = listeners.get(event);\n if (!set) listeners.set(event, (set = new Set()));\n set.add(handler);\n return () => set!.delete(handler);\n },\n onSave(handler) {\n saveHandler = handler;\n return () => {\n if (saveHandler === handler) saveHandler = null;\n };\n },\n request,\n };\n\n if (opts.saveShortcut !== false && typeof document !== 'undefined') {\n // ⚠ The keystroke is handled HERE and never forwarded: a frame that could\n // send key events to filex could press filex's own shortcuts. Ctrl+S runs\n // the app's own save handler, which saves through the bridge like any call.\n document.addEventListener(\n 'keydown',\n (e) => {\n if ((e.ctrlKey || e.metaKey) && !e.altKey && !e.shiftKey && (e.key === 's' || e.key === 'S') && saveHandler) {\n e.preventDefault();\n runSave().catch((err) => app.toast(String((err as Error)?.message ?? err), 'error'));\n }\n },\n true,\n );\n }\n return app;\n}\n\n/** Paint filex's look onto the interface's own document. */\nfunction applyLook(theme?: Theme, locale?: string, dir?: 'ltr' | 'rtl') {\n if (typeof document === 'undefined') return;\n const root = document.documentElement;\n if (theme) {\n root.dataset.theme = theme.mode;\n root.style.colorScheme = theme.mode;\n for (const [k, v] of Object.entries(theme.tokens ?? {})) {\n if (/^--fe-[a-z0-9-]+$/.test(k) && typeof v === 'string') root.style.setProperty(k, v);\n }\n }\n if (locale) root.lang = locale;\n if (dir) root.dir = dir;\n}\n"],"names":["BRIDGE_VERSION","HELLO","PORT","METHODS","LIMITS","isHello","d","isPortMessage","isRequest","isKnownMethod","m","FilexError","err","connecting","connect","opts","open","resolve","reject","parent","expected","ancestorOrigin","timer","onMessage","ev","port","app","bridge","session","applyLook","t","l","v","list","seq","pending","listeners","saveHandler","p","answerHost","h","e","res","runSave","data","request","method","params","transfer","id","notify","payload","copy","buf","index","info","read","as","key","r","mime","o","name","pdf","from","kind","body","op","changesSeen","cursor","on","text","tone","action","value","event","handler","set","theme","locale","dir","root","k"],"mappings":"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"}
|