flexdesk 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -104,6 +104,32 @@ const shell = await createShell({
104
104
  A working version of exactly this — planets and moons, a mock host, no backend —
105
105
  lives in `demo/`.
106
106
 
107
+ ### Opt-in shell features
108
+
109
+ Two features change what the user sees, so both are off until you ask:
110
+
111
+ ```js
112
+ await createShell({
113
+ // …everything above…
114
+ snapPromotion: true, // drag a floating window onto a tile: it snaps, and docks back in
115
+ promoteInPlace: true, // a window floated out of a tile stays inside that tile's pane
116
+ chrome: {
117
+ topNav: document.getElementById('nav'),
118
+ zoom: document.getElementById('status-bar'), // content zoom, 50–200%
119
+ },
120
+ });
121
+ ```
122
+
123
+ **`chrome.zoom`** paints a − / track / + / readout control into the element you
124
+ pass, restores the saved zoom through the host's `state` capability, and scales
125
+ the content of tiles and floating windows. It deliberately scales *content only*
126
+ — never the root, a tile's frame or a window's frame — because CSS `zoom`
127
+ establishes a scaled coordinate space and every window drag, resize and snap
128
+ measures in real pixels. `shell.chrome.zoom` exposes `get()` and `set(percent)`
129
+ for a settings pane. An embedder that builds its own `WindowManager` instead of
130
+ calling `createShell` mounts the same control with `mountZoomControl(el, { root,
131
+ host })`.
132
+
107
133
  ## The host port
108
134
 
109
135
  FlexDesk never touches `pywebview`, `fetch`, `localStorage` or the filesystem. It
package/css/base.css CHANGED
@@ -4740,3 +4740,71 @@ button.twm-about-dialog__btn--close:focus {
4740
4740
  * `position: fixed` descendants, which would clip every dropdown the grid opens
4741
4741
  * on purpose. */
4742
4742
  .twm-root { position: relative; }
4743
+
4744
+ /* ── C31. Content zoom ─────────────────────────────────────────────
4745
+ Applied by zoom.js as `--twm-zoom` plus `.twm-zoomed` on the shell
4746
+ ROOT. The class is only set away from 100%, so an unzoomed shell has
4747
+ no `zoom` declaration anywhere: `zoom: 1` is not a no-op, it still
4748
+ establishes a scaled coordinate space.
4749
+
4750
+ CONTENT SURFACES ONLY. A tile's body and a promoted window's content
4751
+ sit below every piece of geometry the window drag, resize and C15
4752
+ snap probe read. The root, a leaf wrap, tile chrome, tab bars and a
4753
+ window frame are deliberately absent: a contained window lives in a
4754
+ leaf wrap and is re-parented to the root while it is dragged, so
4755
+ zooming either would drift every drag by the zoom factor. See zoom.js. */
4756
+ .twm-zoomed .twm-leaf__body,
4757
+ .twm-zoomed .twm-window-content { zoom: var(--twm-zoom, 1); }
4758
+
4759
+ /* The control. Sized against the bar it sits in, not the zoom — it is
4760
+ chrome, and it is the one thing that must stay reachable at 200%. */
4761
+ .twm-zoom {
4762
+ display: inline-flex;
4763
+ align-items: center;
4764
+ gap: 3px;
4765
+ height: 100%;
4766
+ padding: 0 8px;
4767
+ border-left: 1px solid var(--surface-3);
4768
+ user-select: none;
4769
+ }
4770
+ .twm-zoom__step {
4771
+ display: inline-flex;
4772
+ align-items: center;
4773
+ justify-content: center;
4774
+ width: 15px;
4775
+ height: 15px;
4776
+ padding: 0;
4777
+ border: none;
4778
+ border-radius: 2px;
4779
+ background: none;
4780
+ color: var(--text-muted);
4781
+ font: inherit;
4782
+ font-size: var(--font-size-sm);
4783
+ line-height: 1;
4784
+ cursor: pointer;
4785
+ }
4786
+ .twm-zoom__step:hover { background: var(--surface-3); color: var(--text-primary); }
4787
+ /* A short track on purpose: it sits between two buttons in a thin bar,
4788
+ and the readout — not the thumb position — answers "where am I". */
4789
+ .twm-zoom__slider {
4790
+ width: 78px;
4791
+ height: 12px;
4792
+ margin: 0;
4793
+ cursor: pointer;
4794
+ accent-color: var(--accent-bright);
4795
+ }
4796
+ /* Tabular numerals, so the readout does not jiggle between 95% and 100%
4797
+ while the thumb moves. */
4798
+ .twm-zoom__value {
4799
+ min-width: 34px;
4800
+ padding: 0;
4801
+ border: none;
4802
+ background: none;
4803
+ color: var(--text-muted);
4804
+ font-family: var(--font-family-mono);
4805
+ font-size: var(--font-size-xs);
4806
+ font-variant-numeric: tabular-nums;
4807
+ text-align: right;
4808
+ cursor: pointer;
4809
+ }
4810
+ .twm-zoom__value:hover { color: var(--text-primary); }
package/css/flexdesk.css CHANGED
@@ -7088,3 +7088,71 @@ tr:hover .twm-row-action { opacity: 1; }
7088
7088
  .twm-top-nav__label {
7089
7089
  /* hidden on narrow viewports; icon alone is enough */
7090
7090
  }
7091
+
7092
+ /* ── C31. Content zoom ─────────────────────────────────────────────
7093
+ Applied by zoom.js as `--twm-zoom` plus `.twm-zoomed` on the shell
7094
+ ROOT. The class is only set away from 100%, so an unzoomed shell has
7095
+ no `zoom` declaration anywhere: `zoom: 1` is not a no-op, it still
7096
+ establishes a scaled coordinate space.
7097
+
7098
+ CONTENT SURFACES ONLY. A tile's body and a promoted window's content
7099
+ sit below every piece of geometry the window drag, resize and C15
7100
+ snap probe read. The root, a leaf wrap, tile chrome, tab bars and a
7101
+ window frame are deliberately absent: a contained window lives in a
7102
+ leaf wrap and is re-parented to the root while it is dragged, so
7103
+ zooming either would drift every drag by the zoom factor. See zoom.js. */
7104
+ .twm-zoomed .twm-leaf__body,
7105
+ .twm-zoomed .twm-window-content { zoom: var(--twm-zoom, 1); }
7106
+
7107
+ /* The control. Sized against the bar it sits in, not the zoom — it is
7108
+ chrome, and it is the one thing that must stay reachable at 200%. */
7109
+ .twm-zoom {
7110
+ display: inline-flex;
7111
+ align-items: center;
7112
+ gap: 3px;
7113
+ height: 100%;
7114
+ padding: 0 8px;
7115
+ border-left: 1px solid var(--surface-3);
7116
+ user-select: none;
7117
+ }
7118
+ .twm-zoom__step {
7119
+ display: inline-flex;
7120
+ align-items: center;
7121
+ justify-content: center;
7122
+ width: 15px;
7123
+ height: 15px;
7124
+ padding: 0;
7125
+ border: none;
7126
+ border-radius: 2px;
7127
+ background: none;
7128
+ color: var(--text-muted);
7129
+ font: inherit;
7130
+ font-size: var(--font-size-sm);
7131
+ line-height: 1;
7132
+ cursor: pointer;
7133
+ }
7134
+ .twm-zoom__step:hover { background: var(--surface-3); color: var(--text-primary); }
7135
+ /* A short track on purpose: it sits between two buttons in a thin bar,
7136
+ and the readout — not the thumb position — answers "where am I". */
7137
+ .twm-zoom__slider {
7138
+ width: 78px;
7139
+ height: 12px;
7140
+ margin: 0;
7141
+ cursor: pointer;
7142
+ accent-color: var(--accent-bright);
7143
+ }
7144
+ /* Tabular numerals, so the readout does not jiggle between 95% and 100%
7145
+ while the thumb moves. */
7146
+ .twm-zoom__value {
7147
+ min-width: 34px;
7148
+ padding: 0;
7149
+ border: none;
7150
+ background: none;
7151
+ color: var(--text-muted);
7152
+ font-family: var(--font-family-mono);
7153
+ font-size: var(--font-size-xs);
7154
+ font-variant-numeric: tabular-nums;
7155
+ text-align: right;
7156
+ cursor: pointer;
7157
+ }
7158
+ .twm-zoom__value:hover { color: var(--text-primary); }
package/dist/flexdesk.css CHANGED
@@ -7088,3 +7088,71 @@ tr:hover .twm-row-action { opacity: 1; }
7088
7088
  .twm-top-nav__label {
7089
7089
  /* hidden on narrow viewports; icon alone is enough */
7090
7090
  }
7091
+
7092
+ /* ── C31. Content zoom ─────────────────────────────────────────────
7093
+ Applied by zoom.js as `--twm-zoom` plus `.twm-zoomed` on the shell
7094
+ ROOT. The class is only set away from 100%, so an unzoomed shell has
7095
+ no `zoom` declaration anywhere: `zoom: 1` is not a no-op, it still
7096
+ establishes a scaled coordinate space.
7097
+
7098
+ CONTENT SURFACES ONLY. A tile's body and a promoted window's content
7099
+ sit below every piece of geometry the window drag, resize and C15
7100
+ snap probe read. The root, a leaf wrap, tile chrome, tab bars and a
7101
+ window frame are deliberately absent: a contained window lives in a
7102
+ leaf wrap and is re-parented to the root while it is dragged, so
7103
+ zooming either would drift every drag by the zoom factor. See zoom.js. */
7104
+ .twm-zoomed .twm-leaf__body,
7105
+ .twm-zoomed .twm-window-content { zoom: var(--twm-zoom, 1); }
7106
+
7107
+ /* The control. Sized against the bar it sits in, not the zoom — it is
7108
+ chrome, and it is the one thing that must stay reachable at 200%. */
7109
+ .twm-zoom {
7110
+ display: inline-flex;
7111
+ align-items: center;
7112
+ gap: 3px;
7113
+ height: 100%;
7114
+ padding: 0 8px;
7115
+ border-left: 1px solid var(--surface-3);
7116
+ user-select: none;
7117
+ }
7118
+ .twm-zoom__step {
7119
+ display: inline-flex;
7120
+ align-items: center;
7121
+ justify-content: center;
7122
+ width: 15px;
7123
+ height: 15px;
7124
+ padding: 0;
7125
+ border: none;
7126
+ border-radius: 2px;
7127
+ background: none;
7128
+ color: var(--text-muted);
7129
+ font: inherit;
7130
+ font-size: var(--font-size-sm);
7131
+ line-height: 1;
7132
+ cursor: pointer;
7133
+ }
7134
+ .twm-zoom__step:hover { background: var(--surface-3); color: var(--text-primary); }
7135
+ /* A short track on purpose: it sits between two buttons in a thin bar,
7136
+ and the readout — not the thumb position — answers "where am I". */
7137
+ .twm-zoom__slider {
7138
+ width: 78px;
7139
+ height: 12px;
7140
+ margin: 0;
7141
+ cursor: pointer;
7142
+ accent-color: var(--accent-bright);
7143
+ }
7144
+ /* Tabular numerals, so the readout does not jiggle between 95% and 100%
7145
+ while the thumb moves. */
7146
+ .twm-zoom__value {
7147
+ min-width: 34px;
7148
+ padding: 0;
7149
+ border: none;
7150
+ background: none;
7151
+ color: var(--text-muted);
7152
+ font-family: var(--font-family-mono);
7153
+ font-size: var(--font-size-xs);
7154
+ font-variant-numeric: tabular-nums;
7155
+ text-align: right;
7156
+ cursor: pointer;
7157
+ }
7158
+ .twm-zoom__value:hover { color: var(--text-primary); }
package/dist/wm.js CHANGED
@@ -588,7 +588,7 @@ var TileTree = class _TileTree {
588
588
  0,
589
589
  Math.min(n.tabs.length - 1, saved.activeTabIdx || 0)
590
590
  );
591
- const wantsTarget = target.props?.id != null || target.kind !== targetTopNav;
591
+ const wantsTarget = target.props?.id != null || target.kind !== targetTopNav || Object.keys(target.props || {}).length > 0;
592
592
  if (wantsTarget) {
593
593
  const wantId = target.props?.id != null;
594
594
  const matchIdx = n.tabs.findIndex((t) => t.kind === target.kind && (!wantId || String(t.props?.id ?? "") === String(target.props.id)));
@@ -757,9 +757,11 @@ var TileTree = class _TileTree {
757
757
  };
758
758
  n.tabs = Array.isArray(n.tabs) ? n.tabs : [];
759
759
  n.tabs.push(tab);
760
- n.activeTabIdx = n.tabs.length - 1;
761
- _syncActiveTab(n);
762
- return n.activeTabIdx;
760
+ if (!opts.background) {
761
+ n.activeTabIdx = n.tabs.length - 1;
762
+ _syncActiveTab(n);
763
+ }
764
+ return n.tabs.length - 1;
763
765
  }
764
766
  /** Switch the active tab on a leaf. No-op if `idx` is out of range. */
765
767
  setActiveLeafTab(leafId, idx) {
@@ -6528,6 +6530,11 @@ var WindowManager = class _WindowManager {
6528
6530
  * `opts.transient` — the appended tab is not persisted/restored
6529
6531
  * (e.g. an add-row form). Only meaningful with
6530
6532
  * `newTab:true`.
6533
+ * `opts.background` — with `newTab`, append the tab WITHOUT switching to
6534
+ * it or focusing its tile. "Open in a background tab"
6535
+ * means the page you are reading stays in front;
6536
+ * without it the tab arrives and takes the screen,
6537
+ * which is what an ordinary click already does.
6531
6538
  *
6532
6539
  * Back-compat: the legacy `opts.target` enum still works and maps
6533
6540
  * onto the axes — 'auto'→origin, 'tab'→origin+newTab,
@@ -6538,6 +6545,7 @@ var WindowManager = class _WindowManager {
6538
6545
  * `openInWindow`) stay internal; callers prefer `wm.navigate(...)`. */
6539
6546
  navigate(kind, props = {}, opts = {}) {
6540
6547
  const { ctx = null, transient = false } = opts;
6548
+ const background = !!opts.background;
6541
6549
  let { dest = "main", newTab = false } = opts;
6542
6550
  if (opts.target != null) {
6543
6551
  switch (opts.target) {
@@ -6573,9 +6581,9 @@ var WindowManager = class _WindowManager {
6573
6581
  }
6574
6582
  if (dest === "window") return this._navigateWindow(kind, props);
6575
6583
  if (dest === "main") {
6576
- return newTab ? this.openInTabInPrimary(kind, props, transient) : this.openInPrimary(kind, props);
6584
+ return newTab ? this.openInTabInPrimary(kind, props, transient, background) : this.openInPrimary(kind, props);
6577
6585
  }
6578
- return newTab ? this._navigateTab(ctx, kind, props, transient) : this._navigateAuto(ctx, kind, props);
6586
+ return newTab ? this._navigateTab(ctx, kind, props, transient, background) : this._navigateAuto(ctx, kind, props);
6579
6587
  }
6580
6588
  _navigateAuto(ctx, kind, props) {
6581
6589
  if (ctx?.windowId && this._windowToLeaf.has(ctx.windowId)) {
@@ -6593,12 +6601,12 @@ var WindowManager = class _WindowManager {
6593
6601
  }
6594
6602
  this.openInPrimary(kind, props);
6595
6603
  }
6596
- _navigateTab(ctx, kind, props, transient = false) {
6604
+ _navigateTab(ctx, kind, props, transient = false, background = false) {
6597
6605
  if (ctx?.windowId && this._windowToLeaf.has(ctx.windowId)) {
6598
6606
  this.openInWindow(ctx.windowId, kind, props);
6599
6607
  return;
6600
6608
  }
6601
- this.openInTabFromContext(ctx || {}, kind, props, transient);
6609
+ this.openInTabFromContext(ctx || {}, kind, props, transient, background);
6602
6610
  }
6603
6611
  /** Spawn a fresh ManagedWindow with the requested content. No
6604
6612
  * source leaf — closing the window just disposes the content. */
@@ -6719,7 +6727,7 @@ var WindowManager = class _WindowManager {
6719
6727
  * Mirrors `openFromContext` (windowed / split-leaf / primary
6720
6728
  * routing) but uses `appendLeafTab` so the existing content
6721
6729
  * stays in place as a tab. */
6722
- openInTabFromContext(ctx, kind, props = {}, transient = false) {
6730
+ openInTabFromContext(ctx, kind, props = {}, transient = false, background = false) {
6723
6731
  if (ctx?.windowId && this._windowToLeaf.has(ctx.windowId)) {
6724
6732
  this.openInWindow(ctx.windowId, kind, props);
6725
6733
  return;
@@ -6733,8 +6741,13 @@ var WindowManager = class _WindowManager {
6733
6741
  this.openInPrimary(kind, props);
6734
6742
  return;
6735
6743
  }
6736
- tree.appendLeafTab(leafId, { kind, props }, _tabTitle(kind, props), { transient });
6737
- tree.focus(leafId);
6744
+ tree.appendLeafTab(
6745
+ leafId,
6746
+ { kind, props },
6747
+ _tabTitle(kind, props),
6748
+ { transient, background }
6749
+ );
6750
+ if (!background) tree.focus(leafId);
6738
6751
  this.renderer.render();
6739
6752
  this._persist();
6740
6753
  this._notifyChange("tab-open");
@@ -6746,15 +6759,20 @@ var WindowManager = class _WindowManager {
6746
6759
  * click from outside the tile system (e.g. the bottom-panel
6747
6760
  * "Add row" button, which passes no ctx) reliably lands as a sibling
6748
6761
  * tab in the main tile rather than swapping its content. */
6749
- openInTabInPrimary(kind, props = {}, transient = false) {
6762
+ openInTabInPrimary(kind, props = {}, transient = false, background = false) {
6750
6763
  const tree = this._tree();
6751
6764
  const leafId = tree.primaryLeafId();
6752
6765
  if (!leafId) {
6753
6766
  this._navigateWindow(kind, props);
6754
6767
  return;
6755
6768
  }
6756
- tree.appendLeafTab(leafId, { kind, props }, _tabTitle(kind, props), { transient });
6757
- tree.focus(leafId);
6769
+ tree.appendLeafTab(
6770
+ leafId,
6771
+ { kind, props },
6772
+ _tabTitle(kind, props),
6773
+ { transient, background }
6774
+ );
6775
+ if (!background) tree.focus(leafId);
6758
6776
  this.renderer.render();
6759
6777
  this._persist();
6760
6778
  this._notifyChange("tab-open");
@@ -7215,6 +7233,112 @@ function _esc7(s) {
7215
7233
  })[c]);
7216
7234
  }
7217
7235
 
7236
+ // src/tiling/zoom.js
7237
+ var ZOOM_MIN = 50;
7238
+ var ZOOM_MAX = 200;
7239
+ var ZOOM_STEP = 5;
7240
+ var ZOOM_NUDGE = 10;
7241
+ var ZOOM_DEFAULT = 100;
7242
+ var ZOOM_STATE_KEY = "zoom";
7243
+ function clampZoom(value) {
7244
+ if (value == null || value === "") return ZOOM_DEFAULT;
7245
+ const n = Number(value);
7246
+ if (!Number.isFinite(n)) return ZOOM_DEFAULT;
7247
+ const stepped = Math.round(n / ZOOM_STEP) * ZOOM_STEP;
7248
+ return Math.min(ZOOM_MAX, Math.max(ZOOM_MIN, stepped));
7249
+ }
7250
+ function applyZoom(root, percent) {
7251
+ if (!root?.style) return;
7252
+ root.style.setProperty("--twm-zoom", String(percent / 100));
7253
+ root.classList.toggle("twm-zoomed", percent !== ZOOM_DEFAULT);
7254
+ }
7255
+ function mountZoomControl(hostEl, { root, host = null, stateKey = ZOOM_STATE_KEY, onChange } = {}) {
7256
+ if (!hostEl || !root) return null;
7257
+ const doc = hostEl.ownerDocument;
7258
+ let current = ZOOM_DEFAULT;
7259
+ let disposed = false;
7260
+ const el = doc.createElement("div");
7261
+ el.className = "twm-zoom";
7262
+ const stepButton = (label, title, delta) => {
7263
+ const b = doc.createElement("button");
7264
+ b.type = "button";
7265
+ b.className = "twm-zoom__step";
7266
+ b.textContent = label;
7267
+ b.title = title;
7268
+ b.setAttribute("aria-label", title);
7269
+ b.addEventListener("click", () => commit(clampZoom(current + delta)));
7270
+ return b;
7271
+ };
7272
+ const slider = doc.createElement("input");
7273
+ slider.type = "range";
7274
+ slider.className = "twm-zoom__slider";
7275
+ slider.min = String(ZOOM_MIN);
7276
+ slider.max = String(ZOOM_MAX);
7277
+ slider.step = String(ZOOM_STEP);
7278
+ slider.value = String(ZOOM_DEFAULT);
7279
+ slider.title = `Zoom the workspace, ${ZOOM_MIN}\u2013${ZOOM_MAX}% \u2014 double-click to reset.`;
7280
+ slider.setAttribute("aria-label", "Zoom the workspace");
7281
+ slider.addEventListener("input", () => commit(clampZoom(slider.value)));
7282
+ slider.addEventListener("dblclick", () => commit(ZOOM_DEFAULT));
7283
+ const readout = doc.createElement("button");
7284
+ readout.type = "button";
7285
+ readout.className = "twm-zoom__value";
7286
+ readout.title = `Back to ${ZOOM_DEFAULT}%`;
7287
+ readout.addEventListener("click", () => commit(ZOOM_DEFAULT));
7288
+ el.append(
7289
+ stepButton("\u2212", `Zoom out ${ZOOM_NUDGE}%`, -ZOOM_NUDGE),
7290
+ slider,
7291
+ stepButton("+", `Zoom in ${ZOOM_NUDGE}%`, ZOOM_NUDGE),
7292
+ readout
7293
+ );
7294
+ hostEl.appendChild(el);
7295
+ const paint = (percent) => {
7296
+ current = percent;
7297
+ slider.value = String(percent);
7298
+ readout.textContent = `${percent}%`;
7299
+ applyZoom(root, percent);
7300
+ };
7301
+ let saveTimer = 0;
7302
+ const persist = (percent) => {
7303
+ const state = host?.state;
7304
+ if (!state) return;
7305
+ clearTimeout(saveTimer);
7306
+ saveTimer = setTimeout(() => {
7307
+ Promise.resolve().then(() => state.write(stateKey, percent)).catch((err) => console.warn("[zoom] save failed", err));
7308
+ }, 400);
7309
+ };
7310
+ const commit = (percent) => {
7311
+ if (disposed || percent === current) return;
7312
+ paint(percent);
7313
+ persist(percent);
7314
+ try {
7315
+ onChange?.(percent);
7316
+ } catch (err) {
7317
+ console.warn("[zoom] onChange threw", err);
7318
+ }
7319
+ };
7320
+ paint(ZOOM_DEFAULT);
7321
+ const ready = Promise.resolve().then(() => host?.state?.read?.(stateKey)).then((saved) => {
7322
+ if (!disposed && saved != null) paint(clampZoom(saved));
7323
+ return current;
7324
+ }).catch((err) => {
7325
+ console.warn("[zoom] load failed", err);
7326
+ return current;
7327
+ });
7328
+ return {
7329
+ el,
7330
+ get: () => current,
7331
+ set: (percent) => commit(clampZoom(percent)),
7332
+ ready,
7333
+ dispose: () => {
7334
+ disposed = true;
7335
+ clearTimeout(saveTimer);
7336
+ el.remove();
7337
+ applyZoom(root, ZOOM_DEFAULT);
7338
+ }
7339
+ };
7340
+ }
7341
+
7218
7342
  // src/tiling/shell.js
7219
7343
  async function createShell({
7220
7344
  root,
@@ -7302,6 +7426,8 @@ async function createShell({
7302
7426
  const paletteBtn = mountPaletteButton(chrome.paletteButton, palette);
7303
7427
  topNavEl = mountTopNav(chrome.topNav, taxonomy, wm);
7304
7428
  desktopsEl = mountDesktopBar(chrome.desktops, wm);
7429
+ const zoom = mountZoomControl(chrome.zoom, { root, host });
7430
+ if (zoom) await zoom.ready;
7305
7431
  toggles = bindPanelToggles(chrome.panelToggles, wm);
7306
7432
  eventBus?.on?.("wm:changed", syncChrome);
7307
7433
  await wm.load();
@@ -7318,7 +7444,10 @@ async function createShell({
7318
7444
  paletteButtonEl: paletteBtn,
7319
7445
  topNavEl,
7320
7446
  desktopsEl,
7321
- panelToggles: toggles
7447
+ panelToggles: toggles,
7448
+ // `get`/`set` so an embedder can drive the zoom from its own settings
7449
+ // pane, or read it, without reaching into the control's DOM.
7450
+ zoom
7322
7451
  }),
7323
7452
  // A shell that can be built can be built TWICE — an embedder that
7324
7453
  // rebuilds on a context change (a different project, a different
@@ -7343,6 +7472,10 @@ async function createShell({
7343
7472
  } catch (err) {
7344
7473
  log.warn?.("renderer teardown", err);
7345
7474
  }
7475
+ try {
7476
+ zoom?.dispose();
7477
+ } catch {
7478
+ }
7346
7479
  }
7347
7480
  });
7348
7481
  }
@@ -7640,10 +7773,18 @@ export {
7640
7773
  TileRenderer,
7641
7774
  TileTree,
7642
7775
  WindowManager,
7776
+ ZOOM_DEFAULT,
7777
+ ZOOM_MAX,
7778
+ ZOOM_MIN,
7779
+ ZOOM_NUDGE,
7780
+ ZOOM_STATE_KEY,
7781
+ ZOOM_STEP,
7643
7782
  actionsCellRenderer,
7783
+ applyZoom,
7644
7784
  attachLandingKeyboardNav,
7645
7785
  attachLandingShell,
7646
7786
  attachLandingTableBehavior,
7787
+ clampZoom,
7647
7788
  createCommandPalette,
7648
7789
  createContentRegistry,
7649
7790
  createEntityCatalog,
@@ -7661,6 +7802,7 @@ export {
7661
7802
  mountLandingShell,
7662
7803
  mountNavPanel,
7663
7804
  mountTileBreadcrumb,
7805
+ mountZoomControl,
7664
7806
  openTileTabMenu,
7665
7807
  openTileTabSwitcher,
7666
7808
  registerPanelKeys,