flexdesk 0.3.0 → 0.4.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 +35 -0
- package/css/base.css +68 -0
- package/css/flexdesk.css +68 -0
- package/dist/flexdesk.css +68 -0
- package/dist/wm.js +224 -16
- package/dist/wm.js.map +3 -3
- package/package.json +1 -1
- package/src/tiling/shell.js +28 -1
- package/src/tiling/tile_tree.js +33 -11
- package/src/tiling/wm.js +101 -12
- package/src/tiling/zoom.js +248 -0
package/README.md
CHANGED
|
@@ -104,6 +104,41 @@ 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
|
+
These change what the user sees, so all of them 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
|
+
backToOpenList: true, // Back in a record opened beside its list closes onto that list
|
|
117
|
+
chrome: {
|
|
118
|
+
topNav: document.getElementById('nav'),
|
|
119
|
+
zoom: document.getElementById('status-bar'), // content zoom, 50–200%
|
|
120
|
+
},
|
|
121
|
+
});
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
**`chrome.zoom`** paints a − / track / + / readout control into the element you
|
|
125
|
+
pass, restores the saved zoom through the host's `state` capability, and scales
|
|
126
|
+
the content of tiles and floating windows. It deliberately scales *content only*
|
|
127
|
+
— never the root, a tile's frame or a window's frame — because CSS `zoom`
|
|
128
|
+
establishes a scaled coordinate space and every window drag, resize and snap
|
|
129
|
+
measures in real pixels. `shell.chrome.zoom` exposes `get()` and `set(percent)`
|
|
130
|
+
for a settings pane. An embedder that builds its own `WindowManager` instead of
|
|
131
|
+
calling `createShell` mounts the same control with `mountZoomControl(el, { root,
|
|
132
|
+
host })`.
|
|
133
|
+
|
|
134
|
+
**`backToOpenList`** is for apps that open a record from a list in a new tab.
|
|
135
|
+
Without it, Back in that record has no history to pop, so it walks the taxonomy
|
|
136
|
+
up and rewrites the record into its parent, and the tile now shows the list
|
|
137
|
+
twice. With it, Back closes the record tab and activates the list it came from:
|
|
138
|
+
the nearest tab to the left, else to the right, that is not a record (no
|
|
139
|
+
`props.id`) and belongs to the same top-nav section. Per-tab history still comes
|
|
140
|
+
first, and a record with no such list beside it still walks up in place.
|
|
141
|
+
|
|
107
142
|
## The host port
|
|
108
143
|
|
|
109
144
|
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
|
-
|
|
761
|
-
|
|
762
|
-
|
|
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) {
|
|
@@ -3976,7 +3978,8 @@ var WindowManager = class _WindowManager {
|
|
|
3976
3978
|
panelDefaults = null,
|
|
3977
3979
|
snapPromotion = false,
|
|
3978
3980
|
promoteInPlace = false,
|
|
3979
|
-
tabLayout = null
|
|
3981
|
+
tabLayout = null,
|
|
3982
|
+
backToOpenList = false
|
|
3980
3983
|
}) {
|
|
3981
3984
|
if (!taxonomy) throw new Error("WindowManager: a taxonomy is required");
|
|
3982
3985
|
if (!content || typeof content.mount !== "function") {
|
|
@@ -4002,6 +4005,7 @@ var WindowManager = class _WindowManager {
|
|
|
4002
4005
|
this.panelDefaults = panelDefaults || null;
|
|
4003
4006
|
this.snapPromotion = !!snapPromotion;
|
|
4004
4007
|
this.promoteInPlace = !!promoteInPlace;
|
|
4008
|
+
this.backToOpenList = !!backToOpenList;
|
|
4005
4009
|
this._snapCtl = null;
|
|
4006
4010
|
this.desktops = new DesktopManager({
|
|
4007
4011
|
seed: this._rootLeaf,
|
|
@@ -4184,6 +4188,7 @@ var WindowManager = class _WindowManager {
|
|
|
4184
4188
|
this._notifyChange("navigate-back");
|
|
4185
4189
|
return;
|
|
4186
4190
|
}
|
|
4191
|
+
if (this.backToOpenList && this._backToOpenList(id)) return;
|
|
4187
4192
|
const leaf = tree.get(id);
|
|
4188
4193
|
if (!leaf?.content) return;
|
|
4189
4194
|
const { kind, props } = leaf.content;
|
|
@@ -4228,6 +4233,64 @@ var WindowManager = class _WindowManager {
|
|
|
4228
4233
|
/** Navigate inside the current tab — preserves every other tab in
|
|
4229
4234
|
* the leaf. Used by Backspace + breadcrumb segments + anything
|
|
4230
4235
|
* else that should walk WITHIN the tile rather than reset it. */
|
|
4236
|
+
/**
|
|
4237
|
+
* C32. BACK FROM AN OPENED RECORD RETURNS TO THE LIST, IT DOES NOT MAKE ONE.
|
|
4238
|
+
*
|
|
4239
|
+
* The gesture this is for: a list tab, a record opened from it into a NEW
|
|
4240
|
+
* tab, and Backspace in that record. With nothing left in the record tab's
|
|
4241
|
+
* own history, step (3) of `navigateBack` walks the taxonomy up and REWRITES
|
|
4242
|
+
* the record tab into its parent — so the tile ends up showing the list
|
|
4243
|
+
* twice, once in the tab you started from and once in the tab you just left.
|
|
4244
|
+
* The user's model is a browser's: you came from the list, the list is still
|
|
4245
|
+
* open, so going back means closing this and being on the list again.
|
|
4246
|
+
*
|
|
4247
|
+
* So: when the active tab is an ENTITY — its props carry an `id`, the same
|
|
4248
|
+
* test `swapToPage` uses for "the caller asked for a specific entity" — look
|
|
4249
|
+
* for an open tab of the SAME section that is NOT one. That is the list or
|
|
4250
|
+
* landing page the record belongs to. Close the record and activate it.
|
|
4251
|
+
*
|
|
4252
|
+
* NEAREST TO THE LEFT FIRST. New tabs are appended at the end, so the tab a
|
|
4253
|
+
* record was opened from sits to its left; when several lists of the section
|
|
4254
|
+
* are open, the nearest one on that side is the one it most plausibly came
|
|
4255
|
+
* from. The right side is searched only after the left has nothing.
|
|
4256
|
+
*
|
|
4257
|
+
* DECLINES, and leaves step (3) to walk up in place, whenever there is no
|
|
4258
|
+
* such tab: a record that is the tile's only tab, or one whose section has no
|
|
4259
|
+
* list open. That keeps the one guarantee the whole rule exists for — Back
|
|
4260
|
+
* never leaves two copies of a list — without ever closing a record into a
|
|
4261
|
+
* tile that has nothing of its own to show.
|
|
4262
|
+
*
|
|
4263
|
+
* Matching is by SECTION (`topNavFor`) and not by kind, because the list a
|
|
4264
|
+
* record was opened from is often not the record's taxonomy parent: a root
|
|
4265
|
+
* landing page that renders the section, or a list that a flattened taxonomy
|
|
4266
|
+
* files beside the record rather than above it.
|
|
4267
|
+
*
|
|
4268
|
+
* @returns {boolean} whether it closed a tab
|
|
4269
|
+
*/
|
|
4270
|
+
_backToOpenList(leafId) {
|
|
4271
|
+
const tree = this._tree();
|
|
4272
|
+
const leaf = tree.get(leafId);
|
|
4273
|
+
if (!leaf || leaf.kind !== "leaf") return false;
|
|
4274
|
+
const tabs = Array.isArray(leaf.tabs) ? leaf.tabs : [];
|
|
4275
|
+
if (tabs.length < 2) return false;
|
|
4276
|
+
const activeIdx = Math.max(0, Math.min(tabs.length - 1, leaf.activeTabIdx || 0));
|
|
4277
|
+
const active = tabs[activeIdx];
|
|
4278
|
+
if (!_isEntityTab(active)) return false;
|
|
4279
|
+
const section = this.taxonomy.topNavFor(active.kind);
|
|
4280
|
+
if (!section) return false;
|
|
4281
|
+
const isList = (t) => t && !_isEntityTab(t) && this.taxonomy.topNavFor(t.kind) === section;
|
|
4282
|
+
let target = -1;
|
|
4283
|
+
for (let i = activeIdx - 1; i >= 0 && target < 0; i--) if (isList(tabs[i])) target = i;
|
|
4284
|
+
for (let i = activeIdx + 1; i < tabs.length && target < 0; i++) if (isList(tabs[i])) target = i;
|
|
4285
|
+
if (target < 0) return false;
|
|
4286
|
+
tree.removeLeafTab(leafId, activeIdx);
|
|
4287
|
+
tree.setActiveLeafTab(leafId, target > activeIdx ? target - 1 : target);
|
|
4288
|
+
tree.focus(leafId);
|
|
4289
|
+
this.renderer.render();
|
|
4290
|
+
this._persist();
|
|
4291
|
+
this._notifyChange("navigate-back-close");
|
|
4292
|
+
return true;
|
|
4293
|
+
}
|
|
4231
4294
|
navigateActiveTab(leafId, kind, props = {}) {
|
|
4232
4295
|
const tree = this._tree();
|
|
4233
4296
|
const leaf = tree.get(leafId);
|
|
@@ -6528,6 +6591,11 @@ var WindowManager = class _WindowManager {
|
|
|
6528
6591
|
* `opts.transient` — the appended tab is not persisted/restored
|
|
6529
6592
|
* (e.g. an add-row form). Only meaningful with
|
|
6530
6593
|
* `newTab:true`.
|
|
6594
|
+
* `opts.background` — with `newTab`, append the tab WITHOUT switching to
|
|
6595
|
+
* it or focusing its tile. "Open in a background tab"
|
|
6596
|
+
* means the page you are reading stays in front;
|
|
6597
|
+
* without it the tab arrives and takes the screen,
|
|
6598
|
+
* which is what an ordinary click already does.
|
|
6531
6599
|
*
|
|
6532
6600
|
* Back-compat: the legacy `opts.target` enum still works and maps
|
|
6533
6601
|
* onto the axes — 'auto'→origin, 'tab'→origin+newTab,
|
|
@@ -6538,6 +6606,7 @@ var WindowManager = class _WindowManager {
|
|
|
6538
6606
|
* `openInWindow`) stay internal; callers prefer `wm.navigate(...)`. */
|
|
6539
6607
|
navigate(kind, props = {}, opts = {}) {
|
|
6540
6608
|
const { ctx = null, transient = false } = opts;
|
|
6609
|
+
const background = !!opts.background;
|
|
6541
6610
|
let { dest = "main", newTab = false } = opts;
|
|
6542
6611
|
if (opts.target != null) {
|
|
6543
6612
|
switch (opts.target) {
|
|
@@ -6573,9 +6642,9 @@ var WindowManager = class _WindowManager {
|
|
|
6573
6642
|
}
|
|
6574
6643
|
if (dest === "window") return this._navigateWindow(kind, props);
|
|
6575
6644
|
if (dest === "main") {
|
|
6576
|
-
return newTab ? this.openInTabInPrimary(kind, props, transient) : this.openInPrimary(kind, props);
|
|
6645
|
+
return newTab ? this.openInTabInPrimary(kind, props, transient, background) : this.openInPrimary(kind, props);
|
|
6577
6646
|
}
|
|
6578
|
-
return newTab ? this._navigateTab(ctx, kind, props, transient) : this._navigateAuto(ctx, kind, props);
|
|
6647
|
+
return newTab ? this._navigateTab(ctx, kind, props, transient, background) : this._navigateAuto(ctx, kind, props);
|
|
6579
6648
|
}
|
|
6580
6649
|
_navigateAuto(ctx, kind, props) {
|
|
6581
6650
|
if (ctx?.windowId && this._windowToLeaf.has(ctx.windowId)) {
|
|
@@ -6593,12 +6662,12 @@ var WindowManager = class _WindowManager {
|
|
|
6593
6662
|
}
|
|
6594
6663
|
this.openInPrimary(kind, props);
|
|
6595
6664
|
}
|
|
6596
|
-
_navigateTab(ctx, kind, props, transient = false) {
|
|
6665
|
+
_navigateTab(ctx, kind, props, transient = false, background = false) {
|
|
6597
6666
|
if (ctx?.windowId && this._windowToLeaf.has(ctx.windowId)) {
|
|
6598
6667
|
this.openInWindow(ctx.windowId, kind, props);
|
|
6599
6668
|
return;
|
|
6600
6669
|
}
|
|
6601
|
-
this.openInTabFromContext(ctx || {}, kind, props, transient);
|
|
6670
|
+
this.openInTabFromContext(ctx || {}, kind, props, transient, background);
|
|
6602
6671
|
}
|
|
6603
6672
|
/** Spawn a fresh ManagedWindow with the requested content. No
|
|
6604
6673
|
* source leaf — closing the window just disposes the content. */
|
|
@@ -6719,7 +6788,7 @@ var WindowManager = class _WindowManager {
|
|
|
6719
6788
|
* Mirrors `openFromContext` (windowed / split-leaf / primary
|
|
6720
6789
|
* routing) but uses `appendLeafTab` so the existing content
|
|
6721
6790
|
* stays in place as a tab. */
|
|
6722
|
-
openInTabFromContext(ctx, kind, props = {}, transient = false) {
|
|
6791
|
+
openInTabFromContext(ctx, kind, props = {}, transient = false, background = false) {
|
|
6723
6792
|
if (ctx?.windowId && this._windowToLeaf.has(ctx.windowId)) {
|
|
6724
6793
|
this.openInWindow(ctx.windowId, kind, props);
|
|
6725
6794
|
return;
|
|
@@ -6733,8 +6802,13 @@ var WindowManager = class _WindowManager {
|
|
|
6733
6802
|
this.openInPrimary(kind, props);
|
|
6734
6803
|
return;
|
|
6735
6804
|
}
|
|
6736
|
-
tree.appendLeafTab(
|
|
6737
|
-
|
|
6805
|
+
tree.appendLeafTab(
|
|
6806
|
+
leafId,
|
|
6807
|
+
{ kind, props },
|
|
6808
|
+
_tabTitle(kind, props),
|
|
6809
|
+
{ transient, background }
|
|
6810
|
+
);
|
|
6811
|
+
if (!background) tree.focus(leafId);
|
|
6738
6812
|
this.renderer.render();
|
|
6739
6813
|
this._persist();
|
|
6740
6814
|
this._notifyChange("tab-open");
|
|
@@ -6746,15 +6820,20 @@ var WindowManager = class _WindowManager {
|
|
|
6746
6820
|
* click from outside the tile system (e.g. the bottom-panel
|
|
6747
6821
|
* "Add row" button, which passes no ctx) reliably lands as a sibling
|
|
6748
6822
|
* tab in the main tile rather than swapping its content. */
|
|
6749
|
-
openInTabInPrimary(kind, props = {}, transient = false) {
|
|
6823
|
+
openInTabInPrimary(kind, props = {}, transient = false, background = false) {
|
|
6750
6824
|
const tree = this._tree();
|
|
6751
6825
|
const leafId = tree.primaryLeafId();
|
|
6752
6826
|
if (!leafId) {
|
|
6753
6827
|
this._navigateWindow(kind, props);
|
|
6754
6828
|
return;
|
|
6755
6829
|
}
|
|
6756
|
-
tree.appendLeafTab(
|
|
6757
|
-
|
|
6830
|
+
tree.appendLeafTab(
|
|
6831
|
+
leafId,
|
|
6832
|
+
{ kind, props },
|
|
6833
|
+
_tabTitle(kind, props),
|
|
6834
|
+
{ transient, background }
|
|
6835
|
+
);
|
|
6836
|
+
if (!background) tree.focus(leafId);
|
|
6758
6837
|
this.renderer.render();
|
|
6759
6838
|
this._persist();
|
|
6760
6839
|
this._notifyChange("tab-open");
|
|
@@ -6825,6 +6904,9 @@ function _swapSiblings(tree, aId, bId) {
|
|
|
6825
6904
|
parent.sizes[j] = size;
|
|
6826
6905
|
return true;
|
|
6827
6906
|
}
|
|
6907
|
+
function _isEntityTab(tab) {
|
|
6908
|
+
return !!tab && tab.props != null && tab.props.id != null && tab.props.id !== "";
|
|
6909
|
+
}
|
|
6828
6910
|
|
|
6829
6911
|
// src/tiling/tile_tab_menu.js
|
|
6830
6912
|
var PAGE_SIZE = 10;
|
|
@@ -7215,6 +7297,112 @@ function _esc7(s) {
|
|
|
7215
7297
|
})[c]);
|
|
7216
7298
|
}
|
|
7217
7299
|
|
|
7300
|
+
// src/tiling/zoom.js
|
|
7301
|
+
var ZOOM_MIN = 50;
|
|
7302
|
+
var ZOOM_MAX = 200;
|
|
7303
|
+
var ZOOM_STEP = 5;
|
|
7304
|
+
var ZOOM_NUDGE = 10;
|
|
7305
|
+
var ZOOM_DEFAULT = 100;
|
|
7306
|
+
var ZOOM_STATE_KEY = "zoom";
|
|
7307
|
+
function clampZoom(value) {
|
|
7308
|
+
if (value == null || value === "") return ZOOM_DEFAULT;
|
|
7309
|
+
const n = Number(value);
|
|
7310
|
+
if (!Number.isFinite(n)) return ZOOM_DEFAULT;
|
|
7311
|
+
const stepped = Math.round(n / ZOOM_STEP) * ZOOM_STEP;
|
|
7312
|
+
return Math.min(ZOOM_MAX, Math.max(ZOOM_MIN, stepped));
|
|
7313
|
+
}
|
|
7314
|
+
function applyZoom(root, percent) {
|
|
7315
|
+
if (!root?.style) return;
|
|
7316
|
+
root.style.setProperty("--twm-zoom", String(percent / 100));
|
|
7317
|
+
root.classList.toggle("twm-zoomed", percent !== ZOOM_DEFAULT);
|
|
7318
|
+
}
|
|
7319
|
+
function mountZoomControl(hostEl, { root, host = null, stateKey = ZOOM_STATE_KEY, onChange } = {}) {
|
|
7320
|
+
if (!hostEl || !root) return null;
|
|
7321
|
+
const doc = hostEl.ownerDocument;
|
|
7322
|
+
let current = ZOOM_DEFAULT;
|
|
7323
|
+
let disposed = false;
|
|
7324
|
+
const el = doc.createElement("div");
|
|
7325
|
+
el.className = "twm-zoom";
|
|
7326
|
+
const stepButton = (label, title, delta) => {
|
|
7327
|
+
const b = doc.createElement("button");
|
|
7328
|
+
b.type = "button";
|
|
7329
|
+
b.className = "twm-zoom__step";
|
|
7330
|
+
b.textContent = label;
|
|
7331
|
+
b.title = title;
|
|
7332
|
+
b.setAttribute("aria-label", title);
|
|
7333
|
+
b.addEventListener("click", () => commit(clampZoom(current + delta)));
|
|
7334
|
+
return b;
|
|
7335
|
+
};
|
|
7336
|
+
const slider = doc.createElement("input");
|
|
7337
|
+
slider.type = "range";
|
|
7338
|
+
slider.className = "twm-zoom__slider";
|
|
7339
|
+
slider.min = String(ZOOM_MIN);
|
|
7340
|
+
slider.max = String(ZOOM_MAX);
|
|
7341
|
+
slider.step = String(ZOOM_STEP);
|
|
7342
|
+
slider.value = String(ZOOM_DEFAULT);
|
|
7343
|
+
slider.title = `Zoom the workspace, ${ZOOM_MIN}\u2013${ZOOM_MAX}% \u2014 double-click to reset.`;
|
|
7344
|
+
slider.setAttribute("aria-label", "Zoom the workspace");
|
|
7345
|
+
slider.addEventListener("input", () => commit(clampZoom(slider.value)));
|
|
7346
|
+
slider.addEventListener("dblclick", () => commit(ZOOM_DEFAULT));
|
|
7347
|
+
const readout = doc.createElement("button");
|
|
7348
|
+
readout.type = "button";
|
|
7349
|
+
readout.className = "twm-zoom__value";
|
|
7350
|
+
readout.title = `Back to ${ZOOM_DEFAULT}%`;
|
|
7351
|
+
readout.addEventListener("click", () => commit(ZOOM_DEFAULT));
|
|
7352
|
+
el.append(
|
|
7353
|
+
stepButton("\u2212", `Zoom out ${ZOOM_NUDGE}%`, -ZOOM_NUDGE),
|
|
7354
|
+
slider,
|
|
7355
|
+
stepButton("+", `Zoom in ${ZOOM_NUDGE}%`, ZOOM_NUDGE),
|
|
7356
|
+
readout
|
|
7357
|
+
);
|
|
7358
|
+
hostEl.appendChild(el);
|
|
7359
|
+
const paint = (percent) => {
|
|
7360
|
+
current = percent;
|
|
7361
|
+
slider.value = String(percent);
|
|
7362
|
+
readout.textContent = `${percent}%`;
|
|
7363
|
+
applyZoom(root, percent);
|
|
7364
|
+
};
|
|
7365
|
+
let saveTimer = 0;
|
|
7366
|
+
const persist = (percent) => {
|
|
7367
|
+
const state = host?.state;
|
|
7368
|
+
if (!state) return;
|
|
7369
|
+
clearTimeout(saveTimer);
|
|
7370
|
+
saveTimer = setTimeout(() => {
|
|
7371
|
+
Promise.resolve().then(() => state.write(stateKey, percent)).catch((err) => console.warn("[zoom] save failed", err));
|
|
7372
|
+
}, 400);
|
|
7373
|
+
};
|
|
7374
|
+
const commit = (percent) => {
|
|
7375
|
+
if (disposed || percent === current) return;
|
|
7376
|
+
paint(percent);
|
|
7377
|
+
persist(percent);
|
|
7378
|
+
try {
|
|
7379
|
+
onChange?.(percent);
|
|
7380
|
+
} catch (err) {
|
|
7381
|
+
console.warn("[zoom] onChange threw", err);
|
|
7382
|
+
}
|
|
7383
|
+
};
|
|
7384
|
+
paint(ZOOM_DEFAULT);
|
|
7385
|
+
const ready = Promise.resolve().then(() => host?.state?.read?.(stateKey)).then((saved) => {
|
|
7386
|
+
if (!disposed && saved != null) paint(clampZoom(saved));
|
|
7387
|
+
return current;
|
|
7388
|
+
}).catch((err) => {
|
|
7389
|
+
console.warn("[zoom] load failed", err);
|
|
7390
|
+
return current;
|
|
7391
|
+
});
|
|
7392
|
+
return {
|
|
7393
|
+
el,
|
|
7394
|
+
get: () => current,
|
|
7395
|
+
set: (percent) => commit(clampZoom(percent)),
|
|
7396
|
+
ready,
|
|
7397
|
+
dispose: () => {
|
|
7398
|
+
disposed = true;
|
|
7399
|
+
clearTimeout(saveTimer);
|
|
7400
|
+
el.remove();
|
|
7401
|
+
applyZoom(root, ZOOM_DEFAULT);
|
|
7402
|
+
}
|
|
7403
|
+
};
|
|
7404
|
+
}
|
|
7405
|
+
|
|
7218
7406
|
// src/tiling/shell.js
|
|
7219
7407
|
async function createShell({
|
|
7220
7408
|
root,
|
|
@@ -7232,6 +7420,7 @@ async function createShell({
|
|
|
7232
7420
|
panels = null,
|
|
7233
7421
|
snapPromotion = false,
|
|
7234
7422
|
promoteInPlace = false,
|
|
7423
|
+
backToOpenList = false,
|
|
7235
7424
|
tabLayout = null,
|
|
7236
7425
|
chrome = {},
|
|
7237
7426
|
// An embedder that moved its sections out of the top bar — into an icon
|
|
@@ -7271,6 +7460,7 @@ async function createShell({
|
|
|
7271
7460
|
panelDefaults: panels,
|
|
7272
7461
|
snapPromotion,
|
|
7273
7462
|
promoteInPlace,
|
|
7463
|
+
backToOpenList,
|
|
7274
7464
|
tabLayout,
|
|
7275
7465
|
// `ctx` is the delivery vehicle for leaf-mounted chrome: tile_renderer
|
|
7276
7466
|
// spreads it into every content factory, which is how the breadcrumb
|
|
@@ -7302,6 +7492,8 @@ async function createShell({
|
|
|
7302
7492
|
const paletteBtn = mountPaletteButton(chrome.paletteButton, palette);
|
|
7303
7493
|
topNavEl = mountTopNav(chrome.topNav, taxonomy, wm);
|
|
7304
7494
|
desktopsEl = mountDesktopBar(chrome.desktops, wm);
|
|
7495
|
+
const zoom = mountZoomControl(chrome.zoom, { root, host });
|
|
7496
|
+
if (zoom) await zoom.ready;
|
|
7305
7497
|
toggles = bindPanelToggles(chrome.panelToggles, wm);
|
|
7306
7498
|
eventBus?.on?.("wm:changed", syncChrome);
|
|
7307
7499
|
await wm.load();
|
|
@@ -7318,7 +7510,10 @@ async function createShell({
|
|
|
7318
7510
|
paletteButtonEl: paletteBtn,
|
|
7319
7511
|
topNavEl,
|
|
7320
7512
|
desktopsEl,
|
|
7321
|
-
panelToggles: toggles
|
|
7513
|
+
panelToggles: toggles,
|
|
7514
|
+
// `get`/`set` so an embedder can drive the zoom from its own settings
|
|
7515
|
+
// pane, or read it, without reaching into the control's DOM.
|
|
7516
|
+
zoom
|
|
7322
7517
|
}),
|
|
7323
7518
|
// A shell that can be built can be built TWICE — an embedder that
|
|
7324
7519
|
// rebuilds on a context change (a different project, a different
|
|
@@ -7343,6 +7538,10 @@ async function createShell({
|
|
|
7343
7538
|
} catch (err) {
|
|
7344
7539
|
log.warn?.("renderer teardown", err);
|
|
7345
7540
|
}
|
|
7541
|
+
try {
|
|
7542
|
+
zoom?.dispose();
|
|
7543
|
+
} catch {
|
|
7544
|
+
}
|
|
7346
7545
|
}
|
|
7347
7546
|
});
|
|
7348
7547
|
}
|
|
@@ -7640,10 +7839,18 @@ export {
|
|
|
7640
7839
|
TileRenderer,
|
|
7641
7840
|
TileTree,
|
|
7642
7841
|
WindowManager,
|
|
7842
|
+
ZOOM_DEFAULT,
|
|
7843
|
+
ZOOM_MAX,
|
|
7844
|
+
ZOOM_MIN,
|
|
7845
|
+
ZOOM_NUDGE,
|
|
7846
|
+
ZOOM_STATE_KEY,
|
|
7847
|
+
ZOOM_STEP,
|
|
7643
7848
|
actionsCellRenderer,
|
|
7849
|
+
applyZoom,
|
|
7644
7850
|
attachLandingKeyboardNav,
|
|
7645
7851
|
attachLandingShell,
|
|
7646
7852
|
attachLandingTableBehavior,
|
|
7853
|
+
clampZoom,
|
|
7647
7854
|
createCommandPalette,
|
|
7648
7855
|
createContentRegistry,
|
|
7649
7856
|
createEntityCatalog,
|
|
@@ -7661,6 +7868,7 @@ export {
|
|
|
7661
7868
|
mountLandingShell,
|
|
7662
7869
|
mountNavPanel,
|
|
7663
7870
|
mountTileBreadcrumb,
|
|
7871
|
+
mountZoomControl,
|
|
7664
7872
|
openTileTabMenu,
|
|
7665
7873
|
openTileTabSwitcher,
|
|
7666
7874
|
registerPanelKeys,
|