@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.
@@ -1,4 +1,4 @@
1
- import { SDATA, SDATA_END, clean_name, createElement2, createOneHtml, create_template_record, data_type_t, debounce, duplicate_objects, empty_string, escapeHtml, event_flag_t, gclass_create, gclass_find_by_name, gclass_flag_t, getPositionRelativeToBody, gobj_change_state, gobj_command, gobj_create, gobj_create_pure_child, gobj_create_service, gobj_current_state, gobj_destroy, gobj_find_child, gobj_find_service, gobj_full_name, gobj_gclass_name, gobj_is_destroying, gobj_is_playing, gobj_is_pure_child, gobj_is_running, gobj_is_service, gobj_is_volatil, gobj_match_children, gobj_name, gobj_parent, gobj_post_event, gobj_publish_event, gobj_read_attr, gobj_read_bool_attr, gobj_read_integer_attr, gobj_read_pointer_attr, gobj_read_str_attr, gobj_save_persistent_attrs, gobj_send_event, gobj_set_trace_machine_format, gobj_short_name, gobj_start, gobj_start_tree, gobj_stop, gobj_stop_children, gobj_subscribe_event, gobj_unsubscribe_event, gobj_write_attr, gobj_write_bool_attr, gobj_write_integer_attr, gobj_write_str_attr, gobj_yuno, is_array, is_boolean, is_date, is_gobj, is_null, is_number, is_object, is_string, json_deep_copy, json_object_size, json_object_update, json_object_update_missing, json_size, kw_clone_by_keys, kw_flag_t, kw_get_dict, kw_get_dict_value, kw_get_int, kw_get_local_storage_value, kw_get_str, kw_has_key, kw_set_dict_value, kw_set_local_storage_value, kwid_find_one_record, kwid_get_ids, kwid_new_dict, log_error, log_warning, msg_iev_get_stack, parseBoolean, parseSVG, refresh_language, safeSrc, sdata_flag_t, set_console_log_enabled, set_log_callback, sprintf, str_in_list, template_get_field_desc, trace_msg, treedb_decoder_fkey, treedb_encoder_fkey, treedb_get_field_desc, treedb_hook_data_size } from "@yuneta/gobj-js";
1
+ import { SDATA, SDATA_END, clean_name, createElement2, createOneHtml, create_template_record, data_type_t, debounce, duplicate_objects, empty_string, escapeHtml, event_flag_t, gclass_create, gclass_find_by_name, gclass_flag_t, getPositionRelativeToBody, gobj_bottom_gobj, gobj_change_state, gobj_command, gobj_create, gobj_create_pure_child, gobj_create_service, gobj_current_state, gobj_destroy, gobj_find_child, gobj_find_service, gobj_full_name, gobj_gclass_name, gobj_is_destroying, gobj_is_playing, gobj_is_pure_child, gobj_is_running, gobj_is_service, gobj_is_volatil, gobj_match_children, gobj_name, gobj_parent, gobj_post_event, gobj_publish_event, gobj_read_attr, gobj_read_bool_attr, gobj_read_integer_attr, gobj_read_pointer_attr, gobj_read_str_attr, gobj_save_persistent_attrs, gobj_send_event, gobj_set_trace_machine_format, gobj_short_name, gobj_start, gobj_start_tree, gobj_stop, gobj_stop_children, gobj_subscribe_event, gobj_unsubscribe_event, gobj_write_attr, gobj_write_bool_attr, gobj_write_integer_attr, gobj_write_str_attr, gobj_yuno, gobj_yuno_name, is_array, is_boolean, is_date, is_gobj, is_null, is_number, is_object, is_string, json_deep_copy, json_object_size, json_object_update, json_object_update_missing, json_size, kw_clone_by_keys, kw_flag_t, kw_get_dict, kw_get_dict_value, kw_get_int, kw_get_local_storage_value, kw_get_str, kw_has_key, kw_set_dict_value, kw_set_local_storage_value, kwid_find_one_record, kwid_get_ids, kwid_new_dict, log_error, log_warning, msg_iev_get_stack, parseBoolean, parseSVG, refresh_language, safeSrc, sdata_flag_t, set_console_log_enabled, set_log_callback, sprintf, str_in_list, template_get_field_desc, trace_msg, treedb_decoder_fkey, treedb_encoder_fkey, treedb_get_field_desc, treedb_hook_data_size } from "@yuneta/gobj-js";
2
2
  import i18next, { t } from "i18next";
3
3
  import TomSelect from "tom-select";
4
4
  import { createJSONEditor } from "vanilla-jsoneditor";
@@ -3513,7 +3513,7 @@ function composed_title(gobj) {
3513
3513
  * Below the mobile breakpoint the window is a full-screen
3514
3514
  * sheet (no float / drag / resize).
3515
3515
  ************************************************************/
3516
- function is_mobile$3() {
3516
+ function is_mobile$4() {
3517
3517
  return (window.innerWidth || document.documentElement.offsetWidth) <= MOBILE_MAX;
3518
3518
  }
3519
3519
  /************************************************************
@@ -3546,7 +3546,7 @@ function do_minimize(gobj) {
3546
3546
  ************************************************************/
3547
3547
  function build_ui$15(gobj) {
3548
3548
  ensure_window_style();
3549
- let mobile = is_mobile$3();
3549
+ let mobile = is_mobile$4();
3550
3550
  let header = gobj_read_attr(gobj, "header");
3551
3551
  if (is_gobj(header)) header = gobj_read_attr(header, "$container");
3552
3552
  if (!header) header = build_default_header(gobj);
@@ -3884,7 +3884,7 @@ function min$1(gobj) {
3884
3884
  * handlers moving
3885
3885
  ************************************************************/
3886
3886
  function mvStart(gobj, evt) {
3887
- if (is_mobile$3()) return;
3887
+ if (is_mobile$4()) return;
3888
3888
  let $container = gobj_read_attr(gobj, "$container");
3889
3889
  let window_rect = $container.getBoundingClientRect();
3890
3890
  let x = evt.screenX;
@@ -4043,7 +4043,7 @@ function do_center(gobj, x, y, width, height) {
4043
4043
  function handleResize(gobj) {
4044
4044
  let $container = gobj_read_attr(gobj, "$container");
4045
4045
  if (!$container) return;
4046
- if (is_mobile$3()) {
4046
+ if (is_mobile$4()) {
4047
4047
  $container.classList.add("is-mobile-sheet");
4048
4048
  let vw = window.innerWidth || document.documentElement.offsetWidth;
4049
4049
  let vh = window.innerHeight || document.documentElement.offsetHeight;
@@ -9706,6 +9706,42 @@ function yui_graph_update_anchor($container, state) {
9706
9706
  * nothing to centre right now".
9707
9707
  ************************************************************/
9708
9708
  var __centring__ = /* @__PURE__ */ new WeakMap();
9709
+ /************************************************************
9710
+ * Put `node_id` at a GIVEN point of the viewport.
9711
+ *
9712
+ * What "the graph does not jump when I fold something" is
9713
+ * made of: read where the card is, rebuild the layout, put
9714
+ * that card back where it was. Everything that did not move
9715
+ * in the layout stays put with it.
9716
+ ************************************************************/
9717
+ async function yui_graph_place_at(graph, node_id, viewport_xy) {
9718
+ if (!graph || !node_id || !viewport_xy) return false;
9719
+ try {
9720
+ if (typeof graph.getElementPosition !== "function" || typeof graph.getCanvasCenter !== "function" || typeof graph.translateTo !== "function") return false;
9721
+ let z = graph.getZoom() || 1;
9722
+ let c = graph.getCanvasCenter();
9723
+ let n = graph.getElementPosition(node_id);
9724
+ await graph.translateTo([(c[0] - n[0]) * z + (viewport_xy[0] - c[0]), (c[1] - n[1]) * z + (viewport_xy[1] - c[1])]);
9725
+ return true;
9726
+ } catch (e) {
9727
+ return false;
9728
+ }
9729
+ }
9730
+ /************************************************************
9731
+ * Where `node_id` is right now, in viewport pixels.
9732
+ *
9733
+ * The pair of the call above: read it before a rebuild, hand
9734
+ * it back after, and the node has not moved on screen even
9735
+ * though the layout under it changed.
9736
+ ************************************************************/
9737
+ function yui_graph_viewport_of(graph, node_id) {
9738
+ if (!graph || !node_id) return null;
9739
+ try {
9740
+ return graph.getViewportByCanvas(graph.getElementPosition(node_id));
9741
+ } catch (e) {
9742
+ return null;
9743
+ }
9744
+ }
9709
9745
  async function yui_graph_center_on(graph, node_id) {
9710
9746
  if (!graph || !node_id) return false;
9711
9747
  let st = __centring__.get(graph);
@@ -10576,6 +10612,7 @@ var PRIVATE_DATA$11 = {
10576
10612
  _resize_raf: 0,
10577
10613
  search: "",
10578
10614
  collapsed: null,
10615
+ reserved: null,
10579
10616
  match_count: 0,
10580
10617
  find_timer: null,
10581
10618
  fold_listener: null,
@@ -10587,7 +10624,8 @@ var PRIVATE_DATA$11 = {
10587
10624
  anchor_path: "",
10588
10625
  anchor_state: "off",
10589
10626
  last_zoom: 1,
10590
- center_posted: false
10627
+ center_posted: false,
10628
+ pending_keep: null
10591
10629
  };
10592
10630
  var __gclass__$11 = null;
10593
10631
  var __layout_registered__$1 = false;
@@ -10697,6 +10735,7 @@ function type_color(type, dark) {
10697
10735
  function mt_create$11(gobj) {
10698
10736
  let priv = gobj.priv;
10699
10737
  priv.collapsed = /* @__PURE__ */ new Set();
10738
+ priv.reserved = /* @__PURE__ */ new Map();
10700
10739
  priv.search = "";
10701
10740
  priv.canvas_id = "json-canvas-" + clean_name(gobj_name(gobj));
10702
10741
  gobj_write_str_attr(gobj, "canvas_id", priv.canvas_id);
@@ -10796,7 +10835,11 @@ function build_ui$10(gobj) {
10796
10835
  evt.stopPropagation();
10797
10836
  evt.preventDefault();
10798
10837
  if (evt.type !== "click") return;
10799
- gobj_send_event(gobj, "EV_TOGGLE_FOLD", { path: $handle.getAttribute("data-fold-path") }, gobj);
10838
+ let $card = $handle.closest ? $handle.closest(".JSON_CARD") : null;
10839
+ gobj_send_event(gobj, "EV_TOGGLE_FOLD", {
10840
+ path: $handle.getAttribute("data-fold-path"),
10841
+ keep: $card ? $card.getAttribute("data-json-path") : ""
10842
+ }, gobj);
10800
10843
  };
10801
10844
  for (let type of FOLD_SWALLOWED_EVENTS) $mount.addEventListener(type, priv.fold_listener, true);
10802
10845
  } else log_error(`${gobj_short_name(gobj)}: no canvas mount, fold handles are dead`);
@@ -11077,6 +11120,8 @@ var BEHAVIORS_READING$1 = [
11077
11120
  var BEHAVIORS_PICKING$1 = ["drag-canvas", ZOOM_CANVAS$1];
11078
11121
  var CARD_HEADER_H$1 = 24;
11079
11122
  var CARD_ROW_H$1 = 22;
11123
+ var H_GAP = 60;
11124
+ var V_GAP = 50;
11080
11125
  function fold_toggle_html(path, foldable, folded, branches, colors) {
11081
11126
  if (!foldable) return FOLD_SPACER;
11082
11127
  let label = folded ? "+" + branches : "&#8722;";
@@ -11198,7 +11243,10 @@ function build_json_nodes(gobj, path, kw, nodes, edges, parent_id, parent_port)
11198
11243
  lines.push(build_cell_html(key, value, type, dark, matched, fold));
11199
11244
  }
11200
11245
  if (parent_id && is_pure_collection(kw)) {
11201
- if (priv.collapsed.has(path)) return parent_id;
11246
+ if (priv.collapsed.has(path)) {
11247
+ push_fold_phantom(gobj, path, parent_id, parent_port, nodes, edges);
11248
+ return parent_id;
11249
+ }
11202
11250
  for (let pending of pending_complex) build_json_nodes(gobj, add_segment(path, pending.key), pending.value, nodes, edges, parent_id, parent_port);
11203
11251
  return parent_id;
11204
11252
  }
@@ -11284,7 +11332,10 @@ function build_json_nodes(gobj, path, kw, nodes, edges, parent_id, parent_port)
11284
11332
  if (parent_port) edge.style.sourcePort = parent_port;
11285
11333
  edges.push(edge);
11286
11334
  }
11287
- if (folded) return group_id;
11335
+ if (folded) {
11336
+ push_fold_phantom(gobj, path, group_id, "", nodes, edges);
11337
+ return group_id;
11338
+ }
11288
11339
  for (let pending of pending_complex) build_json_nodes(gobj, add_segment(path, pending.key), pending.value, nodes, edges, group_id, pending.port);
11289
11340
  return group_id;
11290
11341
  }
@@ -11315,8 +11366,16 @@ function load_json(gobj) {
11315
11366
  });
11316
11367
  graph.render().then(() => {
11317
11368
  reanchor$1(gobj);
11318
- paint_anchor_mark(gobj);
11369
+ paint_anchor_mark$1(gobj);
11319
11370
  if (preserve_view) {
11371
+ let keep = priv.pending_keep;
11372
+ priv.pending_keep = null;
11373
+ if (keep) {
11374
+ for (let n of nodes) if (n.data && n.data.path === keep.path) {
11375
+ yui_graph_place_at(graph, n.id, [keep.x, keep.y]);
11376
+ break;
11377
+ }
11378
+ }
11320
11379
  yui_graph_update_zoom(gobj_read_attr(gobj, "$container"), priv.graph);
11321
11380
  return;
11322
11381
  }
@@ -11368,6 +11427,108 @@ function update_find_result(gobj) {
11368
11427
  priv.$find_result.classList.remove("is-hidden");
11369
11428
  }
11370
11429
  /************************************************************
11430
+ * The invisible child a fold leaves behind.
11431
+ *
11432
+ * It is the whole of "nothing moves when I fold": the layout
11433
+ * is a plain tree that centres a parent over its children, so
11434
+ * the only way to keep the survivors still is to leave the
11435
+ * space where it was, in the order it was, as a CHILD. A width
11436
+ * reserved as a number would still have to be placed, and
11437
+ * appending it moves every sibling by half of it.
11438
+ *
11439
+ * A `rect` and not an `html` node: nothing is drawn, and a
11440
+ * DOM element per fold would be a card nobody can see but the
11441
+ * Inspector would show.
11442
+ ************************************************************/
11443
+ function push_fold_phantom(gobj, path, parent_id, parent_port, nodes, edges) {
11444
+ let width = gobj.priv.reserved.get(path) || 0;
11445
+ if (!(width > 0) || !parent_id) return;
11446
+ let id = gen_node_id$1(gobj);
11447
+ nodes.push({
11448
+ id,
11449
+ type: "rect",
11450
+ data: {
11451
+ path,
11452
+ phantom: true
11453
+ },
11454
+ style: {
11455
+ size: [width, 1],
11456
+ fill: "transparent",
11457
+ stroke: "transparent",
11458
+ opacity: 0,
11459
+ pointerEvents: "none"
11460
+ }
11461
+ });
11462
+ edges.push({
11463
+ source: parent_id,
11464
+ target: id,
11465
+ style: {
11466
+ opacity: 0,
11467
+ endArrow: false,
11468
+ sourcePort: parent_port || void 0
11469
+ }
11470
+ });
11471
+ }
11472
+ /************************************************************
11473
+ * The width the children a fold is about to hide are taking
11474
+ * RIGHT NOW, measured with the layout's own numbers.
11475
+ *
11476
+ * `card_path` is the card that will lose them; `under` limits
11477
+ * it to the ones hanging from one key of that card, which is
11478
+ * the case of a container that has no card of its own -- its
11479
+ * children hang from the parent, so its fold is a hole in the
11480
+ * PARENT.
11481
+ ************************************************************/
11482
+ function group_width_of(gobj, card_path, under) {
11483
+ let priv = gobj.priv;
11484
+ if (!priv.graph) return 0;
11485
+ try {
11486
+ let by_id = {};
11487
+ let card_id = "";
11488
+ for (let node of priv.graph.getNodeData()) {
11489
+ if (!node || !node.data) continue;
11490
+ by_id[node.id] = node;
11491
+ if (node.data.path === card_path) card_id = node.id;
11492
+ }
11493
+ if (!card_id) return 0;
11494
+ let prefix = under ? under + "`" : "";
11495
+ let sum = 0;
11496
+ let n = 0;
11497
+ for (let edge of priv.graph.getEdgeData()) {
11498
+ if (!edge || edge.source !== card_id) continue;
11499
+ let kid = by_id[edge.target];
11500
+ if (!kid) continue;
11501
+ if (prefix && String(kid.data.path).indexOf(prefix) !== 0) continue;
11502
+ sum += kid.data.subtree_width || 0;
11503
+ n++;
11504
+ }
11505
+ return n ? sum + H_GAP * (n - 1) : 0;
11506
+ } catch (e) {
11507
+ return 0;
11508
+ }
11509
+ }
11510
+ /************************************************************
11511
+ * The screen spot of the card at `path`, to be given back
11512
+ * after the rebuild. Null when it has no card of its own --
11513
+ * a pure collection -- and then nothing anchors and the
11514
+ * camera simply stays where it was.
11515
+ ************************************************************/
11516
+ function keep_of(gobj, path) {
11517
+ let priv = gobj.priv;
11518
+ if (empty_string(path) || !priv.graph) return null;
11519
+ try {
11520
+ for (let node of priv.graph.getNodeData()) if (node && node.data && node.data.path === path) {
11521
+ let vp = yui_graph_viewport_of(priv.graph, node.id);
11522
+ return vp ? {
11523
+ path,
11524
+ x: vp[0],
11525
+ y: vp[1]
11526
+ } : null;
11527
+ }
11528
+ } catch (e) {}
11529
+ return null;
11530
+ }
11531
+ /************************************************************
11371
11532
  * Clear and reload.
11372
11533
  * {preserve_view: true}
11373
11534
  * keep the current zoom and translate (no fitView) —
@@ -11409,8 +11570,6 @@ var JsonTreeLayout = class extends BaseLayout {
11409
11570
  h
11410
11571
  };
11411
11572
  }
11412
- const H_GAP = 60;
11413
- const V_GAP = 50;
11414
11573
  let subtree_widths = {};
11415
11574
  function calc_subtree_width(node_id) {
11416
11575
  let kids = children_map[node_id] || [];
@@ -11427,6 +11586,7 @@ var JsonTreeLayout = class extends BaseLayout {
11427
11586
  return subtree_widths[node_id];
11428
11587
  }
11429
11588
  for (let root of roots) calc_subtree_width(root.id);
11589
+ for (let node of nodes) if (node.data) node.data.subtree_width = subtree_widths[node.id];
11430
11590
  let positions = {};
11431
11591
  function position_node(node_id, x, y) {
11432
11592
  positions[node_id] = {
@@ -11474,9 +11634,16 @@ var JsonTreeLayout = class extends BaseLayout {
11474
11634
  /************************************************************
11475
11635
  *
11476
11636
  ************************************************************/
11637
+ function priv_reset_folds(gobj) {
11638
+ gobj.priv.collapsed.clear();
11639
+ gobj.priv.reserved.clear();
11640
+ }
11477
11641
  function ac_load_data$1(gobj, event, kw, src) {
11478
11642
  if (kw.path !== void 0) gobj_write_str_attr(gobj, "path", kw.path);
11479
- if (kw.data !== void 0) gobj_write_attr(gobj, "json_data", kw.data);
11643
+ if (kw.data !== void 0) {
11644
+ gobj_write_attr(gobj, "json_data", kw.data);
11645
+ priv_reset_folds(gobj);
11646
+ }
11480
11647
  refresh_json(gobj);
11481
11648
  return 0;
11482
11649
  }
@@ -11531,8 +11698,16 @@ function ac_toggle_fold(gobj, event, kw, src) {
11531
11698
  log_error(`${gobj_short_name(gobj)}: EV_TOGGLE_FOLD with no path`);
11532
11699
  return -1;
11533
11700
  }
11534
- if (priv.collapsed.has(path)) priv.collapsed.delete(path);
11535
- else priv.collapsed.add(path);
11701
+ let card = kw && kw.keep || path;
11702
+ if (priv.collapsed.has(path)) {
11703
+ priv.collapsed.delete(path);
11704
+ priv.reserved.delete(path);
11705
+ } else {
11706
+ let w = group_width_of(gobj, card, card === path ? "" : path);
11707
+ priv.collapsed.add(path);
11708
+ if (w > 0) priv.reserved.set(path, w);
11709
+ }
11710
+ priv.pending_keep = keep_of(gobj, kw && kw.keep || path);
11536
11711
  refresh_json(gobj, { preserve_view: true });
11537
11712
  update_find_result(gobj);
11538
11713
  return 0;
@@ -11559,6 +11734,7 @@ function ac_expand_all$2(gobj, event, kw, src) {
11559
11734
  let priv = gobj.priv;
11560
11735
  if (priv.collapsed.size === 0) return 0;
11561
11736
  priv.collapsed.clear();
11737
+ priv.reserved.clear();
11562
11738
  refresh_json(gobj);
11563
11739
  update_find_result(gobj);
11564
11740
  return 0;
@@ -11575,6 +11751,7 @@ function ac_collapse_all$2(gobj, event, kw, src) {
11575
11751
  if (empty_string(path)) path = "`";
11576
11752
  collect_foldable_paths(json_data, path, paths, true);
11577
11753
  priv.collapsed = new Set(paths);
11754
+ priv.reserved.clear();
11578
11755
  refresh_json(gobj);
11579
11756
  update_find_result(gobj);
11580
11757
  return 0;
@@ -11664,7 +11841,7 @@ function set_picking_mode$1(gobj, picking) {
11664
11841
  * each time and the mark would otherwise survive only until the
11665
11842
  * next fold.
11666
11843
  ************************************************************/
11667
- function paint_anchor_mark(gobj) {
11844
+ function paint_anchor_mark$1(gobj) {
11668
11845
  let priv = gobj.priv;
11669
11846
  let $container = gobj_read_attr(gobj, "$container");
11670
11847
  if (!$container) return;
@@ -11679,7 +11856,7 @@ function paint_anchor_mark(gobj) {
11679
11856
  function ac_center_anchor$2(gobj, event, kw, src) {
11680
11857
  let priv = gobj.priv;
11681
11858
  priv.center_posted = false;
11682
- paint_anchor_mark(gobj);
11859
+ paint_anchor_mark$1(gobj);
11683
11860
  if (priv.anchor_state === "on" && priv.anchor_id) yui_graph_center_on(priv.graph, priv.anchor_id);
11684
11861
  return 0;
11685
11862
  }
@@ -11700,7 +11877,7 @@ function ac_toggle_anchor$2(gobj, event, kw, src) {
11700
11877
  priv.anchor_path = "";
11701
11878
  }
11702
11879
  yui_graph_update_anchor(gobj_read_attr(gobj, "$container"), priv.anchor_state);
11703
- paint_anchor_mark(gobj);
11880
+ paint_anchor_mark$1(gobj);
11704
11881
  set_picking_mode$1(gobj, priv.anchor_state === "arming");
11705
11882
  return 0;
11706
11883
  }
@@ -13023,7 +13200,7 @@ function ac_json_item_clicked(gobj, event, kw, src) {
13023
13200
  /************************************************************
13024
13201
  * EV_LANGUAGE_CHANGED — re-translate chrome + re-render
13025
13202
  ************************************************************/
13026
- function ac_language_changed$3(gobj, event, kw, src) {
13203
+ function ac_language_changed$4(gobj, event, kw, src) {
13027
13204
  let priv = gobj.priv;
13028
13205
  let $container = gobj_read_attr(gobj, "$container");
13029
13206
  if ($container) {
@@ -13084,7 +13261,7 @@ function create_gclass$10(gclass_name) {
13084
13261
  ],
13085
13262
  [
13086
13263
  "EV_LANGUAGE_CHANGED",
13087
- ac_language_changed$3,
13264
+ ac_language_changed$4,
13088
13265
  null
13089
13266
  ],
13090
13267
  [
@@ -13160,7 +13337,7 @@ function create_gclass$10(gclass_name) {
13160
13337
  ],
13161
13338
  [
13162
13339
  "EV_LANGUAGE_CHANGED",
13163
- ac_language_changed$3,
13340
+ ac_language_changed$4,
13164
13341
  null
13165
13342
  ],
13166
13343
  [
@@ -13210,6 +13387,567 @@ function register_c_yui_json() {
13210
13387
  return create_gclass$10(GCLASS_NAME$10);
13211
13388
  }
13212
13389
  //#endregion
13390
+ //#region src/gobj_tree_model.js
13391
+ /***********************************************************************
13392
+ * gobj_tree_model.js
13393
+ *
13394
+ * The gobj tree of a yuno, as plain data.
13395
+ *
13396
+ * The view (C_YUI_GOBJ_TREE_JS) draws NODE DESCRIPTORS, never
13397
+ * gobjs. There are two producers of a descriptor, because the
13398
+ * view has to show the gobjs of the BACKEND too:
13399
+ *
13400
+ * - describe_js_gobj() the gobj tree of THIS browser yuno
13401
+ * - describe_backend_tree() the answer of the backend command
13402
+ * `view-gobj-tree` of another yuno
13403
+ *
13404
+ * Both answer the SAME shape, and that shape is the backend's:
13405
+ * the field names are the ones `gobj2json()` writes in
13406
+ * `kernel/c/gobj-c/src/gobj.c` (fullname, shortname, gclass,
13407
+ * name, parent, state, running, playing, service, disabled,
13408
+ * volatil, bottom_gobj, gobj_flags, commands, gobj_trace_level).
13409
+ * Anything the browser runtime cannot answer is simply absent,
13410
+ * and every reader here treats absent as "not known", never as
13411
+ * false.
13412
+ *
13413
+ * Copyright (c) 2026, ArtGins.
13414
+ * All Rights Reserved.
13415
+ ***********************************************************************/
13416
+ /************************************************************
13417
+ * The role of a node: "yuno" | "service" | "volatil" | "pure" | "child"
13418
+ ************************************************************/
13419
+ function node_role(d) {
13420
+ if (!d) return "child";
13421
+ if (d.is_yuno) return "yuno";
13422
+ if (d.service) return "service";
13423
+ if (d.volatil) return "volatil";
13424
+ if (d.pure_child) return "pure";
13425
+ return "child";
13426
+ }
13427
+ /************************************************************
13428
+ * What the node is doing: "stopped" | "running" | "playing"
13429
+ ************************************************************/
13430
+ function node_status(d) {
13431
+ if (!d || !d.running) return "stopped";
13432
+ return d.playing ? "playing" : "running";
13433
+ }
13434
+ /************************************************************
13435
+ * The i18n key of the status, which is NOT the status itself:
13436
+ * a gobj that runs and does not play is PAUSED to the reader,
13437
+ * and "running" alone never said which of the two it was.
13438
+ ************************************************************/
13439
+ function node_status_key(d) {
13440
+ switch (node_status(d)) {
13441
+ case "playing": return "playing";
13442
+ case "running": return "running (paused)";
13443
+ default: return "stopped";
13444
+ }
13445
+ }
13446
+ /******************************
13447
+ * Producers
13448
+ ******************************/
13449
+ /************************************************************
13450
+ * A descriptor of a LIVE gobj of this browser yuno, children
13451
+ * included (the whole subtree, depth-first).
13452
+ *
13453
+ * `is_root` marks the yuno itself: the JS runtime has no
13454
+ * gobj_flag_yuno test of its own.
13455
+ ************************************************************/
13456
+ function describe_js_gobj(target_gobj, is_root) {
13457
+ if (!target_gobj) return null;
13458
+ let parent_gobj = gobj_parent(target_gobj);
13459
+ let bottom = gobj_bottom_gobj(target_gobj);
13460
+ let d = {
13461
+ gclass: gobj_gclass_name(target_gobj) || "",
13462
+ name: gobj_name(target_gobj) || "",
13463
+ shortname: gobj_short_name(target_gobj) || "",
13464
+ fullname: gobj_full_name(target_gobj) || "",
13465
+ parent: parent_gobj ? gobj_short_name(parent_gobj) || "" : "",
13466
+ state: gobj_current_state(target_gobj) || "",
13467
+ running: !!gobj_is_running(target_gobj),
13468
+ playing: !!gobj_is_playing(target_gobj),
13469
+ service: !!gobj_is_service(target_gobj),
13470
+ volatil: !!gobj_is_volatil(target_gobj),
13471
+ pure_child: !!gobj_is_pure_child(target_gobj),
13472
+ disabled: !!target_gobj.disabled,
13473
+ bottom_gobj: bottom ? gobj_short_name(bottom) || "" : "",
13474
+ commands: !!(target_gobj.gclass && target_gobj.gclass.command_table),
13475
+ is_yuno: !!is_root,
13476
+ children: []
13477
+ };
13478
+ let children = target_gobj.dl_children || [];
13479
+ for (let child of children) {
13480
+ let cd = describe_js_gobj(child, false);
13481
+ if (cd) d.children.push(cd);
13482
+ }
13483
+ return d;
13484
+ }
13485
+ /******************************
13486
+ * Readouts
13487
+ ******************************/
13488
+ /************************************************************
13489
+ * The i18n key of a role. Two of the five roles are not the
13490
+ * word the reader wants ("pure" and "volatil" alone say
13491
+ * nothing), so the map is explicit.
13492
+ ************************************************************/
13493
+ function role_i18n_key(role) {
13494
+ switch (role) {
13495
+ case "yuno": return "yuno";
13496
+ case "service": return "service";
13497
+ case "volatil": return "volatil child";
13498
+ case "pure": return "pure child";
13499
+ default: return "child";
13500
+ }
13501
+ }
13502
+ /************************************************************
13503
+ * The marks a node carries besides its role: what the reader
13504
+ * would otherwise have to open the popover to learn.
13505
+ *
13506
+ * Returns i18n KEYS, in a fixed order, so the card and the
13507
+ * popover cannot disagree about what a node is.
13508
+ ************************************************************/
13509
+ function node_badge_keys(d) {
13510
+ let badges = [];
13511
+ if (!d) return badges;
13512
+ let role = node_role(d);
13513
+ if (role !== "child") badges.push(role_i18n_key(role));
13514
+ if (d.disabled) badges.push("disabled");
13515
+ if (d.bottom_gobj) badges.push("bottom");
13516
+ if (d.commands) badges.push("commands");
13517
+ return badges;
13518
+ }
13519
+ /************************************************************
13520
+ * The vocabulary of this view, translated.
13521
+ *
13522
+ * Written as literal t("...") calls and not as t(key): a key
13523
+ * reached through a variable is invisible to the locale
13524
+ * validator, which is how a fixed vocabulary ships in one
13525
+ * language and never changes it.
13526
+ ************************************************************/
13527
+ function node_labels(t) {
13528
+ if (typeof t !== "function") t = (k) => k;
13529
+ return {
13530
+ role: {
13531
+ "yuno": t("yuno"),
13532
+ "service": t("service"),
13533
+ "volatil child": t("volatil child"),
13534
+ "pure child": t("pure child"),
13535
+ "child": t("child")
13536
+ },
13537
+ status: {
13538
+ "playing": t("playing"),
13539
+ "running (paused)": t("running (paused)"),
13540
+ "stopped": t("stopped")
13541
+ },
13542
+ status_short: {
13543
+ "playing": t("playing"),
13544
+ "running": t("running"),
13545
+ "stopped": t("stopped")
13546
+ },
13547
+ badge: {
13548
+ "yuno": t("yuno"),
13549
+ "service": t("service"),
13550
+ "volatil child": t("volatil child"),
13551
+ "pure child": t("pure child"),
13552
+ "disabled": t("disabled"),
13553
+ "bottom": t("bottom"),
13554
+ "commands": t("commands")
13555
+ },
13556
+ field: {
13557
+ gclass: t("gclass"),
13558
+ name: t("name"),
13559
+ full_name: t("full name"),
13560
+ role: t("role"),
13561
+ status: t("status"),
13562
+ state: t("state"),
13563
+ disabled: t("disabled"),
13564
+ parent: t("parent"),
13565
+ bottom_gobj: t("bottom gobj"),
13566
+ commands: t("commands"),
13567
+ traces: t("traces"),
13568
+ flags: t("flags"),
13569
+ children: t("children"),
13570
+ yes: t("yes"),
13571
+ collapsed: t("(collapsed)")
13572
+ }
13573
+ };
13574
+ }
13575
+ /************************************************************
13576
+ * The rows of the popover: [label, value], both translated.
13577
+ *
13578
+ * `t` is the app's translator -- the library has none of its
13579
+ * own. Values that are a fixed vocabulary (the role, the
13580
+ * status, yes) go through it too; free text does not.
13581
+ ************************************************************/
13582
+ function node_info_rows(d, t) {
13583
+ let rows = [];
13584
+ if (!d) return rows;
13585
+ let L = node_labels(t);
13586
+ let f = L.field;
13587
+ rows.push([f.gclass, d.gclass || "—"]);
13588
+ rows.push([f.name, d.name || "—"]);
13589
+ if (d.fullname && d.fullname !== d.shortname) rows.push([f.full_name, d.fullname]);
13590
+ rows.push([f.role, L.role[role_i18n_key(node_role(d))]]);
13591
+ rows.push([f.status, L.status[node_status_key(d)]]);
13592
+ rows.push([f.state, d.state || "—"]);
13593
+ if (d.disabled) rows.push([f.disabled, f.yes]);
13594
+ if (d.parent) rows.push([f.parent, d.parent]);
13595
+ if (d.bottom_gobj) rows.push([f.bottom_gobj, d.bottom_gobj]);
13596
+ if (d.commands) rows.push([f.commands, f.yes]);
13597
+ if (Array.isArray(d.gobj_trace_level) && d.gobj_trace_level.length > 0) rows.push([f.traces, d.gobj_trace_level.join(", ")]);
13598
+ if (Array.isArray(d.gobj_flags) && d.gobj_flags.length > 0) rows.push([f.flags, d.gobj_flags.join(", ")]);
13599
+ let kids = Array.isArray(d.children) ? d.children.length : 0;
13600
+ if (kids > 0) rows.push([f.children, d.is_collapsed ? kids + " " + f.collapsed : String(kids)]);
13601
+ return rows;
13602
+ }
13603
+ //#endregion
13604
+ //#region src/gclass_describe.js
13605
+ /***********************************************************************
13606
+ * gclass_describe.js
13607
+ *
13608
+ * A gclass, as plain JSON, in the shape the BACKEND answers.
13609
+ *
13610
+ * There is no gclass viewer in the framework: the C kernel
13611
+ * answers `view-gclass` with `gclass2json()`
13612
+ * (`kernel/c/gobj-c/src/gobj.c`) and nothing draws it. This
13613
+ * module builds the same document for a gclass of the BROWSER
13614
+ * yuno, out of the gobj-js registry, so one viewer can show a
13615
+ * gclass of either side:
13616
+ *
13617
+ * {id, gcflag, attrs, commands, gclass_methods,
13618
+ * internal_methods, FSM: {events, states}}
13619
+ *
13620
+ * Two keys the backend does not carry are added at the end,
13621
+ * because the browser can answer them for free and the reader
13622
+ * asks them first: `instances` and `trace_levels`.
13623
+ *
13624
+ * Copyright (c) 2026, ArtGins.
13625
+ * All Rights Reserved.
13626
+ ***********************************************************************/
13627
+ /***************************************************************
13628
+ * Bit tables
13629
+ ***************************************************************/
13630
+ var DATA_TYPE_NAMES = {
13631
+ [data_type_t.DTP_STRING]: "DTP_STRING",
13632
+ [data_type_t.DTP_BOOLEAN]: "DTP_BOOLEAN",
13633
+ [data_type_t.DTP_INTEGER]: "DTP_INTEGER",
13634
+ [data_type_t.DTP_REAL]: "DTP_REAL",
13635
+ [data_type_t.DTP_LIST]: "DTP_LIST",
13636
+ [data_type_t.DTP_DICT]: "DTP_DICT",
13637
+ [data_type_t.DTP_JSON]: "DTP_JSON",
13638
+ [data_type_t.DTP_POINTER]: "DTP_POINTER"
13639
+ };
13640
+ /************************************************************
13641
+ * The names of the bits set in `bits`, in table order.
13642
+ ************************************************************/
13643
+ function bits2names(table, bits) {
13644
+ let names = [];
13645
+ let value = Number(bits) || 0;
13646
+ for (let name of Object.keys(table)) if ((value & table[name]) !== 0) names.push(name);
13647
+ return names;
13648
+ }
13649
+ /************************************************************
13650
+ * The name of a data type. An unknown type is reported as
13651
+ * the number it is -- silently drawing "DTP_STRING" for a
13652
+ * type nobody registered would be worse than saying "17".
13653
+ ************************************************************/
13654
+ function data_type_name(type) {
13655
+ return DATA_TYPE_NAMES[type] || String(type);
13656
+ }
13657
+ /************************************************************
13658
+ * One SDATA descriptor -> one row.
13659
+ *
13660
+ * A DTP_POINTER default is a live object (a DOM node, a
13661
+ * gobj): it is never printed, only named, because a viewer
13662
+ * that serializes it would either throw or dump the page.
13663
+ ************************************************************/
13664
+ function sdatadesc2json(it) {
13665
+ let row = {
13666
+ id: it.name,
13667
+ type: data_type_name(it.type),
13668
+ flag: bits2names(sdata_flag_t, it.flag),
13669
+ description: it.description || ""
13670
+ };
13671
+ if (it.type === data_type_t.DTP_POINTER) row.default_value = it.default_value === null || it.default_value === void 0 ? null : "<pointer>";
13672
+ else row.default_value = it.default_value === void 0 ? null : it.default_value;
13673
+ return row;
13674
+ }
13675
+ /************************************************************
13676
+ * The attrs table of a gclass.
13677
+ ************************************************************/
13678
+ function attrs2json(attrs_table) {
13679
+ let rows = [];
13680
+ if (!Array.isArray(attrs_table)) return rows;
13681
+ for (let it of attrs_table) {
13682
+ if (!it || !it.name) continue;
13683
+ rows.push(sdatadesc2json(it));
13684
+ }
13685
+ return rows;
13686
+ }
13687
+ /************************************************************
13688
+ * The command table of a gclass, with the parameters of
13689
+ * each command (the `schema` slot of SDATACM).
13690
+ ************************************************************/
13691
+ function commands2json(command_table) {
13692
+ let rows = [];
13693
+ if (!Array.isArray(command_table)) return rows;
13694
+ for (let it of command_table) {
13695
+ if (!it || !it.name) continue;
13696
+ let row = {
13697
+ id: it.name,
13698
+ alias: it.alias || [],
13699
+ description: it.description || ""
13700
+ };
13701
+ if (Array.isArray(it.schema)) row.parameters = attrs2json(it.schema);
13702
+ rows.push(row);
13703
+ }
13704
+ return rows;
13705
+ }
13706
+ /************************************************************
13707
+ * The methods the gclass actually implements. A table of
13708
+ * every possible one, mostly empty, tells the reader nothing.
13709
+ ************************************************************/
13710
+ function methods2json(mt) {
13711
+ let names = [];
13712
+ if (!mt || typeof mt !== "object") return names;
13713
+ for (let key of Object.keys(mt)) if (typeof mt[key] === "function") names.push(key);
13714
+ return names;
13715
+ }
13716
+ /************************************************************
13717
+ * The FSM: the events the gclass declares, and what each
13718
+ * state does with them.
13719
+ ************************************************************/
13720
+ function fsm2json(gclass) {
13721
+ let events = [];
13722
+ for (let ev of gclass.dl_events || []) events.push({
13723
+ event_name: ev.event_name,
13724
+ event_flag: bits2names(event_flag_t, ev.event_flag)
13725
+ });
13726
+ let states = {};
13727
+ for (let st of gclass.dl_states || []) {
13728
+ let actions = [];
13729
+ for (let ac of st.dl_actions || []) actions.push({
13730
+ event_name: ac.event_name,
13731
+ action: ac.action ? ac.action.name || "action" : "",
13732
+ next_state: ac.next_state || ""
13733
+ });
13734
+ states[st.state_name] = actions;
13735
+ }
13736
+ return {
13737
+ events,
13738
+ states
13739
+ };
13740
+ }
13741
+ /************************************************************
13742
+ * The description of a gclass REGISTERED IN THIS BROWSER YUNO,
13743
+ * or null when the registry does not know that name.
13744
+ *
13745
+ * Null is the answer for a gclass of a BACKEND yuno too: the
13746
+ * browser registry carries the GUI gclasses and nothing else,
13747
+ * so the caller asks the backend `view-gclass` for those and
13748
+ * shows whatever it answers -- this same shape.
13749
+ ************************************************************/
13750
+ function describe_local_gclass(gclass_name) {
13751
+ if (!gclass_name) return null;
13752
+ let gclass = gclass_find_by_name(gclass_name, false);
13753
+ if (!gclass) return null;
13754
+ return {
13755
+ id: gclass.gclass_name,
13756
+ gcflag: bits2names(gclass_flag_t, gclass.gclass_flag),
13757
+ attrs: attrs2json(gclass.attrs_table),
13758
+ commands: commands2json(gclass.command_table),
13759
+ gclass_methods: methods2json(gclass.gmt),
13760
+ internal_methods: methods2json(gclass.lmt),
13761
+ FSM: fsm2json(gclass),
13762
+ instances: Number(gclass.instances) || 0,
13763
+ trace_levels: Array.isArray(gclass.s_user_trace_level) ? gclass.s_user_trace_level : []
13764
+ };
13765
+ }
13766
+ //#endregion
13767
+ //#region src/yui_gclass_view.js
13768
+ /***********************************************************************
13769
+ * yui_gclass_view.js
13770
+ *
13771
+ * The gclass viewer: what a GClass IS, in a window.
13772
+ *
13773
+ * The framework had no such viewer. The C kernel answers
13774
+ * `view-gclass` with a full description (attrs, commands,
13775
+ * methods, FSM) and the only reader of it was a terminal.
13776
+ * This one draws that same document with C_YUI_JSON, so the
13777
+ * FSM of a gclass is a graph and its attrs table is a tree,
13778
+ * and it takes the document from either side:
13779
+ *
13780
+ * - a gclass of the browser yuno -> describe_local_gclass()
13781
+ * - a gclass of a backend yuno -> `opts.description`, the
13782
+ * answer of `view-gclass`
13783
+ *
13784
+ * One window at a time per host: the caller keeps the handle
13785
+ * and closes it, the way the treedb graph keeps its raw-JSON
13786
+ * viewer.
13787
+ *
13788
+ * Copyright (c) 2026, ArtGins.
13789
+ * All Rights Reserved.
13790
+ ***********************************************************************/
13791
+ /************************************************************
13792
+ * Can this app show a gclass at all?
13793
+ *
13794
+ * The viewer is C_YUI_JSON, and an app registers only the
13795
+ * gclasses it mounts. The caller asks this BEFORE drawing a
13796
+ * button, so an app that does not carry the viewer shows no
13797
+ * control instead of a control that fails.
13798
+ ************************************************************/
13799
+ function gclass_view_available() {
13800
+ return !!gclass_find_by_name("C_YUI_JSON", false);
13801
+ }
13802
+ /************************************************************
13803
+ * Open the viewer on `gclass_name`.
13804
+ *
13805
+ * opts:
13806
+ * description the document to draw. Omitted, it is
13807
+ * built from the browser registry.
13808
+ * title_prefix what the window says before the gclass
13809
+ * name (the yuno it belongs to).
13810
+ * on_close called when the reader dismisses it.
13811
+ *
13812
+ * Returns a handle for close_gclass_view(), or null (and a
13813
+ * logged reason) when there is nothing to show.
13814
+ ************************************************************/
13815
+ function open_gclass_view(host, gclass_name, opts) {
13816
+ opts = opts || {};
13817
+ if (!host || !gclass_name) {
13818
+ log_error(`yui_gclass_view: open without a host or a gclass name`);
13819
+ return null;
13820
+ }
13821
+ let shell = yui_shell_of(host);
13822
+ if (!gclass_view_available()) {
13823
+ log_error(`${gobj_short_name(host)}: C_YUI_JSON not registered by the app`);
13824
+ yui_shell_show_error(shell, "gclass viewer unavailable", { t });
13825
+ return null;
13826
+ }
13827
+ let description = opts.description || describe_local_gclass(gclass_name);
13828
+ if (!description) {
13829
+ log_warning(`${gobj_short_name(host)}: gclass not registered here: ${gclass_name}`);
13830
+ yui_shell_show_error(shell, "gclass not registered in this yuno", { t });
13831
+ return null;
13832
+ }
13833
+ let jv = gobj_create_service(`gclass-view-${clean_name(gobj_name(host))}`, "C_YUI_JSON", {
13834
+ json_data: description,
13835
+ subscriber: null
13836
+ }, host);
13837
+ if (!jv) {
13838
+ log_error(`${gobj_short_name(host)}: cannot create the gclass viewer`);
13839
+ return null;
13840
+ }
13841
+ gobj_start(jv);
13842
+ let handle = {
13843
+ json_gobj: jv,
13844
+ win: null,
13845
+ modal: null
13846
+ };
13847
+ let $box = gobj_read_pointer_attr(jv, "$container");
13848
+ let on_close = () => {
13849
+ if (typeof opts.on_close === "function") opts.on_close();
13850
+ };
13851
+ if (is_mobile$3()) {
13852
+ if (!shell) {
13853
+ log_error(`${gobj_short_name(host)}: no shell, cannot open the gclass sheet`);
13854
+ close_gclass_view(handle);
13855
+ return null;
13856
+ }
13857
+ handle.modal = yui_shell_show_modal(shell, $box, {
13858
+ dialog: true,
13859
+ logical_class: "GCLASS_VIEW_SHEET",
13860
+ title_prefix: opts.title_prefix || "",
13861
+ title: gclass_name,
13862
+ t,
13863
+ on_close
13864
+ });
13865
+ return handle;
13866
+ }
13867
+ let $win_parent = window_layer_of(host, shell);
13868
+ handle.win = gobj_create_service(`gclass-win-${clean_name(gobj_name(host))}`, "C_YUI_WINDOW", {
13869
+ $parent: $win_parent,
13870
+ subscriber: null,
13871
+ modal: false,
13872
+ showMax: true,
13873
+ showFooter: false,
13874
+ resizable: true,
13875
+ center: true,
13876
+ auto_save_size_and_position: true,
13877
+ width: 720,
13878
+ height: 640,
13879
+ logical_class: "GCLASS_VIEW_WINDOW",
13880
+ title_prefix: opts.title_prefix || "",
13881
+ title: gclass_name,
13882
+ icon: "yi-code",
13883
+ body: $box,
13884
+ manager: gobj_find_service("__window_manager__", false) || null,
13885
+ on_close
13886
+ }, host);
13887
+ if (!handle.win) {
13888
+ log_error(`${gobj_short_name(host)}: cannot create the gclass window`);
13889
+ close_gclass_view(handle);
13890
+ return null;
13891
+ }
13892
+ gobj_start(handle.win);
13893
+ return handle;
13894
+ }
13895
+ /************************************************************
13896
+ * Close whatever open_gclass_view() opened. Safe on null and
13897
+ * on a handle whose window the reader already dismissed.
13898
+ ************************************************************/
13899
+ function close_gclass_view(handle) {
13900
+ if (!handle) return;
13901
+ let jv = handle.json_gobj;
13902
+ let win = handle.win;
13903
+ let modal = handle.modal;
13904
+ handle.json_gobj = null;
13905
+ handle.win = null;
13906
+ handle.modal = null;
13907
+ if (win && is_gobj(win)) try {
13908
+ if (gobj_is_running(win)) gobj_stop(win);
13909
+ gobj_destroy(win);
13910
+ } catch (e) {
13911
+ log_warning(`yui_gclass_view: window already gone: ${e}`);
13912
+ }
13913
+ if (modal && typeof modal.close === "function") try {
13914
+ modal.close();
13915
+ } catch (e) {
13916
+ log_warning(`yui_gclass_view: modal already gone: ${e}`);
13917
+ }
13918
+ if (jv && is_gobj(jv)) try {
13919
+ if (gobj_is_running(jv)) gobj_stop(jv);
13920
+ gobj_destroy(jv);
13921
+ } catch (e) {
13922
+ log_warning(`yui_gclass_view: viewer already gone: ${e}`);
13923
+ }
13924
+ }
13925
+ /************************************************************
13926
+ * Where to mount the window: the SAME element the caller's
13927
+ * own window hangs from.
13928
+ *
13929
+ * Not a preference -- a requirement. z-index only orders
13930
+ * siblings of one stacking context, so a viewer opened from a
13931
+ * window on `body` and mounted in the shell's popup layer
13932
+ * lands BEHIND its opener whatever number it is given, and
13933
+ * the button reads as one that did nothing.
13934
+ ************************************************************/
13935
+ function window_layer_of(host, shell) {
13936
+ let $c = gobj_read_pointer_attr(host, "$container");
13937
+ if ($c && typeof $c.closest === "function") {
13938
+ let $win = $c.closest(".C_YUI_WINDOW");
13939
+ if ($win && $win.parentNode) return $win.parentNode;
13940
+ }
13941
+ return typeof document !== "undefined" && document.getElementById("top-layer") || shell && yui_shell_popup_layer(shell) || null;
13942
+ }
13943
+ /************************************************************
13944
+ * A window is not a sheet: below this width there is no room
13945
+ * for a title bar the reader can drag.
13946
+ ************************************************************/
13947
+ function is_mobile$3() {
13948
+ return typeof window !== "undefined" && window.innerWidth <= 768;
13949
+ }
13950
+ //#endregion
13213
13951
  //#region src/c_yui_gobj_tree_js.js
13214
13952
  /***********************************************************************
13215
13953
  * c_yui_gobj_tree_js.js
@@ -13245,6 +13983,71 @@ var BEHAVIORS_READING = [
13245
13983
  "drag-element"
13246
13984
  ];
13247
13985
  var BEHAVIORS_PICKING = ["drag-canvas", ZOOM_CANVAS];
13986
+ var VIEW_STORE_PREFIX = "yui_gobj_tree_view:";
13987
+ function view_store_key(gobj) {
13988
+ return VIEW_STORE_PREFIX + gobj_name(gobj);
13989
+ }
13990
+ function restore_view_shape(gobj) {
13991
+ let priv = gobj.priv;
13992
+ let state = load_view_state(gobj);
13993
+ if (!state) return;
13994
+ if (state.layout && LAYOUTS[state.layout]) {
13995
+ gobj_write_str_attr(gobj, "layout", state.layout);
13996
+ if (priv.$layout_select) priv.$layout_select.value = state.layout;
13997
+ }
13998
+ if (state.collapsed && typeof state.collapsed === "object") priv.collapse_state = Object.assign({}, state.collapsed);
13999
+ }
14000
+ function restore_view_state(gobj) {
14001
+ let priv = gobj.priv;
14002
+ let graph = priv.graph;
14003
+ let state = load_view_state(gobj);
14004
+ if (!graph || !state) return false;
14005
+ let done = false;
14006
+ priv.restoring = true;
14007
+ try {
14008
+ if (typeof state.zoom === "number" && state.zoom > 0) {
14009
+ graph.zoomTo(state.zoom);
14010
+ priv.last_zoom = state.zoom;
14011
+ done = true;
14012
+ }
14013
+ if (Array.isArray(state.position) && state.position.length >= 2) {
14014
+ graph.translateTo([state.position[0], state.position[1]]);
14015
+ done = true;
14016
+ }
14017
+ if (state.anchor) {
14018
+ priv.anchor_name = state.anchor;
14019
+ priv.anchor_state = "on";
14020
+ reanchor(gobj);
14021
+ yui_graph_update_anchor(gobj_read_attr(gobj, "$container"), priv.anchor_state);
14022
+ paint_anchor_mark(gobj);
14023
+ }
14024
+ } catch (e) {
14025
+ log_error(`${gobj_short_name(gobj)}: cannot restore the saved view: ${e}`);
14026
+ } finally {
14027
+ priv.restoring = false;
14028
+ }
14029
+ yui_graph_update_zoom(gobj_read_attr(gobj, "$container"), priv.graph);
14030
+ return done;
14031
+ }
14032
+ function save_view_state(gobj) {
14033
+ let priv = gobj.priv;
14034
+ let graph = priv.graph;
14035
+ if (!graph || priv.restoring) return;
14036
+ let state = {
14037
+ layout: gobj_read_str_attr(gobj, "layout"),
14038
+ collapsed: priv.collapse_state || {},
14039
+ anchor: priv.anchor_state === "on" ? priv.anchor_name : ""
14040
+ };
14041
+ try {
14042
+ state.zoom = graph.getZoom();
14043
+ state.position = graph.getPosition();
14044
+ } catch (e) {}
14045
+ kw_set_local_storage_value(view_store_key(gobj), state);
14046
+ }
14047
+ function load_view_state(gobj) {
14048
+ let state = kw_get_local_storage_value(view_store_key(gobj), null, false);
14049
+ return state && typeof state === "object" ? state : null;
14050
+ }
13248
14051
  var PRIVATE_DATA$9 = {
13249
14052
  canvas_id: "",
13250
14053
  graph: null,
@@ -13255,6 +14058,9 @@ var PRIVATE_DATA$9 = {
13255
14058
  $popover: null,
13256
14059
  $popover_title: null,
13257
14060
  $popover_body: null,
14061
+ $popover_gclass: null,
14062
+ popover_node: null,
14063
+ gclass_view: null,
13258
14064
  node_by_id: null,
13259
14065
  collapse_state: null,
13260
14066
  resize_observer: null,
@@ -13262,39 +14068,99 @@ var PRIVATE_DATA$9 = {
13262
14068
  anchor_id: "",
13263
14069
  anchor_name: "",
13264
14070
  anchor_state: "off",
14071
+ restoring: false,
14072
+ restored: false,
13265
14073
  last_zoom: 1
13266
14074
  };
13267
14075
  var __gclass__$9 = null;
13268
14076
  var __layout_registered__ = false;
13269
14077
  /***************************************************************
13270
- * Node colors by role
14078
+ * Node colours by role.
14079
+ *
14080
+ * Saturated hues, one family apart from the next, and the same
14081
+ * five everywhere the reader meets a gobj. `stroke` draws the
14082
+ * border and the edge; `accent` is the brighter sibling used
14083
+ * for the bar down the left of the card and the toggle badge,
14084
+ * so a card carries its colour twice at two intensities and is
14085
+ * readable at the zoom where the text is not.
13271
14086
  ***************************************************************/
13272
14087
  var ROLE_COLORS = {
13273
14088
  yuno: {
13274
- fill: "#E0F2FE",
13275
- stroke: "#075985"
14089
+ stroke: "#0284c7",
14090
+ accent: "#38bdf8"
13276
14091
  },
13277
14092
  service: {
13278
- fill: "#DCFCE7",
13279
- stroke: "#166534"
14093
+ stroke: "#059669",
14094
+ accent: "#34d399"
13280
14095
  },
13281
14096
  pure: {
13282
- fill: "#FEF9C3",
13283
- stroke: "#854D0E"
14097
+ stroke: "#d97706",
14098
+ accent: "#fbbf24"
13284
14099
  },
13285
14100
  volatil: {
13286
- fill: "#FCE7F3",
13287
- stroke: "#9D174D"
14101
+ stroke: "#db2777",
14102
+ accent: "#f472b6"
13288
14103
  },
13289
- default: {
13290
- fill: "#F3F4F6",
13291
- stroke: "#374151"
14104
+ child: {
14105
+ stroke: "#7c3aed",
14106
+ accent: "#a78bfa"
13292
14107
  }
13293
14108
  };
13294
- var STATE_COLORS = {
13295
- running_playing: "#15803D",
13296
- running: "#B45309",
13297
- stopped: "#B91C1C"
14109
+ /***************************************************************
14110
+ * Status colours: what the gobj is DOING.
14111
+ *
14112
+ * A pill and not a word: three states of one gobj are told
14113
+ * apart across a whole tree by colour long before anybody
14114
+ * reads them, and the dot alone never said which was which.
14115
+ * `disabled` is a fourth entry and not a status -- a disabled
14116
+ * gobj is still stopped or running -- but it takes the pill,
14117
+ * because it is the fact that explains the other one.
14118
+ ***************************************************************/
14119
+ var STATUS_COLORS = {
14120
+ light: {
14121
+ playing: {
14122
+ fg: "#065f46",
14123
+ bg: "#a7f3d0",
14124
+ dot: "#10b981"
14125
+ },
14126
+ running: {
14127
+ fg: "#92400e",
14128
+ bg: "#fde68a",
14129
+ dot: "#f59e0b"
14130
+ },
14131
+ stopped: {
14132
+ fg: "#991b1b",
14133
+ bg: "#fecaca",
14134
+ dot: "#ef4444"
14135
+ },
14136
+ disabled: {
14137
+ fg: "#334155",
14138
+ bg: "#e2e8f0",
14139
+ dot: "#94a3b8"
14140
+ }
14141
+ },
14142
+ dark: {
14143
+ playing: {
14144
+ fg: "#6ee7b7",
14145
+ bg: "rgba(16,185,129,0.28)",
14146
+ dot: "#34d399"
14147
+ },
14148
+ running: {
14149
+ fg: "#fcd34d",
14150
+ bg: "rgba(245,158,11,0.28)",
14151
+ dot: "#fbbf24"
14152
+ },
14153
+ stopped: {
14154
+ fg: "#fca5a5",
14155
+ bg: "rgba(239,68,68,0.28)",
14156
+ dot: "#f87171"
14157
+ },
14158
+ disabled: {
14159
+ fg: "#cbd5e1",
14160
+ bg: "rgba(148,163,184,0.28)",
14161
+ dot: "#94a3b8"
14162
+ }
14163
+ }
13298
14164
  };
13299
14165
  /***************************************************************
13300
14166
  * Fixed node sizes per role (px).
@@ -13304,16 +14170,16 @@ var STATE_COLORS = {
13304
14170
  var NODE_SIZES = {
13305
14171
  full: {
13306
14172
  yuno: {
13307
- w: 240,
13308
- h: 72
14173
+ w: 260,
14174
+ h: 96
13309
14175
  },
13310
14176
  service: {
13311
- w: 210,
13312
- h: 68
14177
+ w: 232,
14178
+ h: 92
13313
14179
  },
13314
14180
  child: {
13315
- w: 160,
13316
- h: 60
14181
+ w: 196,
14182
+ h: 86
13317
14183
  }
13318
14184
  },
13319
14185
  compact: {
@@ -13336,27 +14202,47 @@ var NODE_SIZES = {
13336
14202
  ***************************************************************/
13337
14203
  var GT_FONT = "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif";
13338
14204
  /***************************************************************
13339
- * Soft, theme-aware card palette derived from the role colour
13340
- * (same visual language as the treedb graph cards): light tint
13341
- * fill + role-colour border, brightened on dark.
14205
+ * Theme-aware card palette derived from the role colour.
14206
+ *
14207
+ * Brighter than a tint: at 9% of the role colour every card
14208
+ * was the same near-white rectangle and the tree read as a
14209
+ * wireframe. The fill carries enough of the hue to be named
14210
+ * from across the canvas, and the border and the accent bar
14211
+ * carry the rest.
13342
14212
  ***************************************************************/
13343
- function role_card_style(stroke, dark) {
14213
+ function role_card_style(colors, dark) {
14214
+ let stroke = colors.stroke;
14215
+ let accent = colors.accent;
13344
14216
  return {
13345
- bg: dark ? `color-mix(in srgb, ${stroke} 30%, #2c3542)` : `color-mix(in srgb, ${stroke} 9%, #ffffff)`,
13346
- border: dark ? `color-mix(in srgb, ${stroke} 85%, #ffffff)` : stroke,
13347
- title: dark ? "#e8eaed" : "#0f172a",
13348
- sub: dark ? "#9aa4b2" : "#64748b",
13349
- tagbg: dark ? `color-mix(in srgb, ${stroke} 42%, #2c3542)` : `color-mix(in srgb, ${stroke} 14%, #ffffff)`,
13350
- shadow: dark ? "0 1px 3px rgba(0,0,0,0.45)" : "0 1px 3px rgba(15,23,42,0.12)"
14217
+ bg: dark ? `color-mix(in srgb, ${stroke} 34%, #253044)` : `color-mix(in srgb, ${stroke} 13%, #ffffff)`,
14218
+ border: dark ? accent : stroke,
14219
+ accent: dark ? accent : stroke,
14220
+ title: dark ? "#f1f5f9" : "#0f172a",
14221
+ sub: dark ? "#a8b3c2" : "#475569",
14222
+ tagbg: dark ? `color-mix(in srgb, ${stroke} 48%, #253044)` : `color-mix(in srgb, ${stroke} 20%, #ffffff)`,
14223
+ tagfg: dark ? "#f1f5f9" : stroke,
14224
+ shadow: dark ? "0 2px 6px rgba(0,0,0,0.50)" : "0 2px 5px rgba(15,23,42,0.16)"
13351
14225
  };
13352
14226
  }
13353
14227
  /***************************************************************
13354
- * Map a gobj to its visual category.
14228
+ * The pill colours of a node, for the theme in force.
13355
14229
  ***************************************************************/
13356
- function get_node_category(target_gobj, is_root) {
13357
- if (is_root) return "yuno";
13358
- if (gobj_is_service(target_gobj)) return "service";
13359
- return "child";
14230
+ function status_card_style(d, dark) {
14231
+ let table = dark ? STATUS_COLORS.dark : STATUS_COLORS.light;
14232
+ if (d.disabled) return table.disabled;
14233
+ return table[node_status(d)] || table.stopped;
14234
+ }
14235
+ /***************************************************************
14236
+ * Map a node to its visual size class. Services are drawn
14237
+ * bigger than plain children so the service boundary of the
14238
+ * hierarchy is visible before anything is read.
14239
+ ***************************************************************/
14240
+ function get_node_category(d) {
14241
+ switch (node_role(d)) {
14242
+ case "yuno": return "yuno";
14243
+ case "service": return "service";
14244
+ default: return "child";
14245
+ }
13360
14246
  }
13361
14247
  /***************************************************************
13362
14248
  * Layouts registry.
@@ -13468,13 +14354,22 @@ function mt_create$9(gobj) {
13468
14354
  * Framework Method: Start
13469
14355
  ***************************************************************/
13470
14356
  function mt_start$9(gobj) {
14357
+ restore_view_shape(gobj);
14358
+ let shell = yui_shell_of(gobj);
14359
+ if (shell) gobj_subscribe_event(shell, "EV_LANGUAGE_CHANGED", {}, gobj);
13471
14360
  build_graph$2(gobj);
13472
14361
  load_tree(gobj);
13473
14362
  }
13474
14363
  /***************************************************************
13475
14364
  * Framework Method: Stop
13476
14365
  ***************************************************************/
13477
- function mt_stop$9(gobj) {}
14366
+ function mt_stop$9(gobj) {
14367
+ let priv = gobj.priv;
14368
+ let shell = yui_shell_of(gobj);
14369
+ if (shell) gobj_unsubscribe_event(shell, "EV_LANGUAGE_CHANGED", {}, gobj);
14370
+ close_gclass_view(priv.gclass_view);
14371
+ priv.gclass_view = null;
14372
+ }
13478
14373
  /***************************************************************
13479
14374
  * Framework Method: Destroy
13480
14375
  ***************************************************************/
@@ -13500,6 +14395,8 @@ function mt_destroy$9(gobj) {
13500
14395
  priv.$popover = null;
13501
14396
  priv.$popover_title = null;
13502
14397
  priv.$popover_body = null;
14398
+ priv.$popover_gclass = null;
14399
+ priv.popover_node = null;
13503
14400
  priv.$layout_select = null;
13504
14401
  priv.node_by_id = null;
13505
14402
  priv.collapse_state = null;
@@ -13603,10 +14500,38 @@ function build_popover(gobj, $container) {
13603
14500
  text-overflow: ellipsis;
13604
14501
  `;
13605
14502
  $header.appendChild($title);
14503
+ if (gclass_view_available()) {
14504
+ let $gclass = document.createElement("button");
14505
+ $gclass.type = "button";
14506
+ $gclass.className = "GOBJ_TREE_POPOVER_GCLASS button is-small";
14507
+ $gclass.style.cssText = `
14508
+ margin-right: 6px;
14509
+ padding: 0 8px;
14510
+ height: 22px;
14511
+ font-size: 11px;
14512
+ white-space: nowrap;
14513
+ `;
14514
+ $gclass.textContent = t("gclass");
14515
+ $gclass.setAttribute("i18n", "gclass");
14516
+ $gclass.title = t("view gclass");
14517
+ $gclass.setAttribute("data-i18n-title", "view gclass");
14518
+ $gclass.setAttribute("aria-label", t("view gclass"));
14519
+ $gclass.setAttribute("data-i18n-aria-label", "view gclass");
14520
+ $gclass.addEventListener("click", (evt) => {
14521
+ evt.stopPropagation();
14522
+ gobj_send_event(gobj, "EV_OPEN_GCLASS", {}, gobj);
14523
+ });
14524
+ $header.appendChild($gclass);
14525
+ priv.$popover_gclass = $gclass;
14526
+ } else log_warning(`${gobj_short_name(gobj)}: no C_YUI_JSON registered, the gclass viewer is not offered`);
13606
14527
  let $close = document.createElement("button");
13607
14528
  $close.type = "button";
14529
+ $close.className = "GOBJ_TREE_POPOVER_CLOSE";
13608
14530
  $close.textContent = "×";
13609
14531
  $close.title = t("close");
14532
+ $close.setAttribute("data-i18n-title", "close");
14533
+ $close.setAttribute("aria-label", t("close"));
14534
+ $close.setAttribute("data-i18n-aria-label", "close");
13610
14535
  $close.style.cssText = `
13611
14536
  background: transparent;
13612
14537
  border: none;
@@ -13740,6 +14665,7 @@ function build_graph$2(gobj) {
13740
14665
  if (z === priv.last_zoom) return;
13741
14666
  priv.last_zoom = z;
13742
14667
  if (priv.anchor_state === "on" && priv.anchor_id) yui_graph_center_on(priv.graph, priv.anchor_id);
14668
+ save_view_state(gobj);
13743
14669
  });
13744
14670
  graph.on(NodeEvent.CLICK, (evt) => {
13745
14671
  gobj_send_event(gobj, "EV_NODE_CLICK", { evt }, gobj);
@@ -13782,22 +14708,20 @@ function apply_layout(gobj) {
13782
14708
  });
13783
14709
  }
13784
14710
  /************************************************************
13785
- * Resolve role colors for a gobj
13786
- ************************************************************/
13787
- function get_role_colors(target_gobj, is_root) {
13788
- if (is_root) return ROLE_COLORS.yuno;
13789
- if (gobj_is_volatil(target_gobj)) return ROLE_COLORS.volatil;
13790
- if (gobj_is_service(target_gobj)) return ROLE_COLORS.service;
13791
- if (gobj_is_pure_child(target_gobj)) return ROLE_COLORS.pure;
13792
- return ROLE_COLORS.default;
13793
- }
13794
- /************************************************************
13795
- * Resolve state color
14711
+ * The role colours of a node.
14712
+ *
14713
+ * Note the ORDER: volatil before service. A volatil gobj that
14714
+ * is also a service is drawn volatil, because that is the fact
14715
+ * that decides how long it lives.
13796
14716
  ************************************************************/
13797
- function get_state_color(target_gobj) {
13798
- if (!gobj_is_running(target_gobj)) return STATE_COLORS.stopped;
13799
- if (gobj_is_playing(target_gobj)) return STATE_COLORS.running_playing;
13800
- return STATE_COLORS.running;
14717
+ function get_role_colors(d) {
14718
+ switch (node_role(d)) {
14719
+ case "yuno": return ROLE_COLORS.yuno;
14720
+ case "volatil": return ROLE_COLORS.volatil;
14721
+ case "service": return ROLE_COLORS.service;
14722
+ case "pure": return ROLE_COLORS.pure;
14723
+ default: return ROLE_COLORS.child;
14724
+ }
13801
14725
  }
13802
14726
  /************************************************************
13803
14727
  * Generate a unique node id
@@ -13829,72 +14753,135 @@ function is_node_collapsed(gobj, full_name, num_children) {
13829
14753
  ************************************************************/
13830
14754
  function render_toggle_html(node_id, collapsed, num_children, cs) {
13831
14755
  let label = collapsed ? "+" + num_children : "−";
14756
+ let title = collapsed ? t("expand children") : t("collapse children");
13832
14757
  return `<span
13833
14758
  data-gobj-toggle="true"
13834
14759
  data-node-id="${node_id}"
13835
- title="${collapsed ? "Expand children" : "Collapse children"}"
14760
+ title="${escapeHtml(title)}"
13836
14761
  style="
13837
14762
  flex: 0 0 auto;
13838
14763
  display: inline-flex;
13839
14764
  align-items: center;
13840
14765
  justify-content: center;
13841
- min-width: 22px;
13842
- height: 16px;
13843
- padding: 0 5px;
14766
+ min-width: 24px;
14767
+ height: 18px;
14768
+ padding: 0 6px;
13844
14769
  margin-left: 4px;
13845
- background: ${cs.tagbg};
13846
- color: ${cs.border};
13847
- border: 1px solid ${cs.border};
13848
- border-radius: 6px;
14770
+ background: ${cs.accent};
14771
+ color: #ffffff;
14772
+ border: 1px solid ${cs.accent};
14773
+ border-radius: 9px;
13849
14774
  font-size: 10px;
13850
- font-weight: 600;
14775
+ font-weight: 700;
13851
14776
  line-height: 1;
13852
14777
  cursor: pointer;
13853
14778
  user-select: none;
13854
14779
  ">${label}</span>`;
13855
14780
  }
13856
14781
  /************************************************************
13857
- * Recursively build nodes and edges from the gobj tree
14782
+ * HTML for the status pill: what the gobj is DOING.
14783
+ *
14784
+ * A dot plus a word, and the word is dropped in the compact
14785
+ * card where there is no room for it -- the colour still
14786
+ * carries it, and the `title` says it in full either way.
14787
+ ************************************************************/
14788
+ function render_status_html(d, ss, labels, with_label) {
14789
+ let full = d.disabled ? labels.field.disabled : labels.status[node_status_key(d)] || node_status_key(d);
14790
+ let label = d.disabled ? labels.field.disabled : labels.status_short[node_status(d)] || node_status(d);
14791
+ if (!with_label) return `<span title="${escapeHtml(full)}" style="
14792
+ flex: 0 0 auto;
14793
+ width: 10px; height: 10px; border-radius: 50%;
14794
+ background: ${ss.dot};
14795
+ box-shadow: 0 0 0 2px ${ss.bg};
14796
+ margin-left: 6px;
14797
+ "></span>`;
14798
+ return `<span title="${escapeHtml(full)}" style="
14799
+ flex: 0 0 auto;
14800
+ display: inline-flex;
14801
+ align-items: center;
14802
+ gap: 4px;
14803
+ padding: 1px 7px 1px 5px;
14804
+ background: ${ss.bg};
14805
+ color: ${ss.fg};
14806
+ border-radius: 9px;
14807
+ font-size: 10px;
14808
+ font-weight: 700;
14809
+ line-height: 1.45;
14810
+ white-space: nowrap;
14811
+ "><span style="width:7px; height:7px; border-radius:50%; background:${ss.dot};"></span>${escapeHtml(label)}</span>`;
14812
+ }
14813
+ /************************************************************
14814
+ * HTML for the badges: what the gobj IS, besides its colour.
14815
+ ************************************************************/
14816
+ function render_badges_html(d, cs, labels) {
14817
+ let html = "";
14818
+ for (let key of node_badge_keys(d)) {
14819
+ let label = labels.badge[key] || key;
14820
+ html += `<span style="
14821
+ font-size: 10px;
14822
+ font-weight: 600;
14823
+ padding: 0 6px;
14824
+ margin-right: 4px;
14825
+ background: ${cs.tagbg};
14826
+ color: ${cs.tagfg};
14827
+ border: 1px solid ${cs.accent};
14828
+ border-radius: 9px;
14829
+ white-space: nowrap;
14830
+ ">${escapeHtml(label)}</span>`;
14831
+ }
14832
+ return html;
14833
+ }
14834
+ /************************************************************
14835
+ * Recursively build nodes and edges from a NODE DESCRIPTOR
14836
+ * tree (gobj_tree_model.js), never from gobjs: the same
14837
+ * builder draws the browser yuno and a backend one.
14838
+ *
14839
+ * The descriptor is also what the popover reads, so the view
14840
+ * fields it needs are written onto it here (`full_name`,
14841
+ * `short_name`, `num_children`, `is_collapsed`,
14842
+ * `direct_children`) instead of copied into a second object
14843
+ * that can then disagree with the first.
13858
14844
  ************************************************************/
13859
- function build_gobj_nodes(gobj, target_gobj, nodes, edges, parent_id, is_root, compact) {
14845
+ function build_gobj_nodes(gobj, d, nodes, edges, parent_id, compact, labels) {
13860
14846
  let priv = gobj.priv;
13861
14847
  let node_id = gen_node_id(gobj);
13862
- let gclass_name = gobj_gclass_name(target_gobj) || "";
13863
- let instance_name = gobj_name(target_gobj) || "";
13864
- let state = gobj_current_state(target_gobj) || "";
13865
- let full_name = gobj_full_name(target_gobj);
13866
- let colors = get_role_colors(target_gobj, is_root);
13867
14848
  let dark = yui_is_dark();
13868
- let cs = role_card_style(colors.stroke, dark);
13869
- let state_color = get_state_color(target_gobj);
13870
- let running = gobj_is_running(target_gobj);
13871
- let playing = gobj_is_playing(target_gobj);
13872
- let status_text = !running ? "stopped" : playing ? "playing" : "running";
13873
- let category = get_node_category(target_gobj, is_root);
14849
+ let colors = get_role_colors(d);
14850
+ let cs = role_card_style(colors, dark);
14851
+ let ss = status_card_style(d, dark);
14852
+ let category = get_node_category(d);
13874
14853
  let size = NODE_SIZES[compact ? "compact" : "full"][category];
13875
14854
  let width = size.w;
13876
14855
  let height = size.h;
13877
14856
  let border_width = category === "yuno" || category === "service" ? 2 : 1;
13878
- let children = target_gobj.dl_children || [];
14857
+ let border_style = d.disabled ? "dashed" : "solid";
14858
+ let children = d.children || [];
13879
14859
  let num_children = children.length;
13880
14860
  let has_children = num_children > 0;
13881
- let collapsed = has_children && is_node_collapsed(gobj, full_name, num_children);
14861
+ let collapsed = has_children && is_node_collapsed(gobj, d.fullname, num_children);
13882
14862
  let direct_children = [];
13883
14863
  for (let child of children) direct_children.push({
13884
- full_name: gobj_full_name(child),
13885
- has_grandchildren: (child.dl_children || []).length > 0
14864
+ full_name: child.fullname,
14865
+ has_grandchildren: (child.children || []).length > 0
13886
14866
  });
14867
+ d.full_name = d.fullname;
14868
+ d.short_name = d.shortname;
14869
+ d.num_children = num_children;
14870
+ d.is_collapsed = collapsed;
14871
+ d.direct_children = direct_children;
14872
+ d.node_id = node_id;
13887
14873
  let toggle_html = has_children ? render_toggle_html(node_id, collapsed, num_children, cs) : "";
14874
+ let card_title = d.gclass + (d.name ? "^" + d.name : "");
13888
14875
  let node_html;
13889
14876
  if (compact) {
13890
- let compact_label = escapeHtml(instance_name ? gclass_name + "^" + instance_name : gclass_name);
14877
+ let compact_label = escapeHtml(card_title);
13891
14878
  node_html = `
13892
- <div style="
14879
+ <div class="GOBJ_CARD" data-gobj-name="${escapeHtml(d.fullname)}" style="
13893
14880
  width: ${width}px;
13894
14881
  height: ${height}px;
13895
14882
  background: ${cs.bg};
13896
- border: ${border_width}px solid ${cs.border};
13897
- border-left: 5px solid ${cs.border};
14883
+ border: ${border_width}px ${border_style} ${cs.border};
14884
+ border-left: 6px solid ${cs.accent};
13898
14885
  border-radius: 7px;
13899
14886
  box-shadow: ${cs.shadow};
13900
14887
  font-family: ${GT_FONT};
@@ -13903,34 +14890,24 @@ function build_gobj_nodes(gobj, target_gobj, nodes, edges, parent_id, is_root, c
13903
14890
  padding: 0 8px;
13904
14891
  box-sizing: border-box;
13905
14892
  overflow: hidden;
14893
+ opacity: ${d.disabled ? "0.72" : "1"};
13906
14894
  cursor: pointer;
13907
14895
  " title="${compact_label}">
13908
14896
  <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>
13909
- <span title="${escapeHtml(state || status_text)}" style="
13910
- flex: 0 0 auto;
13911
- width: 8px; height: 8px; border-radius: 50%;
13912
- background: ${state_color}; margin-left: 6px;
13913
- "></span>
14897
+ ${render_status_html(d, ss, labels, false)}
13914
14898
  ${toggle_html}
13915
14899
  </div>`;
13916
14900
  } else {
13917
- let header_label = escapeHtml(gclass_name);
13918
- let name_label = escapeHtml(instance_name);
13919
- let state_label = escapeHtml(state);
13920
- let badges = [];
13921
- if (is_root) badges.push("yuno");
13922
- else {
13923
- if (gobj_is_service(target_gobj)) badges.push("service");
13924
- if (gobj_is_pure_child(target_gobj)) badges.push("pure");
13925
- if (gobj_is_volatil(target_gobj)) badges.push("volatil");
13926
- }
13927
- 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("");
14901
+ let header_label = escapeHtml(d.gclass);
14902
+ let name_label = escapeHtml(d.name);
14903
+ let state_label = escapeHtml(d.state);
13928
14904
  node_html = `
13929
- <div style="
14905
+ <div class="GOBJ_CARD" data-gobj-name="${escapeHtml(d.fullname)}" style="
13930
14906
  width: ${width}px;
13931
14907
  height: ${height}px;
13932
14908
  background: ${cs.bg};
13933
- border: ${border_width}px solid ${cs.border};
14909
+ border: ${border_width}px ${border_style} ${cs.border};
14910
+ border-left: 6px solid ${cs.accent};
13934
14911
  border-radius: 9px;
13935
14912
  box-shadow: ${cs.shadow};
13936
14913
  overflow: hidden;
@@ -13939,48 +14916,29 @@ function build_gobj_nodes(gobj, target_gobj, nodes, edges, parent_id, is_root, c
13939
14916
  display: flex;
13940
14917
  flex-direction: column;
13941
14918
  justify-content: center;
13942
- padding: 5px 9px;
13943
- gap: 1px;
14919
+ padding: 6px 9px;
14920
+ gap: 2px;
14921
+ opacity: ${d.disabled ? "0.72" : "1"};
13944
14922
  cursor: pointer;
13945
- " title="${escapeHtml(gclass_name + (instance_name ? "^" + instance_name : ""))}">
14923
+ " title="${escapeHtml(card_title)}">
13946
14924
  <div style="flex:0 0 auto; display:flex; align-items:center; gap:6px; overflow:hidden;">
13947
- <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>
14925
+ <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>
13948
14926
  ${toggle_html}
13949
14927
  </div>
13950
14928
  <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>
13951
14929
  <div style="flex:0 0 auto; display:flex; justify-content:space-between; align-items:center; gap:6px; overflow:hidden;">
13952
- <span style="flex:1 1 auto; min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;">${badges_html}</span>
13953
- <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;">
13954
- <span style="width:7px; height:7px; border-radius:50%; background:${state_color};"></span>${state_label || status_text}
13955
- </span>
14930
+ <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>
14931
+ ${render_status_html(d, ss, labels, true)}
13956
14932
  </div>
14933
+ <div style="flex:0 0 auto; overflow:hidden; white-space:nowrap; text-overflow:ellipsis;">${render_badges_html(d, cs, labels)}</div>
13957
14934
  </div>`;
13958
14935
  }
13959
- let parent_gobj = gobj_parent(target_gobj);
13960
- let node_data = {
13961
- gclass: gclass_name,
13962
- name: instance_name,
13963
- short_name: gobj_short_name(target_gobj),
13964
- full_name,
13965
- parent_short_name: parent_gobj ? gobj_short_name(parent_gobj) : "",
13966
- state,
13967
- category,
13968
- is_yuno: is_root,
13969
- is_service: gobj_is_service(target_gobj),
13970
- is_pure_child: gobj_is_pure_child(target_gobj),
13971
- is_volatil: gobj_is_volatil(target_gobj),
13972
- is_running: running,
13973
- is_playing: playing,
13974
- num_children,
13975
- is_collapsed: collapsed,
13976
- direct_children
13977
- };
13978
14936
  if (!priv.node_by_id) priv.node_by_id = {};
13979
- priv.node_by_id[node_id] = node_data;
14937
+ priv.node_by_id[node_id] = d;
13980
14938
  let node = {
13981
14939
  id: node_id,
13982
14940
  type: "html",
13983
- data: node_data,
14941
+ data: d,
13984
14942
  style: {
13985
14943
  innerHTML: node_html,
13986
14944
  size: [width, height],
@@ -13993,11 +14951,11 @@ function build_gobj_nodes(gobj, target_gobj, nodes, edges, parent_id, is_root, c
13993
14951
  source: parent_id,
13994
14952
  target: node_id,
13995
14953
  style: {
13996
- stroke: colors.stroke,
13997
- lineWidth: 1
14954
+ stroke: colors.accent,
14955
+ lineWidth: 1.4
13998
14956
  }
13999
14957
  });
14000
- if (!collapsed) for (let child of children) build_gobj_nodes(gobj, child, nodes, edges, node_id, false, compact);
14958
+ if (!collapsed) for (let child of children) build_gobj_nodes(gobj, child, nodes, edges, node_id, compact, labels);
14001
14959
  return node_id;
14002
14960
  }
14003
14961
  /************************************************************
@@ -14014,9 +14972,13 @@ function load_tree(gobj) {
14014
14972
  let priv = gobj.priv;
14015
14973
  let graph = priv.graph;
14016
14974
  if (!graph) return;
14017
- let yuno = gobj_yuno();
14018
- if (!yuno) return;
14975
+ let root = describe_js_gobj(gobj_yuno(), true);
14976
+ if (!root) {
14977
+ log_error(`${gobj_short_name(gobj)}: no yuno to draw`);
14978
+ return;
14979
+ }
14019
14980
  let layout_cfg = get_layout_cfg(gobj_read_str_attr(gobj, "layout"));
14981
+ let labels = node_labels(t);
14020
14982
  let anchor = priv.pending_anchor || null;
14021
14983
  let preserve_view = !!priv.pending_preserve_view;
14022
14984
  priv.pending_anchor = null;
@@ -14025,7 +14987,7 @@ function load_tree(gobj) {
14025
14987
  priv.node_by_id = {};
14026
14988
  let nodes = [];
14027
14989
  let edges = [];
14028
- build_gobj_nodes(gobj, yuno, nodes, edges, null, true, layout_cfg.compact);
14990
+ build_gobj_nodes(gobj, root, nodes, edges, null, layout_cfg.compact, labels);
14029
14991
  if (nodes.length > 0) {
14030
14992
  let root_id = nodes[0].id;
14031
14993
  graph.setData({
@@ -14034,11 +14996,16 @@ function load_tree(gobj) {
14034
14996
  });
14035
14997
  graph.render().then(() => {
14036
14998
  reanchor(gobj);
14999
+ paint_anchor_mark(gobj);
14037
15000
  if (anchor) {
14038
15001
  restore_view_anchored(gobj, anchor);
14039
15002
  return;
14040
15003
  }
14041
15004
  if (preserve_view) return;
15005
+ if (!priv.restored) {
15006
+ priv.restored = true;
15007
+ if (restore_view_state(gobj)) return;
15008
+ }
14042
15009
  Promise.resolve(graph.zoomTo(1)).then(() => {
14043
15010
  if (!yui_graph_center_on(graph, priv.anchor_id)) yui_graph_center_on(graph, root_id);
14044
15011
  priv.last_zoom = 1;
@@ -14121,46 +15088,17 @@ function refresh_tree(gobj, opts) {
14121
15088
  }
14122
15089
  }
14123
15090
  /************************************************************
14124
- * Rows to show in the popover for a given node data.
14125
- *
14126
- * This is the extension point: to surface a new attribute,
14127
- * collect it into `node_data` in build_gobj_nodes(), then
14128
- * append a `[label, value]` pair here.
14129
- ************************************************************/
14130
- function get_node_info_rows(node_data) {
14131
- let rows = [];
14132
- rows.push([t("gclass"), node_data.gclass || "—"]);
14133
- rows.push([t("name"), node_data.name || "—"]);
14134
- if (node_data.full_name && node_data.full_name !== node_data.short_name) rows.push([t("full name"), node_data.full_name]);
14135
- let role;
14136
- if (node_data.is_yuno) role = "yuno";
14137
- else if (node_data.is_service) role = "service";
14138
- else if (node_data.is_volatil) role = "volatil child";
14139
- else if (node_data.is_pure_child) role = "pure child";
14140
- else role = "child";
14141
- rows.push([t("role"), role]);
14142
- let status;
14143
- if (!node_data.is_running) status = "stopped";
14144
- else if (node_data.is_playing) status = "running + playing";
14145
- else status = "running";
14146
- rows.push([t("status"), status]);
14147
- rows.push([t("state"), node_data.state || "—"]);
14148
- if (node_data.parent_short_name) rows.push([t("parent"), node_data.parent_short_name]);
14149
- if (typeof node_data.num_children === "number") {
14150
- let kids = node_data.num_children;
14151
- if (kids > 0 && node_data.is_collapsed) rows.push([t("children"), kids + " " + t("(collapsed)")]);
14152
- else if (kids > 0) rows.push([t("children"), String(kids)]);
14153
- }
14154
- return rows;
14155
- }
14156
- /************************************************************
14157
15091
  * Render popover body from the info rows of a node.
15092
+ *
15093
+ * The rows come from gobj_tree_model.js, so a node of the
15094
+ * browser yuno and a node of a backend one are read out with
15095
+ * the same words in the same order.
14158
15096
  ************************************************************/
14159
15097
  function render_popover_body(gobj, node_data) {
14160
15098
  let $body = gobj.priv.$popover_body;
14161
15099
  if (!$body) return;
14162
- $body.innerHTML = get_node_info_rows(node_data).map(([label, value]) => `
14163
- <div style="display:grid; grid-template-columns: 90px 1fr; column-gap:10px; padding: 2px 0;">
15100
+ $body.innerHTML = node_info_rows(node_data, t).map(([label, value]) => `
15101
+ <div style="display:grid; grid-template-columns: 96px 1fr; column-gap:10px; padding: 2px 0;">
14164
15102
  <span style="color:var(--bulma-text-weak, #6B7280); font-weight:500;">${escapeHtml(String(label))}</span>
14165
15103
  <span style="color:var(--bulma-text-strong, #1A1A1A); word-break:break-all;">${escapeHtml(String(value))}</span>
14166
15104
  </div>
@@ -14177,6 +15115,7 @@ function show_popover(gobj, node_data, client_x, client_y) {
14177
15115
  let $title = priv.$popover_title;
14178
15116
  let $container = gobj_read_attr(gobj, "$container");
14179
15117
  if (!$popover || !$title || !$container) return;
15118
+ priv.popover_node = node_data;
14180
15119
  $title.textContent = node_data.short_name || node_data.gclass + (node_data.name ? "^" + node_data.name : "");
14181
15120
  $title.title = $title.textContent;
14182
15121
  render_popover_body(gobj, node_data);
@@ -14207,6 +15146,7 @@ function hide_popover(gobj) {
14207
15146
  if (priv.$popover) priv.$popover.style.display = "none";
14208
15147
  if (priv.$popover_body) priv.$popover_body.innerHTML = "";
14209
15148
  if (priv.$popover_title) priv.$popover_title.textContent = "";
15149
+ priv.popover_node = null;
14210
15150
  }
14211
15151
  /************************************************************
14212
15152
  * Generic tree layout engine.
@@ -14469,6 +15409,7 @@ var GobjLanesHLayout = class extends BaseLayout {
14469
15409
  *
14470
15410
  ************************************************************/
14471
15411
  function ac_refresh$3(gobj, event, kw, src) {
15412
+ save_view_state(gobj);
14472
15413
  refresh_tree(gobj);
14473
15414
  return 0;
14474
15415
  }
@@ -14496,6 +15437,8 @@ function ac_change_layout(gobj, event, kw, src) {
14496
15437
  gobj_write_str_attr(gobj, "layout", new_key);
14497
15438
  if (priv.$layout_select && priv.$layout_select.value !== new_key) priv.$layout_select.value = new_key;
14498
15439
  apply_layout(gobj);
15440
+ save_view_state(gobj);
15441
+ save_view_state(gobj);
14499
15442
  refresh_tree(gobj);
14500
15443
  return 0;
14501
15444
  }
@@ -14515,6 +15458,7 @@ function ac_toggle_collapse(gobj, event, kw, src) {
14515
15458
  priv.collapse_state[node_data.full_name] = "expanded";
14516
15459
  for (let child_info of node_data.direct_children || []) if (child_info.has_grandchildren) priv.collapse_state[child_info.full_name] = "collapsed";
14517
15460
  } else priv.collapse_state[node_data.full_name] = "collapsed";
15461
+ save_view_state(gobj);
14518
15462
  refresh_tree(gobj, { anchor });
14519
15463
  return 0;
14520
15464
  }
@@ -14594,10 +15538,31 @@ function reanchor(gobj) {
14594
15538
  ************************************************************/
14595
15539
  function ac_center_anchor$1(gobj, event, kw, src) {
14596
15540
  let priv = gobj.priv;
15541
+ paint_anchor_mark(gobj);
15542
+ save_view_state(gobj);
14597
15543
  if (priv.anchor_state === "on" && priv.anchor_id) yui_graph_center_on(priv.graph, priv.anchor_id);
14598
15544
  return 0;
14599
15545
  }
14600
15546
  /************************************************************
15547
+ * Mark the card the camera is holding.
15548
+ *
15549
+ * A class on the card's own element and not a G6 node state:
15550
+ * G6 paints no state style on an html node.
15551
+ *
15552
+ * Re-applied after every rebuild, because the cards are new
15553
+ * DOM each time and the mark would otherwise last until the
15554
+ * next expand.
15555
+ ************************************************************/
15556
+ function paint_anchor_mark(gobj) {
15557
+ let priv = gobj.priv;
15558
+ let $container = gobj_read_attr(gobj, "$container");
15559
+ if (!$container) return;
15560
+ for (let $card of $container.querySelectorAll(".GOBJ_CARD_ANCHORED")) $card.classList.remove("GOBJ_CARD_ANCHORED");
15561
+ if (priv.anchor_state !== "on" || !priv.anchor_name) return;
15562
+ let $it = $container.querySelector(".GOBJ_CARD[data-gobj-name=\"" + CSS.escape(priv.anchor_name) + "\"]");
15563
+ if ($it) $it.classList.add("GOBJ_CARD_ANCHORED");
15564
+ }
15565
+ /************************************************************
14601
15566
  * Reading, or picking the node to anchor.
14602
15567
  ************************************************************/
14603
15568
  function set_picking_mode(gobj, picking) {
@@ -14625,6 +15590,8 @@ function ac_toggle_anchor$1(gobj, event, kw, src) {
14625
15590
  }
14626
15591
  yui_graph_update_anchor(gobj_read_attr(gobj, "$container"), priv.anchor_state);
14627
15592
  set_picking_mode(gobj, priv.anchor_state === "arming");
15593
+ paint_anchor_mark(gobj);
15594
+ save_view_state(gobj);
14628
15595
  return 0;
14629
15596
  }
14630
15597
  /************************************************************
@@ -14708,6 +15675,7 @@ function ac_node_click$3(gobj, event, kw, src) {
14708
15675
  function ac_show$6(gobj, event, kw, src) {
14709
15676
  let priv = gobj.priv;
14710
15677
  let graph = priv.graph;
15678
+ save_view_state(gobj);
14711
15679
  refresh_tree(gobj);
14712
15680
  if (graph) {
14713
15681
  let $canvas = document.getElementById(priv.canvas_id);
@@ -14748,6 +15716,68 @@ function ac_hide$5(gobj, event, kw, src) {
14748
15716
  hide_popover(gobj);
14749
15717
  return 0;
14750
15718
  }
15719
+ /************************************************************
15720
+ * Open the gclass of the node the popover is showing.
15721
+ *
15722
+ * One window at a time: opening a second gclass replaces the
15723
+ * first rather than stacking windows nobody closes.
15724
+ ************************************************************/
15725
+ function ac_open_gclass(gobj, event, kw, src) {
15726
+ let priv = gobj.priv;
15727
+ let node_data = priv.popover_node;
15728
+ if (!node_data || !node_data.gclass) {
15729
+ log_error(`${gobj_short_name(gobj)}: EV_OPEN_GCLASS with no node`);
15730
+ return -1;
15731
+ }
15732
+ close_gclass_view(priv.gclass_view);
15733
+ priv.gclass_view = null;
15734
+ priv.gclass_view = open_gclass_view(gobj, node_data.gclass, {
15735
+ title_prefix: gobj_yuno_name() || "",
15736
+ on_close: () => {
15737
+ if (gobj_is_destroying(gobj)) return;
15738
+ gobj_send_event(gobj, "EV_GCLASS_CLOSED", {}, gobj);
15739
+ }
15740
+ });
15741
+ return 0;
15742
+ }
15743
+ /************************************************************
15744
+ * The reader dismissed the gclass window (its own ✕).
15745
+ *
15746
+ * C_YUI_WINDOW.close_window() calls on_close and THEN destroys
15747
+ * itself, so here we only drop our reference -- destroying it
15748
+ * again would destroy it twice.
15749
+ ************************************************************/
15750
+ function ac_gclass_closed(gobj, event, kw, src) {
15751
+ gobj.priv.gclass_view = null;
15752
+ return 0;
15753
+ }
15754
+ /************************************************************
15755
+ * The gclass viewer asked for a subtree.
15756
+ *
15757
+ * It cannot: the gclass description is a COMPLETE document,
15758
+ * it carries no `__collapsed__` stub, so nothing in it is
15759
+ * fetchable. The event is declared because the viewer is
15760
+ * hosted as a child and subscribes its host to everything it
15761
+ * publishes -- and it is logged, because arriving here means
15762
+ * the document was not the one we built.
15763
+ ************************************************************/
15764
+ function ac_gclass_expand_path(gobj, event, kw, src) {
15765
+ log_error(`${gobj_short_name(gobj)}: EV_EXPAND_PATH on a complete document: ${kw && kw.path}`);
15766
+ return -1;
15767
+ }
15768
+ /************************************************************
15769
+ * The app switched language.
15770
+ *
15771
+ * A rebuild, not a re-translation: the words live inside the
15772
+ * innerHTML of G6 nodes, where refresh_language() cannot go.
15773
+ ************************************************************/
15774
+ function ac_language_changed$3(gobj, event, kw, src) {
15775
+ let $container = gobj_read_attr(gobj, "$container");
15776
+ if ($container) refresh_language($container, t);
15777
+ hide_popover(gobj);
15778
+ refresh_tree(gobj);
15779
+ return 0;
15780
+ }
14751
15781
  /***************************
14752
15782
  * FSM
14753
15783
  ***************************/
@@ -14781,7 +15811,11 @@ function create_gclass$9(gclass_name) {
14781
15811
  ["EV_CENTER_ANCHOR", 0],
14782
15812
  ["EV_RESIZE", 0],
14783
15813
  ["EV_SHOW", 0],
14784
- ["EV_HIDE", 0]
15814
+ ["EV_HIDE", 0],
15815
+ ["EV_OPEN_GCLASS", 0],
15816
+ ["EV_GCLASS_CLOSED", 0],
15817
+ ["EV_EXPAND_PATH", 0],
15818
+ ["EV_LANGUAGE_CHANGED", 0]
14785
15819
  ], [["ST_IDLE", [
14786
15820
  [
14787
15821
  "EV_REFRESH",
@@ -14862,6 +15896,26 @@ function create_gclass$9(gclass_name) {
14862
15896
  "EV_HIDE",
14863
15897
  ac_hide$5,
14864
15898
  null
15899
+ ],
15900
+ [
15901
+ "EV_OPEN_GCLASS",
15902
+ ac_open_gclass,
15903
+ null
15904
+ ],
15905
+ [
15906
+ "EV_GCLASS_CLOSED",
15907
+ ac_gclass_closed,
15908
+ null
15909
+ ],
15910
+ [
15911
+ "EV_EXPAND_PATH",
15912
+ ac_gclass_expand_path,
15913
+ null
15914
+ ],
15915
+ [
15916
+ "EV_LANGUAGE_CHANGED",
15917
+ ac_language_changed$3,
15918
+ null
14865
15919
  ]
14866
15920
  ]]], gmt$9, 0, attrs_table$9, PRIVATE_DATA$9, 0, 0, 0, 0);
14867
15921
  if (!__gclass__$9) return -1;
@@ -49026,7 +50080,7 @@ function configure_toolbar(gobj) {
49026
50080
  {
49027
50081
  id: "g6-icon-anchor",
49028
50082
  value: "anchor",
49029
- className: "EV_TOGGLE_ANCHOR" + (priv.anchor_state !== "off" ? " pressed_state" : "") + (priv.anchor_state === "arming" ? " color_pending_state" : ""),
50083
+ className: "EV_TOGGLE_ANCHOR" + (priv.anchor_state === "on" ? " pressed_state" : "") + (priv.anchor_state === "arming" ? " color_pending_state" : ""),
49030
50084
  title: anchor_title(priv.anchor_state)
49031
50085
  }
49032
50086
  ];
@@ -51935,15 +52989,15 @@ function refresh_minimap(gobj) {
51935
52989
  * same three-way choice — it lived inline in the theme refresh and is
51936
52990
  * shared now. Returns null for a node that carries no desc.
51937
52991
  ************************************************************/
51938
- function node_innerHTML_of(nd, theme, highlight, selected) {
52992
+ function node_innerHTML_of(nd, theme, highlight, selected, anchored) {
51939
52993
  if (!nd || !nd.data || !nd.data.desc) return null;
51940
52994
  let desc = nd.data.desc;
51941
52995
  let record = nd.data.record || {};
51942
52996
  let label = node_label(desc, record);
51943
52997
  switch (desc.node_treedb_type) {
51944
- case "child": return build_chip_innerHTML(desc.color, theme, record.icon, label, record.id, highlight, selected);
51945
- case "extended": return build_node_innerHTML(desc.color, theme, record.icon, label, desc.topic_name, true, record.id, highlight, selected);
51946
- case "hierarchical": return build_node_innerHTML(desc.color, theme, record.icon, label, desc.topic_name, false, record.id, highlight, selected);
52998
+ case "child": return build_chip_innerHTML(desc.color, theme, record.icon, label, record.id, highlight, selected, anchored);
52999
+ case "extended": return build_node_innerHTML(desc.color, theme, record.icon, label, desc.topic_name, true, record.id, highlight, selected, anchored);
53000
+ case "hierarchical": return build_node_innerHTML(desc.color, theme, record.icon, label, desc.topic_name, false, record.id, highlight, selected, anchored);
51947
53001
  }
51948
53002
  return null;
51949
53003
  }
@@ -51964,7 +53018,7 @@ function repaint_cards(gobj, ids) {
51964
53018
  let selected = new Set(priv._selected_paint_ids || []);
51965
53019
  let updates = [];
51966
53020
  for (let id of ids) {
51967
- let html = node_innerHTML_of(graph.getNodeData(id), priv.theme, focus.has(id), selected.has(id));
53021
+ let html = node_innerHTML_of(graph.getNodeData(id), priv.theme, focus.has(id), selected.has(id), priv.anchor_state === "on" && id === priv.anchor_id);
51968
53022
  if (html !== null) updates.push({
51969
53023
  id,
51970
53024
  style: { innerHTML: html }
@@ -52038,6 +53092,23 @@ function refresh_default_edges_theme(gobj, theme) {
52038
53092
  * it, which is also the order that reads correctly when only one
52039
53093
  * of them is on.
52040
53094
  ************************************************************/
53095
+ /************************************************************
53096
+ * The mark of the card the CAMERA is holding.
53097
+ *
53098
+ * An outline and not another ring, and DASHED: the two rings
53099
+ * already mean things here -- amber is a find match or a
53100
+ * focused topic, blue is the selection -- and a third solid
53101
+ * ring would be a third thing to learn in the same channel.
53102
+ * A dash is a different channel, so it reads beside either of
53103
+ * them, and it says "provisional" the way a camera state is.
53104
+ *
53105
+ * The same dash the other two viewers draw: this is one
53106
+ * feature with one control, and it should not be three
53107
+ * drawings.
53108
+ ************************************************************/
53109
+ function anchor_outline(anchored) {
53110
+ return anchored ? "outline: 3px dashed #fa8c16; outline-offset: 2px;" : "";
53111
+ }
52041
53112
  function ring_shadow(highlight, selected, base_shadow) {
52042
53113
  let rings = [];
52043
53114
  if (highlight) rings.push(`0 0 0 4px ${HIGHLIGHT_HALO}`);
@@ -52052,7 +53123,7 @@ function ring_shadow(highlight, selected, base_shadow) {
52052
53123
  * but lighter (1px border, no shadow, single line). The name is
52053
53124
  * always legible (ellipsis + native title tooltip on overflow).
52054
53125
  ************************************************************/
52055
- function build_chip_innerHTML(color, theme, icon, label, key, highlight, selected) {
53126
+ function build_chip_innerHTML(color, theme, icon, label, key, highlight, selected, anchored) {
52056
53127
  let title = key || label;
52057
53128
  let dark = theme === "dark";
52058
53129
  let bg = dark ? `color-mix(in srgb, ${color} 30%, #2c3542)` : `color-mix(in srgb, ${color} 10%, ${dark ? "#1b2230" : "#ffffff"})`;
@@ -52065,13 +53136,14 @@ function build_chip_innerHTML(color, theme, icon, label, key, highlight, selecte
52065
53136
  margin-right: 6px; flex: 0 0 auto;
52066
53137
  "/>`;
52067
53138
  return `
52068
- <div title="${escapeHtml(title)}" style="
53139
+ <div class="TREEDB_CARD" title="${escapeHtml(title)}" style="
52069
53140
  box-sizing: border-box;
52070
53141
  width: 100%;
52071
53142
  height: 100%;
52072
53143
  background: ${bg};
52073
53144
  border: ${highlight ? "3px" : "1px"} solid ${border};
52074
53145
  ${ring_shadow(highlight, selected, "")}
53146
+ ${anchor_outline(anchored)}
52075
53147
  border-radius: 8px;
52076
53148
  color: ${text_color};
52077
53149
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
@@ -52097,7 +53169,7 @@ function build_chip_innerHTML(color, theme, icon, label, key, highlight, selecte
52097
53169
  * colour is kept (per-topic differentiation) but softened via
52098
53170
  * color-mix instead of a harsh saturated fill. Theme-aware.
52099
53171
  ************************************************************/
52100
- function build_node_innerHTML(color, theme, icon, label, topic_name, structural, key, highlight, selected) {
53172
+ function build_node_innerHTML(color, theme, icon, label, topic_name, structural, key, highlight, selected, anchored) {
52101
53173
  let title = key || label;
52102
53174
  let dark = theme === "dark";
52103
53175
  let surface = dark ? "#1b2230" : "#ffffff";
@@ -52132,7 +53204,7 @@ function build_node_innerHTML(color, theme, icon, label, topic_name, structural,
52132
53204
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
52133
53205
  ">${escapeHtml(topic_name)}</div>`;
52134
53206
  return `
52135
- <div title="${escapeHtml(title)}" style="
53207
+ <div class="TREEDB_CARD" title="${escapeHtml(title)}" style="
52136
53208
  box-sizing: border-box;
52137
53209
  width: 100%;
52138
53210
  height: 100%;
@@ -52140,6 +53212,7 @@ function build_node_innerHTML(color, theme, icon, label, topic_name, structural,
52140
53212
  border: ${highlight ? "3px" : "1.5px"} ${border_style} ${border};
52141
53213
  border-radius: 10px;
52142
53214
  ${ring_shadow(highlight, selected, shadow)}
53215
+ ${anchor_outline(anchored)}
52143
53216
  color: ${title_color};
52144
53217
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
52145
53218
  display: flex;
@@ -53123,12 +54196,14 @@ function after_camera(gobj, moved) {
53123
54196
  ************************************************************/
53124
54197
  function ac_toggle_anchor(gobj, event, kw, src) {
53125
54198
  let priv = gobj.priv;
54199
+ let was = priv.anchor_id;
53126
54200
  if (priv.anchor_state === "off") priv.anchor_state = "arming";
53127
54201
  else {
53128
54202
  priv.anchor_state = "off";
53129
54203
  priv.anchor_id = "";
53130
54204
  }
53131
54205
  update_toolbar(gobj);
54206
+ if (was) repaint_cards(gobj, /* @__PURE__ */ new Set([was]));
53132
54207
  return 0;
53133
54208
  }
53134
54209
  /************************************************************
@@ -53306,6 +54381,7 @@ function ac_node_click$2(gobj, event, kw, src) {
53306
54381
  priv.anchor_id = node_id;
53307
54382
  priv.anchor_state = "on";
53308
54383
  update_toolbar(gobj);
54384
+ repaint_cards(gobj, /* @__PURE__ */ new Set([node_id]));
53309
54385
  gobj_post_event(gobj, "EV_CENTER_ANCHOR", {}, gobj);
53310
54386
  return 0;
53311
54387
  }