@yuneta/gobj-ui 3.0.0 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/README.md +324 -6
  2. package/dist/gobj-ui.cjs.js +10891 -5006
  3. package/dist/gobj-ui.es.js +10915 -5064
  4. package/index.js +45 -1
  5. package/package.json +4 -4
  6. package/src/c_g6_nodes_tree.js +128 -40
  7. package/src/c_yui_form.js +112 -53
  8. package/src/c_yui_gobj_tree_js.js +58 -36
  9. package/src/c_yui_json.css +139 -0
  10. package/src/c_yui_json.js +928 -0
  11. package/src/c_yui_json_graph.js +110 -20
  12. package/src/c_yui_map.js +18 -36
  13. package/src/c_yui_nav.js +6 -9
  14. package/src/c_yui_period.css +184 -0
  15. package/src/c_yui_period.js +1441 -0
  16. package/src/c_yui_shell.css +33 -0
  17. package/src/c_yui_shell.js +672 -111
  18. package/src/c_yui_treedb_graph.js +639 -37
  19. package/src/c_yui_treedb_schema.js +478 -0
  20. package/src/c_yui_treedb_topic_with_form.css +7 -42
  21. package/src/c_yui_treedb_topic_with_form.js +152 -103
  22. package/src/c_yui_treedb_topics.css +73 -0
  23. package/src/c_yui_treedb_topics.js +1070 -29
  24. package/src/c_yui_window.js +180 -97
  25. package/src/c_yui_window_manager.js +38 -4
  26. package/src/json_view_helpers.js +214 -0
  27. package/src/json_view_helpers.test.js +135 -0
  28. package/src/route_map_model.js +317 -0
  29. package/src/route_map_model.test.js +209 -0
  30. package/src/route_resolver.js +24 -1
  31. package/src/route_resolver.test.js +40 -1
  32. package/src/shell_modals.js +117 -39
  33. package/src/shell_route_map.css +225 -0
  34. package/src/shell_route_map.js +363 -0
  35. package/src/tabulator.css +67 -0
  36. package/src/yui_dev.js +130 -8
  37. package/src/yui_frontend_view.js +106 -0
  38. package/src/yui_icons.css +62 -0
  39. package/src/yui_inputs.css +8 -3
  40. package/src/yui_inputs.js +51 -8
  41. package/src/yui_tabulator_i18n.js +128 -0
  42. package/src/yui_theme.js +147 -0
  43. package/src/yui_time.js +666 -0
  44. package/src/yui_time.test.js +330 -0
  45. package/src/yui_toolbar.js +86 -43
@@ -0,0 +1,225 @@
1
+ /***********************************************************************
2
+ * shell_route_map.css
3
+ *
4
+ * Styling for the site-map (route-tree) viewer in shell_route_map.js.
5
+ * A monospaced, indented tree; theme-aware via Bulma variables;
6
+ * and @media print rules (scoped by body.routemap-printing) so the
7
+ * Print button prints only the tree.
8
+ *
9
+ * Copyright (c) 2026, ArtGins.
10
+ * All Rights Reserved.
11
+ ***********************************************************************/
12
+ .ROUTEMAP_BODY {
13
+ display: flex;
14
+ flex-direction: column;
15
+ width: 100%;
16
+ min-height: 0;
17
+ }
18
+ /* In the resizable window, fill the window body so the tree grows/shrinks
19
+ * with it (the WINDOW_BODY is is-flex-grow-1 with a definite height). */
20
+ .yui-window-body .ROUTEMAP_BODY {
21
+ height: 100%;
22
+ }
23
+ .ROUTEMAP_SEARCH_ROW {
24
+ display: flex;
25
+ align-items: center;
26
+ gap: 0.6rem;
27
+ }
28
+ .ROUTEMAP_SEARCH_CTRL {
29
+ max-width: 22rem;
30
+ flex: 1 1 auto;
31
+ }
32
+ .ROUTEMAP_COUNT {
33
+ display: inline-flex;
34
+ gap: 0.25rem;
35
+ white-space: nowrap;
36
+ flex: 0 0 auto;
37
+ }
38
+ .ROUTEMAP_COUNT_N {
39
+ font-weight: 600;
40
+ }
41
+ /* Highlight a row that itself matches the filter. */
42
+ .ROUTEMAP_MATCH {
43
+ background: var(--bulma-warning-light, #fff6d9) !important;
44
+ }
45
+ [data-theme="dark"] .ROUTEMAP_MATCH {
46
+ background: rgba(255, 221, 87, 0.20) !important;
47
+ }
48
+ .ROUTEMAP_MATCH .ROUTEMAP_NAME,
49
+ .ROUTEMAP_MATCH .ROUTEMAP_ROUTE {
50
+ color: var(--bulma-text-strong, #363636);
51
+ }
52
+ [data-theme="dark"] .ROUTEMAP_MATCH .ROUTEMAP_NAME,
53
+ [data-theme="dark"] .ROUTEMAP_MATCH .ROUTEMAP_ROUTE {
54
+ color: #1a1a1a;
55
+ }
56
+ .ROUTEMAP_TREE {
57
+ font-family: var(--bulma-family-monospace, monospace);
58
+ font-size: 0.85rem;
59
+ line-height: 1.5;
60
+ overflow: auto;
61
+ min-height: 6rem;
62
+ /* Modal fallback: cap and scroll. In the window the rule below
63
+ * makes it flex-fill instead. */
64
+ flex: 0 1 auto;
65
+ max-height: 68vh;
66
+ border: 1px solid var(--bulma-border-weak, #dbdbdb);
67
+ border-radius: 0.375rem;
68
+ padding: 0.75rem 1rem;
69
+ background: var(--bulma-scheme-main, #fff);
70
+ }
71
+ /* In the window: grow to fill the remaining height (after the filter
72
+ * and print row) and scroll internally. */
73
+ .yui-window-body .ROUTEMAP_TREE {
74
+ flex: 1 1 auto;
75
+ max-height: none;
76
+ }
77
+ .ROUTEMAP_UL {
78
+ list-style: none;
79
+ margin: 0;
80
+ padding-left: 1.1rem;
81
+ border-left: 1px dotted var(--bulma-border, #b5b5b5);
82
+ }
83
+ .ROUTEMAP_ROOT {
84
+ padding-left: 0;
85
+ border-left: none;
86
+ }
87
+ .ROUTEMAP_LI {
88
+ margin: 0.05rem 0;
89
+ }
90
+ .ROUTEMAP_ROW {
91
+ display: inline-flex;
92
+ align-items: center;
93
+ gap: 0.4rem;
94
+ padding: 0.05rem 0.25rem;
95
+ border-radius: 0.25rem;
96
+ }
97
+ .ROUTEMAP_ICON {
98
+ font-size: 0.95rem;
99
+ color: var(--bulma-text-weak, #7a7a7a);
100
+ }
101
+ a.ROUTEMAP_LINK {
102
+ text-decoration: none;
103
+ color: inherit;
104
+ }
105
+ a.ROUTEMAP_LINK:hover {
106
+ background: var(--bulma-background, #f5f5f5);
107
+ }
108
+ a.ROUTEMAP_LINK .ROUTEMAP_NAME {
109
+ font-weight: 600;
110
+ color: var(--bulma-text, inherit);
111
+ }
112
+ .ROUTEMAP_NAME {
113
+ color: var(--bulma-text, inherit);
114
+ }
115
+ .ROUTEMAP_ROUTE {
116
+ background: transparent;
117
+ color: var(--bulma-link, #485fc7);
118
+ padding: 0;
119
+ font-size: 0.8rem;
120
+ }
121
+ /* Implementing GClass pill (teal) — distinct from route (blue) and
122
+ * event (grey). */
123
+ .ROUTEMAP_GCLASS {
124
+ font-family: var(--bulma-family-monospace, monospace);
125
+ font-size: 0.7rem;
126
+ padding: 0.02rem 0.35rem;
127
+ border-radius: 0.25rem;
128
+ background: var(--bulma-link-light, #eff1fa);
129
+ color: var(--bulma-link-dark, #3850b7);
130
+ white-space: nowrap;
131
+ }
132
+ [data-theme="dark"] .ROUTEMAP_GCLASS {
133
+ background: rgba(120, 150, 255, 0.16);
134
+ color: #aab8ff;
135
+ }
136
+
137
+ /* Action event badge — explicit colours (not Bulma's is-light tag) so it
138
+ * stays legible in dark mode. */
139
+ .ROUTEMAP_EVENT {
140
+ font-family: var(--bulma-family-monospace, monospace);
141
+ font-size: 0.72rem;
142
+ padding: 0.02rem 0.4rem;
143
+ border-radius: 0.25rem;
144
+ background: var(--bulma-background, #f0f0f0);
145
+ color: var(--bulma-text-strong, #363636);
146
+ border: 1px solid var(--bulma-border-weak, #dbdbdb);
147
+ white-space: nowrap;
148
+ }
149
+ [data-theme="dark"] .ROUTEMAP_EVENT {
150
+ background: rgba(255, 255, 255, 0.10);
151
+ color: #e6e6e6;
152
+ border-color: rgba(255, 255, 255, 0.20);
153
+ }
154
+ @media (prefers-color-scheme: dark) {
155
+ :root:not([data-theme="light"]) .ROUTEMAP_EVENT {
156
+ background: rgba(255, 255, 255, 0.10);
157
+ color: #e6e6e6;
158
+ border-color: rgba(255, 255, 255, 0.20);
159
+ }
160
+ }
161
+ /* "You are here" — the row of the route the user is currently on. */
162
+ .ROUTEMAP_CURRENT {
163
+ background: var(--bulma-link-light, #eff1fa) !important;
164
+ outline: 1px solid var(--bulma-link, #485fc7);
165
+ }
166
+ [data-theme="dark"] .ROUTEMAP_CURRENT {
167
+ background: rgba(120, 150, 255, 0.16) !important;
168
+ outline-color: rgba(120, 150, 255, 0.55);
169
+ }
170
+ .ROUTEMAP_HERE {
171
+ font-size: 0.68rem;
172
+ font-weight: 700;
173
+ text-transform: uppercase;
174
+ letter-spacing: 0.03em;
175
+ padding: 0.02rem 0.4rem;
176
+ border-radius: 0.25rem;
177
+ background: var(--bulma-link, #485fc7);
178
+ color: var(--bulma-link-invert, #fff);
179
+ white-space: nowrap;
180
+ }
181
+ .ROUTEMAP_STRUCT .ROUTEMAP_NAME {
182
+ color: var(--bulma-text-weak, #7a7a7a);
183
+ text-transform: uppercase;
184
+ letter-spacing: 0.03em;
185
+ font-size: 0.78rem;
186
+ font-weight: 700;
187
+ }
188
+ .ROUTEMAP_HINT {
189
+ color: var(--bulma-text-weak, #7a7a7a);
190
+ }
191
+ .ROUTEMAP_ACTIONS {
192
+ display: flex;
193
+ justify-content: flex-end;
194
+ margin-top: 0.75rem;
195
+ }
196
+
197
+ /* Print only the tree. On print the button clones the tree into
198
+ * `.routemap-print-area` (off-screen) and sets body.routemap-printing;
199
+ * everything else is hidden regardless of where the window/modal lives. */
200
+ .routemap-print-area {
201
+ display: none;
202
+ }
203
+ @media print {
204
+ body.routemap-printing > *:not(.routemap-print-area) {
205
+ display: none !important;
206
+ }
207
+ body.routemap-printing .routemap-print-area {
208
+ display: block !important;
209
+ }
210
+ .routemap-print-area .ROUTEMAP_TREE {
211
+ max-height: none;
212
+ border: none;
213
+ overflow: visible;
214
+ font-size: 10pt;
215
+ }
216
+ .routemap-print-area .ROUTEMAP_ROUTE,
217
+ .routemap-print-area .ROUTEMAP_NAME {
218
+ color: #000 !important;
219
+ }
220
+ .routemap-print-area .ROUTEMAP_EVENT {
221
+ background: transparent !important;
222
+ color: #000 !important;
223
+ border-color: #999 !important;
224
+ }
225
+ }
@@ -0,0 +1,363 @@
1
+ /***********************************************************************
2
+ * shell_route_map.js
3
+ *
4
+ * "Site map" viewer for a C_YUI_SHELL app: renders the WHOLE
5
+ * navigation surface — the toolbar (including the account menu),
6
+ * the primary menu, its live dynamic tabs, and each view's
7
+ * declared sub-routes (topics / info / schema / focus topics) —
8
+ * as a printable, clickable tree, in DECLARATION order (see
9
+ * ROUTING.md). Meant to double as the app's basic documentation.
10
+ * Hosted in a resizable C_YUI_WINDOW (a modal is the fallback when
11
+ * C_YUI_WINDOW is not registered).
12
+ *
13
+ * Usage (e.g. from an "Account → Site map" menu action):
14
+ * import {yui_shell_show_route_map} from
15
+ * "@yuneta/gobj-ui/src/shell_route_map.js";
16
+ * yui_shell_show_route_map(shell, {t});
17
+ *
18
+ * Copyright (c) 2026, ArtGins.
19
+ * All Rights Reserved.
20
+ ***********************************************************************/
21
+ import "./shell_route_map.css";
22
+
23
+ import {
24
+ createElement2,
25
+ refresh_language,
26
+ gobj_create_service,
27
+ gobj_find_service,
28
+ gobj_start,
29
+ gobj_send_event,
30
+ is_gobj,
31
+ gclass_find_by_name,
32
+ log_error,
33
+ } from "@yuneta/gobj-js";
34
+
35
+ import {yui_shell_show_modal, yui_shell_popup_layer} from "./shell_modals.js";
36
+ import {yui_shell_nav_map} from "./c_yui_shell.js";
37
+ import {attach_clear} from "./yui_inputs.js";
38
+
39
+ import i18next from "i18next";
40
+
41
+ const WIN_NAME = "shell-route-map-window";
42
+
43
+ /* Modal fallback currently open (the window flavour is a service and
44
+ * is found by name; the modal is not, so it is tracked here to make
45
+ * the second call a TOGGLE for both flavours). */
46
+ let __open_modal__ = null;
47
+
48
+ /* Bring the "you are here" row into view once the map is mounted. */
49
+ function scroll_to_current($body)
50
+ {
51
+ let $cur = $body.querySelector(".ROUTEMAP_CURRENT");
52
+ if($cur && typeof $cur.scrollIntoView === "function") {
53
+ $cur.scrollIntoView({block: "center"});
54
+ }
55
+ }
56
+
57
+
58
+ /***************************************************************
59
+ * Render one nav node as an <li> (with a nested <ul> for children),
60
+ * preserving the given order. A node with a `route` is a live hash
61
+ * link; a node with an `event` is an action (shown, not fired); a
62
+ * structural/group node is plain text.
63
+ ***************************************************************/
64
+ function render_node(node)
65
+ {
66
+ let row = [];
67
+ if(node.icon && /^yi-[a-z0-9-]+$/.test(node.icon)) {
68
+ row.push(["span", {class: "icon ROUTEMAP_ICON"},
69
+ [["i", {class: node.icon}]]]);
70
+ }
71
+ row.push(["span", {class: "ROUTEMAP_NAME", i18n: node.label}, node.label]);
72
+ /* Documentation columns: route + implementing gclass + action event. */
73
+ if(node.route) {
74
+ row.push(["code", {class: "ROUTEMAP_ROUTE"}, node.route]);
75
+ }
76
+ if(node.gclass) {
77
+ row.push(["span", {class: "ROUTEMAP_GCLASS",
78
+ title: "gclass", "data-i18n-title": "gclass"},
79
+ node.gclass]);
80
+ }
81
+ if(node.event) {
82
+ row.push(["span", {class: "ROUTEMAP_EVENT"}, node.event]);
83
+ }
84
+ /* "You are here" — the node whose route best matches the current
85
+ * one (marked by the model; at most one). */
86
+ if(node.current) {
87
+ row.push(["span", {class: "ROUTEMAP_HERE", i18n: "you are here"},
88
+ "you are here"]);
89
+ }
90
+
91
+ let row_class = node.current ? " ROUTEMAP_CURRENT" : "";
92
+ let $row;
93
+ if(node.route) {
94
+ $row = createElement2(
95
+ ["a", {class: "ROUTEMAP_LINK ROUTEMAP_ROW" + row_class,
96
+ href: "#" + node.route,
97
+ title: node.route}, row]);
98
+ } else {
99
+ $row = createElement2(
100
+ ["span", {class: "ROUTEMAP_ROW ROUTEMAP_STRUCT" + row_class}, row]);
101
+ }
102
+
103
+ let kids = (node.children && node.children.length)
104
+ ? [createElement2(["ul", {class: "ROUTEMAP_UL"},
105
+ node.children.map(render_node)])]
106
+ : [];
107
+ return createElement2(["li", {class: "ROUTEMAP_LI"}, [$row].concat(kids)]);
108
+ }
109
+
110
+ /***************************************************************
111
+ * Filter a rendered <li>: visible when its own row matches `q`, when
112
+ * an ancestor matched (show the whole subtree of a match), or when a
113
+ * descendant matches (keep the ancestor path). Empty `q` shows all.
114
+ * Matches against the visible row text (name + route + event), so it
115
+ * honours the current translation. Returns whether the <li> is shown.
116
+ ***************************************************************/
117
+ function filter_li($li, q, ancestor_match)
118
+ {
119
+ let $row = $li.querySelector(":scope > .ROUTEMAP_ROW");
120
+ let text = $row ? ($row.textContent || "").toLowerCase() : "";
121
+ let self_match = q === "" || text.indexOf(q) >= 0;
122
+ let show_all = ancestor_match || self_match;
123
+
124
+ let any_child = false;
125
+ let $ul = $li.querySelector(":scope > ul");
126
+ if($ul) {
127
+ let kids = $ul.children;
128
+ for(let i = 0; i < kids.length; i++) {
129
+ if(filter_li(kids[i], q, show_all)) {
130
+ any_child = true;
131
+ }
132
+ }
133
+ }
134
+
135
+ let visible = show_all || any_child;
136
+ $li.style.display = visible ? "" : "none";
137
+ if($row) {
138
+ $row.classList.toggle("ROUTEMAP_MATCH", q !== "" && self_match);
139
+ }
140
+ return visible;
141
+ }
142
+
143
+ /***************************************************************
144
+ * Build the site-map body (tree + hint + print), and wire the
145
+ * print button and the link-jump behaviour. Links navigate
146
+ * natively; the shell's transient-overlay drain closes the host
147
+ * on a resting-route change. `on_jump()` closes the host for the
148
+ * one click that navigates nowhere (the current route).
149
+ ***************************************************************/
150
+ function build_body(shell, t, on_jump)
151
+ {
152
+ let map = yui_shell_nav_map(shell);
153
+ let children = [
154
+ {label: "toolbar", icon: "", route: "", event: "",
155
+ kind: "group", children: map.toolbar}
156
+ ].concat(map.nav);
157
+ /* Routes declared only in the route table (config.shell.routes)
158
+ * that no menu/toolbar item points at — root "/", URL-only action
159
+ * routes. Rendered last, as their own group. */
160
+ if(Array.isArray(map.other) && map.other.length) {
161
+ children.push({label: "other routes", icon: "", route: "", event: "",
162
+ kind: "group", children: map.other});
163
+ }
164
+ let root = {
165
+ label: map.brand.label || "app",
166
+ icon: "",
167
+ route: map.brand.route || "",
168
+ event: "",
169
+ current: !!map.brand.current,
170
+ children: children
171
+ };
172
+
173
+ let $tree = createElement2(
174
+ ["div", {class: "ROUTEMAP_TREE"}, [
175
+ createElement2(["ul", {class: "ROUTEMAP_UL ROUTEMAP_ROOT"},
176
+ [render_node(root)]])
177
+ ]]
178
+ );
179
+
180
+ /* Search filter: matching nodes plus their ancestor path (and the
181
+ * matched node's whole subtree) stay visible; the rest collapse. */
182
+ let $search = createElement2(
183
+ ["input", {class: "input is-small ROUTEMAP_SEARCH", type: "text",
184
+ placeholder: t("filter", {defaultValue: "Filter…"}),
185
+ "data-i18n-placeholder": "filter",
186
+ "aria-label": t("filter", {defaultValue: "Filter"}),
187
+ "data-i18n-aria-label": "filter"}]
188
+ );
189
+ let $search_ctrl = createElement2(
190
+ ["div", {class: "control ROUTEMAP_SEARCH_CTRL"}, [$search]]
191
+ );
192
+ attach_clear($search_ctrl, $search);
193
+
194
+ /* Live match counter next to the filter. */
195
+ let $count_n = createElement2(["span", {class: "ROUTEMAP_COUNT_N"}, ""]);
196
+ let $count = createElement2(
197
+ ["span", {class: "ROUTEMAP_COUNT is-size-7 has-text-grey is-hidden"},
198
+ [$count_n, ["span", {i18n: "matches"}, "matches"]]]
199
+ );
200
+ let $search_row = createElement2(
201
+ ["div", {class: "ROUTEMAP_SEARCH_ROW mb-2"}, [$search_ctrl, $count]]
202
+ );
203
+
204
+ $search.addEventListener("input", function() {
205
+ let q = ($search.value || "").trim().toLowerCase();
206
+ let $root_li = $tree.querySelector(".ROUTEMAP_ROOT > li");
207
+ if($root_li) {
208
+ filter_li($root_li, q, false);
209
+ }
210
+ if(q === "") {
211
+ $count.classList.add("is-hidden");
212
+ } else {
213
+ $count_n.textContent = $tree.querySelectorAll(".ROUTEMAP_MATCH").length;
214
+ $count.classList.remove("is-hidden");
215
+ }
216
+ });
217
+
218
+ let $body = createElement2(
219
+ ["div", {class: "C_YUI_SHELL_ROUTEMAP ROUTEMAP_BODY"}, [
220
+ ["p", {class: "ROUTEMAP_HINT is-size-7 mb-2", i18n: "site map hint"},
221
+ t("site map hint", {defaultValue:
222
+ "Every reachable position of the app is a URL. Click to jump."})],
223
+ $search_row,
224
+ $tree,
225
+ ["div", {class: "ROUTEMAP_ACTIONS"}, [
226
+ ["button", {class: "button is-small ROUTEMAP_PRINT",
227
+ i18n: "print"}, t("print", {defaultValue: "Print"})]
228
+ ]]
229
+ ]]
230
+ );
231
+
232
+ /* Print only the tree: clone it into an off-screen print area so
233
+ * the @media print rules can hide everything else, regardless of
234
+ * where the window/modal is mounted. */
235
+ let $print = $body.querySelector(".ROUTEMAP_PRINT");
236
+ if($print) {
237
+ $print.addEventListener("click", function() {
238
+ let $area = document.createElement("div");
239
+ $area.className = "routemap-print-area";
240
+ $area.appendChild($tree.cloneNode(true));
241
+ document.body.appendChild($area);
242
+ document.body.classList.add("routemap-printing");
243
+ try {
244
+ window.print();
245
+ } finally {
246
+ document.body.classList.remove("routemap-printing");
247
+ if($area.parentNode) {
248
+ $area.parentNode.removeChild($area);
249
+ }
250
+ }
251
+ });
252
+ }
253
+
254
+ /* A route link jumps there NATIVELY (browser push + hashchange —
255
+ * no preventDefault): a resting-route change then closes this
256
+ * window/modal through the shell's transient-overlay drain, which
257
+ * is deterministic (the old close-then-deferred-navigate raced the
258
+ * dismissal's history.back() and could land back where it
259
+ * started). A subpath or action-route jump keeps the map open —
260
+ * it doubles as a navigation panel. Clicking the route the user
261
+ * is ALREADY on navigates nowhere: close the host instead.
262
+ * Action nodes (no route) are documentation only and do not fire. */
263
+ $body.addEventListener("click", function(ev) {
264
+ let $link = ev.target && ev.target.closest &&
265
+ ev.target.closest(".ROUTEMAP_LINK");
266
+ if(!$link) {
267
+ return;
268
+ }
269
+ let href = $link.getAttribute("href");
270
+ if(href && typeof window !== "undefined" &&
271
+ window.location.hash === href &&
272
+ typeof on_jump === "function") {
273
+ ev.preventDefault();
274
+ on_jump();
275
+ }
276
+ });
277
+
278
+ refresh_language($body, t);
279
+ return $body;
280
+ }
281
+
282
+
283
+ /***************************************************************
284
+ * Show the site map. Toggles: a second call closes the open one.
285
+ ***************************************************************/
286
+ export function yui_shell_show_route_map(shell, opts)
287
+ {
288
+ let t = (opts && opts.t) || i18next.t.bind(i18next);
289
+
290
+ /* Toggle: an open site-map window/modal → close it. */
291
+ let existing = gobj_find_service(WIN_NAME, false);
292
+ if(existing && is_gobj(existing)) {
293
+ gobj_send_event(existing, "EV_CLOSE_WINDOW", {}, shell);
294
+ return null;
295
+ }
296
+ if(__open_modal__) {
297
+ let m = __open_modal__;
298
+ __open_modal__ = null;
299
+ if(typeof m.close === "function") {
300
+ m.close();
301
+ }
302
+ return null;
303
+ }
304
+
305
+ /* Preferred: a resizable, maximisable floating window. */
306
+ if(gclass_find_by_name("C_YUI_WINDOW") !== null) {
307
+ let win_ref = {gobj: null};
308
+ let $body = build_body(shell, t, function() {
309
+ if(win_ref.gobj && is_gobj(win_ref.gobj)) {
310
+ gobj_send_event(win_ref.gobj, "EV_CLOSE_WINDOW", {}, shell);
311
+ }
312
+ });
313
+ let $parent = yui_shell_popup_layer(shell) ||
314
+ (typeof document !== "undefined" &&
315
+ document.getElementById("top-layer")) || null;
316
+ let win = gobj_create_service(WIN_NAME, "C_YUI_WINDOW", {
317
+ $parent: $parent,
318
+ subscriber: null,
319
+ modal: false,
320
+ showMax: true,
321
+ showFooter: false,
322
+ resizable: true,
323
+ center: true,
324
+ auto_save_size_and_position: true,
325
+ width: 780,
326
+ height: 640,
327
+ logical_class: "ROUTEMAP_WINDOW",
328
+ title: "site map",
329
+ icon: "yi-bars",
330
+ body: $body,
331
+ manager: null
332
+ }, shell);
333
+ if(!win) {
334
+ log_error("C_YUI_SHELL: cannot create the site-map window");
335
+ return null;
336
+ }
337
+ win_ref.gobj = win;
338
+ gobj_start(win);
339
+ scroll_to_current($body);
340
+ return win;
341
+ }
342
+
343
+ /* Fallback: a modal (no C_YUI_WINDOW registered). */
344
+ let modal_ref = {modal: null};
345
+ let $body = build_body(shell, t, function() {
346
+ if(modal_ref.modal && typeof modal_ref.modal.close === "function") {
347
+ modal_ref.modal.close();
348
+ }
349
+ });
350
+ modal_ref.modal = yui_shell_show_modal(shell, $body, {
351
+ dialog: true,
352
+ logical_class: "ROUTEMAP_SHEET",
353
+ /* The KEY, not t(key): the modal renders `title` with
354
+ * data-i18n, so a pre-translated string would become the
355
+ * "key" and never re-translate. */
356
+ title: "site map",
357
+ t: t,
358
+ on_close: function() { __open_modal__ = null; }
359
+ });
360
+ __open_modal__ = modal_ref.modal;
361
+ scroll_to_current($body);
362
+ return modal_ref.modal;
363
+ }
package/src/tabulator.css CHANGED
@@ -99,6 +99,39 @@
99
99
  background-color: var(--bulma-background, #ebebeb) !important;
100
100
  }
101
101
 
102
+ /* ====================================================================
103
+ * Opt-out of the whole-row hover wash + pointer cursor. For tables that
104
+ * select ONLY through the checkbox column (selectableRows:"highlight"),
105
+ * the hover highlight reads as a selection. Add `yui-no-row-hover` on the
106
+ * .tabulator element: unselected rows stay visually static on hover; a
107
+ * checkbox-selected row keeps its `.tabulator-selected` colour (the
108
+ * :not() guard leaves it alone). Colours mirror the striping rules so
109
+ * hover === the row's own background in every theme.
110
+ * ==================================================================== */
111
+ .tabulator.yui-no-row-hover .tabulator-row.tabulator-selectable:hover {
112
+ cursor: default !important;
113
+ }
114
+ .tabulator.yui-no-row-hover
115
+ .tabulator-row.tabulator-row-odd.tabulator-selectable:not(.tabulator-selected):hover {
116
+ background-color: var(--bulma-scheme-main, #fff) !important;
117
+ color: var(--bulma-body-color) !important;
118
+ }
119
+ .tabulator.yui-no-row-hover
120
+ .tabulator-row.tabulator-row-even.tabulator-selectable:not(.tabulator-selected):hover {
121
+ background-color: var(--bulma-scheme-main-ter, #f5f5f5) !important;
122
+ color: var(--bulma-body-color) !important;
123
+ }
124
+ [data-theme=dark] .tabulator.yui-no-row-hover
125
+ .tabulator-row.tabulator-row-odd.tabulator-selectable:not(.tabulator-selected):hover {
126
+ background-color: var(--bulma-body-background-color) !important;
127
+ color: var(--bulma-body-color) !important;
128
+ }
129
+ [data-theme=dark] .tabulator.yui-no-row-hover
130
+ .tabulator-row.tabulator-row-even.tabulator-selectable:not(.tabulator-selected):hover {
131
+ background-color: var(--bulma-background) !important;
132
+ color: var(--bulma-body-color) !important;
133
+ }
134
+
102
135
  /*
103
136
  * Tabulator dark theme
104
137
  * Activated by html[data-theme=dark] (set by the app's theme switcher)
@@ -143,6 +176,30 @@
143
176
  color: var(--bulma-text) !important;
144
177
  }
145
178
 
179
+ /*
180
+ * The CELL EDITOR in dark theme.
181
+ *
182
+ * Tabulator gives its editor input no colour of its own, so it inherits the
183
+ * browser default: BLACK text on the dark cell — the value disappears the
184
+ * moment you click into it, and comes back when the field loses focus. Same
185
+ * for a header-filter input and for a <select>.
186
+ */
187
+ [data-theme=dark] .tabulator-row .tabulator-cell.tabulator-editing input,
188
+ [data-theme=dark] .tabulator-row .tabulator-cell.tabulator-editing select,
189
+ [data-theme=dark] .tabulator-row .tabulator-cell.tabulator-editing textarea,
190
+ [data-theme=dark] .tabulator .tabulator-header .tabulator-header-filter input,
191
+ [data-theme=dark] .tabulator .tabulator-header .tabulator-header-filter select {
192
+ background-color: var(--bulma-scheme-main, #16181d);
193
+ color: var(--bulma-body-color, #e6e6e6) !important;
194
+ caret-color: var(--bulma-body-color, #e6e6e6);
195
+ border-color: var(--bulma-border);
196
+ }
197
+
198
+ [data-theme=dark] .tabulator-row .tabulator-cell.tabulator-editing input::placeholder,
199
+ [data-theme=dark] .tabulator .tabulator-header .tabulator-header-filter input::placeholder {
200
+ color: var(--bulma-text-weak, #8a8f98);
201
+ }
202
+
146
203
  [data-theme=dark] .tabulator-row.tabulator-selected {
147
204
  background-color: var(--bulma-link) !important;
148
205
  color: var(--bulma-link-invert) !important;
@@ -209,6 +266,16 @@
209
266
  [data-theme=system] .tabulator-row.tabulator-selectable:hover {
210
267
  background-color: var(--bulma-background) !important;
211
268
  }
269
+ [data-theme=system] .tabulator.yui-no-row-hover
270
+ .tabulator-row.tabulator-row-odd.tabulator-selectable:not(.tabulator-selected):hover {
271
+ background-color: var(--bulma-body-background-color) !important;
272
+ color: var(--bulma-body-color) !important;
273
+ }
274
+ [data-theme=system] .tabulator.yui-no-row-hover
275
+ .tabulator-row.tabulator-row-even.tabulator-selectable:not(.tabulator-selected):hover {
276
+ background-color: var(--bulma-background) !important;
277
+ color: var(--bulma-body-color) !important;
278
+ }
212
279
  [data-theme=system] .tabulator-row.tabulator-selected {
213
280
  background-color: var(--bulma-link) !important;
214
281
  color: var(--bulma-link-invert) !important;