@yuneta/gobj-ui 7.23.29 → 7.23.31

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;
@@ -10978,6 +10978,26 @@ function make_toolbar$3(gobj) {
10978
10978
  return $toolbar;
10979
10979
  }
10980
10980
  /************************************************************
10981
+ * The element G6 mounts its canvas in.
10982
+ *
10983
+ * Read from THIS gclass's own $container, never with
10984
+ * `document.getElementById()`: a view is not always in the
10985
+ * document when it is started -- a host that builds the
10986
+ * viewer and mounts it into a window afterwards starts it
10987
+ * DETACHED -- and a global lookup answers null there. It cost
10988
+ * a resize that never worked: the ResizeObserver below was
10989
+ * simply never attached, silently, and the canvas kept the
10990
+ * size it was born with for the life of the window.
10991
+ *
10992
+ * Null only if the UI was never built; that is a bug, and it
10993
+ * is logged where it matters rather than skipped.
10994
+ ************************************************************/
10995
+ function canvas_mount$1(gobj) {
10996
+ let $container = gobj_read_attr(gobj, "$container");
10997
+ if (!$container) return null;
10998
+ return $container.querySelector("#" + gobj.priv.canvas_id);
10999
+ }
11000
+ /************************************************************
10981
11001
  * Build a G6 graph instance
10982
11002
  ************************************************************/
10983
11003
  function build_graph$3(gobj) {
@@ -11021,8 +11041,9 @@ function build_graph$3(gobj) {
11021
11041
  });
11022
11042
  graph.on(CanvasEvent.CLICK, (evt) => {});
11023
11043
  if (typeof ResizeObserver !== "undefined") {
11024
- let $canvas = document.getElementById(priv.canvas_id);
11025
- if ($canvas) {
11044
+ let $canvas = canvas_mount$1(gobj);
11045
+ if (!$canvas) log_error(`${gobj_short_name(gobj)}: no canvas mount, the graph will never resize`);
11046
+ else {
11026
11047
  let ro = new ResizeObserver(() => {
11027
11048
  if (priv._resize_raf) cancelAnimationFrame(priv._resize_raf);
11028
11049
  priv._resize_raf = requestAnimationFrame(() => {
@@ -11959,10 +11980,9 @@ function ac_node_click$4(gobj, event, kw, src) {
11959
11980
  *
11960
11981
  ************************************************************/
11961
11982
  function ac_show$8(gobj, event, kw, src) {
11962
- let priv = gobj.priv;
11963
- let graph = priv.graph;
11983
+ let graph = gobj.priv.graph;
11964
11984
  if (graph) {
11965
- let $canvas = document.getElementById(priv.canvas_id);
11985
+ let $canvas = canvas_mount$1(gobj);
11966
11986
  if ($canvas) {
11967
11987
  let cw = $canvas.clientWidth;
11968
11988
  let ch = $canvas.clientHeight;
@@ -11981,10 +12001,9 @@ function ac_show$8(gobj, event, kw, src) {
11981
12001
  * No fitView: preserve the user's current pan/zoom.
11982
12002
  ************************************************************/
11983
12003
  function ac_resize$2(gobj, event, kw, src) {
11984
- let priv = gobj.priv;
11985
- let graph = priv.graph;
12004
+ let graph = gobj.priv.graph;
11986
12005
  if (graph) {
11987
- let $canvas = document.getElementById(priv.canvas_id);
12006
+ let $canvas = canvas_mount$1(gobj);
11988
12007
  if ($canvas) {
11989
12008
  let cw = $canvas.clientWidth;
11990
12009
  let ch = $canvas.clientHeight;
@@ -13200,7 +13219,7 @@ function ac_json_item_clicked(gobj, event, kw, src) {
13200
13219
  /************************************************************
13201
13220
  * EV_LANGUAGE_CHANGED — re-translate chrome + re-render
13202
13221
  ************************************************************/
13203
- function ac_language_changed$3(gobj, event, kw, src) {
13222
+ function ac_language_changed$4(gobj, event, kw, src) {
13204
13223
  let priv = gobj.priv;
13205
13224
  let $container = gobj_read_attr(gobj, "$container");
13206
13225
  if ($container) {
@@ -13261,7 +13280,7 @@ function create_gclass$10(gclass_name) {
13261
13280
  ],
13262
13281
  [
13263
13282
  "EV_LANGUAGE_CHANGED",
13264
- ac_language_changed$3,
13283
+ ac_language_changed$4,
13265
13284
  null
13266
13285
  ],
13267
13286
  [
@@ -13337,7 +13356,7 @@ function create_gclass$10(gclass_name) {
13337
13356
  ],
13338
13357
  [
13339
13358
  "EV_LANGUAGE_CHANGED",
13340
- ac_language_changed$3,
13359
+ ac_language_changed$4,
13341
13360
  null
13342
13361
  ],
13343
13362
  [
@@ -13387,6 +13406,568 @@ function register_c_yui_json() {
13387
13406
  return create_gclass$10(GCLASS_NAME$10);
13388
13407
  }
13389
13408
  //#endregion
13409
+ //#region src/gobj_tree_model.js
13410
+ /***********************************************************************
13411
+ * gobj_tree_model.js
13412
+ *
13413
+ * The gobj tree of a yuno, as plain data.
13414
+ *
13415
+ * The view (C_YUI_GOBJ_TREE_JS) draws NODE DESCRIPTORS, never
13416
+ * gobjs. There are two producers of a descriptor, because the
13417
+ * view has to show the gobjs of the BACKEND too:
13418
+ *
13419
+ * - describe_js_gobj() the gobj tree of THIS browser yuno
13420
+ * - describe_backend_tree() the answer of the backend command
13421
+ * `view-gobj-tree` of another yuno
13422
+ *
13423
+ * Both answer the SAME shape, and that shape is the backend's:
13424
+ * the field names are the ones `gobj2json()` writes in
13425
+ * `kernel/c/gobj-c/src/gobj.c` (fullname, shortname, gclass,
13426
+ * name, parent, state, running, playing, service, disabled,
13427
+ * volatil, bottom_gobj, gobj_flags, commands, gobj_trace_level).
13428
+ * Anything the browser runtime cannot answer is simply absent,
13429
+ * and every reader here treats absent as "not known", never as
13430
+ * false.
13431
+ *
13432
+ * Copyright (c) 2026, ArtGins.
13433
+ * All Rights Reserved.
13434
+ ***********************************************************************/
13435
+ /************************************************************
13436
+ * The role of a node: "yuno" | "service" | "volatil" | "pure" | "child"
13437
+ ************************************************************/
13438
+ function node_role(d) {
13439
+ if (!d) return "child";
13440
+ if (d.is_yuno) return "yuno";
13441
+ if (d.service) return "service";
13442
+ if (d.volatil) return "volatil";
13443
+ if (d.pure_child) return "pure";
13444
+ return "child";
13445
+ }
13446
+ /************************************************************
13447
+ * What the node is doing: "stopped" | "running" | "playing"
13448
+ ************************************************************/
13449
+ function node_status(d) {
13450
+ if (!d || !d.running) return "stopped";
13451
+ return d.playing ? "playing" : "running";
13452
+ }
13453
+ /************************************************************
13454
+ * The i18n key of the status, which is NOT the status itself:
13455
+ * a gobj that runs and does not play is PAUSED to the reader,
13456
+ * and "running" alone never said which of the two it was.
13457
+ ************************************************************/
13458
+ function node_status_key(d) {
13459
+ switch (node_status(d)) {
13460
+ case "playing": return "playing";
13461
+ case "running": return "running (paused)";
13462
+ default: return "stopped";
13463
+ }
13464
+ }
13465
+ /******************************
13466
+ * Producers
13467
+ ******************************/
13468
+ /************************************************************
13469
+ * A descriptor of a LIVE gobj of this browser yuno, children
13470
+ * included (the whole subtree, depth-first).
13471
+ *
13472
+ * `is_root` marks the yuno itself: the JS runtime has no
13473
+ * gobj_flag_yuno test of its own.
13474
+ ************************************************************/
13475
+ function describe_js_gobj(target_gobj, is_root) {
13476
+ if (!target_gobj) return null;
13477
+ let parent_gobj = gobj_parent(target_gobj);
13478
+ let bottom = gobj_bottom_gobj(target_gobj);
13479
+ let d = {
13480
+ gclass: gobj_gclass_name(target_gobj) || "",
13481
+ name: gobj_name(target_gobj) || "",
13482
+ shortname: gobj_short_name(target_gobj) || "",
13483
+ fullname: gobj_full_name(target_gobj) || "",
13484
+ parent: parent_gobj ? gobj_short_name(parent_gobj) || "" : "",
13485
+ state: gobj_current_state(target_gobj) || "",
13486
+ running: !!gobj_is_running(target_gobj),
13487
+ playing: !!gobj_is_playing(target_gobj),
13488
+ service: !!gobj_is_service(target_gobj),
13489
+ volatil: !!gobj_is_volatil(target_gobj),
13490
+ pure_child: !!gobj_is_pure_child(target_gobj),
13491
+ disabled: !!target_gobj.disabled,
13492
+ bottom_gobj: bottom ? gobj_short_name(bottom) || "" : "",
13493
+ commands: !!(target_gobj.gclass && target_gobj.gclass.command_table),
13494
+ is_yuno: !!is_root,
13495
+ children: []
13496
+ };
13497
+ let children = target_gobj.dl_children || [];
13498
+ for (let child of children) {
13499
+ let cd = describe_js_gobj(child, false);
13500
+ if (cd) d.children.push(cd);
13501
+ }
13502
+ return d;
13503
+ }
13504
+ /******************************
13505
+ * Readouts
13506
+ ******************************/
13507
+ /************************************************************
13508
+ * The i18n key of a role. Two of the five roles are not the
13509
+ * word the reader wants ("pure" and "volatil" alone say
13510
+ * nothing), so the map is explicit.
13511
+ ************************************************************/
13512
+ function role_i18n_key(role) {
13513
+ switch (role) {
13514
+ case "yuno": return "yuno";
13515
+ case "service": return "service";
13516
+ case "volatil": return "volatil child";
13517
+ case "pure": return "pure child";
13518
+ default: return "child";
13519
+ }
13520
+ }
13521
+ /************************************************************
13522
+ * The marks a node carries besides its role: what the reader
13523
+ * would otherwise have to open the popover to learn.
13524
+ *
13525
+ * Returns i18n KEYS, in a fixed order, so the card and the
13526
+ * popover cannot disagree about what a node is.
13527
+ ************************************************************/
13528
+ function node_badge_keys(d) {
13529
+ let badges = [];
13530
+ if (!d) return badges;
13531
+ let role = node_role(d);
13532
+ if (role !== "child") badges.push(role_i18n_key(role));
13533
+ if (d.disabled) badges.push("disabled");
13534
+ if (d.bottom_gobj) badges.push("bottom");
13535
+ if (d.commands) badges.push("commands");
13536
+ return badges;
13537
+ }
13538
+ /************************************************************
13539
+ * The vocabulary of this view, translated.
13540
+ *
13541
+ * Written as literal t("...") calls and not as t(key): a key
13542
+ * reached through a variable is invisible to the locale
13543
+ * validator, which is how a fixed vocabulary ships in one
13544
+ * language and never changes it.
13545
+ ************************************************************/
13546
+ function node_labels(t) {
13547
+ if (typeof t !== "function") t = (k) => k;
13548
+ return {
13549
+ role: {
13550
+ "yuno": t("yuno"),
13551
+ "service": t("service"),
13552
+ "volatil child": t("volatil child"),
13553
+ "pure child": t("pure child"),
13554
+ "child": t("child")
13555
+ },
13556
+ status: {
13557
+ "playing": t("playing"),
13558
+ "running (paused)": t("running (paused)"),
13559
+ "stopped": t("stopped")
13560
+ },
13561
+ status_short: {
13562
+ "playing": t("playing"),
13563
+ "running": t("running"),
13564
+ "stopped": t("stopped")
13565
+ },
13566
+ badge: {
13567
+ "yuno": t("yuno"),
13568
+ "service": t("service"),
13569
+ "volatil child": t("volatil child"),
13570
+ "pure child": t("pure child"),
13571
+ "disabled": t("disabled"),
13572
+ "bottom": t("bottom"),
13573
+ "commands": t("commands")
13574
+ },
13575
+ field: {
13576
+ gclass: t("gclass"),
13577
+ name: t("name"),
13578
+ full_name: t("full name"),
13579
+ role: t("role"),
13580
+ status: t("status"),
13581
+ state: t("state"),
13582
+ disabled: t("disabled"),
13583
+ parent: t("parent"),
13584
+ bottom_gobj: t("bottom gobj"),
13585
+ commands: t("commands"),
13586
+ traces: t("traces"),
13587
+ flags: t("flags"),
13588
+ children: t("children"),
13589
+ yes: t("yes"),
13590
+ collapsed: t("(collapsed)")
13591
+ }
13592
+ };
13593
+ }
13594
+ /************************************************************
13595
+ * The rows of the popover: [label, value], both translated.
13596
+ *
13597
+ * `t` is the app's translator -- the library has none of its
13598
+ * own. Values that are a fixed vocabulary (the role, the
13599
+ * status, yes) go through it too; free text does not.
13600
+ ************************************************************/
13601
+ function node_info_rows(d, t) {
13602
+ let rows = [];
13603
+ if (!d) return rows;
13604
+ let L = node_labels(t);
13605
+ let f = L.field;
13606
+ rows.push([f.gclass, d.gclass || "—"]);
13607
+ rows.push([f.name, d.name || "—"]);
13608
+ if (d.fullname && d.fullname !== d.shortname) rows.push([f.full_name, d.fullname]);
13609
+ rows.push([f.role, L.role[role_i18n_key(node_role(d))]]);
13610
+ rows.push([f.status, L.status[node_status_key(d)]]);
13611
+ rows.push([f.state, d.state || "—"]);
13612
+ if (d.disabled) rows.push([f.disabled, f.yes]);
13613
+ if (d.parent) rows.push([f.parent, d.parent]);
13614
+ if (d.bottom_gobj) rows.push([f.bottom_gobj, d.bottom_gobj]);
13615
+ if (d.commands) rows.push([f.commands, f.yes]);
13616
+ if (Array.isArray(d.gobj_trace_level) && d.gobj_trace_level.length > 0) rows.push([f.traces, d.gobj_trace_level.join(", ")]);
13617
+ if (Array.isArray(d.gobj_flags) && d.gobj_flags.length > 0) rows.push([f.flags, d.gobj_flags.join(", ")]);
13618
+ let kids = Array.isArray(d.children) ? d.children.length : 0;
13619
+ if (kids > 0) rows.push([f.children, d.is_collapsed ? kids + " " + f.collapsed : String(kids)]);
13620
+ return rows;
13621
+ }
13622
+ //#endregion
13623
+ //#region src/gclass_describe.js
13624
+ /***********************************************************************
13625
+ * gclass_describe.js
13626
+ *
13627
+ * A gclass, as plain JSON, in the shape the BACKEND answers.
13628
+ *
13629
+ * There is no gclass viewer in the framework: the C kernel
13630
+ * answers `view-gclass` with `gclass2json()`
13631
+ * (`kernel/c/gobj-c/src/gobj.c`) and nothing draws it. This
13632
+ * module builds the same document for a gclass of the BROWSER
13633
+ * yuno, out of the gobj-js registry, so one viewer can show a
13634
+ * gclass of either side:
13635
+ *
13636
+ * {id, gcflag, attrs, commands, gclass_methods,
13637
+ * internal_methods, FSM: {events, states}}
13638
+ *
13639
+ * Two keys the backend does not carry are added at the end,
13640
+ * because the browser can answer them for free and the reader
13641
+ * asks them first: `instances` and `trace_levels`.
13642
+ *
13643
+ * Copyright (c) 2026, ArtGins.
13644
+ * All Rights Reserved.
13645
+ ***********************************************************************/
13646
+ /***************************************************************
13647
+ * Bit tables
13648
+ ***************************************************************/
13649
+ var DATA_TYPE_NAMES = {
13650
+ [data_type_t.DTP_STRING]: "DTP_STRING",
13651
+ [data_type_t.DTP_BOOLEAN]: "DTP_BOOLEAN",
13652
+ [data_type_t.DTP_INTEGER]: "DTP_INTEGER",
13653
+ [data_type_t.DTP_REAL]: "DTP_REAL",
13654
+ [data_type_t.DTP_LIST]: "DTP_LIST",
13655
+ [data_type_t.DTP_DICT]: "DTP_DICT",
13656
+ [data_type_t.DTP_JSON]: "DTP_JSON",
13657
+ [data_type_t.DTP_POINTER]: "DTP_POINTER"
13658
+ };
13659
+ /************************************************************
13660
+ * The names of the bits set in `bits`, in table order.
13661
+ ************************************************************/
13662
+ function bits2names(table, bits) {
13663
+ let names = [];
13664
+ let value = Number(bits) || 0;
13665
+ for (let name of Object.keys(table)) if ((value & table[name]) !== 0) names.push(name);
13666
+ return names;
13667
+ }
13668
+ /************************************************************
13669
+ * The name of a data type. An unknown type is reported as
13670
+ * the number it is -- silently drawing "DTP_STRING" for a
13671
+ * type nobody registered would be worse than saying "17".
13672
+ ************************************************************/
13673
+ function data_type_name(type) {
13674
+ return DATA_TYPE_NAMES[type] || String(type);
13675
+ }
13676
+ /************************************************************
13677
+ * One SDATA descriptor -> one row.
13678
+ *
13679
+ * A DTP_POINTER default is a live object (a DOM node, a
13680
+ * gobj): it is never printed, only named, because a viewer
13681
+ * that serializes it would either throw or dump the page.
13682
+ ************************************************************/
13683
+ function sdatadesc2json(it) {
13684
+ let row = {
13685
+ id: it.name,
13686
+ type: data_type_name(it.type),
13687
+ flag: bits2names(sdata_flag_t, it.flag),
13688
+ description: it.description || ""
13689
+ };
13690
+ if (it.type === data_type_t.DTP_POINTER) row.default_value = it.default_value === null || it.default_value === void 0 ? null : "<pointer>";
13691
+ else row.default_value = it.default_value === void 0 ? null : it.default_value;
13692
+ return row;
13693
+ }
13694
+ /************************************************************
13695
+ * The attrs table of a gclass.
13696
+ ************************************************************/
13697
+ function attrs2json(attrs_table) {
13698
+ let rows = [];
13699
+ if (!Array.isArray(attrs_table)) return rows;
13700
+ for (let it of attrs_table) {
13701
+ if (!it || !it.name) continue;
13702
+ rows.push(sdatadesc2json(it));
13703
+ }
13704
+ return rows;
13705
+ }
13706
+ /************************************************************
13707
+ * The command table of a gclass, with the parameters of
13708
+ * each command (the `schema` slot of SDATACM).
13709
+ ************************************************************/
13710
+ function commands2json(command_table) {
13711
+ let rows = [];
13712
+ if (!Array.isArray(command_table)) return rows;
13713
+ for (let it of command_table) {
13714
+ if (!it || !it.name) continue;
13715
+ let row = {
13716
+ id: it.name,
13717
+ alias: it.alias || [],
13718
+ description: it.description || ""
13719
+ };
13720
+ if (Array.isArray(it.schema)) row.parameters = attrs2json(it.schema);
13721
+ rows.push(row);
13722
+ }
13723
+ return rows;
13724
+ }
13725
+ /************************************************************
13726
+ * The methods the gclass actually implements. A table of
13727
+ * every possible one, mostly empty, tells the reader nothing.
13728
+ ************************************************************/
13729
+ function methods2json(mt) {
13730
+ let names = [];
13731
+ if (!mt || typeof mt !== "object") return names;
13732
+ for (let key of Object.keys(mt)) if (typeof mt[key] === "function") names.push(key);
13733
+ return names;
13734
+ }
13735
+ /************************************************************
13736
+ * The FSM: the events the gclass declares, and what each
13737
+ * state does with them.
13738
+ ************************************************************/
13739
+ function fsm2json(gclass) {
13740
+ let events = [];
13741
+ for (let ev of gclass.dl_events || []) events.push({
13742
+ event_name: ev.event_name,
13743
+ event_flag: bits2names(event_flag_t, ev.event_flag)
13744
+ });
13745
+ let states = {};
13746
+ for (let st of gclass.dl_states || []) {
13747
+ let actions = [];
13748
+ for (let ac of st.dl_actions || []) actions.push({
13749
+ event_name: ac.event_name,
13750
+ action: ac.action ? ac.action.name || "action" : "",
13751
+ next_state: ac.next_state || ""
13752
+ });
13753
+ states[st.state_name] = actions;
13754
+ }
13755
+ return {
13756
+ events,
13757
+ states
13758
+ };
13759
+ }
13760
+ /************************************************************
13761
+ * The description of a gclass REGISTERED IN THIS BROWSER YUNO,
13762
+ * or null when the registry does not know that name.
13763
+ *
13764
+ * Null is the answer for a gclass of a BACKEND yuno too: the
13765
+ * browser registry carries the GUI gclasses and nothing else,
13766
+ * so the caller asks the backend `view-gclass` for those and
13767
+ * shows whatever it answers -- this same shape.
13768
+ ************************************************************/
13769
+ function describe_local_gclass(gclass_name) {
13770
+ if (!gclass_name) return null;
13771
+ let gclass = gclass_find_by_name(gclass_name, false);
13772
+ if (!gclass) return null;
13773
+ return {
13774
+ id: gclass.gclass_name,
13775
+ gcflag: bits2names(gclass_flag_t, gclass.gclass_flag),
13776
+ attrs: attrs2json(gclass.attrs_table),
13777
+ commands: commands2json(gclass.command_table),
13778
+ gclass_methods: methods2json(gclass.gmt),
13779
+ internal_methods: methods2json(gclass.lmt),
13780
+ FSM: fsm2json(gclass),
13781
+ instances: Number(gclass.instances) || 0,
13782
+ trace_levels: Array.isArray(gclass.s_user_trace_level) ? gclass.s_user_trace_level : []
13783
+ };
13784
+ }
13785
+ //#endregion
13786
+ //#region src/yui_gclass_view.js
13787
+ /***********************************************************************
13788
+ * yui_gclass_view.js
13789
+ *
13790
+ * The gclass viewer: what a GClass IS, in a window.
13791
+ *
13792
+ * The framework had no such viewer. The C kernel answers
13793
+ * `view-gclass` with a full description (attrs, commands,
13794
+ * methods, FSM) and the only reader of it was a terminal.
13795
+ * This one draws that same document with C_YUI_JSON, so the
13796
+ * FSM of a gclass is a graph and its attrs table is a tree,
13797
+ * and it takes the document from either side:
13798
+ *
13799
+ * - a gclass of the browser yuno -> describe_local_gclass()
13800
+ * - a gclass of a backend yuno -> `opts.description`, the
13801
+ * answer of `view-gclass`
13802
+ *
13803
+ * One window at a time per host: the caller keeps the handle
13804
+ * and closes it, the way the treedb graph keeps its raw-JSON
13805
+ * viewer.
13806
+ *
13807
+ * Copyright (c) 2026, ArtGins.
13808
+ * All Rights Reserved.
13809
+ ***********************************************************************/
13810
+ /************************************************************
13811
+ * Can this app show a gclass at all?
13812
+ *
13813
+ * The viewer is C_YUI_JSON, and an app registers only the
13814
+ * gclasses it mounts. The caller asks this BEFORE drawing a
13815
+ * button, so an app that does not carry the viewer shows no
13816
+ * control instead of a control that fails.
13817
+ ************************************************************/
13818
+ function gclass_view_available() {
13819
+ return !!gclass_find_by_name("C_YUI_JSON", false);
13820
+ }
13821
+ /************************************************************
13822
+ * Open the viewer on `gclass_name`.
13823
+ *
13824
+ * opts:
13825
+ * description the document to draw. Omitted, it is
13826
+ * built from the browser registry.
13827
+ * title_prefix what the window says before the gclass
13828
+ * name (the yuno it belongs to).
13829
+ * on_close called when the reader dismisses it.
13830
+ *
13831
+ * Returns a handle for close_gclass_view(), or null (and a
13832
+ * logged reason) when there is nothing to show.
13833
+ ************************************************************/
13834
+ function open_gclass_view(host, gclass_name, opts) {
13835
+ opts = opts || {};
13836
+ if (!host || !gclass_name) {
13837
+ log_error(`yui_gclass_view: open without a host or a gclass name`);
13838
+ return null;
13839
+ }
13840
+ let shell = yui_shell_of(host);
13841
+ if (!gclass_view_available()) {
13842
+ log_error(`${gobj_short_name(host)}: C_YUI_JSON not registered by the app`);
13843
+ yui_shell_show_error(shell, "gclass viewer unavailable", { t });
13844
+ return null;
13845
+ }
13846
+ let description = opts.description || describe_local_gclass(gclass_name);
13847
+ if (!description) {
13848
+ log_warning(`${gobj_short_name(host)}: gclass not registered here: ${gclass_name}`);
13849
+ yui_shell_show_error(shell, "gclass not registered in this yuno", { t });
13850
+ return null;
13851
+ }
13852
+ let jv = gobj_create_service(`gclass-view-${clean_name(gobj_name(host))}`, "C_YUI_JSON", {
13853
+ json_data: description,
13854
+ subscriber: null
13855
+ }, host);
13856
+ if (!jv) {
13857
+ log_error(`${gobj_short_name(host)}: cannot create the gclass viewer`);
13858
+ return null;
13859
+ }
13860
+ let handle = {
13861
+ json_gobj: jv,
13862
+ win: null,
13863
+ modal: null
13864
+ };
13865
+ let $box = gobj_read_pointer_attr(jv, "$container");
13866
+ let on_close = () => {
13867
+ if (typeof opts.on_close === "function") opts.on_close();
13868
+ };
13869
+ if (is_mobile$3()) {
13870
+ if (!shell) {
13871
+ log_error(`${gobj_short_name(host)}: no shell, cannot open the gclass sheet`);
13872
+ close_gclass_view(handle);
13873
+ return null;
13874
+ }
13875
+ handle.modal = yui_shell_show_modal(shell, $box, {
13876
+ dialog: true,
13877
+ logical_class: "GCLASS_VIEW_SHEET",
13878
+ title_prefix: opts.title_prefix || "",
13879
+ title: gclass_name,
13880
+ t,
13881
+ on_close
13882
+ });
13883
+ gobj_start(jv);
13884
+ return handle;
13885
+ }
13886
+ let $win_parent = window_layer_of(host, shell);
13887
+ handle.win = gobj_create_service(`gclass-win-${clean_name(gobj_name(host))}`, "C_YUI_WINDOW", {
13888
+ $parent: $win_parent,
13889
+ subscriber: null,
13890
+ modal: false,
13891
+ showMax: true,
13892
+ showFooter: false,
13893
+ resizable: true,
13894
+ center: true,
13895
+ auto_save_size_and_position: true,
13896
+ width: 720,
13897
+ height: 640,
13898
+ logical_class: "GCLASS_VIEW_WINDOW",
13899
+ title_prefix: opts.title_prefix || "",
13900
+ title: gclass_name,
13901
+ icon: "yi-code",
13902
+ body: $box,
13903
+ manager: gobj_find_service("__window_manager__", false) || null,
13904
+ on_close
13905
+ }, host);
13906
+ if (!handle.win) {
13907
+ log_error(`${gobj_short_name(host)}: cannot create the gclass window`);
13908
+ close_gclass_view(handle);
13909
+ return null;
13910
+ }
13911
+ gobj_start(handle.win);
13912
+ gobj_start(jv);
13913
+ return handle;
13914
+ }
13915
+ /************************************************************
13916
+ * Close whatever open_gclass_view() opened. Safe on null and
13917
+ * on a handle whose window the reader already dismissed.
13918
+ ************************************************************/
13919
+ function close_gclass_view(handle) {
13920
+ if (!handle) return;
13921
+ let jv = handle.json_gobj;
13922
+ let win = handle.win;
13923
+ let modal = handle.modal;
13924
+ handle.json_gobj = null;
13925
+ handle.win = null;
13926
+ handle.modal = null;
13927
+ if (win && is_gobj(win)) try {
13928
+ if (gobj_is_running(win)) gobj_stop(win);
13929
+ gobj_destroy(win);
13930
+ } catch (e) {
13931
+ log_warning(`yui_gclass_view: window already gone: ${e}`);
13932
+ }
13933
+ if (modal && typeof modal.close === "function") try {
13934
+ modal.close();
13935
+ } catch (e) {
13936
+ log_warning(`yui_gclass_view: modal already gone: ${e}`);
13937
+ }
13938
+ if (jv && is_gobj(jv)) try {
13939
+ if (gobj_is_running(jv)) gobj_stop(jv);
13940
+ gobj_destroy(jv);
13941
+ } catch (e) {
13942
+ log_warning(`yui_gclass_view: viewer already gone: ${e}`);
13943
+ }
13944
+ }
13945
+ /************************************************************
13946
+ * Where to mount the window: the SAME element the caller's
13947
+ * own window hangs from.
13948
+ *
13949
+ * Not a preference -- a requirement. z-index only orders
13950
+ * siblings of one stacking context, so a viewer opened from a
13951
+ * window on `body` and mounted in the shell's popup layer
13952
+ * lands BEHIND its opener whatever number it is given, and
13953
+ * the button reads as one that did nothing.
13954
+ ************************************************************/
13955
+ function window_layer_of(host, shell) {
13956
+ let $c = gobj_read_pointer_attr(host, "$container");
13957
+ if ($c && typeof $c.closest === "function") {
13958
+ let $win = $c.closest(".C_YUI_WINDOW");
13959
+ if ($win && $win.parentNode) return $win.parentNode;
13960
+ }
13961
+ return typeof document !== "undefined" && document.getElementById("top-layer") || shell && yui_shell_popup_layer(shell) || null;
13962
+ }
13963
+ /************************************************************
13964
+ * A window is not a sheet: below this width there is no room
13965
+ * for a title bar the reader can drag.
13966
+ ************************************************************/
13967
+ function is_mobile$3() {
13968
+ return typeof window !== "undefined" && window.innerWidth <= 768;
13969
+ }
13970
+ //#endregion
13390
13971
  //#region src/c_yui_gobj_tree_js.js
13391
13972
  /***********************************************************************
13392
13973
  * c_yui_gobj_tree_js.js
@@ -13497,6 +14078,9 @@ var PRIVATE_DATA$9 = {
13497
14078
  $popover: null,
13498
14079
  $popover_title: null,
13499
14080
  $popover_body: null,
14081
+ $popover_gclass: null,
14082
+ popover_node: null,
14083
+ gclass_view: null,
13500
14084
  node_by_id: null,
13501
14085
  collapse_state: null,
13502
14086
  resize_observer: null,
@@ -13511,34 +14095,92 @@ var PRIVATE_DATA$9 = {
13511
14095
  var __gclass__$9 = null;
13512
14096
  var __layout_registered__ = false;
13513
14097
  /***************************************************************
13514
- * Node colors by role
14098
+ * Node colours by role.
14099
+ *
14100
+ * Saturated hues, one family apart from the next, and the same
14101
+ * five everywhere the reader meets a gobj. `stroke` draws the
14102
+ * border and the edge; `accent` is the brighter sibling used
14103
+ * for the bar down the left of the card and the toggle badge,
14104
+ * so a card carries its colour twice at two intensities and is
14105
+ * readable at the zoom where the text is not.
13515
14106
  ***************************************************************/
13516
14107
  var ROLE_COLORS = {
13517
14108
  yuno: {
13518
- fill: "#E0F2FE",
13519
- stroke: "#075985"
14109
+ stroke: "#0284c7",
14110
+ accent: "#38bdf8"
13520
14111
  },
13521
14112
  service: {
13522
- fill: "#DCFCE7",
13523
- stroke: "#166534"
14113
+ stroke: "#059669",
14114
+ accent: "#34d399"
13524
14115
  },
13525
14116
  pure: {
13526
- fill: "#FEF9C3",
13527
- stroke: "#854D0E"
14117
+ stroke: "#d97706",
14118
+ accent: "#fbbf24"
13528
14119
  },
13529
14120
  volatil: {
13530
- fill: "#FCE7F3",
13531
- stroke: "#9D174D"
14121
+ stroke: "#db2777",
14122
+ accent: "#f472b6"
13532
14123
  },
13533
- default: {
13534
- fill: "#F3F4F6",
13535
- stroke: "#374151"
14124
+ child: {
14125
+ stroke: "#7c3aed",
14126
+ accent: "#a78bfa"
13536
14127
  }
13537
14128
  };
13538
- var STATE_COLORS = {
13539
- running_playing: "#15803D",
13540
- running: "#B45309",
13541
- stopped: "#B91C1C"
14129
+ /***************************************************************
14130
+ * Status colours: what the gobj is DOING.
14131
+ *
14132
+ * A pill and not a word: three states of one gobj are told
14133
+ * apart across a whole tree by colour long before anybody
14134
+ * reads them, and the dot alone never said which was which.
14135
+ * `disabled` is a fourth entry and not a status -- a disabled
14136
+ * gobj is still stopped or running -- but it takes the pill,
14137
+ * because it is the fact that explains the other one.
14138
+ ***************************************************************/
14139
+ var STATUS_COLORS = {
14140
+ light: {
14141
+ playing: {
14142
+ fg: "#065f46",
14143
+ bg: "#a7f3d0",
14144
+ dot: "#10b981"
14145
+ },
14146
+ running: {
14147
+ fg: "#92400e",
14148
+ bg: "#fde68a",
14149
+ dot: "#f59e0b"
14150
+ },
14151
+ stopped: {
14152
+ fg: "#991b1b",
14153
+ bg: "#fecaca",
14154
+ dot: "#ef4444"
14155
+ },
14156
+ disabled: {
14157
+ fg: "#334155",
14158
+ bg: "#e2e8f0",
14159
+ dot: "#94a3b8"
14160
+ }
14161
+ },
14162
+ dark: {
14163
+ playing: {
14164
+ fg: "#6ee7b7",
14165
+ bg: "rgba(16,185,129,0.28)",
14166
+ dot: "#34d399"
14167
+ },
14168
+ running: {
14169
+ fg: "#fcd34d",
14170
+ bg: "rgba(245,158,11,0.28)",
14171
+ dot: "#fbbf24"
14172
+ },
14173
+ stopped: {
14174
+ fg: "#fca5a5",
14175
+ bg: "rgba(239,68,68,0.28)",
14176
+ dot: "#f87171"
14177
+ },
14178
+ disabled: {
14179
+ fg: "#cbd5e1",
14180
+ bg: "rgba(148,163,184,0.28)",
14181
+ dot: "#94a3b8"
14182
+ }
14183
+ }
13542
14184
  };
13543
14185
  /***************************************************************
13544
14186
  * Fixed node sizes per role (px).
@@ -13548,16 +14190,16 @@ var STATE_COLORS = {
13548
14190
  var NODE_SIZES = {
13549
14191
  full: {
13550
14192
  yuno: {
13551
- w: 240,
13552
- h: 72
14193
+ w: 260,
14194
+ h: 96
13553
14195
  },
13554
14196
  service: {
13555
- w: 210,
13556
- h: 68
14197
+ w: 232,
14198
+ h: 92
13557
14199
  },
13558
14200
  child: {
13559
- w: 160,
13560
- h: 60
14201
+ w: 196,
14202
+ h: 86
13561
14203
  }
13562
14204
  },
13563
14205
  compact: {
@@ -13580,27 +14222,47 @@ var NODE_SIZES = {
13580
14222
  ***************************************************************/
13581
14223
  var GT_FONT = "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif";
13582
14224
  /***************************************************************
13583
- * Soft, theme-aware card palette derived from the role colour
13584
- * (same visual language as the treedb graph cards): light tint
13585
- * fill + role-colour border, brightened on dark.
14225
+ * Theme-aware card palette derived from the role colour.
14226
+ *
14227
+ * Brighter than a tint: at 9% of the role colour every card
14228
+ * was the same near-white rectangle and the tree read as a
14229
+ * wireframe. The fill carries enough of the hue to be named
14230
+ * from across the canvas, and the border and the accent bar
14231
+ * carry the rest.
13586
14232
  ***************************************************************/
13587
- function role_card_style(stroke, dark) {
14233
+ function role_card_style(colors, dark) {
14234
+ let stroke = colors.stroke;
14235
+ let accent = colors.accent;
13588
14236
  return {
13589
- bg: dark ? `color-mix(in srgb, ${stroke} 30%, #2c3542)` : `color-mix(in srgb, ${stroke} 9%, #ffffff)`,
13590
- border: dark ? `color-mix(in srgb, ${stroke} 85%, #ffffff)` : stroke,
13591
- title: dark ? "#e8eaed" : "#0f172a",
13592
- sub: dark ? "#9aa4b2" : "#64748b",
13593
- tagbg: dark ? `color-mix(in srgb, ${stroke} 42%, #2c3542)` : `color-mix(in srgb, ${stroke} 14%, #ffffff)`,
13594
- shadow: dark ? "0 1px 3px rgba(0,0,0,0.45)" : "0 1px 3px rgba(15,23,42,0.12)"
14237
+ bg: dark ? `color-mix(in srgb, ${stroke} 34%, #253044)` : `color-mix(in srgb, ${stroke} 13%, #ffffff)`,
14238
+ border: dark ? accent : stroke,
14239
+ accent: dark ? accent : stroke,
14240
+ title: dark ? "#f1f5f9" : "#0f172a",
14241
+ sub: dark ? "#a8b3c2" : "#475569",
14242
+ tagbg: dark ? `color-mix(in srgb, ${stroke} 48%, #253044)` : `color-mix(in srgb, ${stroke} 20%, #ffffff)`,
14243
+ tagfg: dark ? "#f1f5f9" : stroke,
14244
+ shadow: dark ? "0 2px 6px rgba(0,0,0,0.50)" : "0 2px 5px rgba(15,23,42,0.16)"
13595
14245
  };
13596
14246
  }
13597
14247
  /***************************************************************
13598
- * Map a gobj to its visual category.
14248
+ * The pill colours of a node, for the theme in force.
13599
14249
  ***************************************************************/
13600
- function get_node_category(target_gobj, is_root) {
13601
- if (is_root) return "yuno";
13602
- if (gobj_is_service(target_gobj)) return "service";
13603
- return "child";
14250
+ function status_card_style(d, dark) {
14251
+ let table = dark ? STATUS_COLORS.dark : STATUS_COLORS.light;
14252
+ if (d.disabled) return table.disabled;
14253
+ return table[node_status(d)] || table.stopped;
14254
+ }
14255
+ /***************************************************************
14256
+ * Map a node to its visual size class. Services are drawn
14257
+ * bigger than plain children so the service boundary of the
14258
+ * hierarchy is visible before anything is read.
14259
+ ***************************************************************/
14260
+ function get_node_category(d) {
14261
+ switch (node_role(d)) {
14262
+ case "yuno": return "yuno";
14263
+ case "service": return "service";
14264
+ default: return "child";
14265
+ }
13604
14266
  }
13605
14267
  /***************************************************************
13606
14268
  * Layouts registry.
@@ -13713,13 +14375,21 @@ function mt_create$9(gobj) {
13713
14375
  ***************************************************************/
13714
14376
  function mt_start$9(gobj) {
13715
14377
  restore_view_shape(gobj);
14378
+ let shell = yui_shell_of(gobj);
14379
+ if (shell) gobj_subscribe_event(shell, "EV_LANGUAGE_CHANGED", {}, gobj);
13716
14380
  build_graph$2(gobj);
13717
14381
  load_tree(gobj);
13718
14382
  }
13719
14383
  /***************************************************************
13720
14384
  * Framework Method: Stop
13721
14385
  ***************************************************************/
13722
- function mt_stop$9(gobj) {}
14386
+ function mt_stop$9(gobj) {
14387
+ let priv = gobj.priv;
14388
+ let shell = yui_shell_of(gobj);
14389
+ if (shell) gobj_unsubscribe_event(shell, "EV_LANGUAGE_CHANGED", {}, gobj);
14390
+ close_gclass_view(priv.gclass_view);
14391
+ priv.gclass_view = null;
14392
+ }
13723
14393
  /***************************************************************
13724
14394
  * Framework Method: Destroy
13725
14395
  ***************************************************************/
@@ -13745,6 +14415,8 @@ function mt_destroy$9(gobj) {
13745
14415
  priv.$popover = null;
13746
14416
  priv.$popover_title = null;
13747
14417
  priv.$popover_body = null;
14418
+ priv.$popover_gclass = null;
14419
+ priv.popover_node = null;
13748
14420
  priv.$layout_select = null;
13749
14421
  priv.node_by_id = null;
13750
14422
  priv.collapse_state = null;
@@ -13848,6 +14520,30 @@ function build_popover(gobj, $container) {
13848
14520
  text-overflow: ellipsis;
13849
14521
  `;
13850
14522
  $header.appendChild($title);
14523
+ if (gclass_view_available()) {
14524
+ let $gclass = document.createElement("button");
14525
+ $gclass.type = "button";
14526
+ $gclass.className = "GOBJ_TREE_POPOVER_GCLASS button is-small";
14527
+ $gclass.style.cssText = `
14528
+ margin-right: 6px;
14529
+ padding: 0 8px;
14530
+ height: 22px;
14531
+ font-size: 11px;
14532
+ white-space: nowrap;
14533
+ `;
14534
+ $gclass.textContent = t("gclass");
14535
+ $gclass.setAttribute("i18n", "gclass");
14536
+ $gclass.title = t("view gclass");
14537
+ $gclass.setAttribute("data-i18n-title", "view gclass");
14538
+ $gclass.setAttribute("aria-label", t("view gclass"));
14539
+ $gclass.setAttribute("data-i18n-aria-label", "view gclass");
14540
+ $gclass.addEventListener("click", (evt) => {
14541
+ evt.stopPropagation();
14542
+ gobj_send_event(gobj, "EV_OPEN_GCLASS", {}, gobj);
14543
+ });
14544
+ $header.appendChild($gclass);
14545
+ priv.$popover_gclass = $gclass;
14546
+ } else log_warning(`${gobj_short_name(gobj)}: no C_YUI_JSON registered, the gclass viewer is not offered`);
13851
14547
  let $close = document.createElement("button");
13852
14548
  $close.type = "button";
13853
14549
  $close.className = "GOBJ_TREE_POPOVER_CLOSE";
@@ -13955,6 +14651,26 @@ function make_toolbar$1(gobj) {
13955
14651
  return $toolbar;
13956
14652
  }
13957
14653
  /************************************************************
14654
+ * The element G6 mounts its canvas in.
14655
+ *
14656
+ * Read from THIS gclass's own $container, never with
14657
+ * `document.getElementById()`: a view is not always in the
14658
+ * document when it is started -- a host that builds the
14659
+ * viewer and mounts it into a window afterwards starts it
14660
+ * DETACHED -- and a global lookup answers null there. It cost
14661
+ * a resize that never worked: the ResizeObserver below was
14662
+ * simply never attached, silently, and the canvas kept the
14663
+ * size it was born with for the life of the window.
14664
+ *
14665
+ * Null only if the UI was never built; that is a bug, and it
14666
+ * is logged where it matters rather than skipped.
14667
+ ************************************************************/
14668
+ function canvas_mount(gobj) {
14669
+ let $container = gobj_read_attr(gobj, "$container");
14670
+ if (!$container) return null;
14671
+ return $container.querySelector("#" + gobj.priv.canvas_id);
14672
+ }
14673
+ /************************************************************
13958
14674
  * Build a G6 graph instance
13959
14675
  ************************************************************/
13960
14676
  function build_graph$2(gobj) {
@@ -13998,8 +14714,9 @@ function build_graph$2(gobj) {
13998
14714
  hide_popover(gobj);
13999
14715
  });
14000
14716
  if (typeof ResizeObserver !== "undefined") {
14001
- let $canvas = document.getElementById(priv.canvas_id);
14002
- if ($canvas) {
14717
+ let $canvas = canvas_mount(gobj);
14718
+ if (!$canvas) log_error(`${gobj_short_name(gobj)}: no canvas mount, the graph will never resize`);
14719
+ else {
14003
14720
  let ro = new ResizeObserver(() => {
14004
14721
  if (priv._resize_raf) cancelAnimationFrame(priv._resize_raf);
14005
14722
  priv._resize_raf = requestAnimationFrame(() => {
@@ -14032,22 +14749,20 @@ function apply_layout(gobj) {
14032
14749
  });
14033
14750
  }
14034
14751
  /************************************************************
14035
- * Resolve role colors for a gobj
14036
- ************************************************************/
14037
- function get_role_colors(target_gobj, is_root) {
14038
- if (is_root) return ROLE_COLORS.yuno;
14039
- if (gobj_is_volatil(target_gobj)) return ROLE_COLORS.volatil;
14040
- if (gobj_is_service(target_gobj)) return ROLE_COLORS.service;
14041
- if (gobj_is_pure_child(target_gobj)) return ROLE_COLORS.pure;
14042
- return ROLE_COLORS.default;
14043
- }
14044
- /************************************************************
14045
- * Resolve state color
14752
+ * The role colours of a node.
14753
+ *
14754
+ * Note the ORDER: volatil before service. A volatil gobj that
14755
+ * is also a service is drawn volatil, because that is the fact
14756
+ * that decides how long it lives.
14046
14757
  ************************************************************/
14047
- function get_state_color(target_gobj) {
14048
- if (!gobj_is_running(target_gobj)) return STATE_COLORS.stopped;
14049
- if (gobj_is_playing(target_gobj)) return STATE_COLORS.running_playing;
14050
- return STATE_COLORS.running;
14758
+ function get_role_colors(d) {
14759
+ switch (node_role(d)) {
14760
+ case "yuno": return ROLE_COLORS.yuno;
14761
+ case "volatil": return ROLE_COLORS.volatil;
14762
+ case "service": return ROLE_COLORS.service;
14763
+ case "pure": return ROLE_COLORS.pure;
14764
+ default: return ROLE_COLORS.child;
14765
+ }
14051
14766
  }
14052
14767
  /************************************************************
14053
14768
  * Generate a unique node id
@@ -14079,72 +14794,135 @@ function is_node_collapsed(gobj, full_name, num_children) {
14079
14794
  ************************************************************/
14080
14795
  function render_toggle_html(node_id, collapsed, num_children, cs) {
14081
14796
  let label = collapsed ? "+" + num_children : "−";
14797
+ let title = collapsed ? t("expand children") : t("collapse children");
14082
14798
  return `<span
14083
14799
  data-gobj-toggle="true"
14084
14800
  data-node-id="${node_id}"
14085
- title="${collapsed ? "Expand children" : "Collapse children"}"
14801
+ title="${escapeHtml(title)}"
14086
14802
  style="
14087
14803
  flex: 0 0 auto;
14088
14804
  display: inline-flex;
14089
14805
  align-items: center;
14090
14806
  justify-content: center;
14091
- min-width: 22px;
14092
- height: 16px;
14093
- padding: 0 5px;
14807
+ min-width: 24px;
14808
+ height: 18px;
14809
+ padding: 0 6px;
14094
14810
  margin-left: 4px;
14095
- background: ${cs.tagbg};
14096
- color: ${cs.border};
14097
- border: 1px solid ${cs.border};
14098
- border-radius: 6px;
14811
+ background: ${cs.accent};
14812
+ color: #ffffff;
14813
+ border: 1px solid ${cs.accent};
14814
+ border-radius: 9px;
14099
14815
  font-size: 10px;
14100
- font-weight: 600;
14816
+ font-weight: 700;
14101
14817
  line-height: 1;
14102
14818
  cursor: pointer;
14103
14819
  user-select: none;
14104
14820
  ">${label}</span>`;
14105
14821
  }
14106
14822
  /************************************************************
14107
- * Recursively build nodes and edges from the gobj tree
14823
+ * HTML for the status pill: what the gobj is DOING.
14824
+ *
14825
+ * A dot plus a word, and the word is dropped in the compact
14826
+ * card where there is no room for it -- the colour still
14827
+ * carries it, and the `title` says it in full either way.
14108
14828
  ************************************************************/
14109
- function build_gobj_nodes(gobj, target_gobj, nodes, edges, parent_id, is_root, compact) {
14829
+ function render_status_html(d, ss, labels, with_label) {
14830
+ let full = d.disabled ? labels.field.disabled : labels.status[node_status_key(d)] || node_status_key(d);
14831
+ let label = d.disabled ? labels.field.disabled : labels.status_short[node_status(d)] || node_status(d);
14832
+ if (!with_label) return `<span title="${escapeHtml(full)}" style="
14833
+ flex: 0 0 auto;
14834
+ width: 10px; height: 10px; border-radius: 50%;
14835
+ background: ${ss.dot};
14836
+ box-shadow: 0 0 0 2px ${ss.bg};
14837
+ margin-left: 6px;
14838
+ "></span>`;
14839
+ return `<span title="${escapeHtml(full)}" style="
14840
+ flex: 0 0 auto;
14841
+ display: inline-flex;
14842
+ align-items: center;
14843
+ gap: 4px;
14844
+ padding: 1px 7px 1px 5px;
14845
+ background: ${ss.bg};
14846
+ color: ${ss.fg};
14847
+ border-radius: 9px;
14848
+ font-size: 10px;
14849
+ font-weight: 700;
14850
+ line-height: 1.45;
14851
+ white-space: nowrap;
14852
+ "><span style="width:7px; height:7px; border-radius:50%; background:${ss.dot};"></span>${escapeHtml(label)}</span>`;
14853
+ }
14854
+ /************************************************************
14855
+ * HTML for the badges: what the gobj IS, besides its colour.
14856
+ ************************************************************/
14857
+ function render_badges_html(d, cs, labels) {
14858
+ let html = "";
14859
+ for (let key of node_badge_keys(d)) {
14860
+ let label = labels.badge[key] || key;
14861
+ html += `<span style="
14862
+ font-size: 10px;
14863
+ font-weight: 600;
14864
+ padding: 0 6px;
14865
+ margin-right: 4px;
14866
+ background: ${cs.tagbg};
14867
+ color: ${cs.tagfg};
14868
+ border: 1px solid ${cs.accent};
14869
+ border-radius: 9px;
14870
+ white-space: nowrap;
14871
+ ">${escapeHtml(label)}</span>`;
14872
+ }
14873
+ return html;
14874
+ }
14875
+ /************************************************************
14876
+ * Recursively build nodes and edges from a NODE DESCRIPTOR
14877
+ * tree (gobj_tree_model.js), never from gobjs: the same
14878
+ * builder draws the browser yuno and a backend one.
14879
+ *
14880
+ * The descriptor is also what the popover reads, so the view
14881
+ * fields it needs are written onto it here (`full_name`,
14882
+ * `short_name`, `num_children`, `is_collapsed`,
14883
+ * `direct_children`) instead of copied into a second object
14884
+ * that can then disagree with the first.
14885
+ ************************************************************/
14886
+ function build_gobj_nodes(gobj, d, nodes, edges, parent_id, compact, labels) {
14110
14887
  let priv = gobj.priv;
14111
14888
  let node_id = gen_node_id(gobj);
14112
- let gclass_name = gobj_gclass_name(target_gobj) || "";
14113
- let instance_name = gobj_name(target_gobj) || "";
14114
- let state = gobj_current_state(target_gobj) || "";
14115
- let full_name = gobj_full_name(target_gobj);
14116
- let colors = get_role_colors(target_gobj, is_root);
14117
14889
  let dark = yui_is_dark();
14118
- let cs = role_card_style(colors.stroke, dark);
14119
- let state_color = get_state_color(target_gobj);
14120
- let running = gobj_is_running(target_gobj);
14121
- let playing = gobj_is_playing(target_gobj);
14122
- let status_text = !running ? "stopped" : playing ? "playing" : "running";
14123
- let category = get_node_category(target_gobj, is_root);
14890
+ let colors = get_role_colors(d);
14891
+ let cs = role_card_style(colors, dark);
14892
+ let ss = status_card_style(d, dark);
14893
+ let category = get_node_category(d);
14124
14894
  let size = NODE_SIZES[compact ? "compact" : "full"][category];
14125
14895
  let width = size.w;
14126
14896
  let height = size.h;
14127
14897
  let border_width = category === "yuno" || category === "service" ? 2 : 1;
14128
- let children = target_gobj.dl_children || [];
14898
+ let border_style = d.disabled ? "dashed" : "solid";
14899
+ let children = d.children || [];
14129
14900
  let num_children = children.length;
14130
14901
  let has_children = num_children > 0;
14131
- let collapsed = has_children && is_node_collapsed(gobj, full_name, num_children);
14902
+ let collapsed = has_children && is_node_collapsed(gobj, d.fullname, num_children);
14132
14903
  let direct_children = [];
14133
14904
  for (let child of children) direct_children.push({
14134
- full_name: gobj_full_name(child),
14135
- has_grandchildren: (child.dl_children || []).length > 0
14905
+ full_name: child.fullname,
14906
+ has_grandchildren: (child.children || []).length > 0
14136
14907
  });
14908
+ d.full_name = d.fullname;
14909
+ d.short_name = d.shortname;
14910
+ d.num_children = num_children;
14911
+ d.is_collapsed = collapsed;
14912
+ d.direct_children = direct_children;
14913
+ d.node_id = node_id;
14137
14914
  let toggle_html = has_children ? render_toggle_html(node_id, collapsed, num_children, cs) : "";
14915
+ let card_title = d.gclass + (d.name ? "^" + d.name : "");
14138
14916
  let node_html;
14139
14917
  if (compact) {
14140
- let compact_label = escapeHtml(instance_name ? gclass_name + "^" + instance_name : gclass_name);
14918
+ let compact_label = escapeHtml(card_title);
14141
14919
  node_html = `
14142
- <div class="GOBJ_CARD" data-gobj-name="${escapeHtml(full_name)}" style="
14920
+ <div class="GOBJ_CARD" data-gobj-name="${escapeHtml(d.fullname)}" style="
14143
14921
  width: ${width}px;
14144
14922
  height: ${height}px;
14145
14923
  background: ${cs.bg};
14146
- border: ${border_width}px solid ${cs.border};
14147
- border-left: 5px solid ${cs.border};
14924
+ border: ${border_width}px ${border_style} ${cs.border};
14925
+ border-left: 6px solid ${cs.accent};
14148
14926
  border-radius: 7px;
14149
14927
  box-shadow: ${cs.shadow};
14150
14928
  font-family: ${GT_FONT};
@@ -14153,34 +14931,24 @@ function build_gobj_nodes(gobj, target_gobj, nodes, edges, parent_id, is_root, c
14153
14931
  padding: 0 8px;
14154
14932
  box-sizing: border-box;
14155
14933
  overflow: hidden;
14934
+ opacity: ${d.disabled ? "0.72" : "1"};
14156
14935
  cursor: pointer;
14157
14936
  " title="${compact_label}">
14158
14937
  <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>
14159
- <span title="${escapeHtml(state || status_text)}" style="
14160
- flex: 0 0 auto;
14161
- width: 8px; height: 8px; border-radius: 50%;
14162
- background: ${state_color}; margin-left: 6px;
14163
- "></span>
14938
+ ${render_status_html(d, ss, labels, false)}
14164
14939
  ${toggle_html}
14165
14940
  </div>`;
14166
14941
  } else {
14167
- let header_label = escapeHtml(gclass_name);
14168
- let name_label = escapeHtml(instance_name);
14169
- let state_label = escapeHtml(state);
14170
- let badges = [];
14171
- if (is_root) badges.push("yuno");
14172
- else {
14173
- if (gobj_is_service(target_gobj)) badges.push("service");
14174
- if (gobj_is_pure_child(target_gobj)) badges.push("pure");
14175
- if (gobj_is_volatil(target_gobj)) badges.push("volatil");
14176
- }
14177
- 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("");
14942
+ let header_label = escapeHtml(d.gclass);
14943
+ let name_label = escapeHtml(d.name);
14944
+ let state_label = escapeHtml(d.state);
14178
14945
  node_html = `
14179
- <div class="GOBJ_CARD" data-gobj-name="${escapeHtml(full_name)}" style="
14946
+ <div class="GOBJ_CARD" data-gobj-name="${escapeHtml(d.fullname)}" style="
14180
14947
  width: ${width}px;
14181
14948
  height: ${height}px;
14182
14949
  background: ${cs.bg};
14183
- border: ${border_width}px solid ${cs.border};
14950
+ border: ${border_width}px ${border_style} ${cs.border};
14951
+ border-left: 6px solid ${cs.accent};
14184
14952
  border-radius: 9px;
14185
14953
  box-shadow: ${cs.shadow};
14186
14954
  overflow: hidden;
@@ -14189,48 +14957,29 @@ function build_gobj_nodes(gobj, target_gobj, nodes, edges, parent_id, is_root, c
14189
14957
  display: flex;
14190
14958
  flex-direction: column;
14191
14959
  justify-content: center;
14192
- padding: 5px 9px;
14193
- gap: 1px;
14960
+ padding: 6px 9px;
14961
+ gap: 2px;
14962
+ opacity: ${d.disabled ? "0.72" : "1"};
14194
14963
  cursor: pointer;
14195
- " title="${escapeHtml(gclass_name + (instance_name ? "^" + instance_name : ""))}">
14964
+ " title="${escapeHtml(card_title)}">
14196
14965
  <div style="flex:0 0 auto; display:flex; align-items:center; gap:6px; overflow:hidden;">
14197
- <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>
14966
+ <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>
14198
14967
  ${toggle_html}
14199
14968
  </div>
14200
14969
  <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>
14201
14970
  <div style="flex:0 0 auto; display:flex; justify-content:space-between; align-items:center; gap:6px; overflow:hidden;">
14202
- <span style="flex:1 1 auto; min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;">${badges_html}</span>
14203
- <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;">
14204
- <span style="width:7px; height:7px; border-radius:50%; background:${state_color};"></span>${state_label || status_text}
14205
- </span>
14971
+ <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>
14972
+ ${render_status_html(d, ss, labels, true)}
14206
14973
  </div>
14974
+ <div style="flex:0 0 auto; overflow:hidden; white-space:nowrap; text-overflow:ellipsis;">${render_badges_html(d, cs, labels)}</div>
14207
14975
  </div>`;
14208
14976
  }
14209
- let parent_gobj = gobj_parent(target_gobj);
14210
- let node_data = {
14211
- gclass: gclass_name,
14212
- name: instance_name,
14213
- short_name: gobj_short_name(target_gobj),
14214
- full_name,
14215
- parent_short_name: parent_gobj ? gobj_short_name(parent_gobj) : "",
14216
- state,
14217
- category,
14218
- is_yuno: is_root,
14219
- is_service: gobj_is_service(target_gobj),
14220
- is_pure_child: gobj_is_pure_child(target_gobj),
14221
- is_volatil: gobj_is_volatil(target_gobj),
14222
- is_running: running,
14223
- is_playing: playing,
14224
- num_children,
14225
- is_collapsed: collapsed,
14226
- direct_children
14227
- };
14228
14977
  if (!priv.node_by_id) priv.node_by_id = {};
14229
- priv.node_by_id[node_id] = node_data;
14978
+ priv.node_by_id[node_id] = d;
14230
14979
  let node = {
14231
14980
  id: node_id,
14232
14981
  type: "html",
14233
- data: node_data,
14982
+ data: d,
14234
14983
  style: {
14235
14984
  innerHTML: node_html,
14236
14985
  size: [width, height],
@@ -14243,11 +14992,11 @@ function build_gobj_nodes(gobj, target_gobj, nodes, edges, parent_id, is_root, c
14243
14992
  source: parent_id,
14244
14993
  target: node_id,
14245
14994
  style: {
14246
- stroke: colors.stroke,
14247
- lineWidth: 1
14995
+ stroke: colors.accent,
14996
+ lineWidth: 1.4
14248
14997
  }
14249
14998
  });
14250
- if (!collapsed) for (let child of children) build_gobj_nodes(gobj, child, nodes, edges, node_id, false, compact);
14999
+ if (!collapsed) for (let child of children) build_gobj_nodes(gobj, child, nodes, edges, node_id, compact, labels);
14251
15000
  return node_id;
14252
15001
  }
14253
15002
  /************************************************************
@@ -14264,9 +15013,13 @@ function load_tree(gobj) {
14264
15013
  let priv = gobj.priv;
14265
15014
  let graph = priv.graph;
14266
15015
  if (!graph) return;
14267
- let yuno = gobj_yuno();
14268
- if (!yuno) return;
15016
+ let root = describe_js_gobj(gobj_yuno(), true);
15017
+ if (!root) {
15018
+ log_error(`${gobj_short_name(gobj)}: no yuno to draw`);
15019
+ return;
15020
+ }
14269
15021
  let layout_cfg = get_layout_cfg(gobj_read_str_attr(gobj, "layout"));
15022
+ let labels = node_labels(t);
14270
15023
  let anchor = priv.pending_anchor || null;
14271
15024
  let preserve_view = !!priv.pending_preserve_view;
14272
15025
  priv.pending_anchor = null;
@@ -14275,7 +15028,7 @@ function load_tree(gobj) {
14275
15028
  priv.node_by_id = {};
14276
15029
  let nodes = [];
14277
15030
  let edges = [];
14278
- build_gobj_nodes(gobj, yuno, nodes, edges, null, true, layout_cfg.compact);
15031
+ build_gobj_nodes(gobj, root, nodes, edges, null, layout_cfg.compact, labels);
14279
15032
  if (nodes.length > 0) {
14280
15033
  let root_id = nodes[0].id;
14281
15034
  graph.setData({
@@ -14376,46 +15129,17 @@ function refresh_tree(gobj, opts) {
14376
15129
  }
14377
15130
  }
14378
15131
  /************************************************************
14379
- * Rows to show in the popover for a given node data.
14380
- *
14381
- * This is the extension point: to surface a new attribute,
14382
- * collect it into `node_data` in build_gobj_nodes(), then
14383
- * append a `[label, value]` pair here.
14384
- ************************************************************/
14385
- function get_node_info_rows(node_data) {
14386
- let rows = [];
14387
- rows.push([t("gclass"), node_data.gclass || "—"]);
14388
- rows.push([t("name"), node_data.name || "—"]);
14389
- if (node_data.full_name && node_data.full_name !== node_data.short_name) rows.push([t("full name"), node_data.full_name]);
14390
- let role;
14391
- if (node_data.is_yuno) role = "yuno";
14392
- else if (node_data.is_service) role = "service";
14393
- else if (node_data.is_volatil) role = "volatil child";
14394
- else if (node_data.is_pure_child) role = "pure child";
14395
- else role = "child";
14396
- rows.push([t("role"), role]);
14397
- let status;
14398
- if (!node_data.is_running) status = "stopped";
14399
- else if (node_data.is_playing) status = "running + playing";
14400
- else status = "running";
14401
- rows.push([t("status"), status]);
14402
- rows.push([t("state"), node_data.state || "—"]);
14403
- if (node_data.parent_short_name) rows.push([t("parent"), node_data.parent_short_name]);
14404
- if (typeof node_data.num_children === "number") {
14405
- let kids = node_data.num_children;
14406
- if (kids > 0 && node_data.is_collapsed) rows.push([t("children"), kids + " " + t("(collapsed)")]);
14407
- else if (kids > 0) rows.push([t("children"), String(kids)]);
14408
- }
14409
- return rows;
14410
- }
14411
- /************************************************************
14412
15132
  * Render popover body from the info rows of a node.
15133
+ *
15134
+ * The rows come from gobj_tree_model.js, so a node of the
15135
+ * browser yuno and a node of a backend one are read out with
15136
+ * the same words in the same order.
14413
15137
  ************************************************************/
14414
15138
  function render_popover_body(gobj, node_data) {
14415
15139
  let $body = gobj.priv.$popover_body;
14416
15140
  if (!$body) return;
14417
- $body.innerHTML = get_node_info_rows(node_data).map(([label, value]) => `
14418
- <div style="display:grid; grid-template-columns: 90px 1fr; column-gap:10px; padding: 2px 0;">
15141
+ $body.innerHTML = node_info_rows(node_data, t).map(([label, value]) => `
15142
+ <div style="display:grid; grid-template-columns: 96px 1fr; column-gap:10px; padding: 2px 0;">
14419
15143
  <span style="color:var(--bulma-text-weak, #6B7280); font-weight:500;">${escapeHtml(String(label))}</span>
14420
15144
  <span style="color:var(--bulma-text-strong, #1A1A1A); word-break:break-all;">${escapeHtml(String(value))}</span>
14421
15145
  </div>
@@ -14432,6 +15156,7 @@ function show_popover(gobj, node_data, client_x, client_y) {
14432
15156
  let $title = priv.$popover_title;
14433
15157
  let $container = gobj_read_attr(gobj, "$container");
14434
15158
  if (!$popover || !$title || !$container) return;
15159
+ priv.popover_node = node_data;
14435
15160
  $title.textContent = node_data.short_name || node_data.gclass + (node_data.name ? "^" + node_data.name : "");
14436
15161
  $title.title = $title.textContent;
14437
15162
  render_popover_body(gobj, node_data);
@@ -14462,6 +15187,7 @@ function hide_popover(gobj) {
14462
15187
  if (priv.$popover) priv.$popover.style.display = "none";
14463
15188
  if (priv.$popover_body) priv.$popover_body.innerHTML = "";
14464
15189
  if (priv.$popover_title) priv.$popover_title.textContent = "";
15190
+ priv.popover_node = null;
14465
15191
  }
14466
15192
  /************************************************************
14467
15193
  * Generic tree layout engine.
@@ -14988,12 +15714,11 @@ function ac_node_click$3(gobj, event, kw, src) {
14988
15714
  *
14989
15715
  ************************************************************/
14990
15716
  function ac_show$6(gobj, event, kw, src) {
14991
- let priv = gobj.priv;
14992
- let graph = priv.graph;
15717
+ let graph = gobj.priv.graph;
14993
15718
  save_view_state(gobj);
14994
15719
  refresh_tree(gobj);
14995
15720
  if (graph) {
14996
- let $canvas = document.getElementById(priv.canvas_id);
15721
+ let $canvas = canvas_mount(gobj);
14997
15722
  if ($canvas) {
14998
15723
  let cw = $canvas.clientWidth;
14999
15724
  let ch = $canvas.clientHeight;
@@ -15012,10 +15737,9 @@ function ac_show$6(gobj, event, kw, src) {
15012
15737
  * No fitView: preserve the user's current pan/zoom.
15013
15738
  ************************************************************/
15014
15739
  function ac_resize$1(gobj, event, kw, src) {
15015
- let priv = gobj.priv;
15016
- let graph = priv.graph;
15740
+ let graph = gobj.priv.graph;
15017
15741
  if (graph) {
15018
- let $canvas = document.getElementById(priv.canvas_id);
15742
+ let $canvas = canvas_mount(gobj);
15019
15743
  if ($canvas) {
15020
15744
  let cw = $canvas.clientWidth;
15021
15745
  let ch = $canvas.clientHeight;
@@ -15031,6 +15755,68 @@ function ac_hide$5(gobj, event, kw, src) {
15031
15755
  hide_popover(gobj);
15032
15756
  return 0;
15033
15757
  }
15758
+ /************************************************************
15759
+ * Open the gclass of the node the popover is showing.
15760
+ *
15761
+ * One window at a time: opening a second gclass replaces the
15762
+ * first rather than stacking windows nobody closes.
15763
+ ************************************************************/
15764
+ function ac_open_gclass(gobj, event, kw, src) {
15765
+ let priv = gobj.priv;
15766
+ let node_data = priv.popover_node;
15767
+ if (!node_data || !node_data.gclass) {
15768
+ log_error(`${gobj_short_name(gobj)}: EV_OPEN_GCLASS with no node`);
15769
+ return -1;
15770
+ }
15771
+ close_gclass_view(priv.gclass_view);
15772
+ priv.gclass_view = null;
15773
+ priv.gclass_view = open_gclass_view(gobj, node_data.gclass, {
15774
+ title_prefix: gobj_yuno_name() || "",
15775
+ on_close: () => {
15776
+ if (gobj_is_destroying(gobj)) return;
15777
+ gobj_send_event(gobj, "EV_GCLASS_CLOSED", {}, gobj);
15778
+ }
15779
+ });
15780
+ return 0;
15781
+ }
15782
+ /************************************************************
15783
+ * The reader dismissed the gclass window (its own ✕).
15784
+ *
15785
+ * C_YUI_WINDOW.close_window() calls on_close and THEN destroys
15786
+ * itself, so here we only drop our reference -- destroying it
15787
+ * again would destroy it twice.
15788
+ ************************************************************/
15789
+ function ac_gclass_closed(gobj, event, kw, src) {
15790
+ gobj.priv.gclass_view = null;
15791
+ return 0;
15792
+ }
15793
+ /************************************************************
15794
+ * The gclass viewer asked for a subtree.
15795
+ *
15796
+ * It cannot: the gclass description is a COMPLETE document,
15797
+ * it carries no `__collapsed__` stub, so nothing in it is
15798
+ * fetchable. The event is declared because the viewer is
15799
+ * hosted as a child and subscribes its host to everything it
15800
+ * publishes -- and it is logged, because arriving here means
15801
+ * the document was not the one we built.
15802
+ ************************************************************/
15803
+ function ac_gclass_expand_path(gobj, event, kw, src) {
15804
+ log_error(`${gobj_short_name(gobj)}: EV_EXPAND_PATH on a complete document: ${kw && kw.path}`);
15805
+ return -1;
15806
+ }
15807
+ /************************************************************
15808
+ * The app switched language.
15809
+ *
15810
+ * A rebuild, not a re-translation: the words live inside the
15811
+ * innerHTML of G6 nodes, where refresh_language() cannot go.
15812
+ ************************************************************/
15813
+ function ac_language_changed$3(gobj, event, kw, src) {
15814
+ let $container = gobj_read_attr(gobj, "$container");
15815
+ if ($container) refresh_language($container, t);
15816
+ hide_popover(gobj);
15817
+ refresh_tree(gobj);
15818
+ return 0;
15819
+ }
15034
15820
  /***************************
15035
15821
  * FSM
15036
15822
  ***************************/
@@ -15064,7 +15850,11 @@ function create_gclass$9(gclass_name) {
15064
15850
  ["EV_CENTER_ANCHOR", 0],
15065
15851
  ["EV_RESIZE", 0],
15066
15852
  ["EV_SHOW", 0],
15067
- ["EV_HIDE", 0]
15853
+ ["EV_HIDE", 0],
15854
+ ["EV_OPEN_GCLASS", 0],
15855
+ ["EV_GCLASS_CLOSED", 0],
15856
+ ["EV_EXPAND_PATH", 0],
15857
+ ["EV_LANGUAGE_CHANGED", 0]
15068
15858
  ], [["ST_IDLE", [
15069
15859
  [
15070
15860
  "EV_REFRESH",
@@ -15145,6 +15935,26 @@ function create_gclass$9(gclass_name) {
15145
15935
  "EV_HIDE",
15146
15936
  ac_hide$5,
15147
15937
  null
15938
+ ],
15939
+ [
15940
+ "EV_OPEN_GCLASS",
15941
+ ac_open_gclass,
15942
+ null
15943
+ ],
15944
+ [
15945
+ "EV_GCLASS_CLOSED",
15946
+ ac_gclass_closed,
15947
+ null
15948
+ ],
15949
+ [
15950
+ "EV_EXPAND_PATH",
15951
+ ac_gclass_expand_path,
15952
+ null
15953
+ ],
15954
+ [
15955
+ "EV_LANGUAGE_CHANGED",
15956
+ ac_language_changed$3,
15957
+ null
15148
15958
  ]
15149
15959
  ]]], gmt$9, 0, attrs_table$9, PRIVATE_DATA$9, 0, 0, 0, 0);
15150
15960
  if (!__gclass__$9) return -1;
@@ -54590,7 +55400,6 @@ function open_json_viewer(gobj) {
54590
55400
  return;
54591
55401
  }
54592
55402
  priv.json_gobj = jv;
54593
- gobj_start(jv);
54594
55403
  let $box = gobj_read_pointer_attr(jv, "$container");
54595
55404
  if (mobile) {
54596
55405
  if (!shell) {
@@ -54609,6 +55418,7 @@ function open_json_viewer(gobj) {
54609
55418
  gobj_send_event(gobj, "EV_JSON_CLOSED", {}, gobj);
54610
55419
  }
54611
55420
  });
55421
+ gobj_start(jv);
54612
55422
  } else {
54613
55423
  let $win_parent = shell && yui_shell_popup_layer(shell) || typeof document !== "undefined" && document.getElementById("top-layer") || null;
54614
55424
  priv.json_win = gobj_create_service(`treedb-jsonwin-${clean_name(gobj_name(gobj))}`, "C_YUI_WINDOW", {
@@ -54639,6 +55449,7 @@ function open_json_viewer(gobj) {
54639
55449
  return;
54640
55450
  }
54641
55451
  gobj_start(priv.json_win);
55452
+ gobj_start(jv);
54642
55453
  }
54643
55454
  request_print_tranger(gobj, "");
54644
55455
  }
@@ -54666,7 +55477,7 @@ function close_json_viewer(gobj) {
54666
55477
  log_warning(`${gobj_short_name(gobj)}: already gone: ${e}`);
54667
55478
  }
54668
55479
  if (jv && is_gobj(jv)) try {
54669
- gobj_stop(jv);
55480
+ if (gobj_is_running(jv)) gobj_stop(jv);
54670
55481
  gobj_destroy(jv);
54671
55482
  } catch (e) {
54672
55483
  log_warning(`${gobj_short_name(gobj)}: already gone: ${e}`);