flexdesk 0.4.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 CHANGED
@@ -106,13 +106,14 @@ lives in `demo/`.
106
106
 
107
107
  ### Opt-in shell features
108
108
 
109
- Two features change what the user sees, so both are off until you ask:
109
+ These change what the user sees, so all of them are off until you ask:
110
110
 
111
111
  ```js
112
112
  await createShell({
113
113
  // …everything above…
114
114
  snapPromotion: true, // drag a floating window onto a tile: it snaps, and docks back in
115
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
116
117
  chrome: {
117
118
  topNav: document.getElementById('nav'),
118
119
  zoom: document.getElementById('status-bar'), // content zoom, 50–200%
@@ -130,6 +131,14 @@ for a settings pane. An embedder that builds its own `WindowManager` instead of
130
131
  calling `createShell` mounts the same control with `mountZoomControl(el, { root,
131
132
  host })`.
132
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
+
133
142
  ## The host port
134
143
 
135
144
  FlexDesk never touches `pywebview`, `fetch`, `localStorage` or the filesystem. It
package/dist/wm.js CHANGED
@@ -3978,7 +3978,8 @@ var WindowManager = class _WindowManager {
3978
3978
  panelDefaults = null,
3979
3979
  snapPromotion = false,
3980
3980
  promoteInPlace = false,
3981
- tabLayout = null
3981
+ tabLayout = null,
3982
+ backToOpenList = false
3982
3983
  }) {
3983
3984
  if (!taxonomy) throw new Error("WindowManager: a taxonomy is required");
3984
3985
  if (!content || typeof content.mount !== "function") {
@@ -4004,6 +4005,7 @@ var WindowManager = class _WindowManager {
4004
4005
  this.panelDefaults = panelDefaults || null;
4005
4006
  this.snapPromotion = !!snapPromotion;
4006
4007
  this.promoteInPlace = !!promoteInPlace;
4008
+ this.backToOpenList = !!backToOpenList;
4007
4009
  this._snapCtl = null;
4008
4010
  this.desktops = new DesktopManager({
4009
4011
  seed: this._rootLeaf,
@@ -4186,6 +4188,7 @@ var WindowManager = class _WindowManager {
4186
4188
  this._notifyChange("navigate-back");
4187
4189
  return;
4188
4190
  }
4191
+ if (this.backToOpenList && this._backToOpenList(id)) return;
4189
4192
  const leaf = tree.get(id);
4190
4193
  if (!leaf?.content) return;
4191
4194
  const { kind, props } = leaf.content;
@@ -4230,6 +4233,64 @@ var WindowManager = class _WindowManager {
4230
4233
  /** Navigate inside the current tab — preserves every other tab in
4231
4234
  * the leaf. Used by Backspace + breadcrumb segments + anything
4232
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
+ }
4233
4294
  navigateActiveTab(leafId, kind, props = {}) {
4234
4295
  const tree = this._tree();
4235
4296
  const leaf = tree.get(leafId);
@@ -6843,6 +6904,9 @@ function _swapSiblings(tree, aId, bId) {
6843
6904
  parent.sizes[j] = size;
6844
6905
  return true;
6845
6906
  }
6907
+ function _isEntityTab(tab) {
6908
+ return !!tab && tab.props != null && tab.props.id != null && tab.props.id !== "";
6909
+ }
6846
6910
 
6847
6911
  // src/tiling/tile_tab_menu.js
6848
6912
  var PAGE_SIZE = 10;
@@ -7356,6 +7420,7 @@ async function createShell({
7356
7420
  panels = null,
7357
7421
  snapPromotion = false,
7358
7422
  promoteInPlace = false,
7423
+ backToOpenList = false,
7359
7424
  tabLayout = null,
7360
7425
  chrome = {},
7361
7426
  // An embedder that moved its sections out of the top bar — into an icon
@@ -7395,6 +7460,7 @@ async function createShell({
7395
7460
  panelDefaults: panels,
7396
7461
  snapPromotion,
7397
7462
  promoteInPlace,
7463
+ backToOpenList,
7398
7464
  tabLayout,
7399
7465
  // `ctx` is the delivery vehicle for leaf-mounted chrome: tile_renderer
7400
7466
  // spreads it into every content factory, which is how the breadcrumb