@yuneta/gobj-ui 7.23.28 → 7.23.30

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.
@@ -3518,7 +3518,7 @@ function composed_title(gobj) {
3518
3518
  * Below the mobile breakpoint the window is a full-screen
3519
3519
  * sheet (no float / drag / resize).
3520
3520
  ************************************************************/
3521
- function is_mobile$3() {
3521
+ function is_mobile$4() {
3522
3522
  return (window.innerWidth || document.documentElement.offsetWidth) <= MOBILE_MAX;
3523
3523
  }
3524
3524
  /************************************************************
@@ -3551,7 +3551,7 @@ function do_minimize(gobj) {
3551
3551
  ************************************************************/
3552
3552
  function build_ui$15(gobj) {
3553
3553
  ensure_window_style();
3554
- let mobile = is_mobile$3();
3554
+ let mobile = is_mobile$4();
3555
3555
  let header = (0, _yuneta_gobj_js.gobj_read_attr)(gobj, "header");
3556
3556
  if ((0, _yuneta_gobj_js.is_gobj)(header)) header = (0, _yuneta_gobj_js.gobj_read_attr)(header, "$container");
3557
3557
  if (!header) header = build_default_header(gobj);
@@ -3889,7 +3889,7 @@ function min$1(gobj) {
3889
3889
  * handlers moving
3890
3890
  ************************************************************/
3891
3891
  function mvStart(gobj, evt) {
3892
- if (is_mobile$3()) return;
3892
+ if (is_mobile$4()) return;
3893
3893
  let $container = (0, _yuneta_gobj_js.gobj_read_attr)(gobj, "$container");
3894
3894
  let window_rect = $container.getBoundingClientRect();
3895
3895
  let x = evt.screenX;
@@ -4048,7 +4048,7 @@ function do_center(gobj, x, y, width, height) {
4048
4048
  function handleResize(gobj) {
4049
4049
  let $container = (0, _yuneta_gobj_js.gobj_read_attr)(gobj, "$container");
4050
4050
  if (!$container) return;
4051
- if (is_mobile$3()) {
4051
+ if (is_mobile$4()) {
4052
4052
  $container.classList.add("is-mobile-sheet");
4053
4053
  let vw = window.innerWidth || document.documentElement.offsetWidth;
4054
4054
  let vh = window.innerHeight || document.documentElement.offsetHeight;
@@ -9711,6 +9711,42 @@ function yui_graph_update_anchor($container, state) {
9711
9711
  * nothing to centre right now".
9712
9712
  ************************************************************/
9713
9713
  var __centring__ = /* @__PURE__ */ new WeakMap();
9714
+ /************************************************************
9715
+ * Put `node_id` at a GIVEN point of the viewport.
9716
+ *
9717
+ * What "the graph does not jump when I fold something" is
9718
+ * made of: read where the card is, rebuild the layout, put
9719
+ * that card back where it was. Everything that did not move
9720
+ * in the layout stays put with it.
9721
+ ************************************************************/
9722
+ async function yui_graph_place_at(graph, node_id, viewport_xy) {
9723
+ if (!graph || !node_id || !viewport_xy) return false;
9724
+ try {
9725
+ if (typeof graph.getElementPosition !== "function" || typeof graph.getCanvasCenter !== "function" || typeof graph.translateTo !== "function") return false;
9726
+ let z = graph.getZoom() || 1;
9727
+ let c = graph.getCanvasCenter();
9728
+ let n = graph.getElementPosition(node_id);
9729
+ await graph.translateTo([(c[0] - n[0]) * z + (viewport_xy[0] - c[0]), (c[1] - n[1]) * z + (viewport_xy[1] - c[1])]);
9730
+ return true;
9731
+ } catch (e) {
9732
+ return false;
9733
+ }
9734
+ }
9735
+ /************************************************************
9736
+ * Where `node_id` is right now, in viewport pixels.
9737
+ *
9738
+ * The pair of the call above: read it before a rebuild, hand
9739
+ * it back after, and the node has not moved on screen even
9740
+ * though the layout under it changed.
9741
+ ************************************************************/
9742
+ function yui_graph_viewport_of(graph, node_id) {
9743
+ if (!graph || !node_id) return null;
9744
+ try {
9745
+ return graph.getViewportByCanvas(graph.getElementPosition(node_id));
9746
+ } catch (e) {
9747
+ return null;
9748
+ }
9749
+ }
9714
9750
  async function yui_graph_center_on(graph, node_id) {
9715
9751
  if (!graph || !node_id) return false;
9716
9752
  let st = __centring__.get(graph);
@@ -10581,6 +10617,7 @@ var PRIVATE_DATA$11 = {
10581
10617
  _resize_raf: 0,
10582
10618
  search: "",
10583
10619
  collapsed: null,
10620
+ reserved: null,
10584
10621
  match_count: 0,
10585
10622
  find_timer: null,
10586
10623
  fold_listener: null,
@@ -10592,7 +10629,8 @@ var PRIVATE_DATA$11 = {
10592
10629
  anchor_path: "",
10593
10630
  anchor_state: "off",
10594
10631
  last_zoom: 1,
10595
- center_posted: false
10632
+ center_posted: false,
10633
+ pending_keep: null
10596
10634
  };
10597
10635
  var __gclass__$11 = null;
10598
10636
  var __layout_registered__$1 = false;
@@ -10702,6 +10740,7 @@ function type_color(type, dark) {
10702
10740
  function mt_create$11(gobj) {
10703
10741
  let priv = gobj.priv;
10704
10742
  priv.collapsed = /* @__PURE__ */ new Set();
10743
+ priv.reserved = /* @__PURE__ */ new Map();
10705
10744
  priv.search = "";
10706
10745
  priv.canvas_id = "json-canvas-" + (0, _yuneta_gobj_js.clean_name)((0, _yuneta_gobj_js.gobj_name)(gobj));
10707
10746
  (0, _yuneta_gobj_js.gobj_write_str_attr)(gobj, "canvas_id", priv.canvas_id);
@@ -10801,7 +10840,11 @@ function build_ui$10(gobj) {
10801
10840
  evt.stopPropagation();
10802
10841
  evt.preventDefault();
10803
10842
  if (evt.type !== "click") return;
10804
- (0, _yuneta_gobj_js.gobj_send_event)(gobj, "EV_TOGGLE_FOLD", { path: $handle.getAttribute("data-fold-path") }, gobj);
10843
+ let $card = $handle.closest ? $handle.closest(".JSON_CARD") : null;
10844
+ (0, _yuneta_gobj_js.gobj_send_event)(gobj, "EV_TOGGLE_FOLD", {
10845
+ path: $handle.getAttribute("data-fold-path"),
10846
+ keep: $card ? $card.getAttribute("data-json-path") : ""
10847
+ }, gobj);
10805
10848
  };
10806
10849
  for (let type of FOLD_SWALLOWED_EVENTS) $mount.addEventListener(type, priv.fold_listener, true);
10807
10850
  } else (0, _yuneta_gobj_js.log_error)(`${(0, _yuneta_gobj_js.gobj_short_name)(gobj)}: no canvas mount, fold handles are dead`);
@@ -11082,6 +11125,8 @@ var BEHAVIORS_READING$1 = [
11082
11125
  var BEHAVIORS_PICKING$1 = ["drag-canvas", ZOOM_CANVAS$1];
11083
11126
  var CARD_HEADER_H$1 = 24;
11084
11127
  var CARD_ROW_H$1 = 22;
11128
+ var H_GAP = 60;
11129
+ var V_GAP = 50;
11085
11130
  function fold_toggle_html(path, foldable, folded, branches, colors) {
11086
11131
  if (!foldable) return FOLD_SPACER;
11087
11132
  let label = folded ? "+" + branches : "&#8722;";
@@ -11203,7 +11248,10 @@ function build_json_nodes(gobj, path, kw, nodes, edges, parent_id, parent_port)
11203
11248
  lines.push(build_cell_html(key, value, type, dark, matched, fold));
11204
11249
  }
11205
11250
  if (parent_id && is_pure_collection(kw)) {
11206
- if (priv.collapsed.has(path)) return parent_id;
11251
+ if (priv.collapsed.has(path)) {
11252
+ push_fold_phantom(gobj, path, parent_id, parent_port, nodes, edges);
11253
+ return parent_id;
11254
+ }
11207
11255
  for (let pending of pending_complex) build_json_nodes(gobj, add_segment(path, pending.key), pending.value, nodes, edges, parent_id, parent_port);
11208
11256
  return parent_id;
11209
11257
  }
@@ -11289,7 +11337,10 @@ function build_json_nodes(gobj, path, kw, nodes, edges, parent_id, parent_port)
11289
11337
  if (parent_port) edge.style.sourcePort = parent_port;
11290
11338
  edges.push(edge);
11291
11339
  }
11292
- if (folded) return group_id;
11340
+ if (folded) {
11341
+ push_fold_phantom(gobj, path, group_id, "", nodes, edges);
11342
+ return group_id;
11343
+ }
11293
11344
  for (let pending of pending_complex) build_json_nodes(gobj, add_segment(path, pending.key), pending.value, nodes, edges, group_id, pending.port);
11294
11345
  return group_id;
11295
11346
  }
@@ -11320,8 +11371,16 @@ function load_json(gobj) {
11320
11371
  });
11321
11372
  graph.render().then(() => {
11322
11373
  reanchor$1(gobj);
11323
- paint_anchor_mark(gobj);
11374
+ paint_anchor_mark$1(gobj);
11324
11375
  if (preserve_view) {
11376
+ let keep = priv.pending_keep;
11377
+ priv.pending_keep = null;
11378
+ if (keep) {
11379
+ for (let n of nodes) if (n.data && n.data.path === keep.path) {
11380
+ yui_graph_place_at(graph, n.id, [keep.x, keep.y]);
11381
+ break;
11382
+ }
11383
+ }
11325
11384
  yui_graph_update_zoom((0, _yuneta_gobj_js.gobj_read_attr)(gobj, "$container"), priv.graph);
11326
11385
  return;
11327
11386
  }
@@ -11373,6 +11432,108 @@ function update_find_result(gobj) {
11373
11432
  priv.$find_result.classList.remove("is-hidden");
11374
11433
  }
11375
11434
  /************************************************************
11435
+ * The invisible child a fold leaves behind.
11436
+ *
11437
+ * It is the whole of "nothing moves when I fold": the layout
11438
+ * is a plain tree that centres a parent over its children, so
11439
+ * the only way to keep the survivors still is to leave the
11440
+ * space where it was, in the order it was, as a CHILD. A width
11441
+ * reserved as a number would still have to be placed, and
11442
+ * appending it moves every sibling by half of it.
11443
+ *
11444
+ * A `rect` and not an `html` node: nothing is drawn, and a
11445
+ * DOM element per fold would be a card nobody can see but the
11446
+ * Inspector would show.
11447
+ ************************************************************/
11448
+ function push_fold_phantom(gobj, path, parent_id, parent_port, nodes, edges) {
11449
+ let width = gobj.priv.reserved.get(path) || 0;
11450
+ if (!(width > 0) || !parent_id) return;
11451
+ let id = gen_node_id$1(gobj);
11452
+ nodes.push({
11453
+ id,
11454
+ type: "rect",
11455
+ data: {
11456
+ path,
11457
+ phantom: true
11458
+ },
11459
+ style: {
11460
+ size: [width, 1],
11461
+ fill: "transparent",
11462
+ stroke: "transparent",
11463
+ opacity: 0,
11464
+ pointerEvents: "none"
11465
+ }
11466
+ });
11467
+ edges.push({
11468
+ source: parent_id,
11469
+ target: id,
11470
+ style: {
11471
+ opacity: 0,
11472
+ endArrow: false,
11473
+ sourcePort: parent_port || void 0
11474
+ }
11475
+ });
11476
+ }
11477
+ /************************************************************
11478
+ * The width the children a fold is about to hide are taking
11479
+ * RIGHT NOW, measured with the layout's own numbers.
11480
+ *
11481
+ * `card_path` is the card that will lose them; `under` limits
11482
+ * it to the ones hanging from one key of that card, which is
11483
+ * the case of a container that has no card of its own -- its
11484
+ * children hang from the parent, so its fold is a hole in the
11485
+ * PARENT.
11486
+ ************************************************************/
11487
+ function group_width_of(gobj, card_path, under) {
11488
+ let priv = gobj.priv;
11489
+ if (!priv.graph) return 0;
11490
+ try {
11491
+ let by_id = {};
11492
+ let card_id = "";
11493
+ for (let node of priv.graph.getNodeData()) {
11494
+ if (!node || !node.data) continue;
11495
+ by_id[node.id] = node;
11496
+ if (node.data.path === card_path) card_id = node.id;
11497
+ }
11498
+ if (!card_id) return 0;
11499
+ let prefix = under ? under + "`" : "";
11500
+ let sum = 0;
11501
+ let n = 0;
11502
+ for (let edge of priv.graph.getEdgeData()) {
11503
+ if (!edge || edge.source !== card_id) continue;
11504
+ let kid = by_id[edge.target];
11505
+ if (!kid) continue;
11506
+ if (prefix && String(kid.data.path).indexOf(prefix) !== 0) continue;
11507
+ sum += kid.data.subtree_width || 0;
11508
+ n++;
11509
+ }
11510
+ return n ? sum + H_GAP * (n - 1) : 0;
11511
+ } catch (e) {
11512
+ return 0;
11513
+ }
11514
+ }
11515
+ /************************************************************
11516
+ * The screen spot of the card at `path`, to be given back
11517
+ * after the rebuild. Null when it has no card of its own --
11518
+ * a pure collection -- and then nothing anchors and the
11519
+ * camera simply stays where it was.
11520
+ ************************************************************/
11521
+ function keep_of(gobj, path) {
11522
+ let priv = gobj.priv;
11523
+ if ((0, _yuneta_gobj_js.empty_string)(path) || !priv.graph) return null;
11524
+ try {
11525
+ for (let node of priv.graph.getNodeData()) if (node && node.data && node.data.path === path) {
11526
+ let vp = yui_graph_viewport_of(priv.graph, node.id);
11527
+ return vp ? {
11528
+ path,
11529
+ x: vp[0],
11530
+ y: vp[1]
11531
+ } : null;
11532
+ }
11533
+ } catch (e) {}
11534
+ return null;
11535
+ }
11536
+ /************************************************************
11376
11537
  * Clear and reload.
11377
11538
  * {preserve_view: true}
11378
11539
  * keep the current zoom and translate (no fitView) —
@@ -11414,8 +11575,6 @@ var JsonTreeLayout = class extends _antv_g6.BaseLayout {
11414
11575
  h
11415
11576
  };
11416
11577
  }
11417
- const H_GAP = 60;
11418
- const V_GAP = 50;
11419
11578
  let subtree_widths = {};
11420
11579
  function calc_subtree_width(node_id) {
11421
11580
  let kids = children_map[node_id] || [];
@@ -11432,6 +11591,7 @@ var JsonTreeLayout = class extends _antv_g6.BaseLayout {
11432
11591
  return subtree_widths[node_id];
11433
11592
  }
11434
11593
  for (let root of roots) calc_subtree_width(root.id);
11594
+ for (let node of nodes) if (node.data) node.data.subtree_width = subtree_widths[node.id];
11435
11595
  let positions = {};
11436
11596
  function position_node(node_id, x, y) {
11437
11597
  positions[node_id] = {
@@ -11479,9 +11639,16 @@ var JsonTreeLayout = class extends _antv_g6.BaseLayout {
11479
11639
  /************************************************************
11480
11640
  *
11481
11641
  ************************************************************/
11642
+ function priv_reset_folds(gobj) {
11643
+ gobj.priv.collapsed.clear();
11644
+ gobj.priv.reserved.clear();
11645
+ }
11482
11646
  function ac_load_data$1(gobj, event, kw, src) {
11483
11647
  if (kw.path !== void 0) (0, _yuneta_gobj_js.gobj_write_str_attr)(gobj, "path", kw.path);
11484
- if (kw.data !== void 0) (0, _yuneta_gobj_js.gobj_write_attr)(gobj, "json_data", kw.data);
11648
+ if (kw.data !== void 0) {
11649
+ (0, _yuneta_gobj_js.gobj_write_attr)(gobj, "json_data", kw.data);
11650
+ priv_reset_folds(gobj);
11651
+ }
11485
11652
  refresh_json(gobj);
11486
11653
  return 0;
11487
11654
  }
@@ -11536,8 +11703,16 @@ function ac_toggle_fold(gobj, event, kw, src) {
11536
11703
  (0, _yuneta_gobj_js.log_error)(`${(0, _yuneta_gobj_js.gobj_short_name)(gobj)}: EV_TOGGLE_FOLD with no path`);
11537
11704
  return -1;
11538
11705
  }
11539
- if (priv.collapsed.has(path)) priv.collapsed.delete(path);
11540
- else priv.collapsed.add(path);
11706
+ let card = kw && kw.keep || path;
11707
+ if (priv.collapsed.has(path)) {
11708
+ priv.collapsed.delete(path);
11709
+ priv.reserved.delete(path);
11710
+ } else {
11711
+ let w = group_width_of(gobj, card, card === path ? "" : path);
11712
+ priv.collapsed.add(path);
11713
+ if (w > 0) priv.reserved.set(path, w);
11714
+ }
11715
+ priv.pending_keep = keep_of(gobj, kw && kw.keep || path);
11541
11716
  refresh_json(gobj, { preserve_view: true });
11542
11717
  update_find_result(gobj);
11543
11718
  return 0;
@@ -11564,6 +11739,7 @@ function ac_expand_all$2(gobj, event, kw, src) {
11564
11739
  let priv = gobj.priv;
11565
11740
  if (priv.collapsed.size === 0) return 0;
11566
11741
  priv.collapsed.clear();
11742
+ priv.reserved.clear();
11567
11743
  refresh_json(gobj);
11568
11744
  update_find_result(gobj);
11569
11745
  return 0;
@@ -11580,6 +11756,7 @@ function ac_collapse_all$2(gobj, event, kw, src) {
11580
11756
  if ((0, _yuneta_gobj_js.empty_string)(path)) path = "`";
11581
11757
  collect_foldable_paths(json_data, path, paths, true);
11582
11758
  priv.collapsed = new Set(paths);
11759
+ priv.reserved.clear();
11583
11760
  refresh_json(gobj);
11584
11761
  update_find_result(gobj);
11585
11762
  return 0;
@@ -11669,7 +11846,7 @@ function set_picking_mode$1(gobj, picking) {
11669
11846
  * each time and the mark would otherwise survive only until the
11670
11847
  * next fold.
11671
11848
  ************************************************************/
11672
- function paint_anchor_mark(gobj) {
11849
+ function paint_anchor_mark$1(gobj) {
11673
11850
  let priv = gobj.priv;
11674
11851
  let $container = (0, _yuneta_gobj_js.gobj_read_attr)(gobj, "$container");
11675
11852
  if (!$container) return;
@@ -11684,7 +11861,7 @@ function paint_anchor_mark(gobj) {
11684
11861
  function ac_center_anchor$2(gobj, event, kw, src) {
11685
11862
  let priv = gobj.priv;
11686
11863
  priv.center_posted = false;
11687
- paint_anchor_mark(gobj);
11864
+ paint_anchor_mark$1(gobj);
11688
11865
  if (priv.anchor_state === "on" && priv.anchor_id) yui_graph_center_on(priv.graph, priv.anchor_id);
11689
11866
  return 0;
11690
11867
  }
@@ -11705,7 +11882,7 @@ function ac_toggle_anchor$2(gobj, event, kw, src) {
11705
11882
  priv.anchor_path = "";
11706
11883
  }
11707
11884
  yui_graph_update_anchor((0, _yuneta_gobj_js.gobj_read_attr)(gobj, "$container"), priv.anchor_state);
11708
- paint_anchor_mark(gobj);
11885
+ paint_anchor_mark$1(gobj);
11709
11886
  set_picking_mode$1(gobj, priv.anchor_state === "arming");
11710
11887
  return 0;
11711
11888
  }
@@ -13028,7 +13205,7 @@ function ac_json_item_clicked(gobj, event, kw, src) {
13028
13205
  /************************************************************
13029
13206
  * EV_LANGUAGE_CHANGED — re-translate chrome + re-render
13030
13207
  ************************************************************/
13031
- function ac_language_changed$3(gobj, event, kw, src) {
13208
+ function ac_language_changed$4(gobj, event, kw, src) {
13032
13209
  let priv = gobj.priv;
13033
13210
  let $container = (0, _yuneta_gobj_js.gobj_read_attr)(gobj, "$container");
13034
13211
  if ($container) {
@@ -13089,7 +13266,7 @@ function create_gclass$10(gclass_name) {
13089
13266
  ],
13090
13267
  [
13091
13268
  "EV_LANGUAGE_CHANGED",
13092
- ac_language_changed$3,
13269
+ ac_language_changed$4,
13093
13270
  null
13094
13271
  ],
13095
13272
  [
@@ -13165,7 +13342,7 @@ function create_gclass$10(gclass_name) {
13165
13342
  ],
13166
13343
  [
13167
13344
  "EV_LANGUAGE_CHANGED",
13168
- ac_language_changed$3,
13345
+ ac_language_changed$4,
13169
13346
  null
13170
13347
  ],
13171
13348
  [
@@ -13215,6 +13392,567 @@ function register_c_yui_json() {
13215
13392
  return create_gclass$10(GCLASS_NAME$10);
13216
13393
  }
13217
13394
  //#endregion
13395
+ //#region src/gobj_tree_model.js
13396
+ /***********************************************************************
13397
+ * gobj_tree_model.js
13398
+ *
13399
+ * The gobj tree of a yuno, as plain data.
13400
+ *
13401
+ * The view (C_YUI_GOBJ_TREE_JS) draws NODE DESCRIPTORS, never
13402
+ * gobjs. There are two producers of a descriptor, because the
13403
+ * view has to show the gobjs of the BACKEND too:
13404
+ *
13405
+ * - describe_js_gobj() the gobj tree of THIS browser yuno
13406
+ * - describe_backend_tree() the answer of the backend command
13407
+ * `view-gobj-tree` of another yuno
13408
+ *
13409
+ * Both answer the SAME shape, and that shape is the backend's:
13410
+ * the field names are the ones `gobj2json()` writes in
13411
+ * `kernel/c/gobj-c/src/gobj.c` (fullname, shortname, gclass,
13412
+ * name, parent, state, running, playing, service, disabled,
13413
+ * volatil, bottom_gobj, gobj_flags, commands, gobj_trace_level).
13414
+ * Anything the browser runtime cannot answer is simply absent,
13415
+ * and every reader here treats absent as "not known", never as
13416
+ * false.
13417
+ *
13418
+ * Copyright (c) 2026, ArtGins.
13419
+ * All Rights Reserved.
13420
+ ***********************************************************************/
13421
+ /************************************************************
13422
+ * The role of a node: "yuno" | "service" | "volatil" | "pure" | "child"
13423
+ ************************************************************/
13424
+ function node_role(d) {
13425
+ if (!d) return "child";
13426
+ if (d.is_yuno) return "yuno";
13427
+ if (d.service) return "service";
13428
+ if (d.volatil) return "volatil";
13429
+ if (d.pure_child) return "pure";
13430
+ return "child";
13431
+ }
13432
+ /************************************************************
13433
+ * What the node is doing: "stopped" | "running" | "playing"
13434
+ ************************************************************/
13435
+ function node_status(d) {
13436
+ if (!d || !d.running) return "stopped";
13437
+ return d.playing ? "playing" : "running";
13438
+ }
13439
+ /************************************************************
13440
+ * The i18n key of the status, which is NOT the status itself:
13441
+ * a gobj that runs and does not play is PAUSED to the reader,
13442
+ * and "running" alone never said which of the two it was.
13443
+ ************************************************************/
13444
+ function node_status_key(d) {
13445
+ switch (node_status(d)) {
13446
+ case "playing": return "playing";
13447
+ case "running": return "running (paused)";
13448
+ default: return "stopped";
13449
+ }
13450
+ }
13451
+ /******************************
13452
+ * Producers
13453
+ ******************************/
13454
+ /************************************************************
13455
+ * A descriptor of a LIVE gobj of this browser yuno, children
13456
+ * included (the whole subtree, depth-first).
13457
+ *
13458
+ * `is_root` marks the yuno itself: the JS runtime has no
13459
+ * gobj_flag_yuno test of its own.
13460
+ ************************************************************/
13461
+ function describe_js_gobj(target_gobj, is_root) {
13462
+ if (!target_gobj) return null;
13463
+ let parent_gobj = (0, _yuneta_gobj_js.gobj_parent)(target_gobj);
13464
+ let bottom = (0, _yuneta_gobj_js.gobj_bottom_gobj)(target_gobj);
13465
+ let d = {
13466
+ gclass: (0, _yuneta_gobj_js.gobj_gclass_name)(target_gobj) || "",
13467
+ name: (0, _yuneta_gobj_js.gobj_name)(target_gobj) || "",
13468
+ shortname: (0, _yuneta_gobj_js.gobj_short_name)(target_gobj) || "",
13469
+ fullname: (0, _yuneta_gobj_js.gobj_full_name)(target_gobj) || "",
13470
+ parent: parent_gobj ? (0, _yuneta_gobj_js.gobj_short_name)(parent_gobj) || "" : "",
13471
+ state: (0, _yuneta_gobj_js.gobj_current_state)(target_gobj) || "",
13472
+ running: !!(0, _yuneta_gobj_js.gobj_is_running)(target_gobj),
13473
+ playing: !!(0, _yuneta_gobj_js.gobj_is_playing)(target_gobj),
13474
+ service: !!(0, _yuneta_gobj_js.gobj_is_service)(target_gobj),
13475
+ volatil: !!(0, _yuneta_gobj_js.gobj_is_volatil)(target_gobj),
13476
+ pure_child: !!(0, _yuneta_gobj_js.gobj_is_pure_child)(target_gobj),
13477
+ disabled: !!target_gobj.disabled,
13478
+ bottom_gobj: bottom ? (0, _yuneta_gobj_js.gobj_short_name)(bottom) || "" : "",
13479
+ commands: !!(target_gobj.gclass && target_gobj.gclass.command_table),
13480
+ is_yuno: !!is_root,
13481
+ children: []
13482
+ };
13483
+ let children = target_gobj.dl_children || [];
13484
+ for (let child of children) {
13485
+ let cd = describe_js_gobj(child, false);
13486
+ if (cd) d.children.push(cd);
13487
+ }
13488
+ return d;
13489
+ }
13490
+ /******************************
13491
+ * Readouts
13492
+ ******************************/
13493
+ /************************************************************
13494
+ * The i18n key of a role. Two of the five roles are not the
13495
+ * word the reader wants ("pure" and "volatil" alone say
13496
+ * nothing), so the map is explicit.
13497
+ ************************************************************/
13498
+ function role_i18n_key(role) {
13499
+ switch (role) {
13500
+ case "yuno": return "yuno";
13501
+ case "service": return "service";
13502
+ case "volatil": return "volatil child";
13503
+ case "pure": return "pure child";
13504
+ default: return "child";
13505
+ }
13506
+ }
13507
+ /************************************************************
13508
+ * The marks a node carries besides its role: what the reader
13509
+ * would otherwise have to open the popover to learn.
13510
+ *
13511
+ * Returns i18n KEYS, in a fixed order, so the card and the
13512
+ * popover cannot disagree about what a node is.
13513
+ ************************************************************/
13514
+ function node_badge_keys(d) {
13515
+ let badges = [];
13516
+ if (!d) return badges;
13517
+ let role = node_role(d);
13518
+ if (role !== "child") badges.push(role_i18n_key(role));
13519
+ if (d.disabled) badges.push("disabled");
13520
+ if (d.bottom_gobj) badges.push("bottom");
13521
+ if (d.commands) badges.push("commands");
13522
+ return badges;
13523
+ }
13524
+ /************************************************************
13525
+ * The vocabulary of this view, translated.
13526
+ *
13527
+ * Written as literal t("...") calls and not as t(key): a key
13528
+ * reached through a variable is invisible to the locale
13529
+ * validator, which is how a fixed vocabulary ships in one
13530
+ * language and never changes it.
13531
+ ************************************************************/
13532
+ function node_labels(t) {
13533
+ if (typeof t !== "function") t = (k) => k;
13534
+ return {
13535
+ role: {
13536
+ "yuno": t("yuno"),
13537
+ "service": t("service"),
13538
+ "volatil child": t("volatil child"),
13539
+ "pure child": t("pure child"),
13540
+ "child": t("child")
13541
+ },
13542
+ status: {
13543
+ "playing": t("playing"),
13544
+ "running (paused)": t("running (paused)"),
13545
+ "stopped": t("stopped")
13546
+ },
13547
+ status_short: {
13548
+ "playing": t("playing"),
13549
+ "running": t("running"),
13550
+ "stopped": t("stopped")
13551
+ },
13552
+ badge: {
13553
+ "yuno": t("yuno"),
13554
+ "service": t("service"),
13555
+ "volatil child": t("volatil child"),
13556
+ "pure child": t("pure child"),
13557
+ "disabled": t("disabled"),
13558
+ "bottom": t("bottom"),
13559
+ "commands": t("commands")
13560
+ },
13561
+ field: {
13562
+ gclass: t("gclass"),
13563
+ name: t("name"),
13564
+ full_name: t("full name"),
13565
+ role: t("role"),
13566
+ status: t("status"),
13567
+ state: t("state"),
13568
+ disabled: t("disabled"),
13569
+ parent: t("parent"),
13570
+ bottom_gobj: t("bottom gobj"),
13571
+ commands: t("commands"),
13572
+ traces: t("traces"),
13573
+ flags: t("flags"),
13574
+ children: t("children"),
13575
+ yes: t("yes"),
13576
+ collapsed: t("(collapsed)")
13577
+ }
13578
+ };
13579
+ }
13580
+ /************************************************************
13581
+ * The rows of the popover: [label, value], both translated.
13582
+ *
13583
+ * `t` is the app's translator -- the library has none of its
13584
+ * own. Values that are a fixed vocabulary (the role, the
13585
+ * status, yes) go through it too; free text does not.
13586
+ ************************************************************/
13587
+ function node_info_rows(d, t) {
13588
+ let rows = [];
13589
+ if (!d) return rows;
13590
+ let L = node_labels(t);
13591
+ let f = L.field;
13592
+ rows.push([f.gclass, d.gclass || "—"]);
13593
+ rows.push([f.name, d.name || "—"]);
13594
+ if (d.fullname && d.fullname !== d.shortname) rows.push([f.full_name, d.fullname]);
13595
+ rows.push([f.role, L.role[role_i18n_key(node_role(d))]]);
13596
+ rows.push([f.status, L.status[node_status_key(d)]]);
13597
+ rows.push([f.state, d.state || "—"]);
13598
+ if (d.disabled) rows.push([f.disabled, f.yes]);
13599
+ if (d.parent) rows.push([f.parent, d.parent]);
13600
+ if (d.bottom_gobj) rows.push([f.bottom_gobj, d.bottom_gobj]);
13601
+ if (d.commands) rows.push([f.commands, f.yes]);
13602
+ if (Array.isArray(d.gobj_trace_level) && d.gobj_trace_level.length > 0) rows.push([f.traces, d.gobj_trace_level.join(", ")]);
13603
+ if (Array.isArray(d.gobj_flags) && d.gobj_flags.length > 0) rows.push([f.flags, d.gobj_flags.join(", ")]);
13604
+ let kids = Array.isArray(d.children) ? d.children.length : 0;
13605
+ if (kids > 0) rows.push([f.children, d.is_collapsed ? kids + " " + f.collapsed : String(kids)]);
13606
+ return rows;
13607
+ }
13608
+ //#endregion
13609
+ //#region src/gclass_describe.js
13610
+ /***********************************************************************
13611
+ * gclass_describe.js
13612
+ *
13613
+ * A gclass, as plain JSON, in the shape the BACKEND answers.
13614
+ *
13615
+ * There is no gclass viewer in the framework: the C kernel
13616
+ * answers `view-gclass` with `gclass2json()`
13617
+ * (`kernel/c/gobj-c/src/gobj.c`) and nothing draws it. This
13618
+ * module builds the same document for a gclass of the BROWSER
13619
+ * yuno, out of the gobj-js registry, so one viewer can show a
13620
+ * gclass of either side:
13621
+ *
13622
+ * {id, gcflag, attrs, commands, gclass_methods,
13623
+ * internal_methods, FSM: {events, states}}
13624
+ *
13625
+ * Two keys the backend does not carry are added at the end,
13626
+ * because the browser can answer them for free and the reader
13627
+ * asks them first: `instances` and `trace_levels`.
13628
+ *
13629
+ * Copyright (c) 2026, ArtGins.
13630
+ * All Rights Reserved.
13631
+ ***********************************************************************/
13632
+ /***************************************************************
13633
+ * Bit tables
13634
+ ***************************************************************/
13635
+ var DATA_TYPE_NAMES = {
13636
+ [_yuneta_gobj_js.data_type_t.DTP_STRING]: "DTP_STRING",
13637
+ [_yuneta_gobj_js.data_type_t.DTP_BOOLEAN]: "DTP_BOOLEAN",
13638
+ [_yuneta_gobj_js.data_type_t.DTP_INTEGER]: "DTP_INTEGER",
13639
+ [_yuneta_gobj_js.data_type_t.DTP_REAL]: "DTP_REAL",
13640
+ [_yuneta_gobj_js.data_type_t.DTP_LIST]: "DTP_LIST",
13641
+ [_yuneta_gobj_js.data_type_t.DTP_DICT]: "DTP_DICT",
13642
+ [_yuneta_gobj_js.data_type_t.DTP_JSON]: "DTP_JSON",
13643
+ [_yuneta_gobj_js.data_type_t.DTP_POINTER]: "DTP_POINTER"
13644
+ };
13645
+ /************************************************************
13646
+ * The names of the bits set in `bits`, in table order.
13647
+ ************************************************************/
13648
+ function bits2names(table, bits) {
13649
+ let names = [];
13650
+ let value = Number(bits) || 0;
13651
+ for (let name of Object.keys(table)) if ((value & table[name]) !== 0) names.push(name);
13652
+ return names;
13653
+ }
13654
+ /************************************************************
13655
+ * The name of a data type. An unknown type is reported as
13656
+ * the number it is -- silently drawing "DTP_STRING" for a
13657
+ * type nobody registered would be worse than saying "17".
13658
+ ************************************************************/
13659
+ function data_type_name(type) {
13660
+ return DATA_TYPE_NAMES[type] || String(type);
13661
+ }
13662
+ /************************************************************
13663
+ * One SDATA descriptor -> one row.
13664
+ *
13665
+ * A DTP_POINTER default is a live object (a DOM node, a
13666
+ * gobj): it is never printed, only named, because a viewer
13667
+ * that serializes it would either throw or dump the page.
13668
+ ************************************************************/
13669
+ function sdatadesc2json(it) {
13670
+ let row = {
13671
+ id: it.name,
13672
+ type: data_type_name(it.type),
13673
+ flag: bits2names(_yuneta_gobj_js.sdata_flag_t, it.flag),
13674
+ description: it.description || ""
13675
+ };
13676
+ if (it.type === _yuneta_gobj_js.data_type_t.DTP_POINTER) row.default_value = it.default_value === null || it.default_value === void 0 ? null : "<pointer>";
13677
+ else row.default_value = it.default_value === void 0 ? null : it.default_value;
13678
+ return row;
13679
+ }
13680
+ /************************************************************
13681
+ * The attrs table of a gclass.
13682
+ ************************************************************/
13683
+ function attrs2json(attrs_table) {
13684
+ let rows = [];
13685
+ if (!Array.isArray(attrs_table)) return rows;
13686
+ for (let it of attrs_table) {
13687
+ if (!it || !it.name) continue;
13688
+ rows.push(sdatadesc2json(it));
13689
+ }
13690
+ return rows;
13691
+ }
13692
+ /************************************************************
13693
+ * The command table of a gclass, with the parameters of
13694
+ * each command (the `schema` slot of SDATACM).
13695
+ ************************************************************/
13696
+ function commands2json(command_table) {
13697
+ let rows = [];
13698
+ if (!Array.isArray(command_table)) return rows;
13699
+ for (let it of command_table) {
13700
+ if (!it || !it.name) continue;
13701
+ let row = {
13702
+ id: it.name,
13703
+ alias: it.alias || [],
13704
+ description: it.description || ""
13705
+ };
13706
+ if (Array.isArray(it.schema)) row.parameters = attrs2json(it.schema);
13707
+ rows.push(row);
13708
+ }
13709
+ return rows;
13710
+ }
13711
+ /************************************************************
13712
+ * The methods the gclass actually implements. A table of
13713
+ * every possible one, mostly empty, tells the reader nothing.
13714
+ ************************************************************/
13715
+ function methods2json(mt) {
13716
+ let names = [];
13717
+ if (!mt || typeof mt !== "object") return names;
13718
+ for (let key of Object.keys(mt)) if (typeof mt[key] === "function") names.push(key);
13719
+ return names;
13720
+ }
13721
+ /************************************************************
13722
+ * The FSM: the events the gclass declares, and what each
13723
+ * state does with them.
13724
+ ************************************************************/
13725
+ function fsm2json(gclass) {
13726
+ let events = [];
13727
+ for (let ev of gclass.dl_events || []) events.push({
13728
+ event_name: ev.event_name,
13729
+ event_flag: bits2names(_yuneta_gobj_js.event_flag_t, ev.event_flag)
13730
+ });
13731
+ let states = {};
13732
+ for (let st of gclass.dl_states || []) {
13733
+ let actions = [];
13734
+ for (let ac of st.dl_actions || []) actions.push({
13735
+ event_name: ac.event_name,
13736
+ action: ac.action ? ac.action.name || "action" : "",
13737
+ next_state: ac.next_state || ""
13738
+ });
13739
+ states[st.state_name] = actions;
13740
+ }
13741
+ return {
13742
+ events,
13743
+ states
13744
+ };
13745
+ }
13746
+ /************************************************************
13747
+ * The description of a gclass REGISTERED IN THIS BROWSER YUNO,
13748
+ * or null when the registry does not know that name.
13749
+ *
13750
+ * Null is the answer for a gclass of a BACKEND yuno too: the
13751
+ * browser registry carries the GUI gclasses and nothing else,
13752
+ * so the caller asks the backend `view-gclass` for those and
13753
+ * shows whatever it answers -- this same shape.
13754
+ ************************************************************/
13755
+ function describe_local_gclass(gclass_name) {
13756
+ if (!gclass_name) return null;
13757
+ let gclass = (0, _yuneta_gobj_js.gclass_find_by_name)(gclass_name, false);
13758
+ if (!gclass) return null;
13759
+ return {
13760
+ id: gclass.gclass_name,
13761
+ gcflag: bits2names(_yuneta_gobj_js.gclass_flag_t, gclass.gclass_flag),
13762
+ attrs: attrs2json(gclass.attrs_table),
13763
+ commands: commands2json(gclass.command_table),
13764
+ gclass_methods: methods2json(gclass.gmt),
13765
+ internal_methods: methods2json(gclass.lmt),
13766
+ FSM: fsm2json(gclass),
13767
+ instances: Number(gclass.instances) || 0,
13768
+ trace_levels: Array.isArray(gclass.s_user_trace_level) ? gclass.s_user_trace_level : []
13769
+ };
13770
+ }
13771
+ //#endregion
13772
+ //#region src/yui_gclass_view.js
13773
+ /***********************************************************************
13774
+ * yui_gclass_view.js
13775
+ *
13776
+ * The gclass viewer: what a GClass IS, in a window.
13777
+ *
13778
+ * The framework had no such viewer. The C kernel answers
13779
+ * `view-gclass` with a full description (attrs, commands,
13780
+ * methods, FSM) and the only reader of it was a terminal.
13781
+ * This one draws that same document with C_YUI_JSON, so the
13782
+ * FSM of a gclass is a graph and its attrs table is a tree,
13783
+ * and it takes the document from either side:
13784
+ *
13785
+ * - a gclass of the browser yuno -> describe_local_gclass()
13786
+ * - a gclass of a backend yuno -> `opts.description`, the
13787
+ * answer of `view-gclass`
13788
+ *
13789
+ * One window at a time per host: the caller keeps the handle
13790
+ * and closes it, the way the treedb graph keeps its raw-JSON
13791
+ * viewer.
13792
+ *
13793
+ * Copyright (c) 2026, ArtGins.
13794
+ * All Rights Reserved.
13795
+ ***********************************************************************/
13796
+ /************************************************************
13797
+ * Can this app show a gclass at all?
13798
+ *
13799
+ * The viewer is C_YUI_JSON, and an app registers only the
13800
+ * gclasses it mounts. The caller asks this BEFORE drawing a
13801
+ * button, so an app that does not carry the viewer shows no
13802
+ * control instead of a control that fails.
13803
+ ************************************************************/
13804
+ function gclass_view_available() {
13805
+ return !!(0, _yuneta_gobj_js.gclass_find_by_name)("C_YUI_JSON", false);
13806
+ }
13807
+ /************************************************************
13808
+ * Open the viewer on `gclass_name`.
13809
+ *
13810
+ * opts:
13811
+ * description the document to draw. Omitted, it is
13812
+ * built from the browser registry.
13813
+ * title_prefix what the window says before the gclass
13814
+ * name (the yuno it belongs to).
13815
+ * on_close called when the reader dismisses it.
13816
+ *
13817
+ * Returns a handle for close_gclass_view(), or null (and a
13818
+ * logged reason) when there is nothing to show.
13819
+ ************************************************************/
13820
+ function open_gclass_view(host, gclass_name, opts) {
13821
+ opts = opts || {};
13822
+ if (!host || !gclass_name) {
13823
+ (0, _yuneta_gobj_js.log_error)(`yui_gclass_view: open without a host or a gclass name`);
13824
+ return null;
13825
+ }
13826
+ let shell = yui_shell_of(host);
13827
+ if (!gclass_view_available()) {
13828
+ (0, _yuneta_gobj_js.log_error)(`${(0, _yuneta_gobj_js.gobj_short_name)(host)}: C_YUI_JSON not registered by the app`);
13829
+ yui_shell_show_error(shell, "gclass viewer unavailable", { t: i18next.t });
13830
+ return null;
13831
+ }
13832
+ let description = opts.description || describe_local_gclass(gclass_name);
13833
+ if (!description) {
13834
+ (0, _yuneta_gobj_js.log_warning)(`${(0, _yuneta_gobj_js.gobj_short_name)(host)}: gclass not registered here: ${gclass_name}`);
13835
+ yui_shell_show_error(shell, "gclass not registered in this yuno", { t: i18next.t });
13836
+ return null;
13837
+ }
13838
+ let jv = (0, _yuneta_gobj_js.gobj_create_service)(`gclass-view-${(0, _yuneta_gobj_js.clean_name)((0, _yuneta_gobj_js.gobj_name)(host))}`, "C_YUI_JSON", {
13839
+ json_data: description,
13840
+ subscriber: null
13841
+ }, host);
13842
+ if (!jv) {
13843
+ (0, _yuneta_gobj_js.log_error)(`${(0, _yuneta_gobj_js.gobj_short_name)(host)}: cannot create the gclass viewer`);
13844
+ return null;
13845
+ }
13846
+ (0, _yuneta_gobj_js.gobj_start)(jv);
13847
+ let handle = {
13848
+ json_gobj: jv,
13849
+ win: null,
13850
+ modal: null
13851
+ };
13852
+ let $box = (0, _yuneta_gobj_js.gobj_read_pointer_attr)(jv, "$container");
13853
+ let on_close = () => {
13854
+ if (typeof opts.on_close === "function") opts.on_close();
13855
+ };
13856
+ if (is_mobile$3()) {
13857
+ if (!shell) {
13858
+ (0, _yuneta_gobj_js.log_error)(`${(0, _yuneta_gobj_js.gobj_short_name)(host)}: no shell, cannot open the gclass sheet`);
13859
+ close_gclass_view(handle);
13860
+ return null;
13861
+ }
13862
+ handle.modal = yui_shell_show_modal(shell, $box, {
13863
+ dialog: true,
13864
+ logical_class: "GCLASS_VIEW_SHEET",
13865
+ title_prefix: opts.title_prefix || "",
13866
+ title: gclass_name,
13867
+ t: i18next.t,
13868
+ on_close
13869
+ });
13870
+ return handle;
13871
+ }
13872
+ let $win_parent = window_layer_of(host, shell);
13873
+ handle.win = (0, _yuneta_gobj_js.gobj_create_service)(`gclass-win-${(0, _yuneta_gobj_js.clean_name)((0, _yuneta_gobj_js.gobj_name)(host))}`, "C_YUI_WINDOW", {
13874
+ $parent: $win_parent,
13875
+ subscriber: null,
13876
+ modal: false,
13877
+ showMax: true,
13878
+ showFooter: false,
13879
+ resizable: true,
13880
+ center: true,
13881
+ auto_save_size_and_position: true,
13882
+ width: 720,
13883
+ height: 640,
13884
+ logical_class: "GCLASS_VIEW_WINDOW",
13885
+ title_prefix: opts.title_prefix || "",
13886
+ title: gclass_name,
13887
+ icon: "yi-code",
13888
+ body: $box,
13889
+ manager: (0, _yuneta_gobj_js.gobj_find_service)("__window_manager__", false) || null,
13890
+ on_close
13891
+ }, host);
13892
+ if (!handle.win) {
13893
+ (0, _yuneta_gobj_js.log_error)(`${(0, _yuneta_gobj_js.gobj_short_name)(host)}: cannot create the gclass window`);
13894
+ close_gclass_view(handle);
13895
+ return null;
13896
+ }
13897
+ (0, _yuneta_gobj_js.gobj_start)(handle.win);
13898
+ return handle;
13899
+ }
13900
+ /************************************************************
13901
+ * Close whatever open_gclass_view() opened. Safe on null and
13902
+ * on a handle whose window the reader already dismissed.
13903
+ ************************************************************/
13904
+ function close_gclass_view(handle) {
13905
+ if (!handle) return;
13906
+ let jv = handle.json_gobj;
13907
+ let win = handle.win;
13908
+ let modal = handle.modal;
13909
+ handle.json_gobj = null;
13910
+ handle.win = null;
13911
+ handle.modal = null;
13912
+ if (win && (0, _yuneta_gobj_js.is_gobj)(win)) try {
13913
+ if ((0, _yuneta_gobj_js.gobj_is_running)(win)) (0, _yuneta_gobj_js.gobj_stop)(win);
13914
+ (0, _yuneta_gobj_js.gobj_destroy)(win);
13915
+ } catch (e) {
13916
+ (0, _yuneta_gobj_js.log_warning)(`yui_gclass_view: window already gone: ${e}`);
13917
+ }
13918
+ if (modal && typeof modal.close === "function") try {
13919
+ modal.close();
13920
+ } catch (e) {
13921
+ (0, _yuneta_gobj_js.log_warning)(`yui_gclass_view: modal already gone: ${e}`);
13922
+ }
13923
+ if (jv && (0, _yuneta_gobj_js.is_gobj)(jv)) try {
13924
+ if ((0, _yuneta_gobj_js.gobj_is_running)(jv)) (0, _yuneta_gobj_js.gobj_stop)(jv);
13925
+ (0, _yuneta_gobj_js.gobj_destroy)(jv);
13926
+ } catch (e) {
13927
+ (0, _yuneta_gobj_js.log_warning)(`yui_gclass_view: viewer already gone: ${e}`);
13928
+ }
13929
+ }
13930
+ /************************************************************
13931
+ * Where to mount the window: the SAME element the caller's
13932
+ * own window hangs from.
13933
+ *
13934
+ * Not a preference -- a requirement. z-index only orders
13935
+ * siblings of one stacking context, so a viewer opened from a
13936
+ * window on `body` and mounted in the shell's popup layer
13937
+ * lands BEHIND its opener whatever number it is given, and
13938
+ * the button reads as one that did nothing.
13939
+ ************************************************************/
13940
+ function window_layer_of(host, shell) {
13941
+ let $c = (0, _yuneta_gobj_js.gobj_read_pointer_attr)(host, "$container");
13942
+ if ($c && typeof $c.closest === "function") {
13943
+ let $win = $c.closest(".C_YUI_WINDOW");
13944
+ if ($win && $win.parentNode) return $win.parentNode;
13945
+ }
13946
+ return typeof document !== "undefined" && document.getElementById("top-layer") || shell && yui_shell_popup_layer(shell) || null;
13947
+ }
13948
+ /************************************************************
13949
+ * A window is not a sheet: below this width there is no room
13950
+ * for a title bar the reader can drag.
13951
+ ************************************************************/
13952
+ function is_mobile$3() {
13953
+ return typeof window !== "undefined" && window.innerWidth <= 768;
13954
+ }
13955
+ //#endregion
13218
13956
  //#region src/c_yui_gobj_tree_js.js
13219
13957
  /***********************************************************************
13220
13958
  * c_yui_gobj_tree_js.js
@@ -13250,6 +13988,71 @@ var BEHAVIORS_READING = [
13250
13988
  "drag-element"
13251
13989
  ];
13252
13990
  var BEHAVIORS_PICKING = ["drag-canvas", ZOOM_CANVAS];
13991
+ var VIEW_STORE_PREFIX = "yui_gobj_tree_view:";
13992
+ function view_store_key(gobj) {
13993
+ return VIEW_STORE_PREFIX + (0, _yuneta_gobj_js.gobj_name)(gobj);
13994
+ }
13995
+ function restore_view_shape(gobj) {
13996
+ let priv = gobj.priv;
13997
+ let state = load_view_state(gobj);
13998
+ if (!state) return;
13999
+ if (state.layout && LAYOUTS[state.layout]) {
14000
+ (0, _yuneta_gobj_js.gobj_write_str_attr)(gobj, "layout", state.layout);
14001
+ if (priv.$layout_select) priv.$layout_select.value = state.layout;
14002
+ }
14003
+ if (state.collapsed && typeof state.collapsed === "object") priv.collapse_state = Object.assign({}, state.collapsed);
14004
+ }
14005
+ function restore_view_state(gobj) {
14006
+ let priv = gobj.priv;
14007
+ let graph = priv.graph;
14008
+ let state = load_view_state(gobj);
14009
+ if (!graph || !state) return false;
14010
+ let done = false;
14011
+ priv.restoring = true;
14012
+ try {
14013
+ if (typeof state.zoom === "number" && state.zoom > 0) {
14014
+ graph.zoomTo(state.zoom);
14015
+ priv.last_zoom = state.zoom;
14016
+ done = true;
14017
+ }
14018
+ if (Array.isArray(state.position) && state.position.length >= 2) {
14019
+ graph.translateTo([state.position[0], state.position[1]]);
14020
+ done = true;
14021
+ }
14022
+ if (state.anchor) {
14023
+ priv.anchor_name = state.anchor;
14024
+ priv.anchor_state = "on";
14025
+ reanchor(gobj);
14026
+ yui_graph_update_anchor((0, _yuneta_gobj_js.gobj_read_attr)(gobj, "$container"), priv.anchor_state);
14027
+ paint_anchor_mark(gobj);
14028
+ }
14029
+ } catch (e) {
14030
+ (0, _yuneta_gobj_js.log_error)(`${(0, _yuneta_gobj_js.gobj_short_name)(gobj)}: cannot restore the saved view: ${e}`);
14031
+ } finally {
14032
+ priv.restoring = false;
14033
+ }
14034
+ yui_graph_update_zoom((0, _yuneta_gobj_js.gobj_read_attr)(gobj, "$container"), priv.graph);
14035
+ return done;
14036
+ }
14037
+ function save_view_state(gobj) {
14038
+ let priv = gobj.priv;
14039
+ let graph = priv.graph;
14040
+ if (!graph || priv.restoring) return;
14041
+ let state = {
14042
+ layout: (0, _yuneta_gobj_js.gobj_read_str_attr)(gobj, "layout"),
14043
+ collapsed: priv.collapse_state || {},
14044
+ anchor: priv.anchor_state === "on" ? priv.anchor_name : ""
14045
+ };
14046
+ try {
14047
+ state.zoom = graph.getZoom();
14048
+ state.position = graph.getPosition();
14049
+ } catch (e) {}
14050
+ (0, _yuneta_gobj_js.kw_set_local_storage_value)(view_store_key(gobj), state);
14051
+ }
14052
+ function load_view_state(gobj) {
14053
+ let state = (0, _yuneta_gobj_js.kw_get_local_storage_value)(view_store_key(gobj), null, false);
14054
+ return state && typeof state === "object" ? state : null;
14055
+ }
13253
14056
  var PRIVATE_DATA$9 = {
13254
14057
  canvas_id: "",
13255
14058
  graph: null,
@@ -13260,6 +14063,9 @@ var PRIVATE_DATA$9 = {
13260
14063
  $popover: null,
13261
14064
  $popover_title: null,
13262
14065
  $popover_body: null,
14066
+ $popover_gclass: null,
14067
+ popover_node: null,
14068
+ gclass_view: null,
13263
14069
  node_by_id: null,
13264
14070
  collapse_state: null,
13265
14071
  resize_observer: null,
@@ -13267,39 +14073,99 @@ var PRIVATE_DATA$9 = {
13267
14073
  anchor_id: "",
13268
14074
  anchor_name: "",
13269
14075
  anchor_state: "off",
14076
+ restoring: false,
14077
+ restored: false,
13270
14078
  last_zoom: 1
13271
14079
  };
13272
14080
  var __gclass__$9 = null;
13273
14081
  var __layout_registered__ = false;
13274
14082
  /***************************************************************
13275
- * Node colors by role
14083
+ * Node colours by role.
14084
+ *
14085
+ * Saturated hues, one family apart from the next, and the same
14086
+ * five everywhere the reader meets a gobj. `stroke` draws the
14087
+ * border and the edge; `accent` is the brighter sibling used
14088
+ * for the bar down the left of the card and the toggle badge,
14089
+ * so a card carries its colour twice at two intensities and is
14090
+ * readable at the zoom where the text is not.
13276
14091
  ***************************************************************/
13277
14092
  var ROLE_COLORS = {
13278
14093
  yuno: {
13279
- fill: "#E0F2FE",
13280
- stroke: "#075985"
14094
+ stroke: "#0284c7",
14095
+ accent: "#38bdf8"
13281
14096
  },
13282
14097
  service: {
13283
- fill: "#DCFCE7",
13284
- stroke: "#166534"
14098
+ stroke: "#059669",
14099
+ accent: "#34d399"
13285
14100
  },
13286
14101
  pure: {
13287
- fill: "#FEF9C3",
13288
- stroke: "#854D0E"
14102
+ stroke: "#d97706",
14103
+ accent: "#fbbf24"
13289
14104
  },
13290
14105
  volatil: {
13291
- fill: "#FCE7F3",
13292
- stroke: "#9D174D"
14106
+ stroke: "#db2777",
14107
+ accent: "#f472b6"
13293
14108
  },
13294
- default: {
13295
- fill: "#F3F4F6",
13296
- stroke: "#374151"
14109
+ child: {
14110
+ stroke: "#7c3aed",
14111
+ accent: "#a78bfa"
13297
14112
  }
13298
14113
  };
13299
- var STATE_COLORS = {
13300
- running_playing: "#15803D",
13301
- running: "#B45309",
13302
- stopped: "#B91C1C"
14114
+ /***************************************************************
14115
+ * Status colours: what the gobj is DOING.
14116
+ *
14117
+ * A pill and not a word: three states of one gobj are told
14118
+ * apart across a whole tree by colour long before anybody
14119
+ * reads them, and the dot alone never said which was which.
14120
+ * `disabled` is a fourth entry and not a status -- a disabled
14121
+ * gobj is still stopped or running -- but it takes the pill,
14122
+ * because it is the fact that explains the other one.
14123
+ ***************************************************************/
14124
+ var STATUS_COLORS = {
14125
+ light: {
14126
+ playing: {
14127
+ fg: "#065f46",
14128
+ bg: "#a7f3d0",
14129
+ dot: "#10b981"
14130
+ },
14131
+ running: {
14132
+ fg: "#92400e",
14133
+ bg: "#fde68a",
14134
+ dot: "#f59e0b"
14135
+ },
14136
+ stopped: {
14137
+ fg: "#991b1b",
14138
+ bg: "#fecaca",
14139
+ dot: "#ef4444"
14140
+ },
14141
+ disabled: {
14142
+ fg: "#334155",
14143
+ bg: "#e2e8f0",
14144
+ dot: "#94a3b8"
14145
+ }
14146
+ },
14147
+ dark: {
14148
+ playing: {
14149
+ fg: "#6ee7b7",
14150
+ bg: "rgba(16,185,129,0.28)",
14151
+ dot: "#34d399"
14152
+ },
14153
+ running: {
14154
+ fg: "#fcd34d",
14155
+ bg: "rgba(245,158,11,0.28)",
14156
+ dot: "#fbbf24"
14157
+ },
14158
+ stopped: {
14159
+ fg: "#fca5a5",
14160
+ bg: "rgba(239,68,68,0.28)",
14161
+ dot: "#f87171"
14162
+ },
14163
+ disabled: {
14164
+ fg: "#cbd5e1",
14165
+ bg: "rgba(148,163,184,0.28)",
14166
+ dot: "#94a3b8"
14167
+ }
14168
+ }
13303
14169
  };
13304
14170
  /***************************************************************
13305
14171
  * Fixed node sizes per role (px).
@@ -13309,16 +14175,16 @@ var STATE_COLORS = {
13309
14175
  var NODE_SIZES = {
13310
14176
  full: {
13311
14177
  yuno: {
13312
- w: 240,
13313
- h: 72
14178
+ w: 260,
14179
+ h: 96
13314
14180
  },
13315
14181
  service: {
13316
- w: 210,
13317
- h: 68
14182
+ w: 232,
14183
+ h: 92
13318
14184
  },
13319
14185
  child: {
13320
- w: 160,
13321
- h: 60
14186
+ w: 196,
14187
+ h: 86
13322
14188
  }
13323
14189
  },
13324
14190
  compact: {
@@ -13341,27 +14207,47 @@ var NODE_SIZES = {
13341
14207
  ***************************************************************/
13342
14208
  var GT_FONT = "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif";
13343
14209
  /***************************************************************
13344
- * Soft, theme-aware card palette derived from the role colour
13345
- * (same visual language as the treedb graph cards): light tint
13346
- * fill + role-colour border, brightened on dark.
14210
+ * Theme-aware card palette derived from the role colour.
14211
+ *
14212
+ * Brighter than a tint: at 9% of the role colour every card
14213
+ * was the same near-white rectangle and the tree read as a
14214
+ * wireframe. The fill carries enough of the hue to be named
14215
+ * from across the canvas, and the border and the accent bar
14216
+ * carry the rest.
13347
14217
  ***************************************************************/
13348
- function role_card_style(stroke, dark) {
14218
+ function role_card_style(colors, dark) {
14219
+ let stroke = colors.stroke;
14220
+ let accent = colors.accent;
13349
14221
  return {
13350
- bg: dark ? `color-mix(in srgb, ${stroke} 30%, #2c3542)` : `color-mix(in srgb, ${stroke} 9%, #ffffff)`,
13351
- border: dark ? `color-mix(in srgb, ${stroke} 85%, #ffffff)` : stroke,
13352
- title: dark ? "#e8eaed" : "#0f172a",
13353
- sub: dark ? "#9aa4b2" : "#64748b",
13354
- tagbg: dark ? `color-mix(in srgb, ${stroke} 42%, #2c3542)` : `color-mix(in srgb, ${stroke} 14%, #ffffff)`,
13355
- shadow: dark ? "0 1px 3px rgba(0,0,0,0.45)" : "0 1px 3px rgba(15,23,42,0.12)"
14222
+ bg: dark ? `color-mix(in srgb, ${stroke} 34%, #253044)` : `color-mix(in srgb, ${stroke} 13%, #ffffff)`,
14223
+ border: dark ? accent : stroke,
14224
+ accent: dark ? accent : stroke,
14225
+ title: dark ? "#f1f5f9" : "#0f172a",
14226
+ sub: dark ? "#a8b3c2" : "#475569",
14227
+ tagbg: dark ? `color-mix(in srgb, ${stroke} 48%, #253044)` : `color-mix(in srgb, ${stroke} 20%, #ffffff)`,
14228
+ tagfg: dark ? "#f1f5f9" : stroke,
14229
+ shadow: dark ? "0 2px 6px rgba(0,0,0,0.50)" : "0 2px 5px rgba(15,23,42,0.16)"
13356
14230
  };
13357
14231
  }
13358
14232
  /***************************************************************
13359
- * Map a gobj to its visual category.
14233
+ * The pill colours of a node, for the theme in force.
13360
14234
  ***************************************************************/
13361
- function get_node_category(target_gobj, is_root) {
13362
- if (is_root) return "yuno";
13363
- if ((0, _yuneta_gobj_js.gobj_is_service)(target_gobj)) return "service";
13364
- return "child";
14235
+ function status_card_style(d, dark) {
14236
+ let table = dark ? STATUS_COLORS.dark : STATUS_COLORS.light;
14237
+ if (d.disabled) return table.disabled;
14238
+ return table[node_status(d)] || table.stopped;
14239
+ }
14240
+ /***************************************************************
14241
+ * Map a node to its visual size class. Services are drawn
14242
+ * bigger than plain children so the service boundary of the
14243
+ * hierarchy is visible before anything is read.
14244
+ ***************************************************************/
14245
+ function get_node_category(d) {
14246
+ switch (node_role(d)) {
14247
+ case "yuno": return "yuno";
14248
+ case "service": return "service";
14249
+ default: return "child";
14250
+ }
13365
14251
  }
13366
14252
  /***************************************************************
13367
14253
  * Layouts registry.
@@ -13473,13 +14359,22 @@ function mt_create$9(gobj) {
13473
14359
  * Framework Method: Start
13474
14360
  ***************************************************************/
13475
14361
  function mt_start$9(gobj) {
14362
+ restore_view_shape(gobj);
14363
+ let shell = yui_shell_of(gobj);
14364
+ if (shell) (0, _yuneta_gobj_js.gobj_subscribe_event)(shell, "EV_LANGUAGE_CHANGED", {}, gobj);
13476
14365
  build_graph$2(gobj);
13477
14366
  load_tree(gobj);
13478
14367
  }
13479
14368
  /***************************************************************
13480
14369
  * Framework Method: Stop
13481
14370
  ***************************************************************/
13482
- function mt_stop$9(gobj) {}
14371
+ function mt_stop$9(gobj) {
14372
+ let priv = gobj.priv;
14373
+ let shell = yui_shell_of(gobj);
14374
+ if (shell) (0, _yuneta_gobj_js.gobj_unsubscribe_event)(shell, "EV_LANGUAGE_CHANGED", {}, gobj);
14375
+ close_gclass_view(priv.gclass_view);
14376
+ priv.gclass_view = null;
14377
+ }
13483
14378
  /***************************************************************
13484
14379
  * Framework Method: Destroy
13485
14380
  ***************************************************************/
@@ -13505,6 +14400,8 @@ function mt_destroy$9(gobj) {
13505
14400
  priv.$popover = null;
13506
14401
  priv.$popover_title = null;
13507
14402
  priv.$popover_body = null;
14403
+ priv.$popover_gclass = null;
14404
+ priv.popover_node = null;
13508
14405
  priv.$layout_select = null;
13509
14406
  priv.node_by_id = null;
13510
14407
  priv.collapse_state = null;
@@ -13608,10 +14505,38 @@ function build_popover(gobj, $container) {
13608
14505
  text-overflow: ellipsis;
13609
14506
  `;
13610
14507
  $header.appendChild($title);
14508
+ if (gclass_view_available()) {
14509
+ let $gclass = document.createElement("button");
14510
+ $gclass.type = "button";
14511
+ $gclass.className = "GOBJ_TREE_POPOVER_GCLASS button is-small";
14512
+ $gclass.style.cssText = `
14513
+ margin-right: 6px;
14514
+ padding: 0 8px;
14515
+ height: 22px;
14516
+ font-size: 11px;
14517
+ white-space: nowrap;
14518
+ `;
14519
+ $gclass.textContent = (0, i18next.t)("gclass");
14520
+ $gclass.setAttribute("i18n", "gclass");
14521
+ $gclass.title = (0, i18next.t)("view gclass");
14522
+ $gclass.setAttribute("data-i18n-title", "view gclass");
14523
+ $gclass.setAttribute("aria-label", (0, i18next.t)("view gclass"));
14524
+ $gclass.setAttribute("data-i18n-aria-label", "view gclass");
14525
+ $gclass.addEventListener("click", (evt) => {
14526
+ evt.stopPropagation();
14527
+ (0, _yuneta_gobj_js.gobj_send_event)(gobj, "EV_OPEN_GCLASS", {}, gobj);
14528
+ });
14529
+ $header.appendChild($gclass);
14530
+ priv.$popover_gclass = $gclass;
14531
+ } else (0, _yuneta_gobj_js.log_warning)(`${(0, _yuneta_gobj_js.gobj_short_name)(gobj)}: no C_YUI_JSON registered, the gclass viewer is not offered`);
13611
14532
  let $close = document.createElement("button");
13612
14533
  $close.type = "button";
14534
+ $close.className = "GOBJ_TREE_POPOVER_CLOSE";
13613
14535
  $close.textContent = "×";
13614
14536
  $close.title = (0, i18next.t)("close");
14537
+ $close.setAttribute("data-i18n-title", "close");
14538
+ $close.setAttribute("aria-label", (0, i18next.t)("close"));
14539
+ $close.setAttribute("data-i18n-aria-label", "close");
13615
14540
  $close.style.cssText = `
13616
14541
  background: transparent;
13617
14542
  border: none;
@@ -13745,6 +14670,7 @@ function build_graph$2(gobj) {
13745
14670
  if (z === priv.last_zoom) return;
13746
14671
  priv.last_zoom = z;
13747
14672
  if (priv.anchor_state === "on" && priv.anchor_id) yui_graph_center_on(priv.graph, priv.anchor_id);
14673
+ save_view_state(gobj);
13748
14674
  });
13749
14675
  graph.on(_antv_g6.NodeEvent.CLICK, (evt) => {
13750
14676
  (0, _yuneta_gobj_js.gobj_send_event)(gobj, "EV_NODE_CLICK", { evt }, gobj);
@@ -13787,22 +14713,20 @@ function apply_layout(gobj) {
13787
14713
  });
13788
14714
  }
13789
14715
  /************************************************************
13790
- * Resolve role colors for a gobj
13791
- ************************************************************/
13792
- function get_role_colors(target_gobj, is_root) {
13793
- if (is_root) return ROLE_COLORS.yuno;
13794
- if ((0, _yuneta_gobj_js.gobj_is_volatil)(target_gobj)) return ROLE_COLORS.volatil;
13795
- if ((0, _yuneta_gobj_js.gobj_is_service)(target_gobj)) return ROLE_COLORS.service;
13796
- if ((0, _yuneta_gobj_js.gobj_is_pure_child)(target_gobj)) return ROLE_COLORS.pure;
13797
- return ROLE_COLORS.default;
13798
- }
13799
- /************************************************************
13800
- * Resolve state color
14716
+ * The role colours of a node.
14717
+ *
14718
+ * Note the ORDER: volatil before service. A volatil gobj that
14719
+ * is also a service is drawn volatil, because that is the fact
14720
+ * that decides how long it lives.
13801
14721
  ************************************************************/
13802
- function get_state_color(target_gobj) {
13803
- if (!(0, _yuneta_gobj_js.gobj_is_running)(target_gobj)) return STATE_COLORS.stopped;
13804
- if ((0, _yuneta_gobj_js.gobj_is_playing)(target_gobj)) return STATE_COLORS.running_playing;
13805
- return STATE_COLORS.running;
14722
+ function get_role_colors(d) {
14723
+ switch (node_role(d)) {
14724
+ case "yuno": return ROLE_COLORS.yuno;
14725
+ case "volatil": return ROLE_COLORS.volatil;
14726
+ case "service": return ROLE_COLORS.service;
14727
+ case "pure": return ROLE_COLORS.pure;
14728
+ default: return ROLE_COLORS.child;
14729
+ }
13806
14730
  }
13807
14731
  /************************************************************
13808
14732
  * Generate a unique node id
@@ -13834,72 +14758,135 @@ function is_node_collapsed(gobj, full_name, num_children) {
13834
14758
  ************************************************************/
13835
14759
  function render_toggle_html(node_id, collapsed, num_children, cs) {
13836
14760
  let label = collapsed ? "+" + num_children : "−";
14761
+ let title = collapsed ? (0, i18next.t)("expand children") : (0, i18next.t)("collapse children");
13837
14762
  return `<span
13838
14763
  data-gobj-toggle="true"
13839
14764
  data-node-id="${node_id}"
13840
- title="${collapsed ? "Expand children" : "Collapse children"}"
14765
+ title="${(0, _yuneta_gobj_js.escapeHtml)(title)}"
13841
14766
  style="
13842
14767
  flex: 0 0 auto;
13843
14768
  display: inline-flex;
13844
14769
  align-items: center;
13845
14770
  justify-content: center;
13846
- min-width: 22px;
13847
- height: 16px;
13848
- padding: 0 5px;
14771
+ min-width: 24px;
14772
+ height: 18px;
14773
+ padding: 0 6px;
13849
14774
  margin-left: 4px;
13850
- background: ${cs.tagbg};
13851
- color: ${cs.border};
13852
- border: 1px solid ${cs.border};
13853
- border-radius: 6px;
14775
+ background: ${cs.accent};
14776
+ color: #ffffff;
14777
+ border: 1px solid ${cs.accent};
14778
+ border-radius: 9px;
13854
14779
  font-size: 10px;
13855
- font-weight: 600;
14780
+ font-weight: 700;
13856
14781
  line-height: 1;
13857
14782
  cursor: pointer;
13858
14783
  user-select: none;
13859
14784
  ">${label}</span>`;
13860
14785
  }
13861
14786
  /************************************************************
13862
- * Recursively build nodes and edges from the gobj tree
14787
+ * HTML for the status pill: what the gobj is DOING.
14788
+ *
14789
+ * A dot plus a word, and the word is dropped in the compact
14790
+ * card where there is no room for it -- the colour still
14791
+ * carries it, and the `title` says it in full either way.
14792
+ ************************************************************/
14793
+ function render_status_html(d, ss, labels, with_label) {
14794
+ let full = d.disabled ? labels.field.disabled : labels.status[node_status_key(d)] || node_status_key(d);
14795
+ let label = d.disabled ? labels.field.disabled : labels.status_short[node_status(d)] || node_status(d);
14796
+ if (!with_label) return `<span title="${(0, _yuneta_gobj_js.escapeHtml)(full)}" style="
14797
+ flex: 0 0 auto;
14798
+ width: 10px; height: 10px; border-radius: 50%;
14799
+ background: ${ss.dot};
14800
+ box-shadow: 0 0 0 2px ${ss.bg};
14801
+ margin-left: 6px;
14802
+ "></span>`;
14803
+ return `<span title="${(0, _yuneta_gobj_js.escapeHtml)(full)}" style="
14804
+ flex: 0 0 auto;
14805
+ display: inline-flex;
14806
+ align-items: center;
14807
+ gap: 4px;
14808
+ padding: 1px 7px 1px 5px;
14809
+ background: ${ss.bg};
14810
+ color: ${ss.fg};
14811
+ border-radius: 9px;
14812
+ font-size: 10px;
14813
+ font-weight: 700;
14814
+ line-height: 1.45;
14815
+ white-space: nowrap;
14816
+ "><span style="width:7px; height:7px; border-radius:50%; background:${ss.dot};"></span>${(0, _yuneta_gobj_js.escapeHtml)(label)}</span>`;
14817
+ }
14818
+ /************************************************************
14819
+ * HTML for the badges: what the gobj IS, besides its colour.
14820
+ ************************************************************/
14821
+ function render_badges_html(d, cs, labels) {
14822
+ let html = "";
14823
+ for (let key of node_badge_keys(d)) {
14824
+ let label = labels.badge[key] || key;
14825
+ html += `<span style="
14826
+ font-size: 10px;
14827
+ font-weight: 600;
14828
+ padding: 0 6px;
14829
+ margin-right: 4px;
14830
+ background: ${cs.tagbg};
14831
+ color: ${cs.tagfg};
14832
+ border: 1px solid ${cs.accent};
14833
+ border-radius: 9px;
14834
+ white-space: nowrap;
14835
+ ">${(0, _yuneta_gobj_js.escapeHtml)(label)}</span>`;
14836
+ }
14837
+ return html;
14838
+ }
14839
+ /************************************************************
14840
+ * Recursively build nodes and edges from a NODE DESCRIPTOR
14841
+ * tree (gobj_tree_model.js), never from gobjs: the same
14842
+ * builder draws the browser yuno and a backend one.
14843
+ *
14844
+ * The descriptor is also what the popover reads, so the view
14845
+ * fields it needs are written onto it here (`full_name`,
14846
+ * `short_name`, `num_children`, `is_collapsed`,
14847
+ * `direct_children`) instead of copied into a second object
14848
+ * that can then disagree with the first.
13863
14849
  ************************************************************/
13864
- function build_gobj_nodes(gobj, target_gobj, nodes, edges, parent_id, is_root, compact) {
14850
+ function build_gobj_nodes(gobj, d, nodes, edges, parent_id, compact, labels) {
13865
14851
  let priv = gobj.priv;
13866
14852
  let node_id = gen_node_id(gobj);
13867
- let gclass_name = (0, _yuneta_gobj_js.gobj_gclass_name)(target_gobj) || "";
13868
- let instance_name = (0, _yuneta_gobj_js.gobj_name)(target_gobj) || "";
13869
- let state = (0, _yuneta_gobj_js.gobj_current_state)(target_gobj) || "";
13870
- let full_name = (0, _yuneta_gobj_js.gobj_full_name)(target_gobj);
13871
- let colors = get_role_colors(target_gobj, is_root);
13872
14853
  let dark = yui_is_dark();
13873
- let cs = role_card_style(colors.stroke, dark);
13874
- let state_color = get_state_color(target_gobj);
13875
- let running = (0, _yuneta_gobj_js.gobj_is_running)(target_gobj);
13876
- let playing = (0, _yuneta_gobj_js.gobj_is_playing)(target_gobj);
13877
- let status_text = !running ? "stopped" : playing ? "playing" : "running";
13878
- let category = get_node_category(target_gobj, is_root);
14854
+ let colors = get_role_colors(d);
14855
+ let cs = role_card_style(colors, dark);
14856
+ let ss = status_card_style(d, dark);
14857
+ let category = get_node_category(d);
13879
14858
  let size = NODE_SIZES[compact ? "compact" : "full"][category];
13880
14859
  let width = size.w;
13881
14860
  let height = size.h;
13882
14861
  let border_width = category === "yuno" || category === "service" ? 2 : 1;
13883
- let children = target_gobj.dl_children || [];
14862
+ let border_style = d.disabled ? "dashed" : "solid";
14863
+ let children = d.children || [];
13884
14864
  let num_children = children.length;
13885
14865
  let has_children = num_children > 0;
13886
- let collapsed = has_children && is_node_collapsed(gobj, full_name, num_children);
14866
+ let collapsed = has_children && is_node_collapsed(gobj, d.fullname, num_children);
13887
14867
  let direct_children = [];
13888
14868
  for (let child of children) direct_children.push({
13889
- full_name: (0, _yuneta_gobj_js.gobj_full_name)(child),
13890
- has_grandchildren: (child.dl_children || []).length > 0
14869
+ full_name: child.fullname,
14870
+ has_grandchildren: (child.children || []).length > 0
13891
14871
  });
14872
+ d.full_name = d.fullname;
14873
+ d.short_name = d.shortname;
14874
+ d.num_children = num_children;
14875
+ d.is_collapsed = collapsed;
14876
+ d.direct_children = direct_children;
14877
+ d.node_id = node_id;
13892
14878
  let toggle_html = has_children ? render_toggle_html(node_id, collapsed, num_children, cs) : "";
14879
+ let card_title = d.gclass + (d.name ? "^" + d.name : "");
13893
14880
  let node_html;
13894
14881
  if (compact) {
13895
- let compact_label = (0, _yuneta_gobj_js.escapeHtml)(instance_name ? gclass_name + "^" + instance_name : gclass_name);
14882
+ let compact_label = (0, _yuneta_gobj_js.escapeHtml)(card_title);
13896
14883
  node_html = `
13897
- <div style="
14884
+ <div class="GOBJ_CARD" data-gobj-name="${(0, _yuneta_gobj_js.escapeHtml)(d.fullname)}" style="
13898
14885
  width: ${width}px;
13899
14886
  height: ${height}px;
13900
14887
  background: ${cs.bg};
13901
- border: ${border_width}px solid ${cs.border};
13902
- border-left: 5px solid ${cs.border};
14888
+ border: ${border_width}px ${border_style} ${cs.border};
14889
+ border-left: 6px solid ${cs.accent};
13903
14890
  border-radius: 7px;
13904
14891
  box-shadow: ${cs.shadow};
13905
14892
  font-family: ${GT_FONT};
@@ -13908,34 +14895,24 @@ function build_gobj_nodes(gobj, target_gobj, nodes, edges, parent_id, is_root, c
13908
14895
  padding: 0 8px;
13909
14896
  box-sizing: border-box;
13910
14897
  overflow: hidden;
14898
+ opacity: ${d.disabled ? "0.72" : "1"};
13911
14899
  cursor: pointer;
13912
14900
  " title="${compact_label}">
13913
14901
  <span style="flex:1 1 auto; min-width:0; font-size:11px; font-weight:600; color:${cs.title}; white-space:nowrap; overflow:hidden; text-overflow:ellipsis;">${compact_label}</span>
13914
- <span title="${(0, _yuneta_gobj_js.escapeHtml)(state || status_text)}" style="
13915
- flex: 0 0 auto;
13916
- width: 8px; height: 8px; border-radius: 50%;
13917
- background: ${state_color}; margin-left: 6px;
13918
- "></span>
14902
+ ${render_status_html(d, ss, labels, false)}
13919
14903
  ${toggle_html}
13920
14904
  </div>`;
13921
14905
  } else {
13922
- let header_label = (0, _yuneta_gobj_js.escapeHtml)(gclass_name);
13923
- let name_label = (0, _yuneta_gobj_js.escapeHtml)(instance_name);
13924
- let state_label = (0, _yuneta_gobj_js.escapeHtml)(state);
13925
- let badges = [];
13926
- if (is_root) badges.push("yuno");
13927
- else {
13928
- if ((0, _yuneta_gobj_js.gobj_is_service)(target_gobj)) badges.push("service");
13929
- if ((0, _yuneta_gobj_js.gobj_is_pure_child)(target_gobj)) badges.push("pure");
13930
- if ((0, _yuneta_gobj_js.gobj_is_volatil)(target_gobj)) badges.push("volatil");
13931
- }
13932
- let badges_html = badges.map((b) => `<span style="font-size:10px; padding:0 5px; margin-right:4px; background:${cs.tagbg}; color:${cs.border}; border:1px solid ${cs.border}; border-radius:6px; white-space:nowrap;">${b}</span>`).join("");
14906
+ let header_label = (0, _yuneta_gobj_js.escapeHtml)(d.gclass);
14907
+ let name_label = (0, _yuneta_gobj_js.escapeHtml)(d.name);
14908
+ let state_label = (0, _yuneta_gobj_js.escapeHtml)(d.state);
13933
14909
  node_html = `
13934
- <div style="
14910
+ <div class="GOBJ_CARD" data-gobj-name="${(0, _yuneta_gobj_js.escapeHtml)(d.fullname)}" style="
13935
14911
  width: ${width}px;
13936
14912
  height: ${height}px;
13937
14913
  background: ${cs.bg};
13938
- border: ${border_width}px solid ${cs.border};
14914
+ border: ${border_width}px ${border_style} ${cs.border};
14915
+ border-left: 6px solid ${cs.accent};
13939
14916
  border-radius: 9px;
13940
14917
  box-shadow: ${cs.shadow};
13941
14918
  overflow: hidden;
@@ -13944,48 +14921,29 @@ function build_gobj_nodes(gobj, target_gobj, nodes, edges, parent_id, is_root, c
13944
14921
  display: flex;
13945
14922
  flex-direction: column;
13946
14923
  justify-content: center;
13947
- padding: 5px 9px;
13948
- gap: 1px;
14924
+ padding: 6px 9px;
14925
+ gap: 2px;
14926
+ opacity: ${d.disabled ? "0.72" : "1"};
13949
14927
  cursor: pointer;
13950
- " title="${(0, _yuneta_gobj_js.escapeHtml)(gclass_name + (instance_name ? "^" + instance_name : ""))}">
14928
+ " title="${(0, _yuneta_gobj_js.escapeHtml)(card_title)}">
13951
14929
  <div style="flex:0 0 auto; display:flex; align-items:center; gap:6px; overflow:hidden;">
13952
- <span style="flex:1 1 auto; min-width:0; font-size:12px; line-height:1.25; font-weight:600; color:${cs.title}; white-space:nowrap; overflow:hidden; text-overflow:ellipsis;">${header_label}</span>
14930
+ <span style="flex:1 1 auto; min-width:0; font-size:12px; line-height:1.25; font-weight:700; color:${cs.title}; white-space:nowrap; overflow:hidden; text-overflow:ellipsis;">${header_label}</span>
13953
14931
  ${toggle_html}
13954
14932
  </div>
13955
14933
  <div style="flex:0 0 auto; font-size:12px; line-height:1.25; color:${cs.sub}; white-space:nowrap; overflow:hidden; text-overflow:ellipsis;">${name_label || "&nbsp;"}</div>
13956
14934
  <div style="flex:0 0 auto; display:flex; justify-content:space-between; align-items:center; gap:6px; overflow:hidden;">
13957
- <span style="flex:1 1 auto; min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;">${badges_html}</span>
13958
- <span style="flex:0 0 auto; display:flex; align-items:center; gap:4px; color:${state_color}; font-weight:600; font-size:11px; white-space:nowrap;">
13959
- <span style="width:7px; height:7px; border-radius:50%; background:${state_color};"></span>${state_label || status_text}
13960
- </span>
14935
+ <span style="flex:1 1 auto; min-width:0; font-size:10px; line-height:1.25; color:${cs.sub}; white-space:nowrap; overflow:hidden; text-overflow:ellipsis;">${state_label || "&nbsp;"}</span>
14936
+ ${render_status_html(d, ss, labels, true)}
13961
14937
  </div>
14938
+ <div style="flex:0 0 auto; overflow:hidden; white-space:nowrap; text-overflow:ellipsis;">${render_badges_html(d, cs, labels)}</div>
13962
14939
  </div>`;
13963
14940
  }
13964
- let parent_gobj = (0, _yuneta_gobj_js.gobj_parent)(target_gobj);
13965
- let node_data = {
13966
- gclass: gclass_name,
13967
- name: instance_name,
13968
- short_name: (0, _yuneta_gobj_js.gobj_short_name)(target_gobj),
13969
- full_name,
13970
- parent_short_name: parent_gobj ? (0, _yuneta_gobj_js.gobj_short_name)(parent_gobj) : "",
13971
- state,
13972
- category,
13973
- is_yuno: is_root,
13974
- is_service: (0, _yuneta_gobj_js.gobj_is_service)(target_gobj),
13975
- is_pure_child: (0, _yuneta_gobj_js.gobj_is_pure_child)(target_gobj),
13976
- is_volatil: (0, _yuneta_gobj_js.gobj_is_volatil)(target_gobj),
13977
- is_running: running,
13978
- is_playing: playing,
13979
- num_children,
13980
- is_collapsed: collapsed,
13981
- direct_children
13982
- };
13983
14941
  if (!priv.node_by_id) priv.node_by_id = {};
13984
- priv.node_by_id[node_id] = node_data;
14942
+ priv.node_by_id[node_id] = d;
13985
14943
  let node = {
13986
14944
  id: node_id,
13987
14945
  type: "html",
13988
- data: node_data,
14946
+ data: d,
13989
14947
  style: {
13990
14948
  innerHTML: node_html,
13991
14949
  size: [width, height],
@@ -13998,11 +14956,11 @@ function build_gobj_nodes(gobj, target_gobj, nodes, edges, parent_id, is_root, c
13998
14956
  source: parent_id,
13999
14957
  target: node_id,
14000
14958
  style: {
14001
- stroke: colors.stroke,
14002
- lineWidth: 1
14959
+ stroke: colors.accent,
14960
+ lineWidth: 1.4
14003
14961
  }
14004
14962
  });
14005
- if (!collapsed) for (let child of children) build_gobj_nodes(gobj, child, nodes, edges, node_id, false, compact);
14963
+ if (!collapsed) for (let child of children) build_gobj_nodes(gobj, child, nodes, edges, node_id, compact, labels);
14006
14964
  return node_id;
14007
14965
  }
14008
14966
  /************************************************************
@@ -14019,9 +14977,13 @@ function load_tree(gobj) {
14019
14977
  let priv = gobj.priv;
14020
14978
  let graph = priv.graph;
14021
14979
  if (!graph) return;
14022
- let yuno = (0, _yuneta_gobj_js.gobj_yuno)();
14023
- if (!yuno) return;
14980
+ let root = describe_js_gobj((0, _yuneta_gobj_js.gobj_yuno)(), true);
14981
+ if (!root) {
14982
+ (0, _yuneta_gobj_js.log_error)(`${(0, _yuneta_gobj_js.gobj_short_name)(gobj)}: no yuno to draw`);
14983
+ return;
14984
+ }
14024
14985
  let layout_cfg = get_layout_cfg((0, _yuneta_gobj_js.gobj_read_str_attr)(gobj, "layout"));
14986
+ let labels = node_labels(i18next.t);
14025
14987
  let anchor = priv.pending_anchor || null;
14026
14988
  let preserve_view = !!priv.pending_preserve_view;
14027
14989
  priv.pending_anchor = null;
@@ -14030,7 +14992,7 @@ function load_tree(gobj) {
14030
14992
  priv.node_by_id = {};
14031
14993
  let nodes = [];
14032
14994
  let edges = [];
14033
- build_gobj_nodes(gobj, yuno, nodes, edges, null, true, layout_cfg.compact);
14995
+ build_gobj_nodes(gobj, root, nodes, edges, null, layout_cfg.compact, labels);
14034
14996
  if (nodes.length > 0) {
14035
14997
  let root_id = nodes[0].id;
14036
14998
  graph.setData({
@@ -14039,11 +15001,16 @@ function load_tree(gobj) {
14039
15001
  });
14040
15002
  graph.render().then(() => {
14041
15003
  reanchor(gobj);
15004
+ paint_anchor_mark(gobj);
14042
15005
  if (anchor) {
14043
15006
  restore_view_anchored(gobj, anchor);
14044
15007
  return;
14045
15008
  }
14046
15009
  if (preserve_view) return;
15010
+ if (!priv.restored) {
15011
+ priv.restored = true;
15012
+ if (restore_view_state(gobj)) return;
15013
+ }
14047
15014
  Promise.resolve(graph.zoomTo(1)).then(() => {
14048
15015
  if (!yui_graph_center_on(graph, priv.anchor_id)) yui_graph_center_on(graph, root_id);
14049
15016
  priv.last_zoom = 1;
@@ -14126,46 +15093,17 @@ function refresh_tree(gobj, opts) {
14126
15093
  }
14127
15094
  }
14128
15095
  /************************************************************
14129
- * Rows to show in the popover for a given node data.
14130
- *
14131
- * This is the extension point: to surface a new attribute,
14132
- * collect it into `node_data` in build_gobj_nodes(), then
14133
- * append a `[label, value]` pair here.
14134
- ************************************************************/
14135
- function get_node_info_rows(node_data) {
14136
- let rows = [];
14137
- rows.push([(0, i18next.t)("gclass"), node_data.gclass || "—"]);
14138
- rows.push([(0, i18next.t)("name"), node_data.name || "—"]);
14139
- if (node_data.full_name && node_data.full_name !== node_data.short_name) rows.push([(0, i18next.t)("full name"), node_data.full_name]);
14140
- let role;
14141
- if (node_data.is_yuno) role = "yuno";
14142
- else if (node_data.is_service) role = "service";
14143
- else if (node_data.is_volatil) role = "volatil child";
14144
- else if (node_data.is_pure_child) role = "pure child";
14145
- else role = "child";
14146
- rows.push([(0, i18next.t)("role"), role]);
14147
- let status;
14148
- if (!node_data.is_running) status = "stopped";
14149
- else if (node_data.is_playing) status = "running + playing";
14150
- else status = "running";
14151
- rows.push([(0, i18next.t)("status"), status]);
14152
- rows.push([(0, i18next.t)("state"), node_data.state || "—"]);
14153
- if (node_data.parent_short_name) rows.push([(0, i18next.t)("parent"), node_data.parent_short_name]);
14154
- if (typeof node_data.num_children === "number") {
14155
- let kids = node_data.num_children;
14156
- if (kids > 0 && node_data.is_collapsed) rows.push([(0, i18next.t)("children"), kids + " " + (0, i18next.t)("(collapsed)")]);
14157
- else if (kids > 0) rows.push([(0, i18next.t)("children"), String(kids)]);
14158
- }
14159
- return rows;
14160
- }
14161
- /************************************************************
14162
15096
  * Render popover body from the info rows of a node.
15097
+ *
15098
+ * The rows come from gobj_tree_model.js, so a node of the
15099
+ * browser yuno and a node of a backend one are read out with
15100
+ * the same words in the same order.
14163
15101
  ************************************************************/
14164
15102
  function render_popover_body(gobj, node_data) {
14165
15103
  let $body = gobj.priv.$popover_body;
14166
15104
  if (!$body) return;
14167
- $body.innerHTML = get_node_info_rows(node_data).map(([label, value]) => `
14168
- <div style="display:grid; grid-template-columns: 90px 1fr; column-gap:10px; padding: 2px 0;">
15105
+ $body.innerHTML = node_info_rows(node_data, i18next.t).map(([label, value]) => `
15106
+ <div style="display:grid; grid-template-columns: 96px 1fr; column-gap:10px; padding: 2px 0;">
14169
15107
  <span style="color:var(--bulma-text-weak, #6B7280); font-weight:500;">${(0, _yuneta_gobj_js.escapeHtml)(String(label))}</span>
14170
15108
  <span style="color:var(--bulma-text-strong, #1A1A1A); word-break:break-all;">${(0, _yuneta_gobj_js.escapeHtml)(String(value))}</span>
14171
15109
  </div>
@@ -14182,6 +15120,7 @@ function show_popover(gobj, node_data, client_x, client_y) {
14182
15120
  let $title = priv.$popover_title;
14183
15121
  let $container = (0, _yuneta_gobj_js.gobj_read_attr)(gobj, "$container");
14184
15122
  if (!$popover || !$title || !$container) return;
15123
+ priv.popover_node = node_data;
14185
15124
  $title.textContent = node_data.short_name || node_data.gclass + (node_data.name ? "^" + node_data.name : "");
14186
15125
  $title.title = $title.textContent;
14187
15126
  render_popover_body(gobj, node_data);
@@ -14212,6 +15151,7 @@ function hide_popover(gobj) {
14212
15151
  if (priv.$popover) priv.$popover.style.display = "none";
14213
15152
  if (priv.$popover_body) priv.$popover_body.innerHTML = "";
14214
15153
  if (priv.$popover_title) priv.$popover_title.textContent = "";
15154
+ priv.popover_node = null;
14215
15155
  }
14216
15156
  /************************************************************
14217
15157
  * Generic tree layout engine.
@@ -14474,6 +15414,7 @@ var GobjLanesHLayout = class extends _antv_g6.BaseLayout {
14474
15414
  *
14475
15415
  ************************************************************/
14476
15416
  function ac_refresh$3(gobj, event, kw, src) {
15417
+ save_view_state(gobj);
14477
15418
  refresh_tree(gobj);
14478
15419
  return 0;
14479
15420
  }
@@ -14501,6 +15442,8 @@ function ac_change_layout(gobj, event, kw, src) {
14501
15442
  (0, _yuneta_gobj_js.gobj_write_str_attr)(gobj, "layout", new_key);
14502
15443
  if (priv.$layout_select && priv.$layout_select.value !== new_key) priv.$layout_select.value = new_key;
14503
15444
  apply_layout(gobj);
15445
+ save_view_state(gobj);
15446
+ save_view_state(gobj);
14504
15447
  refresh_tree(gobj);
14505
15448
  return 0;
14506
15449
  }
@@ -14520,6 +15463,7 @@ function ac_toggle_collapse(gobj, event, kw, src) {
14520
15463
  priv.collapse_state[node_data.full_name] = "expanded";
14521
15464
  for (let child_info of node_data.direct_children || []) if (child_info.has_grandchildren) priv.collapse_state[child_info.full_name] = "collapsed";
14522
15465
  } else priv.collapse_state[node_data.full_name] = "collapsed";
15466
+ save_view_state(gobj);
14523
15467
  refresh_tree(gobj, { anchor });
14524
15468
  return 0;
14525
15469
  }
@@ -14599,10 +15543,31 @@ function reanchor(gobj) {
14599
15543
  ************************************************************/
14600
15544
  function ac_center_anchor$1(gobj, event, kw, src) {
14601
15545
  let priv = gobj.priv;
15546
+ paint_anchor_mark(gobj);
15547
+ save_view_state(gobj);
14602
15548
  if (priv.anchor_state === "on" && priv.anchor_id) yui_graph_center_on(priv.graph, priv.anchor_id);
14603
15549
  return 0;
14604
15550
  }
14605
15551
  /************************************************************
15552
+ * Mark the card the camera is holding.
15553
+ *
15554
+ * A class on the card's own element and not a G6 node state:
15555
+ * G6 paints no state style on an html node.
15556
+ *
15557
+ * Re-applied after every rebuild, because the cards are new
15558
+ * DOM each time and the mark would otherwise last until the
15559
+ * next expand.
15560
+ ************************************************************/
15561
+ function paint_anchor_mark(gobj) {
15562
+ let priv = gobj.priv;
15563
+ let $container = (0, _yuneta_gobj_js.gobj_read_attr)(gobj, "$container");
15564
+ if (!$container) return;
15565
+ for (let $card of $container.querySelectorAll(".GOBJ_CARD_ANCHORED")) $card.classList.remove("GOBJ_CARD_ANCHORED");
15566
+ if (priv.anchor_state !== "on" || !priv.anchor_name) return;
15567
+ let $it = $container.querySelector(".GOBJ_CARD[data-gobj-name=\"" + CSS.escape(priv.anchor_name) + "\"]");
15568
+ if ($it) $it.classList.add("GOBJ_CARD_ANCHORED");
15569
+ }
15570
+ /************************************************************
14606
15571
  * Reading, or picking the node to anchor.
14607
15572
  ************************************************************/
14608
15573
  function set_picking_mode(gobj, picking) {
@@ -14630,6 +15595,8 @@ function ac_toggle_anchor$1(gobj, event, kw, src) {
14630
15595
  }
14631
15596
  yui_graph_update_anchor((0, _yuneta_gobj_js.gobj_read_attr)(gobj, "$container"), priv.anchor_state);
14632
15597
  set_picking_mode(gobj, priv.anchor_state === "arming");
15598
+ paint_anchor_mark(gobj);
15599
+ save_view_state(gobj);
14633
15600
  return 0;
14634
15601
  }
14635
15602
  /************************************************************
@@ -14713,6 +15680,7 @@ function ac_node_click$3(gobj, event, kw, src) {
14713
15680
  function ac_show$6(gobj, event, kw, src) {
14714
15681
  let priv = gobj.priv;
14715
15682
  let graph = priv.graph;
15683
+ save_view_state(gobj);
14716
15684
  refresh_tree(gobj);
14717
15685
  if (graph) {
14718
15686
  let $canvas = document.getElementById(priv.canvas_id);
@@ -14753,6 +15721,68 @@ function ac_hide$5(gobj, event, kw, src) {
14753
15721
  hide_popover(gobj);
14754
15722
  return 0;
14755
15723
  }
15724
+ /************************************************************
15725
+ * Open the gclass of the node the popover is showing.
15726
+ *
15727
+ * One window at a time: opening a second gclass replaces the
15728
+ * first rather than stacking windows nobody closes.
15729
+ ************************************************************/
15730
+ function ac_open_gclass(gobj, event, kw, src) {
15731
+ let priv = gobj.priv;
15732
+ let node_data = priv.popover_node;
15733
+ if (!node_data || !node_data.gclass) {
15734
+ (0, _yuneta_gobj_js.log_error)(`${(0, _yuneta_gobj_js.gobj_short_name)(gobj)}: EV_OPEN_GCLASS with no node`);
15735
+ return -1;
15736
+ }
15737
+ close_gclass_view(priv.gclass_view);
15738
+ priv.gclass_view = null;
15739
+ priv.gclass_view = open_gclass_view(gobj, node_data.gclass, {
15740
+ title_prefix: (0, _yuneta_gobj_js.gobj_yuno_name)() || "",
15741
+ on_close: () => {
15742
+ if ((0, _yuneta_gobj_js.gobj_is_destroying)(gobj)) return;
15743
+ (0, _yuneta_gobj_js.gobj_send_event)(gobj, "EV_GCLASS_CLOSED", {}, gobj);
15744
+ }
15745
+ });
15746
+ return 0;
15747
+ }
15748
+ /************************************************************
15749
+ * The reader dismissed the gclass window (its own ✕).
15750
+ *
15751
+ * C_YUI_WINDOW.close_window() calls on_close and THEN destroys
15752
+ * itself, so here we only drop our reference -- destroying it
15753
+ * again would destroy it twice.
15754
+ ************************************************************/
15755
+ function ac_gclass_closed(gobj, event, kw, src) {
15756
+ gobj.priv.gclass_view = null;
15757
+ return 0;
15758
+ }
15759
+ /************************************************************
15760
+ * The gclass viewer asked for a subtree.
15761
+ *
15762
+ * It cannot: the gclass description is a COMPLETE document,
15763
+ * it carries no `__collapsed__` stub, so nothing in it is
15764
+ * fetchable. The event is declared because the viewer is
15765
+ * hosted as a child and subscribes its host to everything it
15766
+ * publishes -- and it is logged, because arriving here means
15767
+ * the document was not the one we built.
15768
+ ************************************************************/
15769
+ function ac_gclass_expand_path(gobj, event, kw, src) {
15770
+ (0, _yuneta_gobj_js.log_error)(`${(0, _yuneta_gobj_js.gobj_short_name)(gobj)}: EV_EXPAND_PATH on a complete document: ${kw && kw.path}`);
15771
+ return -1;
15772
+ }
15773
+ /************************************************************
15774
+ * The app switched language.
15775
+ *
15776
+ * A rebuild, not a re-translation: the words live inside the
15777
+ * innerHTML of G6 nodes, where refresh_language() cannot go.
15778
+ ************************************************************/
15779
+ function ac_language_changed$3(gobj, event, kw, src) {
15780
+ let $container = (0, _yuneta_gobj_js.gobj_read_attr)(gobj, "$container");
15781
+ if ($container) (0, _yuneta_gobj_js.refresh_language)($container, i18next.t);
15782
+ hide_popover(gobj);
15783
+ refresh_tree(gobj);
15784
+ return 0;
15785
+ }
14756
15786
  /***************************
14757
15787
  * FSM
14758
15788
  ***************************/
@@ -14786,7 +15816,11 @@ function create_gclass$9(gclass_name) {
14786
15816
  ["EV_CENTER_ANCHOR", 0],
14787
15817
  ["EV_RESIZE", 0],
14788
15818
  ["EV_SHOW", 0],
14789
- ["EV_HIDE", 0]
15819
+ ["EV_HIDE", 0],
15820
+ ["EV_OPEN_GCLASS", 0],
15821
+ ["EV_GCLASS_CLOSED", 0],
15822
+ ["EV_EXPAND_PATH", 0],
15823
+ ["EV_LANGUAGE_CHANGED", 0]
14790
15824
  ], [["ST_IDLE", [
14791
15825
  [
14792
15826
  "EV_REFRESH",
@@ -14867,6 +15901,26 @@ function create_gclass$9(gclass_name) {
14867
15901
  "EV_HIDE",
14868
15902
  ac_hide$5,
14869
15903
  null
15904
+ ],
15905
+ [
15906
+ "EV_OPEN_GCLASS",
15907
+ ac_open_gclass,
15908
+ null
15909
+ ],
15910
+ [
15911
+ "EV_GCLASS_CLOSED",
15912
+ ac_gclass_closed,
15913
+ null
15914
+ ],
15915
+ [
15916
+ "EV_EXPAND_PATH",
15917
+ ac_gclass_expand_path,
15918
+ null
15919
+ ],
15920
+ [
15921
+ "EV_LANGUAGE_CHANGED",
15922
+ ac_language_changed$3,
15923
+ null
14870
15924
  ]
14871
15925
  ]]], gmt$9, 0, attrs_table$9, PRIVATE_DATA$9, 0, 0, 0, 0);
14872
15926
  if (!__gclass__$9) return -1;
@@ -49031,7 +50085,7 @@ function configure_toolbar(gobj) {
49031
50085
  {
49032
50086
  id: "g6-icon-anchor",
49033
50087
  value: "anchor",
49034
- className: "EV_TOGGLE_ANCHOR" + (priv.anchor_state !== "off" ? " pressed_state" : "") + (priv.anchor_state === "arming" ? " color_pending_state" : ""),
50088
+ className: "EV_TOGGLE_ANCHOR" + (priv.anchor_state === "on" ? " pressed_state" : "") + (priv.anchor_state === "arming" ? " color_pending_state" : ""),
49035
50089
  title: anchor_title(priv.anchor_state)
49036
50090
  }
49037
50091
  ];
@@ -51940,15 +52994,15 @@ function refresh_minimap(gobj) {
51940
52994
  * same three-way choice — it lived inline in the theme refresh and is
51941
52995
  * shared now. Returns null for a node that carries no desc.
51942
52996
  ************************************************************/
51943
- function node_innerHTML_of(nd, theme, highlight, selected) {
52997
+ function node_innerHTML_of(nd, theme, highlight, selected, anchored) {
51944
52998
  if (!nd || !nd.data || !nd.data.desc) return null;
51945
52999
  let desc = nd.data.desc;
51946
53000
  let record = nd.data.record || {};
51947
53001
  let label = node_label(desc, record);
51948
53002
  switch (desc.node_treedb_type) {
51949
- case "child": return build_chip_innerHTML(desc.color, theme, record.icon, label, record.id, highlight, selected);
51950
- case "extended": return build_node_innerHTML(desc.color, theme, record.icon, label, desc.topic_name, true, record.id, highlight, selected);
51951
- case "hierarchical": return build_node_innerHTML(desc.color, theme, record.icon, label, desc.topic_name, false, record.id, highlight, selected);
53003
+ case "child": return build_chip_innerHTML(desc.color, theme, record.icon, label, record.id, highlight, selected, anchored);
53004
+ case "extended": return build_node_innerHTML(desc.color, theme, record.icon, label, desc.topic_name, true, record.id, highlight, selected, anchored);
53005
+ case "hierarchical": return build_node_innerHTML(desc.color, theme, record.icon, label, desc.topic_name, false, record.id, highlight, selected, anchored);
51952
53006
  }
51953
53007
  return null;
51954
53008
  }
@@ -51969,7 +53023,7 @@ function repaint_cards(gobj, ids) {
51969
53023
  let selected = new Set(priv._selected_paint_ids || []);
51970
53024
  let updates = [];
51971
53025
  for (let id of ids) {
51972
- let html = node_innerHTML_of(graph.getNodeData(id), priv.theme, focus.has(id), selected.has(id));
53026
+ let html = node_innerHTML_of(graph.getNodeData(id), priv.theme, focus.has(id), selected.has(id), priv.anchor_state === "on" && id === priv.anchor_id);
51973
53027
  if (html !== null) updates.push({
51974
53028
  id,
51975
53029
  style: { innerHTML: html }
@@ -52043,6 +53097,23 @@ function refresh_default_edges_theme(gobj, theme) {
52043
53097
  * it, which is also the order that reads correctly when only one
52044
53098
  * of them is on.
52045
53099
  ************************************************************/
53100
+ /************************************************************
53101
+ * The mark of the card the CAMERA is holding.
53102
+ *
53103
+ * An outline and not another ring, and DASHED: the two rings
53104
+ * already mean things here -- amber is a find match or a
53105
+ * focused topic, blue is the selection -- and a third solid
53106
+ * ring would be a third thing to learn in the same channel.
53107
+ * A dash is a different channel, so it reads beside either of
53108
+ * them, and it says "provisional" the way a camera state is.
53109
+ *
53110
+ * The same dash the other two viewers draw: this is one
53111
+ * feature with one control, and it should not be three
53112
+ * drawings.
53113
+ ************************************************************/
53114
+ function anchor_outline(anchored) {
53115
+ return anchored ? "outline: 3px dashed #fa8c16; outline-offset: 2px;" : "";
53116
+ }
52046
53117
  function ring_shadow(highlight, selected, base_shadow) {
52047
53118
  let rings = [];
52048
53119
  if (highlight) rings.push(`0 0 0 4px ${HIGHLIGHT_HALO}`);
@@ -52057,7 +53128,7 @@ function ring_shadow(highlight, selected, base_shadow) {
52057
53128
  * but lighter (1px border, no shadow, single line). The name is
52058
53129
  * always legible (ellipsis + native title tooltip on overflow).
52059
53130
  ************************************************************/
52060
- function build_chip_innerHTML(color, theme, icon, label, key, highlight, selected) {
53131
+ function build_chip_innerHTML(color, theme, icon, label, key, highlight, selected, anchored) {
52061
53132
  let title = key || label;
52062
53133
  let dark = theme === "dark";
52063
53134
  let bg = dark ? `color-mix(in srgb, ${color} 30%, #2c3542)` : `color-mix(in srgb, ${color} 10%, ${dark ? "#1b2230" : "#ffffff"})`;
@@ -52070,13 +53141,14 @@ function build_chip_innerHTML(color, theme, icon, label, key, highlight, selecte
52070
53141
  margin-right: 6px; flex: 0 0 auto;
52071
53142
  "/>`;
52072
53143
  return `
52073
- <div title="${(0, _yuneta_gobj_js.escapeHtml)(title)}" style="
53144
+ <div class="TREEDB_CARD" title="${(0, _yuneta_gobj_js.escapeHtml)(title)}" style="
52074
53145
  box-sizing: border-box;
52075
53146
  width: 100%;
52076
53147
  height: 100%;
52077
53148
  background: ${bg};
52078
53149
  border: ${highlight ? "3px" : "1px"} solid ${border};
52079
53150
  ${ring_shadow(highlight, selected, "")}
53151
+ ${anchor_outline(anchored)}
52080
53152
  border-radius: 8px;
52081
53153
  color: ${text_color};
52082
53154
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
@@ -52102,7 +53174,7 @@ function build_chip_innerHTML(color, theme, icon, label, key, highlight, selecte
52102
53174
  * colour is kept (per-topic differentiation) but softened via
52103
53175
  * color-mix instead of a harsh saturated fill. Theme-aware.
52104
53176
  ************************************************************/
52105
- function build_node_innerHTML(color, theme, icon, label, topic_name, structural, key, highlight, selected) {
53177
+ function build_node_innerHTML(color, theme, icon, label, topic_name, structural, key, highlight, selected, anchored) {
52106
53178
  let title = key || label;
52107
53179
  let dark = theme === "dark";
52108
53180
  let surface = dark ? "#1b2230" : "#ffffff";
@@ -52137,7 +53209,7 @@ function build_node_innerHTML(color, theme, icon, label, topic_name, structural,
52137
53209
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
52138
53210
  ">${(0, _yuneta_gobj_js.escapeHtml)(topic_name)}</div>`;
52139
53211
  return `
52140
- <div title="${(0, _yuneta_gobj_js.escapeHtml)(title)}" style="
53212
+ <div class="TREEDB_CARD" title="${(0, _yuneta_gobj_js.escapeHtml)(title)}" style="
52141
53213
  box-sizing: border-box;
52142
53214
  width: 100%;
52143
53215
  height: 100%;
@@ -52145,6 +53217,7 @@ function build_node_innerHTML(color, theme, icon, label, topic_name, structural,
52145
53217
  border: ${highlight ? "3px" : "1.5px"} ${border_style} ${border};
52146
53218
  border-radius: 10px;
52147
53219
  ${ring_shadow(highlight, selected, shadow)}
53220
+ ${anchor_outline(anchored)}
52148
53221
  color: ${title_color};
52149
53222
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
52150
53223
  display: flex;
@@ -53128,12 +54201,14 @@ function after_camera(gobj, moved) {
53128
54201
  ************************************************************/
53129
54202
  function ac_toggle_anchor(gobj, event, kw, src) {
53130
54203
  let priv = gobj.priv;
54204
+ let was = priv.anchor_id;
53131
54205
  if (priv.anchor_state === "off") priv.anchor_state = "arming";
53132
54206
  else {
53133
54207
  priv.anchor_state = "off";
53134
54208
  priv.anchor_id = "";
53135
54209
  }
53136
54210
  update_toolbar(gobj);
54211
+ if (was) repaint_cards(gobj, /* @__PURE__ */ new Set([was]));
53137
54212
  return 0;
53138
54213
  }
53139
54214
  /************************************************************
@@ -53311,6 +54386,7 @@ function ac_node_click$2(gobj, event, kw, src) {
53311
54386
  priv.anchor_id = node_id;
53312
54387
  priv.anchor_state = "on";
53313
54388
  update_toolbar(gobj);
54389
+ repaint_cards(gobj, /* @__PURE__ */ new Set([node_id]));
53314
54390
  (0, _yuneta_gobj_js.gobj_post_event)(gobj, "EV_CENTER_ANCHOR", {}, gobj);
53315
54391
  return 0;
53316
54392
  }