@yuneta/gobj-ui 2.0.0 → 2.2.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 (58) hide show
  1. package/README.md +25 -20
  2. package/dist/gobj-ui.cjs.js +2331 -526
  3. package/dist/gobj-ui.es.js +2331 -528
  4. package/index.js +32 -31
  5. package/package.json +5 -6
  6. package/{c_g6_nodes_tree.js → src/c_g6_nodes_tree.js} +6 -1
  7. package/{c_yui_form.js → src/c_yui_form.js} +1 -1
  8. package/{c_yui_gobj_tree_js.js → src/c_yui_gobj_tree_js.js} +1 -1
  9. package/{c_yui_json_graph.js → src/c_yui_json_graph.js} +1 -1
  10. package/{c_yui_main.js → src/c_yui_main.js} +3 -3
  11. package/{c_yui_map.js → src/c_yui_map.js} +6 -1
  12. package/{c_yui_nav.js → src/c_yui_nav.js} +134 -26
  13. package/{c_yui_pager.js → src/c_yui_pager.js} +1 -1
  14. package/{c_yui_routing.css → src/c_yui_routing.css} +1 -1
  15. package/{c_yui_routing.js → src/c_yui_routing.js} +1 -1
  16. package/{c_yui_shell.css → src/c_yui_shell.css} +110 -0
  17. package/{c_yui_shell.js → src/c_yui_shell.js} +163 -10
  18. package/{c_yui_tabs.js → src/c_yui_tabs.js} +1 -1
  19. package/{c_yui_treedb_graph.js → src/c_yui_treedb_graph.js} +35 -9
  20. package/{c_yui_treedb_topic_with_form.js → src/c_yui_treedb_topic_with_form.js} +1 -1
  21. package/{c_yui_treedb_topics.js → src/c_yui_treedb_topics.js} +36 -8
  22. package/{c_yui_uplot.js → src/c_yui_uplot.js} +1 -1
  23. package/{c_yui_window.js → src/c_yui_window.js} +242 -21
  24. package/src/c_yui_window_manager.js +602 -0
  25. package/{c_yui_wizard.js → src/c_yui_wizard.js} +1 -1
  26. package/{shell_modals.js → src/shell_modals.js} +53 -13
  27. package/src/tabulator.css +53 -0
  28. package/src/yui_dev.js +1478 -0
  29. package/{yui_icons.css → src/yui_icons.css} +5 -0
  30. package/src/yui_inputs.css +32 -0
  31. package/src/yui_inputs.js +71 -0
  32. package/vite.config.js +0 -131
  33. package/yui_dev.js +0 -583
  34. /package/{c_yui_main.css → src/c_yui_main.css} +0 -0
  35. /package/{c_yui_map.css → src/c_yui_map.css} +0 -0
  36. /package/{c_yui_treedb_topic_with_form.css → src/c_yui_treedb_topic_with_form.css} +0 -0
  37. /package/{g6_drag_canvas_touch.js → src/g6_drag_canvas_touch.js} +0 -0
  38. /package/{lib_graph.css → src/lib_graph.css} +0 -0
  39. /package/{lib_graph.js → src/lib_graph.js} +0 -0
  40. /package/{lib_icons.js → src/lib_icons.js} +0 -0
  41. /package/{lib_maplibre.js → src/lib_maplibre.js} +0 -0
  42. /package/{pager_helpers.js → src/pager_helpers.js} +0 -0
  43. /package/{pager_helpers.test.js → src/pager_helpers.test.js} +0 -0
  44. /package/{route_resolver.js → src/route_resolver.js} +0 -0
  45. /package/{route_resolver.test.js → src/route_resolver.test.js} +0 -0
  46. /package/{shell_focus_trap.js → src/shell_focus_trap.js} +0 -0
  47. /package/{shell_focus_trap.test.js → src/shell_focus_trap.test.js} +0 -0
  48. /package/{shell_show_on.js → src/shell_show_on.js} +0 -0
  49. /package/{shell_show_on.test.js → src/shell_show_on.test.js} +0 -0
  50. /package/{shell_toolbar_helpers.js → src/shell_toolbar_helpers.js} +0 -0
  51. /package/{shell_toolbar_helpers.test.js → src/shell_toolbar_helpers.test.js} +0 -0
  52. /package/{themes.js → src/themes.js} +0 -0
  53. /package/{wizard_helpers.js → src/wizard_helpers.js} +0 -0
  54. /package/{wizard_helpers.test.js → src/wizard_helpers.test.js} +0 -0
  55. /package/{ytable.css → src/ytable.css} +0 -0
  56. /package/{ytable.js → src/ytable.js} +0 -0
  57. /package/{yui_toolbar.css → src/yui_toolbar.css} +0 -0
  58. /package/{yui_toolbar.js → src/yui_toolbar.js} +0 -0
@@ -30,6 +30,7 @@ import {
30
30
  gobj_create, gobj_destroy,
31
31
  gobj_start, gobj_stop,
32
32
  gobj_publish_event,
33
+ gobj_send_event,
33
34
  gobj_subscribe_event,
34
35
  gobj_read_attr, gobj_read_pointer_attr, gobj_write_attr,
35
36
  createElement2, empty_string, is_object, is_array, is_string,
@@ -495,8 +496,8 @@ function build_ui(gobj)
495
496
  let shell_cfg = config.shell || {};
496
497
 
497
498
  /* Root */
498
- let $root = createElement2(
499
- ["div", {class: "yui-shell"}]
499
+ let $container = createElement2(
500
+ ["div", {class: "C_YUI_SHELL yui-shell"}]
500
501
  );
501
502
 
502
503
  /* Build layers */
@@ -504,7 +505,7 @@ function build_ui(gobj)
504
505
  let $layer = createElement2(
505
506
  ["div", {class: `yui-layer yui-layer-${id}`, style: `z-index:${z};`}]
506
507
  );
507
- $root.appendChild($layer);
508
+ $container.appendChild($layer);
508
509
  priv.layers[id] = $layer;
509
510
  }
510
511
 
@@ -561,9 +562,9 @@ function build_ui(gobj)
561
562
 
562
563
  /* Mount */
563
564
  let $mount = gobj_read_attr(gobj, "mount_element") || document.body;
564
- $mount.appendChild($root);
565
+ $mount.appendChild($container);
565
566
 
566
- gobj_write_attr(gobj, "$container", $root);
567
+ gobj_write_attr(gobj, "$container", $container);
567
568
  }
568
569
 
569
570
  /************************************************************
@@ -2085,6 +2086,151 @@ function ac_drawer_close_requested(gobj, event, kw, src)
2085
2086
  return 0;
2086
2087
  }
2087
2088
 
2089
+ /************************************************************
2090
+ * Close 'x' on a closable tab (from a C_YUI_NAV child). The
2091
+ * shell does not own the item set — re-publish so the app (which
2092
+ * owns the underlying data, e.g. the selected-nodes list) removes
2093
+ * the item and calls yui_shell_set_submenu() with the new list.
2094
+ ************************************************************/
2095
+ function ac_nav_item_close(gobj, event, kw, src)
2096
+ {
2097
+ gobj_publish_event(gobj, "EV_NAV_ITEM_CLOSE", {
2098
+ item_id: (kw && kw.item_id) || "",
2099
+ route: (kw && kw.route) || "",
2100
+ menu_id: (kw && kw.menu_id) || "",
2101
+ zone: (kw && kw.zone) || ""
2102
+ });
2103
+ return 0;
2104
+ }
2105
+
2106
+ /************************************************************
2107
+ * Runtime nav API (Yuneta philosophy: the app_config path is the
2108
+ * first, startup caller of the very same machinery — build_item_index
2109
+ * + instantiate + set items; this is its dynamic counterpart).
2110
+ *
2111
+ * yui_shell_set_submenu(shell, parent_item_id, items) replaces the
2112
+ * items of a primary item's submenu (its secondary nav) at runtime
2113
+ * and re-registers their routes so navigation resolves. Item
2114
+ * descriptors may carry { id, name, icon, route, class, closable,
2115
+ * target:{stage,gclass,kw,lifecycle} }. Routes present before but
2116
+ * absent now are pruned (index entry removed, mounted view destroyed).
2117
+ ************************************************************/
2118
+ function find_secondary_nav(priv, parent_item_id)
2119
+ {
2120
+ let suffix = "." + parent_item_id;
2121
+ for(let nav of priv.navs) {
2122
+ let menu_id = gobj_read_attr(nav, "menu_id") || "";
2123
+ if(menu_id.startsWith("secondary.") && menu_id.endsWith(suffix)) {
2124
+ return nav;
2125
+ }
2126
+ }
2127
+ return null;
2128
+ }
2129
+
2130
+ function find_primary_item(shell_gobj, menu_id, item_id)
2131
+ {
2132
+ let config = gobj_read_attr(shell_gobj, "config") || {};
2133
+ let menu = (config.menu && config.menu[menu_id]) || null;
2134
+ if(menu && is_array(menu.items)) {
2135
+ for(let it of menu.items) {
2136
+ if(it && it.id === item_id) {
2137
+ return it;
2138
+ }
2139
+ }
2140
+ }
2141
+ return null;
2142
+ }
2143
+
2144
+ /* Drop a route from the index and destroy any mounted view for it. */
2145
+ function prune_route(shell_gobj, route)
2146
+ {
2147
+ let priv = gobj_read_attr(shell_gobj, "priv");
2148
+ delete priv.item_index[route];
2149
+ for(let stage_name in priv.stages) {
2150
+ let stage = priv.stages[stage_name];
2151
+ let view = stage.items && stage.items[route];
2152
+ if(!view) {
2153
+ continue;
2154
+ }
2155
+ let $c = gobj_read_attr(view, "$container");
2156
+ if($c) {
2157
+ $c.classList.add("is-hidden");
2158
+ }
2159
+ try {
2160
+ gobj_stop(view);
2161
+ gobj_destroy(view);
2162
+ } catch(e) {
2163
+ log_warning(`C_YUI_SHELL: prune_route destroy '${route}' failed: ${e}`);
2164
+ }
2165
+ delete stage.items[route];
2166
+ if(stage.active_route === route) {
2167
+ stage.active_route = "";
2168
+ }
2169
+ }
2170
+ }
2171
+
2172
+ function yui_shell_set_submenu(shell_gobj, parent_item_id, items)
2173
+ {
2174
+ let priv = gobj_read_attr(shell_gobj, "priv");
2175
+ if(!priv) {
2176
+ return -1;
2177
+ }
2178
+ let nav = find_secondary_nav(priv, parent_item_id);
2179
+ if(!nav) {
2180
+ log_warning(
2181
+ `C_YUI_SHELL: yui_shell_set_submenu — no secondary nav for '${parent_item_id}'`
2182
+ );
2183
+ return -1;
2184
+ }
2185
+ items = is_array(items) ? items : [];
2186
+
2187
+ /* Resolve owning menu_id + primary item from the nav's synthesized
2188
+ * id "secondary.<menu_id>.<parent_item_id>", so registered routes
2189
+ * highlight correctly and keep this secondary nav visible. */
2190
+ let nav_menu_id = gobj_read_attr(nav, "menu_id") || "";
2191
+ let m = /^secondary\.(.+)\.([^.]+)$/.exec(nav_menu_id);
2192
+ let owning_menu_id = m ? m[1] : "";
2193
+ let parent_item = find_primary_item(shell_gobj, owning_menu_id, parent_item_id);
2194
+
2195
+ /* Track the routes THIS submenu owns dynamically, so we only prune
2196
+ * our own previous routes — never the static config routes (e.g. a
2197
+ * base "/console/agent" landing declared in app_config). */
2198
+ priv.dynamic_routes = priv.dynamic_routes || {};
2199
+ let prev_routes = priv.dynamic_routes[parent_item_id] || [];
2200
+ let new_routes = [];
2201
+ let new_set = {};
2202
+ for(let it of items) {
2203
+ if(it && it.route) {
2204
+ new_routes.push(it.route);
2205
+ new_set[it.route] = true;
2206
+ }
2207
+ }
2208
+ for(let route of prev_routes) {
2209
+ if(!new_set[route]) {
2210
+ prune_route(shell_gobj, route);
2211
+ }
2212
+ }
2213
+
2214
+ /* Register / refresh routes for the new items. */
2215
+ for(let it of items) {
2216
+ if(!it || !it.route) {
2217
+ continue;
2218
+ }
2219
+ priv.item_index[it.route] = {
2220
+ item: it,
2221
+ parent_item: parent_item,
2222
+ stage: (it.target && it.target.stage) || null,
2223
+ target: it.target || null,
2224
+ menu_id: owning_menu_id
2225
+ };
2226
+ }
2227
+ priv.dynamic_routes[parent_item_id] = new_routes;
2228
+
2229
+ /* Push the new items into the nav (rebuilds its DOM in place). */
2230
+ gobj_send_event(nav, "EV_SET_ITEMS", {items: items}, shell_gobj);
2231
+ return 0;
2232
+ }
2233
+
2088
2234
  /***************************************************************
2089
2235
  * FSM
2090
2236
  ***************************************************************/
@@ -2113,13 +2259,19 @@ function create_gclass(gclass_name)
2113
2259
  ["EV_NAV_CLICKED", ac_nav_clicked, null],
2114
2260
  /* Drawer backdrop close (from a C_YUI_NAV child whose
2115
2261
  * layout is "drawer"). */
2116
- ["EV_DRAWER_CLOSE_REQUESTED", ac_drawer_close_requested, null]
2262
+ ["EV_DRAWER_CLOSE_REQUESTED", ac_drawer_close_requested, null],
2263
+ /* Close 'x' on a closable tab (from a child C_YUI_NAV);
2264
+ * re-published for the app. */
2265
+ ["EV_NAV_ITEM_CLOSE", ac_nav_item_close, null]
2117
2266
  ]]
2118
2267
  ];
2119
2268
 
2120
2269
  const event_types = [
2121
2270
  ["EV_NAV_CLICKED", 0],
2122
2271
  ["EV_DRAWER_CLOSE_REQUESTED", 0],
2272
+ ["EV_NAV_ITEM_CLOSE", event_flag_t.EVF_OUTPUT_EVENT
2273
+ |event_flag_t.EVF_PUBLIC_EVENT
2274
+ |event_flag_t.EVF_NO_WARN_SUBS],
2123
2275
  /* Audit witness: every navigation attempt publishes this BEFORE
2124
2276
  * any work is done, so the FSM trace records intent regardless
2125
2277
  * of whether the route resolves successfully. Pairs with
@@ -2269,11 +2421,11 @@ function yui_shell_set_connection_state(shell_gobj, connected)
2269
2421
  ************************************************************/
2270
2422
  function yui_shell_set_toolbar_item_icon(shell_gobj, item_id, icon_class)
2271
2423
  {
2272
- let $root = gobj_read_attr(shell_gobj, "$container");
2273
- if(!$root || empty_string(item_id) || empty_string(icon_class)) {
2424
+ let $container = gobj_read_attr(shell_gobj, "$container");
2425
+ if(!$container || empty_string(item_id) || empty_string(icon_class)) {
2274
2426
  return;
2275
2427
  }
2276
- let $i = $root.querySelector(
2428
+ let $i = $container.querySelector(
2277
2429
  `[data-toolbar-item-id="${item_id}"] .icon i`
2278
2430
  );
2279
2431
  if($i) {
@@ -2317,5 +2469,6 @@ export {
2317
2469
  yui_shell_set_translator,
2318
2470
  yui_shell_set_connection_state,
2319
2471
  yui_shell_set_toolbar_item_icon,
2320
- yui_shell_close_dropdown
2472
+ yui_shell_close_dropdown,
2473
+ yui_shell_set_submenu
2321
2474
  };
@@ -183,7 +183,7 @@ function build_ui(gobj)
183
183
  let tabs_style = gobj_read_attr(gobj, "tabs_style");
184
184
  let $container = createElement2(
185
185
  // Don't use is-flex, don't work well with is-hidden
186
- ['div', {class: `yui_tabs`, style: 'height:100%; display:flex; flex-direction:column;'}, [
186
+ ['div', {class: `C_YUI_TABS`, style: 'height:100%; display:flex; flex-direction:column;'}, [
187
187
  ['div', {class: 'is-flex-grow-0'}, [
188
188
  ['div', {class: `tabs ${tabs_style}`, style: ''}, [
189
189
  ['ul', {}]
@@ -319,7 +319,7 @@ function build_ui(gobj)
319
319
 
320
320
  let $container = createElement2(
321
321
  // Don't use is-flex, don't work well with is-hidden
322
- ['div', {class: 'graphs', style: `height:100%; display:flex; flex-direction:column;`}, [
322
+ ['div', {class: 'C_YUI_TREEDB_GRAPH', style: `height:100%; display:flex; flex-direction:column;`}, [
323
323
  ['div', {class: 'is-flex-grow-0 is-flex is-align-items-center toolbar_yui_treedb_graph'}, $toolbar],
324
324
  ['div', {class: `is-flex-grow-1 ${padding}`, style: 'height:100%; min-height:0; overflow:hidden;'}, [
325
325
  ['div', {id: priv.canvas_id, class: `graph-container`, style: 'height:100%; min-height:0;border: 1px solid var(--bulma-border-weak);border-radius:0.2rem;'}, [
@@ -333,6 +333,28 @@ function build_ui(gobj)
333
333
  return $container;
334
334
  }
335
335
 
336
+ /************************************************************
337
+ * Show a non-blocking inline error banner at the top of the view
338
+ * (used when the treedb schema `descs` cannot load). Reuses a single
339
+ * banner so retries don't stack.
340
+ ************************************************************/
341
+ function show_load_error(gobj, message)
342
+ {
343
+ let $container = gobj_read_attr(gobj, "$container");
344
+ if(!$container) {
345
+ return;
346
+ }
347
+ let $err = $container.querySelector(".treedb-load-error");
348
+ if(!$err) {
349
+ $err = createElement2(
350
+ ["div", {class: "notification is-danger is-light m-3 treedb-load-error"}, []]
351
+ );
352
+ $container.insertBefore($err, $container.firstChild);
353
+ }
354
+ let treedb = gobj_read_attr(gobj, "treedb_name") || "";
355
+ $err.textContent = (treedb ? `${treedb}: ` : "") + (message || "cannot load treedb");
356
+ }
357
+
336
358
  /************************************************************
337
359
  * Destroy UI
338
360
  ************************************************************/
@@ -908,18 +930,22 @@ function ac_mt_command_answer(gobj, event, kw, src)
908
930
  log_error(e);
909
931
  return;
910
932
  }
911
- if(result < 0) {
912
- display_error_message(
913
- "Error",
914
- t(comment)
915
- );
916
- // HACK don't return, pass errors when need it.
917
- }
918
-
919
933
  let __command__ = msg_iev_get_stack(gobj, kw, "command_stack", true);
920
934
  let command = kw_get_str(gobj, __command__, "command", "", kw_flag_t.KW_REQUIRED);
921
935
  let kw_command = kw_get_dict(gobj, __command__, "kw", {}, kw_flag_t.KW_REQUIRED);
922
936
 
937
+ if(result < 0) {
938
+ if(command === "descs") {
939
+ /* The schema couldn't load (not a treedb, no authz for it, backend
940
+ * down…). Show an inline banner in the view rather than a blocking
941
+ * app-modal that wedges the whole SPA behind an empty tab. */
942
+ show_load_error(gobj, t(comment));
943
+ } else {
944
+ display_error_message("Error", t(comment));
945
+ }
946
+ // HACK don't return for non-descs, pass errors when need it.
947
+ }
948
+
923
949
  switch(command) {
924
950
  case "descs":
925
951
  if(result >= 0) {
@@ -506,7 +506,7 @@ function build_ui(gobj)
506
506
  *----------------------------------------------*/
507
507
  let table_id = gobj_read_str_attr(gobj, "table_id");
508
508
  let $container = createElement2(
509
- ['div', {class: 'container-treedb-topic-with-form', style: 'height:100%;'}, [
509
+ ['div', {class: 'C_YUI_TREEDB_TOPIC_WITH_FORM', style: 'height:100%;'}, [
510
510
  ['div', {class: 'toolbar_tabulator_table m-1', style: 'display:flex; justify-content:space-between; align-items:center; flex-wrap:wrap;'}],
511
511
  ['div',
512
512
  {
@@ -216,7 +216,7 @@ function build_ui(gobj)
216
216
  * Layout Schema
217
217
  *----------------------------------------------*/
218
218
  let $container = createElement2(
219
- ['div', {class: `${gobj_read_attr(gobj, "treedb_name")}`, style: 'height:100%; display:flex; flex-direction:column;'}, [
219
+ ['div', {class: `C_YUI_TREEDB_TOPICS ${gobj_read_attr(gobj, "treedb_name")}`, style: 'height:100%; display:flex; flex-direction:column;'}, [
220
220
  ['div', {class: 'is-flex-grow-0'}, [
221
221
  ['div', {class: `tabs ${gobj_read_attr(gobj, "tabs_style")}`, style: ''}, [
222
222
  ['ul', {}]
@@ -229,6 +229,28 @@ function build_ui(gobj)
229
229
  gobj_write_attr(gobj, "$container", $container);
230
230
  }
231
231
 
232
+ /************************************************************
233
+ * Show a non-blocking inline error banner at the top of the view
234
+ * (used when the treedb schema `descs` cannot load). Reuses a single
235
+ * banner so retries don't stack.
236
+ ************************************************************/
237
+ function show_load_error(gobj, message)
238
+ {
239
+ let $container = gobj_read_attr(gobj, "$container");
240
+ if(!$container) {
241
+ return;
242
+ }
243
+ let $err = $container.querySelector(".treedb-load-error");
244
+ if(!$err) {
245
+ $err = createElement2(
246
+ ["div", {class: "notification is-danger is-light m-3 treedb-load-error"}, []]
247
+ );
248
+ $container.insertBefore($err, $container.firstChild);
249
+ }
250
+ let treedb = gobj_read_attr(gobj, "treedb_name") || "";
251
+ $err.textContent = (treedb ? `${treedb}: ` : "") + (message || "cannot load treedb");
252
+ }
253
+
232
254
  /************************************************************
233
255
  * Destroy UI
234
256
  ************************************************************/
@@ -786,16 +808,22 @@ function ac_mt_command_answer(gobj, event, kw, src)
786
808
  log_error(e);
787
809
  return;
788
810
  }
789
- if(result < 0) {
790
- display_error_message(
791
- "Error",
792
- t(comment)
793
- );
794
- }
795
-
796
811
  let __command__ = msg_iev_get_stack(gobj, kw, "command_stack", true);
797
812
  let command = kw_get_str(gobj, __command__, "command", "", kw_flag_t.KW_REQUIRED);
798
813
  let kw_command = kw_get_dict(gobj, __command__, "kw", {}, kw_flag_t.KW_REQUIRED);
814
+
815
+ if(result < 0) {
816
+ if(command === "descs") {
817
+ /* The schema couldn't load (not a treedb, no authz for it, backend
818
+ * down…). Show an inline banner in the view rather than a blocking
819
+ * app-modal that wedges the whole SPA behind an empty tab. */
820
+ show_load_error(gobj, t(comment));
821
+ } else {
822
+ display_error_message("Error", t(comment));
823
+ }
824
+ return 0;
825
+ }
826
+
799
827
  switch(command) {
800
828
  case "descs":
801
829
  if(result >= 0) {
@@ -145,7 +145,7 @@ function build_ui(gobj)
145
145
  $container = createElement2(
146
146
  ['div',
147
147
  {
148
- class: '',
148
+ class: 'C_YUI_UPLOT',
149
149
  style: 'overflow:auto;'
150
150
  }
151
151
  ]