@yuneta/gobj-ui 2.6.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 (56) hide show
  1. package/README.md +331 -11
  2. package/dist/gobj-ui.cjs.js +21386 -17919
  3. package/dist/gobj-ui.es.js +26059 -22613
  4. package/index.js +45 -9
  5. package/package.json +4 -4
  6. package/src/c_g6_nodes_tree.js +128 -40
  7. package/src/c_yui_form.css +9 -0
  8. package/src/c_yui_form.js +113 -53
  9. package/src/c_yui_gobj_tree_js.js +58 -36
  10. package/src/c_yui_json.css +139 -0
  11. package/src/c_yui_json.js +928 -0
  12. package/src/c_yui_json_graph.js +110 -20
  13. package/src/c_yui_map.js +18 -36
  14. package/src/c_yui_nav.js +6 -9
  15. package/src/c_yui_period.css +184 -0
  16. package/src/c_yui_period.js +1441 -0
  17. package/src/c_yui_shell.css +120 -0
  18. package/src/c_yui_shell.js +672 -111
  19. package/src/c_yui_treedb_graph.js +639 -37
  20. package/src/c_yui_treedb_schema.js +478 -0
  21. package/src/c_yui_treedb_topic_with_form.css +18 -0
  22. package/src/c_yui_treedb_topic_with_form.js +153 -103
  23. package/src/c_yui_treedb_topics.css +73 -0
  24. package/src/c_yui_treedb_topics.js +1070 -29
  25. package/src/c_yui_window.css +22 -0
  26. package/src/c_yui_window.js +182 -97
  27. package/src/c_yui_window_manager.js +38 -4
  28. package/src/json_view_helpers.js +214 -0
  29. package/src/json_view_helpers.test.js +135 -0
  30. package/src/route_map_model.js +317 -0
  31. package/src/route_map_model.test.js +209 -0
  32. package/src/route_resolver.js +24 -1
  33. package/src/route_resolver.test.js +40 -1
  34. package/src/shell_modals.js +162 -52
  35. package/src/shell_route_map.css +225 -0
  36. package/src/shell_route_map.js +363 -0
  37. package/src/tabulator.css +245 -0
  38. package/src/yui_dev.js +130 -8
  39. package/src/yui_frontend_view.js +106 -0
  40. package/src/yui_icons.css +62 -0
  41. package/src/yui_inputs.css +8 -3
  42. package/src/yui_inputs.js +51 -8
  43. package/src/yui_tabulator_i18n.js +128 -0
  44. package/src/yui_theme.js +147 -0
  45. package/src/yui_time.js +666 -0
  46. package/src/yui_time.test.js +330 -0
  47. package/src/yui_toolbar.css +28 -0
  48. package/src/yui_toolbar.js +86 -43
  49. package/src/c_yui_main.css +0 -501
  50. package/src/c_yui_main.js +0 -1623
  51. package/src/c_yui_routing.css +0 -18
  52. package/src/c_yui_routing.js +0 -837
  53. package/src/c_yui_tabs.js +0 -487
  54. package/src/themes.js +0 -215
  55. package/src/ytable.css +0 -63
  56. package/src/ytable.js +0 -505
@@ -0,0 +1,22 @@
1
+ /***********************************************************************
2
+ * c_yui_window.css
3
+ *
4
+ * Styles C_YUI_WINDOW tags on its chrome. Moved from
5
+ * c_yui_main.css (2.6.1) so windows are self-contained and no
6
+ * longer depend on the legacy stack's stylesheet.
7
+ *
8
+ * Copyright (c) 2026, ArtGins.
9
+ * All Rights Reserved.
10
+ ***********************************************************************/
11
+
12
+ .strong-shadow {
13
+ box-shadow:
14
+ 0 1em 2em -0.125em hsla(var(--bulma-shadow-h), var(--bulma-shadow-s), var(--bulma-shadow-l), 0.3),
15
+ 0 0 0 2px hsla(var(--bulma-shadow-h), var(--bulma-shadow-s), var(--bulma-shadow-l), 0.1) !important;
16
+ }
17
+
18
+ .without-border {
19
+ border: none !important;
20
+ box-shadow: none !important;
21
+ background: transparent !important;
22
+ }
@@ -27,6 +27,7 @@ import {
27
27
  kw_set_local_storage_value,
28
28
  gobj_read_attr,
29
29
  gobj_read_bool_attr,
30
+ gobj_read_str_attr,
30
31
  gobj_is_service,
31
32
  gobj_name,
32
33
  gobj_read_integer_attr,
@@ -34,18 +35,24 @@ import {
34
35
  gobj_destroy,
35
36
  gobj_write_bool_attr,
36
37
  gobj_write_integer_attr,
37
- gobj_unsubscribe_event,
38
38
  gobj_stop,
39
39
  gobj_stop_children,
40
40
  gobj_is_running,
41
41
  gobj_is_destroying,
42
42
  refresh_language,
43
+ empty_string,
43
44
  } from "@yuneta/gobj-js";
44
45
 
45
46
  import {t} from "i18next";
46
47
 
47
48
  import {yui_shell_confirm_yesnocancel} from "./shell_modals.js";
48
- import {yui_shell_of} from "./c_yui_shell.js";
49
+ import {
50
+ yui_shell_of,
51
+ yui_shell_register_overlay,
52
+ yui_shell_overlay_dismissed,
53
+ } from "./c_yui_shell.js";
54
+
55
+ import "./c_yui_window.css";
49
56
 
50
57
  /***************************************************************
51
58
  * Constants
@@ -76,7 +83,7 @@ SDATA(data_type_t.DTP_INTEGER, "y", 0, "100", "Y position"),
76
83
  SDATA(data_type_t.DTP_INTEGER, "width", 0, "700", "Width of the window"),
77
84
  SDATA(data_type_t.DTP_INTEGER, "height", 0, "500", "Height of the window"),
78
85
  SDATA(data_type_t.DTP_BOOLEAN, "auto_save_size_and_position", 0, false, "Automatically save size and position"),
79
- SDATA(data_type_t.DTP_POINTER, "header", 0, null, "Can be a gobj with $container or any createElement2() 'content' parameter"),
86
+ SDATA(data_type_t.DTP_POINTER, "header", 0, null, "Title-bar content, overriding the default `icon`+`title` strip: a gobj with $container or any createElement2() 'content' parameter. Only for a bar that is more than a title (e.g. a toolbar)"),
80
87
  SDATA(data_type_t.DTP_POINTER, "body", 0, null, "Can be a gobj with $container or any createElement2() 'content' parameter"),
81
88
  SDATA(data_type_t.DTP_POINTER, "footer", 0, null, "Can be a gobj with $container or any createElement2() 'content' parameter"),
82
89
  SDATA(data_type_t.DTP_BOOLEAN, "center", 0, true, "Center the window"),
@@ -86,17 +93,21 @@ SDATA(data_type_t.DTP_BOOLEAN, "resizable", 0, true, "Allow resizing"),
86
93
  SDATA(data_type_t.DTP_BOOLEAN, "showFooter", 0, true, "Show footer"),
87
94
  SDATA(data_type_t.DTP_BOOLEAN, "openMaximized",0, false, "Open the window maximized"),
88
95
  SDATA(data_type_t.DTP_BOOLEAN, "showMax", 0, true, "Show maximize button"),
89
- SDATA(data_type_t.DTP_BOOLEAN, "showMin", 0, true, "Show minimize (roll-up / shade) button"),
96
+ SDATA(data_type_t.DTP_BOOLEAN, "showMin", 0, true, "Show minimize (to dock) button; ignored without a `manager`"),
90
97
  SDATA(data_type_t.DTP_BOOLEAN, "maximized", 0, false, "Flag to indicate if maximized"),
91
98
  SDATA(data_type_t.DTP_JSON, "window_style", 0, "{}", "Override window style"),
92
99
  SDATA(data_type_t.DTP_POINTER, "on_close", 0, null, "Callback on destroy"),
93
100
  SDATA(data_type_t.DTP_POINTER, "manager", 0, null, "Optional C_YUI_WINDOW_MANAGER (gobj or service name) for dock/taskbar"),
94
- SDATA(data_type_t.DTP_STRING, "title", 0, "", "Window title, shown on the dock chip"),
95
- SDATA(data_type_t.DTP_STRING, "icon", 0, "", "Dock-chip icon: a yi-* class name or inline SVG (by window type)"),
101
+ SDATA(data_type_t.DTP_STRING, "logical_class",0, "", "Logical UPPER_SNAKE class(es) added to the window root, so the app can target THIS window exactly (e.g. 'TRANGER_KEYS_WINDOW')"),
102
+ SDATA(data_type_t.DTP_STRING, "title_prefix", 0, "", "Optional DATA half of the title (a topic/service/marker name), shown before `title` and never translated. Use it instead of composing `${name} · ${t(key)}` into `title`, which cannot re-translate"),
103
+ SDATA(data_type_t.DTP_STRING, "title", 0, "", "Window title, an i18n KEY: painted in the title bar (unless `header` overrides it) and on the dock chip. Pass the key, not t(key), or it cannot re-translate"),
104
+ SDATA(data_type_t.DTP_STRING, "icon", 0, "", "Window icon (by window type), leading the title bar and the dock chip: a yi-* class name or inline SVG"),
96
105
  // TODO pendiente focus modal keyboard
97
106
  SDATA(data_type_t.DTP_POINTER, "focus", 0, null, "Brings focus to the element, can be a number or selector"),
98
107
  SDATA(data_type_t.DTP_BOOLEAN, "modal", 0, false, "Enable modal mode"),
99
108
  SDATA(data_type_t.DTP_BOOLEAN, "keyboard", 0, true, "Close window on ESC if not modal"),
109
+ SDATA(data_type_t.DTP_BOOLEAN, "back_dismissable", 0, true, "Browser Back closes this window (floating overlays only; ignored when it has a `manager`)"),
110
+ SDATA(data_type_t.DTP_POINTER, "back_overlay", 0, null, "Internal: overlay-history entry (Back-button integration)"),
100
111
  SDATA(data_type_t.DTP_POINTER, "$container", 0, null, "Internal: Window container element"),
101
112
  SDATA(data_type_t.DTP_STRING, "window_id", 0, "", "Internal: Window ID"),
102
113
  SDATA(data_type_t.DTP_POINTER, "win_resize_handler", 0, null, "Internal: native window 'resize' listener"),
@@ -105,7 +116,6 @@ SDATA_END()
105
116
 
106
117
  let PRIVATE_DATA = {
107
118
  prevSize: 0,
108
- preShadeHeight: 0,
109
119
  };
110
120
 
111
121
  let __gclass__ = null;
@@ -135,12 +145,15 @@ function mt_create(gobj)
135
145
 
136
146
  let window_id = "window-" + clean_name(gobj_short_name(gobj));
137
147
  gobj_write_attr(gobj, "window_id", window_id);
138
- build_ui(gobj);
139
148
 
140
149
  /* Optional window manager (dock/taskbar): register, and raise/
141
- * highlight on any pointer press. Absent the window is a plain
142
- * floating window and minimize falls back to shade in place. */
150
+ * highlight on any pointer press. Resolved BEFORE build_ui: minimize
151
+ * means "send to the dock", so without a manager there is nowhere to
152
+ * minimize TO and build_ui must not paint the button at all. */
143
153
  let manager = resolve_manager(gobj);
154
+
155
+ build_ui(gobj);
156
+
144
157
  if(manager) {
145
158
  let $c = gobj_read_attr(gobj, "$container");
146
159
  if($c) {
@@ -151,21 +164,44 @@ function mt_create(gobj)
151
164
  }
152
165
  }, true);
153
166
  }
167
+ /* The chip gets the SPLIT halves so its label re-translates
168
+ * like the title bar: `title_key` is the i18n KEY (the chip
169
+ * renders it with data-i18n, so refresh_language reaches it),
170
+ * `title` the translated fallback text so the chip is born in
171
+ * the current language (the manager has no translator of its
172
+ * own), `title_prefix` the DATA half, never translated. */
173
+ let chip_title = composed_title(gobj);
154
174
  gobj_send_event(manager, "EV_REGISTER_WINDOW", {
155
175
  window: gobj,
156
- title: gobj_read_attr(gobj, "title") || gobj_short_name(gobj),
176
+ title: chip_title ? chip_title : gobj_short_name(gobj),
177
+ title_key: gobj_read_str_attr(gobj, "title") || "",
178
+ title_kind_text: empty_string(gobj_read_str_attr(gobj, "title"))
179
+ ? "" : t(gobj_read_str_attr(gobj, "title")),
180
+ title_prefix: gobj_read_str_attr(gobj, "title_prefix") || "",
157
181
  icon: gobj_read_attr(gobj, "icon") || "",
158
182
  }, gobj);
183
+ } else if(gobj_read_bool_attr(gobj, "back_dismissable")) {
184
+ /* Floating overlay window (no dock manager): the browser Back
185
+ * button closes it, like a modal/popup. Dock-managed windows
186
+ * are persistent workspace surfaces and are left out. Retired
187
+ * in mt_destroy (covers every teardown path). */
188
+ let shell = yui_shell_of(gobj);
189
+ if(shell) {
190
+ let overlay = yui_shell_register_overlay(
191
+ shell, function() { close_window(gobj); }
192
+ );
193
+ gobj_write_attr(gobj, "back_overlay", overlay);
194
+ }
159
195
  }
160
196
 
161
197
  /* Keep the window inside the viewport on a breakpoint change.
162
- * Wired in mt_create (NOT mt_start) on purpose: windows are
163
- * often created via gobj_create_service WITHOUT being started
164
- * (e.g. setup_dev, the connection-info window), so mt_start
165
- * never runsthe legacy __yui_main__/EV_RESIZE path is dead
166
- * under C_YUI_SHELL too. A native 'resize' listener is
167
- * start-independent and reuses handleResize() (clamp-to-screen
168
- * + optional re-center). Detached in mt_destroy. */
198
+ * The browser's own 'resize' is the source: this IS the OS
199
+ * notification, and handleResize() clamps to screen (+ optional
200
+ * re-center). The legacy path subscribing to EV_RESIZE from a
201
+ * C_YUI_MAIN "__yui_main__" service was retired: C_YUI_SHELL has
202
+ * no such service, so it never fired under v2, and it duplicated
203
+ * this listener wherever an app did provide one. Detached in
204
+ * mt_destroy. */
169
205
  let on_win_resize = function() {
170
206
  handleResize(gobj);
171
207
  };
@@ -173,29 +209,6 @@ function mt_create(gobj)
173
209
  window.addEventListener("resize", on_win_resize);
174
210
  }
175
211
 
176
- /***************************************************************
177
- * Framework Method: Start
178
- ***************************************************************/
179
- function mt_start(gobj)
180
- {
181
- let __yui_main__ = gobj_find_service("__yui_main__", false);
182
- if(__yui_main__) {
183
- gobj_subscribe_event(__yui_main__, "EV_RESIZE", {}, gobj);
184
- }
185
- }
186
-
187
- /***************************************************************
188
- * Framework Method: Stop
189
- ***************************************************************/
190
- function mt_stop(gobj)
191
- {
192
- // TODO quita esto para chequear el fallo dl_subscribings not implemented
193
- let __yui_main__ = gobj_find_service("__yui_main__", false);
194
- if(__yui_main__) {
195
- gobj_unsubscribe_event(__yui_main__, "EV_RESIZE", {}, gobj);
196
- }
197
- }
198
-
199
212
  /***************************************************************
200
213
  * Framework Method: Destroy
201
214
  ***************************************************************/
@@ -206,6 +219,17 @@ function mt_destroy(gobj)
206
219
  gobj_send_event(manager, "EV_UNREGISTER_WINDOW", {window: gobj}, gobj);
207
220
  }
208
221
 
222
+ /* Retire the overlay-history entry, if this was a Back-dismissable
223
+ * floating window (no-op when Back itself triggered the teardown). */
224
+ let back_overlay = gobj_read_attr(gobj, "back_overlay");
225
+ if(back_overlay) {
226
+ let shell = yui_shell_of(gobj);
227
+ if(shell) {
228
+ yui_shell_overlay_dismissed(shell, back_overlay);
229
+ }
230
+ gobj_write_attr(gobj, "back_overlay", null);
231
+ }
232
+
209
233
  let on_win_resize = gobj_read_attr(gobj, "win_resize_handler");
210
234
  if(on_win_resize) {
211
235
  window.removeEventListener("resize", on_win_resize);
@@ -241,6 +265,16 @@ function ensure_window_style()
241
265
  -webkit-user-select: none; user-select: none;
242
266
  }
243
267
  .yui-window-titlebar-controls { display: flex; align-items: center; gap: 2px; padding-left: 6px; }
268
+ .yui-window-title { white-space: nowrap; }
269
+ .yui-window-title .icon { flex: 0 0 auto; }
270
+ .yui-window-title svg { width: 14px; height: 14px; display: block; }
271
+ /* Separator between the data half and the translated kind half. CSS,
272
+ * not a text node: createElement2 trims text nodes and eats the spaces. */
273
+ .WINDOW_TITLE_PREFIX + .WINDOW_TITLE_KIND::before {
274
+ content: "·";
275
+ padding: 0 0.4em;
276
+ color: var(--bulma-text-weak);
277
+ }
244
278
  .yui-wc {
245
279
  width: 30px; height: 26px; display: inline-flex; align-items: center; justify-content: center;
246
280
  padding: 0; border: 0; background: transparent; color: var(--bulma-text); cursor: pointer;
@@ -249,9 +283,6 @@ function ensure_window_style()
249
283
  .yui-wc:hover { background: var(--bulma-scheme-main-ter); color: var(--bulma-text-strong); }
250
284
  .yui-wc.wc-close:hover { background: #e0364a; color: #fff; }
251
285
  .yui-wc svg { width: 15px; height: 15px; display: block; }
252
- .C_YUI_WINDOW.is-shaded .yui-window-body,
253
- .C_YUI_WINDOW.is-shaded .yui-window-footer { display: none !important; }
254
- .C_YUI_WINDOW.is-shaded { height: auto !important; }
255
286
  .yui-window-resize { color: var(--bulma-text-weak); opacity: 0.55; }
256
287
  .yui-window-resize:hover { opacity: 1; color: var(--bulma-text); }
257
288
  @media (max-width: ${MOBILE_MAX}px) {
@@ -268,6 +299,81 @@ function ensure_window_style()
268
299
  document.head.appendChild($style);
269
300
  }
270
301
 
302
+ /************************************************************
303
+ * The title bar's default content: `icon` + `title_prefix` +
304
+ * `title`, used when the caller supplies no `header`. Until this
305
+ * existed, `title` only reached the dock chip, so a window without
306
+ * a hand-rolled header painted an EMPTY bar — every caller that
307
+ * wanted a title built the same icon+text strip itself, and the
308
+ * ones that didn't (Keys, Raw JSON) ended up anonymous or titled
309
+ * inside their body.
310
+ *
311
+ * `icon` follows the dock chip's convention: inline SVG when it
312
+ * starts with '<', otherwise a yi-* class name.
313
+ *
314
+ * The two halves are separate text nodes on purpose. Titles here are
315
+ * nearly always "<what> · <kind>" — `raw_tracks · keys` — and
316
+ * composing that into one string is what makes a title untranslatable:
317
+ * `${topic} · ${t("keys")}` is not an i18n key, so i18next answers it
318
+ * with itself and it stays in the language it was built in for the
319
+ * life of the window. Split, the DATA half (`title_prefix`) is left
320
+ * alone and the KIND half carries its key, so a host
321
+ * refresh_language() re-translates just that one. The separator is
322
+ * CSS (::before), never a text node: createElement2 TRIMS text nodes
323
+ * and would eat the spaces around it.
324
+ ************************************************************/
325
+ function build_default_header(gobj)
326
+ {
327
+ let title = gobj_read_str_attr(gobj, "title");
328
+ let prefix = gobj_read_str_attr(gobj, "title_prefix");
329
+ if(empty_string(title) && empty_string(prefix)) {
330
+ return null;
331
+ }
332
+
333
+ let icon = gobj_read_str_attr(gobj, "icon");
334
+ let items = [];
335
+ if(!empty_string(icon)) {
336
+ if(icon.charAt(0) === '<') {
337
+ items.push(['span', {class: 'icon'}, icon]);
338
+ } else {
339
+ items.push(['span', {class: 'icon'}, [['i', {class: icon}]]]);
340
+ }
341
+ }
342
+ if(!empty_string(prefix)) {
343
+ items.push(
344
+ ['span', {class: 'WINDOW_TITLE_PREFIX has-text-weight-semibold'},
345
+ prefix]
346
+ );
347
+ }
348
+ if(!empty_string(title)) {
349
+ items.push(
350
+ ['span', {class: 'WINDOW_TITLE_KIND has-text-weight-semibold',
351
+ i18n: title}, title]
352
+ );
353
+ }
354
+
355
+ return createElement2(
356
+ ['span', {class: 'WINDOW_TITLE yui-window-title icon-text ml-1'}, items]
357
+ );
358
+ }
359
+
360
+ /************************************************************
361
+ * The window's title as ONE plain string, for consumers that
362
+ * cannot hold markup: the dock chip's label and its `title`
363
+ * tooltip. Same two halves as the bar, joined here.
364
+ ************************************************************/
365
+ function composed_title(gobj)
366
+ {
367
+ let title = gobj_read_str_attr(gobj, "title");
368
+ let prefix = gobj_read_str_attr(gobj, "title_prefix");
369
+ let kind = empty_string(title) ? "" : t(title);
370
+
371
+ if(!empty_string(prefix) && !empty_string(kind)) {
372
+ return `${prefix} · ${kind}`;
373
+ }
374
+ return empty_string(prefix) ? kind : prefix;
375
+ }
376
+
271
377
  /************************************************************
272
378
  * Below the mobile breakpoint the window is a full-screen
273
379
  * sheet (no float / drag / resize).
@@ -296,40 +402,17 @@ function resolve_manager(gobj)
296
402
  }
297
403
 
298
404
  /************************************************************
299
- * Minimize: with a manager, send the window to the dock;
300
- * otherwise "shade" it in place. Called by the minimize button.
405
+ * Minimize: send the window to the dock. Only reachable when
406
+ * a manager exists the button is not painted otherwise.
301
407
  ************************************************************/
302
408
  function do_minimize(gobj)
303
409
  {
304
410
  let manager = gobj_read_pointer_attr(gobj, "manager");
305
- if(manager) {
306
- gobj_send_event(manager, "EV_MINIMIZE_WINDOW", {window: gobj}, gobj);
307
- } else {
308
- toggle_shade(gobj);
309
- }
310
- }
311
-
312
- /************************************************************
313
- * Minimize as "shade": roll the window up to its title bar
314
- * in place (self-contained; no window manager needed). Toggles.
315
- ************************************************************/
316
- function toggle_shade(gobj)
317
- {
318
- let priv = gobj.priv;
319
- let $container = gobj_read_attr(gobj, "$container");
320
- if(!$container) {
411
+ if(!manager) {
412
+ log_error(`${gobj_short_name(gobj)}: minimize without a window manager`);
321
413
  return;
322
414
  }
323
- if($container.classList.contains("is-shaded")) {
324
- $container.classList.remove("is-shaded");
325
- if(priv.preShadeHeight) {
326
- $container.style.height = parseInt(priv.preShadeHeight) + 'px';
327
- }
328
- } else {
329
- priv.preShadeHeight = $container.getBoundingClientRect().height;
330
- $container.classList.add("is-shaded");
331
- $container.style.height = "auto";
332
- }
415
+ gobj_send_event(manager, "EV_MINIMIZE_WINDOW", {window: gobj}, gobj);
333
416
  }
334
417
 
335
418
  /************************************************************
@@ -344,6 +427,9 @@ function build_ui(gobj)
344
427
  if(is_gobj(header)) {
345
428
  header = gobj_read_attr(header, "$container");
346
429
  }
430
+ if(!header) {
431
+ header = build_default_header(gobj);
432
+ }
347
433
  let body = gobj_read_attr(gobj, "body");
348
434
  if(is_gobj(body)) {
349
435
  body = gobj_read_attr(body, "$container");
@@ -411,16 +497,24 @@ function build_ui(gobj)
411
497
  };
412
498
  Object.assign(window_style, gobj_read_attr(gobj, "window_style"));
413
499
 
500
+ /* Logical (UPPER_SNAKE) block names, per the repo's DOM convention:
501
+ * uppercase identifies the block, lowercase styles it. `logical_class`
502
+ * is the CALLER's name for this particular window (e.g.
503
+ * TRANGER_KEYS_WINDOW) — several windows share the C_YUI_WINDOW class,
504
+ * only this tells them apart in the Inspector or in a selector. */
505
+ let logical = gobj_read_str_attr(gobj, "logical_class");
506
+
414
507
  let $container = createElement2(
415
508
  ['div', {
416
- class: 'C_YUI_WINDOW strong-shadow is-flex is-flex-direction-column' + (mobile ? ' is-mobile-sheet' : ''),
509
+ class: 'C_YUI_WINDOW' + (logical ? ' ' + logical : '') +
510
+ ' strong-shadow is-flex is-flex-direction-column' + (mobile ? ' is-mobile-sheet' : ''),
417
511
  style: window_style}, [
418
512
 
419
513
  /*----------------------------*
420
514
  * Header
421
515
  *----------------------------*/
422
516
  ['div', {
423
- class: 'yui-window-header p-1 is-flex-shrink-0 is-flex is-flex-nowrap is-justify-content-space-between is-align-items-center', style: 'border-bottom:1px solid var(--bulma-border); cursor:move; box-sizing: border-box;'
517
+ class: 'WINDOW_HEADER yui-window-header p-1 is-flex-shrink-0 is-flex is-flex-nowrap is-justify-content-space-between is-align-items-center', style: 'border-bottom:1px solid var(--bulma-border); cursor:move; box-sizing: border-box;'
424
518
  }, [
425
519
  /* Custom header content: a single-row, horizontally
426
520
  * scrollable strip. It takes the remaining width and
@@ -442,14 +536,18 @@ function build_ui(gobj)
442
536
  ]],
443
537
  /* Window controls: pinned top-right, never shrink,
444
538
  * never wrap, always on top so the click always lands.
445
- * Minimize (shade) · maximize/restore · close. */
446
- ['div', {class: 'yui-window-titlebar-controls is-flex-shrink-0', style: 'position:relative; z-index:1; cursor:default;'}, [
539
+ * Minimize (to dock) · maximize/restore · close.
540
+ * Minimize exists ONLY with a window manager: it means
541
+ * "send to the dock", and there is no such place without
542
+ * one. `manager` is already resolved (mt_create). */
543
+ ['div', {class: 'WINDOW_CONTROLS yui-window-titlebar-controls is-flex-shrink-0', style: 'position:relative; z-index:1; cursor:default;'}, [
447
544
  /*----------------------------*
448
- * Minimize (shade)
545
+ * Minimize (to dock)
449
546
  *----------------------------*/
450
547
  ['button', {
451
- class: 'yui-wc wc-min', type: 'button', 'aria-label': 'minimize',
452
- style: gobj_read_bool_attr(gobj, "showMin") ? '' : 'display:none;',
548
+ class: 'WINDOW_MIN yui-wc wc-min', type: 'button', 'aria-label': 'minimize',
549
+ style: (gobj_read_bool_attr(gobj, "showMin") &&
550
+ gobj_read_pointer_attr(gobj, "manager")) ? '' : 'display:none;',
453
551
  }, WC_MIN, {
454
552
  click: (evt) => {
455
553
  evt.stopPropagation();
@@ -460,7 +558,7 @@ function build_ui(gobj)
460
558
  * Maximize / restore
461
559
  *----------------------------*/
462
560
  ['button', {
463
- class: 'yui-wc wc-max', type: 'button', 'aria-label': 'maximize',
561
+ class: 'WINDOW_MAX yui-wc wc-max', type: 'button', 'aria-label': 'maximize',
464
562
  style: gobj_read_bool_attr(gobj, "showMax") ? '' : 'display:none;',
465
563
  }, WC_MAX, {
466
564
  click: (evt) => {
@@ -472,7 +570,7 @@ function build_ui(gobj)
472
570
  * Close
473
571
  *----------------------------*/
474
572
  ['button', {
475
- class: 'yui-wc wc-close', type: 'button', 'aria-label': 'close',
573
+ class: 'WINDOW_CLOSE yui-wc wc-close', type: 'button', 'aria-label': 'close',
476
574
  }, WC_CLOSE, {
477
575
  click: (evt) => {
478
576
  evt.stopPropagation();
@@ -521,7 +619,7 @@ function build_ui(gobj)
521
619
  * Body
522
620
  *----------------------------*/
523
621
  ['div', {
524
- class: 'yui-window-body is-flex-grow-1 p-1',
622
+ class: 'WINDOW_BODY yui-window-body is-flex-grow-1 p-1',
525
623
  style: {
526
624
  "position": "relative", // to resize button (absolute position) when no footer bar
527
625
  "overflow": "auto",
@@ -533,7 +631,7 @@ function build_ui(gobj)
533
631
  /*----------------------------*
534
632
  * Footer
535
633
  *----------------------------*/
536
- ['div', {class: 'yui-window-footer is-flex-shrink-0'}, [
634
+ ['div', {class: 'WINDOW_FOOTER yui-window-footer is-flex-shrink-0'}, [
537
635
  ['div', {
538
636
  class: 'is-justify-content-space-between is-align-items-center p-1',
539
637
  style: {
@@ -584,7 +682,7 @@ function build_ui(gobj)
584
682
  *----------------------------*/
585
683
  if(gobj_read_bool_attr(gobj, "resizable") && !gobj_read_bool_attr(gobj, "showFooter")) {
586
684
  let $resizable_btn = createElement2(['div', {
587
- class: 'without-border yui-window-resize',
685
+ class: 'WINDOW_RESIZE without-border yui-window-resize',
588
686
  style: {
589
687
  cursor: "nwse-resize",
590
688
  display: "flex",
@@ -1049,15 +1147,6 @@ function handleResize(gobj)
1049
1147
 
1050
1148
 
1051
1149
 
1052
- /************************************************************
1053
- *
1054
- ************************************************************/
1055
- function ac_resize(gobj, event, kw, src)
1056
- {
1057
- handleResize(gobj);
1058
- return 0;
1059
- }
1060
-
1061
1150
  /************************************************************
1062
1151
  *
1063
1152
  ************************************************************/
@@ -1108,8 +1197,6 @@ function ac_close(gobj, event, kw, src)
1108
1197
  *---------------------------------------------*/
1109
1198
  const gmt = {
1110
1199
  mt_create: mt_create,
1111
- mt_start: mt_start,
1112
- mt_stop: mt_stop,
1113
1200
  mt_destroy: mt_destroy
1114
1201
  };
1115
1202
 
@@ -1128,7 +1215,6 @@ function create_gclass(gclass_name)
1128
1215
  *---------------------------------------------*/
1129
1216
  const states = [
1130
1217
  ["ST_IDLE", [
1131
- ["EV_RESIZE", ac_resize, null],
1132
1218
  ["EV_REFRESH", ac_refresh, null],
1133
1219
  ["EV_SHOW", ac_show, null],
1134
1220
  ["EV_HIDE", ac_hide, null],
@@ -1143,7 +1229,6 @@ function create_gclass(gclass_name)
1143
1229
  ["EV_WINDOW_TO_CLOSE", event_flag_t.EVF_OUTPUT_EVENT|event_flag_t.EVF_NO_WARN_SUBS],
1144
1230
  ["EV_WINDOW_MOVED", event_flag_t.EVF_OUTPUT_EVENT|event_flag_t.EVF_NO_WARN_SUBS],
1145
1231
  ["EV_WINDOW_RESIZED", event_flag_t.EVF_OUTPUT_EVENT|event_flag_t.EVF_NO_WARN_SUBS],
1146
- ["EV_RESIZE", 0],
1147
1232
  ["EV_REFRESH", 0],
1148
1233
  ["EV_SHOW", 0],
1149
1234
  ["EV_HIDE", 0],
@@ -205,6 +205,10 @@ function ensure_dock_style()
205
205
  .yui-dock-icon svg { width: 14px; height: 14px; display: block; }
206
206
  .yui-dock-icon i { line-height: 1; }
207
207
  .yui-dock-label { flex: 0 0 auto; }
208
+ /* Split label halves (prefix = DATA, kind = translatable). The
209
+ * separator is CSS, never a text node — same rule as the window
210
+ * title bar (createElement2 trims text nodes). */
211
+ .yui-dock-label .yui-dock-label-prefix + .yui-dock-label-kind::before { content: " \\00b7 "; }
208
212
  .yui-dock-close {
209
213
  width: 18px; height: 18px; display: inline-flex; align-items: center; justify-content: center;
210
214
  padding: 0; margin-left: 1px; border: 0; border-radius: 4px;
@@ -428,7 +432,15 @@ function on_chip_click(gobj, entry)
428
432
 
429
433
 
430
434
  /************************************************************
431
- * EV_REGISTER_WINDOW { window, title }
435
+ * EV_REGISTER_WINDOW { window, title, title_key,
436
+ * title_kind_text, title_prefix, icon }
437
+ *
438
+ * `title` is the composed, already-translated fallback (also the
439
+ * tooltip). The label itself renders the SPLIT halves so it can
440
+ * change language: `title_prefix` (DATA, plain text) + the KIND
441
+ * half carrying `title_key` as data-i18n, born as
442
+ * `title_kind_text` (the manager has no translator of its own —
443
+ * the app's refresh_language() re-translates the key later).
432
444
  ************************************************************/
433
445
  function ac_register_window(gobj, event, kw, src)
434
446
  {
@@ -442,16 +454,38 @@ function ac_register_window(gobj, event, kw, src)
442
454
  apply_placement(gobj);
443
455
 
444
456
  let title = kw.title || gobj_name(win) || "window";
457
+ let title_key = kw.title_key || "";
458
+ let kind_text = kw.title_kind_text || "";
459
+ let prefix = kw.title_prefix || "";
460
+ let label_items = [];
461
+ if(prefix) {
462
+ label_items.push(['span', {class: 'yui-dock-label-prefix'}, prefix]);
463
+ }
464
+ if(title_key) {
465
+ label_items.push(
466
+ ['span', {class: 'yui-dock-label-kind', i18n: title_key},
467
+ kind_text || title_key]
468
+ );
469
+ }
470
+ if(label_items.length === 0) {
471
+ /* No split halves supplied (legacy caller): plain text. */
472
+ label_items.push(['span', {class: 'yui-dock-label-kind'}, title]);
473
+ }
445
474
 
446
475
  let entry = {gobj: win, $chip: null, minimized: false};
447
476
  /* A div (not a button) so it can hold the close button — a
448
477
  * button inside a button is invalid. The chip toggles; the ✕
449
478
  * closes the window (EV_CLOSE_WINDOW → the window's own teardown
450
- * → EV_UNREGISTER_WINDOW removes this chip). */
479
+ * → EV_UNREGISTER_WINDOW removes this chip).
480
+ * Tooltip: the composed string; when there is no DATA half it
481
+ * carries the key as data-i18n-title so it re-translates too. */
451
482
  let $chip = createElement2(
452
- ['div', {class: 'yui-dock-chip', role: 'button', tabindex: '0', title: title}, [
483
+ ['div', Object.assign(
484
+ {class: 'yui-dock-chip', role: 'button', tabindex: '0', title: title},
485
+ (!prefix && title_key) ? {'data-i18n-title': title_key} : {}
486
+ ), [
453
487
  chip_lead(kw.icon),
454
- ['span', {class: 'yui-dock-label'}, title],
488
+ ['span', {class: 'yui-dock-label'}, label_items],
455
489
  ['button', {class: 'yui-dock-close', type: 'button', 'aria-label': 'close'}, WC_X, {
456
490
  click: (evt) => {
457
491
  evt.stopPropagation();