@brftech/filex-app-ui 0.48.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/LICENSE +21 -0
- package/README.md +50 -0
- package/dist/filex-app-ui.iife.js +2 -0
- package/dist/filex-app-ui.iife.js.map +1 -0
- package/dist/filex-app-ui.js +258 -0
- package/dist/filex-app-ui.js.map +1 -0
- package/dist/index.d.ts +336 -0
- package/package.json +56 -0
- package/src/index.ts +384 -0
- package/src/protocol.ts +320 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 BRF Tech
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# @brftech/filex-app-ui
|
|
2
|
+
|
|
3
|
+
The SDK an app's own interface bundles to talk to [filex](https://github.com/BRF-Tech/filex).
|
|
4
|
+
|
|
5
|
+
An app may bring its own HTML/JS/CSS interface. filex serves it from the app's
|
|
6
|
+
approved package and runs it in a sandboxed frame whose policy allows no
|
|
7
|
+
connection, no storage and no cookie (a browser cannot fully stop a page
|
|
8
|
+
sending data out — see filex's APP-PLUGINS.md). Everything the interface does
|
|
9
|
+
goes through filex over one checked channel, and this package is that channel: a few KB, no dependency.
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npm install @brftech/filex-app-ui
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
```js
|
|
16
|
+
import { connect } from '@brftech/filex-app-ui';
|
|
17
|
+
|
|
18
|
+
const fx = await connect(); // the handshake; rejects outside filex
|
|
19
|
+
const file = await fx.open(); // { name, ext, size, mime, readOnly, text(), bytes(), stream(), save() }
|
|
20
|
+
editor.value = await file.text();
|
|
21
|
+
editor.addEventListener('input', () => fx.dirty(true));
|
|
22
|
+
fx.onSave(() => editor.value); // filex's Save, a draft's "Save to disk", Ctrl+S
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Without a bundler, ship `dist/filex-app-ui.iife.js` in your bundle and load it
|
|
26
|
+
with a `<script src>`: it defines `window.FilexAppUI.connect`.
|
|
27
|
+
|
|
28
|
+
| Call | What it does |
|
|
29
|
+
|---|---|
|
|
30
|
+
| `fx.session` | the app and view, the reader's language and direction, filex's colours, the person's display name, the files, the grants |
|
|
31
|
+
| `fx.open(i)` / `fx.save(data)` / `fx.saveAs(name, data)` | read an opened file; save over it (a new version, or the draft it is); save a new file where the person picks |
|
|
32
|
+
| `fx.dirty(on)`, `fx.title(t)`, `fx.toast(t, tone)`, `fx.confirm(opts)`, `fx.close()` | tell filex about unsaved changes; ask filex to draw a line, a question, to close (a toast and a question carry your app's name) |
|
|
33
|
+
| `fx.copy(text)` | put text on the clipboard — call it from a click or a key press: without one, filex asks the person first |
|
|
34
|
+
| `fx.download(name, data, mime?)` | hand the person a file for their own disk (`"ui": { "download": true }` in the manifest) — from a click, or filex asks; 256 MiB at most; Chromium streams it into the file they pick |
|
|
35
|
+
| `fx.call(method, params)` | ask the app's own module (`ui_call`) |
|
|
36
|
+
| `fx.submit(action, params)` | queue one of the app's actions on the opened files — like `copy`, from a click, or filex asks |
|
|
37
|
+
| `fx.state.get(key)` / `fx.state.set(key, value)` | a small store for this app and this person |
|
|
38
|
+
| `fx.on('theme' \| 'locale' \| 'file.changed' \| 'close.request' \| 'app.updated', fn)` | listen to filex |
|
|
39
|
+
|
|
40
|
+
`connect()` paints filex's colours (`--fe-*` custom properties), `lang`, `dir`
|
|
41
|
+
and `data-theme` onto `<html>` and keeps them current; pass
|
|
42
|
+
`{ applyTheme: false }` to do it yourself.
|
|
43
|
+
|
|
44
|
+
The whole contract — manifest, serving policy, every message — is
|
|
45
|
+
`docs/APP-PLUGINS-API.md` → "An app's own interface"; how to build and package
|
|
46
|
+
an interface is `docs/PLUGIN-KIT.md` → "Writing an interface". The protocol
|
|
47
|
+
itself is `src/protocol.ts` (also exported as `@brftech/filex-app-ui/protocol`),
|
|
48
|
+
which filex's own frame imports, so the two sides cannot drift.
|
|
49
|
+
|
|
50
|
+
MIT © BRF Tech
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
var FilexAppUI=function(o){"use strict";const S="filex:hello",O="filex:port",L=["session.get","file.read","file.save","file.saveAs","ui.dirty","ui.title","ui.toast","ui.confirm","ui.close","clipboard.write","ui.download","engine.call","job.submit","state.get","state.set"],h={maxTextBytes:33554432,maxStateBytes:8192,maxLineChars:300,connectTimeoutMs:1e4,maxDownloadBytes:268435456};function x(n){return!!n&&typeof n=="object"&&n.type===S&&typeof n.v=="number"}function R(n){return!!n&&typeof n=="object"&&n.type===O}function B(n){return!!n&&typeof n=="object"&&typeof n.id=="number"&&typeof n.method=="string"}function I(n){return L.includes(n)}class p extends Error{constructor(u){super(u.message||u.code),this.name="FilexError",this.code=u.code}}let E=null;function T(n={}){return E||(E=k(n)),E}function k(n){return new Promise((u,y)=>{if(typeof window>"u"||window.parent===window){y(new p({code:"unavailable",message:"not running inside filex"}));return}const a=window.parent,l=D(),c=setTimeout(()=>{window.removeEventListener("message",w),y(new p({code:"unavailable",message:"filex did not answer"}))},n.timeoutMs??h.connectTimeoutMs);function w(m){if(m.source!==a||!R(m.data)||!m.ports[0]||l&&m.origin!==l)return;window.removeEventListener("message",w),clearTimeout(c);const i=m.ports[0],f=H(i,n);f.request("session.get").then(g=>{f.session=g,n.applyTheme!==!1&&(M(g.theme,g.locale,g.dir),f.on("theme",d=>M(d)),f.on("locale",d=>{const e=d;M(void 0,e.locale,e.dir)})),u(f)}).catch(y)}window.addEventListener("message",w),a.postMessage({type:S,v:1},"*")})}function D(){try{const n=window.location.ancestorOrigins;if(n&&n.length>0&&n[0]!=="null")return n[0]}catch{}return""}function H(n,u){let y=0;const a=new Map,l=new Map;let c=null;n.onmessage=e=>{const t=e.data;if(!(!t||typeof t!="object")){if(typeof t.id=="number"){const s=a.get(t.id);if(!s)return;a.delete(t.id),t.error?s.reject(new p(t.error)):s.resolve(t.result);return}if(typeof t.hid=="number"){w(t);return}if(typeof t.event=="string")for(const s of l.get(t.event)??[])try{s(t.data)}catch(r){console.error("[filex-app-ui] listener failed",r)}}};async function w(e){if(e.request==="save"){if(!c){n.postMessage({hid:e.hid,error:{code:"unavailable",message:"this app has no save handler"}});return}try{const t=await m();n.postMessage({hid:e.hid,result:t??{saved:!0}})}catch(t){const s=t instanceof p?{code:t.code,message:t.message}:{code:"failed",message:String(t?.message??t)};n.postMessage({hid:e.hid,error:s})}return}n.postMessage({hid:e.hid,error:{code:"unknown_method"}})}async function m(){if(!c)return null;const e=await c();return e==null?null:d.save(e)}function i(e,t,s=[]){const r=++y;return new Promise((b,v)=>{a.set(r,{resolve:b,reject:v});try{n.postMessage({id:r,method:e,params:t},s)}catch(P){a.delete(r),v(P)}})}function f(e,t){i(e,t).catch(s=>console.warn("[filex-app-ui]",e,s))}async function g(e){if(typeof e=="string")return{data:e,transfer:[]};if(e instanceof ArrayBuffer)return{data:e,transfer:[e]};if(ArrayBuffer.isView(e)){const t=e.buffer.slice(e.byteOffset,e.byteOffset+e.byteLength);return{data:t,transfer:[t]}}if(typeof Blob<"u"&&e instanceof Blob){const t=await e.arrayBuffer();return{data:t,transfer:[t]}}if(typeof ReadableStream<"u"&&e instanceof ReadableStream)return{data:e,transfer:[e]};throw new p({code:"invalid",message:"save takes a string, a Blob, bytes or a ReadableStream"})}const d={session:void 0,async open(e=0){const t=d.session?.files?.[e];if(!t)throw new p({code:"not_found",message:`no file at ${e}`});const s=(r,b)=>i("file.read",{index:e,as:r}).then(v=>v[b]);return{...t,text:()=>s("text","text"),bytes:()=>s("bytes","bytes"),stream:()=>s("stream","stream"),save:(r,b)=>d.save(r,{index:e,mime:b})}},async save(e,t={}){const s=await g(e);return i("file.save",{index:t.index??0,data:s.data,mime:t.mime},s.transfer)},async saveAs(e,t,s){const r=await g(t);return i("file.saveAs",{name:e,data:r.data,mime:s},r.transfer)},async download(e,t,s){const r=await g(t);return i("ui.download",{name:e,data:r.data,mime:s},r.transfer)},dirty(e){f("ui.dirty",{dirty:!!e})},title(e){f("ui.title",{text:String(e).slice(0,h.maxLineChars)})},toast(e,t){f("ui.toast",{text:String(e).slice(0,h.maxLineChars),tone:t})},confirm(e){return i("ui.confirm",typeof e=="string"?{text:e}:e)},close(){f("ui.close")},copy(e){return i("clipboard.write",{text:String(e)})},call(e,t){return i("engine.call",{method:e,params:t})},submit(e,t){return i("job.submit",{action:e,params:t})},state:{get(e){return i("state.get",{key:e})},set(e,t){return i("state.set",{key:e,value:t})}},on(e,t){let s=l.get(e);return s||l.set(e,s=new Set),s.add(t),()=>s.delete(t)},onSave(e){return c=e,()=>{c===e&&(c=null)}},request:i};return u.saveShortcut!==!1&&typeof document<"u"&&document.addEventListener("keydown",e=>{(e.ctrlKey||e.metaKey)&&!e.altKey&&!e.shiftKey&&(e.key==="s"||e.key==="S")&&c&&(e.preventDefault(),m().catch(t=>d.toast(String(t?.message??t),"error")))},!0),d}function M(n,u,y){if(typeof document>"u")return;const a=document.documentElement;if(n){a.dataset.theme=n.mode,a.style.colorScheme=n.mode;for(const[l,c]of Object.entries(n.tokens??{}))/^--fe-[a-z0-9-]+$/.test(l)&&typeof c=="string"&&a.style.setProperty(l,c)}u&&(a.lang=u),y&&(a.dir=y)}return o.BRIDGE_VERSION=1,o.FilexError=p,o.HELLO=S,o.LIMITS=h,o.METHODS=L,o.PORT=O,o.connect=T,o.isHello=x,o.isKnownMethod=I,o.isPortMessage=R,o.isRequest=B,Object.defineProperty(o,Symbol.toStringTag,{value:"Module"}),o}({});
|
|
2
|
+
//# sourceMappingURL=filex-app-ui.iife.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"filex-app-ui.iife.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\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];\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/** `file.saveAs`: a NEW file, somewhere the person picks in filex's own dialog. */\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\nexport interface SaveAsParams {\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/** `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} 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 /** 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 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":["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","on","text","tone","action","value","event","handler","set","theme","locale","dir","root","k"],"mappings":"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"}
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
const k = 1, S = "filex:hello", E = "filex:port", L = [
|
|
2
|
+
"session.get",
|
|
3
|
+
"file.read",
|
|
4
|
+
"file.save",
|
|
5
|
+
"file.saveAs",
|
|
6
|
+
"ui.dirty",
|
|
7
|
+
"ui.title",
|
|
8
|
+
"ui.toast",
|
|
9
|
+
"ui.confirm",
|
|
10
|
+
"ui.close",
|
|
11
|
+
"clipboard.write",
|
|
12
|
+
"ui.download",
|
|
13
|
+
"engine.call",
|
|
14
|
+
"job.submit",
|
|
15
|
+
"state.get",
|
|
16
|
+
"state.set"
|
|
17
|
+
], x = {
|
|
18
|
+
/** `file.read({as: 'text'})` refuses a file larger than this; read a stream. */
|
|
19
|
+
maxTextBytes: 33554432,
|
|
20
|
+
/** One `state.set` value, as JSON. */
|
|
21
|
+
maxStateBytes: 8192,
|
|
22
|
+
/** A `ui.toast` / `ui.title` text, in characters. */
|
|
23
|
+
maxLineChars: 300,
|
|
24
|
+
/** How long the SDK waits for the host's port. */
|
|
25
|
+
connectTimeoutMs: 1e4,
|
|
26
|
+
/** The largest file `ui.download` hands the person (streamed or not). */
|
|
27
|
+
maxDownloadBytes: 268435456
|
|
28
|
+
};
|
|
29
|
+
function I(n) {
|
|
30
|
+
return !!n && typeof n == "object" && n.type === S && typeof n.v == "number";
|
|
31
|
+
}
|
|
32
|
+
function B(n) {
|
|
33
|
+
return !!n && typeof n == "object" && n.type === E;
|
|
34
|
+
}
|
|
35
|
+
function j(n) {
|
|
36
|
+
return !!n && typeof n == "object" && typeof n.id == "number" && typeof n.method == "string";
|
|
37
|
+
}
|
|
38
|
+
function q(n) {
|
|
39
|
+
return L.includes(n);
|
|
40
|
+
}
|
|
41
|
+
class p extends Error {
|
|
42
|
+
constructor(c) {
|
|
43
|
+
super(c.message || c.code), this.name = "FilexError", this.code = c.code;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
let b = null;
|
|
47
|
+
function A(n = {}) {
|
|
48
|
+
return b || (b = O(n)), b;
|
|
49
|
+
}
|
|
50
|
+
function O(n) {
|
|
51
|
+
return new Promise((c, d) => {
|
|
52
|
+
if (typeof window > "u" || window.parent === window) {
|
|
53
|
+
d(new p({ code: "unavailable", message: "not running inside filex" }));
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
const i = window.parent, u = R(), a = setTimeout(() => {
|
|
57
|
+
window.removeEventListener("message", g), d(new p({ code: "unavailable", message: "filex did not answer" }));
|
|
58
|
+
}, n.timeoutMs ?? x.connectTimeoutMs);
|
|
59
|
+
function g(y) {
|
|
60
|
+
if (y.source !== i || !B(y.data) || !y.ports[0] || u && y.origin !== u) return;
|
|
61
|
+
window.removeEventListener("message", g), clearTimeout(a);
|
|
62
|
+
const o = y.ports[0], f = T(o, n);
|
|
63
|
+
f.request("session.get").then((m) => {
|
|
64
|
+
f.session = m, n.applyTheme !== !1 && (v(m.theme, m.locale, m.dir), f.on("theme", (l) => v(l)), f.on("locale", (l) => {
|
|
65
|
+
const e = l;
|
|
66
|
+
v(void 0, e.locale, e.dir);
|
|
67
|
+
})), c(f);
|
|
68
|
+
}).catch(d);
|
|
69
|
+
}
|
|
70
|
+
window.addEventListener("message", g), i.postMessage({ type: S, v: 1 }, "*");
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
function R() {
|
|
74
|
+
try {
|
|
75
|
+
const n = window.location.ancestorOrigins;
|
|
76
|
+
if (n && n.length > 0 && n[0] !== "null") return n[0];
|
|
77
|
+
} catch {
|
|
78
|
+
}
|
|
79
|
+
return "";
|
|
80
|
+
}
|
|
81
|
+
function T(n, c) {
|
|
82
|
+
let d = 0;
|
|
83
|
+
const i = /* @__PURE__ */ new Map(), u = /* @__PURE__ */ new Map();
|
|
84
|
+
let a = null;
|
|
85
|
+
n.onmessage = (e) => {
|
|
86
|
+
const t = e.data;
|
|
87
|
+
if (!(!t || typeof t != "object")) {
|
|
88
|
+
if (typeof t.id == "number") {
|
|
89
|
+
const s = i.get(t.id);
|
|
90
|
+
if (!s) return;
|
|
91
|
+
i.delete(t.id), t.error ? s.reject(new p(t.error)) : s.resolve(t.result);
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
if (typeof t.hid == "number") {
|
|
95
|
+
g(t);
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
if (typeof t.event == "string")
|
|
99
|
+
for (const s of u.get(t.event) ?? [])
|
|
100
|
+
try {
|
|
101
|
+
s(t.data);
|
|
102
|
+
} catch (r) {
|
|
103
|
+
console.error("[filex-app-ui] listener failed", r);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
async function g(e) {
|
|
108
|
+
if (e.request === "save") {
|
|
109
|
+
if (!a) {
|
|
110
|
+
n.postMessage({ hid: e.hid, error: { code: "unavailable", message: "this app has no save handler" } });
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
try {
|
|
114
|
+
const t = await y();
|
|
115
|
+
n.postMessage({ hid: e.hid, result: t ?? { saved: !0 } });
|
|
116
|
+
} catch (t) {
|
|
117
|
+
const s = t instanceof p ? { code: t.code, message: t.message } : { code: "failed", message: String(t?.message ?? t) };
|
|
118
|
+
n.postMessage({ hid: e.hid, error: s });
|
|
119
|
+
}
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
n.postMessage({ hid: e.hid, error: { code: "unknown_method" } });
|
|
123
|
+
}
|
|
124
|
+
async function y() {
|
|
125
|
+
if (!a) return null;
|
|
126
|
+
const e = await a();
|
|
127
|
+
return e == null ? null : l.save(e);
|
|
128
|
+
}
|
|
129
|
+
function o(e, t, s = []) {
|
|
130
|
+
const r = ++d;
|
|
131
|
+
return new Promise((w, h) => {
|
|
132
|
+
i.set(r, { resolve: w, reject: h });
|
|
133
|
+
try {
|
|
134
|
+
n.postMessage({ id: r, method: e, params: t }, s);
|
|
135
|
+
} catch (M) {
|
|
136
|
+
i.delete(r), h(M);
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
function f(e, t) {
|
|
141
|
+
o(e, t).catch((s) => console.warn("[filex-app-ui]", e, s));
|
|
142
|
+
}
|
|
143
|
+
async function m(e) {
|
|
144
|
+
if (typeof e == "string") return { data: e, transfer: [] };
|
|
145
|
+
if (e instanceof ArrayBuffer) return { data: e, transfer: [e] };
|
|
146
|
+
if (ArrayBuffer.isView(e)) {
|
|
147
|
+
const t = e.buffer.slice(e.byteOffset, e.byteOffset + e.byteLength);
|
|
148
|
+
return { data: t, transfer: [t] };
|
|
149
|
+
}
|
|
150
|
+
if (typeof Blob < "u" && e instanceof Blob) {
|
|
151
|
+
const t = await e.arrayBuffer();
|
|
152
|
+
return { data: t, transfer: [t] };
|
|
153
|
+
}
|
|
154
|
+
if (typeof ReadableStream < "u" && e instanceof ReadableStream)
|
|
155
|
+
return { data: e, transfer: [e] };
|
|
156
|
+
throw new p({ code: "invalid", message: "save takes a string, a Blob, bytes or a ReadableStream" });
|
|
157
|
+
}
|
|
158
|
+
const l = {
|
|
159
|
+
session: void 0,
|
|
160
|
+
async open(e = 0) {
|
|
161
|
+
const t = l.session?.files?.[e];
|
|
162
|
+
if (!t) throw new p({ code: "not_found", message: `no file at ${e}` });
|
|
163
|
+
const s = (r, w) => o("file.read", { index: e, as: r }).then((h) => h[w]);
|
|
164
|
+
return {
|
|
165
|
+
...t,
|
|
166
|
+
text: () => s("text", "text"),
|
|
167
|
+
bytes: () => s("bytes", "bytes"),
|
|
168
|
+
stream: () => s("stream", "stream"),
|
|
169
|
+
save: (r, w) => l.save(r, { index: e, mime: w })
|
|
170
|
+
};
|
|
171
|
+
},
|
|
172
|
+
async save(e, t = {}) {
|
|
173
|
+
const s = await m(e);
|
|
174
|
+
return o("file.save", { index: t.index ?? 0, data: s.data, mime: t.mime }, s.transfer);
|
|
175
|
+
},
|
|
176
|
+
async saveAs(e, t, s) {
|
|
177
|
+
const r = await m(t);
|
|
178
|
+
return o("file.saveAs", { name: e, data: r.data, mime: s }, r.transfer);
|
|
179
|
+
},
|
|
180
|
+
async download(e, t, s) {
|
|
181
|
+
const r = await m(t);
|
|
182
|
+
return o("ui.download", { name: e, data: r.data, mime: s }, r.transfer);
|
|
183
|
+
},
|
|
184
|
+
dirty(e) {
|
|
185
|
+
f("ui.dirty", { dirty: !!e });
|
|
186
|
+
},
|
|
187
|
+
title(e) {
|
|
188
|
+
f("ui.title", { text: String(e).slice(0, x.maxLineChars) });
|
|
189
|
+
},
|
|
190
|
+
toast(e, t) {
|
|
191
|
+
f("ui.toast", { text: String(e).slice(0, x.maxLineChars), tone: t });
|
|
192
|
+
},
|
|
193
|
+
confirm(e) {
|
|
194
|
+
return o("ui.confirm", typeof e == "string" ? { text: e } : e);
|
|
195
|
+
},
|
|
196
|
+
close() {
|
|
197
|
+
f("ui.close");
|
|
198
|
+
},
|
|
199
|
+
copy(e) {
|
|
200
|
+
return o("clipboard.write", { text: String(e) });
|
|
201
|
+
},
|
|
202
|
+
call(e, t) {
|
|
203
|
+
return o("engine.call", { method: e, params: t });
|
|
204
|
+
},
|
|
205
|
+
submit(e, t) {
|
|
206
|
+
return o("job.submit", { action: e, params: t });
|
|
207
|
+
},
|
|
208
|
+
state: {
|
|
209
|
+
get(e) {
|
|
210
|
+
return o("state.get", { key: e });
|
|
211
|
+
},
|
|
212
|
+
set(e, t) {
|
|
213
|
+
return o("state.set", { key: e, value: t });
|
|
214
|
+
}
|
|
215
|
+
},
|
|
216
|
+
on(e, t) {
|
|
217
|
+
let s = u.get(e);
|
|
218
|
+
return s || u.set(e, s = /* @__PURE__ */ new Set()), s.add(t), () => s.delete(t);
|
|
219
|
+
},
|
|
220
|
+
onSave(e) {
|
|
221
|
+
return a = e, () => {
|
|
222
|
+
a === e && (a = null);
|
|
223
|
+
};
|
|
224
|
+
},
|
|
225
|
+
request: o
|
|
226
|
+
};
|
|
227
|
+
return c.saveShortcut !== !1 && typeof document < "u" && document.addEventListener(
|
|
228
|
+
"keydown",
|
|
229
|
+
(e) => {
|
|
230
|
+
(e.ctrlKey || e.metaKey) && !e.altKey && !e.shiftKey && (e.key === "s" || e.key === "S") && a && (e.preventDefault(), y().catch((t) => l.toast(String(t?.message ?? t), "error")));
|
|
231
|
+
},
|
|
232
|
+
!0
|
|
233
|
+
), l;
|
|
234
|
+
}
|
|
235
|
+
function v(n, c, d) {
|
|
236
|
+
if (typeof document > "u") return;
|
|
237
|
+
const i = document.documentElement;
|
|
238
|
+
if (n) {
|
|
239
|
+
i.dataset.theme = n.mode, i.style.colorScheme = n.mode;
|
|
240
|
+
for (const [u, a] of Object.entries(n.tokens ?? {}))
|
|
241
|
+
/^--fe-[a-z0-9-]+$/.test(u) && typeof a == "string" && i.style.setProperty(u, a);
|
|
242
|
+
}
|
|
243
|
+
c && (i.lang = c), d && (i.dir = d);
|
|
244
|
+
}
|
|
245
|
+
export {
|
|
246
|
+
k as BRIDGE_VERSION,
|
|
247
|
+
p as FilexError,
|
|
248
|
+
S as HELLO,
|
|
249
|
+
x as LIMITS,
|
|
250
|
+
L as METHODS,
|
|
251
|
+
E as PORT,
|
|
252
|
+
A as connect,
|
|
253
|
+
I as isHello,
|
|
254
|
+
q as isKnownMethod,
|
|
255
|
+
B as isPortMessage,
|
|
256
|
+
j as isRequest
|
|
257
|
+
};
|
|
258
|
+
//# sourceMappingURL=filex-app-ui.js.map
|
|
@@ -0,0 +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\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];\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/** `file.saveAs`: a NEW file, somewhere the person picks in filex's own dialog. */\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\nexport interface SaveAsParams {\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/** `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} 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 /** 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 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,cAoBPC,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;AACF,GAuNaC,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;ACrRO,MAAMC,UAAmB,MAAM;AAAA,EAEpC,YAAYC,GAAkB;AAC5B,UAAMA,EAAI,WAAWA,EAAI,IAAI,GAC7B,KAAK,OAAO,cACZ,KAAK,OAAOA,EAAI;AAAA,EAClB;AACF;AAsEA,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,GAAGC,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;"}
|