@yuneta/gobj-ui 7.14.3 → 7.16.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.
package/README.md CHANGED
@@ -668,6 +668,96 @@ Two implementation notes worth keeping:
668
668
  still arrive from a keyboard path or a form that outlived the flag, and an
669
669
  ignored write is exactly the behaviour this whole change exists to stop.
670
670
 
671
+ ### The graph's viewport toolbar
672
+
673
+ `C_G6_NODES_TREE` floats a vertical toolbar over the canvas:
674
+
675
+ | control | what it does |
676
+ |---|---|
677
+ | zoom in / zoom out | one step of scale |
678
+ | **the zoom level** | a readout, not a button — `85%` |
679
+ | ── | |
680
+ | fit | `fitView()`: the whole graph in the viewport |
681
+ | **`1:1`** | `zoomTo(1)`: actual size |
682
+ | ── | |
683
+ | full screen | the container, not the camera |
684
+
685
+ Two of those rows are the answer to a real complaint, and the reasoning
686
+ generalises:
687
+
688
+ - **`1:1` used to be a house**, and the house was the thing people reached for
689
+ when they wanted the graph back. It never gave it to them: the action is
690
+ `zoomTo(1)`, which sets the **scale** and leaves the camera where it was, so
691
+ from a corner of a large graph it answered with the same corner at 100%. A
692
+ house means *the initial extent* in a map and *the starting view* in an
693
+ editor — never a scale — and this one sat directly under `fit`, so the pair
694
+ read as two ways to do one thing. Actual size is **written** in every editor
695
+ that offers it, because there is no glyph anybody recognises for it.
696
+ - **the zoom level is shown** because `1:1` is a jump to a number, and a jump
697
+ to a number is only meaningful next to the number you are on.
698
+
699
+ The **separators are gaps, not lines**: every item already carries a hairline
700
+ against its neighbour, so one more line would not group anything. Full screen
701
+ is behind the second one because it is a window control that happens to live in
702
+ a camera toolbar.
703
+
704
+ Both toolbars (this one and the edit one) **follow the theme**. They used to be
705
+ pinned to a light background in both themes, with the icon colour pinned dark
706
+ so it survived that — two light islands over a dark canvas.
707
+
708
+ **New keys for consumers: `actual size`, `zoom level`** (both tooltips, so a
709
+ host that has not defined them shows the key on hover and nothing else breaks).
710
+
711
+ ### Selecting several nodes, and moving them together
712
+
713
+ In **edition** mode the graph has a real selection, not just "the node you
714
+ clicked":
715
+
716
+ | gesture | what it does |
717
+ |---|---|
718
+ | click a node | selects it **and opens it**: resize handles, ports, popovers |
719
+ | **shift + click** | adds that node to the selection, or takes it out |
720
+ | **shift + drag on the canvas** | rubber band: the selection becomes what it enclosed |
721
+ | drag any selected node | **moves the whole selection**, as one undo |
722
+ | click the canvas | clears it |
723
+
724
+ Three decisions are worth knowing, because each one is where this could have
725
+ gone wrong:
726
+
727
+ - **G6's `selected` element state IS the selection.** `drag-element` decides
728
+ what a drag moves by asking the graph for it
729
+ (`getElementDataByState('node', 'selected')`), so a set kept anywhere else
730
+ would be a second truth the drag never consults — the ring would say five and
731
+ one would move. It also batches the move, so a group drag is one history
732
+ entry rather than one per node.
733
+
734
+ - **The ring is painted into the card's own html**, and it had to be. A state
735
+ style paints on a node's KEY SHAPE, and every node here is an `html` node
736
+ whose key shape is a DOM element — the same reason the amber highlight had
737
+ never appeared before `7.3.0`. Selecting with `brush-select` and nothing else
738
+ would have selected correctly and shown **nothing**. The ring is blue and
739
+ drawn OUTSIDE the amber halo, so a node that is both a find match and
740
+ selected wears both; one function composes them (`ring_shadow`), because
741
+ before it each repaint wrote its own flag and erased the other's.
742
+
743
+ - **The gesture is G6's, the result is an event.** `brush-select` gets an
744
+ `onSelect` that sends `EV_BRUSH_SELECT` with the ids, and the action does the
745
+ work — so a marquee shows up in the `machine` trace like every other action.
746
+ Shift+click is not G6's `click-select` at all: this gclass already owns
747
+ `EV_NODE_CLICK`, and adding a second selection owner outside the FSM is how
748
+ the two end up disagreeing.
749
+
750
+ Panning gives way while **Shift** is held (`drag-canvas` takes an `enable`
751
+ predicate), or the canvas would pan under the rubber band — G6 binds
752
+ `drag-canvas` straight to the drag events, and its own docs warn that the two
753
+ gestures cannot both be a plain drag.
754
+
755
+ **A marquee selects, it does not open.** Even when it encloses exactly one
756
+ node, the handles and ports stay away: `_selected_node_id` means *the node
757
+ opened for editing*, and only a click sets it. Everything that hangs off a
758
+ single node reads that field, so a multiple selection puts all of it away by
759
+ construction rather than by a check in twenty places.
760
+
671
761
  ### Finding a node in the graph
672
762
 
673
763
  `C_YUI_TREEDB_GRAPH` carries a find box in the middle of its toolbar. It
@@ -53702,11 +53702,22 @@ function inject_svg_icons() {
53702
53702
  ***********************************************************************/
53703
53703
  /***************************************************************
53704
53704
  * YuiToolbar — G6 Toolbar subclass that adds per-item className
53705
- * and disabled support.
53705
+ * and disabled support, plus three item kinds G6 does not have.
53706
53706
  *
53707
53707
  * Each item in getItems() may carry:
53708
53708
  * className — CSS class added to the div (e.g. 'EV_SAVE_GRAPH')
53709
53709
  * disabled — boolean; sets the 'disabled' attribute initially
53710
+ * text — render this text instead of a sprite symbol; a
53711
+ * label is the only readable way to say `1:1`, and
53712
+ * every editor that offers actual-size writes it
53713
+ * readout — a text item that REPORTS instead of acting (the
53714
+ * zoom level); it is not a button
53715
+ * separator — a group divider, not an item
53716
+ *
53717
+ * The base class fires onClick only for elements whose class list
53718
+ * contains `g6-toolbar-item`, so a separator and a readout carry
53719
+ * their own class and are inert by construction, with no guard in
53720
+ * the click handler.
53710
53721
  *
53711
53722
  * With className set to the event name, the lib_graph.js state
53712
53723
  * functions (set_submit_state, disableElements, …) work on toolbar
@@ -53716,9 +53727,15 @@ function inject_svg_icons() {
53716
53727
  var YuiToolbar = class extends _antv_g6.Toolbar {
53717
53728
  async getDOMContent() {
53718
53729
  return (await this.options.getItems()).map((item) => {
53730
+ if (item.separator) return `<div class="g6-toolbar-sep" aria-hidden="true"></div>`;
53719
53731
  const extra_class = item.className ? ` ${item.className}` : "";
53732
+ const title = item.title ?? "";
53733
+ if (item.readout) return `<div class="g6-toolbar-readout${extra_class}" title="${title}" aria-label="${title}">${item.text ?? ""}</div>`;
53720
53734
  const disabled = item.disabled ? " disabled" : "";
53721
- return `<div class="g6-toolbar-item${extra_class}" value="${item.value}" title="${item.title ?? ""}"${disabled}><svg aria-hidden="true" focusable="false"><use xlink:href="#${item.id}"></use></svg></div>`;
53735
+ const is_text = item.text !== void 0;
53736
+ const kind_class = is_text ? " g6-toolbar-item-text" : "";
53737
+ const body = is_text ? `<span class="g6-toolbar-text">${item.text}</span>` : `<svg aria-hidden="true" focusable="false"><use xlink:href="#${item.id}"></use></svg>`;
53738
+ return `<div class="g6-toolbar-item${kind_class}${extra_class}" value="${item.value}" title="${title}" aria-label="${title}"${disabled}>${body}</div>`;
53722
53739
  }).join("");
53723
53740
  }
53724
53741
  };
@@ -53730,6 +53747,7 @@ ensure_drag_canvas_patch();
53730
53747
  var GCLASS_NAME$1 = "C_G6_NODES_TREE";
53731
53748
  var HIGHLIGHT_COLOR = "#f0a020";
53732
53749
  var HIGHLIGHT_HALO = "rgba(240,160,32,0.35)";
53750
+ var SELECT_RING = "rgba(59,130,246,0.95)";
53733
53751
  /***************************************************************
53734
53752
  * Internal layout and operation mode definitions
53735
53753
  ***************************************************************/
@@ -53841,6 +53859,7 @@ var PRIVATE_DATA$1 = {
53841
53859
  _link_saved_styles: [],
53842
53860
  _focus_topic: null,
53843
53861
  _focus_ids: [],
53862
+ _selected_paint_ids: [],
53844
53863
  _pending_focus_topic: null,
53845
53864
  _pending_find: null,
53846
53865
  _layout_asked: "",
@@ -54063,6 +54082,7 @@ function configure_events(gobj) {
54063
54082
  update_edge_icon_position(gobj);
54064
54083
  update_node_icon_position(gobj);
54065
54084
  update_link_icon_position(gobj);
54085
+ update_zoom_readout(gobj);
54066
54086
  });
54067
54087
  priv._on_language_changed = () => {
54068
54088
  update_toolbar(gobj);
@@ -54128,6 +54148,59 @@ function update_toolbar(gobj) {
54128
54148
  configure_toolbar_edit(gobj);
54129
54149
  if (graph_get_plugin(gobj, "toolbar-edit")) graph.updatePlugin({ key: "toolbar-edit" });
54130
54150
  }
54151
+ /************************************************************
54152
+ * The floating toolbars follow the theme.
54153
+ *
54154
+ * They used to be pinned to a light background in BOTH themes,
54155
+ * with the icon colour pinned dark so it survived that -- two
54156
+ * light islands sitting over a dark canvas. The tokens do the
54157
+ * flipping now; the fallbacks are the old forced-light values, so
54158
+ * a host without Bulma gets exactly what it had.
54159
+ ************************************************************/
54160
+ function toolbar_style(extra) {
54161
+ return Object.assign({
54162
+ backgroundColor: "var(--bulma-scheme-main, #f5f5f5)",
54163
+ color: "var(--bulma-text-strong, #333)",
54164
+ padding: "8px",
54165
+ boxShadow: "0 2px 8px rgba(0, 0, 0, 0.15)",
54166
+ borderRadius: "8px",
54167
+ border: "1px solid var(--bulma-border-weak, #e8e8e8)",
54168
+ opacity: "0.85"
54169
+ }, extra || {});
54170
+ }
54171
+ /************************************************************
54172
+ * The zoom level as a percentage, the way every editor that
54173
+ * shows one writes it. Empty while the graph cannot be asked --
54174
+ * the toolbar is built before the first draw, and a viewport
54175
+ * that does not exist yet has no zoom to report.
54176
+ ************************************************************/
54177
+ function zoom_percent_text(gobj) {
54178
+ let graph = gobj.priv.graph;
54179
+ if (!graph || typeof graph.getZoom !== "function") return "";
54180
+ let zoom;
54181
+ try {
54182
+ zoom = graph.getZoom();
54183
+ } catch (e) {
54184
+ return "";
54185
+ }
54186
+ if (!zoom) return "";
54187
+ return `${Math.round(zoom * 100)}%`;
54188
+ }
54189
+ /************************************************************
54190
+ * Repaint the zoom readout in place.
54191
+ *
54192
+ * The text node is patched instead of re-rendering the plugin:
54193
+ * the readout follows the wheel, and updatePlugin() rebuilds the
54194
+ * whole toolbar's innerHTML -- which would also drop the disabled
54195
+ * state of the edit buttons on every notch of the wheel.
54196
+ ************************************************************/
54197
+ function update_zoom_readout(gobj) {
54198
+ let $container = (0, _yuneta_gobj_js.gobj_read_attr)(gobj, "$container");
54199
+ if (!$container) return;
54200
+ let $readout = $container.querySelector(".G6_ZOOM_LEVEL");
54201
+ if (!$readout) return;
54202
+ $readout.textContent = zoom_percent_text(gobj);
54203
+ }
54131
54204
  function configure_toolbar(gobj) {
54132
54205
  let priv = gobj.priv;
54133
54206
  let toolbar_position = (0, _yuneta_gobj_js.gobj_read_str_attr)(gobj, "toolbar_position") || "top-left";
@@ -54136,17 +54209,10 @@ function configure_toolbar(gobj) {
54136
54209
  type: "yui-toolbar",
54137
54210
  className: "g6-toolbar-large",
54138
54211
  position: toolbar_position,
54139
- style: {
54140
- backgroundColor: "#f5f5f5",
54141
- color: "#333",
54142
- padding: "8px",
54143
- boxShadow: "0 2px 8px rgba(0, 0, 0, 0.15)",
54144
- borderRadius: "8px",
54145
- border: "1px solid #e8e8e8",
54146
- opacity: "0.85",
54212
+ style: toolbar_style({
54147
54213
  marginTop: "12px",
54148
54214
  marginLeft: "12px"
54149
- },
54215
+ }),
54150
54216
  getItems: () => {
54151
54217
  let items = [
54152
54218
  {
@@ -54161,6 +54227,13 @@ function configure_toolbar(gobj) {
54161
54227
  className: "EV_ZOOM_OUT",
54162
54228
  title: (0, i18next.t)("zoom out")
54163
54229
  },
54230
+ {
54231
+ readout: true,
54232
+ className: "G6_ZOOM_LEVEL",
54233
+ text: zoom_percent_text(gobj),
54234
+ title: (0, i18next.t)("zoom level")
54235
+ },
54236
+ { separator: true },
54164
54237
  {
54165
54238
  id: "g6-icon-fit",
54166
54239
  value: "auto-fit",
@@ -54168,13 +54241,14 @@ function configure_toolbar(gobj) {
54168
54241
  title: (0, i18next.t)("auto fit")
54169
54242
  },
54170
54243
  {
54171
- id: "g6-icon-home",
54244
+ text: "1:1",
54172
54245
  value: "reset",
54173
54246
  className: "EV_ZOOM_RESET",
54174
- title: (0, i18next.t)("reset zoom")
54247
+ title: (0, i18next.t)("actual size")
54175
54248
  }
54176
54249
  ];
54177
54250
  if ((0, _yuneta_gobj_js.gobj_read_bool_attr)(gobj, "with_fullscreen")) {
54251
+ items.push({ separator: true });
54178
54252
  if (priv.is_fullscreen) items.push({
54179
54253
  id: "g6-icon-fullscreen-exit",
54180
54254
  value: "exit-fullscreen",
@@ -54230,15 +54304,7 @@ function configure_toolbar_edit(gobj) {
54230
54304
  type: "yui-toolbar",
54231
54305
  className: "g6-toolbar-large",
54232
54306
  position: "left-top",
54233
- style: {
54234
- backgroundColor: "#f5f5f5",
54235
- color: "#333",
54236
- padding: "8px",
54237
- boxShadow: "0 2px 8px rgba(0, 0, 0, 0.15)",
54238
- borderRadius: "8px",
54239
- border: "1px solid #e8e8e8",
54240
- opacity: "0.85"
54241
- },
54307
+ style: toolbar_style(),
54242
54308
  getItems: () => {
54243
54309
  return [
54244
54310
  {
@@ -54314,9 +54380,24 @@ function configure_behaviour(gobj) {
54314
54380
  case "edition":
54315
54381
  priv.edit_mode = true;
54316
54382
  behaviors = [
54317
- "drag-canvas",
54383
+ {
54384
+ type: "drag-canvas",
54385
+ key: "drag-canvas",
54386
+ enable: (event) => !event.shiftKey
54387
+ },
54318
54388
  "zoom-canvas",
54319
- "drag-element"
54389
+ "drag-element",
54390
+ {
54391
+ type: "brush-select",
54392
+ key: "brush-select",
54393
+ trigger: ["shift"],
54394
+ enableElements: ["node"],
54395
+ immediately: false,
54396
+ onSelect: (states) => {
54397
+ let ids = Object.keys(states || {}).filter((id) => (states[id] || []).includes("selected"));
54398
+ (0, _yuneta_gobj_js.gobj_send_event)(gobj, "EV_BRUSH_SELECT", { ids }, gobj);
54399
+ }
54400
+ }
54320
54401
  ];
54321
54402
  break;
54322
54403
  case "operation":
@@ -55431,6 +55512,81 @@ function perform_history_op(gobj, is_redo) {
55431
55512
  sync_history_to_backend(gobj, cmd ? cmd.original : null);
55432
55513
  }
55433
55514
  }
55515
+ /************************************************************
55516
+ * The selection.
55517
+ *
55518
+ * G6's `selected` element state IS the selection: `drag-element`
55519
+ * reads it (`getElementDataByState`) to decide what a drag moves,
55520
+ * so keeping the set anywhere else would be a second truth the
55521
+ * drag does not consult. What this gclass keeps is what is
55522
+ * PAINTED -- an html node draws no state style at all (its key
55523
+ * shape is a DOM element), so the ring lives in the card's own
55524
+ * html, exactly like the find highlight, and the painted set
55525
+ * exists to diff the repaint.
55526
+ ************************************************************/
55527
+ function selected_node_ids(gobj) {
55528
+ let graph = gobj.priv.graph;
55529
+ if (!graph) return [];
55530
+ let data;
55531
+ try {
55532
+ data = graph.getElementDataByState("node", "selected") || [];
55533
+ } catch (e) {
55534
+ return [];
55535
+ }
55536
+ return data.map((nd) => nd.id);
55537
+ }
55538
+ /************************************************************
55539
+ * Move the ring to these nodes and off the ones that had it.
55540
+ ************************************************************/
55541
+ function paint_selection(gobj, ids) {
55542
+ let priv = gobj.priv;
55543
+ let prev = priv._selected_paint_ids || [];
55544
+ let next = ids || [];
55545
+ priv._selected_paint_ids = next;
55546
+ repaint_cards(gobj, /* @__PURE__ */ new Set([...prev, ...next]));
55547
+ }
55548
+ /************************************************************
55549
+ * Select a SET of nodes: what a marquee and a shift-click do.
55550
+ *
55551
+ * It leaves `_selected_node_id` null even for a set of one,
55552
+ * because that field is not "the selection", it is "the node
55553
+ * opened for editing" -- the resize handles, the ports and the
55554
+ * popovers hang off it, and none of them means anything over a
55555
+ * set. Clicking a node is what opens one (`select_node`); a
55556
+ * marquee selects, it does not open.
55557
+ ************************************************************/
55558
+ function set_selection(gobj, ids) {
55559
+ let graph = gobj.priv.graph;
55560
+ let next = [];
55561
+ for (let id of ids || []) try {
55562
+ if (graph.getNodeData(id)) next.push(id);
55563
+ } catch (e) {}
55564
+ deselect_node(gobj);
55565
+ if (!next.length) return;
55566
+ history_pause(gobj);
55567
+ try {
55568
+ let states = {};
55569
+ for (let id of next) states[id] = ["selected"];
55570
+ graph.setElementState(states);
55571
+ } catch (e) {
55572
+ (0, _yuneta_gobj_js.log_error)(`${(0, _yuneta_gobj_js.gobj_short_name)(gobj)}: cannot set the selection: ${e}`);
55573
+ }
55574
+ paint_selection(gobj, next);
55575
+ graph_draw(gobj).then(() => {
55576
+ history_resume(gobj);
55577
+ });
55578
+ }
55579
+ /************************************************************
55580
+ * Add a node to the selection, or take it out of it.
55581
+ ************************************************************/
55582
+ function toggle_in_selection(gobj, node_id) {
55583
+ let priv = gobj.priv;
55584
+ let current = new Set(selected_node_ids(gobj));
55585
+ for (let id of priv._selected_paint_ids || []) current.add(id);
55586
+ if (current.has(node_id)) current.delete(node_id);
55587
+ else current.add(node_id);
55588
+ set_selection(gobj, [...current]);
55589
+ }
55434
55590
  function select_node(gobj, node_id) {
55435
55591
  let priv = gobj.priv;
55436
55592
  let graph = priv.graph;
@@ -55440,6 +55596,7 @@ function select_node(gobj, node_id) {
55440
55596
  graph.setElementState(node_id, ["selected"]);
55441
55597
  } catch (e) {}
55442
55598
  priv._selected_node_id = node_id;
55599
+ paint_selection(gobj, [node_id]);
55443
55600
  show_resize_handles(gobj);
55444
55601
  show_node_icon(gobj);
55445
55602
  graph_draw(gobj).then(() => {
@@ -55455,12 +55612,17 @@ function deselect_node(gobj) {
55455
55612
  hide_node_popover(gobj);
55456
55613
  hide_delete_confirm(gobj);
55457
55614
  hide_unlink_confirm(gobj);
55458
- if (priv._selected_node_id) {
55615
+ let clearing = /* @__PURE__ */ new Set([...selected_node_ids(gobj), ...priv._selected_paint_ids || []]);
55616
+ if (priv._selected_node_id) clearing.add(priv._selected_node_id);
55617
+ if (clearing.size) {
55459
55618
  history_pause(gobj);
55460
55619
  try {
55461
- graph.setElementState(priv._selected_node_id, []);
55620
+ let states = {};
55621
+ for (let id of clearing) states[id] = [];
55622
+ graph.setElementState(states);
55462
55623
  } catch (e) {}
55463
55624
  priv._selected_node_id = null;
55625
+ paint_selection(gobj, []);
55464
55626
  graph_draw(gobj).then(() => {
55465
55627
  history_resume(gobj);
55466
55628
  });
@@ -56747,33 +56909,36 @@ function refresh_minimap(gobj) {
56747
56909
  * same three-way choice — it lived inline in the theme refresh and is
56748
56910
  * shared now. Returns null for a node that carries no desc.
56749
56911
  ************************************************************/
56750
- function node_innerHTML_of(nd, theme, highlight) {
56912
+ function node_innerHTML_of(nd, theme, highlight, selected) {
56751
56913
  if (!nd || !nd.data || !nd.data.desc) return null;
56752
56914
  let desc = nd.data.desc;
56753
56915
  let record = nd.data.record || {};
56754
56916
  let label = node_label(desc, record);
56755
56917
  switch (desc.node_treedb_type) {
56756
- case "child": return build_chip_innerHTML(desc.color, theme, record.icon, label, record.id, highlight);
56757
- case "extended": return build_node_innerHTML(desc.color, theme, record.icon, label, desc.topic_name, true, record.id, highlight);
56758
- case "hierarchical": return build_node_innerHTML(desc.color, theme, record.icon, label, desc.topic_name, false, record.id, highlight);
56918
+ case "child": return build_chip_innerHTML(desc.color, theme, record.icon, label, record.id, highlight, selected);
56919
+ case "extended": return build_node_innerHTML(desc.color, theme, record.icon, label, desc.topic_name, true, record.id, highlight, selected);
56920
+ case "hierarchical": return build_node_innerHTML(desc.color, theme, record.icon, label, desc.topic_name, false, record.id, highlight, selected);
56759
56921
  }
56760
56922
  return null;
56761
56923
  }
56762
56924
  /************************************************************
56763
- * Repaint the cards whose highlight state CHANGES: the ones that had it
56764
- * and lose it, plus the ones that gain it. Only those — a treedb graph
56765
- * is redrawn per keystroke of the find box otherwise.
56925
+ * Repaint these cards with the flags they carry RIGHT NOW.
56926
+ *
56927
+ * One place, because the flags are independent and each used to
56928
+ * be written by whoever repainted last: a find that repainted its
56929
+ * matches erased the selection ring off them, and a selection
56930
+ * repainted over a match erased the amber. Both are read here
56931
+ * from where they live.
56766
56932
  ************************************************************/
56767
- function apply_node_highlight(gobj, prev_ids, next_ids) {
56933
+ function repaint_cards(gobj, ids) {
56768
56934
  let priv = gobj.priv;
56769
56935
  let graph = priv.graph;
56770
- if (!graph) return;
56771
- let next = new Set(next_ids || []);
56772
- let touched = /* @__PURE__ */ new Set([...prev_ids || [], ...next]);
56773
- if (touched.size === 0) return;
56936
+ if (!graph || !ids || !ids.size) return;
56937
+ let focus = new Set(priv._focus_ids || []);
56938
+ let selected = new Set(priv._selected_paint_ids || []);
56774
56939
  let updates = [];
56775
- for (let id of touched) {
56776
- let html = node_innerHTML_of(graph.getNodeData(id), priv.theme, next.has(id));
56940
+ for (let id of ids) {
56941
+ let html = node_innerHTML_of(graph.getNodeData(id), priv.theme, focus.has(id), selected.has(id));
56777
56942
  if (html !== null) updates.push({
56778
56943
  id,
56779
56944
  style: { innerHTML: html }
@@ -56784,19 +56949,32 @@ function apply_node_highlight(gobj, prev_ids, next_ids) {
56784
56949
  graph.updateNodeData(updates);
56785
56950
  graph.draw();
56786
56951
  } catch (e) {
56787
- (0, _yuneta_gobj_js.log_error)(`${(0, _yuneta_gobj_js.gobj_short_name)(gobj)}: cannot repaint the highlight: ${e}`);
56952
+ (0, _yuneta_gobj_js.log_error)(`${(0, _yuneta_gobj_js.gobj_short_name)(gobj)}: cannot repaint the cards: ${e}`);
56788
56953
  }
56789
56954
  }
56955
+ /************************************************************
56956
+ * Repaint the cards whose highlight state CHANGES: the ones that had it
56957
+ * and lose it, plus the ones that gain it. Only those — a treedb graph
56958
+ * is redrawn per keystroke of the find box otherwise.
56959
+ ************************************************************/
56960
+ function apply_node_highlight(gobj, prev_ids, next_ids) {
56961
+ if (!gobj.priv.graph) return;
56962
+ let next = new Set(next_ids || []);
56963
+ let touched = /* @__PURE__ */ new Set([...prev_ids || [], ...next]);
56964
+ if (touched.size === 0) return;
56965
+ repaint_cards(gobj, touched);
56966
+ }
56790
56967
  function refresh_html_nodes_theme(gobj, theme) {
56791
56968
  let priv = gobj.priv;
56792
56969
  let graph = priv.graph;
56793
56970
  if (!graph) return;
56794
56971
  let nodes = graph.getData().nodes || [];
56795
56972
  let highlighted = new Set(priv._focus_ids || []);
56973
+ let selected = new Set(priv._selected_paint_ids || []);
56796
56974
  let updates = [];
56797
56975
  for (let i = 0; i < nodes.length; i++) {
56798
56976
  let id = nodes[i].id;
56799
- let html = node_innerHTML_of(graph.getNodeData(id), theme, highlighted.has(id));
56977
+ let html = node_innerHTML_of(graph.getNodeData(id), theme, highlighted.has(id), selected.has(id));
56800
56978
  if (html !== null) updates.push({
56801
56979
  id,
56802
56980
  style: { innerHTML: html }
@@ -56826,12 +57004,29 @@ function refresh_default_edges_theme(gobj, theme) {
56826
57004
  if (updates.length > 0) graph.updateEdgeData(updates);
56827
57005
  }
56828
57006
  /************************************************************
57007
+ * The rings a card can wear, composed into one `box-shadow`.
57008
+ *
57009
+ * A node can be a find match and be selected at the same time, so
57010
+ * neither ring may be written by overwriting the other: the amber
57011
+ * halo hugs the card and the blue selection ring is drawn outside
57012
+ * it, which is also the order that reads correctly when only one
57013
+ * of them is on.
57014
+ ************************************************************/
57015
+ function ring_shadow(highlight, selected, base_shadow) {
57016
+ let rings = [];
57017
+ if (highlight) rings.push(`0 0 0 4px ${HIGHLIGHT_HALO}`);
57018
+ if (selected) rings.push(`0 0 0 ${highlight ? "7px" : "3px"} ${SELECT_RING}`);
57019
+ if (base_shadow) rings.push(base_shadow);
57020
+ if (!rings.length) return "";
57021
+ return `box-shadow: ${rings.join(", ")};`;
57022
+ }
57023
+ /************************************************************
56829
57024
  * Build innerHTML for pure-child (leaf) nodes: a compact
56830
57025
  * chip-card. Same colour/typography family as the entity card
56831
57026
  * but lighter (1px border, no shadow, single line). The name is
56832
57027
  * always legible (ellipsis + native title tooltip on overflow).
56833
57028
  ************************************************************/
56834
- function build_chip_innerHTML(color, theme, icon, label, key, highlight) {
57029
+ function build_chip_innerHTML(color, theme, icon, label, key, highlight, selected) {
56835
57030
  let title = key || label;
56836
57031
  let dark = theme === "dark";
56837
57032
  let bg = dark ? `color-mix(in srgb, ${color} 30%, #2c3542)` : `color-mix(in srgb, ${color} 10%, ${dark ? "#1b2230" : "#ffffff"})`;
@@ -56850,7 +57045,7 @@ function build_chip_innerHTML(color, theme, icon, label, key, highlight) {
56850
57045
  height: 100%;
56851
57046
  background: ${bg};
56852
57047
  border: ${highlight ? "3px" : "1px"} solid ${border};
56853
- ${highlight ? `box-shadow: 0 0 0 4px ${HIGHLIGHT_HALO};` : ""}
57048
+ ${ring_shadow(highlight, selected, "")}
56854
57049
  border-radius: 8px;
56855
57050
  color: ${text_color};
56856
57051
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
@@ -56876,7 +57071,7 @@ function build_chip_innerHTML(color, theme, icon, label, key, highlight) {
56876
57071
  * colour is kept (per-topic differentiation) but softened via
56877
57072
  * color-mix instead of a harsh saturated fill. Theme-aware.
56878
57073
  ************************************************************/
56879
- function build_node_innerHTML(color, theme, icon, label, topic_name, structural, key, highlight) {
57074
+ function build_node_innerHTML(color, theme, icon, label, topic_name, structural, key, highlight, selected) {
56880
57075
  let title = key || label;
56881
57076
  let dark = theme === "dark";
56882
57077
  let surface = dark ? "#1b2230" : "#ffffff";
@@ -56918,7 +57113,7 @@ function build_node_innerHTML(color, theme, icon, label, topic_name, structural,
56918
57113
  background: ${bg};
56919
57114
  border: ${highlight ? "3px" : "1.5px"} ${border_style} ${border};
56920
57115
  border-radius: 10px;
56921
- box-shadow: ${highlight ? `0 0 0 4px ${HIGHLIGHT_HALO}, ${shadow}` : shadow};
57116
+ ${ring_shadow(highlight, selected, shadow)}
56922
57117
  color: ${title_color};
56923
57118
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
56924
57119
  display: flex;
@@ -57839,6 +58034,17 @@ function ac_node_drag_end(gobj, event, kw, src) {
57839
58034
  return 0;
57840
58035
  }
57841
58036
  /************************************************************
58037
+ * The rubber band let go: these are the nodes it enclosed.
58038
+ *
58039
+ * They arrive in the kw because `onSelect` runs BEFORE G6 writes
58040
+ * the state, so asking the graph here would answer the previous
58041
+ * selection.
58042
+ ************************************************************/
58043
+ function ac_brush_select(gobj, event, kw, src) {
58044
+ set_selection(gobj, kw && kw.ids || []);
58045
+ return 0;
58046
+ }
58047
+ /************************************************************
57842
58048
  * Node click - publish vertex clicked event
57843
58049
  ************************************************************/
57844
58050
  function ac_node_click(gobj, event, kw, src) {
@@ -57853,7 +58059,8 @@ function ac_node_click(gobj, event, kw, src) {
57853
58059
  topic_name: nodedata.data.desc.topic_name,
57854
58060
  record: nodedata.data.record
57855
58061
  });
57856
- if (priv.edit_mode) {
58062
+ if (priv.edit_mode && kw.evt.shiftKey) toggle_in_selection(gobj, node_id);
58063
+ else if (priv.edit_mode) {
57857
58064
  let containerRect = priv.$container.getBoundingClientRect();
57858
58065
  let canvasPoint = graph.getCanvasByViewport([kw.evt.client.x - containerRect.left, kw.evt.client.y - containerRect.top]);
57859
58066
  let port_key = detect_port_click(gobj, node_id, canvasPoint[0], canvasPoint[1]);
@@ -58080,6 +58287,11 @@ function create_gclass$1(gclass_name) {
58080
58287
  ac_node_drag_end,
58081
58288
  null
58082
58289
  ],
58290
+ [
58291
+ "EV_BRUSH_SELECT",
58292
+ ac_brush_select,
58293
+ null
58294
+ ],
58083
58295
  [
58084
58296
  "EV_ZOOM_IN",
58085
58297
  ac_zoom_in,
@@ -58188,6 +58400,7 @@ function create_gclass$1(gclass_name) {
58188
58400
  ["EV_NODE_CONTEXT_MENU", 0],
58189
58401
  ["EV_CANVAS_CLICK", 0],
58190
58402
  ["EV_NODE_DRAG_END", 0],
58403
+ ["EV_BRUSH_SELECT", 0],
58191
58404
  ["EV_ZOOM_IN", 0],
58192
58405
  ["EV_ZOOM_OUT", 0],
58193
58406
  ["EV_ZOOM_RESET", 0],