@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
package/index.js CHANGED
@@ -1,31 +1,31 @@
1
1
  /***********************************************************************
2
- * src/gobj-ui/index.js
2
+ * @yuneta/gobj-ui/index.js (v2 / main line)
3
3
  *
4
4
  * Yuneta UI Library - Reusable GUI components.
5
5
  *
6
- * VENDORED copy of @yuneta/gobj-ui, owned by wattyzer for
7
- * fast in-place development. Edit these files directly.
8
- * When the work stabilizes, fold the changes back into
9
- * yunetas/kernel/js/gobj-ui and restore the package dep.
6
+ * Barrel re-exports for the v2 source, which lives in src/.
7
+ * This is the canonical line, embedded as the yunetas submodule
8
+ * kernel/js/gobj-ui and consumed by wattyzer via a file: dep.
10
9
  *
11
- * Copyright (c) 2025, ArtGins.
10
+ * Copyright (c) 2024-2026, ArtGins.
12
11
  * All Rights Reserved.
13
12
  ***********************************************************************/
14
13
 
15
14
  /*
16
15
  * Components
17
16
  */
18
- export { register_c_yui_main, display_volatil_modal, display_info_message, display_warning_message, display_error_message, get_yesnocancel, get_yesno, get_ok } from "./c_yui_main.js";
19
- export { register_c_yui_window } from "./c_yui_window.js";
20
- export { register_c_yui_tabs } from "./c_yui_tabs.js";
21
- export { register_c_yui_pager } from "./c_yui_pager.js";
22
- export { register_c_yui_wizard } from "./c_yui_wizard.js";
23
- export { register_c_yui_form } from "./c_yui_form.js";
24
- export { register_c_yui_routing } from "./c_yui_routing.js";
25
- export { register_c_yui_map } from "./c_yui_map.js";
26
- export { register_c_yui_uplot } from "./c_yui_uplot.js";
27
- export { register_c_yui_json_graph } from "./c_yui_json_graph.js";
28
- export { register_c_yui_gobj_tree_js } from "./c_yui_gobj_tree_js.js";
17
+ export { register_c_yui_main, display_volatil_modal, display_info_message, display_warning_message, display_error_message, get_yesnocancel, get_yesno, get_ok } from "./src/c_yui_main.js";
18
+ export { register_c_yui_window } from "./src/c_yui_window.js";
19
+ export { register_c_yui_window_manager } from "./src/c_yui_window_manager.js";
20
+ export { register_c_yui_tabs } from "./src/c_yui_tabs.js";
21
+ export { register_c_yui_pager } from "./src/c_yui_pager.js";
22
+ export { register_c_yui_wizard } from "./src/c_yui_wizard.js";
23
+ export { register_c_yui_form } from "./src/c_yui_form.js";
24
+ export { register_c_yui_routing } from "./src/c_yui_routing.js";
25
+ export { register_c_yui_map } from "./src/c_yui_map.js";
26
+ export { register_c_yui_uplot } from "./src/c_yui_uplot.js";
27
+ export { register_c_yui_json_graph } from "./src/c_yui_json_graph.js";
28
+ export { register_c_yui_gobj_tree_js } from "./src/c_yui_gobj_tree_js.js";
29
29
 
30
30
  /*
31
31
  * Declarative shell + menu navigation (new in v7.4)
@@ -44,8 +44,8 @@ export {
44
44
  yui_shell_set_connection_state,
45
45
  yui_shell_set_toolbar_item_icon,
46
46
  yui_shell_close_dropdown,
47
- } from "./c_yui_shell.js";
48
- export { register_c_yui_nav } from "./c_yui_nav.js";
47
+ } from "./src/c_yui_shell.js";
48
+ export { register_c_yui_nav } from "./src/c_yui_nav.js";
49
49
  export {
50
50
  yui_shell_show_info,
51
51
  yui_shell_show_warning,
@@ -54,26 +54,27 @@ export {
54
54
  yui_shell_confirm_ok,
55
55
  yui_shell_confirm_yesno,
56
56
  yui_shell_confirm_yesnocancel,
57
- } from "./shell_modals.js";
57
+ } from "./src/shell_modals.js";
58
58
 
59
59
  /*
60
60
  * TreeDB components
61
61
  */
62
- export { register_c_yui_treedb_topics } from "./c_yui_treedb_topics.js";
63
- export { register_c_yui_treedb_topic_with_form } from "./c_yui_treedb_topic_with_form.js";
64
- export { register_c_yui_treedb_graph } from "./c_yui_treedb_graph.js";
65
- export { register_c_g6_nodes_tree } from "./c_g6_nodes_tree.js";
62
+ export { register_c_yui_treedb_topics } from "./src/c_yui_treedb_topics.js";
63
+ export { register_c_yui_treedb_topic_with_form } from "./src/c_yui_treedb_topic_with_form.js";
64
+ export { register_c_yui_treedb_graph } from "./src/c_yui_treedb_graph.js";
65
+ export { register_c_g6_nodes_tree } from "./src/c_g6_nodes_tree.js";
66
66
 
67
67
  /*
68
68
  * Libraries and utilities
69
69
  */
70
- export { addClasses, removeClasses, toggleClasses, removeChildElements, disableElements, enableElements, set_submit_state, set_cancel_state, set_active_state, getStrokeColor } from "./lib_graph.js";
71
- export { inject_svg_icons } from "./lib_icons.js";
72
- export { EditControl, MarkerControl } from "./lib_maplibre.js";
73
- export { themes } from "./themes.js";
74
- export { YTable, createYTable } from "./ytable.js";
75
- export { yui_toolbar } from "./yui_toolbar.js";
76
- export { info_traffic, setup_dev, build_dev_panel, apply_dev_traces, dev_window_was_open } from "./yui_dev.js";
70
+ export { addClasses, removeClasses, toggleClasses, removeChildElements, disableElements, enableElements, set_submit_state, set_cancel_state, set_active_state, getStrokeColor } from "./src/lib_graph.js";
71
+ export { inject_svg_icons } from "./src/lib_icons.js";
72
+ export { EditControl, MarkerControl } from "./src/lib_maplibre.js";
73
+ export { themes } from "./src/themes.js";
74
+ export { YTable, createYTable } from "./src/ytable.js";
75
+ export { yui_toolbar } from "./src/yui_toolbar.js";
76
+ export { attach_clear } from "./src/yui_inputs.js";
77
+ export { info_traffic, setup_dev, build_dev_panel, apply_dev_traces, dev_window_was_open } from "./src/yui_dev.js";
77
78
 
78
79
  /*
79
80
  * CSS - import these in your main entry point
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yuneta/gobj-ui",
3
- "version": "2.0.0",
3
+ "version": "2.2.0",
4
4
  "type": "module",
5
5
  "main": "dist/gobj-ui.cjs.js",
6
6
  "module": "dist/gobj-ui.es.js",
@@ -11,14 +11,13 @@
11
11
  },
12
12
  "./index.js": "./index.js",
13
13
  "./vite-plugin-yuneta-html.js": "./vite-plugin-yuneta-html.js",
14
- "./*": "./*"
14
+ "./src/*": "./src/*"
15
15
  },
16
16
  "files": [
17
17
  "dist/gobj-ui.es.js",
18
18
  "dist/gobj-ui.cjs.js",
19
19
  "index.js",
20
- "/*.js",
21
- "/*.css",
20
+ "src/",
22
21
  "vite-plugin-yuneta-html.js"
23
22
  ],
24
23
  "description": "Yuneta UI Library — v2 (active development) declarative shell (C_YUI_SHELL+NAV+PAGER+WIZARD) + legacy GClass GUI stack (C_YUI_MAIN/WINDOW/TABS/ROUTING + TreeDB editors + charts/maps). Consumed by wattyzer. The frozen legacy-only line lives on the v1 branch (consumed by estadodelaire and hidraulia).",
@@ -35,12 +34,12 @@
35
34
  "vitest": "^4.1.6"
36
35
  },
37
36
  "peerDependencies": {
38
- "@yuneta/gobj-js": "^7.3.4",
39
37
  "@antv/g6": "^5.1.0",
38
+ "@yuneta/gobj-js": "^7.3.4",
40
39
  "bulma": "^1.0.4",
41
40
  "i18next": "^26.0.7",
42
41
  "maplibre-gl": "^5.24.0",
43
- "tabulator-tables": "^6.4.0",
42
+ "tabulator-tables": "^6.5.2",
44
43
  "tom-select": "^2.6.0",
45
44
  "uplot": "^1.6.32",
46
45
  "vanilla-jsoneditor": "^0.23.0"
@@ -575,7 +575,12 @@ function get_default_ne_xy(gobj)
575
575
  ************************************************************/
576
576
  function build_ui(gobj)
577
577
  {
578
- // Nothing to do, $container set externally
578
+ // $container set externally; tag it so the gclass owning this
579
+ // G6 canvas is identifiable in the browser Inspector.
580
+ let $container = gobj_read_attr(gobj, "$container");
581
+ if($container) {
582
+ $container.classList.add(GCLASS_NAME);
583
+ }
579
584
  }
580
585
 
581
586
  /************************************************************
@@ -284,7 +284,7 @@ function build_ui(gobj)
284
284
  *
285
285
  *----------------------------------------------*/
286
286
  let $container = createElement2(
287
- ['div', {class: 'yui-form-container is-flex is-flex-direction-row overscroll-contain', style:'height:100%; width:100%; box-sizing:border-box;'}, [
287
+ ['div', {class: 'C_YUI_FORM is-flex is-flex-direction-row overscroll-contain', style:'height:100%; width:100%; box-sizing:border-box;'}, [
288
288
 
289
289
  /*----------------------------*
290
290
  * Form
@@ -385,7 +385,7 @@ function build_ui(gobj)
385
385
  let $toolbar = make_toolbar(gobj);
386
386
 
387
387
  let $container = createElement2(
388
- ['div', {class: 'gobj-tree', style: 'height:100%; display:flex; flex-direction:column; position:relative;'}, [
388
+ ['div', {class: 'C_YUI_GOBJ_TREE_JS', style: 'height:100%; display:flex; flex-direction:column; position:relative;'}, [
389
389
  ['div', {class: 'is-flex-grow-0 is-flex toolbar_yui_gobj_tree'}, $toolbar],
390
390
  ['div', {class: 'is-flex-grow-1', style: 'height:100%; min-height:0; overflow:hidden;'}, [
391
391
  ['div', {id: priv.canvas_id, class: 'gobj-tree-container', style: 'height:100%; min-height:0; border: 1px solid var(--bulma-border-weak); border-radius:0.2rem;'}, [
@@ -221,7 +221,7 @@ function build_ui(gobj)
221
221
  let $toolbar = make_toolbar(gobj);
222
222
 
223
223
  let $container = createElement2(
224
- ['div', {class: 'json-graph', style: 'height:100%; display:flex; flex-direction:column;'}, [
224
+ ['div', {class: 'C_YUI_JSON_GRAPH', style: 'height:100%; display:flex; flex-direction:column;'}, [
225
225
  ['div', {class: 'is-flex-grow-0 is-flex toolbar_yui_json_graph'}, $toolbar],
226
226
  ['div', {class: 'is-flex-grow-1', style: 'height:100%; min-height:0; overflow:hidden;'}, [
227
227
  ['div', {id: priv.canvas_id, class: 'json-graph-container', style: 'height:100%; min-height:0; border: 1px solid var(--bulma-border-weak); border-radius:0.2rem;'}, [
@@ -167,8 +167,8 @@ function build_ui(gobj)
167
167
  /*----------------------------------------------*
168
168
  * Layout Schema
169
169
  *----------------------------------------------*/
170
- const $layout = createElement2(
171
- ['div', {id: 'root', class: 'root'}, [
170
+ const $container = createElement2(
171
+ ['div', {id: 'root', class: 'C_YUI_MAIN root'}, [
172
172
  ['div', {id: 'top-layer', class: 'top-layer'}],
173
173
  ['div', {id: 'content-layer', class: 'content-layer is-hidden'}],
174
174
  ['div', {id: 'bottom-layer', class: 'bottom-layer'}],
@@ -178,7 +178,7 @@ function build_ui(gobj)
178
178
  `<iframe id="iframe-publi-page" src="./${gobj_read_attr(gobj, "publi_page")}/index.html" width="100%" height="100%"></iframe>`]
179
179
  ]]
180
180
  );
181
- document.body.appendChild($layout);
181
+ document.body.appendChild($container);
182
182
 
183
183
  const iframe = document.getElementById('iframe-publi-page');
184
184
  if(iframe) {
@@ -111,8 +111,13 @@ function mt_create(gobj)
111
111
 
112
112
  let map_settings = json_deep_copy(gobj_read_attr(gobj, "map_settings"));
113
113
 
114
+ let $map = gobj_read_attr(gobj, "$map");
115
+ if($map) {
116
+ /* Tag the external mount so this gclass is identifiable in the Inspector */
117
+ $map.classList.add(GCLASS_NAME);
118
+ }
114
119
  Object.assign(map_settings, {
115
- container: gobj_read_attr(gobj, "$map"),
120
+ container: $map,
116
121
  });
117
122
 
118
123
  /*-----------------------------*
@@ -15,6 +15,15 @@
15
15
  *
16
16
  * Each item supports:
17
17
  * id, name, icon (CSS class or svg id), route, badge, disabled
18
+ * class — extra CSS class(es) on the item (tabs layout), e.g.
19
+ * a per-item state colour like "yui-nav-disconnected"
20
+ * closable — render a trailing ✕ that emits EV_NAV_ITEM_CLOSE
21
+ * instead of navigating (tabs layout)
22
+ *
23
+ * Items can be replaced at runtime by sending EV_SET_ITEMS
24
+ * {items:[...]} to the nav; the DOM is rebuilt in place and the
25
+ * active highlight restored. The app_config path is simply the
26
+ * first, startup caller of the same mechanism.
18
27
  *
19
28
  * In addition to navigable items, secondary navs accept two
20
29
  * decorative item kinds for in-place visual grouping:
@@ -182,34 +191,90 @@ function build_ui(gobj)
182
191
  let items = gobj_read_attr(gobj, "menu_items") || [];
183
192
  let zone = gobj_read_attr(gobj, "zone") || "";
184
193
  let menu_id = gobj_read_attr(gobj, "menu_id") || "nav";
185
- let $root;
194
+ let $container;
186
195
 
187
196
  switch(layout) {
188
- case "vertical": $root = render_vertical(gobj, items); break;
189
- case "icon-bar": $root = render_icon_bar(gobj, items); break;
190
- case "tabs": $root = render_tabs(gobj, items); break;
191
- case "drawer": $root = render_drawer(gobj, items); break;
192
- case "submenu": $root = render_submenu(gobj, items); break;
193
- case "accordion": $root = render_accordion(gobj, items); break;
194
- default: $root = render_vertical(gobj, items);
195
- }
196
- $root.setAttribute("data-nav-zone", zone);
197
- $root.setAttribute("data-nav-layout", layout);
198
- $root.classList.add("yui-nav", `yui-nav-${layout}`);
197
+ case "vertical": $container = render_vertical(gobj, items); break;
198
+ case "icon-bar": $container = render_icon_bar(gobj, items); break;
199
+ case "tabs": $container = render_tabs(gobj, items); break;
200
+ case "drawer": $container = render_drawer(gobj, items); break;
201
+ case "submenu": $container = render_submenu(gobj, items); break;
202
+ case "accordion": $container = render_accordion(gobj, items); break;
203
+ default: $container = render_vertical(gobj, items);
204
+ }
205
+ $container.setAttribute("data-nav-zone", zone);
206
+ $container.setAttribute("data-nav-layout", layout);
207
+ $container.classList.add("C_YUI_NAV", "yui-nav", `yui-nav-${layout}`);
199
208
 
200
209
  /* Accessibility: every nav root is a landmark. For the drawer we
201
210
  * tag the wrapper as role=dialog and the panel as role=navigation. */
202
211
  if(layout !== "drawer") {
203
- $root.setAttribute("role", "navigation");
204
- if(!$root.hasAttribute("aria-label")) {
212
+ $container.setAttribute("role", "navigation");
213
+ if(!$container.hasAttribute("aria-label")) {
205
214
  let label = gobj_read_attr(gobj, "nav_label") || menu_id;
206
- $root.setAttribute("aria-label", label);
215
+ $container.setAttribute("aria-label", label);
207
216
  }
208
217
  }
209
218
 
210
- wire_clicks(gobj, $root);
219
+ wire_clicks(gobj, $container);
220
+
221
+ gobj_write_attr(gobj, "$container", $container);
222
+ }
223
+
224
+ /************************************************************
225
+ * Rebuild the nav DOM in place from the current menu_items.
226
+ * Used by EV_SET_ITEMS so a nav's tabs can be changed at runtime
227
+ * (the app_config path is just the first, startup caller).
228
+ ************************************************************/
229
+ function rebuild(gobj)
230
+ {
231
+ let $old = gobj_read_attr(gobj, "$container");
232
+ let priv = gobj_read_attr(gobj, "priv") || {};
233
+ let parent = $old ? $old.parentNode : null;
234
+ let next = $old ? $old.nextSibling : null;
235
+ let was_hidden = !!($old && $old.classList.contains("is-hidden"));
236
+
237
+ /* Drop the old delegated click listener before discarding the node. */
238
+ if($old && priv.click_handler) {
239
+ $old.removeEventListener("click", priv.click_handler);
240
+ }
211
241
 
212
- gobj_write_attr(gobj, "$container", $root);
242
+ build_ui(gobj); /* writes a fresh $container + wires new clicks */
243
+
244
+ let $new = gobj_read_attr(gobj, "$container");
245
+ if($new && was_hidden) {
246
+ $new.classList.add("is-hidden");
247
+ }
248
+ if(parent && $new) {
249
+ parent.insertBefore($new, next);
250
+ }
251
+ if($old && $old.parentNode) {
252
+ $old.parentNode.removeChild($old);
253
+ }
254
+
255
+ apply_active_route(gobj);
256
+ }
257
+
258
+ /************************************************************
259
+ * Re-apply the active-route highlight after a rebuild (the
260
+ * shell only pushes EV_ROUTE_CHANGED on navigation, not on a
261
+ * self-triggered items change).
262
+ ************************************************************/
263
+ function apply_active_route(gobj)
264
+ {
265
+ let $c = gobj_read_attr(gobj, "$container");
266
+ let route = gobj_read_attr(gobj, "active_route") || "";
267
+ if(!$c || empty_string(route)) {
268
+ return;
269
+ }
270
+ let $a = $c.querySelector(`a[data-route="${css_escape(route)}"]`);
271
+ if($a) {
272
+ let $li = $a.closest("li");
273
+ if($li) {
274
+ $li.classList.add("is-active");
275
+ }
276
+ $a.classList.add("is-active");
277
+ }
213
278
  }
214
279
 
215
280
  /************************************************************
@@ -283,6 +348,15 @@ function render_tabs(gobj, items)
283
348
  if(show_label && !empty_string(it.name)) {
284
349
  children.push(["span", {i18n: it.name}, it.name]);
285
350
  }
351
+ /* Optional close affordance: a trailing ✕ that emits
352
+ * EV_NAV_ITEM_CLOSE instead of navigating (caught first in
353
+ * wire_clicks by its data-close-item marker). */
354
+ if(it.closable) {
355
+ children.push(["span", {class: "icon is-small yui-nav-close ml-2",
356
+ "data-close-item": it.id, role: "button",
357
+ "aria-label": "close", title: "close"},
358
+ ["i", {class: "yi-xmark", "aria-hidden":"true"}]]);
359
+ }
286
360
  let a_attrs = {
287
361
  href: it.route ? "#" + it.route : "#",
288
362
  "data-item-id": it.id,
@@ -293,8 +367,11 @@ function render_tabs(gobj, items)
293
367
  a_attrs.title = tip;
294
368
  a_attrs["data-i18n-title"] = tip;
295
369
  }
370
+ /* Per-item state class (e.g. a "disconnected" colour) rides on
371
+ * the <li>, alongside Bulma's own is-active. */
372
+ let li_class = empty_string(it.class) ? "" : String(it.class);
296
373
  lis.push(
297
- ["li", {class: "", "data-item-id": it.id, "data-route": it.route || ""},
374
+ ["li", {class: li_class, "data-item-id": it.id, "data-route": it.route || ""},
298
375
  ["a", a_attrs, children]
299
376
  ]
300
377
  );
@@ -369,7 +446,7 @@ function render_accordion(gobj, items)
369
446
  let show_label = gobj_read_attr(gobj, "show_label");
370
447
  let icon_pos = gobj_read_attr(gobj, "icon_pos");
371
448
 
372
- let $root = createElement2(["aside", {class: "menu yui-nav-accordion p-2"}]);
449
+ let $container = createElement2(["aside", {class: "menu yui-nav-accordion p-2"}]);
373
450
  for(let it of items) {
374
451
  if(is_decorative(it)) {
375
452
  continue; /* primary-level decorations don't apply to accordion sections */
@@ -392,7 +469,7 @@ function render_accordion(gobj, items)
392
469
  head_attrs.i18n = head_text;
393
470
  }
394
471
  let $hdr = createElement2(["button", head_attrs, head_text]);
395
- $root.appendChild($hdr);
472
+ $container.appendChild($hdr);
396
473
 
397
474
  let $ul = createElement2(
398
475
  ["ul", {id: body_id,
@@ -407,9 +484,9 @@ function render_accordion(gobj, items)
407
484
  ));
408
485
  }
409
486
  }
410
- $root.appendChild($ul);
487
+ $container.appendChild($ul);
411
488
  }
412
- return $root;
489
+ return $container;
413
490
  }
414
491
 
415
492
  /************************************************************
@@ -557,7 +634,7 @@ function item_iconbar(gobj, it, opts)
557
634
  ];
558
635
  }
559
636
 
560
- function wire_clicks(gobj, $root)
637
+ function wire_clicks(gobj, $container)
561
638
  {
562
639
  let priv = gobj_read_attr(gobj, "priv") || {};
563
640
 
@@ -567,6 +644,22 @@ function wire_clicks(gobj, $root)
567
644
  return;
568
645
  }
569
646
 
647
+ /* Close affordance (✕ on a closable tab): emit the intent and
648
+ * let the shell/app remove the item; never navigate. */
649
+ let $close = target.closest("[data-close-item]");
650
+ if($close) {
651
+ let $routed = $close.closest("[data-route]");
652
+ gobj_publish_event(gobj, "EV_NAV_ITEM_CLOSE", {
653
+ item_id: $close.getAttribute("data-close-item") || "",
654
+ route: ($routed && $routed.getAttribute("data-route")) || "",
655
+ menu_id: gobj_read_attr(gobj, "menu_id") || "",
656
+ zone: gobj_read_attr(gobj, "zone") || ""
657
+ });
658
+ ev.preventDefault();
659
+ ev.stopPropagation();
660
+ return;
661
+ }
662
+
570
663
  /* Drawer backdrop close. Don't mutate the DOM directly —
571
664
  * the shell owns the drawer state (focus-trap, escape
572
665
  * stack) and must run its full close path. Publish the
@@ -584,7 +677,7 @@ function wire_clicks(gobj, $root)
584
677
  /* Accordion head toggle takes precedence over navigation: the
585
678
  * head is a container for its sub-items, not a destination. */
586
679
  let $head = target.closest(".yui-accordion-head");
587
- if($head && $root.contains($head)) {
680
+ if($head && $container.contains($head)) {
588
681
  let $next = $head.nextElementSibling;
589
682
  if($next && $next.classList.contains("yui-accordion-body")) {
590
683
  let open = $next.classList.contains("is-hidden");
@@ -622,7 +715,7 @@ function wire_clicks(gobj, $root)
622
715
  });
623
716
  };
624
717
 
625
- $root.addEventListener("click", handler);
718
+ $container.addEventListener("click", handler);
626
719
  priv.click_handler = handler;
627
720
  }
628
721
 
@@ -636,6 +729,18 @@ function wire_clicks(gobj, $root)
636
729
 
637
730
 
638
731
 
732
+ /************************************************************
733
+ * EV_SET_ITEMS — replace this nav's items and re-render in place.
734
+ * This is the runtime counterpart of the startup config path:
735
+ * the shell (or app, via the shell) drives dynamic navs through it.
736
+ ************************************************************/
737
+ function ac_set_items(gobj, event, kw, src)
738
+ {
739
+ gobj_write_attr(gobj, "menu_items", is_array(kw.items) ? kw.items : []);
740
+ rebuild(gobj);
741
+ return 0;
742
+ }
743
+
639
744
  /************************************************************
640
745
  * EV_ROUTE_CHANGED from the shell — refresh active highlight
641
746
  ************************************************************/
@@ -739,13 +844,16 @@ function create_gclass(gclass_name)
739
844
 
740
845
  const states = [
741
846
  ["ST_IDLE", [
742
- ["EV_ROUTE_CHANGED", ac_route_changed, null]
847
+ ["EV_ROUTE_CHANGED", ac_route_changed, null],
848
+ ["EV_SET_ITEMS", ac_set_items, null]
743
849
  ]]
744
850
  ];
745
851
 
746
852
  const event_types = [
747
853
  ["EV_ROUTE_CHANGED", 0],
854
+ ["EV_SET_ITEMS", 0],
748
855
  ["EV_NAV_CLICKED", event_flag_t.EVF_OUTPUT_EVENT|event_flag_t.EVF_PUBLIC_EVENT],
856
+ ["EV_NAV_ITEM_CLOSE", event_flag_t.EVF_OUTPUT_EVENT|event_flag_t.EVF_PUBLIC_EVENT],
749
857
  ["EV_DRAWER_CLOSE_REQUESTED", event_flag_t.EVF_OUTPUT_EVENT|event_flag_t.EVF_PUBLIC_EVENT]
750
858
  ];
751
859
 
@@ -145,7 +145,7 @@ function mt_destroy(gobj)
145
145
  function build_ui(gobj)
146
146
  {
147
147
  let $container = createElement2(
148
- ['div', {class: 'yui-pager', style: 'height:100%; display:flex; flex-direction:column;'}, [
148
+ ['div', {class: 'C_YUI_PAGER', style: 'height:100%; display:flex; flex-direction:column;'}, [
149
149
  ['div', {class: 'yui-pager-header is-flex is-align-items-center is-flex-grow-0',
150
150
  style: 'gap:.25rem; padding:.25rem .25rem;'}, [
151
151
  ['button', {class: 'yui-pager-back button is-white is-hidden', 'aria-label': 'back'}, [
@@ -1,7 +1,7 @@
1
1
  /*********************************************************
2
2
  * Layout ui_route
3
3
  *********************************************************/
4
- .container-grid-2col {
4
+ .C_YUI_ROUTING {
5
5
  display: grid;
6
6
  grid-template-columns: max-content 1fr;
7
7
  height: 100%;
@@ -301,7 +301,7 @@ function build_layout(gobj)
301
301
  * Layout Schema
302
302
  *----------------------------------------------*/
303
303
  const $container = createElement2(
304
- ['div', {class: 'container-grid-2col', style: ''}, [
304
+ ['div', {class: 'C_YUI_ROUTING', style: ''}, [
305
305
  ['div', {class: 'container-grid-2col-left'},
306
306
  ['aside', {class: 'menu'}, [
307
307
  ['ul', {class: 'menu-list'}, []]
@@ -174,6 +174,21 @@
174
174
  /*--- tabs (Bulma .tabs) ---*/
175
175
  .yui-nav-tabs .tabs ul { flex-wrap: wrap; }
176
176
 
177
+ /*--- closable tab: trailing ✕ affordance (item.closable) ---*/
178
+ .yui-nav-close {
179
+ opacity: 0.55;
180
+ cursor: pointer;
181
+ border-radius: 3px;
182
+ }
183
+ .yui-nav-close:hover {
184
+ opacity: 1;
185
+ background: var(--bulma-danger, #f14668);
186
+ color: #fff;
187
+ }
188
+
189
+ /*--- per-item state class (item.class) — generic disconnected tint ---*/
190
+ .yui-nav-tabs .tabs li.yui-nav-disconnected a { color: var(--bulma-danger, #f14668); }
191
+
177
192
  /*--- stacked item (icon above label) ---*/
178
193
  .yui-nav-stacked {
179
194
  display: inline-flex;
@@ -459,3 +474,98 @@
459
474
  font: inherit;
460
475
  color: inherit;
461
476
  }
477
+
478
+ /*============================================================
479
+ * Adaptive dialog (yui_shell_show_modal, opts.dialog:true)
480
+ *
481
+ * The standardized "single window / popup" chrome:
482
+ * - Desktop : a centered card, close X at the top-right.
483
+ * - Mobile : a full-screen sheet, back arrow at the top-left.
484
+ * One dismiss control shows per breakpoint; both call close().
485
+ * 768px is the shell-wide mobile breakpoint (matches the dock).
486
+ *============================================================*/
487
+ /* Lighter backdrop than Bulma's default (0.86): a popup should dim the
488
+ * page, not black it out. */
489
+ .yui-dialog .modal-background {
490
+ background-color: rgba(10, 10, 10, 0.4);
491
+ }
492
+ .yui-dialog .yui-dialog-content {
493
+ display: flex;
494
+ flex-direction: column;
495
+ width: 100%;
496
+ max-width: 640px;
497
+ max-height: 90vh;
498
+ background: var(--bulma-scheme-main, #fff);
499
+ border-radius: 8px;
500
+ overflow: hidden;
501
+ box-shadow: 0 10px 40px rgba(0, 0, 0, 0.35);
502
+ }
503
+ .yui-dialog-header {
504
+ display: flex;
505
+ align-items: center;
506
+ gap: 0.5rem;
507
+ flex: 0 0 auto;
508
+ padding: 0.5rem 0.6rem;
509
+ border-bottom: 1px solid var(--bulma-border-weak, #dbdbdb);
510
+ }
511
+ .yui-dialog-title {
512
+ flex: 1 1 auto;
513
+ min-width: 0;
514
+ overflow: hidden;
515
+ text-overflow: ellipsis;
516
+ white-space: nowrap;
517
+ font-weight: 600;
518
+ }
519
+ .yui-dialog-back,
520
+ .yui-dialog-x {
521
+ flex: 0 0 auto;
522
+ width: 2rem;
523
+ height: 2rem;
524
+ display: inline-flex;
525
+ align-items: center;
526
+ justify-content: center;
527
+ padding: 0;
528
+ border: 0;
529
+ border-radius: 6px;
530
+ background: transparent;
531
+ color: var(--bulma-text, currentColor);
532
+ cursor: pointer;
533
+ }
534
+ .yui-dialog-back:hover,
535
+ .yui-dialog-x:hover {
536
+ background: var(--bulma-background, rgba(0, 0, 0, 0.06));
537
+ }
538
+ .yui-dialog-body {
539
+ flex: 1 1 auto;
540
+ min-height: 0;
541
+ overflow: auto;
542
+ padding: 1rem;
543
+ }
544
+
545
+ /* Desktop: X (right) shown, back arrow hidden. */
546
+ .yui-dialog-back {
547
+ display: none;
548
+ }
549
+
550
+ /* Mobile: full-screen sheet, back arrow (left) shown, X hidden. */
551
+ @media (max-width: 768px) {
552
+ .yui-dialog .modal-content {
553
+ margin: 0;
554
+ width: 100%;
555
+ max-height: 100%;
556
+ height: 100%;
557
+ }
558
+ .yui-dialog .yui-dialog-content {
559
+ max-width: 100%;
560
+ max-height: 100%;
561
+ height: 100%;
562
+ border-radius: 0;
563
+ box-shadow: none;
564
+ }
565
+ .yui-dialog-back {
566
+ display: inline-flex;
567
+ }
568
+ .yui-dialog-x {
569
+ display: none;
570
+ }
571
+ }