@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.
@@ -26,22 +26,33 @@ import {
26
26
  kw_set_local_storage_value,
27
27
  gobj_write_str_attr,
28
28
  gobj_read_str_attr,
29
- gobj_publish_event,
30
29
  gobj_yuno,
31
- gobj_gclass_name,
30
+ gobj_yuno_name,
32
31
  gobj_short_name,
33
- gobj_full_name,
34
- gobj_current_state,
35
- gobj_is_running,
36
- gobj_is_playing,
37
- gobj_is_service,
38
- gobj_is_pure_child,
39
- gobj_is_volatil,
32
+ gobj_is_destroying,
33
+ gobj_unsubscribe_event,
40
34
  createElement2,
41
35
  escapeHtml,
36
+ log_warning,
42
37
  refresh_language,
43
38
  } from "@yuneta/gobj-js";
44
39
 
40
+ import {
41
+ describe_js_gobj,
42
+ node_badge_keys,
43
+ node_info_rows,
44
+ node_labels,
45
+ node_role,
46
+ node_status,
47
+ node_status_key,
48
+ } from "./gobj_tree_model.js";
49
+ import {
50
+ close_gclass_view,
51
+ gclass_view_available,
52
+ open_gclass_view,
53
+ } from "./yui_gclass_view.js";
54
+ import {yui_shell_of} from "./c_yui_shell.js";
55
+
45
56
  import {yui_toolbar} from "./yui_toolbar.js";
46
57
  import {
47
58
  yui_graph_camera_items,
@@ -249,6 +260,9 @@ let PRIVATE_DATA = {
249
260
  $popover: null,
250
261
  $popover_title: null,
251
262
  $popover_body: null,
263
+ $popover_gclass: null, // the "gclass" button, or null when unavailable
264
+ popover_node: null, // the descriptor the popover is showing
265
+ gclass_view: null, // handle of the open gclass window (or null)
252
266
  node_by_id: null, // map of node_id -> node_data (for popover)
253
267
  collapse_state: null, // map of full_name -> "collapsed" | "expanded"
254
268
  resize_observer: null, // ResizeObserver on the canvas mount element
@@ -274,20 +288,46 @@ let __gclass__ = null;
274
288
  let __layout_registered__ = false;
275
289
 
276
290
  /***************************************************************
277
- * Node colors by role
291
+ * Node colours by role.
292
+ *
293
+ * Saturated hues, one family apart from the next, and the same
294
+ * five everywhere the reader meets a gobj. `stroke` draws the
295
+ * border and the edge; `accent` is the brighter sibling used
296
+ * for the bar down the left of the card and the toggle badge,
297
+ * so a card carries its colour twice at two intensities and is
298
+ * readable at the zoom where the text is not.
278
299
  ***************************************************************/
279
300
  const ROLE_COLORS = {
280
- yuno: {fill: "#E0F2FE", stroke: "#075985"}, // blue
281
- service: {fill: "#DCFCE7", stroke: "#166534"}, // green
282
- pure: {fill: "#FEF9C3", stroke: "#854D0E"}, // yellow
283
- volatil: {fill: "#FCE7F3", stroke: "#9D174D"}, // pink
284
- default: {fill: "#F3F4F6", stroke: "#374151"}, // gray
301
+ yuno: {stroke: "#0284c7", accent: "#38bdf8"}, // sky
302
+ service: {stroke: "#059669", accent: "#34d399"}, // emerald
303
+ pure: {stroke: "#d97706", accent: "#fbbf24"}, // amber
304
+ volatil: {stroke: "#db2777", accent: "#f472b6"}, // pink
305
+ child: {stroke: "#7c3aed", accent: "#a78bfa"}, // violet
285
306
  };
286
307
 
287
- const STATE_COLORS = {
288
- running_playing: "#15803D", // green
289
- running: "#B45309", // amber
290
- stopped: "#B91C1C", // red
308
+ /***************************************************************
309
+ * Status colours: what the gobj is DOING.
310
+ *
311
+ * A pill and not a word: three states of one gobj are told
312
+ * apart across a whole tree by colour long before anybody
313
+ * reads them, and the dot alone never said which was which.
314
+ * `disabled` is a fourth entry and not a status -- a disabled
315
+ * gobj is still stopped or running -- but it takes the pill,
316
+ * because it is the fact that explains the other one.
317
+ ***************************************************************/
318
+ const STATUS_COLORS = {
319
+ light: {
320
+ playing: {fg: "#065f46", bg: "#a7f3d0", dot: "#10b981"},
321
+ running: {fg: "#92400e", bg: "#fde68a", dot: "#f59e0b"},
322
+ stopped: {fg: "#991b1b", bg: "#fecaca", dot: "#ef4444"},
323
+ disabled: {fg: "#334155", bg: "#e2e8f0", dot: "#94a3b8"},
324
+ },
325
+ dark: {
326
+ playing: {fg: "#6ee7b7", bg: "rgba(16,185,129,0.28)", dot: "#34d399"},
327
+ running: {fg: "#fcd34d", bg: "rgba(245,158,11,0.28)", dot: "#fbbf24"},
328
+ stopped: {fg: "#fca5a5", bg: "rgba(239,68,68,0.28)", dot: "#f87171"},
329
+ disabled: {fg: "#cbd5e1", bg: "rgba(148,163,184,0.28)", dot: "#94a3b8"},
330
+ },
291
331
  };
292
332
 
293
333
  /***************************************************************
@@ -297,9 +337,9 @@ const STATE_COLORS = {
297
337
  ***************************************************************/
298
338
  const NODE_SIZES = {
299
339
  full: {
300
- yuno: {w: 240, h: 72},
301
- service: {w: 210, h: 68},
302
- child: {w: 160, h: 60},
340
+ yuno: {w: 260, h: 96},
341
+ service: {w: 232, h: 92},
342
+ child: {w: 196, h: 86},
303
343
  },
304
344
  compact: {
305
345
  yuno: {w: 210, h: 30},
@@ -316,45 +356,64 @@ const GT_FONT =
316
356
  "Helvetica, Arial, sans-serif";
317
357
 
318
358
  /***************************************************************
319
- * Soft, theme-aware card palette derived from the role colour
320
- * (same visual language as the treedb graph cards): light tint
321
- * fill + role-colour border, brightened on dark.
359
+ * Theme-aware card palette derived from the role colour.
360
+ *
361
+ * Brighter than a tint: at 9% of the role colour every card
362
+ * was the same near-white rectangle and the tree read as a
363
+ * wireframe. The fill carries enough of the hue to be named
364
+ * from across the canvas, and the border and the accent bar
365
+ * carry the rest.
322
366
  ***************************************************************/
323
- function role_card_style(stroke, dark)
367
+ function role_card_style(colors, dark)
324
368
  {
325
- // On dark, lift the card off the near-black canvas with a
326
- // clearly-lighter slate base + vivid border (a faint tint on
327
- // the canvas colour makes the cards invisible).
369
+ let stroke = colors.stroke;
370
+ let accent = colors.accent;
371
+
328
372
  return {
329
373
  bg: dark
330
- ? `color-mix(in srgb, ${stroke} 30%, #2c3542)`
331
- : `color-mix(in srgb, ${stroke} 9%, #ffffff)`,
332
- border: dark
333
- ? `color-mix(in srgb, ${stroke} 85%, #ffffff)`
334
- : stroke,
335
- title: dark ? "#e8eaed" : "#0f172a",
336
- sub: dark ? "#9aa4b2" : "#64748b",
374
+ ? `color-mix(in srgb, ${stroke} 34%, #253044)`
375
+ : `color-mix(in srgb, ${stroke} 13%, #ffffff)`,
376
+ border: dark? accent: stroke,
377
+ accent: dark? accent: stroke,
378
+ title: dark ? "#f1f5f9" : "#0f172a",
379
+ sub: dark ? "#a8b3c2" : "#475569",
337
380
  tagbg: dark
338
- ? `color-mix(in srgb, ${stroke} 42%, #2c3542)`
339
- : `color-mix(in srgb, ${stroke} 14%, #ffffff)`,
381
+ ? `color-mix(in srgb, ${stroke} 48%, #253044)`
382
+ : `color-mix(in srgb, ${stroke} 20%, #ffffff)`,
383
+ tagfg: dark? "#f1f5f9": stroke,
340
384
  shadow: dark
341
- ? "0 1px 3px rgba(0,0,0,0.45)"
342
- : "0 1px 3px rgba(15,23,42,0.12)",
385
+ ? "0 2px 6px rgba(0,0,0,0.50)"
386
+ : "0 2px 5px rgba(15,23,42,0.16)",
343
387
  };
344
388
  }
345
389
 
346
390
  /***************************************************************
347
- * Map a gobj to its visual category.
391
+ * The pill colours of a node, for the theme in force.
348
392
  ***************************************************************/
349
- function get_node_category(target_gobj, is_root)
393
+ function status_card_style(d, dark)
350
394
  {
351
- if(is_root) {
352
- return "yuno";
395
+ let table = dark? STATUS_COLORS.dark: STATUS_COLORS.light;
396
+ if(d.disabled) {
397
+ return table.disabled;
353
398
  }
354
- if(gobj_is_service(target_gobj)) {
355
- return "service";
399
+ return table[node_status(d)] || table.stopped;
400
+ }
401
+
402
+ /***************************************************************
403
+ * Map a node to its visual size class. Services are drawn
404
+ * bigger than plain children so the service boundary of the
405
+ * hierarchy is visible before anything is read.
406
+ ***************************************************************/
407
+ function get_node_category(d)
408
+ {
409
+ switch(node_role(d)) {
410
+ case "yuno":
411
+ return "yuno";
412
+ case "service":
413
+ return "service";
414
+ default:
415
+ return "child";
356
416
  }
357
- return "child";
358
417
  }
359
418
 
360
419
  /***************************************************************
@@ -498,6 +557,17 @@ function mt_start(gobj)
498
557
  */
499
558
  restore_view_shape(gobj);
500
559
 
560
+ /* The cards carry translated text now (the status pill, the
561
+ * badges), and G6 draws them as innerHTML inside its own
562
+ * canvas: refresh_language() cannot reach in there, so a
563
+ * language switch is a REBUILD. Subscribe to the shell
564
+ * directly, like C_YUI_PERIOD -- a host that mounts a bare
565
+ * tree gets the repaint without having to forward anything. */
566
+ let shell = yui_shell_of(gobj);
567
+ if(shell) {
568
+ gobj_subscribe_event(shell, "EV_LANGUAGE_CHANGED", {}, gobj);
569
+ }
570
+
501
571
  build_graph(gobj);
502
572
  load_tree(gobj);
503
573
  }
@@ -507,6 +577,18 @@ function mt_start(gobj)
507
577
  ***************************************************************/
508
578
  function mt_stop(gobj)
509
579
  {
580
+ let priv = gobj.priv;
581
+
582
+ let shell = yui_shell_of(gobj);
583
+ if(shell) {
584
+ gobj_unsubscribe_event(shell, "EV_LANGUAGE_CHANGED", {}, gobj);
585
+ }
586
+
587
+ /* In mt_stop and NOT in mt_destroy: gobj_destroy() destroys the
588
+ * children BEFORE calling mt_destroy, so a hosted gobj retired
589
+ * there is retired after the framework already took it down. */
590
+ close_gclass_view(priv.gclass_view);
591
+ priv.gclass_view = null;
510
592
  }
511
593
 
512
594
  /***************************************************************
@@ -539,6 +621,8 @@ function mt_destroy(gobj)
539
621
  priv.$popover = null;
540
622
  priv.$popover_title = null;
541
623
  priv.$popover_body = null;
624
+ priv.$popover_gclass = null;
625
+ priv.popover_node = null;
542
626
  priv.$layout_select = null;
543
627
  priv.node_by_id = null;
544
628
  priv.collapse_state = null;
@@ -657,6 +741,42 @@ function build_popover(gobj, $container)
657
741
  `;
658
742
  $header.appendChild($title);
659
743
 
744
+ /* What this gobj IS: its gclass, opened in the gclass viewer.
745
+ *
746
+ * Built only where the app registers C_YUI_JSON -- the viewer
747
+ * the window hosts. An app that does not carry it gets no
748
+ * button rather than a button that fails, and the reason is
749
+ * logged once, at build time. */
750
+ if(gclass_view_available()) {
751
+ let $gclass = document.createElement("button");
752
+ $gclass.type = "button";
753
+ $gclass.className = "GOBJ_TREE_POPOVER_GCLASS button is-small";
754
+ $gclass.style.cssText = `
755
+ margin-right: 6px;
756
+ padding: 0 8px;
757
+ height: 22px;
758
+ font-size: 11px;
759
+ white-space: nowrap;
760
+ `;
761
+ $gclass.textContent = t("gclass");
762
+ $gclass.setAttribute("i18n", "gclass");
763
+ $gclass.title = t("view gclass");
764
+ $gclass.setAttribute("data-i18n-title", "view gclass");
765
+ $gclass.setAttribute("aria-label", t("view gclass"));
766
+ $gclass.setAttribute("data-i18n-aria-label", "view gclass");
767
+ $gclass.addEventListener("click", (evt) => {
768
+ evt.stopPropagation();
769
+ gobj_send_event(gobj, "EV_OPEN_GCLASS", {}, gobj);
770
+ });
771
+ $header.appendChild($gclass);
772
+ priv.$popover_gclass = $gclass;
773
+ } else {
774
+ log_warning(
775
+ `${gobj_short_name(gobj)}: no C_YUI_JSON registered, ` +
776
+ `the gclass viewer is not offered`
777
+ );
778
+ }
779
+
660
780
  let $close = document.createElement("button");
661
781
  $close.type = "button";
662
782
  /* Named so a stylesheet can reach it: the size a FINGER needs is a
@@ -917,37 +1037,26 @@ function apply_layout(gobj)
917
1037
  }
918
1038
 
919
1039
  /************************************************************
920
- * Resolve role colors for a gobj
1040
+ * The role colours of a node.
1041
+ *
1042
+ * Note the ORDER: volatil before service. A volatil gobj that
1043
+ * is also a service is drawn volatil, because that is the fact
1044
+ * that decides how long it lives.
921
1045
  ************************************************************/
922
- function get_role_colors(target_gobj, is_root)
1046
+ function get_role_colors(d)
923
1047
  {
924
- if(is_root) {
925
- return ROLE_COLORS.yuno;
1048
+ switch(node_role(d)) {
1049
+ case "yuno":
1050
+ return ROLE_COLORS.yuno;
1051
+ case "volatil":
1052
+ return ROLE_COLORS.volatil;
1053
+ case "service":
1054
+ return ROLE_COLORS.service;
1055
+ case "pure":
1056
+ return ROLE_COLORS.pure;
1057
+ default:
1058
+ return ROLE_COLORS.child;
926
1059
  }
927
- if(gobj_is_volatil(target_gobj)) {
928
- return ROLE_COLORS.volatil;
929
- }
930
- if(gobj_is_service(target_gobj)) {
931
- return ROLE_COLORS.service;
932
- }
933
- if(gobj_is_pure_child(target_gobj)) {
934
- return ROLE_COLORS.pure;
935
- }
936
- return ROLE_COLORS.default;
937
- }
938
-
939
- /************************************************************
940
- * Resolve state color
941
- ************************************************************/
942
- function get_state_color(target_gobj)
943
- {
944
- if(!gobj_is_running(target_gobj)) {
945
- return STATE_COLORS.stopped;
946
- }
947
- if(gobj_is_playing(target_gobj)) {
948
- return STATE_COLORS.running_playing;
949
- }
950
- return STATE_COLORS.running;
951
1060
  }
952
1061
 
953
1062
  /************************************************************
@@ -994,27 +1103,27 @@ function is_node_collapsed(gobj, full_name, num_children)
994
1103
  ************************************************************/
995
1104
  function render_toggle_html(node_id, collapsed, num_children, cs)
996
1105
  {
997
- let label = collapsed ? ("+" + num_children) : "−";
998
- let title = collapsed ? "Expand children" : "Collapse children";
1106
+ let label = collapsed ? ("+" + num_children) : "\u2212";
1107
+ let title = collapsed ? t("expand children") : t("collapse children");
999
1108
  return `<span
1000
1109
  data-gobj-toggle="true"
1001
1110
  data-node-id="${node_id}"
1002
- title="${title}"
1111
+ title="${escapeHtml(title)}"
1003
1112
  style="
1004
1113
  flex: 0 0 auto;
1005
1114
  display: inline-flex;
1006
1115
  align-items: center;
1007
1116
  justify-content: center;
1008
- min-width: 22px;
1009
- height: 16px;
1010
- padding: 0 5px;
1117
+ min-width: 24px;
1118
+ height: 18px;
1119
+ padding: 0 6px;
1011
1120
  margin-left: 4px;
1012
- background: ${cs.tagbg};
1013
- color: ${cs.border};
1014
- border: 1px solid ${cs.border};
1015
- border-radius: 6px;
1121
+ background: ${cs.accent};
1122
+ color: #ffffff;
1123
+ border: 1px solid ${cs.accent};
1124
+ border-radius: 9px;
1016
1125
  font-size: 10px;
1017
- font-weight: 600;
1126
+ font-weight: 700;
1018
1127
  line-height: 1;
1019
1128
  cursor: pointer;
1020
1129
  user-select: none;
@@ -1022,27 +1131,92 @@ function render_toggle_html(node_id, collapsed, num_children, cs)
1022
1131
  }
1023
1132
 
1024
1133
  /************************************************************
1025
- * Recursively build nodes and edges from the gobj tree
1134
+ * HTML for the status pill: what the gobj is DOING.
1135
+ *
1136
+ * A dot plus a word, and the word is dropped in the compact
1137
+ * card where there is no room for it -- the colour still
1138
+ * carries it, and the `title` says it in full either way.
1026
1139
  ************************************************************/
1027
- function build_gobj_nodes(gobj, target_gobj, nodes, edges, parent_id, is_root, compact)
1140
+ function render_status_html(d, ss, labels, with_label)
1141
+ {
1142
+ let full = d.disabled?
1143
+ labels.field.disabled:
1144
+ (labels.status[node_status_key(d)] || node_status_key(d));
1145
+ let label = d.disabled?
1146
+ labels.field.disabled:
1147
+ (labels.status_short[node_status(d)] || node_status(d));
1148
+
1149
+ if(!with_label) {
1150
+ return `<span title="${escapeHtml(full)}" style="
1151
+ flex: 0 0 auto;
1152
+ width: 10px; height: 10px; border-radius: 50%;
1153
+ background: ${ss.dot};
1154
+ box-shadow: 0 0 0 2px ${ss.bg};
1155
+ margin-left: 6px;
1156
+ "></span>`;
1157
+ }
1158
+
1159
+ return `<span title="${escapeHtml(full)}" style="
1160
+ flex: 0 0 auto;
1161
+ display: inline-flex;
1162
+ align-items: center;
1163
+ gap: 4px;
1164
+ padding: 1px 7px 1px 5px;
1165
+ background: ${ss.bg};
1166
+ color: ${ss.fg};
1167
+ border-radius: 9px;
1168
+ font-size: 10px;
1169
+ font-weight: 700;
1170
+ line-height: 1.45;
1171
+ white-space: nowrap;
1172
+ "><span style="width:7px; height:7px; border-radius:50%; background:${ss.dot};"></span>${escapeHtml(label)}</span>`;
1173
+ }
1174
+
1175
+ /************************************************************
1176
+ * HTML for the badges: what the gobj IS, besides its colour.
1177
+ ************************************************************/
1178
+ function render_badges_html(d, cs, labels)
1179
+ {
1180
+ let html = "";
1181
+ for(let key of node_badge_keys(d)) {
1182
+ let label = labels.badge[key] || key;
1183
+ html += `<span style="
1184
+ font-size: 10px;
1185
+ font-weight: 600;
1186
+ padding: 0 6px;
1187
+ margin-right: 4px;
1188
+ background: ${cs.tagbg};
1189
+ color: ${cs.tagfg};
1190
+ border: 1px solid ${cs.accent};
1191
+ border-radius: 9px;
1192
+ white-space: nowrap;
1193
+ ">${escapeHtml(label)}</span>`;
1194
+ }
1195
+ return html;
1196
+ }
1197
+
1198
+ /************************************************************
1199
+ * Recursively build nodes and edges from a NODE DESCRIPTOR
1200
+ * tree (gobj_tree_model.js), never from gobjs: the same
1201
+ * builder draws the browser yuno and a backend one.
1202
+ *
1203
+ * The descriptor is also what the popover reads, so the view
1204
+ * fields it needs are written onto it here (`full_name`,
1205
+ * `short_name`, `num_children`, `is_collapsed`,
1206
+ * `direct_children`) instead of copied into a second object
1207
+ * that can then disagree with the first.
1208
+ ************************************************************/
1209
+ function build_gobj_nodes(gobj, d, nodes, edges, parent_id, compact, labels)
1028
1210
  {
1029
1211
  let priv = gobj.priv;
1030
1212
  let node_id = gen_node_id(gobj);
1031
1213
 
1032
- let gclass_name = gobj_gclass_name(target_gobj) || "";
1033
- let instance_name = gobj_name(target_gobj) || "";
1034
- let state = gobj_current_state(target_gobj) || "";
1035
- let full_name = gobj_full_name(target_gobj);
1036
- let colors = get_role_colors(target_gobj, is_root);
1037
1214
  let dark = yui_is_dark();
1038
- let cs = role_card_style(colors.stroke, dark);
1039
- let state_color = get_state_color(target_gobj);
1040
-
1041
- let running = gobj_is_running(target_gobj);
1042
- let playing = gobj_is_playing(target_gobj);
1043
- let status_text = !running ? "stopped" : (playing ? "playing" : "running");
1215
+ let colors = get_role_colors(d);
1216
+ let cs = role_card_style(colors, dark);
1217
+ let ss = status_card_style(d, dark);
1044
1218
 
1045
- let category = get_node_category(target_gobj, is_root);
1219
+ let category = get_node_category(d);
1046
1220
  let size = NODE_SIZES[compact ? "compact" : "full"][category];
1047
1221
  let width = size.w;
1048
1222
  let height = size.h;
@@ -1053,10 +1227,15 @@ function build_gobj_nodes(gobj, target_gobj, nodes, edges, parent_id, is_root, c
1053
1227
  */
1054
1228
  let border_width = (category === "yuno" || category === "service") ? 2 : 1;
1055
1229
 
1056
- let children = target_gobj.dl_children || [];
1230
+ /* A disabled gobj is drawn as one: a dashed border says
1231
+ * "this branch is out of the game" at any zoom. */
1232
+ let border_style = d.disabled? "dashed": "solid";
1233
+
1234
+ let children = d.children || [];
1057
1235
  let num_children = children.length;
1058
1236
  let has_children = num_children > 0;
1059
- let collapsed = has_children && is_node_collapsed(gobj, full_name, num_children);
1237
+ let collapsed = has_children &&
1238
+ is_node_collapsed(gobj, d.fullname, num_children);
1060
1239
 
1061
1240
  /*
1062
1241
  * Direct-children index (collected even when this node is
@@ -1066,33 +1245,41 @@ function build_gobj_nodes(gobj, target_gobj, nodes, edges, parent_id, is_root, c
1066
1245
  let direct_children = [];
1067
1246
  for(let child of children) {
1068
1247
  direct_children.push({
1069
- full_name: gobj_full_name(child),
1070
- has_grandchildren: (child.dl_children || []).length > 0,
1248
+ full_name: child.fullname,
1249
+ has_grandchildren: (child.children || []).length > 0,
1071
1250
  });
1072
1251
  }
1073
1252
 
1253
+ /* What the VIEW needs on top of the descriptor. Written onto
1254
+ * it, because the popover reads the same object. */
1255
+ d.full_name = d.fullname;
1256
+ d.short_name = d.shortname;
1257
+ d.num_children = num_children;
1258
+ d.is_collapsed = collapsed;
1259
+ d.direct_children = direct_children;
1260
+ d.node_id = node_id;
1261
+
1074
1262
  let toggle_html = has_children
1075
1263
  ? render_toggle_html(node_id, collapsed, num_children, cs)
1076
1264
  : "";
1077
1265
 
1266
+ let card_title = d.gclass + (d.name? ("^" + d.name): "");
1078
1267
  let node_html;
1079
1268
 
1080
1269
  if(compact) {
1081
1270
  /*
1082
- * Compact one-line node: "gclass^name" with a left color bar,
1083
- * state dot and optional +/- toggle. Name truncates with "...".
1271
+ * Compact one-line node: "gclass^name" with a left colour bar
1272
+ * and the status dot. Name truncates with "...".
1084
1273
  */
1085
- let compact_label = escapeHtml(
1086
- instance_name ? (gclass_name + "^" + instance_name) : gclass_name
1087
- );
1274
+ let compact_label = escapeHtml(card_title);
1088
1275
 
1089
1276
  node_html = `
1090
- <div class="GOBJ_CARD" data-gobj-name="${escapeHtml(full_name)}" style="
1277
+ <div class="GOBJ_CARD" data-gobj-name="${escapeHtml(d.fullname)}" style="
1091
1278
  width: ${width}px;
1092
1279
  height: ${height}px;
1093
1280
  background: ${cs.bg};
1094
- border: ${border_width}px solid ${cs.border};
1095
- border-left: 5px solid ${cs.border};
1281
+ border: ${border_width}px ${border_style} ${cs.border};
1282
+ border-left: 6px solid ${cs.accent};
1096
1283
  border-radius: 7px;
1097
1284
  box-shadow: ${cs.shadow};
1098
1285
  font-family: ${GT_FONT};
@@ -1101,49 +1288,30 @@ function build_gobj_nodes(gobj, target_gobj, nodes, edges, parent_id, is_root, c
1101
1288
  padding: 0 8px;
1102
1289
  box-sizing: border-box;
1103
1290
  overflow: hidden;
1291
+ opacity: ${d.disabled? "0.72": "1"};
1104
1292
  cursor: pointer;
1105
1293
  " title="${compact_label}">
1106
1294
  <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>
1107
- <span title="${escapeHtml(state || status_text)}" style="
1108
- flex: 0 0 auto;
1109
- width: 8px; height: 8px; border-radius: 50%;
1110
- background: ${state_color}; margin-left: 6px;
1111
- "></span>
1295
+ ${render_status_html(d, ss, labels, false)}
1112
1296
  ${toggle_html}
1113
1297
  </div>`;
1114
1298
  } else {
1115
1299
  /*
1116
- * Full node: header (gclass + optional toggle), name, badges + state.
1117
- * All rows truncate with "..." — width is fixed.
1300
+ * Full node: header (gclass + optional toggle), name, state,
1301
+ * badges + status pill. All rows truncate with "..." -- the
1302
+ * width is fixed.
1118
1303
  */
1119
- let header_label = escapeHtml(gclass_name);
1120
- let name_label = escapeHtml(instance_name);
1121
- let state_label = escapeHtml(state);
1122
-
1123
- let badges = [];
1124
- if(is_root) {
1125
- badges.push("yuno");
1126
- } else {
1127
- if(gobj_is_service(target_gobj)) {
1128
- badges.push("service");
1129
- }
1130
- if(gobj_is_pure_child(target_gobj)) {
1131
- badges.push("pure");
1132
- }
1133
- if(gobj_is_volatil(target_gobj)) {
1134
- badges.push("volatil");
1135
- }
1136
- }
1137
- let badges_html = badges.map(b =>
1138
- `<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>`
1139
- ).join("");
1304
+ let header_label = escapeHtml(d.gclass);
1305
+ let name_label = escapeHtml(d.name);
1306
+ let state_label = escapeHtml(d.state);
1140
1307
 
1141
1308
  node_html = `
1142
- <div class="GOBJ_CARD" data-gobj-name="${escapeHtml(full_name)}" style="
1309
+ <div class="GOBJ_CARD" data-gobj-name="${escapeHtml(d.fullname)}" style="
1143
1310
  width: ${width}px;
1144
1311
  height: ${height}px;
1145
1312
  background: ${cs.bg};
1146
- border: ${border_width}px solid ${cs.border};
1313
+ border: ${border_width}px ${border_style} ${cs.border};
1314
+ border-left: 6px solid ${cs.accent};
1147
1315
  border-radius: 9px;
1148
1316
  box-shadow: ${cs.shadow};
1149
1317
  overflow: hidden;
@@ -1152,59 +1320,33 @@ function build_gobj_nodes(gobj, target_gobj, nodes, edges, parent_id, is_root, c
1152
1320
  display: flex;
1153
1321
  flex-direction: column;
1154
1322
  justify-content: center;
1155
- padding: 5px 9px;
1156
- gap: 1px;
1323
+ padding: 6px 9px;
1324
+ gap: 2px;
1325
+ opacity: ${d.disabled? "0.72": "1"};
1157
1326
  cursor: pointer;
1158
- " title="${escapeHtml(gclass_name + (instance_name ? "^" + instance_name : ""))}">
1327
+ " title="${escapeHtml(card_title)}">
1159
1328
  <div style="flex:0 0 auto; display:flex; align-items:center; gap:6px; overflow:hidden;">
1160
- <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>
1329
+ <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>
1161
1330
  ${toggle_html}
1162
1331
  </div>
1163
1332
  <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>
1164
1333
  <div style="flex:0 0 auto; display:flex; justify-content:space-between; align-items:center; gap:6px; overflow:hidden;">
1165
- <span style="flex:1 1 auto; min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;">${badges_html}</span>
1166
- <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;">
1167
- <span style="width:7px; height:7px; border-radius:50%; background:${state_color};"></span>${state_label || status_text}
1168
- </span>
1334
+ <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>
1335
+ ${render_status_html(d, ss, labels, true)}
1169
1336
  </div>
1337
+ <div style="flex:0 0 auto; overflow:hidden; white-space:nowrap; text-overflow:ellipsis;">${render_badges_html(d, cs, labels)}</div>
1170
1338
  </div>`;
1171
1339
  }
1172
1340
 
1173
- let parent_gobj = gobj_parent(target_gobj);
1174
-
1175
- /*
1176
- * Node data kept for the popover. Extend this object when
1177
- * you want to surface more attributes — then add a row in
1178
- * get_node_info_rows() with the same key.
1179
- */
1180
- let node_data = {
1181
- gclass: gclass_name,
1182
- name: instance_name,
1183
- short_name: gobj_short_name(target_gobj),
1184
- full_name: full_name,
1185
- parent_short_name: parent_gobj ? gobj_short_name(parent_gobj) : "",
1186
- state: state,
1187
- category: category,
1188
- is_yuno: is_root,
1189
- is_service: gobj_is_service(target_gobj),
1190
- is_pure_child: gobj_is_pure_child(target_gobj),
1191
- is_volatil: gobj_is_volatil(target_gobj),
1192
- is_running: running,
1193
- is_playing: playing,
1194
- num_children: num_children,
1195
- is_collapsed: collapsed,
1196
- direct_children: direct_children,
1197
- };
1198
-
1199
1341
  if(!priv.node_by_id) {
1200
1342
  priv.node_by_id = {};
1201
1343
  }
1202
- priv.node_by_id[node_id] = node_data;
1344
+ priv.node_by_id[node_id] = d;
1203
1345
 
1204
1346
  let node = {
1205
1347
  id: node_id,
1206
1348
  type: 'html',
1207
- data: node_data,
1349
+ data: d,
1208
1350
  style: {
1209
1351
  innerHTML: node_html,
1210
1352
  size: [width, height],
@@ -1219,8 +1361,8 @@ function build_gobj_nodes(gobj, target_gobj, nodes, edges, parent_id, is_root, c
1219
1361
  source: parent_id,
1220
1362
  target: node_id,
1221
1363
  style: {
1222
- stroke: colors.stroke,
1223
- lineWidth: 1,
1364
+ stroke: colors.accent,
1365
+ lineWidth: 1.4,
1224
1366
  }
1225
1367
  });
1226
1368
  }
@@ -1230,7 +1372,7 @@ function build_gobj_nodes(gobj, target_gobj, nodes, edges, parent_id, is_root, c
1230
1372
  */
1231
1373
  if(!collapsed) {
1232
1374
  for(let child of children) {
1233
- build_gobj_nodes(gobj, child, nodes, edges, node_id, false, compact);
1375
+ build_gobj_nodes(gobj, child, nodes, edges, node_id, compact, labels);
1234
1376
  }
1235
1377
  }
1236
1378
 
@@ -1256,12 +1398,23 @@ function load_tree(gobj)
1256
1398
  return;
1257
1399
  }
1258
1400
 
1259
- let yuno = gobj_yuno();
1260
- if(!yuno) {
1401
+ /*
1402
+ * The tree, as data.
1403
+ *
1404
+ * The graph draws DESCRIPTORS, so this is the one line that
1405
+ * says which yuno is on screen. A backend yuno enters here:
1406
+ * describe_backend_tree(<the `view-gobj-tree` answer>) in
1407
+ * place of describe_js_gobj(gobj_yuno(), true), and nothing
1408
+ * below this point changes.
1409
+ */
1410
+ let root = describe_js_gobj(gobj_yuno(), true);
1411
+ if(!root) {
1412
+ log_error(`${gobj_short_name(gobj)}: no yuno to draw`);
1261
1413
  return;
1262
1414
  }
1263
1415
 
1264
1416
  let layout_cfg = get_layout_cfg(gobj_read_str_attr(gobj, "layout"));
1417
+ let labels = node_labels(t);
1265
1418
 
1266
1419
  let anchor = priv.pending_anchor || null;
1267
1420
  let preserve_view = !!priv.pending_preserve_view;
@@ -1274,7 +1427,7 @@ function load_tree(gobj)
1274
1427
  let nodes = [];
1275
1428
  let edges = [];
1276
1429
 
1277
- build_gobj_nodes(gobj, yuno, nodes, edges, null, true, layout_cfg.compact);
1430
+ build_gobj_nodes(gobj, root, nodes, edges, null, layout_cfg.compact, labels);
1278
1431
 
1279
1432
  if(nodes.length > 0) {
1280
1433
  let root_id = nodes[0].id; /* built before its children */
@@ -1434,67 +1587,12 @@ function refresh_tree(gobj, opts)
1434
1587
  }
1435
1588
  }
1436
1589
 
1437
- /************************************************************
1438
- * Rows to show in the popover for a given node data.
1439
- *
1440
- * This is the extension point: to surface a new attribute,
1441
- * collect it into `node_data` in build_gobj_nodes(), then
1442
- * append a `[label, value]` pair here.
1443
- ************************************************************/
1444
- function get_node_info_rows(node_data)
1445
- {
1446
- let rows = [];
1447
-
1448
- rows.push([t("gclass"), node_data.gclass || "—"]);
1449
- rows.push([t("name"), node_data.name || "—"]);
1450
-
1451
- if(node_data.full_name && node_data.full_name !== node_data.short_name) {
1452
- rows.push([t("full name"), node_data.full_name]);
1453
- }
1454
-
1455
- let role;
1456
- if(node_data.is_yuno) {
1457
- role = "yuno";
1458
- } else if(node_data.is_service) {
1459
- role = "service";
1460
- } else if(node_data.is_volatil) {
1461
- role = "volatil child";
1462
- } else if(node_data.is_pure_child) {
1463
- role = "pure child";
1464
- } else {
1465
- role = "child";
1466
- }
1467
- rows.push([t("role"), role]);
1468
-
1469
- let status;
1470
- if(!node_data.is_running) {
1471
- status = "stopped";
1472
- } else if(node_data.is_playing) {
1473
- status = "running + playing";
1474
- } else {
1475
- status = "running";
1476
- }
1477
- rows.push([t("status"), status]);
1478
- rows.push([t("state"), node_data.state || "—"]);
1479
-
1480
- if(node_data.parent_short_name) {
1481
- rows.push([t("parent"), node_data.parent_short_name]);
1482
- }
1483
-
1484
- if(typeof node_data.num_children === "number") {
1485
- let kids = node_data.num_children;
1486
- if(kids > 0 && node_data.is_collapsed) {
1487
- rows.push([t("children"), kids + " " + t("(collapsed)")]);
1488
- } else if(kids > 0) {
1489
- rows.push([t("children"), String(kids)]);
1490
- }
1491
- }
1492
-
1493
- return rows;
1494
- }
1495
-
1496
1590
  /************************************************************
1497
1591
  * Render popover body from the info rows of a node.
1592
+ *
1593
+ * The rows come from gobj_tree_model.js, so a node of the
1594
+ * browser yuno and a node of a backend one are read out with
1595
+ * the same words in the same order.
1498
1596
  ************************************************************/
1499
1597
  function render_popover_body(gobj, node_data)
1500
1598
  {
@@ -1504,14 +1602,14 @@ function render_popover_body(gobj, node_data)
1504
1602
  return;
1505
1603
  }
1506
1604
 
1507
- let rows = get_node_info_rows(node_data);
1605
+ let rows = node_info_rows(node_data, t);
1508
1606
 
1509
1607
  /* Bulma scheme vars, like the popover chrome around it (build_popover):
1510
1608
  * they flip with <html data-theme>, so the popover follows the theme
1511
1609
  * with no redraw. Hardcoding them is what made the value near-black
1512
1610
  * (#1A1A1A) on the near-black dark card background — invisible. */
1513
1611
  let rows_html = rows.map(([label, value]) => `
1514
- <div style="display:grid; grid-template-columns: 90px 1fr; column-gap:10px; padding: 2px 0;">
1612
+ <div style="display:grid; grid-template-columns: 96px 1fr; column-gap:10px; padding: 2px 0;">
1515
1613
  <span style="color:var(--bulma-text-weak, #6B7280); font-weight:500;">${escapeHtml(String(label))}</span>
1516
1614
  <span style="color:var(--bulma-text-strong, #1A1A1A); word-break:break-all;">${escapeHtml(String(value))}</span>
1517
1615
  </div>
@@ -1535,6 +1633,8 @@ function show_popover(gobj, node_data, client_x, client_y)
1535
1633
  return;
1536
1634
  }
1537
1635
 
1636
+ priv.popover_node = node_data;
1637
+
1538
1638
  $title.textContent = node_data.short_name ||
1539
1639
  (node_data.gclass + (node_data.name ? "^" + node_data.name : ""));
1540
1640
  $title.title = $title.textContent;
@@ -1588,6 +1688,7 @@ function hide_popover(gobj)
1588
1688
  if(priv.$popover_title) {
1589
1689
  priv.$popover_title.textContent = "";
1590
1690
  }
1691
+ priv.popover_node = null;
1591
1692
  }
1592
1693
 
1593
1694
  /************************************************************
@@ -2455,6 +2556,87 @@ function ac_hide(gobj, event, kw, src)
2455
2556
  return 0;
2456
2557
  }
2457
2558
 
2559
+ /************************************************************
2560
+ * Open the gclass of the node the popover is showing.
2561
+ *
2562
+ * One window at a time: opening a second gclass replaces the
2563
+ * first rather than stacking windows nobody closes.
2564
+ ************************************************************/
2565
+ function ac_open_gclass(gobj, event, kw, src)
2566
+ {
2567
+ let priv = gobj.priv;
2568
+ let node_data = priv.popover_node;
2569
+
2570
+ if(!node_data || !node_data.gclass) {
2571
+ log_error(`${gobj_short_name(gobj)}: EV_OPEN_GCLASS with no node`);
2572
+ return -1;
2573
+ }
2574
+
2575
+ close_gclass_view(priv.gclass_view);
2576
+ priv.gclass_view = null;
2577
+
2578
+ priv.gclass_view = open_gclass_view(gobj, node_data.gclass, {
2579
+ title_prefix: gobj_yuno_name() || "",
2580
+ on_close: () => {
2581
+ if(gobj_is_destroying(gobj)) {
2582
+ return;
2583
+ }
2584
+ gobj_send_event(gobj, "EV_GCLASS_CLOSED", {}, gobj);
2585
+ },
2586
+ });
2587
+
2588
+ return 0;
2589
+ }
2590
+
2591
+ /************************************************************
2592
+ * The reader dismissed the gclass window (its own ✕).
2593
+ *
2594
+ * C_YUI_WINDOW.close_window() calls on_close and THEN destroys
2595
+ * itself, so here we only drop our reference -- destroying it
2596
+ * again would destroy it twice.
2597
+ ************************************************************/
2598
+ function ac_gclass_closed(gobj, event, kw, src)
2599
+ {
2600
+ gobj.priv.gclass_view = null;
2601
+ return 0;
2602
+ }
2603
+
2604
+ /************************************************************
2605
+ * The gclass viewer asked for a subtree.
2606
+ *
2607
+ * It cannot: the gclass description is a COMPLETE document,
2608
+ * it carries no `__collapsed__` stub, so nothing in it is
2609
+ * fetchable. The event is declared because the viewer is
2610
+ * hosted as a child and subscribes its host to everything it
2611
+ * publishes -- and it is logged, because arriving here means
2612
+ * the document was not the one we built.
2613
+ ************************************************************/
2614
+ function ac_gclass_expand_path(gobj, event, kw, src)
2615
+ {
2616
+ log_error(
2617
+ `${gobj_short_name(gobj)}: EV_EXPAND_PATH on a complete document: ` +
2618
+ `${kw && kw.path}`
2619
+ );
2620
+ return -1;
2621
+ }
2622
+
2623
+ /************************************************************
2624
+ * The app switched language.
2625
+ *
2626
+ * A rebuild, not a re-translation: the words live inside the
2627
+ * innerHTML of G6 nodes, where refresh_language() cannot go.
2628
+ ************************************************************/
2629
+ function ac_language_changed(gobj, event, kw, src)
2630
+ {
2631
+ let $container = gobj_read_attr(gobj, "$container");
2632
+ if($container) {
2633
+ refresh_language($container, t);
2634
+ }
2635
+ hide_popover(gobj);
2636
+ refresh_tree(gobj);
2637
+ return 0;
2638
+ }
2639
+
2458
2640
 
2459
2641
 
2460
2642
 
@@ -2506,6 +2688,10 @@ function create_gclass(gclass_name)
2506
2688
  ["EV_RESIZE", ac_resize, null],
2507
2689
  ["EV_SHOW", ac_show, null],
2508
2690
  ["EV_HIDE", ac_hide, null],
2691
+ ["EV_OPEN_GCLASS", ac_open_gclass, null],
2692
+ ["EV_GCLASS_CLOSED", ac_gclass_closed, null],
2693
+ ["EV_EXPAND_PATH", ac_gclass_expand_path, null],
2694
+ ["EV_LANGUAGE_CHANGED", ac_language_changed, null],
2509
2695
  ]]
2510
2696
  ];
2511
2697
 
@@ -2529,6 +2715,10 @@ function create_gclass(gclass_name)
2529
2715
  ["EV_RESIZE", 0],
2530
2716
  ["EV_SHOW", 0],
2531
2717
  ["EV_HIDE", 0],
2718
+ ["EV_OPEN_GCLASS", 0],
2719
+ ["EV_GCLASS_CLOSED", 0],
2720
+ ["EV_EXPAND_PATH", 0],
2721
+ ["EV_LANGUAGE_CHANGED", 0],
2532
2722
  ];
2533
2723
 
2534
2724
  __gclass__ = gclass_create(