flexdesk 0.14.1 → 0.15.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/README.md CHANGED
@@ -283,7 +283,9 @@ what a tile's factory returned. To draw the page's levels, call
283
283
  `renderBreadcrumb(container, segments)` from `@flexdesk/wm`, with
284
284
  `segments = [{ label, icon?, onClick? }]`. It uses the tile breadcrumb's
285
285
  markup, so it looks the same, and each `onClick` is yours: the last segment,
286
- and any without `onClick`, is drawn as plain text.
286
+ and any without `onClick`, is drawn as plain text. The current segment's item
287
+ carries `aria-current="page"`, and every label has a `title` with its full text,
288
+ so a label your CSS truncates still reads whole (0.15.1).
287
289
 
288
290
  ### Column sizing
289
291
 
@@ -1751,9 +1753,41 @@ import { createPhotinoHost } from '@flexdesk/host';
1751
1753
  const host = createPhotinoHost({ state: myServerState, onEvent: (name, value) => {} });
1752
1754
  ```
1753
1755
 
1754
- Its `window.chrome()` answers `'native'`, `dialogs.saveFile` falls back to a
1755
- Blob download when the window refuses, and the messages the C# side has to
1756
- answer are in [`src/host/photino_host.md`](src/host/photino_host.md).
1756
+ `dialogs.saveFile` falls back to a Blob download when the window refuses, and
1757
+ the messages the C# side has to answer are in
1758
+ [`src/host/photino_host.md`](src/host/photino_host.md).
1759
+
1760
+ ### A window with its own chrome (0.15.0)
1761
+
1762
+ The same app runs three ways, and the page needs no switch for it:
1763
+
1764
+ | | title bar and borders | FlexDesk draws |
1765
+ |---|---|---|
1766
+ | a browser tab | the browser's | nothing |
1767
+ | a Photino window, native frame | the OS's | nothing |
1768
+ | a Photino window, custom frame | none: your top bar is the title bar | minimise / maximise / close, resize handles, drag |
1769
+
1770
+ ```js
1771
+ import { WindowChromeController } from '@flexdesk/widgets';
1772
+
1773
+ new WindowChromeController({
1774
+ host, // createPhotinoHost(), or any Host
1775
+ topBar: '.my-top-bar', // drags the window (default .twm-global-top-bar)
1776
+ controls: '.my-top-bar .right', // where the three buttons go (default: the bar's .twm-bar-right)
1777
+ noDrag: '.my-search', // more that never starts a drag
1778
+ }).initialize(); // resolves true only with custom chrome
1779
+ ```
1780
+
1781
+ `host.window.chrome()` decides. The Photino host asks its native side once
1782
+ (`host.describe`, its `frame` field) and answers `'custom'` only when the
1783
+ window was made frameless, so a browser tab and a native-frame window get no
1784
+ handles, no buttons and no CSS. With custom chrome, a drag on the top bar and
1785
+ a press on a resize handle go to the host's `startNativeDrag()` /
1786
+ `startNativeResize(edge)`. On Windows the native side hands them to the OS,
1787
+ so the window snaps, takes Win+arrows and resizes like any other.
1788
+ [`photino_host.md`](src/host/photino_host.md) has the native half: keep the
1789
+ frame styles, return the whole window from `WM_NCCALCSIZE`, start gestures with
1790
+ `WM_NCLBUTTONDOWN`. Don't use Photino's `SetChromeless`, which loses all of that.
1757
1791
 
1758
1792
  ## Peer dependencies
1759
1793
 
package/css/flexdesk.css CHANGED
@@ -1920,6 +1920,40 @@ body.twm-dt-col-resizing {
1920
1920
  body.twm-window-maximized .twm-resize-handle {
1921
1921
  display: none;
1922
1922
  }
1923
+ /* Where the handles are. WindowChromeController creates them only for a
1924
+ window with custom chrome, so none of this reaches a browser tab. */
1925
+ .twm-resize-handle { background: transparent; }
1926
+ .twm-resize-handle.right { top: 0; right: 0; width: 6px; height: 100%; cursor: ew-resize; }
1927
+ .twm-resize-handle.left { top: 0; left: 0; width: 6px; height: 100%; cursor: ew-resize; }
1928
+ .twm-resize-handle.top { top: 0; left: 0; width: 100%; height: 6px; cursor: ns-resize; }
1929
+ .twm-resize-handle.bottom { bottom: 0; left: 0; width: 100%; height: 6px; cursor: ns-resize; }
1930
+ .twm-resize-handle.corner { width: 12px; height: 12px; z-index: 10000; }
1931
+ .twm-resize-handle.corner.br { bottom: 0; right: 0; cursor: nwse-resize; }
1932
+ .twm-resize-handle.corner.tl { top: 0; left: 0; cursor: nwse-resize; }
1933
+ .twm-resize-handle.corner.bl { bottom: 0; left: 0; cursor: nesw-resize; }
1934
+ .twm-resize-handle.corner.tr { top: 0; right: 0; cursor: nesw-resize; }
1935
+ /* Minimise, maximise/restore, close: drawn by WindowChromeController at the
1936
+ right of the top bar, for a window with custom chrome only. */
1937
+ .twm-window-controls {
1938
+ display: flex;
1939
+ align-items: stretch;
1940
+ align-self: stretch;
1941
+ margin-left: 6px;
1942
+ }
1943
+ .twm-window-controls__btn {
1944
+ width: 46px;
1945
+ border: 0;
1946
+ padding: 0;
1947
+ background: transparent;
1948
+ color: var(--text-secondary, #ccc);
1949
+ display: flex;
1950
+ align-items: center;
1951
+ justify-content: center;
1952
+ cursor: default;
1953
+ }
1954
+ .twm-window-controls__btn .material-symbols-outlined { font-size: 18px; }
1955
+ .twm-window-controls__btn:hover { background: rgba(255, 255, 255, 0.08); }
1956
+ .twm-window-controls__btn--close:hover { background: #c42b1c; color: #fff; }
1923
1957
  /* Shared sidebar controls (filter + icon buttons) - used by data, functions, etc. */
1924
1958
  .twm-sidebar-controls {
1925
1959
  display: flex;
package/dist/flexdesk.css CHANGED
@@ -1920,6 +1920,40 @@ body.twm-dt-col-resizing {
1920
1920
  body.twm-window-maximized .twm-resize-handle {
1921
1921
  display: none;
1922
1922
  }
1923
+ /* Where the handles are. WindowChromeController creates them only for a
1924
+ window with custom chrome, so none of this reaches a browser tab. */
1925
+ .twm-resize-handle { background: transparent; }
1926
+ .twm-resize-handle.right { top: 0; right: 0; width: 6px; height: 100%; cursor: ew-resize; }
1927
+ .twm-resize-handle.left { top: 0; left: 0; width: 6px; height: 100%; cursor: ew-resize; }
1928
+ .twm-resize-handle.top { top: 0; left: 0; width: 100%; height: 6px; cursor: ns-resize; }
1929
+ .twm-resize-handle.bottom { bottom: 0; left: 0; width: 100%; height: 6px; cursor: ns-resize; }
1930
+ .twm-resize-handle.corner { width: 12px; height: 12px; z-index: 10000; }
1931
+ .twm-resize-handle.corner.br { bottom: 0; right: 0; cursor: nwse-resize; }
1932
+ .twm-resize-handle.corner.tl { top: 0; left: 0; cursor: nwse-resize; }
1933
+ .twm-resize-handle.corner.bl { bottom: 0; left: 0; cursor: nesw-resize; }
1934
+ .twm-resize-handle.corner.tr { top: 0; right: 0; cursor: nesw-resize; }
1935
+ /* Minimise, maximise/restore, close: drawn by WindowChromeController at the
1936
+ right of the top bar, for a window with custom chrome only. */
1937
+ .twm-window-controls {
1938
+ display: flex;
1939
+ align-items: stretch;
1940
+ align-self: stretch;
1941
+ margin-left: 6px;
1942
+ }
1943
+ .twm-window-controls__btn {
1944
+ width: 46px;
1945
+ border: 0;
1946
+ padding: 0;
1947
+ background: transparent;
1948
+ color: var(--text-secondary, #ccc);
1949
+ display: flex;
1950
+ align-items: center;
1951
+ justify-content: center;
1952
+ cursor: default;
1953
+ }
1954
+ .twm-window-controls__btn .material-symbols-outlined { font-size: 18px; }
1955
+ .twm-window-controls__btn:hover { background: rgba(255, 255, 255, 0.08); }
1956
+ .twm-window-controls__btn--close:hover { background: #c42b1c; color: #fff; }
1923
1957
  /* Shared sidebar controls (filter + icon buttons) - used by data, functions, etc. */
1924
1958
  .twm-sidebar-controls {
1925
1959
  display: flex;
package/dist/host.js CHANGED
@@ -14,7 +14,8 @@ var HOST_CONTRACT = Object.freeze({
14
14
  "getBounds",
15
15
  "setBounds"
16
16
  ]),
17
- optional: Object.freeze(["startNativeDrag"])
17
+ // `chrome()` answers 'native' or 'custom', or a promise of one.
18
+ optional: Object.freeze(["startNativeDrag", "startNativeResize"])
18
19
  }),
19
20
  dialogs: Object.freeze({ required: Object.freeze(["saveFile"]), optional: Object.freeze([]) }),
20
21
  state: Object.freeze({ required: Object.freeze(["read", "write"]), optional: Object.freeze([]) })
@@ -177,7 +178,8 @@ function createPhotinoHost(opts = {}) {
177
178
  external = typeof window !== "undefined" ? window.external : void 0,
178
179
  timeoutMs = 1e4,
179
180
  onEvent = null,
180
- logger = console
181
+ logger = console,
182
+ chrome = "auto"
181
183
  } = opts;
182
184
  if (!isPhotinoWindow(external)) return NULL_HOST;
183
185
  const { call, request } = createPhotinoTransport({ external, timeoutMs, onEvent, logger });
@@ -193,11 +195,30 @@ function createPhotinoHost(opts = {}) {
193
195
  const fire = (cap, op, ...args) => {
194
196
  void soft(null, cap, op, ...args);
195
197
  };
198
+ let frame = chrome === "custom" || chrome === "native" ? chrome : null;
199
+ let asking = null;
200
+ const askFrame = () => {
201
+ if (frame) return frame;
202
+ asking ??= request("host", "describe", [], { wait: 3e3 }).then((d) => d && d.frame === "custom" ? "custom" : "native", () => "native").then((f) => {
203
+ frame = f;
204
+ return f;
205
+ });
206
+ return asking;
207
+ };
208
+ const gesture = async (op, ...args) => {
209
+ try {
210
+ await request("window", op, args, { wait: 0 });
211
+ return true;
212
+ } catch (e) {
213
+ return false;
214
+ }
215
+ };
196
216
  const defaults = {
197
217
  window: {
198
- // Photino draws a decorated, native window on every OS: FlexDesk's
199
- // own min/max/close buttons would be a second set.
200
- chrome: () => "native",
218
+ // 'native' (the OS draws the frame: FlexDesk's own buttons would be
219
+ // a second set) or 'custom' (the native side made the window
220
+ // frameless). A promise until the native side has said which.
221
+ chrome: () => askFrame(),
201
222
  minimize: () => {
202
223
  fire("window", "minimize");
203
224
  },
@@ -213,6 +234,8 @@ function createPhotinoHost(opts = {}) {
213
234
  if (!b || typeof b !== "object") return null;
214
235
  return { x: b.x, y: b.y, width: b.width, height: b.height };
215
236
  },
237
+ startNativeDrag: () => gesture("startDrag"),
238
+ startNativeResize: (edge) => gesture("startResize", String(edge || "")),
216
239
  // Not async, like pywebview's: a rAF hot path during a drag; nobody awaits.
217
240
  setBounds({ x = null, y = null, width = null, height = null } = {}) {
218
241
  fire("window", "setBounds", { x, y, width, height });
package/dist/host.js.map CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../src/host/host.js", "../src/host/pywebview_host.js", "../src/host/photino_host.js"],
4
- "sourcesContent": ["/**\n * host.js \u2014 the host port.\n *\n * The framework declares what it needs from its embedder. A Host is a bag of\n * INDEPENDENTLY OPTIONAL capability objects:\n *\n * - absent capability => absent key (never a stub)\n * - present capability => must be complete (validated at boot)\n *\n * There is NO module-level default host and NO getHost(). Hosts are injected \u2014\n * a module singleton here would just be `window.pywebview` wearing a hat, and\n * would make a second host (mock, browser tab, VS Code webview) impossible.\n *\n * Zero pywebview. Zero EcoAgent. See pywebview_host.js for the reference\n * adapter that implements this port against the pywebview / HTTP transports.\n */\n\nexport const HOST_CONTRACT = Object.freeze({\n window: Object.freeze({\n required: Object.freeze(['chrome', 'minimize', 'close', 'isMaximized', 'setMaximized',\n 'isFullscreen', 'toggleFullscreen', 'getBounds', 'setBounds']),\n optional: Object.freeze(['startNativeDrag']),\n }),\n dialogs: Object.freeze({ required: Object.freeze(['saveFile']), optional: Object.freeze([]) }),\n state: Object.freeze({ required: Object.freeze(['read', 'write']), optional: Object.freeze([]) }),\n});\n\n/**\n * Validate ONE capability object. We validate the capability the adapter\n * authored \u2014 a plain object literal with real closures \u2014 not the raw bridge:\n * `index.html`'s browser-mode polyfill installs a `new Proxy({}, {get})` that\n * returns a function for EVERY property name, so duck-typing the bridge is\n * vacuously true and validates nothing. A Proxy cannot forge a missing key in\n * an object literal.\n *\n * @returns {{ ok: boolean, missing: string[] }}\n */\nexport function validateCapability(name, cap) {\n const spec = HOST_CONTRACT[name];\n if (!spec) return { ok: false, missing: [`unknown capability '${name}'`] };\n if (!cap || typeof cap !== 'object') return { ok: false, missing: [...spec.required] };\n const missing = spec.required.filter((m) => typeof cap[m] !== 'function');\n return { ok: missing.length === 0, missing };\n}\n\n/**\n * Validate + freeze a set of capabilities into a Host.\n * Throws at boot on an incomplete capability \u2014 a half-wired host is a bug,\n * and a bug at boot beats a silent degrade at click time.\n *\n * @param {{ window?: object, dialogs?: object, state?: object }} caps\n * @returns {Readonly<object>} the Host\n */\nexport function createHost(caps = {}) {\n const host = {};\n for (const name of Object.keys(HOST_CONTRACT)) {\n const cap = caps[name];\n if (cap == null) continue; // legitimately absent\n const v = validateCapability(name, cap);\n if (!v.ok) {\n throw new Error(`Host capability '${name}' is incomplete: missing ${v.missing.join(', ')}`);\n }\n host[name] = Object.freeze(cap);\n }\n return Object.freeze(host);\n}\n\n/** The zero-capability host. Every framework consumer MUST survive it. */\nexport const NULL_HOST = createHost({});\n", "/**\n * pywebview_host.js \u2014 the REFERENCE host adapter.\n *\n * Knows pywebview. Knows NOTHING about EcoAgent: every domain fact (which\n * bridge ref to use, where a state key lands on disk) arrives as a parameter.\n * This is the adapter a standalone consumer copies; EcoAgent is just its first\n * caller (see ui/js/bootstrap/app_bootstrap.js).\n *\n * It speaks to whatever `window.pywebview.api` currently is \u2014 the real\n * pywebview bridge in the desktop app, or the `POST /api/<method>` Proxy\n * polyfill that ui/index.html installs in --browser mode. Both transports are\n * positional-on-the-wire, which is why setBounds() spreads its object back out\n * into positional args.\n */\n\nimport { createHost } from './host.js';\n\nconst FRAMELESS_BACKENDS = new Set(['edgechromium', 'mshtml']);\n\n/**\n * Custom (frameless) window chrome \u2014 the min/max/close buttons, edge resize\n * handles and drag-to-move top bar \u2014 only makes sense when our top bar IS the\n * window's title bar. That holds on a native pywebview window the OS draws\n * *without* decorations of its own: Windows (frameless). On Linux/macOS the\n * window manager already draws a native title bar above our top bar, and in\n * `--browser` mode the browser supplies its own chrome \u2014 in both cases the\n * custom controls are a redundant second set, so we neither render nor wire\n * them (which also stops us swallowing native F11 fullscreen and showing dead\n * resize cursors at the window edges).\n *\n * Detection keys off the pywebview backend string, which is unambiguous: the\n * browser-mode polyfill in index.html injects only `.api`, never `.platform`,\n * so any `.platform` at all \u21D2 running as a native pywebview app. The frameless\n * (custom-title-bar) backends are the Windows WebViews \u2014 'edgechromium' /\n * 'mshtml'. Linux ('gtkwebkit2' / 'qtwebengine') and macOS ('cocoa') draw\n * native decorations, so they return false. Verified live: WebKitGTK reports\n * platform 'gtkwebkit2'.\n */\nexport function usesCustomWindowChrome() {\n // Authoritative signal from the Python host: run_pywebview_mode() appends\n // `?frameless=1` to the window URL exactly when it created a frameless native\n // window (Windows). Prefer it over the backend sniff below because it's\n // present at document parse \u2014 before pywebview finishes injecting\n // `window.pywebview` (and before the browser-mode polyfill, which sets `.api`\n // but no `.platform`, can clobber it). Reading the sniff at shell-build time\n // otherwise races and the whole custom chrome silently fails to render.\n try {\n if (new URLSearchParams(window.location.search).get('frameless') === '1') {\n return true;\n }\n } catch (_) { /* location unavailable (non-browser env) \u2014 fall through */ }\n const platform = window.pywebview && window.pywebview.platform;\n return !!platform && FRAMELESS_BACKENDS.has(platform);\n}\n\n/**\n * Build a Host backed by the pywebview bridge.\n *\n * @param {object} opts\n * @param {{current: object|null}} [opts.bridgeRef] LIVE ref to the bridge.\n * MUST be a ref, never a snapshot: `setupHostBridgeListener` mutates\n * `.current` when a late bridge arrives, and `resolveHostBridge` can\n * resolve `null` after its timeout. Snapshotting the bridge here would\n * freeze that null and reproduce as an intermittent boot failure.\n * @param {(key: string) => string} [opts.resolvePath] logical state key ->\n * storage path. THE domain seam: the framework only ever names keys\n * ('desktops', 'datatable_state'); the embedder decides where they land.\n * @param {object} [opts.logger]\n */\nexport function createPywebviewHost({ bridgeRef = null, resolvePath = (k) => `${k}.json`, logger = console } = {}) {\n const api = () => bridgeRef?.current ?? window.pywebview?.api ?? null;\n\n const call = async (name, args = []) => {\n const a = api();\n if (!a || typeof a[name] !== 'function') return null;\n try { return await a[name](...args); }\n catch (e) { logger.warn?.('[host] call failed', name, e); return null; }\n };\n\n // Envelope unwrapping, in ONE place. Transcribed from the call sites this\n // replaces, so app.py's catch-all `{ok: true}` reply (which carries no\n // `maximized` key) still reads as false, exactly as it does today.\n const okMax = (r) => Boolean(r && r.ok && r.maximized === true);\n const okFs = (r) => Boolean(r && r.ok && r.fullscreen);\n\n // ALL THREE capabilities are ALWAYS advertised. Do NOT gate them on api()\n // being non-null at construction time: the methods late-bind through the\n // ref, and gating here would permanently strand a late-arriving bridge on\n // the fallback paths (Blob download instead of the native save dialog).\n return createHost({\n window: {\n chrome: () => (usesCustomWindowChrome() ? 'custom' : 'native'),\n minimize: () => { void call('window_minimize'); },\n close: () => { void call('window_close'); },\n isMaximized: async () => okMax(await call('window_is_maximized')),\n setMaximized: async (on) => okMax(await call('window_set_maximized', [on])),\n isFullscreen: async () => okFs(await call('window_is_fullscreen')),\n toggleFullscreen: async () => okFs(await call('window_toggle_fullscreen')),\n async getBounds() {\n const r = await call('window_get_bounds');\n if (!r || !r.ok) return null;\n return { x: r.x, y: r.y, width: r.width, height: r.height };\n },\n // POSITIONAL on the wire \u2014 both transports depend on it. Not async:\n // this is a rAF hot path during a window drag/resize; nobody awaits.\n setBounds({ x = null, y = null, width = null, height = null } = {}) {\n void call('window_set_bounds', [x, y, width, height]);\n },\n startNativeDrag: async () => Boolean((await call('window_start_native_drag'))?.ok),\n },\n dialogs: {\n // Resolves to null when the transport has no save_file_dialog behind it.\n // Per host.js that is the contract for \"unavailable\", and every caller\n // treats it exactly like an absent `dialogs` capability: fall back to a\n // Blob download, never report a failed save. Covered by\n // tests/ui/test_host_save_fallback.mjs.\n saveFile: ({ data, filename, kind }) => call('save_file_dialog', [data, filename, kind]),\n },\n state: {\n async read(key) {\n const blob = await call('workspace_state_read', [{ path: resolvePath(key) }]);\n if (!blob) return null;\n try { return typeof blob === 'string' ? JSON.parse(blob) : blob; }\n catch (e) { logger.warn?.('[host] bad JSON for state key', key, e); return null; }\n },\n async write(key, value) {\n const r = await call('workspace_state_write',\n [{ path: resolvePath(key), data: JSON.stringify(value) }]);\n return r !== null;\n },\n },\n });\n}\n", "/**\n * photino_host.js \u2014 the host port over a Photino.NET window.\n *\n * Photino gives the page exactly two things: `window.external.sendMessage(text)`\n * and `window.external.receiveMessage(callback)`. Strings in, strings out, no\n * replies. This adapter puts a request/reply protocol over them, matched by id:\n *\n * page \u2192 host {\"id\": 7, \"cap\": \"window\", \"op\": \"setBounds\", \"args\": [{\"x\":0,\"y\":0,\"width\":1200,\"height\":800}]}\n * host \u2192 page {\"id\": 7, \"ok\": true, \"value\": null}\n * host \u2192 page {\"id\": 8, \"ok\": false, \"error\": \"no such op\"}\n * host \u2192 page {\"event\": \"window.maximizedChanged\", \"value\": true} // unsolicited, no id\n *\n * and offers the port's `window`, `dialogs` and `state` capabilities on top.\n * The C# side \u2014 one dispatcher on the window's web-message handler, every op\n * with its args and reply \u2014 is documented in `photino_host.md` beside this file.\n *\n * Like pywebview_host.js it knows the transport and NOTHING about the app: an\n * embedder that keeps some capability elsewhere (BugDesk keeps `state` on its\n * own server) passes that capability in, or `null` to leave it out.\n *\n * In a browser tab there is no `window.external.sendMessage`, and\n * createPhotinoHost() returns NULL_HOST \u2014 the fallback every FlexDesk consumer\n * already has to survive.\n */\n\nimport { createHost, NULL_HOST } from './host.js';\n\n/** True when `ext` (default: `window.external`) is a Photino bridge. */\nexport function isPhotinoWindow(ext = (typeof window !== 'undefined' ? window.external : undefined)) {\n return !!ext && typeof ext.sendMessage === 'function' && typeof ext.receiveMessage === 'function';\n}\n\n/**\n * The bare request/reply layer. `call(cap, op, ...args)` sends one request and\n * resolves with the reply's `value` (`request(cap, op, args, {wait})` is the\n * same with a timeout of its own). It REJECTS when the host answers\n * `ok: false` (with the host's error) and when no reply arrives within\n * `timeoutMs`. A reply for an id nobody is waiting on, a message that is not\n * JSON, and anything that is neither a reply nor an event are dropped.\n *\n * @param {object} opts\n * @param {{sendMessage: Function, receiveMessage: Function}} opts.external\n * @param {number} [opts.timeoutMs=10000]\n * @param {(name: string, value: any) => void} [opts.onEvent] id-less `{event, value}` messages\n * @param {object} [opts.logger]\n */\nexport function createPhotinoTransport({ external, timeoutMs = 10000, onEvent = null, logger = console }) {\n const pending = new Map();\n let seq = 0;\n external.receiveMessage((text) => {\n let m;\n try { m = typeof text === 'string' ? JSON.parse(text) : text; } catch { return; }\n if (!m || typeof m !== 'object') return;\n if (m.id == null) {\n if (typeof m.event === 'string' && onEvent) {\n try { onEvent(m.event, m.value); }\n catch (e) { logger.warn?.('[host] photino event handler failed', m.event, e); }\n }\n return;\n }\n const p = pending.get(m.id);\n if (!p) return;\n pending.delete(m.id);\n clearTimeout(p.timer);\n if (m.ok) p.resolve(m.value === undefined ? null : m.value);\n else p.reject(new Error(m.error || `${p.what}: refused by the host`));\n });\n // `wait` is this request's timeout in ms; 0 waits for as long as it takes.\n const request = (cap, op, args = [], { wait = timeoutMs } = {}) => new Promise((resolve, reject) => {\n const id = ++seq;\n const what = `${cap}.${op}`;\n const timer = wait > 0 ? setTimeout(() => {\n if (pending.delete(id)) reject(new Error(`${what}: no answer from the host within ${wait} ms`));\n }, wait) : null;\n pending.set(id, { resolve, reject, timer, what });\n try {\n external.sendMessage(JSON.stringify({ id, cap, op, args }));\n } catch (e) {\n pending.delete(id);\n clearTimeout(timer);\n reject(e);\n }\n });\n const call = (cap, op, ...args) => request(cap, op, args);\n return { call, request, pending: () => pending.size };\n}\n\n/**\n * Build a Host backed by a Photino window.\n *\n * The capability methods follow pywebview_host.js's contract, so a consumer\n * cannot tell the two apart: they DEGRADE instead of rejecting. A failed query\n * reads as false / null, a failed fire-and-forget is logged, and a save that\n * the host refused or never answered resolves `null` \u2014 the host.js meaning of\n * \"unavailable\", which every caller turns into a Blob download.\n *\n * @param {object} [opts]\n * @param {object} [opts.external=window.external] the Photino bridge (a fake one in tests)\n * @param {number} [opts.timeoutMs=10000]\n * @param {(name: string, value: any) => void} [opts.onEvent]\n * @param {object} [opts.logger=console]\n * @param {object|null} [opts.window] replace the window capability, or `null` to leave it out\n * @param {object|null} [opts.dialogs] replace the dialogs capability, or `null` to leave it out\n * @param {object|null} [opts.state] replace the state capability, or `null` to leave it out\n * @returns {Readonly<object>} a Host, or NULL_HOST outside a Photino window\n */\nexport function createPhotinoHost(opts = {}) {\n const {\n external = (typeof window !== 'undefined' ? window.external : undefined),\n timeoutMs = 10000,\n onEvent = null,\n logger = console,\n } = opts;\n if (!isPhotinoWindow(external)) return NULL_HOST;\n\n const { call, request } = createPhotinoTransport({ external, timeoutMs, onEvent, logger });\n const warn = (cap, op, e) => logger.warn?.('[host] photino call failed', `${cap}.${op}`, e);\n const soft = async (fallback, cap, op, ...args) => {\n try { return await call(cap, op, ...args); }\n catch (e) { warn(cap, op, e); return fallback; }\n };\n const fire = (cap, op, ...args) => { void soft(null, cap, op, ...args); };\n\n const defaults = {\n window: {\n // Photino draws a decorated, native window on every OS: FlexDesk's\n // own min/max/close buttons would be a second set.\n chrome: () => 'native',\n minimize: () => { fire('window', 'minimize'); },\n close: () => { fire('window', 'close'); },\n isMaximized: async () => (await soft(false, 'window', 'isMaximized')) === true,\n setMaximized: async (on) => (await soft(false, 'window', 'setMaximized', !!on)) === true,\n isFullscreen: async () => (await soft(false, 'window', 'isFullscreen')) === true,\n toggleFullscreen: async () => (await soft(false, 'window', 'toggleFullscreen')) === true,\n async getBounds() {\n const b = await soft(null, 'window', 'getBounds');\n if (!b || typeof b !== 'object') return null;\n return { x: b.x, y: b.y, width: b.width, height: b.height };\n },\n // Not async, like pywebview's: a rAF hot path during a drag; nobody awaits.\n setBounds({ x = null, y = null, width = null, height = null } = {}) {\n fire('window', 'setBounds', { x, y, width, height });\n },\n },\n dialogs: {\n // `{ok: true, path}` | `{ok: false, cancelled: true}` | `{ok: false, error}`,\n // or null when the host refused: fall back to a download. NO TIMEOUT \u2014\n // the dialog is open for as long as the user takes, and giving up on it\n // would download the file a second time behind the dialog.\n async saveFile({ data, filename, kind } = {}) {\n let r = null;\n try { r = await request('dialogs', 'saveFile', [{ data, filename, kind }], { wait: 0 }); }\n catch (e) { warn('dialogs', 'saveFile', e); }\n return r && typeof r === 'object' ? r : null;\n },\n },\n state: {\n async read(key) {\n const v = await soft(null, 'state', 'read', key);\n if (typeof v !== 'string') return v ?? null;\n try { return JSON.parse(v); }\n catch (e) { logger.warn?.('[host] bad JSON for state key', key, e); return null; }\n },\n async write(key, value) {\n return (await soft(false, 'state', 'write', key, value)) !== false;\n },\n },\n };\n\n const caps = {};\n for (const name of Object.keys(defaults)) {\n if (!(name in opts)) caps[name] = defaults[name];\n else if (opts[name] != null) caps[name] = opts[name];\n }\n return createHost(caps);\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["/**\n * host.js \u2014 the host port.\n *\n * The framework declares what it needs from its embedder. A Host is a bag of\n * INDEPENDENTLY OPTIONAL capability objects:\n *\n * - absent capability => absent key (never a stub)\n * - present capability => must be complete (validated at boot)\n *\n * There is NO module-level default host and NO getHost(). Hosts are injected \u2014\n * a module singleton here would just be `window.pywebview` wearing a hat, and\n * would make a second host (mock, browser tab, VS Code webview) impossible.\n *\n * Zero pywebview. Zero EcoAgent. See pywebview_host.js for the reference\n * adapter that implements this port against the pywebview / HTTP transports.\n */\n\nexport const HOST_CONTRACT = Object.freeze({\n window: Object.freeze({\n required: Object.freeze(['chrome', 'minimize', 'close', 'isMaximized', 'setMaximized',\n 'isFullscreen', 'toggleFullscreen', 'getBounds', 'setBounds']),\n // `chrome()` answers 'native' or 'custom', or a promise of one.\n optional: Object.freeze(['startNativeDrag', 'startNativeResize']),\n }),\n dialogs: Object.freeze({ required: Object.freeze(['saveFile']), optional: Object.freeze([]) }),\n state: Object.freeze({ required: Object.freeze(['read', 'write']), optional: Object.freeze([]) }),\n});\n\n/**\n * Validate ONE capability object. We validate the capability the adapter\n * authored \u2014 a plain object literal with real closures \u2014 not the raw bridge:\n * `index.html`'s browser-mode polyfill installs a `new Proxy({}, {get})` that\n * returns a function for EVERY property name, so duck-typing the bridge is\n * vacuously true and validates nothing. A Proxy cannot forge a missing key in\n * an object literal.\n *\n * @returns {{ ok: boolean, missing: string[] }}\n */\nexport function validateCapability(name, cap) {\n const spec = HOST_CONTRACT[name];\n if (!spec) return { ok: false, missing: [`unknown capability '${name}'`] };\n if (!cap || typeof cap !== 'object') return { ok: false, missing: [...spec.required] };\n const missing = spec.required.filter((m) => typeof cap[m] !== 'function');\n return { ok: missing.length === 0, missing };\n}\n\n/**\n * Validate + freeze a set of capabilities into a Host.\n * Throws at boot on an incomplete capability \u2014 a half-wired host is a bug,\n * and a bug at boot beats a silent degrade at click time.\n *\n * @param {{ window?: object, dialogs?: object, state?: object }} caps\n * @returns {Readonly<object>} the Host\n */\nexport function createHost(caps = {}) {\n const host = {};\n for (const name of Object.keys(HOST_CONTRACT)) {\n const cap = caps[name];\n if (cap == null) continue; // legitimately absent\n const v = validateCapability(name, cap);\n if (!v.ok) {\n throw new Error(`Host capability '${name}' is incomplete: missing ${v.missing.join(', ')}`);\n }\n host[name] = Object.freeze(cap);\n }\n return Object.freeze(host);\n}\n\n/** The zero-capability host. Every framework consumer MUST survive it. */\nexport const NULL_HOST = createHost({});\n", "/**\n * pywebview_host.js \u2014 the REFERENCE host adapter.\n *\n * Knows pywebview. Knows NOTHING about EcoAgent: every domain fact (which\n * bridge ref to use, where a state key lands on disk) arrives as a parameter.\n * This is the adapter a standalone consumer copies; EcoAgent is just its first\n * caller (see ui/js/bootstrap/app_bootstrap.js).\n *\n * It speaks to whatever `window.pywebview.api` currently is \u2014 the real\n * pywebview bridge in the desktop app, or the `POST /api/<method>` Proxy\n * polyfill that ui/index.html installs in --browser mode. Both transports are\n * positional-on-the-wire, which is why setBounds() spreads its object back out\n * into positional args.\n */\n\nimport { createHost } from './host.js';\n\nconst FRAMELESS_BACKENDS = new Set(['edgechromium', 'mshtml']);\n\n/**\n * Custom (frameless) window chrome \u2014 the min/max/close buttons, edge resize\n * handles and drag-to-move top bar \u2014 only makes sense when our top bar IS the\n * window's title bar. That holds on a native pywebview window the OS draws\n * *without* decorations of its own: Windows (frameless). On Linux/macOS the\n * window manager already draws a native title bar above our top bar, and in\n * `--browser` mode the browser supplies its own chrome \u2014 in both cases the\n * custom controls are a redundant second set, so we neither render nor wire\n * them (which also stops us swallowing native F11 fullscreen and showing dead\n * resize cursors at the window edges).\n *\n * Detection keys off the pywebview backend string, which is unambiguous: the\n * browser-mode polyfill in index.html injects only `.api`, never `.platform`,\n * so any `.platform` at all \u21D2 running as a native pywebview app. The frameless\n * (custom-title-bar) backends are the Windows WebViews \u2014 'edgechromium' /\n * 'mshtml'. Linux ('gtkwebkit2' / 'qtwebengine') and macOS ('cocoa') draw\n * native decorations, so they return false. Verified live: WebKitGTK reports\n * platform 'gtkwebkit2'.\n */\nexport function usesCustomWindowChrome() {\n // Authoritative signal from the Python host: run_pywebview_mode() appends\n // `?frameless=1` to the window URL exactly when it created a frameless native\n // window (Windows). Prefer it over the backend sniff below because it's\n // present at document parse \u2014 before pywebview finishes injecting\n // `window.pywebview` (and before the browser-mode polyfill, which sets `.api`\n // but no `.platform`, can clobber it). Reading the sniff at shell-build time\n // otherwise races and the whole custom chrome silently fails to render.\n try {\n if (new URLSearchParams(window.location.search).get('frameless') === '1') {\n return true;\n }\n } catch (_) { /* location unavailable (non-browser env) \u2014 fall through */ }\n const platform = window.pywebview && window.pywebview.platform;\n return !!platform && FRAMELESS_BACKENDS.has(platform);\n}\n\n/**\n * Build a Host backed by the pywebview bridge.\n *\n * @param {object} opts\n * @param {{current: object|null}} [opts.bridgeRef] LIVE ref to the bridge.\n * MUST be a ref, never a snapshot: `setupHostBridgeListener` mutates\n * `.current` when a late bridge arrives, and `resolveHostBridge` can\n * resolve `null` after its timeout. Snapshotting the bridge here would\n * freeze that null and reproduce as an intermittent boot failure.\n * @param {(key: string) => string} [opts.resolvePath] logical state key ->\n * storage path. THE domain seam: the framework only ever names keys\n * ('desktops', 'datatable_state'); the embedder decides where they land.\n * @param {object} [opts.logger]\n */\nexport function createPywebviewHost({ bridgeRef = null, resolvePath = (k) => `${k}.json`, logger = console } = {}) {\n const api = () => bridgeRef?.current ?? window.pywebview?.api ?? null;\n\n const call = async (name, args = []) => {\n const a = api();\n if (!a || typeof a[name] !== 'function') return null;\n try { return await a[name](...args); }\n catch (e) { logger.warn?.('[host] call failed', name, e); return null; }\n };\n\n // Envelope unwrapping, in ONE place. Transcribed from the call sites this\n // replaces, so app.py's catch-all `{ok: true}` reply (which carries no\n // `maximized` key) still reads as false, exactly as it does today.\n const okMax = (r) => Boolean(r && r.ok && r.maximized === true);\n const okFs = (r) => Boolean(r && r.ok && r.fullscreen);\n\n // ALL THREE capabilities are ALWAYS advertised. Do NOT gate them on api()\n // being non-null at construction time: the methods late-bind through the\n // ref, and gating here would permanently strand a late-arriving bridge on\n // the fallback paths (Blob download instead of the native save dialog).\n return createHost({\n window: {\n chrome: () => (usesCustomWindowChrome() ? 'custom' : 'native'),\n minimize: () => { void call('window_minimize'); },\n close: () => { void call('window_close'); },\n isMaximized: async () => okMax(await call('window_is_maximized')),\n setMaximized: async (on) => okMax(await call('window_set_maximized', [on])),\n isFullscreen: async () => okFs(await call('window_is_fullscreen')),\n toggleFullscreen: async () => okFs(await call('window_toggle_fullscreen')),\n async getBounds() {\n const r = await call('window_get_bounds');\n if (!r || !r.ok) return null;\n return { x: r.x, y: r.y, width: r.width, height: r.height };\n },\n // POSITIONAL on the wire \u2014 both transports depend on it. Not async:\n // this is a rAF hot path during a window drag/resize; nobody awaits.\n setBounds({ x = null, y = null, width = null, height = null } = {}) {\n void call('window_set_bounds', [x, y, width, height]);\n },\n startNativeDrag: async () => Boolean((await call('window_start_native_drag'))?.ok),\n },\n dialogs: {\n // Resolves to null when the transport has no save_file_dialog behind it.\n // Per host.js that is the contract for \"unavailable\", and every caller\n // treats it exactly like an absent `dialogs` capability: fall back to a\n // Blob download, never report a failed save. Covered by\n // tests/ui/test_host_save_fallback.mjs.\n saveFile: ({ data, filename, kind }) => call('save_file_dialog', [data, filename, kind]),\n },\n state: {\n async read(key) {\n const blob = await call('workspace_state_read', [{ path: resolvePath(key) }]);\n if (!blob) return null;\n try { return typeof blob === 'string' ? JSON.parse(blob) : blob; }\n catch (e) { logger.warn?.('[host] bad JSON for state key', key, e); return null; }\n },\n async write(key, value) {\n const r = await call('workspace_state_write',\n [{ path: resolvePath(key), data: JSON.stringify(value) }]);\n return r !== null;\n },\n },\n });\n}\n", "/**\n * photino_host.js \u2014 the host port over a Photino.NET window.\n *\n * Photino gives the page exactly two things: `window.external.sendMessage(text)`\n * and `window.external.receiveMessage(callback)`. Strings in, strings out, no\n * replies. This adapter puts a request/reply protocol over them, matched by id:\n *\n * page \u2192 host {\"id\": 7, \"cap\": \"window\", \"op\": \"setBounds\", \"args\": [{\"x\":0,\"y\":0,\"width\":1200,\"height\":800}]}\n * host \u2192 page {\"id\": 7, \"ok\": true, \"value\": null}\n * host \u2192 page {\"id\": 8, \"ok\": false, \"error\": \"no such op\"}\n * host \u2192 page {\"event\": \"window.maximizedChanged\", \"value\": true} // unsolicited, no id\n *\n * and offers the port's `window`, `dialogs` and `state` capabilities on top.\n * The C# side \u2014 one dispatcher on the window's web-message handler, every op\n * with its args and reply \u2014 is documented in `photino_host.md` beside this file.\n *\n * Like pywebview_host.js it knows the transport and NOTHING about the app: an\n * embedder that keeps some capability elsewhere (BugDesk keeps `state` on its\n * own server) passes that capability in, or `null` to leave it out.\n *\n * In a browser tab there is no `window.external.sendMessage`, and\n * createPhotinoHost() returns NULL_HOST \u2014 the fallback every FlexDesk consumer\n * already has to survive.\n */\n\nimport { createHost, NULL_HOST } from './host.js';\n\n/** True when `ext` (default: `window.external`) is a Photino bridge. */\nexport function isPhotinoWindow(ext = (typeof window !== 'undefined' ? window.external : undefined)) {\n return !!ext && typeof ext.sendMessage === 'function' && typeof ext.receiveMessage === 'function';\n}\n\n/**\n * The bare request/reply layer. `call(cap, op, ...args)` sends one request and\n * resolves with the reply's `value` (`request(cap, op, args, {wait})` is the\n * same with a timeout of its own). It REJECTS when the host answers\n * `ok: false` (with the host's error) and when no reply arrives within\n * `timeoutMs`. A reply for an id nobody is waiting on, a message that is not\n * JSON, and anything that is neither a reply nor an event are dropped.\n *\n * @param {object} opts\n * @param {{sendMessage: Function, receiveMessage: Function}} opts.external\n * @param {number} [opts.timeoutMs=10000]\n * @param {(name: string, value: any) => void} [opts.onEvent] id-less `{event, value}` messages\n * @param {object} [opts.logger]\n */\nexport function createPhotinoTransport({ external, timeoutMs = 10000, onEvent = null, logger = console }) {\n const pending = new Map();\n let seq = 0;\n external.receiveMessage((text) => {\n let m;\n try { m = typeof text === 'string' ? JSON.parse(text) : text; } catch { return; }\n if (!m || typeof m !== 'object') return;\n if (m.id == null) {\n if (typeof m.event === 'string' && onEvent) {\n try { onEvent(m.event, m.value); }\n catch (e) { logger.warn?.('[host] photino event handler failed', m.event, e); }\n }\n return;\n }\n const p = pending.get(m.id);\n if (!p) return;\n pending.delete(m.id);\n clearTimeout(p.timer);\n if (m.ok) p.resolve(m.value === undefined ? null : m.value);\n else p.reject(new Error(m.error || `${p.what}: refused by the host`));\n });\n // `wait` is this request's timeout in ms; 0 waits for as long as it takes.\n const request = (cap, op, args = [], { wait = timeoutMs } = {}) => new Promise((resolve, reject) => {\n const id = ++seq;\n const what = `${cap}.${op}`;\n const timer = wait > 0 ? setTimeout(() => {\n if (pending.delete(id)) reject(new Error(`${what}: no answer from the host within ${wait} ms`));\n }, wait) : null;\n pending.set(id, { resolve, reject, timer, what });\n try {\n external.sendMessage(JSON.stringify({ id, cap, op, args }));\n } catch (e) {\n pending.delete(id);\n clearTimeout(timer);\n reject(e);\n }\n });\n const call = (cap, op, ...args) => request(cap, op, args);\n return { call, request, pending: () => pending.size };\n}\n\n/**\n * Build a Host backed by a Photino window.\n *\n * The capability methods follow pywebview_host.js's contract, so a consumer\n * cannot tell the two apart: they DEGRADE instead of rejecting. A failed query\n * reads as false / null, a failed fire-and-forget is logged, and a save that\n * the host refused or never answered resolves `null` \u2014 the host.js meaning of\n * \"unavailable\", which every caller turns into a Blob download.\n *\n * @param {object} [opts]\n * @param {object} [opts.external=window.external] the Photino bridge (a fake one in tests)\n * @param {number} [opts.timeoutMs=10000]\n * @param {(name: string, value: any) => void} [opts.onEvent]\n * @param {object} [opts.logger=console]\n * @param {'auto'|'native'|'custom'} [opts.chrome='auto'] who draws the window's\n * title bar and borders. `auto` asks the native side once (`host.describe`,\n * its `frame` field), the first time somebody needs to know; `native`\n * or `custom` fixes the answer without asking. See photino_host.md.\n * @param {object|null} [opts.window] replace the window capability, or `null` to leave it out\n * @param {object|null} [opts.dialogs] replace the dialogs capability, or `null` to leave it out\n * @param {object|null} [opts.state] replace the state capability, or `null` to leave it out\n * @returns {Readonly<object>} a Host, or NULL_HOST outside a Photino window\n */\nexport function createPhotinoHost(opts = {}) {\n const {\n external = (typeof window !== 'undefined' ? window.external : undefined),\n timeoutMs = 10000,\n onEvent = null,\n logger = console,\n chrome = 'auto',\n } = opts;\n if (!isPhotinoWindow(external)) return NULL_HOST;\n\n const { call, request } = createPhotinoTransport({ external, timeoutMs, onEvent, logger });\n const warn = (cap, op, e) => logger.warn?.('[host] photino call failed', `${cap}.${op}`, e);\n const soft = async (fallback, cap, op, ...args) => {\n try { return await call(cap, op, ...args); }\n catch (e) { warn(cap, op, e); return fallback; }\n };\n const fire = (cap, op, ...args) => { void soft(null, cap, op, ...args); };\n\n // Who draws the frame. Asked once and remembered: a string once known, the\n // pending question before that. A native side that predates `frame`, or\n // doesn't answer within 3 s, has native chrome.\n let frame = chrome === 'custom' || chrome === 'native' ? chrome : null;\n let asking = null;\n const askFrame = () => {\n if (frame) return frame;\n asking ??= request('host', 'describe', [], { wait: 3000 })\n .then((d) => (d && d.frame === 'custom' ? 'custom' : 'native'), () => 'native')\n .then((f) => { frame = f; return f; });\n return asking;\n };\n // A drag or resize handed to the OS: the reply comes when the mouse is\n // released, however long that takes. Refused (a native side without the op,\n // or a window with native chrome) means \"not handled\": the caller does it in JS.\n const gesture = async (op, ...args) => {\n try { await request('window', op, args, { wait: 0 }); return true; }\n catch (e) { return false; }\n };\n\n const defaults = {\n window: {\n // 'native' (the OS draws the frame: FlexDesk's own buttons would be\n // a second set) or 'custom' (the native side made the window\n // frameless). A promise until the native side has said which.\n chrome: () => askFrame(),\n minimize: () => { fire('window', 'minimize'); },\n close: () => { fire('window', 'close'); },\n isMaximized: async () => (await soft(false, 'window', 'isMaximized')) === true,\n setMaximized: async (on) => (await soft(false, 'window', 'setMaximized', !!on)) === true,\n isFullscreen: async () => (await soft(false, 'window', 'isFullscreen')) === true,\n toggleFullscreen: async () => (await soft(false, 'window', 'toggleFullscreen')) === true,\n async getBounds() {\n const b = await soft(null, 'window', 'getBounds');\n if (!b || typeof b !== 'object') return null;\n return { x: b.x, y: b.y, width: b.width, height: b.height };\n },\n startNativeDrag: () => gesture('startDrag'),\n startNativeResize: (edge) => gesture('startResize', String(edge || '')),\n // Not async, like pywebview's: a rAF hot path during a drag; nobody awaits.\n setBounds({ x = null, y = null, width = null, height = null } = {}) {\n fire('window', 'setBounds', { x, y, width, height });\n },\n },\n dialogs: {\n // `{ok: true, path}` | `{ok: false, cancelled: true}` | `{ok: false, error}`,\n // or null when the host refused: fall back to a download. NO TIMEOUT \u2014\n // the dialog is open for as long as the user takes, and giving up on it\n // would download the file a second time behind the dialog.\n async saveFile({ data, filename, kind } = {}) {\n let r = null;\n try { r = await request('dialogs', 'saveFile', [{ data, filename, kind }], { wait: 0 }); }\n catch (e) { warn('dialogs', 'saveFile', e); }\n return r && typeof r === 'object' ? r : null;\n },\n },\n state: {\n async read(key) {\n const v = await soft(null, 'state', 'read', key);\n if (typeof v !== 'string') return v ?? null;\n try { return JSON.parse(v); }\n catch (e) { logger.warn?.('[host] bad JSON for state key', key, e); return null; }\n },\n async write(key, value) {\n return (await soft(false, 'state', 'write', key, value)) !== false;\n },\n },\n };\n\n const caps = {};\n for (const name of Object.keys(defaults)) {\n if (!(name in opts)) caps[name] = defaults[name];\n else if (opts[name] != null) caps[name] = opts[name];\n }\n return createHost(caps);\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
package/dist/widgets.js CHANGED
@@ -5050,9 +5050,13 @@ var MIN_WIDTH = 500;
5050
5050
  var MIN_HEIGHT = 350;
5051
5051
  var DRAG_THRESHOLD = 4;
5052
5052
  var WindowChromeController = class {
5053
- constructor({ eventBus, logger, host } = {}) {
5053
+ constructor({ eventBus, logger, host, topBar = ".twm-global-top-bar", controls = null, noDrag = "" } = {}) {
5054
5054
  this.eventBus = eventBus;
5055
5055
  this.logger = logger || console;
5056
+ this._topBarOpt = topBar;
5057
+ this._controlsOpt = controls;
5058
+ this._noDrag = noDrag;
5059
+ this._createdControls = null;
5056
5060
  this.win = host?.window || null;
5057
5061
  this.resizingActive = false;
5058
5062
  this.lastDblClickTs = 0;
@@ -5064,12 +5068,21 @@ var WindowChromeController = class {
5064
5068
  /**
5065
5069
  * Initialize the window chrome controller
5066
5070
  */
5067
- initialize() {
5068
- if (this._disposed) return;
5069
- if (!this.win || this.win.chrome() !== "custom") {
5071
+ async initialize() {
5072
+ if (this._disposed) return false;
5073
+ let chrome = "native";
5074
+ try {
5075
+ chrome = this.win ? await this.win.chrome() : "native";
5076
+ } catch (_) {
5077
+ chrome = "native";
5078
+ }
5079
+ if (this._disposed) return false;
5080
+ if (chrome !== "custom") {
5070
5081
  this.logger.info?.("[WindowChrome] Native/browser chrome detected \u2014 custom window chrome disabled");
5071
- return;
5082
+ return false;
5072
5083
  }
5084
+ document.body.classList.add("twm-custom-chrome");
5085
+ this.#createWindowControls();
5073
5086
  this.#wireWindowButtons();
5074
5087
  this.#createResizeHandles();
5075
5088
  this.#wireTopBarDrag();
@@ -5077,6 +5090,43 @@ var WindowChromeController = class {
5077
5090
  this.#wireNativeStateSync();
5078
5091
  this.#initializeState();
5079
5092
  this.logger.info?.("[WindowChrome] Initialized");
5093
+ return true;
5094
+ }
5095
+ /** An element from a selector or an element; null when there is none. */
5096
+ #resolve(target, within = document) {
5097
+ if (!target) return null;
5098
+ if (typeof target !== "string") return target;
5099
+ return within.querySelector(target);
5100
+ }
5101
+ /**
5102
+ * Draw minimise, maximise/restore and close, unless the page has its own
5103
+ * `#win-*` buttons (EcoSim's markup), which are then wired instead.
5104
+ */
5105
+ #createWindowControls() {
5106
+ if (document.getElementById("win-minimize") || document.getElementById("win-close")) return;
5107
+ const bar = this.#resolve(this._topBarOpt);
5108
+ const host = this.#resolve(this._controlsOpt) || bar?.querySelector(".twm-bar-right") || bar;
5109
+ if (!host) return;
5110
+ const box = document.createElement("div");
5111
+ box.className = "twm-window-controls";
5112
+ const button = (id, icon, label, extra = "") => {
5113
+ const b = document.createElement("button");
5114
+ b.type = "button";
5115
+ b.id = id;
5116
+ b.className = `twm-window-controls__btn${extra}`;
5117
+ b.setAttribute("aria-label", label);
5118
+ b.title = label;
5119
+ const i = document.createElement("span");
5120
+ i.className = "material-symbols-outlined";
5121
+ i.textContent = icon;
5122
+ b.appendChild(i);
5123
+ box.appendChild(b);
5124
+ };
5125
+ button("win-minimize", "remove", "Minimize");
5126
+ button("win-maximize", "check_box_outline_blank", "Maximize");
5127
+ button("win-close", "close", "Close", " twm-window-controls__btn--close");
5128
+ host.appendChild(box);
5129
+ this._createdControls = box;
5080
5130
  }
5081
5131
  /**
5082
5132
  * Wire minimize, maximize, close buttons
@@ -5122,6 +5172,7 @@ var WindowChromeController = class {
5122
5172
  const tooltipText = isMax ? "Restore" : "Maximize";
5123
5173
  window.LatexTooltip?.set(maxBtn, tooltipText);
5124
5174
  maxBtn.setAttribute("aria-label", tooltipText);
5175
+ maxBtn.title = tooltipText;
5125
5176
  document.body.classList.toggle("twm-window-maximized", isMax);
5126
5177
  }
5127
5178
  /**
@@ -5188,8 +5239,30 @@ var WindowChromeController = class {
5188
5239
  }
5189
5240
  };
5190
5241
  let activePointerId = null;
5242
+ const EDGE = {
5243
+ right: "e",
5244
+ left: "w",
5245
+ top: "n",
5246
+ bottom: "s",
5247
+ cornerBR: "se",
5248
+ cornerBL: "sw",
5249
+ cornerTR: "ne",
5250
+ cornerTL: "nw"
5251
+ };
5191
5252
  const startDrag = async (e) => {
5253
+ if (e.button != null && e.button !== 0) return;
5192
5254
  e.preventDefault();
5255
+ if (this.win.startNativeResize) {
5256
+ this.resizingActive = true;
5257
+ let handled = false;
5258
+ try {
5259
+ handled = await this.win.startNativeResize(EDGE[mode]);
5260
+ } catch (_) {
5261
+ handled = false;
5262
+ }
5263
+ this.resizingActive = false;
5264
+ if (handled) return;
5265
+ }
5193
5266
  this.resizingActive = true;
5194
5267
  startX = e.clientX;
5195
5268
  startY = e.clientY;
@@ -5300,9 +5373,25 @@ var WindowChromeController = class {
5300
5373
  * Setup top bar dragging for window movement
5301
5374
  */
5302
5375
  #wireTopBarDrag() {
5303
- this.topBar = document.querySelector(".twm-global-top-bar");
5376
+ this.topBar = this.#resolve(this._topBarOpt);
5304
5377
  if (!this.topBar) return;
5305
- const NO_DRAG_SELECTOR = ".no-drag, button, input, select, textarea, .menu-item, .menu-dropdown, .window-controls, .panel-toggles, .global-search-dropdown";
5378
+ const NO_DRAG_SELECTOR = [
5379
+ ".no-drag",
5380
+ "button",
5381
+ "a",
5382
+ "input",
5383
+ "select",
5384
+ "textarea",
5385
+ "label",
5386
+ '[role="button"]',
5387
+ '[role="menuitem"]',
5388
+ '[role="tab"]',
5389
+ '[contenteditable="true"]',
5390
+ ".twm-window-controls",
5391
+ ".window-controls",
5392
+ ".panel-toggles",
5393
+ this._noDrag
5394
+ ].filter(Boolean).join(", ");
5306
5395
  let dragging = false;
5307
5396
  let startSX = 0, startSY = 0, startX = null, startY = null, winW = 0, winH = 0;
5308
5397
  let wasMax = false;
@@ -5479,8 +5568,7 @@ var WindowChromeController = class {
5479
5568
  this.topBar.addEventListener("mousedown", blocker, true);
5480
5569
  this.topBar.addEventListener("pointerdown", blocker, true);
5481
5570
  const dblClickHandler = async (e) => {
5482
- const interactiveSelector = 'button, input, select, textarea, a, [role="button"], [role="menuitem"], .menu-bar, .fl-bar, .fl-item, .global-search-wrapper, .panel-toggle-btn';
5483
- if (e.target.closest(interactiveSelector)) {
5571
+ if (isInteractiveTarget(e.target)) {
5484
5572
  return;
5485
5573
  }
5486
5574
  this.lastDblClickTs = Date.now();
@@ -5586,6 +5674,9 @@ var WindowChromeController = class {
5586
5674
  }
5587
5675
  });
5588
5676
  this.handles = {};
5677
+ this._createdControls?.remove();
5678
+ this._createdControls = null;
5679
+ document.body.classList.remove("twm-custom-chrome", "twm-window-maximized", "window-fullscreen");
5589
5680
  this.logger.info?.("[WindowChrome] Disposed");
5590
5681
  }
5591
5682
  };