@yuneta/gobj-ui 7.23.29 → 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;
@@ -13200,7 +13200,7 @@ function ac_json_item_clicked(gobj, event, kw, src) {
13200
13200
  /************************************************************
13201
13201
  * EV_LANGUAGE_CHANGED — re-translate chrome + re-render
13202
13202
  ************************************************************/
13203
- function ac_language_changed$3(gobj, event, kw, src) {
13203
+ function ac_language_changed$4(gobj, event, kw, src) {
13204
13204
  let priv = gobj.priv;
13205
13205
  let $container = gobj_read_attr(gobj, "$container");
13206
13206
  if ($container) {
@@ -13261,7 +13261,7 @@ function create_gclass$10(gclass_name) {
13261
13261
  ],
13262
13262
  [
13263
13263
  "EV_LANGUAGE_CHANGED",
13264
- ac_language_changed$3,
13264
+ ac_language_changed$4,
13265
13265
  null
13266
13266
  ],
13267
13267
  [
@@ -13337,7 +13337,7 @@ function create_gclass$10(gclass_name) {
13337
13337
  ],
13338
13338
  [
13339
13339
  "EV_LANGUAGE_CHANGED",
13340
- ac_language_changed$3,
13340
+ ac_language_changed$4,
13341
13341
  null
13342
13342
  ],
13343
13343
  [
@@ -13387,6 +13387,567 @@ function register_c_yui_json() {
13387
13387
  return create_gclass$10(GCLASS_NAME$10);
13388
13388
  }
13389
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
13390
13951
  //#region src/c_yui_gobj_tree_js.js
13391
13952
  /***********************************************************************
13392
13953
  * c_yui_gobj_tree_js.js
@@ -13497,6 +14058,9 @@ var PRIVATE_DATA$9 = {
13497
14058
  $popover: null,
13498
14059
  $popover_title: null,
13499
14060
  $popover_body: null,
14061
+ $popover_gclass: null,
14062
+ popover_node: null,
14063
+ gclass_view: null,
13500
14064
  node_by_id: null,
13501
14065
  collapse_state: null,
13502
14066
  resize_observer: null,
@@ -13511,34 +14075,92 @@ var PRIVATE_DATA$9 = {
13511
14075
  var __gclass__$9 = null;
13512
14076
  var __layout_registered__ = false;
13513
14077
  /***************************************************************
13514
- * 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.
13515
14086
  ***************************************************************/
13516
14087
  var ROLE_COLORS = {
13517
14088
  yuno: {
13518
- fill: "#E0F2FE",
13519
- stroke: "#075985"
14089
+ stroke: "#0284c7",
14090
+ accent: "#38bdf8"
13520
14091
  },
13521
14092
  service: {
13522
- fill: "#DCFCE7",
13523
- stroke: "#166534"
14093
+ stroke: "#059669",
14094
+ accent: "#34d399"
13524
14095
  },
13525
14096
  pure: {
13526
- fill: "#FEF9C3",
13527
- stroke: "#854D0E"
14097
+ stroke: "#d97706",
14098
+ accent: "#fbbf24"
13528
14099
  },
13529
14100
  volatil: {
13530
- fill: "#FCE7F3",
13531
- stroke: "#9D174D"
14101
+ stroke: "#db2777",
14102
+ accent: "#f472b6"
13532
14103
  },
13533
- default: {
13534
- fill: "#F3F4F6",
13535
- stroke: "#374151"
14104
+ child: {
14105
+ stroke: "#7c3aed",
14106
+ accent: "#a78bfa"
13536
14107
  }
13537
14108
  };
13538
- var STATE_COLORS = {
13539
- running_playing: "#15803D",
13540
- running: "#B45309",
13541
- 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
+ }
13542
14164
  };
13543
14165
  /***************************************************************
13544
14166
  * Fixed node sizes per role (px).
@@ -13548,16 +14170,16 @@ var STATE_COLORS = {
13548
14170
  var NODE_SIZES = {
13549
14171
  full: {
13550
14172
  yuno: {
13551
- w: 240,
13552
- h: 72
14173
+ w: 260,
14174
+ h: 96
13553
14175
  },
13554
14176
  service: {
13555
- w: 210,
13556
- h: 68
14177
+ w: 232,
14178
+ h: 92
13557
14179
  },
13558
14180
  child: {
13559
- w: 160,
13560
- h: 60
14181
+ w: 196,
14182
+ h: 86
13561
14183
  }
13562
14184
  },
13563
14185
  compact: {
@@ -13580,27 +14202,47 @@ var NODE_SIZES = {
13580
14202
  ***************************************************************/
13581
14203
  var GT_FONT = "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif";
13582
14204
  /***************************************************************
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.
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.
13586
14212
  ***************************************************************/
13587
- function role_card_style(stroke, dark) {
14213
+ function role_card_style(colors, dark) {
14214
+ let stroke = colors.stroke;
14215
+ let accent = colors.accent;
13588
14216
  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)"
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)"
13595
14225
  };
13596
14226
  }
13597
14227
  /***************************************************************
13598
- * Map a gobj to its visual category.
14228
+ * The pill colours of a node, for the theme in force.
13599
14229
  ***************************************************************/
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";
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
+ }
13604
14246
  }
13605
14247
  /***************************************************************
13606
14248
  * Layouts registry.
@@ -13713,13 +14355,21 @@ function mt_create$9(gobj) {
13713
14355
  ***************************************************************/
13714
14356
  function mt_start$9(gobj) {
13715
14357
  restore_view_shape(gobj);
14358
+ let shell = yui_shell_of(gobj);
14359
+ if (shell) gobj_subscribe_event(shell, "EV_LANGUAGE_CHANGED", {}, gobj);
13716
14360
  build_graph$2(gobj);
13717
14361
  load_tree(gobj);
13718
14362
  }
13719
14363
  /***************************************************************
13720
14364
  * Framework Method: Stop
13721
14365
  ***************************************************************/
13722
- 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
+ }
13723
14373
  /***************************************************************
13724
14374
  * Framework Method: Destroy
13725
14375
  ***************************************************************/
@@ -13745,6 +14395,8 @@ function mt_destroy$9(gobj) {
13745
14395
  priv.$popover = null;
13746
14396
  priv.$popover_title = null;
13747
14397
  priv.$popover_body = null;
14398
+ priv.$popover_gclass = null;
14399
+ priv.popover_node = null;
13748
14400
  priv.$layout_select = null;
13749
14401
  priv.node_by_id = null;
13750
14402
  priv.collapse_state = null;
@@ -13848,6 +14500,30 @@ function build_popover(gobj, $container) {
13848
14500
  text-overflow: ellipsis;
13849
14501
  `;
13850
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`);
13851
14527
  let $close = document.createElement("button");
13852
14528
  $close.type = "button";
13853
14529
  $close.className = "GOBJ_TREE_POPOVER_CLOSE";
@@ -14032,22 +14708,20 @@ function apply_layout(gobj) {
14032
14708
  });
14033
14709
  }
14034
14710
  /************************************************************
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
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.
14046
14716
  ************************************************************/
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;
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
+ }
14051
14725
  }
14052
14726
  /************************************************************
14053
14727
  * Generate a unique node id
@@ -14079,72 +14753,135 @@ function is_node_collapsed(gobj, full_name, num_children) {
14079
14753
  ************************************************************/
14080
14754
  function render_toggle_html(node_id, collapsed, num_children, cs) {
14081
14755
  let label = collapsed ? "+" + num_children : "−";
14756
+ let title = collapsed ? t("expand children") : t("collapse children");
14082
14757
  return `<span
14083
14758
  data-gobj-toggle="true"
14084
14759
  data-node-id="${node_id}"
14085
- title="${collapsed ? "Expand children" : "Collapse children"}"
14760
+ title="${escapeHtml(title)}"
14086
14761
  style="
14087
14762
  flex: 0 0 auto;
14088
14763
  display: inline-flex;
14089
14764
  align-items: center;
14090
14765
  justify-content: center;
14091
- min-width: 22px;
14092
- height: 16px;
14093
- padding: 0 5px;
14766
+ min-width: 24px;
14767
+ height: 18px;
14768
+ padding: 0 6px;
14094
14769
  margin-left: 4px;
14095
- background: ${cs.tagbg};
14096
- color: ${cs.border};
14097
- border: 1px solid ${cs.border};
14098
- border-radius: 6px;
14770
+ background: ${cs.accent};
14771
+ color: #ffffff;
14772
+ border: 1px solid ${cs.accent};
14773
+ border-radius: 9px;
14099
14774
  font-size: 10px;
14100
- font-weight: 600;
14775
+ font-weight: 700;
14101
14776
  line-height: 1;
14102
14777
  cursor: pointer;
14103
14778
  user-select: none;
14104
14779
  ">${label}</span>`;
14105
14780
  }
14106
14781
  /************************************************************
14107
- * 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.
14108
14844
  ************************************************************/
14109
- 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) {
14110
14846
  let priv = gobj.priv;
14111
14847
  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
14848
  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);
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);
14124
14853
  let size = NODE_SIZES[compact ? "compact" : "full"][category];
14125
14854
  let width = size.w;
14126
14855
  let height = size.h;
14127
14856
  let border_width = category === "yuno" || category === "service" ? 2 : 1;
14128
- let children = target_gobj.dl_children || [];
14857
+ let border_style = d.disabled ? "dashed" : "solid";
14858
+ let children = d.children || [];
14129
14859
  let num_children = children.length;
14130
14860
  let has_children = num_children > 0;
14131
- 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);
14132
14862
  let direct_children = [];
14133
14863
  for (let child of children) direct_children.push({
14134
- full_name: gobj_full_name(child),
14135
- has_grandchildren: (child.dl_children || []).length > 0
14864
+ full_name: child.fullname,
14865
+ has_grandchildren: (child.children || []).length > 0
14136
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;
14137
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 : "");
14138
14875
  let node_html;
14139
14876
  if (compact) {
14140
- let compact_label = escapeHtml(instance_name ? gclass_name + "^" + instance_name : gclass_name);
14877
+ let compact_label = escapeHtml(card_title);
14141
14878
  node_html = `
14142
- <div class="GOBJ_CARD" data-gobj-name="${escapeHtml(full_name)}" style="
14879
+ <div class="GOBJ_CARD" data-gobj-name="${escapeHtml(d.fullname)}" style="
14143
14880
  width: ${width}px;
14144
14881
  height: ${height}px;
14145
14882
  background: ${cs.bg};
14146
- border: ${border_width}px solid ${cs.border};
14147
- border-left: 5px solid ${cs.border};
14883
+ border: ${border_width}px ${border_style} ${cs.border};
14884
+ border-left: 6px solid ${cs.accent};
14148
14885
  border-radius: 7px;
14149
14886
  box-shadow: ${cs.shadow};
14150
14887
  font-family: ${GT_FONT};
@@ -14153,34 +14890,24 @@ function build_gobj_nodes(gobj, target_gobj, nodes, edges, parent_id, is_root, c
14153
14890
  padding: 0 8px;
14154
14891
  box-sizing: border-box;
14155
14892
  overflow: hidden;
14893
+ opacity: ${d.disabled ? "0.72" : "1"};
14156
14894
  cursor: pointer;
14157
14895
  " title="${compact_label}">
14158
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>
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>
14897
+ ${render_status_html(d, ss, labels, false)}
14164
14898
  ${toggle_html}
14165
14899
  </div>`;
14166
14900
  } 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("");
14901
+ let header_label = escapeHtml(d.gclass);
14902
+ let name_label = escapeHtml(d.name);
14903
+ let state_label = escapeHtml(d.state);
14178
14904
  node_html = `
14179
- <div class="GOBJ_CARD" data-gobj-name="${escapeHtml(full_name)}" style="
14905
+ <div class="GOBJ_CARD" data-gobj-name="${escapeHtml(d.fullname)}" style="
14180
14906
  width: ${width}px;
14181
14907
  height: ${height}px;
14182
14908
  background: ${cs.bg};
14183
- border: ${border_width}px solid ${cs.border};
14909
+ border: ${border_width}px ${border_style} ${cs.border};
14910
+ border-left: 6px solid ${cs.accent};
14184
14911
  border-radius: 9px;
14185
14912
  box-shadow: ${cs.shadow};
14186
14913
  overflow: hidden;
@@ -14189,48 +14916,29 @@ function build_gobj_nodes(gobj, target_gobj, nodes, edges, parent_id, is_root, c
14189
14916
  display: flex;
14190
14917
  flex-direction: column;
14191
14918
  justify-content: center;
14192
- padding: 5px 9px;
14193
- gap: 1px;
14919
+ padding: 6px 9px;
14920
+ gap: 2px;
14921
+ opacity: ${d.disabled ? "0.72" : "1"};
14194
14922
  cursor: pointer;
14195
- " title="${escapeHtml(gclass_name + (instance_name ? "^" + instance_name : ""))}">
14923
+ " title="${escapeHtml(card_title)}">
14196
14924
  <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>
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>
14198
14926
  ${toggle_html}
14199
14927
  </div>
14200
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>
14201
14929
  <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>
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)}
14206
14932
  </div>
14933
+ <div style="flex:0 0 auto; overflow:hidden; white-space:nowrap; text-overflow:ellipsis;">${render_badges_html(d, cs, labels)}</div>
14207
14934
  </div>`;
14208
14935
  }
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
14936
  if (!priv.node_by_id) priv.node_by_id = {};
14229
- priv.node_by_id[node_id] = node_data;
14937
+ priv.node_by_id[node_id] = d;
14230
14938
  let node = {
14231
14939
  id: node_id,
14232
14940
  type: "html",
14233
- data: node_data,
14941
+ data: d,
14234
14942
  style: {
14235
14943
  innerHTML: node_html,
14236
14944
  size: [width, height],
@@ -14243,11 +14951,11 @@ function build_gobj_nodes(gobj, target_gobj, nodes, edges, parent_id, is_root, c
14243
14951
  source: parent_id,
14244
14952
  target: node_id,
14245
14953
  style: {
14246
- stroke: colors.stroke,
14247
- lineWidth: 1
14954
+ stroke: colors.accent,
14955
+ lineWidth: 1.4
14248
14956
  }
14249
14957
  });
14250
- 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);
14251
14959
  return node_id;
14252
14960
  }
14253
14961
  /************************************************************
@@ -14264,9 +14972,13 @@ function load_tree(gobj) {
14264
14972
  let priv = gobj.priv;
14265
14973
  let graph = priv.graph;
14266
14974
  if (!graph) return;
14267
- let yuno = gobj_yuno();
14268
- 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
+ }
14269
14980
  let layout_cfg = get_layout_cfg(gobj_read_str_attr(gobj, "layout"));
14981
+ let labels = node_labels(t);
14270
14982
  let anchor = priv.pending_anchor || null;
14271
14983
  let preserve_view = !!priv.pending_preserve_view;
14272
14984
  priv.pending_anchor = null;
@@ -14275,7 +14987,7 @@ function load_tree(gobj) {
14275
14987
  priv.node_by_id = {};
14276
14988
  let nodes = [];
14277
14989
  let edges = [];
14278
- 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);
14279
14991
  if (nodes.length > 0) {
14280
14992
  let root_id = nodes[0].id;
14281
14993
  graph.setData({
@@ -14376,46 +15088,17 @@ function refresh_tree(gobj, opts) {
14376
15088
  }
14377
15089
  }
14378
15090
  /************************************************************
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
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.
14413
15096
  ************************************************************/
14414
15097
  function render_popover_body(gobj, node_data) {
14415
15098
  let $body = gobj.priv.$popover_body;
14416
15099
  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;">
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;">
14419
15102
  <span style="color:var(--bulma-text-weak, #6B7280); font-weight:500;">${escapeHtml(String(label))}</span>
14420
15103
  <span style="color:var(--bulma-text-strong, #1A1A1A); word-break:break-all;">${escapeHtml(String(value))}</span>
14421
15104
  </div>
@@ -14432,6 +15115,7 @@ function show_popover(gobj, node_data, client_x, client_y) {
14432
15115
  let $title = priv.$popover_title;
14433
15116
  let $container = gobj_read_attr(gobj, "$container");
14434
15117
  if (!$popover || !$title || !$container) return;
15118
+ priv.popover_node = node_data;
14435
15119
  $title.textContent = node_data.short_name || node_data.gclass + (node_data.name ? "^" + node_data.name : "");
14436
15120
  $title.title = $title.textContent;
14437
15121
  render_popover_body(gobj, node_data);
@@ -14462,6 +15146,7 @@ function hide_popover(gobj) {
14462
15146
  if (priv.$popover) priv.$popover.style.display = "none";
14463
15147
  if (priv.$popover_body) priv.$popover_body.innerHTML = "";
14464
15148
  if (priv.$popover_title) priv.$popover_title.textContent = "";
15149
+ priv.popover_node = null;
14465
15150
  }
14466
15151
  /************************************************************
14467
15152
  * Generic tree layout engine.
@@ -15031,6 +15716,68 @@ function ac_hide$5(gobj, event, kw, src) {
15031
15716
  hide_popover(gobj);
15032
15717
  return 0;
15033
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
+ }
15034
15781
  /***************************
15035
15782
  * FSM
15036
15783
  ***************************/
@@ -15064,7 +15811,11 @@ function create_gclass$9(gclass_name) {
15064
15811
  ["EV_CENTER_ANCHOR", 0],
15065
15812
  ["EV_RESIZE", 0],
15066
15813
  ["EV_SHOW", 0],
15067
- ["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]
15068
15819
  ], [["ST_IDLE", [
15069
15820
  [
15070
15821
  "EV_REFRESH",
@@ -15145,6 +15896,26 @@ function create_gclass$9(gclass_name) {
15145
15896
  "EV_HIDE",
15146
15897
  ac_hide$5,
15147
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
15148
15919
  ]
15149
15920
  ]]], gmt$9, 0, attrs_table$9, PRIVATE_DATA$9, 0, 0, 0, 0);
15150
15921
  if (!__gclass__$9) return -1;