@yuneta/gobj-ui 2.6.0 → 4.0.0

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.
Files changed (56) hide show
  1. package/README.md +331 -11
  2. package/dist/gobj-ui.cjs.js +21386 -17919
  3. package/dist/gobj-ui.es.js +26059 -22613
  4. package/index.js +45 -9
  5. package/package.json +4 -4
  6. package/src/c_g6_nodes_tree.js +128 -40
  7. package/src/c_yui_form.css +9 -0
  8. package/src/c_yui_form.js +113 -53
  9. package/src/c_yui_gobj_tree_js.js +58 -36
  10. package/src/c_yui_json.css +139 -0
  11. package/src/c_yui_json.js +928 -0
  12. package/src/c_yui_json_graph.js +110 -20
  13. package/src/c_yui_map.js +18 -36
  14. package/src/c_yui_nav.js +6 -9
  15. package/src/c_yui_period.css +184 -0
  16. package/src/c_yui_period.js +1441 -0
  17. package/src/c_yui_shell.css +120 -0
  18. package/src/c_yui_shell.js +672 -111
  19. package/src/c_yui_treedb_graph.js +639 -37
  20. package/src/c_yui_treedb_schema.js +478 -0
  21. package/src/c_yui_treedb_topic_with_form.css +18 -0
  22. package/src/c_yui_treedb_topic_with_form.js +153 -103
  23. package/src/c_yui_treedb_topics.css +73 -0
  24. package/src/c_yui_treedb_topics.js +1070 -29
  25. package/src/c_yui_window.css +22 -0
  26. package/src/c_yui_window.js +182 -97
  27. package/src/c_yui_window_manager.js +38 -4
  28. package/src/json_view_helpers.js +214 -0
  29. package/src/json_view_helpers.test.js +135 -0
  30. package/src/route_map_model.js +317 -0
  31. package/src/route_map_model.test.js +209 -0
  32. package/src/route_resolver.js +24 -1
  33. package/src/route_resolver.test.js +40 -1
  34. package/src/shell_modals.js +162 -52
  35. package/src/shell_route_map.css +225 -0
  36. package/src/shell_route_map.js +363 -0
  37. package/src/tabulator.css +245 -0
  38. package/src/yui_dev.js +130 -8
  39. package/src/yui_frontend_view.js +106 -0
  40. package/src/yui_icons.css +62 -0
  41. package/src/yui_inputs.css +8 -3
  42. package/src/yui_inputs.js +51 -8
  43. package/src/yui_tabulator_i18n.js +128 -0
  44. package/src/yui_theme.js +147 -0
  45. package/src/yui_time.js +666 -0
  46. package/src/yui_time.test.js +330 -0
  47. package/src/yui_toolbar.css +28 -0
  48. package/src/yui_toolbar.js +86 -43
  49. package/src/c_yui_main.css +0 -501
  50. package/src/c_yui_main.js +0 -1623
  51. package/src/c_yui_routing.css +0 -18
  52. package/src/c_yui_routing.js +0 -837
  53. package/src/c_yui_tabs.js +0 -487
  54. package/src/themes.js +0 -215
  55. package/src/ytable.css +0 -63
  56. package/src/ytable.js +0 -505
package/src/c_yui_form.js CHANGED
@@ -56,6 +56,12 @@ import TomSelect from "tom-select"; // Import Tom-Select JS
56
56
  import { createJSONEditor } from 'vanilla-jsoneditor';
57
57
  import "vanilla-jsoneditor/themes/jse-theme-dark.css";
58
58
  import "./c_yui_form.css";
59
+ import {attach_clear, refresh_clear} from "./yui_inputs.js";
60
+ /* Read at field-build time only — deliberately NOT watched: the hosting
61
+ * dialog rebuilds the form on every open, and re-rendering a form under
62
+ * the user mid-edit would throw away what they typed. */
63
+ import {yui_is_dark} from "./yui_theme.js";
64
+ import "./tabulator.css";
59
65
 
60
66
  import "tabulator-tables/dist/css/tabulator.min.css"; // Import Tabulator CSS
61
67
  import "tabulator-tables/dist/css/tabulator_bulma.css";
@@ -82,6 +88,7 @@ SDATA(data_type_t.DTP_JSON, "record", 0, "{}", "Data to form"),
82
88
  SDATA(data_type_t.DTP_JSON, "placeholders", 0, "{}", "Placeholder values for form fields"),
83
89
  SDATA(data_type_t.DTP_JSON, "fkey_options", 0, "{}", "Options for fkey fields: {topic_name: [ids or {id} records]}, read at build time"),
84
90
  SDATA(data_type_t.DTP_STRING, "form_mode", 0, "", "'create' (pkey editable+required) or 'update' (pkey readonly); empty = no pkey handling"),
91
+ SDATA(data_type_t.DTP_STRING, "render_mode", 0, "exec", "'exec' interprets structured cols (template->sub-form, table->grid, coordinates->picker); 'edit' shows them as raw JSON editors (record-editing, e.g. the treedb topic form)"),
85
92
  SDATA(data_type_t.DTP_STRING, "pkey", 0, "id", "Primary key field name, used by form_mode"),
86
93
  SDATA(data_type_t.DTP_STRING, "topic_name", 0, "", "Treedb topic name: labels use the i18n cascade '<topic>.<col>' -> '<col>' -> header (like table headers)"),
87
94
  SDATA(data_type_t.DTP_BOOLEAN, "editable", 0, false, "Default is editable, set false to readonly"),
@@ -528,6 +535,12 @@ function build_form_field_conf(gobj, field_desc)
528
535
  default_value: field_desc.default_value,
529
536
  };
530
537
 
538
+ /* 'edit' mode edits the raw record: the structured cols
539
+ * (template / table / coordinates) are shown as raw JSON editors,
540
+ * like the pre-merge treedb topic form. 'exec' (default) interprets
541
+ * them into sub-widgets (sub-form / grid / coordinates picker). */
542
+ let raw_structured = gobj_read_str_attr(gobj, "render_mode") === "edit";
543
+
531
544
  switch(field_desc.type) {
532
545
  case "object":
533
546
  case "dict":
@@ -537,13 +550,21 @@ function build_form_field_conf(gobj, field_desc)
537
550
  break;
538
551
 
539
552
  case "template":
540
- field_conf.tag = "fieldset";
541
- field_conf.options = field_desc.enum_list;
553
+ if(raw_structured) {
554
+ field_conf.tag = "jsoneditor";
555
+ } else {
556
+ field_conf.tag = "fieldset";
557
+ field_conf.options = field_desc.enum_list;
558
+ }
542
559
  break;
543
560
 
544
561
  case "table":
545
- field_conf.tag = "table";
546
- field_conf.options = field_desc.enum_list;
562
+ if(raw_structured) {
563
+ field_conf.tag = "jsoneditor";
564
+ } else {
565
+ field_conf.tag = "table";
566
+ field_conf.options = field_desc.enum_list;
567
+ }
547
568
  break;
548
569
 
549
570
  case "blob":
@@ -683,7 +704,7 @@ function build_form_field_conf(gobj, field_desc)
683
704
  break;
684
705
 
685
706
  case "coordinates":
686
- field_conf.tag = 'coordinates';
707
+ field_conf.tag = raw_structured ? "jsoneditor" : "coordinates";
687
708
  break;
688
709
 
689
710
  default:
@@ -834,23 +855,10 @@ function apply_form_mode(gobj, $form)
834
855
  $input.setAttribute("readonly", "readonly");
835
856
  $input.removeAttribute("required");
836
857
  }
837
- }
838
-
839
- /******************************************************************
840
- * True when the app renders dark: explicit <html data-theme>, or
841
- * the OS scheme when the attribute is absent ("system" theme
842
- * removes it — see themes.js). Read at field-build time; the
843
- * hosting dialog rebuilds the form on every open.
844
- ******************************************************************/
845
- function form_is_dark_theme()
846
- {
847
- let attr = document.documentElement.getAttribute("data-theme");
848
- if(attr) {
849
- return attr === "dark";
850
- }
851
- return typeof window !== "undefined" &&
852
- window.matchMedia &&
853
- window.matchMedia("(prefers-color-scheme: dark)").matches;
858
+ /* The pkey's editability just changed but no `input` fired — resync its
859
+ * clear (✕): shown now that "create" made it editable, hidden in
860
+ * "update". */
861
+ refresh_clear($input);
854
862
  }
855
863
 
856
864
  /******************************************************************
@@ -1088,8 +1096,9 @@ function create_form_field(
1088
1096
  switch (tag) {
1089
1097
  case 'input':
1090
1098
  {
1099
+ let is_pkey = (name === gobj_read_str_attr(gobj, "pkey"));
1091
1100
  let attrs = {
1092
- class: `input ${name===gobj_read_str_attr(gobj, "pkey")?'with-focus':''}`,
1101
+ class: `input ${is_pkey?'with-focus':''}`,
1093
1102
  name: name,
1094
1103
  type: inputType,
1095
1104
  placeholder: placeholder
@@ -1128,6 +1137,16 @@ function create_form_field(
1128
1137
 
1129
1138
  $extend = createElement2(extend);
1130
1139
  $control.appendChild($extend);
1140
+ /* NORM: every editable free-text field carries the clear (✕).
1141
+ * Skip the non-text pickers (color swatch, datetime-local) where
1142
+ * an emptied value makes no sense. The pkey is built `readonly`
1143
+ * from the schema but apply_form_mode() makes it editable in
1144
+ * "create" mode — attach the clear anyway (it self-hides while
1145
+ * readonly). The synthetic `input` re-fires EV_RECORD_CHANGED. */
1146
+ if(inputType !== 'color' && inputType !== 'datetime-local' &&
1147
+ (!readonly || is_pkey)) {
1148
+ attach_clear($control, $extend);
1149
+ }
1131
1150
  break;
1132
1151
  }
1133
1152
 
@@ -1155,12 +1174,20 @@ function create_form_field(
1155
1174
  {
1156
1175
  // ["item"]
1157
1176
  // <option value="item" data-i18n:"item"> item </option>
1177
+ let opts = options;
1178
+ if(!Array.isArray(opts)) {
1179
+ log_error(sprintf(
1180
+ "%s: select field '%s' options not an array (type=%s, real_type=%s); rendering empty",
1181
+ gobj_short_name(gobj), name, field_desc.type, field_desc.real_type
1182
+ ));
1183
+ opts = [];
1184
+ }
1158
1185
  let extend = ['div', {class: "select" },
1159
1186
  ["select",
1160
1187
  {
1161
1188
  name: name
1162
1189
  },
1163
- options.map(option =>
1190
+ opts.map(option =>
1164
1191
  ['option', {value:option, i18n:option}, option]
1165
1192
  ),
1166
1193
  {
@@ -1183,13 +1210,24 @@ function create_form_field(
1183
1210
  /*
1184
1211
  * https://tom-select.js.org/docs/api/
1185
1212
  */
1213
+ /* A malformed schema (enum without an array `enum_list`) must not
1214
+ * crash the whole form — render an empty select and name the
1215
+ * offender. */
1216
+ let opts = options;
1217
+ if(!Array.isArray(opts)) {
1218
+ log_error(sprintf(
1219
+ "%s: select2 field '%s' options not an array (type=%s, real_type=%s); rendering empty",
1220
+ gobj_short_name(gobj), name, field_desc.type, field_desc.real_type
1221
+ ));
1222
+ opts = [];
1223
+ }
1186
1224
  let extend = ["select",
1187
1225
  {
1188
1226
  name: name,
1189
1227
  class: '',
1190
1228
  multiple: true,
1191
1229
  },
1192
- options.map(option =>
1230
+ opts.map(option =>
1193
1231
  ['option', {value:option, i18n:option}, option]
1194
1232
  )
1195
1233
  ];
@@ -1203,7 +1241,11 @@ function create_form_field(
1203
1241
  field_desc.real_type === "string")? 1 : null,
1204
1242
  plugins: {
1205
1243
  'clear_button': {
1206
- 'title': t('Remove all selected options'),
1244
+ /* i18n keys are lower-case by convention (the apps'
1245
+ validate-locales enforces it): this one was the only
1246
+ capitalized key in the library, so no locale could
1247
+ legally define it and it rendered as its own key. */
1248
+ 'title': t('remove all selected options'),
1207
1249
  },
1208
1250
  'no_active_items': {},
1209
1251
  'no_backspace_delete': {},
@@ -1254,7 +1296,7 @@ function create_form_field(
1254
1296
  case "jsoneditor":
1255
1297
  {
1256
1298
  let attrs = {
1257
- class: 'jsoneditor' + (form_is_dark_theme() ? ' jse-theme-dark' : ''),
1299
+ class: 'jsoneditor' + (yui_is_dark() ? ' jse-theme-dark' : ''),
1258
1300
  name: name,
1259
1301
  style: ''
1260
1302
  };
@@ -1416,19 +1458,13 @@ function create_coordinates(gobj, attrs)
1416
1458
  }
1417
1459
 
1418
1460
  let extend = ['div', {class: 'field is-flex is-align-items-center'}, [
1419
- ['div', {class: 'control has-icons-left has-icons-right'}, [
1461
+ ['div', {class: 'control has-icons-left'}, [
1420
1462
  [
1421
1463
  'input',
1422
1464
  {class: 'input', type: 'text', placeholder: t('coordinates') + '...'},
1423
1465
  '',
1424
1466
  {
1425
1467
  'input': function (evt) {
1426
- let $btn = this.closest('.control').querySelector('.clear-button');
1427
- if (this.value) {
1428
- $btn.style.display = 'inline-flex';
1429
- } else {
1430
- $btn.style.display = 'none';
1431
- }
1432
1468
  gobj_send_event(gobj, "EV_RECORD_CHANGED", {}, gobj);
1433
1469
  }
1434
1470
  }
@@ -1446,29 +1482,16 @@ function create_coordinates(gobj, attrs)
1446
1482
  getLocation(gobj, $input);
1447
1483
  }
1448
1484
  }
1449
- ],
1450
- [
1451
- 'span',
1452
- {
1453
- class: 'icon clear-button is-right is-clickable',
1454
- style: 'display:none;'
1455
- },
1456
- '<i class="yi-xmark"></i>',
1457
- {
1458
- 'click': function (evt) {
1459
- evt.stopPropagation();
1460
- let $input = this.closest('.control').querySelector('input');
1461
- $input.value = '';
1462
- this.style.display = 'none';
1463
- // gobj.config.select_by_name = '';
1464
- // gobj_send_event(gobj, "EV_SELECT", {}, gobj);
1465
- }
1466
- }
1467
1485
  ]
1468
1486
  ]]
1469
1487
  ]];
1470
1488
 
1471
- return createElement2(extend);
1489
+ /* NORM clear (): its synthetic `input` re-fires EV_RECORD_CHANGED, so
1490
+ * clearing the coordinates updates the record like any hand edit. */
1491
+ let $extend = createElement2(extend);
1492
+ let $control = $extend.querySelector('.control');
1493
+ attach_clear($control, $control.querySelector('input'));
1494
+ return $extend;
1472
1495
  }
1473
1496
 
1474
1497
  /******************************************************************
@@ -2269,6 +2292,9 @@ function set_form_values(gobj, template, $form, record)
2269
2292
  );
2270
2293
  break;
2271
2294
  }
2295
+ /* Values were set programmatically (no `input` event) — resync the
2296
+ * field's clear (✕) so a loaded value shows it right away. */
2297
+ refresh_clear($input);
2272
2298
  });
2273
2299
 
2274
2300
  return modified;
@@ -2325,6 +2351,18 @@ function treedb_value_2_form_value(gobj, field_desc, value)
2325
2351
  let type = field_desc.type;
2326
2352
  let real_type = field_desc.real_type;
2327
2353
 
2354
+ /* 'edit' mode shows template/table/coordinates as raw JSON editors:
2355
+ * hand the raw value to jsoneditor in its Content shape, exactly like
2356
+ * object/dict/blob/array. In 'exec' mode these fall through to their
2357
+ * interpreted-widget conversions below. */
2358
+ if(gobj_read_str_attr(gobj, "render_mode") === "edit" &&
2359
+ (type === "template" || type === "table" || type === "coordinates")) {
2360
+ if(type === "table") {
2361
+ return {json: is_array(value)? value : []};
2362
+ }
2363
+ return {json: (is_object(value) || is_array(value))? value : {}};
2364
+ }
2365
+
2328
2366
  switch(type) {
2329
2367
  case "fkey": {
2330
2368
  /* Decode ref(s) ("topic^id^hook", bare "$id", dict keys or
@@ -2451,6 +2489,28 @@ function form_value_2_treedb_value(gobj, field_desc, value)
2451
2489
  let type = field_desc.type;
2452
2490
  let real_type = field_desc.real_type;
2453
2491
 
2492
+ /* 'edit' mode: template/table/coordinates come back as the raw JSON
2493
+ * string typed into the jsoneditor — parse it, don't run the
2494
+ * interpreted-widget conversions ('exec' mode) below. */
2495
+ if(gobj_read_str_attr(gobj, "render_mode") === "edit" &&
2496
+ (type === "template" || type === "table" || type === "coordinates")) {
2497
+ if(is_string(value)) {
2498
+ try {
2499
+ value = JSON.parse(value);
2500
+ } catch (e) {
2501
+ value = (type === "table")? [] : {};
2502
+ }
2503
+ }
2504
+ if(type === "table") {
2505
+ if(!is_array(value)) {
2506
+ value = [];
2507
+ }
2508
+ } else if(!is_object(value) && !is_array(value)) {
2509
+ value = {};
2510
+ }
2511
+ return value;
2512
+ }
2513
+
2454
2514
  switch(type) {
2455
2515
  case "fkey":
2456
2516
  value = build_fkey_ref(gobj, field_desc, value);
@@ -21,7 +21,6 @@ import {
21
21
  gobj_read_attr,
22
22
  gobj_write_attr,
23
23
  gobj_send_event,
24
- gobj_find_service,
25
24
  gobj_write_str_attr,
26
25
  gobj_read_str_attr,
27
26
  gobj_publish_event,
@@ -54,6 +53,7 @@ import {
54
53
  } from '@antv/g6';
55
54
 
56
55
  import { ensure_drag_canvas_patch } from "./g6_drag_canvas_touch.js";
56
+ import { yui_is_dark, yui_theme_now, yui_watch_theme } from "./yui_theme.js";
57
57
 
58
58
  /***************************************************************
59
59
  * Constants
@@ -84,6 +84,7 @@ let PRIVATE_DATA = {
84
84
  graph: null,
85
85
  node_counter: 0,
86
86
  theme: null,
87
+ theme_observer: null, // MutationObserver on <html data-theme>
87
88
  $layout_select: null,
88
89
  $popover: null,
89
90
  $popover_title: null,
@@ -139,15 +140,6 @@ const GT_FONT =
139
140
  "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, " +
140
141
  "Helvetica, Arial, sans-serif";
141
142
 
142
- /***************************************************************
143
- * True when the app is in dark theme (<html data-theme>).
144
- ***************************************************************/
145
- function gt_is_dark()
146
- {
147
- return (typeof document !== "undefined") &&
148
- document.documentElement.getAttribute("data-theme") === "dark";
149
- }
150
-
151
143
  /***************************************************************
152
144
  * Soft, theme-aware card palette derived from the role colour
153
145
  * (same visual language as the treedb graph cards): light tint
@@ -294,16 +286,15 @@ function mt_create(gobj)
294
286
  }
295
287
  gobj_subscribe_event(gobj, null, {}, subscriber);
296
288
 
297
- /* Optional legacy integration with the old C_YUI_MAIN shell.
298
- * Under the new C_YUI_SHELL there is no __yui_main__ — look it
299
- * up SILENTLY (no verbose) so its absence is not logged as an
300
- * error, and fall back to the <html data-theme> convention. */
301
- let __yui_main__ = gobj_find_service("__yui_main__");
302
- if(__yui_main__) {
303
- priv.theme = gobj_read_str_attr(__yui_main__, "theme");
304
- } else {
305
- priv.theme = gt_is_dark() ? "dark" : "light";
306
- }
289
+ /* Follow the app theme. This used to read a legacy C_YUI_MAIN
290
+ * "__yui_main__" service's `theme` attr when one existed, falling
291
+ * back to <html data-theme> otherwise but nothing ever WROTE that
292
+ * attr, so the service branch answered "light" for the life of the
293
+ * app. And neither branch WATCHED: the theme was read once, so
294
+ * toggling to dark with the view open left a white canvas on a dark
295
+ * app. Watch it, and restyle in ac_theme. */
296
+ priv.theme = yui_theme_now();
297
+ priv.theme_observer = yui_watch_theme(gobj);
307
298
 
308
299
  build_ui(gobj);
309
300
 
@@ -341,6 +332,10 @@ function mt_destroy(gobj)
341
332
  {
342
333
  let priv = gobj.priv;
343
334
 
335
+ if(priv.theme_observer) {
336
+ priv.theme_observer.disconnect();
337
+ priv.theme_observer = null;
338
+ }
344
339
  if(priv.resize_observer) {
345
340
  priv.resize_observer.disconnect();
346
341
  priv.resize_observer = null;
@@ -481,7 +476,7 @@ function build_popover(gobj, $container)
481
476
  let $close = document.createElement("button");
482
477
  $close.type = "button";
483
478
  $close.textContent = "×";
484
- $close.title = t("Close");
479
+ $close.title = t("close");
485
480
  $close.style.cssText = `
486
481
  background: transparent;
487
482
  border: none;
@@ -591,7 +586,7 @@ function make_toolbar(gobj)
591
586
  ['select', {
592
587
  class: 'select',
593
588
  style: {height: toolbar_wide, "margin-left": "0.5em"},
594
- title: t("Layout"),
589
+ title: t("layout"),
595
590
  },
596
591
  options,
597
592
  {
@@ -638,7 +633,7 @@ function build_graph(gobj)
638
633
  edge: {
639
634
  type: layout_cfg.edge_type,
640
635
  style: {
641
- stroke: gt_is_dark() ? '#8b94a3' : '#6b7280',
636
+ stroke: yui_is_dark() ? '#8b94a3' : '#6b7280',
642
637
  lineWidth: 1,
643
638
  endArrow: true,
644
639
  },
@@ -711,7 +706,7 @@ function apply_layout(gobj)
711
706
  edge: {
712
707
  type: layout_cfg.edge_type,
713
708
  style: {
714
- stroke: gt_is_dark() ? '#8b94a3' : '#6b7280',
709
+ stroke: yui_is_dark() ? '#8b94a3' : '#6b7280',
715
710
  lineWidth: 1,
716
711
  endArrow: true,
717
712
  },
@@ -838,7 +833,7 @@ function build_gobj_nodes(gobj, target_gobj, nodes, edges, parent_id, is_root, c
838
833
  let state = gobj_current_state(target_gobj) || "";
839
834
  let full_name = gobj_full_name(target_gobj);
840
835
  let colors = get_role_colors(target_gobj, is_root);
841
- let dark = gt_is_dark();
836
+ let dark = yui_is_dark();
842
837
  let cs = role_card_style(colors.stroke, dark);
843
838
  let state_color = get_state_color(target_gobj);
844
839
 
@@ -1208,11 +1203,11 @@ function get_node_info_rows(node_data)
1208
1203
  {
1209
1204
  let rows = [];
1210
1205
 
1211
- rows.push([t("GClass"), node_data.gclass || "—"]);
1212
- rows.push([t("Name"), node_data.name || "—"]);
1206
+ rows.push([t("gclass"), node_data.gclass || "—"]);
1207
+ rows.push([t("name"), node_data.name || "—"]);
1213
1208
 
1214
1209
  if(node_data.full_name && node_data.full_name !== node_data.short_name) {
1215
- rows.push([t("Full name"), node_data.full_name]);
1210
+ rows.push([t("full name"), node_data.full_name]);
1216
1211
  }
1217
1212
 
1218
1213
  let role;
@@ -1227,7 +1222,7 @@ function get_node_info_rows(node_data)
1227
1222
  } else {
1228
1223
  role = "child";
1229
1224
  }
1230
- rows.push([t("Role"), role]);
1225
+ rows.push([t("role"), role]);
1231
1226
 
1232
1227
  let status;
1233
1228
  if(!node_data.is_running) {
@@ -1237,19 +1232,19 @@ function get_node_info_rows(node_data)
1237
1232
  } else {
1238
1233
  status = "running";
1239
1234
  }
1240
- rows.push([t("Status"), status]);
1241
- rows.push([t("State"), node_data.state || "—"]);
1235
+ rows.push([t("status"), status]);
1236
+ rows.push([t("state"), node_data.state || "—"]);
1242
1237
 
1243
1238
  if(node_data.parent_short_name) {
1244
- rows.push([t("Parent"), node_data.parent_short_name]);
1239
+ rows.push([t("parent"), node_data.parent_short_name]);
1245
1240
  }
1246
1241
 
1247
1242
  if(typeof node_data.num_children === "number") {
1248
1243
  let kids = node_data.num_children;
1249
1244
  if(kids > 0 && node_data.is_collapsed) {
1250
- rows.push([t("Children"), kids + " " + t("(collapsed)")]);
1245
+ rows.push([t("children"), kids + " " + t("(collapsed)")]);
1251
1246
  } else if(kids > 0) {
1252
- rows.push([t("Children"), String(kids)]);
1247
+ rows.push([t("children"), String(kids)]);
1253
1248
  }
1254
1249
  }
1255
1250
 
@@ -1269,10 +1264,14 @@ function render_popover_body(gobj, node_data)
1269
1264
 
1270
1265
  let rows = get_node_info_rows(node_data);
1271
1266
 
1267
+ /* Bulma scheme vars, like the popover chrome around it (build_popover):
1268
+ * they flip with <html data-theme>, so the popover follows the theme
1269
+ * with no redraw. Hardcoding them is what made the value near-black
1270
+ * (#1A1A1A) on the near-black dark card background — invisible. */
1272
1271
  let rows_html = rows.map(([label, value]) => `
1273
1272
  <div style="display:grid; grid-template-columns: 90px 1fr; column-gap:10px; padding: 2px 0;">
1274
- <span style="color:#6B7280; font-weight:500;">${escapeHtml(String(label))}</span>
1275
- <span style="color:#1A1A1A; word-break:break-all;">${escapeHtml(String(value))}</span>
1273
+ <span style="color:var(--bulma-text-weak, #6B7280); font-weight:500;">${escapeHtml(String(label))}</span>
1274
+ <span style="color:var(--bulma-text-strong, #1A1A1A); word-break:break-all;">${escapeHtml(String(value))}</span>
1276
1275
  </div>
1277
1276
  `).join("");
1278
1277
 
@@ -1712,6 +1711,27 @@ function ac_refresh(gobj, event, kw, src)
1712
1711
  return 0;
1713
1712
  }
1714
1713
 
1714
+ /************************************************************
1715
+ * {theme: "dark"|"light"} — the app switched theme.
1716
+ *
1717
+ * G6 gets the new theme, and the tree is rebuilt: the node/edge
1718
+ * colours are picked from yui_is_dark() as they are drawn, so only a
1719
+ * redraw actually repaints them.
1720
+ ************************************************************/
1721
+ function ac_theme(gobj, event, kw, src)
1722
+ {
1723
+ let priv = gobj.priv;
1724
+
1725
+ priv.theme = kw.theme || yui_theme_now();
1726
+ if(priv.graph) {
1727
+ priv.graph.setTheme(priv.theme);
1728
+ }
1729
+ /* A restyle, not new data: keep the user's zoom/pan. */
1730
+ refresh_tree(gobj, {preserve_view: true});
1731
+
1732
+ return 0;
1733
+ }
1734
+
1715
1735
  /************************************************************
1716
1736
  * {layout: "<key>"}
1717
1737
  ************************************************************/
@@ -2040,6 +2060,7 @@ function create_gclass(gclass_name)
2040
2060
  const states = [
2041
2061
  ["ST_IDLE", [
2042
2062
  ["EV_REFRESH", ac_refresh, null],
2063
+ ["EV_THEME", ac_theme, null],
2043
2064
  ["EV_CHANGE_LAYOUT", ac_change_layout, null],
2044
2065
  ["EV_TOGGLE_COLLAPSE", ac_toggle_collapse, null],
2045
2066
  ["EV_EXPAND_ALL", ac_expand_all, null],
@@ -2060,6 +2081,7 @@ function create_gclass(gclass_name)
2060
2081
  *---------------------------------------------*/
2061
2082
  const event_types = [
2062
2083
  ["EV_REFRESH", 0],
2084
+ ["EV_THEME", 0],
2063
2085
  ["EV_CHANGE_LAYOUT", 0],
2064
2086
  ["EV_TOGGLE_COLLAPSE", 0],
2065
2087
  ["EV_EXPAND_ALL", 0],
@@ -0,0 +1,139 @@
1
+ /***********************************************************************
2
+ * c_yui_json.css
3
+ *
4
+ * Styling for C_YUI_JSON (lazy JSON tree viewer).
5
+ * Monospace tree, type-coloured leaves, dark-aware.
6
+ * No transitions/animations by design (lib-yui convention).
7
+ *
8
+ * Copyright (c) 2026, ArtGins.
9
+ * All Rights Reserved.
10
+ ***********************************************************************/
11
+
12
+ .C_YUI_JSON {
13
+ --json-string: #006000;
14
+ --json-number: #b5330b;
15
+ --json-boolean: #b36b00;
16
+ --json-null: #475ed0;
17
+ --json-key: var(--bulma-text-strong, #1a1a1a);
18
+ --json-row-hover: rgba(0, 0, 0, 0.05);
19
+ }
20
+
21
+ :root[data-theme="dark"] .C_YUI_JSON,
22
+ .C_YUI_JSON[data-theme="dark"] {
23
+ --json-string: #7ec87e;
24
+ --json-number: #ff8a65;
25
+ --json-boolean: #ffb74d;
26
+ --json-null: #90a4f4;
27
+ --json-key: var(--bulma-text-strong, #e6e6e6);
28
+ --json-row-hover: rgba(255, 255, 255, 0.06);
29
+ }
30
+
31
+ @media (prefers-color-scheme: dark) {
32
+ :root:not([data-theme="light"]) .C_YUI_JSON {
33
+ --json-string: #7ec87e;
34
+ --json-number: #ff8a65;
35
+ --json-boolean: #ffb74d;
36
+ --json-null: #90a4f4;
37
+ --json-key: var(--bulma-text-strong, #e6e6e6);
38
+ --json-row-hover: rgba(255, 255, 255, 0.06);
39
+ }
40
+ }
41
+
42
+ .C_YUI_JSON .JSON_TOOLBAR {
43
+ border-bottom: 1px solid var(--bulma-border-weak, #e0e0e0);
44
+ }
45
+
46
+ .C_YUI_JSON .JSON_TREE {
47
+ font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
48
+ font-size: 1em;
49
+ line-height: 1.5;
50
+ padding: 0.25rem 0;
51
+ }
52
+
53
+ .C_YUI_JSON .JSON_ROW {
54
+ display: flex;
55
+ align-items: flex-start;
56
+ white-space: pre;
57
+ padding-right: 0.5rem;
58
+ }
59
+
60
+ .C_YUI_JSON .JSON_ROW:hover {
61
+ background: var(--json-row-hover);
62
+ }
63
+
64
+ .C_YUI_JSON .JSON_ROW_BODY {
65
+ white-space: pre-wrap;
66
+ word-break: break-word;
67
+ min-width: 0;
68
+ }
69
+
70
+ /*
71
+ * Toggle affordance: a fixed-width gutter so keys align across depths.
72
+ * The chevron is rotated 90deg when open (static transform, no
73
+ * transition — animation is disabled by convention).
74
+ */
75
+ .C_YUI_JSON .JSON_TOGGLE {
76
+ flex: 0 0 auto;
77
+ width: 1.2em;
78
+ text-align: center;
79
+ color: var(--bulma-text-weak, #7a7a7a);
80
+ cursor: default;
81
+ user-select: none;
82
+ }
83
+
84
+ .C_YUI_JSON .JSON_CONTAINER,
85
+ .C_YUI_JSON .JSON_COLLAPSED {
86
+ cursor: pointer;
87
+ }
88
+
89
+ .C_YUI_JSON .JSON_CONTAINER .JSON_TOGGLE i {
90
+ display: inline-block;
91
+ font-size: 0.85em;
92
+ }
93
+
94
+ .C_YUI_JSON .JSON_TOGGLE.is-open i {
95
+ transform: rotate(90deg);
96
+ }
97
+
98
+ /* "Expand all" reuses the chevron (the only one available) rotated to point
99
+ * DOWN — the open state — while "collapse all" keeps it pointing right (the
100
+ * closed state), mirroring the per-node toggle. */
101
+ .C_YUI_JSON .EV_EXPAND_ALL i {
102
+ transform: rotate(90deg);
103
+ }
104
+
105
+ .C_YUI_JSON .JSON_TOGGLE_REMOTE i {
106
+ font-size: 0.8em;
107
+ color: var(--bulma-link, #3273dc);
108
+ }
109
+
110
+ .C_YUI_JSON .JSON_KEY {
111
+ color: var(--json-key);
112
+ font-weight: 600;
113
+ }
114
+
115
+ .C_YUI_JSON .JSON_PUNCT {
116
+ color: var(--bulma-text-weak, #7a7a7a);
117
+ }
118
+
119
+ .C_YUI_JSON .JSON_TYPE_STRING { color: var(--json-string); }
120
+ .C_YUI_JSON .JSON_TYPE_NUMBER { color: var(--json-number); }
121
+ .C_YUI_JSON .JSON_TYPE_BOOLEAN { color: var(--json-boolean); }
122
+ .C_YUI_JSON .JSON_TYPE_NULL { color: var(--json-null); font-style: italic; }
123
+
124
+ .C_YUI_JSON .JSON_STUB {
125
+ color: var(--bulma-link, #3273dc);
126
+ font-weight: 600;
127
+ }
128
+
129
+ .C_YUI_JSON .JSON_COLLAPSED.is-pending {
130
+ cursor: progress;
131
+ opacity: 0.7;
132
+ }
133
+
134
+ /*
135
+ * Disable any transition a host stylesheet might inject.
136
+ */
137
+ .C_YUI_JSON * {
138
+ transition: none !important;
139
+ }