@yuneta/gobj-ui 7.23.38 → 7.23.39

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.
package/README.md CHANGED
@@ -1381,14 +1381,32 @@ because opening a popover per node to learn it is not reading a tree:
1381
1381
  | On the card | Says |
1382
1382
  |---|---|
1383
1383
  | Colour (border + left bar) | the ROLE: sky = the yuno, emerald = a service, amber = a pure child, pink = a volatil one, violet = a plain child |
1384
- | Status pill | `stopped` / `running` / `playing`, coloured. `running` and `playing` are not the same thing: a gobj that runs and does not play is PAUSED, which is what the popover calls it in full |
1384
+ | Status pill | a SYMBOL and, where there is room, the word: `▶` playing, `‖` running but not playing (PAUSED, which is what the popover calls it in full), `■` stopped, `⊘` disabled. The symbol and not only a colour, because a dot has one shape and stopped is the state that has to be seen |
1385
1385
  | Badges | `service`, `pure child`, `volatil child`, `disabled`, `bottom` (it has a bottom gobj), `commands` (its gclass exposes commands) |
1386
1386
  | Its own line | the FSM state |
1387
1387
  | Dashed border, dimmed | disabled — that branch is out of the game |
1388
1388
 
1389
1389
  The popover adds the rest (`full name`, `parent`, `bottom gobj`, `traces`,
1390
1390
  `flags`, how many children a fold is hiding) and shows only the rows that have
1391
- an answer.
1391
+ an answer. Its two state rows are `status` and **`fsm state`** — different
1392
+ keys on purpose: they used to be `status` and `state`, which every Spanish app
1393
+ translated to the same word, so the popover showed *Estado: Parado* directly
1394
+ above *Estado: ST_IDLE*.
1395
+
1396
+ **The find box** matches gclass, name, full name and FSM state; a match wears
1397
+ an amber ring over its role colour (the card still has to say what it IS) and
1398
+ the chip counts them, because a graph that did not move looks the same whether
1399
+ nothing matched or the match was already on screen. A find is a REBUILD — an
1400
+ html node paints no G6 state, so the highlight lives in the card's own markup —
1401
+ which is why the box is rate-limited, and why the view is preserved: somebody
1402
+ who typed a letter did not ask to be moved.
1403
+
1404
+ **A service the app creates is the app's to START.** `c_yuno`'s `mt_play` starts
1405
+ only the DEFAULT service, so anything else stays stopped unless its creator
1406
+ starts it — and a gclass with no `mt_start` (`C_YUI_WINDOW_MANAGER` builds its
1407
+ dock in `mt_create` and runs off events) WORKS stopped, which is how one shipped
1408
+ that way in two apps: nothing failed, it just read `!!C_YUI_WINDOW_MANAGER` in
1409
+ every trace line and *stopped* in this view.
1392
1410
 
1393
1411
  **It draws DESCRIPTORS, not gobjs** (`gobj_tree_model.js`), which is what lets
1394
1412
  this same view show a **backend** yuno. A descriptor is plain data whose field
@@ -11442,7 +11442,7 @@ function collect_foldable_paths(value, path, out, is_root) {
11442
11442
  * move looks the same whether nothing matched or the match
11443
11443
  * was already on screen.
11444
11444
  ************************************************************/
11445
- function update_find_result(gobj) {
11445
+ function update_find_result$1(gobj) {
11446
11446
  let priv = gobj.priv;
11447
11447
  if (!priv.$find_result || !priv.$find_count) return;
11448
11448
  if ((0, _yuneta_gobj_js.empty_string)(priv.search)) {
@@ -11707,7 +11707,7 @@ function ac_change_layout$2(gobj, event, kw, src) {
11707
11707
  edge: { type: cfg.edge_type }
11708
11708
  });
11709
11709
  refresh_json(gobj);
11710
- update_find_result(gobj);
11710
+ update_find_result$1(gobj);
11711
11711
  return 0;
11712
11712
  }
11713
11713
  /************************************************************
@@ -11736,7 +11736,7 @@ function ac_toggle_fold(gobj, event, kw, src) {
11736
11736
  }
11737
11737
  priv.pending_keep = keep_of(gobj, kw && kw.keep || path);
11738
11738
  refresh_json(gobj, { preserve_view: true });
11739
- update_find_result(gobj);
11739
+ update_find_result$1(gobj);
11740
11740
  return 0;
11741
11741
  }
11742
11742
  /************************************************************
@@ -11747,11 +11747,11 @@ function ac_toggle_fold(gobj, event, kw, src) {
11747
11747
  * keystroke is unusable. The count says whether there is
11748
11748
  * anything to look for elsewhere.
11749
11749
  ************************************************************/
11750
- function ac_find_nodes$2(gobj, event, kw, src) {
11750
+ function ac_find_nodes$3(gobj, event, kw, src) {
11751
11751
  let priv = gobj.priv;
11752
11752
  priv.search = (kw && kw.text || "").trim().toLowerCase();
11753
11753
  refresh_json(gobj, { preserve_view: true });
11754
- update_find_result(gobj);
11754
+ update_find_result$1(gobj);
11755
11755
  return 0;
11756
11756
  }
11757
11757
  /************************************************************
@@ -11763,7 +11763,7 @@ function ac_expand_all$2(gobj, event, kw, src) {
11763
11763
  priv.collapsed.clear();
11764
11764
  priv.reserved.clear();
11765
11765
  refresh_json(gobj);
11766
- update_find_result(gobj);
11766
+ update_find_result$1(gobj);
11767
11767
  return 0;
11768
11768
  }
11769
11769
  /************************************************************
@@ -11780,7 +11780,7 @@ function ac_collapse_all$2(gobj, event, kw, src) {
11780
11780
  priv.collapsed = new Set(paths);
11781
11781
  priv.reserved.clear();
11782
11782
  refresh_json(gobj);
11783
- update_find_result(gobj);
11783
+ update_find_result$1(gobj);
11784
11784
  return 0;
11785
11785
  }
11786
11786
  /************************************************************
@@ -12054,7 +12054,7 @@ function create_gclass$13(gclass_name) {
12054
12054
  ],
12055
12055
  [
12056
12056
  "EV_FIND_NODES",
12057
- ac_find_nodes$2,
12057
+ ac_find_nodes$3,
12058
12058
  null
12059
12059
  ],
12060
12060
  [
@@ -16132,6 +16132,21 @@ function node_status(d) {
16132
16132
  if (!d || !d.running) return "stopped";
16133
16133
  return d.playing ? "playing" : "running";
16134
16134
  }
16135
+ var STATUS_SYMBOLS = {
16136
+ "playing": "▶︎",
16137
+ "running": "‖",
16138
+ "stopped": "■︎"
16139
+ };
16140
+ var DISABLED_SYMBOL = "⊘";
16141
+ /************************************************************
16142
+ * The symbol of a node's status, which is what the reader
16143
+ * actually sees first. `disabled` overrides the rest: a
16144
+ * disabled gobj is not going to run whatever it is doing now.
16145
+ ************************************************************/
16146
+ function node_status_symbol(d) {
16147
+ if (d && d.disabled) return DISABLED_SYMBOL;
16148
+ return STATUS_SYMBOLS[node_status(d)] || STATUS_SYMBOLS.stopped;
16149
+ }
16135
16150
  /************************************************************
16136
16151
  * The i18n key of the status, which is NOT the status itself:
16137
16152
  * a gobj that runs and does not play is PAUSED to the reader,
@@ -16260,7 +16275,7 @@ function node_labels(t) {
16260
16275
  full_name: t("full name"),
16261
16276
  role: t("role"),
16262
16277
  status: t("status"),
16263
- state: t("state"),
16278
+ fsm_state: t("fsm state"),
16264
16279
  disabled: t("disabled"),
16265
16280
  parent: t("parent"),
16266
16281
  bottom_gobj: t("bottom gobj"),
@@ -16289,8 +16304,8 @@ function node_info_rows(d, t) {
16289
16304
  rows.push([f.name, d.name || "—"]);
16290
16305
  if (d.fullname && d.fullname !== d.shortname) rows.push([f.full_name, d.fullname]);
16291
16306
  rows.push([f.role, L.role[role_i18n_key(node_role(d))]]);
16292
- rows.push([f.status, L.status[node_status_key(d)]]);
16293
- rows.push([f.state, d.state || "—"]);
16307
+ rows.push([f.status, node_status_symbol(d) + " " + L.status[node_status_key(d)]]);
16308
+ rows.push([f.fsm_state, d.state || "—"]);
16294
16309
  if (d.disabled) rows.push([f.disabled, f.yes]);
16295
16310
  if (d.parent) rows.push([f.parent, d.parent]);
16296
16311
  if (d.bottom_gobj) rows.push([f.bottom_gobj, d.bottom_gobj]);
@@ -16772,6 +16787,12 @@ var PRIVATE_DATA$9 = {
16772
16787
  theme: null,
16773
16788
  theme_observer: null,
16774
16789
  $layout_select: null,
16790
+ search: "",
16791
+ match_count: 0,
16792
+ find_timer: null,
16793
+ $find_input: null,
16794
+ $find_result: null,
16795
+ $find_count: null,
16775
16796
  $popover: null,
16776
16797
  $popover_title: null,
16777
16798
  $popover_body: null,
@@ -16917,6 +16938,7 @@ var NODE_SIZES = {
16917
16938
  /***************************************************************
16918
16939
  * Shared card typography.
16919
16940
  ***************************************************************/
16941
+ var FIND_MATCH_COLOR = "#D9A23F";
16920
16942
  var GT_FONT = "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif";
16921
16943
  /***************************************************************
16922
16944
  * Theme-aware card palette derived from the role colour.
@@ -17289,11 +17311,107 @@ function destroy_ui$4(gobj) {
17289
17311
  /************************************************************
17290
17312
  * Toolbar
17291
17313
  ************************************************************/
17314
+ /************************************************************
17315
+ * The find box and its count.
17316
+ *
17317
+ * A tree of a real yuno is a hundred cards, and the only way
17318
+ * to locate one was to read them all. The COUNT is what makes
17319
+ * it honest: a graph that did not move looks the same whether
17320
+ * nothing matched or the match was already on screen -- the
17321
+ * same reason the JSON graph carries one, and the same
17322
+ * drawing, because the two sit in the same console.
17323
+ ************************************************************/
17324
+ function make_find_items(gobj) {
17325
+ let priv = gobj.priv;
17326
+ let $find_input = (0, _yuneta_gobj_js.createElement2)([
17327
+ "input",
17328
+ {
17329
+ class: "GOBJ_TREE_FIND_INPUT input",
17330
+ type: "text",
17331
+ placeholder: (0, i18next.t)("search"),
17332
+ "data-i18n-placeholder": "search",
17333
+ "aria-label": (0, i18next.t)("search"),
17334
+ "data-i18n-aria-label": "search"
17335
+ },
17336
+ [],
17337
+ { input: (evt) => {
17338
+ evt.stopPropagation();
17339
+ let text = evt.target.value.trim();
17340
+ if (priv.find_timer) clearTimeout(priv.find_timer);
17341
+ priv.find_timer = setTimeout(function() {
17342
+ priv.find_timer = null;
17343
+ (0, _yuneta_gobj_js.gobj_send_event)(gobj, "EV_FIND_NODES", { text }, gobj);
17344
+ }, 250);
17345
+ } }
17346
+ ]);
17347
+ let $find_control = (0, _yuneta_gobj_js.createElement2)([
17348
+ "div",
17349
+ {
17350
+ class: "GOBJ_TREE_FIND control has-icons-left",
17351
+ style: "margin-left:.5rem; margin-right:.5rem; max-width:12rem; min-width:7rem;"
17352
+ },
17353
+ [$find_input, [
17354
+ "span",
17355
+ { class: "icon is-left" },
17356
+ [["i", { class: "yi-magnifying-glass" }]]
17357
+ ]]
17358
+ ]);
17359
+ attach_clear($find_control, $find_input);
17360
+ priv.$find_input = $find_input;
17361
+ return [$find_control, [
17362
+ "div",
17363
+ {
17364
+ class: "GOBJ_TREE_FIND_RESULT is-hidden",
17365
+ style: "display:flex; align-items:center; gap:.3rem; margin-right:.5rem; font-size:.85rem;"
17366
+ },
17367
+ [[
17368
+ "span",
17369
+ { class: "GOBJ_TREE_FIND_COUNT" },
17370
+ ""
17371
+ ], [
17372
+ "span",
17373
+ { i18n: "matches" },
17374
+ (0, i18next.t)("matches")
17375
+ ]]
17376
+ ]];
17377
+ }
17378
+ /************************************************************
17379
+ * Does this node answer the current find term?
17380
+ *
17381
+ * What a reader types is a gclass, a name or a state -- the
17382
+ * three things a card shows. The full name is searched too,
17383
+ * because it is what the popover shows and what a log line
17384
+ * carries.
17385
+ ************************************************************/
17386
+ function node_matches(d, term) {
17387
+ if (!term) return false;
17388
+ return [
17389
+ d.gclass,
17390
+ d.name,
17391
+ d.fullname,
17392
+ d.state
17393
+ ].filter((x) => typeof x === "string").join(" ").toLowerCase().indexOf(term) >= 0;
17394
+ }
17395
+ /************************************************************
17396
+ * Repaint the count chip. Hidden while nothing is typed:
17397
+ * "0 matches" beside an empty box says nothing.
17398
+ ************************************************************/
17399
+ function update_find_result(gobj) {
17400
+ let priv = gobj.priv;
17401
+ if (!priv.$find_result || !priv.$find_count) return;
17402
+ if (!priv.search) {
17403
+ priv.$find_result.classList.add("is-hidden");
17404
+ priv.$find_count.textContent = "";
17405
+ return;
17406
+ }
17407
+ priv.$find_count.textContent = String(priv.match_count);
17408
+ priv.$find_result.classList.remove("is-hidden");
17409
+ }
17292
17410
  function make_toolbar$1(gobj) {
17293
17411
  let priv = gobj.priv;
17294
17412
  let toolbar_wide = (0, _yuneta_gobj_js.gobj_read_attr)(gobj, "wide");
17295
17413
  let current_layout = (0, _yuneta_gobj_js.gobj_read_str_attr)(gobj, "layout");
17296
- let left_items = yui_graph_fold_items(gobj, toolbar_wide);
17414
+ let left_items = yui_graph_fold_items(gobj, toolbar_wide).concat(make_find_items(gobj));
17297
17415
  let center_items = [];
17298
17416
  let right_items = [];
17299
17417
  center_items = yui_graph_camera_items(gobj, priv.graph, toolbar_wide);
@@ -17344,6 +17462,8 @@ function make_toolbar$1(gobj) {
17344
17462
  right_items
17345
17463
  ]
17346
17464
  ]);
17465
+ priv.$find_result = $toolbar.querySelector(".GOBJ_TREE_FIND_RESULT");
17466
+ priv.$find_count = $toolbar.querySelector(".GOBJ_TREE_FIND_COUNT");
17347
17467
  (0, _yuneta_gobj_js.refresh_language)($toolbar, i18next.t);
17348
17468
  return $toolbar;
17349
17469
  }
@@ -17519,26 +17639,32 @@ function render_toggle_html(node_id, collapsed, num_children, cs) {
17519
17639
  /************************************************************
17520
17640
  * HTML for the status pill: what the gobj is DOING.
17521
17641
  *
17522
- * A dot plus a word, and the word is dropped in the compact
17523
- * card where there is no room for it -- the colour still
17524
- * carries it, and the `title` says it in full either way.
17642
+ * A SYMBOL plus a word, and the word is dropped in the
17643
+ * compact card where there is no room for it.
17644
+ *
17645
+ * The symbol carries the state on its own, which a dot never
17646
+ * did: a dot has one shape, so telling running from stopped
17647
+ * meant telling green from red at 10 pixels -- and stopped is
17648
+ * the one that has to be seen. Play, pause and stop are also
17649
+ * the framework's own three verbs, not a borrowed metaphor.
17525
17650
  ************************************************************/
17526
17651
  function render_status_html(d, ss, labels, with_label) {
17527
17652
  let full = d.disabled ? labels.field.disabled : labels.status[node_status_key(d)] || node_status_key(d);
17528
17653
  let label = d.disabled ? labels.field.disabled : labels.status_short[node_status(d)] || node_status(d);
17654
+ let symbol = node_status_symbol(d);
17529
17655
  if (!with_label) return `<span title="${(0, _yuneta_gobj_js.escapeHtml)(full)}" style="
17530
17656
  flex: 0 0 auto;
17531
- width: 10px; height: 10px; border-radius: 50%;
17532
- background: ${ss.dot};
17533
- box-shadow: 0 0 0 2px ${ss.bg};
17534
17657
  margin-left: 6px;
17535
- "></span>`;
17658
+ color: ${ss.dot};
17659
+ font-size: 11px;
17660
+ line-height: 1;
17661
+ ">${(0, _yuneta_gobj_js.escapeHtml)(symbol)}</span>`;
17536
17662
  return `<span title="${(0, _yuneta_gobj_js.escapeHtml)(full)}" style="
17537
17663
  flex: 0 0 auto;
17538
17664
  display: inline-flex;
17539
17665
  align-items: center;
17540
17666
  gap: 4px;
17541
- padding: 1px 7px 1px 5px;
17667
+ padding: 1px 7px 1px 6px;
17542
17668
  background: ${ss.bg};
17543
17669
  color: ${ss.fg};
17544
17670
  border-radius: 9px;
@@ -17546,7 +17672,8 @@ function render_status_html(d, ss, labels, with_label) {
17546
17672
  font-weight: 700;
17547
17673
  line-height: 1.45;
17548
17674
  white-space: nowrap;
17549
- "><span style="width:7px; height:7px; border-radius:50%; background:${ss.dot};"></span>${(0, _yuneta_gobj_js.escapeHtml)(label)}</span>`;
17675
+ "><span style="color:${ss.dot}; font-size:11px; line-height:1;"
17676
+ >${(0, _yuneta_gobj_js.escapeHtml)(symbol)}</span>${(0, _yuneta_gobj_js.escapeHtml)(label)}</span>`;
17550
17677
  }
17551
17678
  /************************************************************
17552
17679
  * HTML for the badges: what the gobj IS, besides its colour.
@@ -17593,6 +17720,12 @@ function build_gobj_nodes(gobj, d, nodes, edges, parent_id, compact, labels) {
17593
17720
  let height = size.h;
17594
17721
  let border_width = category === "yuno" || category === "service" ? 2 : 1;
17595
17722
  let border_style = d.disabled ? "dashed" : "solid";
17723
+ if (node_matches(d, priv.search)) {
17724
+ priv.match_count++;
17725
+ border_width = 3;
17726
+ border_style = "solid";
17727
+ cs = Object.assign({}, cs, { border: FIND_MATCH_COLOR });
17728
+ }
17596
17729
  let children = d.children || [];
17597
17730
  let num_children = children.length;
17598
17731
  let has_children = num_children > 0;
@@ -17717,6 +17850,7 @@ function load_tree(gobj) {
17717
17850
  }
17718
17851
  let layout_cfg = get_layout_cfg((0, _yuneta_gobj_js.gobj_read_str_attr)(gobj, "layout"));
17719
17852
  let labels = node_labels(i18next.t);
17853
+ priv.match_count = 0;
17720
17854
  let anchor = priv.pending_anchor || null;
17721
17855
  let preserve_view = !!priv.pending_preserve_view;
17722
17856
  priv.pending_anchor = null;
@@ -18146,6 +18280,23 @@ var GobjLanesHLayout = class extends _antv_g6.BaseLayout {
18146
18280
  /************************************************************
18147
18281
  *
18148
18282
  ************************************************************/
18283
+ /************************************************************
18284
+ * EV_FIND_NODES {text}
18285
+ *
18286
+ * The cards carry the highlight in their own markup (an html
18287
+ * node paints no G6 state), so finding is a REBUILD -- which
18288
+ * is also why the box is rate-limited. The view is preserved:
18289
+ * a reader who typed a letter did not ask to be moved.
18290
+ ************************************************************/
18291
+ function ac_find_nodes$2(gobj, event, kw, src) {
18292
+ let priv = gobj.priv;
18293
+ let text = String(kw && kw.text || "").trim().toLowerCase();
18294
+ if (text === priv.search) return 0;
18295
+ priv.search = text;
18296
+ refresh_tree(gobj, { preserve_view: true });
18297
+ update_find_result(gobj);
18298
+ return 0;
18299
+ }
18149
18300
  function ac_refresh$3(gobj, event, kw, src) {
18150
18301
  save_view_state(gobj);
18151
18302
  refresh_tree(gobj);
@@ -18519,6 +18670,7 @@ function create_gclass$9(gclass_name) {
18519
18670
  return -1;
18520
18671
  }
18521
18672
  __gclass__$9 = (0, _yuneta_gobj_js.gclass_create)(gclass_name, [
18673
+ ["EV_FIND_NODES", 0],
18522
18674
  ["EV_REFRESH", 0],
18523
18675
  ["EV_THEME", 0],
18524
18676
  ["EV_CHANGE_LAYOUT", 0],
@@ -18539,6 +18691,11 @@ function create_gclass$9(gclass_name) {
18539
18691
  ["EV_GCLASS_CLOSED", 0],
18540
18692
  ["EV_LANGUAGE_CHANGED", 0]
18541
18693
  ], [["ST_IDLE", [
18694
+ [
18695
+ "EV_FIND_NODES",
18696
+ ac_find_nodes$2,
18697
+ null
18698
+ ],
18542
18699
  [
18543
18700
  "EV_REFRESH",
18544
18701
  ac_refresh$3,
@@ -11437,7 +11437,7 @@ function collect_foldable_paths(value, path, out, is_root) {
11437
11437
  * move looks the same whether nothing matched or the match
11438
11438
  * was already on screen.
11439
11439
  ************************************************************/
11440
- function update_find_result(gobj) {
11440
+ function update_find_result$1(gobj) {
11441
11441
  let priv = gobj.priv;
11442
11442
  if (!priv.$find_result || !priv.$find_count) return;
11443
11443
  if (empty_string(priv.search)) {
@@ -11702,7 +11702,7 @@ function ac_change_layout$2(gobj, event, kw, src) {
11702
11702
  edge: { type: cfg.edge_type }
11703
11703
  });
11704
11704
  refresh_json(gobj);
11705
- update_find_result(gobj);
11705
+ update_find_result$1(gobj);
11706
11706
  return 0;
11707
11707
  }
11708
11708
  /************************************************************
@@ -11731,7 +11731,7 @@ function ac_toggle_fold(gobj, event, kw, src) {
11731
11731
  }
11732
11732
  priv.pending_keep = keep_of(gobj, kw && kw.keep || path);
11733
11733
  refresh_json(gobj, { preserve_view: true });
11734
- update_find_result(gobj);
11734
+ update_find_result$1(gobj);
11735
11735
  return 0;
11736
11736
  }
11737
11737
  /************************************************************
@@ -11742,11 +11742,11 @@ function ac_toggle_fold(gobj, event, kw, src) {
11742
11742
  * keystroke is unusable. The count says whether there is
11743
11743
  * anything to look for elsewhere.
11744
11744
  ************************************************************/
11745
- function ac_find_nodes$2(gobj, event, kw, src) {
11745
+ function ac_find_nodes$3(gobj, event, kw, src) {
11746
11746
  let priv = gobj.priv;
11747
11747
  priv.search = (kw && kw.text || "").trim().toLowerCase();
11748
11748
  refresh_json(gobj, { preserve_view: true });
11749
- update_find_result(gobj);
11749
+ update_find_result$1(gobj);
11750
11750
  return 0;
11751
11751
  }
11752
11752
  /************************************************************
@@ -11758,7 +11758,7 @@ function ac_expand_all$2(gobj, event, kw, src) {
11758
11758
  priv.collapsed.clear();
11759
11759
  priv.reserved.clear();
11760
11760
  refresh_json(gobj);
11761
- update_find_result(gobj);
11761
+ update_find_result$1(gobj);
11762
11762
  return 0;
11763
11763
  }
11764
11764
  /************************************************************
@@ -11775,7 +11775,7 @@ function ac_collapse_all$2(gobj, event, kw, src) {
11775
11775
  priv.collapsed = new Set(paths);
11776
11776
  priv.reserved.clear();
11777
11777
  refresh_json(gobj);
11778
- update_find_result(gobj);
11778
+ update_find_result$1(gobj);
11779
11779
  return 0;
11780
11780
  }
11781
11781
  /************************************************************
@@ -12049,7 +12049,7 @@ function create_gclass$13(gclass_name) {
12049
12049
  ],
12050
12050
  [
12051
12051
  "EV_FIND_NODES",
12052
- ac_find_nodes$2,
12052
+ ac_find_nodes$3,
12053
12053
  null
12054
12054
  ],
12055
12055
  [
@@ -16127,6 +16127,21 @@ function node_status(d) {
16127
16127
  if (!d || !d.running) return "stopped";
16128
16128
  return d.playing ? "playing" : "running";
16129
16129
  }
16130
+ var STATUS_SYMBOLS = {
16131
+ "playing": "▶︎",
16132
+ "running": "‖",
16133
+ "stopped": "■︎"
16134
+ };
16135
+ var DISABLED_SYMBOL = "⊘";
16136
+ /************************************************************
16137
+ * The symbol of a node's status, which is what the reader
16138
+ * actually sees first. `disabled` overrides the rest: a
16139
+ * disabled gobj is not going to run whatever it is doing now.
16140
+ ************************************************************/
16141
+ function node_status_symbol(d) {
16142
+ if (d && d.disabled) return DISABLED_SYMBOL;
16143
+ return STATUS_SYMBOLS[node_status(d)] || STATUS_SYMBOLS.stopped;
16144
+ }
16130
16145
  /************************************************************
16131
16146
  * The i18n key of the status, which is NOT the status itself:
16132
16147
  * a gobj that runs and does not play is PAUSED to the reader,
@@ -16255,7 +16270,7 @@ function node_labels(t) {
16255
16270
  full_name: t("full name"),
16256
16271
  role: t("role"),
16257
16272
  status: t("status"),
16258
- state: t("state"),
16273
+ fsm_state: t("fsm state"),
16259
16274
  disabled: t("disabled"),
16260
16275
  parent: t("parent"),
16261
16276
  bottom_gobj: t("bottom gobj"),
@@ -16284,8 +16299,8 @@ function node_info_rows(d, t) {
16284
16299
  rows.push([f.name, d.name || "—"]);
16285
16300
  if (d.fullname && d.fullname !== d.shortname) rows.push([f.full_name, d.fullname]);
16286
16301
  rows.push([f.role, L.role[role_i18n_key(node_role(d))]]);
16287
- rows.push([f.status, L.status[node_status_key(d)]]);
16288
- rows.push([f.state, d.state || "—"]);
16302
+ rows.push([f.status, node_status_symbol(d) + " " + L.status[node_status_key(d)]]);
16303
+ rows.push([f.fsm_state, d.state || "—"]);
16289
16304
  if (d.disabled) rows.push([f.disabled, f.yes]);
16290
16305
  if (d.parent) rows.push([f.parent, d.parent]);
16291
16306
  if (d.bottom_gobj) rows.push([f.bottom_gobj, d.bottom_gobj]);
@@ -16767,6 +16782,12 @@ var PRIVATE_DATA$9 = {
16767
16782
  theme: null,
16768
16783
  theme_observer: null,
16769
16784
  $layout_select: null,
16785
+ search: "",
16786
+ match_count: 0,
16787
+ find_timer: null,
16788
+ $find_input: null,
16789
+ $find_result: null,
16790
+ $find_count: null,
16770
16791
  $popover: null,
16771
16792
  $popover_title: null,
16772
16793
  $popover_body: null,
@@ -16912,6 +16933,7 @@ var NODE_SIZES = {
16912
16933
  /***************************************************************
16913
16934
  * Shared card typography.
16914
16935
  ***************************************************************/
16936
+ var FIND_MATCH_COLOR = "#D9A23F";
16915
16937
  var GT_FONT = "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif";
16916
16938
  /***************************************************************
16917
16939
  * Theme-aware card palette derived from the role colour.
@@ -17284,11 +17306,107 @@ function destroy_ui$4(gobj) {
17284
17306
  /************************************************************
17285
17307
  * Toolbar
17286
17308
  ************************************************************/
17309
+ /************************************************************
17310
+ * The find box and its count.
17311
+ *
17312
+ * A tree of a real yuno is a hundred cards, and the only way
17313
+ * to locate one was to read them all. The COUNT is what makes
17314
+ * it honest: a graph that did not move looks the same whether
17315
+ * nothing matched or the match was already on screen -- the
17316
+ * same reason the JSON graph carries one, and the same
17317
+ * drawing, because the two sit in the same console.
17318
+ ************************************************************/
17319
+ function make_find_items(gobj) {
17320
+ let priv = gobj.priv;
17321
+ let $find_input = createElement2([
17322
+ "input",
17323
+ {
17324
+ class: "GOBJ_TREE_FIND_INPUT input",
17325
+ type: "text",
17326
+ placeholder: t("search"),
17327
+ "data-i18n-placeholder": "search",
17328
+ "aria-label": t("search"),
17329
+ "data-i18n-aria-label": "search"
17330
+ },
17331
+ [],
17332
+ { input: (evt) => {
17333
+ evt.stopPropagation();
17334
+ let text = evt.target.value.trim();
17335
+ if (priv.find_timer) clearTimeout(priv.find_timer);
17336
+ priv.find_timer = setTimeout(function() {
17337
+ priv.find_timer = null;
17338
+ gobj_send_event(gobj, "EV_FIND_NODES", { text }, gobj);
17339
+ }, 250);
17340
+ } }
17341
+ ]);
17342
+ let $find_control = createElement2([
17343
+ "div",
17344
+ {
17345
+ class: "GOBJ_TREE_FIND control has-icons-left",
17346
+ style: "margin-left:.5rem; margin-right:.5rem; max-width:12rem; min-width:7rem;"
17347
+ },
17348
+ [$find_input, [
17349
+ "span",
17350
+ { class: "icon is-left" },
17351
+ [["i", { class: "yi-magnifying-glass" }]]
17352
+ ]]
17353
+ ]);
17354
+ attach_clear($find_control, $find_input);
17355
+ priv.$find_input = $find_input;
17356
+ return [$find_control, [
17357
+ "div",
17358
+ {
17359
+ class: "GOBJ_TREE_FIND_RESULT is-hidden",
17360
+ style: "display:flex; align-items:center; gap:.3rem; margin-right:.5rem; font-size:.85rem;"
17361
+ },
17362
+ [[
17363
+ "span",
17364
+ { class: "GOBJ_TREE_FIND_COUNT" },
17365
+ ""
17366
+ ], [
17367
+ "span",
17368
+ { i18n: "matches" },
17369
+ t("matches")
17370
+ ]]
17371
+ ]];
17372
+ }
17373
+ /************************************************************
17374
+ * Does this node answer the current find term?
17375
+ *
17376
+ * What a reader types is a gclass, a name or a state -- the
17377
+ * three things a card shows. The full name is searched too,
17378
+ * because it is what the popover shows and what a log line
17379
+ * carries.
17380
+ ************************************************************/
17381
+ function node_matches(d, term) {
17382
+ if (!term) return false;
17383
+ return [
17384
+ d.gclass,
17385
+ d.name,
17386
+ d.fullname,
17387
+ d.state
17388
+ ].filter((x) => typeof x === "string").join(" ").toLowerCase().indexOf(term) >= 0;
17389
+ }
17390
+ /************************************************************
17391
+ * Repaint the count chip. Hidden while nothing is typed:
17392
+ * "0 matches" beside an empty box says nothing.
17393
+ ************************************************************/
17394
+ function update_find_result(gobj) {
17395
+ let priv = gobj.priv;
17396
+ if (!priv.$find_result || !priv.$find_count) return;
17397
+ if (!priv.search) {
17398
+ priv.$find_result.classList.add("is-hidden");
17399
+ priv.$find_count.textContent = "";
17400
+ return;
17401
+ }
17402
+ priv.$find_count.textContent = String(priv.match_count);
17403
+ priv.$find_result.classList.remove("is-hidden");
17404
+ }
17287
17405
  function make_toolbar$1(gobj) {
17288
17406
  let priv = gobj.priv;
17289
17407
  let toolbar_wide = gobj_read_attr(gobj, "wide");
17290
17408
  let current_layout = gobj_read_str_attr(gobj, "layout");
17291
- let left_items = yui_graph_fold_items(gobj, toolbar_wide);
17409
+ let left_items = yui_graph_fold_items(gobj, toolbar_wide).concat(make_find_items(gobj));
17292
17410
  let center_items = [];
17293
17411
  let right_items = [];
17294
17412
  center_items = yui_graph_camera_items(gobj, priv.graph, toolbar_wide);
@@ -17339,6 +17457,8 @@ function make_toolbar$1(gobj) {
17339
17457
  right_items
17340
17458
  ]
17341
17459
  ]);
17460
+ priv.$find_result = $toolbar.querySelector(".GOBJ_TREE_FIND_RESULT");
17461
+ priv.$find_count = $toolbar.querySelector(".GOBJ_TREE_FIND_COUNT");
17342
17462
  refresh_language($toolbar, t);
17343
17463
  return $toolbar;
17344
17464
  }
@@ -17514,26 +17634,32 @@ function render_toggle_html(node_id, collapsed, num_children, cs) {
17514
17634
  /************************************************************
17515
17635
  * HTML for the status pill: what the gobj is DOING.
17516
17636
  *
17517
- * A dot plus a word, and the word is dropped in the compact
17518
- * card where there is no room for it -- the colour still
17519
- * carries it, and the `title` says it in full either way.
17637
+ * A SYMBOL plus a word, and the word is dropped in the
17638
+ * compact card where there is no room for it.
17639
+ *
17640
+ * The symbol carries the state on its own, which a dot never
17641
+ * did: a dot has one shape, so telling running from stopped
17642
+ * meant telling green from red at 10 pixels -- and stopped is
17643
+ * the one that has to be seen. Play, pause and stop are also
17644
+ * the framework's own three verbs, not a borrowed metaphor.
17520
17645
  ************************************************************/
17521
17646
  function render_status_html(d, ss, labels, with_label) {
17522
17647
  let full = d.disabled ? labels.field.disabled : labels.status[node_status_key(d)] || node_status_key(d);
17523
17648
  let label = d.disabled ? labels.field.disabled : labels.status_short[node_status(d)] || node_status(d);
17649
+ let symbol = node_status_symbol(d);
17524
17650
  if (!with_label) return `<span title="${escapeHtml(full)}" style="
17525
17651
  flex: 0 0 auto;
17526
- width: 10px; height: 10px; border-radius: 50%;
17527
- background: ${ss.dot};
17528
- box-shadow: 0 0 0 2px ${ss.bg};
17529
17652
  margin-left: 6px;
17530
- "></span>`;
17653
+ color: ${ss.dot};
17654
+ font-size: 11px;
17655
+ line-height: 1;
17656
+ ">${escapeHtml(symbol)}</span>`;
17531
17657
  return `<span title="${escapeHtml(full)}" style="
17532
17658
  flex: 0 0 auto;
17533
17659
  display: inline-flex;
17534
17660
  align-items: center;
17535
17661
  gap: 4px;
17536
- padding: 1px 7px 1px 5px;
17662
+ padding: 1px 7px 1px 6px;
17537
17663
  background: ${ss.bg};
17538
17664
  color: ${ss.fg};
17539
17665
  border-radius: 9px;
@@ -17541,7 +17667,8 @@ function render_status_html(d, ss, labels, with_label) {
17541
17667
  font-weight: 700;
17542
17668
  line-height: 1.45;
17543
17669
  white-space: nowrap;
17544
- "><span style="width:7px; height:7px; border-radius:50%; background:${ss.dot};"></span>${escapeHtml(label)}</span>`;
17670
+ "><span style="color:${ss.dot}; font-size:11px; line-height:1;"
17671
+ >${escapeHtml(symbol)}</span>${escapeHtml(label)}</span>`;
17545
17672
  }
17546
17673
  /************************************************************
17547
17674
  * HTML for the badges: what the gobj IS, besides its colour.
@@ -17588,6 +17715,12 @@ function build_gobj_nodes(gobj, d, nodes, edges, parent_id, compact, labels) {
17588
17715
  let height = size.h;
17589
17716
  let border_width = category === "yuno" || category === "service" ? 2 : 1;
17590
17717
  let border_style = d.disabled ? "dashed" : "solid";
17718
+ if (node_matches(d, priv.search)) {
17719
+ priv.match_count++;
17720
+ border_width = 3;
17721
+ border_style = "solid";
17722
+ cs = Object.assign({}, cs, { border: FIND_MATCH_COLOR });
17723
+ }
17591
17724
  let children = d.children || [];
17592
17725
  let num_children = children.length;
17593
17726
  let has_children = num_children > 0;
@@ -17712,6 +17845,7 @@ function load_tree(gobj) {
17712
17845
  }
17713
17846
  let layout_cfg = get_layout_cfg(gobj_read_str_attr(gobj, "layout"));
17714
17847
  let labels = node_labels(t);
17848
+ priv.match_count = 0;
17715
17849
  let anchor = priv.pending_anchor || null;
17716
17850
  let preserve_view = !!priv.pending_preserve_view;
17717
17851
  priv.pending_anchor = null;
@@ -18141,6 +18275,23 @@ var GobjLanesHLayout = class extends BaseLayout {
18141
18275
  /************************************************************
18142
18276
  *
18143
18277
  ************************************************************/
18278
+ /************************************************************
18279
+ * EV_FIND_NODES {text}
18280
+ *
18281
+ * The cards carry the highlight in their own markup (an html
18282
+ * node paints no G6 state), so finding is a REBUILD -- which
18283
+ * is also why the box is rate-limited. The view is preserved:
18284
+ * a reader who typed a letter did not ask to be moved.
18285
+ ************************************************************/
18286
+ function ac_find_nodes$2(gobj, event, kw, src) {
18287
+ let priv = gobj.priv;
18288
+ let text = String(kw && kw.text || "").trim().toLowerCase();
18289
+ if (text === priv.search) return 0;
18290
+ priv.search = text;
18291
+ refresh_tree(gobj, { preserve_view: true });
18292
+ update_find_result(gobj);
18293
+ return 0;
18294
+ }
18144
18295
  function ac_refresh$3(gobj, event, kw, src) {
18145
18296
  save_view_state(gobj);
18146
18297
  refresh_tree(gobj);
@@ -18514,6 +18665,7 @@ function create_gclass$9(gclass_name) {
18514
18665
  return -1;
18515
18666
  }
18516
18667
  __gclass__$9 = gclass_create(gclass_name, [
18668
+ ["EV_FIND_NODES", 0],
18517
18669
  ["EV_REFRESH", 0],
18518
18670
  ["EV_THEME", 0],
18519
18671
  ["EV_CHANGE_LAYOUT", 0],
@@ -18534,6 +18686,11 @@ function create_gclass$9(gclass_name) {
18534
18686
  ["EV_GCLASS_CLOSED", 0],
18535
18687
  ["EV_LANGUAGE_CHANGED", 0]
18536
18688
  ], [["ST_IDLE", [
18689
+ [
18690
+ "EV_FIND_NODES",
18691
+ ac_find_nodes$2,
18692
+ null
18693
+ ],
18537
18694
  [
18538
18695
  "EV_REFRESH",
18539
18696
  ac_refresh$3,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yuneta/gobj-ui",
3
- "version": "7.23.38",
3
+ "version": "7.23.39",
4
4
  "type": "module",
5
5
  "main": "dist/gobj-ui.cjs.js",
6
6
  "module": "dist/gobj-ui.es.js",
@@ -45,6 +45,7 @@ import {
45
45
  node_role,
46
46
  node_status,
47
47
  node_status_key,
48
+ node_status_symbol,
48
49
  } from "./gobj_tree_model.js";
49
50
  import {
50
51
  close_gclass_view,
@@ -54,6 +55,7 @@ import {
54
55
  import {yui_shell_of} from "./c_yui_shell.js";
55
56
 
56
57
  import {yui_toolbar} from "./yui_toolbar.js";
58
+ import {attach_clear} from "./yui_inputs.js";
57
59
  import {
58
60
  yui_graph_camera_items,
59
61
  yui_graph_anchor_item,
@@ -257,6 +259,15 @@ let PRIVATE_DATA = {
257
259
  theme: null,
258
260
  theme_observer: null, // MutationObserver on <html data-theme>
259
261
  $layout_select: null,
262
+
263
+ /*---------------- find ----------------*/
264
+ search: "", // current find term, lower-cased
265
+ match_count: 0, // how many cards it matched, for the chip
266
+ find_timer: null, // rate-limits the find box
267
+ $find_input: null,
268
+ $find_result: null,
269
+ $find_count: null,
270
+
260
271
  $popover: null,
261
272
  $popover_title: null,
262
273
  $popover_body: null,
@@ -351,6 +362,13 @@ const NODE_SIZES = {
351
362
  /***************************************************************
352
363
  * Shared card typography.
353
364
  ***************************************************************/
365
+ /*
366
+ * The ring a find match wears. Amber, which in this library means
367
+ * "here" -- the same colour the gclass viewer lights the current
368
+ * state with -- and not one of the role colours, which are taken.
369
+ */
370
+ const FIND_MATCH_COLOR = "#D9A23F";
371
+
354
372
  const GT_FONT =
355
373
  "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, " +
356
374
  "Helvetica, Arial, sans-serif";
@@ -836,6 +854,120 @@ function destroy_ui(gobj)
836
854
  /************************************************************
837
855
  * Toolbar
838
856
  ************************************************************/
857
+ /************************************************************
858
+ * The find box and its count.
859
+ *
860
+ * A tree of a real yuno is a hundred cards, and the only way
861
+ * to locate one was to read them all. The COUNT is what makes
862
+ * it honest: a graph that did not move looks the same whether
863
+ * nothing matched or the match was already on screen -- the
864
+ * same reason the JSON graph carries one, and the same
865
+ * drawing, because the two sit in the same console.
866
+ ************************************************************/
867
+ function make_find_items(gobj)
868
+ {
869
+ let priv = gobj.priv;
870
+
871
+ /* Materialised, not a spec: attach_clear() hangs the NORM clear (✕)
872
+ * on a real element. Clearing dispatches a synthetic `input`, which
873
+ * goes through the same rate-limited handler and fires
874
+ * EV_FIND_NODES with an empty term -- box and highlight clear
875
+ * together. */
876
+ let $find_input = createElement2(
877
+ ['input', {
878
+ class: 'GOBJ_TREE_FIND_INPUT input',
879
+ type: 'text',
880
+ /* A placeholder is not a text node, so the data-i18n walk
881
+ * cannot reach it: it needs its own key. */
882
+ placeholder: t('search'),
883
+ 'data-i18n-placeholder': 'search',
884
+ 'aria-label': t('search'),
885
+ 'data-i18n-aria-label': 'search'
886
+ }, [], {
887
+ /* Rate-limited, not delayed for effect: every keystroke
888
+ * REBUILDS the cards -- an html node paints no G6 state, so
889
+ * the highlight has to live in the card's own markup -- and
890
+ * the first letter typed matches most of a tree. */
891
+ input: (evt) => {
892
+ evt.stopPropagation();
893
+ let text = evt.target.value.trim();
894
+ if(priv.find_timer) {
895
+ clearTimeout(priv.find_timer);
896
+ }
897
+ priv.find_timer = setTimeout(function() {
898
+ priv.find_timer = null;
899
+ gobj_send_event(gobj, "EV_FIND_NODES", {text: text}, gobj);
900
+ }, 250);
901
+ }
902
+ }]);
903
+
904
+ let $find_control = createElement2(
905
+ ['div', {class: 'GOBJ_TREE_FIND control has-icons-left',
906
+ style: 'margin-left:.5rem; margin-right:.5rem; ' +
907
+ 'max-width:12rem; min-width:7rem;'}, [
908
+ $find_input,
909
+ ['span', {class: 'icon is-left'}, [['i', {class: 'yi-magnifying-glass'}]]]
910
+ ]]);
911
+ attach_clear($find_control, $find_input);
912
+
913
+ priv.$find_input = $find_input;
914
+
915
+ return [
916
+ $find_control,
917
+ /* Two spans, not one string: the number is DATA and "matches"
918
+ * is the word, so a language switch re-translates the half that
919
+ * is a word. `display:flex` inline and NOT the `is-flex` helper
920
+ * -- both Bulma helpers carry !important, so `is-hidden is-flex`
921
+ * on one element is decided by stylesheet order. */
922
+ ['div', {class: 'GOBJ_TREE_FIND_RESULT is-hidden',
923
+ style: 'display:flex; align-items:center; gap:.3rem; ' +
924
+ 'margin-right:.5rem; font-size:.85rem;'}, [
925
+ ['span', {class: 'GOBJ_TREE_FIND_COUNT'}, ''],
926
+ ['span', {i18n: 'matches'}, t('matches')]
927
+ ]]
928
+ ];
929
+ }
930
+
931
+ /************************************************************
932
+ * Does this node answer the current find term?
933
+ *
934
+ * What a reader types is a gclass, a name or a state -- the
935
+ * three things a card shows. The full name is searched too,
936
+ * because it is what the popover shows and what a log line
937
+ * carries.
938
+ ************************************************************/
939
+ function node_matches(d, term)
940
+ {
941
+ if(!term) {
942
+ return false;
943
+ }
944
+ let hay = [d.gclass, d.name, d.fullname, d.state]
945
+ .filter((x) => typeof x === "string")
946
+ .join(" ")
947
+ .toLowerCase();
948
+ return hay.indexOf(term) >= 0;
949
+ }
950
+
951
+ /************************************************************
952
+ * Repaint the count chip. Hidden while nothing is typed:
953
+ * "0 matches" beside an empty box says nothing.
954
+ ************************************************************/
955
+ function update_find_result(gobj)
956
+ {
957
+ let priv = gobj.priv;
958
+
959
+ if(!priv.$find_result || !priv.$find_count) {
960
+ return;
961
+ }
962
+ if(!priv.search) {
963
+ priv.$find_result.classList.add("is-hidden");
964
+ priv.$find_count.textContent = "";
965
+ return;
966
+ }
967
+ priv.$find_count.textContent = String(priv.match_count);
968
+ priv.$find_result.classList.remove("is-hidden");
969
+ }
970
+
839
971
  function make_toolbar(gobj)
840
972
  {
841
973
  let priv = gobj.priv;
@@ -846,7 +978,9 @@ function make_toolbar(gobj)
846
978
  * and not a per-node one, so it sits on its own at the left — ahead
847
979
  * of a find box where the toolbar has one. The per-node handles
848
980
  * live inside the cards, on the right of each header. */
849
- let left_items = yui_graph_fold_items(gobj, toolbar_wide);
981
+ let left_items = yui_graph_fold_items(gobj, toolbar_wide).concat(
982
+ make_find_items(gobj)
983
+ );
850
984
  let center_items = [];
851
985
  let right_items = [];
852
986
 
@@ -899,6 +1033,11 @@ function make_toolbar(gobj)
899
1033
  ['div', {class: 'yui-horizontal-toolbar-section right'}, right_items],
900
1034
  ]);
901
1035
 
1036
+ /* The count chip is built as a spec, so its elements only exist
1037
+ * once yui_toolbar() has materialised the row. */
1038
+ priv.$find_result = $toolbar.querySelector('.GOBJ_TREE_FIND_RESULT');
1039
+ priv.$find_count = $toolbar.querySelector('.GOBJ_TREE_FIND_COUNT');
1040
+
902
1041
  refresh_language($toolbar, t);
903
1042
  return $toolbar;
904
1043
  }
@@ -1159,9 +1298,14 @@ function render_toggle_html(node_id, collapsed, num_children, cs)
1159
1298
  /************************************************************
1160
1299
  * HTML for the status pill: what the gobj is DOING.
1161
1300
  *
1162
- * A dot plus a word, and the word is dropped in the compact
1163
- * card where there is no room for it -- the colour still
1164
- * carries it, and the `title` says it in full either way.
1301
+ * A SYMBOL plus a word, and the word is dropped in the
1302
+ * compact card where there is no room for it.
1303
+ *
1304
+ * The symbol carries the state on its own, which a dot never
1305
+ * did: a dot has one shape, so telling running from stopped
1306
+ * meant telling green from red at 10 pixels -- and stopped is
1307
+ * the one that has to be seen. Play, pause and stop are also
1308
+ * the framework's own three verbs, not a borrowed metaphor.
1165
1309
  ************************************************************/
1166
1310
  function render_status_html(d, ss, labels, with_label)
1167
1311
  {
@@ -1171,15 +1315,16 @@ function render_status_html(d, ss, labels, with_label)
1171
1315
  let label = d.disabled?
1172
1316
  labels.field.disabled:
1173
1317
  (labels.status_short[node_status(d)] || node_status(d));
1318
+ let symbol = node_status_symbol(d);
1174
1319
 
1175
1320
  if(!with_label) {
1176
1321
  return `<span title="${escapeHtml(full)}" style="
1177
1322
  flex: 0 0 auto;
1178
- width: 10px; height: 10px; border-radius: 50%;
1179
- background: ${ss.dot};
1180
- box-shadow: 0 0 0 2px ${ss.bg};
1181
1323
  margin-left: 6px;
1182
- "></span>`;
1324
+ color: ${ss.dot};
1325
+ font-size: 11px;
1326
+ line-height: 1;
1327
+ ">${escapeHtml(symbol)}</span>`;
1183
1328
  }
1184
1329
 
1185
1330
  return `<span title="${escapeHtml(full)}" style="
@@ -1187,7 +1332,7 @@ function render_status_html(d, ss, labels, with_label)
1187
1332
  display: inline-flex;
1188
1333
  align-items: center;
1189
1334
  gap: 4px;
1190
- padding: 1px 7px 1px 5px;
1335
+ padding: 1px 7px 1px 6px;
1191
1336
  background: ${ss.bg};
1192
1337
  color: ${ss.fg};
1193
1338
  border-radius: 9px;
@@ -1195,7 +1340,8 @@ function render_status_html(d, ss, labels, with_label)
1195
1340
  font-weight: 700;
1196
1341
  line-height: 1.45;
1197
1342
  white-space: nowrap;
1198
- "><span style="width:7px; height:7px; border-radius:50%; background:${ss.dot};"></span>${escapeHtml(label)}</span>`;
1343
+ "><span style="color:${ss.dot}; font-size:11px; line-height:1;"
1344
+ >${escapeHtml(symbol)}</span>${escapeHtml(label)}</span>`;
1199
1345
  }
1200
1346
 
1201
1347
  /************************************************************
@@ -1257,6 +1403,20 @@ function build_gobj_nodes(gobj, d, nodes, edges, parent_id, compact, labels)
1257
1403
  * "this branch is out of the game" at any zoom. */
1258
1404
  let border_style = d.disabled? "dashed": "solid";
1259
1405
 
1406
+ /*
1407
+ * A card the find box matched. The ring is drawn OVER the role
1408
+ * colour and not instead of it -- the card still has to say what
1409
+ * it is -- and it is counted here, while every card passes
1410
+ * through, so the chip and the drawing can never disagree.
1411
+ */
1412
+ let matched = node_matches(d, priv.search);
1413
+ if(matched) {
1414
+ priv.match_count++;
1415
+ border_width = 3;
1416
+ border_style = "solid";
1417
+ cs = Object.assign({}, cs, {border: FIND_MATCH_COLOR});
1418
+ }
1419
+
1260
1420
  let children = d.children || [];
1261
1421
  let num_children = children.length;
1262
1422
  let has_children = num_children > 0;
@@ -1442,6 +1602,11 @@ function load_tree(gobj)
1442
1602
  let layout_cfg = get_layout_cfg(gobj_read_str_attr(gobj, "layout"));
1443
1603
  let labels = node_labels(t);
1444
1604
 
1605
+ /* Counted as the cards are built, so it is reset HERE and nowhere
1606
+ * else: a count kept by the action would drift the moment the tree
1607
+ * was rebuilt for any other reason. */
1608
+ priv.match_count = 0;
1609
+
1445
1610
  let anchor = priv.pending_anchor || null;
1446
1611
  let preserve_view = !!priv.pending_preserve_view;
1447
1612
  priv.pending_anchor = null;
@@ -2074,6 +2239,29 @@ class GobjLanesHLayout extends BaseLayout {
2074
2239
  /************************************************************
2075
2240
  *
2076
2241
  ************************************************************/
2242
+ /************************************************************
2243
+ * EV_FIND_NODES {text}
2244
+ *
2245
+ * The cards carry the highlight in their own markup (an html
2246
+ * node paints no G6 state), so finding is a REBUILD -- which
2247
+ * is also why the box is rate-limited. The view is preserved:
2248
+ * a reader who typed a letter did not ask to be moved.
2249
+ ************************************************************/
2250
+ function ac_find_nodes(gobj, event, kw, src)
2251
+ {
2252
+ let priv = gobj.priv;
2253
+ let text = String((kw && kw.text) || "").trim().toLowerCase();
2254
+
2255
+ if(text === priv.search) {
2256
+ return 0;
2257
+ }
2258
+
2259
+ priv.search = text;
2260
+ refresh_tree(gobj, {preserve_view: true});
2261
+ update_find_result(gobj);
2262
+ return 0;
2263
+ }
2264
+
2077
2265
  function ac_refresh(gobj, event, kw, src)
2078
2266
  {
2079
2267
  save_view_state(gobj);
@@ -2684,6 +2872,7 @@ function create_gclass(gclass_name)
2684
2872
  *---------------------------------------------*/
2685
2873
  const states = [
2686
2874
  ["ST_IDLE", [
2875
+ ["EV_FIND_NODES", ac_find_nodes, null],
2687
2876
  ["EV_REFRESH", ac_refresh, null],
2688
2877
  ["EV_THEME", ac_theme, null],
2689
2878
  ["EV_CHANGE_LAYOUT", ac_change_layout, null],
@@ -2710,6 +2899,7 @@ function create_gclass(gclass_name)
2710
2899
  * Events
2711
2900
  *---------------------------------------------*/
2712
2901
  const event_types = [
2902
+ ["EV_FIND_NODES", 0],
2713
2903
  ["EV_REFRESH", 0],
2714
2904
  ["EV_THEME", 0],
2715
2905
  ["EV_CHANGE_LAYOUT", 0],
@@ -88,6 +88,43 @@ function node_status(d)
88
88
  return d.playing? "playing": "running";
89
89
  }
90
90
 
91
+ /*
92
+ * What the status LOOKS like, before any word is read.
93
+ *
94
+ * The transport vocabulary, because it is Yuneta's own: a yuno is RUN
95
+ * (the process), PLAYED (the services) and PAUSED, so play / pause /
96
+ * stop are not a metaphor here, they are the three verbs.
97
+ *
98
+ * A coloured dot was the whole encoding before, and a dot has one
99
+ * SHAPE: telling "running" from "stopped" meant telling green from
100
+ * red, at 10px, which is no distinction at all for a reader who does
101
+ * not see colour -- and stopped is the state that matters.
102
+ *
103
+ * U+FE0E asks for the TEXT presentation: bare, several of these
104
+ * render as colour emoji on some platforms, which would make the
105
+ * glyph the loudest thing on a card.
106
+ */
107
+ const STATUS_SYMBOLS = {
108
+ "playing": "\u25B6\uFE0E", /* play */
109
+ "running": "\u2016", /* paused: two bars */
110
+ "stopped": "\u25A0\uFE0E", /* stop */
111
+ };
112
+
113
+ const DISABLED_SYMBOL = "\u2298"; /* a slashed circle */
114
+
115
+ /************************************************************
116
+ * The symbol of a node's status, which is what the reader
117
+ * actually sees first. `disabled` overrides the rest: a
118
+ * disabled gobj is not going to run whatever it is doing now.
119
+ ************************************************************/
120
+ function node_status_symbol(d)
121
+ {
122
+ if(d && d.disabled) {
123
+ return DISABLED_SYMBOL;
124
+ }
125
+ return STATUS_SYMBOLS[node_status(d)] || STATUS_SYMBOLS.stopped;
126
+ }
127
+
91
128
  /************************************************************
92
129
  * The i18n key of the status, which is NOT the status itself:
93
130
  * a gobj that runs and does not play is PAUSED to the reader,
@@ -349,7 +386,7 @@ function node_labels(t)
349
386
  full_name: t("full name"),
350
387
  role: t("role"),
351
388
  status: t("status"),
352
- state: t("state"),
389
+ fsm_state: t("fsm state"),
353
390
  disabled: t("disabled"),
354
391
  parent: t("parent"),
355
392
  bottom_gobj: t("bottom gobj"),
@@ -387,8 +424,14 @@ function node_info_rows(d, t)
387
424
  }
388
425
 
389
426
  rows.push([f.role, L.role[role_i18n_key(node_role(d))]]);
390
- rows.push([f.status, L.status[node_status_key(d)]]);
391
- rows.push([f.state, d.state || "\u2014"]);
427
+ rows.push([
428
+ f.status,
429
+ node_status_symbol(d) + " " + L.status[node_status_key(d)]
430
+ ]);
431
+ /* NOT `state`: both rows read "Estado" in Spanish, side by side,
432
+ * one saying "Parado" and the other "ST_IDLE". They are different
433
+ * questions -- what the gobj is DOING, and where its machine IS. */
434
+ rows.push([f.fsm_state, d.state || "\u2014"]);
392
435
 
393
436
  if(d.disabled) {
394
437
  rows.push([f.disabled, f.yes]);
@@ -423,9 +466,11 @@ function node_info_rows(d, t)
423
466
  export {
424
467
  ROLES,
425
468
  STATUSES,
469
+ STATUS_SYMBOLS,
426
470
  node_role,
427
471
  node_status,
428
472
  node_status_key,
473
+ node_status_symbol,
429
474
  node_badge_keys,
430
475
  node_labels,
431
476
  node_info_rows,
@@ -2,6 +2,7 @@ import {describe, it, expect} from "vitest";
2
2
  import {
3
3
  node_role,
4
4
  node_status,
5
+ node_status_symbol,
5
6
  node_status_key,
6
7
  node_badge_keys,
7
8
  node_info_rows,
@@ -148,8 +149,10 @@ describe("node_info_rows", () => {
148
149
  let timer = root.children.find(c => c.gclass === "C_TIMER");
149
150
  let r = rows_of(timer);
150
151
  expect(r["role"]).toBe("pure child");
151
- expect(r["status"]).toBe("running (paused)");
152
- expect(r["state"]).toBe("ST_IDLE");
152
+ /* The symbol travels with the word: the popover is where a
153
+ * reader learns which glyph means what. */
154
+ expect(r["status"]).toBe("\u2016 running (paused)");
155
+ expect(r["fsm state"]).toBe("ST_IDLE");
153
156
  });
154
157
 
155
158
  it("shows the facts a card has no room for", () => {
@@ -158,7 +161,7 @@ describe("node_info_rows", () => {
158
161
  let r = rows_of(tcp);
159
162
  expect(r["disabled"]).toBe("yes");
160
163
  expect(r["bottom gobj"]).toBe("C_TCP^raw");
161
- expect(r["status"]).toBe("stopped");
164
+ expect(r["status"]).toBe("\u2298 stopped");
162
165
  });
163
166
 
164
167
  it("carries the traces and the flags the backend reports", () => {
@@ -182,3 +185,21 @@ describe("node_info_rows", () => {
182
185
  expect(rows_of(root)["children"]).toBe("2 (collapsed)");
183
186
  });
184
187
  });
188
+
189
+ describe("node_status_symbol", () => {
190
+ it("gives each status its own SHAPE, not only a colour", () => {
191
+ expect(node_status_symbol({running: true, playing: true})).toBe("\u25B6\uFE0E");
192
+ expect(node_status_symbol({running: true, playing: false})).toBe("\u2016");
193
+ expect(node_status_symbol({running: false})).toBe("\u25A0\uFE0E");
194
+ });
195
+
196
+ it("says disabled over anything else", () => {
197
+ expect(node_status_symbol({running: true, playing: true, disabled: true}))
198
+ .toBe("\u2298");
199
+ });
200
+
201
+ it("falls back to stopped for nothing at all", () => {
202
+ expect(node_status_symbol(null)).toBe("\u25A0\uFE0E");
203
+ expect(node_status_symbol(undefined)).toBe("\u25A0\uFE0E");
204
+ });
205
+ });