@yuneta/gobj-ui 5.2.1 → 5.3.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.
@@ -344,6 +344,11 @@ function _is_non_empty_string(v) {
344
344
  * - root `/` is NEVER an ancestor catch-all (it only matches
345
345
  * exactly): otherwise every typo route would silently mount
346
346
  * the home view and the unknown-route diagnostic would die.
347
+ * ONE exception, opt-in: an entry whose target declares
348
+ * `owns_subtree` is a view that resolves the tail ITSELF and
349
+ * says so when a segment names nothing — a C_YUI_NODE tree
350
+ * rooted at `/`. The diagnostic is not lost there, it moves
351
+ * one layer down to whoever actually knows the names.
347
352
  * - nothing matched → entry as found (may be
348
353
  * null or a targetless submenu parent), subpath "".
349
354
  ************************************************************/
@@ -371,7 +376,15 @@ function resolve_route(item_index, route) {
371
376
  while (parts.length > 0 && (!entry || !entry.target)) {
372
377
  parts.pop();
373
378
  let cand = "/" + parts.join("/");
374
- if (cand === "/") break;
379
+ if (cand === "/") {
380
+ let root = item_index["/"];
381
+ if (root && root.target && root.target.owns_subtree) {
382
+ entry = root;
383
+ matched_route = "/";
384
+ subpath = route.replace(/^\/+/, "");
385
+ }
386
+ break;
387
+ }
375
388
  let e = item_index[cand];
376
389
  if (e && e.target) {
377
390
  entry = e;
@@ -488,6 +501,38 @@ function collect_routes(nodes, into) {
488
501
  }
489
502
  }
490
503
  /************************************************************
504
+ * A route can be reached from more than one surface: the primary
505
+ * menu, a drawer, an account dropdown. Each occurrence rendered
506
+ * its whole SUBTREE, so a branch reachable three ways was drawn
507
+ * three times — bearable when a section had four children, noise
508
+ * once a node tree hangs there with fourteen.
509
+ *
510
+ * Exactly one occurrence owns the subtree; the others become
511
+ * references (`ref: true`, no children) — still real, clickable
512
+ * routes, just not repeated structure.
513
+ *
514
+ * Who owns it: the first occurrence in the NAV, then in `other`,
515
+ * then in the toolbar. The nav is where the app's structure
516
+ * lives; a toolbar entry pointing at the same route is a
517
+ * shortcut — a non-structural edge — and hanging the tree off it
518
+ * would bury the whole structure inside the account menu.
519
+ ************************************************************/
520
+ function dedupe_subtrees(groups) {
521
+ let owner = {};
522
+ let visit = (node) => {
523
+ if (node.route) {
524
+ if (owner[node.route] === void 0) owner[node.route] = node;
525
+ else if (owner[node.route] !== node) {
526
+ node.ref = true;
527
+ node.children = [];
528
+ return;
529
+ }
530
+ }
531
+ if (Array.isArray(node.children)) node.children.forEach(visit);
532
+ };
533
+ for (let group of groups) group.forEach(visit);
534
+ }
535
+ /************************************************************
491
536
  * Mark "you are here": the node whose route best matches
492
537
  * current_route — exact hit wins, else the LONGEST declared
493
538
  * route that is a path-prefix of it (the base view of a deep
@@ -497,6 +542,7 @@ function mark_current(groups, current_route) {
497
542
  if (!current_route) return;
498
543
  let best = null;
499
544
  let visit = (n) => {
545
+ if (n.ref) return;
500
546
  if (n.route) {
501
547
  if (n.route === current_route) {
502
548
  if (!best || best.node.route !== current_route) best = {
@@ -614,6 +660,11 @@ function build_nav_map(input) {
614
660
  });
615
661
  }
616
662
  other.forEach(enrich);
663
+ dedupe_subtrees([
664
+ nav,
665
+ other,
666
+ toolbar
667
+ ]);
617
668
  mark_current([
618
669
  toolbar,
619
670
  nav,
@@ -775,6 +826,7 @@ var attrs_table$17 = [
775
826
  (0, _yuneta_gobj_js.SDATA)(_yuneta_gobj_js.data_type_t.DTP_JSON, "config", 0, null, "Shell declarative config (zones, menu, stages, toolbar)"),
776
827
  (0, _yuneta_gobj_js.SDATA)(_yuneta_gobj_js.data_type_t.DTP_STRING, "default_route", 0, "", "Fallback route if hash is empty"),
777
828
  (0, _yuneta_gobj_js.SDATA)(_yuneta_gobj_js.data_type_t.DTP_STRING, "current_route", 0, "", "Current active route"),
829
+ (0, _yuneta_gobj_js.SDATA)(_yuneta_gobj_js.data_type_t.DTP_BOOLEAN, "remember_section_position", 0, false, "A menu click returns to the last position visited inside that section"),
778
830
  (0, _yuneta_gobj_js.SDATA)(_yuneta_gobj_js.data_type_t.DTP_BOOLEAN, "use_hash", 0, true, "Bind navigation to window.location.hash"),
779
831
  (0, _yuneta_gobj_js.SDATA)(_yuneta_gobj_js.data_type_t.DTP_POINTER, "mount_element", 0, null, "HTMLElement to mount shell into (default: document.body)"),
780
832
  (0, _yuneta_gobj_js.SDATA)(_yuneta_gobj_js.data_type_t.DTP_POINTER, "$container", 0, null, "Root HTMLElement of the shell"),
@@ -786,6 +838,7 @@ var PRIVATE_DATA$17 = {
786
838
  stages: {},
787
839
  navs: [],
788
840
  item_index: {},
841
+ section_pos: {},
789
842
  sub_routes: {},
790
843
  event_handlers: {},
791
844
  hash_handler: null,
@@ -833,6 +886,8 @@ function mt_start$14(gobj) {
833
886
  ]));
834
887
  return;
835
888
  }
889
+ let shell_cfg = config.shell || {};
890
+ if (typeof shell_cfg.remember_section_position === "boolean") (0, _yuneta_gobj_js.gobj_write_attr)(gobj, "remember_section_position", shell_cfg.remember_section_position);
836
891
  build_item_index(gobj, config);
837
892
  instantiate_menus(gobj, config);
838
893
  build_toolbar(gobj, config);
@@ -1192,6 +1247,59 @@ function build_item_index(gobj, config) {
1192
1247
  menu_id: prev && prev.menu_id || ""
1193
1248
  };
1194
1249
  }
1250
+ index_node_tree(gobj, config);
1251
+ }
1252
+ /************************************************************
1253
+ * config.shell.tree — the navigation is a TREE OF NODES and
1254
+ * its root is where the shell's own root used to be.
1255
+ *
1256
+ * The shell keeps what it is good at, the SPACE: zones, layers,
1257
+ * stages, toolbar, overlays, theme, breakpoints. It stops being
1258
+ * the owner of the menu: the root C_YUI_NODE holds the primary
1259
+ * options as its children and projects them into zones (a left
1260
+ * rail, a bottom bar) exactly as `menu.primary.render` used to
1261
+ * say — a per-zone projection is still a projection.
1262
+ *
1263
+ * All this does is synthesize ONE route entry. `owns_subtree`
1264
+ * lets it match as the ancestor of everything (route_resolver),
1265
+ * so the whole url space below it is the tree's `subpath` and no
1266
+ * route table grows. The unknown-route diagnostic is not lost:
1267
+ * it moves to the node that knows the names of its children.
1268
+ *
1269
+ * `menu` and `tree` are not exclusive — an app may declare both
1270
+ * while it migrates — but a tree at "/" owns every route no
1271
+ * other entry claims, which is the point.
1272
+ ************************************************************/
1273
+ function index_node_tree(gobj, config) {
1274
+ let priv = gobj.priv;
1275
+ let tree = config.shell && config.shell.tree || null;
1276
+ if (!tree) return;
1277
+ if (!(0, _yuneta_gobj_js.is_object)(tree)) {
1278
+ (0, _yuneta_gobj_js.log_error)("C_YUI_SHELL: config.shell.tree must be an object");
1279
+ return;
1280
+ }
1281
+ let base_route = normalize_route(tree.base_route || "/");
1282
+ let kw = {};
1283
+ for (let key of Object.keys(tree)) {
1284
+ if (key.charAt(0) === "_" || key === "stage") continue;
1285
+ kw[key] = tree[key];
1286
+ }
1287
+ kw.base_route = base_route;
1288
+ if (!kw.node_id) kw.node_id = "root";
1289
+ let prev = priv.item_index[base_route];
1290
+ priv.item_index[base_route] = {
1291
+ item: prev && prev.item || null,
1292
+ parent_item: prev && prev.parent_item || null,
1293
+ stage: tree.stage || "main",
1294
+ menu_id: prev && prev.menu_id || "",
1295
+ target: {
1296
+ stage: tree.stage || "main",
1297
+ gclass: "C_YUI_NODE",
1298
+ lifecycle: "keep_alive",
1299
+ owns_subtree: true,
1300
+ kw
1301
+ }
1302
+ };
1195
1303
  }
1196
1304
  /************************************************************
1197
1305
  * For each menu declared: for each zone hosting it, create
@@ -1436,6 +1544,7 @@ function navigate_to(gobj, route, depth, no_drain) {
1436
1544
  if ($c) $c.classList.remove("is-hidden");
1437
1545
  stage.active_route = matched_route;
1438
1546
  (0, _yuneta_gobj_js.gobj_write_attr)(gobj, "current_route", route);
1547
+ remember_position(gobj, route);
1439
1548
  update_secondary_nav_visibility(gobj, entry);
1440
1549
  close_all_drawers(gobj);
1441
1550
  close_toolbar_dropdown(gobj);
@@ -2060,7 +2169,11 @@ function pop_escape(gobj, handler) {
2060
2169
  * - navigate_to() drains (closes) every registered overlay when the
2061
2170
  * RESTING route changes — overlays are transient and do not
2062
2171
  * outlive the view they float above. An action route or a
2063
- * subpath-only move keeps them open.
2172
+ * subpath-only move keeps them open. An overlay registered with
2173
+ * `keep_on_navigate` opts OUT of the drain: it is a NAVIGATION
2174
+ * PANEL, not a thing floating above one view — the site map is
2175
+ * the reference case, where every row click is meant to move the
2176
+ * user with the panel still up.
2064
2177
  * - a drained/buried entry is left INERT: overlay_dismissed only
2065
2178
  * history.back()s when the marker is the CURRENT history entry
2066
2179
  * (adjacent, so the back() is invisible) — never over real route
@@ -2073,14 +2186,20 @@ function pop_escape(gobj, handler) {
2073
2186
  function drain_overlays(gobj) {
2074
2187
  let priv = gobj.priv;
2075
2188
  if (!priv || !priv.overlay_stack) return;
2189
+ let kept = [];
2076
2190
  while (priv.overlay_stack.length > 0) {
2077
2191
  let entry = priv.overlay_stack.pop();
2192
+ if (entry.keep_on_navigate) {
2193
+ kept.unshift(entry);
2194
+ continue;
2195
+ }
2078
2196
  try {
2079
2197
  entry.close();
2080
2198
  } catch (e) {
2081
2199
  (0, _yuneta_gobj_js.log_warning)(`C_YUI_SHELL: overlay close on navigation failed: ${e}`);
2082
2200
  }
2083
2201
  }
2202
+ for (let entry of kept) priv.overlay_stack.push(entry);
2084
2203
  }
2085
2204
  function retag_overlay_hash(gobj, st, hash) {
2086
2205
  let priv = gobj.priv;
@@ -2088,7 +2207,7 @@ function retag_overlay_hash(gobj, st, hash) {
2088
2207
  let entry = priv.overlay_stack.find((e) => e.id === st.__yui_overlay__);
2089
2208
  if (entry) entry.hash = hash;
2090
2209
  }
2091
- function push_overlay_history(gobj, close) {
2210
+ function push_overlay_history(gobj, close, opts) {
2092
2211
  let priv = gobj.priv;
2093
2212
  if (!priv || !priv.overlay_stack || !(0, _yuneta_gobj_js.gobj_read_attr)(gobj, "use_hash")) return null;
2094
2213
  let entry = {
@@ -2219,6 +2338,7 @@ function update_secondary_nav_visibility(gobj, entry) {
2219
2338
  function ac_nav_clicked(gobj, event, kw, src) {
2220
2339
  let route = kw && kw.route || "";
2221
2340
  if ((0, _yuneta_gobj_js.empty_string)(route)) return 0;
2341
+ route = remembered_position(gobj, route);
2222
2342
  if ((0, _yuneta_gobj_js.gobj_read_attr)(gobj, "use_hash") && !route_restores_url(gobj, route)) {
2223
2343
  let target_hash = route_to_hash(route);
2224
2344
  if (window.location.hash !== target_hash) window.location.hash = target_hash;
@@ -2302,6 +2422,48 @@ function register_c_yui_shell() {
2302
2422
  * bottom of the file to keep the skeleton layout intact.
2303
2423
  ***************************************************************/
2304
2424
  /************************************************************
2425
+ * Last position inside each section — the routes a MENU CLICK
2426
+ * returns to (`remember_section_position`).
2427
+ *
2428
+ * A section is a menu item's route; anything under it is a
2429
+ * position inside it. The memory MIRRORS the url and is never
2430
+ * the authority for it (ROUTING.md §3): it lives for the page,
2431
+ * not on disk, and only a click on the section's own menu item
2432
+ * consults it. Typing the url, a deep link, Back/Forward and
2433
+ * programmatic navigation all land exactly where they say —
2434
+ * which is the property that would be lost if this were stored
2435
+ * and applied everywhere.
2436
+ ************************************************************/
2437
+ function remember_position(gobj, route) {
2438
+ let priv = gobj.priv;
2439
+ if (!(0, _yuneta_gobj_js.gobj_read_attr)(gobj, "remember_section_position")) return;
2440
+ for (let section of Object.keys(priv.item_index)) {
2441
+ if (section === "/") continue;
2442
+ if (route === section || route.indexOf(section + "/") === 0) priv.section_pos[section] = route;
2443
+ }
2444
+ }
2445
+ /************************************************************
2446
+ * Where a click on `route` should actually land: the last
2447
+ * position inside it, when there is one and it still resolves.
2448
+ * A remembered route whose node has since disappeared is
2449
+ * dropped rather than followed — the tree is allowed to change
2450
+ * under a memory.
2451
+ ************************************************************/
2452
+ function remembered_position(gobj, route) {
2453
+ let priv = gobj.priv;
2454
+ if (!(0, _yuneta_gobj_js.gobj_read_attr)(gobj, "remember_section_position")) return route;
2455
+ let current = (0, _yuneta_gobj_js.gobj_read_attr)(gobj, "current_route") || "";
2456
+ if (current === route || current.indexOf(route + "/") === 0) return route;
2457
+ let last = priv.section_pos[route];
2458
+ if (!last || last === route) return route;
2459
+ let r = resolve_route(priv.item_index, last);
2460
+ if (!r || !r.entry || !r.entry.target) {
2461
+ delete priv.section_pos[route];
2462
+ return route;
2463
+ }
2464
+ return last;
2465
+ }
2466
+ /************************************************************
2305
2467
  * Resolve the shell that governs `gobj`: the nearest
2306
2468
  * C_YUI_SHELL ancestor, else the last shell created on the
2307
2469
  * page (apps have exactly one). Null when no shell exists —
@@ -2403,8 +2565,8 @@ function yui_shell_push_escape(shell_gobj, layer, handler) {
2403
2565
  function yui_shell_pop_escape(shell_gobj, handler) {
2404
2566
  pop_escape(shell_gobj, handler);
2405
2567
  }
2406
- function yui_shell_register_overlay(shell_gobj, close_fn) {
2407
- return push_overlay_history(shell_gobj, close_fn);
2568
+ function yui_shell_register_overlay(shell_gobj, close_fn, opts) {
2569
+ return push_overlay_history(shell_gobj, close_fn, opts);
2408
2570
  }
2409
2571
  function yui_shell_overlay_dismissed(shell_gobj, overlay) {
2410
2572
  overlay_dismissed(shell_gobj, overlay);
@@ -3008,6 +3170,7 @@ var attrs_table$16 = [
3008
3170
  (0, _yuneta_gobj_js.SDATA)(_yuneta_gobj_js.data_type_t.DTP_BOOLEAN, "modal", 0, false, "Enable modal mode"),
3009
3171
  (0, _yuneta_gobj_js.SDATA)(_yuneta_gobj_js.data_type_t.DTP_BOOLEAN, "keyboard", 0, true, "Close window on ESC if not modal"),
3010
3172
  (0, _yuneta_gobj_js.SDATA)(_yuneta_gobj_js.data_type_t.DTP_BOOLEAN, "back_dismissable", 0, true, "Browser Back closes this window (floating overlays only; ignored when it has a `manager`)"),
3173
+ (0, _yuneta_gobj_js.SDATA)(_yuneta_gobj_js.data_type_t.DTP_BOOLEAN, "keep_on_navigate", 0, false, "This window is a navigation PANEL: a route change does not close it"),
3011
3174
  (0, _yuneta_gobj_js.SDATA)(_yuneta_gobj_js.data_type_t.DTP_POINTER, "back_overlay", 0, null, "Internal: overlay-history entry (Back-button integration)"),
3012
3175
  (0, _yuneta_gobj_js.SDATA)(_yuneta_gobj_js.data_type_t.DTP_POINTER, "$container", 0, null, "Internal: Window container element"),
3013
3176
  (0, _yuneta_gobj_js.SDATA)(_yuneta_gobj_js.data_type_t.DTP_STRING, "window_id", 0, "", "Internal: Window ID"),
@@ -3047,7 +3210,7 @@ function mt_create$16(gobj) {
3047
3210
  let shell = yui_shell_of(gobj);
3048
3211
  if (shell) (0, _yuneta_gobj_js.gobj_write_attr)(gobj, "back_overlay", yui_shell_register_overlay(shell, function() {
3049
3212
  close_window(gobj);
3050
- }));
3213
+ }, { keep_on_navigate: (0, _yuneta_gobj_js.gobj_read_bool_attr)(gobj, "keep_on_navigate") }));
3051
3214
  }
3052
3215
  let on_win_resize = function() {
3053
3216
  handleResize(gobj);
@@ -10493,10 +10656,19 @@ function leaf_row(key, value, depth) {
10493
10656
  ];
10494
10657
  }
10495
10658
  /************************************************************
10496
- * Depth indentation (inline style keeps it CSS-framework free)
10659
+ * Depth indentation (inline style keeps it CSS-framework free).
10660
+ *
10661
+ * FOUR characters per level, house rule: structure is read as
10662
+ * indentation, and it has to be the same four everywhere — this
10663
+ * viewer, the raw dump behind it, the site map's tree. `ch` is
10664
+ * the character width of the row's own font, so the rows line up
10665
+ * with the monospace text they carry instead of drifting from it
10666
+ * at some zoom level.
10497
10667
  ************************************************************/
10668
+ var INDENT_CH = 4;
10669
+ var INDENT_PAD = .4;
10498
10670
  function row_indent(depth) {
10499
- return "padding-left:" + (.4 + depth * 1.1) + "em;";
10671
+ return "padding-left:" + (INDENT_PAD + depth * INDENT_CH) + "ch;background-size:" + depth * INDENT_CH + "ch 100%;";
10500
10672
  }
10501
10673
  /************************************************************
10502
10674
  * String() that never throws on a non-primitive
@@ -10636,7 +10808,7 @@ function ac_collapse_all$1(gobj, event, kw, src) {
10636
10808
  function ac_copy_all(gobj, event, kw, src) {
10637
10809
  let priv = gobj.priv;
10638
10810
  if (priv.root === null || priv.root === void 0) return 0;
10639
- let text = JSON.stringify(priv.root, null, 2);
10811
+ let text = JSON.stringify(priv.root, null, 4);
10640
10812
  if (navigator.clipboard && navigator.clipboard.writeText) navigator.clipboard.writeText(text).catch(function(e) {
10641
10813
  (0, _yuneta_gobj_js.log_error)(`${GCLASS_NAME$8}: clipboard write failed: ${e}`);
10642
10814
  });
@@ -12493,6 +12665,13 @@ function cards_grid_descriptor(items, show_label) {
12493
12665
  * "cards" — grid of tappable cards (section-index landing;
12494
12666
  * mounted as a stage view by C_YUI_SHELL when a
12495
12667
  * primary item declares submenu.index)
12668
+ * "breadcrumb" — the PATH as one line ("Cards › Energy › South
12669
+ * hall › Meter 3"), each crumb a link to that
12670
+ * level. Unlike every other layout its items
12671
+ * are not a node's children but the trail down
12672
+ * to where the user is: the second way to show
12673
+ * depth, for when stacking one strip per level
12674
+ * costs more vertical room than it is worth.
12496
12675
  * "backbar" — single "← <section>" link back to the section
12497
12676
  * index; the mobile replacement of the tab strip
12498
12677
  * for submenu.index sections (the shell collapses
@@ -12540,7 +12719,8 @@ var SUPPORTED_LAYOUTS = [
12540
12719
  "submenu",
12541
12720
  "accordion",
12542
12721
  "cards",
12543
- "backbar"
12722
+ "backbar",
12723
+ "breadcrumb"
12544
12724
  ];
12545
12725
  function is_decorative(it) {
12546
12726
  return !!(it && (it.type === "header" || it.type === "divider"));
@@ -12646,6 +12826,9 @@ function build_ui$6(gobj) {
12646
12826
  case "backbar":
12647
12827
  $container = render_backbar(gobj);
12648
12828
  break;
12829
+ case "breadcrumb":
12830
+ $container = render_breadcrumb(gobj, items);
12831
+ break;
12649
12832
  default: $container = render_vertical(gobj, items);
12650
12833
  }
12651
12834
  $container.setAttribute("data-nav-zone", zone);
@@ -12666,6 +12849,7 @@ function build_ui$6(gobj) {
12666
12849
  }
12667
12850
  wire_clicks(gobj, $container);
12668
12851
  (0, _yuneta_gobj_js.gobj_write_attr)(gobj, "$container", $container);
12852
+ apply_active_route(gobj);
12669
12853
  }
12670
12854
  /************************************************************
12671
12855
  * Rebuild the nav DOM in place from the current menu_items.
@@ -12684,7 +12868,6 @@ function rebuild(gobj) {
12684
12868
  if ($new && was_hidden) $new.classList.add("is-hidden");
12685
12869
  if (parent && $new) parent.insertBefore($new, next);
12686
12870
  if ($old && $old.parentNode) $old.parentNode.removeChild($old);
12687
- apply_active_route(gobj);
12688
12871
  }
12689
12872
  /************************************************************
12690
12873
  * Re-apply the active-route highlight after a rebuild (the
@@ -12838,6 +13021,74 @@ function render_cards(gobj, items) {
12838
13021
  * collapses the whole secondary zone (tabs and cards never
12839
13022
  * coexist), so the bar needs no self-hiding logic.
12840
13023
  ************************************************************/
13024
+ /************************************************************
13025
+ * breadcrumb: the trail to here, one line.
13026
+ *
13027
+ * Bulma's own markup (nav.breadcrumb > ul > li, separators drawn by
13028
+ * CSS), with the SAME data-route/data-item-id contract as every
13029
+ * other layout — so the delegated click handler and the active
13030
+ * highlight work unchanged, and the last crumb marks itself through
13031
+ * `active_route` like a tab does.
13032
+ ************************************************************/
13033
+ function render_breadcrumb(gobj, items) {
13034
+ let show_label = (0, _yuneta_gobj_js.gobj_read_attr)(gobj, "show_label");
13035
+ let lis = [];
13036
+ for (let it of items || []) {
13037
+ if (is_decorative(it)) continue;
13038
+ let children = [];
13039
+ if (it.icon) children.push([
13040
+ "span",
13041
+ { class: "icon is-small" },
13042
+ ["i", {
13043
+ class: it.icon,
13044
+ "aria-hidden": "true"
13045
+ }]
13046
+ ]);
13047
+ if (show_label !== false) {
13048
+ let label = it.name || it.id || "";
13049
+ children.push([
13050
+ "span",
13051
+ {
13052
+ class: "yui-nav-label",
13053
+ i18n: label
13054
+ },
13055
+ label
13056
+ ]);
13057
+ }
13058
+ let a_attrs = {
13059
+ class: "yui-nav-item yui-nav-crumb",
13060
+ href: it.route ? "#" + it.route : "#",
13061
+ "data-item-id": it.id,
13062
+ "data-route": it.route || "",
13063
+ "data-disabled": it.disabled ? "1" : "0"
13064
+ };
13065
+ if (it.name) {
13066
+ a_attrs["aria-label"] = it.name;
13067
+ a_attrs["data-i18n-aria-label"] = it.name;
13068
+ }
13069
+ lis.push([
13070
+ "li",
13071
+ {},
13072
+ [[
13073
+ "a",
13074
+ a_attrs,
13075
+ children
13076
+ ]]
13077
+ ]);
13078
+ }
13079
+ return (0, _yuneta_gobj_js.createElement2)([
13080
+ "nav",
13081
+ {
13082
+ class: "breadcrumb is-small",
13083
+ "aria-label": "breadcrumbs"
13084
+ },
13085
+ [[
13086
+ "ul",
13087
+ {},
13088
+ lis
13089
+ ]]
13090
+ ]);
13091
+ }
12841
13092
  function render_backbar(gobj) {
12842
13093
  let back_route = (0, _yuneta_gobj_js.gobj_read_attr)(gobj, "back_route") || "";
12843
13094
  let label = (0, _yuneta_gobj_js.gobj_read_attr)(gobj, "nav_label") || "";
@@ -13243,6 +13494,7 @@ function register_c_yui_nav() {
13243
13494
  //#endregion
13244
13495
  //#region src/shell_route_map.js
13245
13496
  var WIN_NAME$1 = "shell-route-map-window";
13497
+ var __show_refs__ = true;
13246
13498
  var __open_modal__ = null;
13247
13499
  function scroll_to_current($body) {
13248
13500
  let $cur = $body.querySelector(".ROUTEMAP_CURRENT");
@@ -13288,6 +13540,16 @@ function render_node(node) {
13288
13540
  { class: "ROUTEMAP_EVENT" },
13289
13541
  node.event
13290
13542
  ]);
13543
+ if (node.ref) row.push([
13544
+ "span",
13545
+ {
13546
+ class: "ROUTEMAP_REF",
13547
+ i18n: "shown above",
13548
+ title: "shown above",
13549
+ "data-i18n-title": "shown above"
13550
+ },
13551
+ "shown above"
13552
+ ]);
13291
13553
  if (node.current) row.push([
13292
13554
  "span",
13293
13555
  {
@@ -13296,7 +13558,7 @@ function render_node(node) {
13296
13558
  },
13297
13559
  "you are here"
13298
13560
  ]);
13299
- let row_class = node.current ? " ROUTEMAP_CURRENT" : "";
13561
+ let row_class = (node.current ? " ROUTEMAP_CURRENT" : "") + (node.ref ? " ROUTEMAP_IS_REF" : "");
13300
13562
  let $row;
13301
13563
  if (node.route) $row = (0, _yuneta_gobj_js.createElement2)([
13302
13564
  "a",
@@ -13329,21 +13591,37 @@ function render_node(node) {
13329
13591
  * descendant matches (keep the ancestor path). Empty `q` shows all.
13330
13592
  * Matches against the visible row text (name + route + event), so it
13331
13593
  * honours the current translation. Returns whether the <li> is shown.
13594
+ *
13595
+ * `show_refs` off hides the reference rows — the second and third
13596
+ * place a route is reachable from, which carry no structure. They are
13597
+ * hidden by DEFAULT: in an app with a drawer and an account menu they
13598
+ * are a third of the map, and what a reader comes here for is the
13599
+ * structure. A ref stays hidden even inside a matched subtree — an
13600
+ * ancestor match must not smuggle it back in — and a group left with
13601
+ * nothing but refs collapses with them, since a group is only ever
13602
+ * shown by what it contains.
13332
13603
  ***************************************************************/
13333
- function filter_li($li, q, ancestor_match) {
13604
+ function filter_li($li, q, ancestor_match, show_refs) {
13334
13605
  let $row = $li.querySelector(":scope > .ROUTEMAP_ROW");
13335
13606
  let text = $row ? ($row.textContent || "").toLowerCase() : "";
13607
+ let hidden_ref = !show_refs && !!($row && $row.classList.contains("ROUTEMAP_IS_REF"));
13336
13608
  let self_match = q === "" || text.indexOf(q) >= 0;
13337
- let show_all = ancestor_match || self_match;
13609
+ let show_all = (ancestor_match || self_match) && !hidden_ref;
13338
13610
  let any_child = false;
13339
13611
  let $ul = $li.querySelector(":scope > ul");
13340
13612
  if ($ul) {
13341
13613
  let kids = $ul.children;
13342
- for (let i = 0; i < kids.length; i++) if (filter_li(kids[i], q, show_all)) any_child = true;
13614
+ for (let i = 0; i < kids.length; i++) if (filter_li(kids[i], q, show_all, show_refs)) any_child = true;
13615
+ }
13616
+ let is_group = !!($row && $row.classList.contains("ROUTEMAP_STRUCT"));
13617
+ let had_children = !!($ul && $ul.children.length);
13618
+ if (is_group && had_children && !any_child && !show_refs) {
13619
+ let kids = $ul.children;
13620
+ for (let i = 0; i < kids.length; i++) if (filter_li(kids[i], q, show_all, true)) any_child = true;
13343
13621
  }
13344
- let visible = show_all || any_child;
13622
+ let visible = is_group && had_children ? any_child || q !== "" && self_match : (show_all || any_child) && !hidden_ref;
13345
13623
  $li.style.display = visible ? "" : "none";
13346
- if ($row) $row.classList.toggle("ROUTEMAP_MATCH", q !== "" && self_match);
13624
+ if ($row) $row.classList.toggle("ROUTEMAP_MATCH", q !== "" && self_match && visible);
13347
13625
  return visible;
13348
13626
  }
13349
13627
  /***************************************************************
@@ -13353,16 +13631,17 @@ function filter_li($li, q, ancestor_match) {
13353
13631
  * on a resting-route change. `on_jump()` closes the host for the
13354
13632
  * one click that navigates nowhere (the current route).
13355
13633
  ***************************************************************/
13356
- function build_body(shell, t, on_jump) {
13634
+ function build_body(shell, t) {
13357
13635
  let map = yui_shell_nav_map(shell);
13358
- let children = [{
13636
+ let children = [].concat(map.nav);
13637
+ if (Array.isArray(map.toolbar) && map.toolbar.length) children.push({
13359
13638
  label: "toolbar",
13360
13639
  icon: "",
13361
13640
  route: "",
13362
13641
  event: "",
13363
13642
  kind: "group",
13364
13643
  children: map.toolbar
13365
- }].concat(map.nav);
13644
+ });
13366
13645
  if (Array.isArray(map.other) && map.other.length) children.push({
13367
13646
  label: "other routes",
13368
13647
  icon: "",
@@ -13415,21 +13694,53 @@ function build_body(shell, t, on_jump) {
13415
13694
  "matches"
13416
13695
  ]]
13417
13696
  ]);
13697
+ let $refs_input = (0, _yuneta_gobj_js.createElement2)(["input", {
13698
+ class: "ROUTEMAP_REFS_INPUT",
13699
+ type: "checkbox"
13700
+ }]);
13701
+ if (__show_refs__) $refs_input.checked = true;
13702
+ let $refs_toggle = (0, _yuneta_gobj_js.createElement2)([
13703
+ "label",
13704
+ {
13705
+ class: "checkbox is-size-7 ROUTEMAP_REFS_TOGGLE",
13706
+ title: "show references",
13707
+ "data-i18n-title": "show references"
13708
+ },
13709
+ [$refs_input, [
13710
+ "span",
13711
+ {
13712
+ class: "ml-1",
13713
+ i18n: "show references"
13714
+ },
13715
+ "show references"
13716
+ ]]
13717
+ ]);
13418
13718
  let $search_row = (0, _yuneta_gobj_js.createElement2)([
13419
13719
  "div",
13420
13720
  { class: "ROUTEMAP_SEARCH_ROW mb-2" },
13421
- [$search_ctrl, $count]
13721
+ [
13722
+ $search_ctrl,
13723
+ $count,
13724
+ $refs_toggle
13725
+ ]
13422
13726
  ]);
13423
- $search.addEventListener("input", function() {
13727
+ let apply_filter = function() {
13424
13728
  let q = ($search.value || "").trim().toLowerCase();
13425
13729
  let $root_li = $tree.querySelector(".ROUTEMAP_ROOT > li");
13426
- if ($root_li) filter_li($root_li, q, false);
13730
+ if ($root_li) filter_li($root_li, q, false, __show_refs__);
13427
13731
  if (q === "") $count.classList.add("is-hidden");
13428
13732
  else {
13429
13733
  $count_n.textContent = $tree.querySelectorAll(".ROUTEMAP_MATCH").length;
13430
13734
  $count.classList.remove("is-hidden");
13431
13735
  }
13736
+ };
13737
+ $search.addEventListener("input", apply_filter);
13738
+ $refs_input.addEventListener("change", function() {
13739
+ __show_refs__ = !!$refs_input.checked;
13740
+ apply_filter();
13432
13741
  });
13742
+ if ($tree.querySelectorAll(".ROUTEMAP_IS_REF").length === 0) $refs_toggle.classList.add("is-hidden");
13743
+ apply_filter();
13433
13744
  let $body = (0, _yuneta_gobj_js.createElement2)([
13434
13745
  "div",
13435
13746
  { class: "C_YUI_SHELL_ROUTEMAP ROUTEMAP_BODY" },
@@ -13472,19 +13783,55 @@ function build_body(shell, t, on_jump) {
13472
13783
  if ($area.parentNode) $area.parentNode.removeChild($area);
13473
13784
  }
13474
13785
  });
13475
- $body.addEventListener("click", function(ev) {
13476
- let $link = ev.target && ev.target.closest && ev.target.closest(".ROUTEMAP_LINK");
13477
- if (!$link) return;
13478
- let href = $link.getAttribute("href");
13479
- if (href && typeof window !== "undefined" && window.location.hash === href && typeof on_jump === "function") {
13480
- ev.preventDefault();
13481
- on_jump();
13786
+ let on_hash = function() {
13787
+ if (typeof document === "undefined" || !$body.isConnected) {
13788
+ if (typeof window !== "undefined") window.removeEventListener("hashchange", on_hash);
13789
+ return;
13482
13790
  }
13483
- });
13791
+ mark_current_row($body, window.location.hash, t);
13792
+ };
13793
+ if (typeof window !== "undefined") window.addEventListener("hashchange", on_hash);
13484
13794
  (0, _yuneta_gobj_js.refresh_language)($body, t);
13485
13795
  return $body;
13486
13796
  }
13487
13797
  /***************************************************************
13798
+ * Move the "you are here" mark to the row that best matches
13799
+ * `hash`: an exact route wins, else the LONGEST declared route
13800
+ * that is a path-prefix of it (the base view of a deep subpath
13801
+ * position). Same rule as the model's mark_current(), applied
13802
+ * to the rendered rows so the open panel stays truthful.
13803
+ ***************************************************************/
13804
+ function mark_current_row($body, hash, t) {
13805
+ let route = String(hash || "").replace(/^#/, "");
13806
+ let $best = null;
13807
+ let best_len = -1;
13808
+ for (let $a of $body.querySelectorAll(".ROUTEMAP_LINK")) {
13809
+ let r = String($a.getAttribute("href") || "").replace(/^#/, "");
13810
+ if (!r) continue;
13811
+ if (r === route) {
13812
+ $best = $a;
13813
+ break;
13814
+ }
13815
+ if (route.indexOf(r + "/") === 0 && r.length > best_len) {
13816
+ $best = $a;
13817
+ best_len = r.length;
13818
+ }
13819
+ }
13820
+ for (let $marked of $body.querySelectorAll(".ROUTEMAP_CURRENT")) $marked.classList.remove("ROUTEMAP_CURRENT");
13821
+ if (!$best) return;
13822
+ $best.classList.add("ROUTEMAP_CURRENT");
13823
+ let $badge = $body.querySelector(".ROUTEMAP_HERE");
13824
+ if (!$badge) $badge = (0, _yuneta_gobj_js.createElement2)([
13825
+ "span",
13826
+ {
13827
+ class: "ROUTEMAP_HERE",
13828
+ i18n: "you are here"
13829
+ },
13830
+ t ? t("you are here") : "you are here"
13831
+ ]);
13832
+ $best.appendChild($badge);
13833
+ }
13834
+ /***************************************************************
13488
13835
  * Show the site map. Toggles: a second call closes the open one.
13489
13836
  ***************************************************************/
13490
13837
  function yui_shell_show_route_map(shell, opts) {
@@ -13502,9 +13849,7 @@ function yui_shell_show_route_map(shell, opts) {
13502
13849
  }
13503
13850
  if ((0, _yuneta_gobj_js.gclass_find_by_name)("C_YUI_WINDOW") !== null) {
13504
13851
  let win_ref = { gobj: null };
13505
- let $body = build_body(shell, t, function() {
13506
- if (win_ref.gobj && (0, _yuneta_gobj_js.is_gobj)(win_ref.gobj)) (0, _yuneta_gobj_js.gobj_send_event)(win_ref.gobj, "EV_CLOSE_WINDOW", {}, shell);
13507
- });
13852
+ let $body = build_body(shell, t);
13508
13853
  let $parent = yui_shell_popup_layer(shell) || typeof document !== "undefined" && document.getElementById("top-layer") || null;
13509
13854
  let win = (0, _yuneta_gobj_js.gobj_create_service)(WIN_NAME$1, "C_YUI_WINDOW", {
13510
13855
  $parent,
@@ -13521,7 +13866,8 @@ function yui_shell_show_route_map(shell, opts) {
13521
13866
  title: "site map",
13522
13867
  icon: "yi-bars",
13523
13868
  body: $body,
13524
- manager: null
13869
+ manager: (0, _yuneta_gobj_js.gobj_find_service)("__window_manager__", false) || null,
13870
+ keep_on_navigate: true
13525
13871
  }, shell);
13526
13872
  if (!win) {
13527
13873
  (0, _yuneta_gobj_js.log_error)("C_YUI_SHELL: cannot create the site-map window");
@@ -13533,9 +13879,7 @@ function yui_shell_show_route_map(shell, opts) {
13533
13879
  return win;
13534
13880
  }
13535
13881
  let modal_ref = { modal: null };
13536
- let $body = build_body(shell, t, function() {
13537
- if (modal_ref.modal && typeof modal_ref.modal.close === "function") modal_ref.modal.close();
13538
- });
13882
+ let $body = build_body(shell, t);
13539
13883
  modal_ref.modal = yui_shell_show_modal(shell, $body, {
13540
13884
  dialog: true,
13541
13885
  logical_class: "ROUTEMAP_SHEET",
@@ -52011,7 +52355,7 @@ function render_full(e) {
52011
52355
  let payload = full_sections(e.kw ? e.kw : e.jn);
52012
52356
  let text;
52013
52357
  try {
52014
- text = JSON.stringify(payload, null, 2);
52358
+ text = JSON.stringify(payload, null, 4);
52015
52359
  } catch (err) {
52016
52360
  text = String(payload);
52017
52361
  }
@@ -52274,7 +52618,7 @@ function info_log(level, msg, hora) {
52274
52618
  let text;
52275
52619
  if (lvl === "json") {
52276
52620
  try {
52277
- text = JSON.stringify(msg, null, 2);
52621
+ text = JSON.stringify(msg, null, 4);
52278
52622
  } catch (e) {
52279
52623
  text = String(msg);
52280
52624
  }
@@ -52448,7 +52792,7 @@ function traffic_to_text() {
52448
52792
  out.push(head);
52449
52793
  let payload = e.kw ? e.kw : e.jn;
52450
52794
  try {
52451
- out.push(JSON.stringify(payload, null, 2));
52795
+ out.push(JSON.stringify(payload, null, 4));
52452
52796
  } catch (err) {
52453
52797
  out.push(String(payload));
52454
52798
  }
@@ -52820,6 +53164,7 @@ function setup_dev(self, show) {
52820
53164
  title: "Developer",
52821
53165
  icon: "yi-terminal",
52822
53166
  manager: (0, _yuneta_gobj_js.gobj_find_service)("__window_manager__", false) || null,
53167
+ keep_on_navigate: true,
52823
53168
  on_close: function() {
52824
53169
  (0, _yuneta_gobj_js.kw_set_local_storage_value)("open_developer_window", 0);
52825
53170
  }