@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
@@ -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,22 @@ 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";
49
54
 
50
55
  import "./c_yui_window.css";
51
56
 
@@ -78,7 +83,7 @@ SDATA(data_type_t.DTP_INTEGER, "y", 0, "100", "Y position"),
78
83
  SDATA(data_type_t.DTP_INTEGER, "width", 0, "700", "Width of the window"),
79
84
  SDATA(data_type_t.DTP_INTEGER, "height", 0, "500", "Height of the window"),
80
85
  SDATA(data_type_t.DTP_BOOLEAN, "auto_save_size_and_position", 0, false, "Automatically save size and position"),
81
- 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)"),
82
87
  SDATA(data_type_t.DTP_POINTER, "body", 0, null, "Can be a gobj with $container or any createElement2() 'content' parameter"),
83
88
  SDATA(data_type_t.DTP_POINTER, "footer", 0, null, "Can be a gobj with $container or any createElement2() 'content' parameter"),
84
89
  SDATA(data_type_t.DTP_BOOLEAN, "center", 0, true, "Center the window"),
@@ -88,17 +93,21 @@ SDATA(data_type_t.DTP_BOOLEAN, "resizable", 0, true, "Allow resizing"),
88
93
  SDATA(data_type_t.DTP_BOOLEAN, "showFooter", 0, true, "Show footer"),
89
94
  SDATA(data_type_t.DTP_BOOLEAN, "openMaximized",0, false, "Open the window maximized"),
90
95
  SDATA(data_type_t.DTP_BOOLEAN, "showMax", 0, true, "Show maximize button"),
91
- 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`"),
92
97
  SDATA(data_type_t.DTP_BOOLEAN, "maximized", 0, false, "Flag to indicate if maximized"),
93
98
  SDATA(data_type_t.DTP_JSON, "window_style", 0, "{}", "Override window style"),
94
99
  SDATA(data_type_t.DTP_POINTER, "on_close", 0, null, "Callback on destroy"),
95
100
  SDATA(data_type_t.DTP_POINTER, "manager", 0, null, "Optional C_YUI_WINDOW_MANAGER (gobj or service name) for dock/taskbar"),
96
- SDATA(data_type_t.DTP_STRING, "title", 0, "", "Window title, shown on the dock chip"),
97
- 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"),
98
105
  // TODO pendiente focus modal keyboard
99
106
  SDATA(data_type_t.DTP_POINTER, "focus", 0, null, "Brings focus to the element, can be a number or selector"),
100
107
  SDATA(data_type_t.DTP_BOOLEAN, "modal", 0, false, "Enable modal mode"),
101
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)"),
102
111
  SDATA(data_type_t.DTP_POINTER, "$container", 0, null, "Internal: Window container element"),
103
112
  SDATA(data_type_t.DTP_STRING, "window_id", 0, "", "Internal: Window ID"),
104
113
  SDATA(data_type_t.DTP_POINTER, "win_resize_handler", 0, null, "Internal: native window 'resize' listener"),
@@ -107,7 +116,6 @@ SDATA_END()
107
116
 
108
117
  let PRIVATE_DATA = {
109
118
  prevSize: 0,
110
- preShadeHeight: 0,
111
119
  };
112
120
 
113
121
  let __gclass__ = null;
@@ -137,12 +145,15 @@ function mt_create(gobj)
137
145
 
138
146
  let window_id = "window-" + clean_name(gobj_short_name(gobj));
139
147
  gobj_write_attr(gobj, "window_id", window_id);
140
- build_ui(gobj);
141
148
 
142
149
  /* Optional window manager (dock/taskbar): register, and raise/
143
- * highlight on any pointer press. Absent the window is a plain
144
- * 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. */
145
153
  let manager = resolve_manager(gobj);
154
+
155
+ build_ui(gobj);
156
+
146
157
  if(manager) {
147
158
  let $c = gobj_read_attr(gobj, "$container");
148
159
  if($c) {
@@ -153,21 +164,44 @@ function mt_create(gobj)
153
164
  }
154
165
  }, true);
155
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);
156
174
  gobj_send_event(manager, "EV_REGISTER_WINDOW", {
157
175
  window: gobj,
158
- 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") || "",
159
181
  icon: gobj_read_attr(gobj, "icon") || "",
160
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
+ }
161
195
  }
162
196
 
163
197
  /* Keep the window inside the viewport on a breakpoint change.
164
- * Wired in mt_create (NOT mt_start) on purpose: windows are
165
- * often created via gobj_create_service WITHOUT being started
166
- * (e.g. setup_dev, the connection-info window), so mt_start
167
- * never runsthe legacy __yui_main__/EV_RESIZE path is dead
168
- * under C_YUI_SHELL too. A native 'resize' listener is
169
- * start-independent and reuses handleResize() (clamp-to-screen
170
- * + 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. */
171
205
  let on_win_resize = function() {
172
206
  handleResize(gobj);
173
207
  };
@@ -175,29 +209,6 @@ function mt_create(gobj)
175
209
  window.addEventListener("resize", on_win_resize);
176
210
  }
177
211
 
178
- /***************************************************************
179
- * Framework Method: Start
180
- ***************************************************************/
181
- function mt_start(gobj)
182
- {
183
- let __yui_main__ = gobj_find_service("__yui_main__", false);
184
- if(__yui_main__) {
185
- gobj_subscribe_event(__yui_main__, "EV_RESIZE", {}, gobj);
186
- }
187
- }
188
-
189
- /***************************************************************
190
- * Framework Method: Stop
191
- ***************************************************************/
192
- function mt_stop(gobj)
193
- {
194
- // TODO quita esto para chequear el fallo dl_subscribings not implemented
195
- let __yui_main__ = gobj_find_service("__yui_main__", false);
196
- if(__yui_main__) {
197
- gobj_unsubscribe_event(__yui_main__, "EV_RESIZE", {}, gobj);
198
- }
199
- }
200
-
201
212
  /***************************************************************
202
213
  * Framework Method: Destroy
203
214
  ***************************************************************/
@@ -208,6 +219,17 @@ function mt_destroy(gobj)
208
219
  gobj_send_event(manager, "EV_UNREGISTER_WINDOW", {window: gobj}, gobj);
209
220
  }
210
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
+
211
233
  let on_win_resize = gobj_read_attr(gobj, "win_resize_handler");
212
234
  if(on_win_resize) {
213
235
  window.removeEventListener("resize", on_win_resize);
@@ -243,6 +265,16 @@ function ensure_window_style()
243
265
  -webkit-user-select: none; user-select: none;
244
266
  }
245
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
+ }
246
278
  .yui-wc {
247
279
  width: 30px; height: 26px; display: inline-flex; align-items: center; justify-content: center;
248
280
  padding: 0; border: 0; background: transparent; color: var(--bulma-text); cursor: pointer;
@@ -251,9 +283,6 @@ function ensure_window_style()
251
283
  .yui-wc:hover { background: var(--bulma-scheme-main-ter); color: var(--bulma-text-strong); }
252
284
  .yui-wc.wc-close:hover { background: #e0364a; color: #fff; }
253
285
  .yui-wc svg { width: 15px; height: 15px; display: block; }
254
- .C_YUI_WINDOW.is-shaded .yui-window-body,
255
- .C_YUI_WINDOW.is-shaded .yui-window-footer { display: none !important; }
256
- .C_YUI_WINDOW.is-shaded { height: auto !important; }
257
286
  .yui-window-resize { color: var(--bulma-text-weak); opacity: 0.55; }
258
287
  .yui-window-resize:hover { opacity: 1; color: var(--bulma-text); }
259
288
  @media (max-width: ${MOBILE_MAX}px) {
@@ -270,6 +299,81 @@ function ensure_window_style()
270
299
  document.head.appendChild($style);
271
300
  }
272
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
+
273
377
  /************************************************************
274
378
  * Below the mobile breakpoint the window is a full-screen
275
379
  * sheet (no float / drag / resize).
@@ -298,40 +402,17 @@ function resolve_manager(gobj)
298
402
  }
299
403
 
300
404
  /************************************************************
301
- * Minimize: with a manager, send the window to the dock;
302
- * 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.
303
407
  ************************************************************/
304
408
  function do_minimize(gobj)
305
409
  {
306
410
  let manager = gobj_read_pointer_attr(gobj, "manager");
307
- if(manager) {
308
- gobj_send_event(manager, "EV_MINIMIZE_WINDOW", {window: gobj}, gobj);
309
- } else {
310
- toggle_shade(gobj);
311
- }
312
- }
313
-
314
- /************************************************************
315
- * Minimize as "shade": roll the window up to its title bar
316
- * in place (self-contained; no window manager needed). Toggles.
317
- ************************************************************/
318
- function toggle_shade(gobj)
319
- {
320
- let priv = gobj.priv;
321
- let $container = gobj_read_attr(gobj, "$container");
322
- if(!$container) {
411
+ if(!manager) {
412
+ log_error(`${gobj_short_name(gobj)}: minimize without a window manager`);
323
413
  return;
324
414
  }
325
- if($container.classList.contains("is-shaded")) {
326
- $container.classList.remove("is-shaded");
327
- if(priv.preShadeHeight) {
328
- $container.style.height = parseInt(priv.preShadeHeight) + 'px';
329
- }
330
- } else {
331
- priv.preShadeHeight = $container.getBoundingClientRect().height;
332
- $container.classList.add("is-shaded");
333
- $container.style.height = "auto";
334
- }
415
+ gobj_send_event(manager, "EV_MINIMIZE_WINDOW", {window: gobj}, gobj);
335
416
  }
336
417
 
337
418
  /************************************************************
@@ -346,6 +427,9 @@ function build_ui(gobj)
346
427
  if(is_gobj(header)) {
347
428
  header = gobj_read_attr(header, "$container");
348
429
  }
430
+ if(!header) {
431
+ header = build_default_header(gobj);
432
+ }
349
433
  let body = gobj_read_attr(gobj, "body");
350
434
  if(is_gobj(body)) {
351
435
  body = gobj_read_attr(body, "$container");
@@ -413,16 +497,24 @@ function build_ui(gobj)
413
497
  };
414
498
  Object.assign(window_style, gobj_read_attr(gobj, "window_style"));
415
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
+
416
507
  let $container = createElement2(
417
508
  ['div', {
418
- 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' : ''),
419
511
  style: window_style}, [
420
512
 
421
513
  /*----------------------------*
422
514
  * Header
423
515
  *----------------------------*/
424
516
  ['div', {
425
- 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;'
426
518
  }, [
427
519
  /* Custom header content: a single-row, horizontally
428
520
  * scrollable strip. It takes the remaining width and
@@ -444,14 +536,18 @@ function build_ui(gobj)
444
536
  ]],
445
537
  /* Window controls: pinned top-right, never shrink,
446
538
  * never wrap, always on top so the click always lands.
447
- * Minimize (shade) · maximize/restore · close. */
448
- ['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;'}, [
449
544
  /*----------------------------*
450
- * Minimize (shade)
545
+ * Minimize (to dock)
451
546
  *----------------------------*/
452
547
  ['button', {
453
- class: 'yui-wc wc-min', type: 'button', 'aria-label': 'minimize',
454
- 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;',
455
551
  }, WC_MIN, {
456
552
  click: (evt) => {
457
553
  evt.stopPropagation();
@@ -462,7 +558,7 @@ function build_ui(gobj)
462
558
  * Maximize / restore
463
559
  *----------------------------*/
464
560
  ['button', {
465
- class: 'yui-wc wc-max', type: 'button', 'aria-label': 'maximize',
561
+ class: 'WINDOW_MAX yui-wc wc-max', type: 'button', 'aria-label': 'maximize',
466
562
  style: gobj_read_bool_attr(gobj, "showMax") ? '' : 'display:none;',
467
563
  }, WC_MAX, {
468
564
  click: (evt) => {
@@ -474,7 +570,7 @@ function build_ui(gobj)
474
570
  * Close
475
571
  *----------------------------*/
476
572
  ['button', {
477
- class: 'yui-wc wc-close', type: 'button', 'aria-label': 'close',
573
+ class: 'WINDOW_CLOSE yui-wc wc-close', type: 'button', 'aria-label': 'close',
478
574
  }, WC_CLOSE, {
479
575
  click: (evt) => {
480
576
  evt.stopPropagation();
@@ -523,7 +619,7 @@ function build_ui(gobj)
523
619
  * Body
524
620
  *----------------------------*/
525
621
  ['div', {
526
- class: 'yui-window-body is-flex-grow-1 p-1',
622
+ class: 'WINDOW_BODY yui-window-body is-flex-grow-1 p-1',
527
623
  style: {
528
624
  "position": "relative", // to resize button (absolute position) when no footer bar
529
625
  "overflow": "auto",
@@ -535,7 +631,7 @@ function build_ui(gobj)
535
631
  /*----------------------------*
536
632
  * Footer
537
633
  *----------------------------*/
538
- ['div', {class: 'yui-window-footer is-flex-shrink-0'}, [
634
+ ['div', {class: 'WINDOW_FOOTER yui-window-footer is-flex-shrink-0'}, [
539
635
  ['div', {
540
636
  class: 'is-justify-content-space-between is-align-items-center p-1',
541
637
  style: {
@@ -586,7 +682,7 @@ function build_ui(gobj)
586
682
  *----------------------------*/
587
683
  if(gobj_read_bool_attr(gobj, "resizable") && !gobj_read_bool_attr(gobj, "showFooter")) {
588
684
  let $resizable_btn = createElement2(['div', {
589
- class: 'without-border yui-window-resize',
685
+ class: 'WINDOW_RESIZE without-border yui-window-resize',
590
686
  style: {
591
687
  cursor: "nwse-resize",
592
688
  display: "flex",
@@ -1051,15 +1147,6 @@ function handleResize(gobj)
1051
1147
 
1052
1148
 
1053
1149
 
1054
- /************************************************************
1055
- *
1056
- ************************************************************/
1057
- function ac_resize(gobj, event, kw, src)
1058
- {
1059
- handleResize(gobj);
1060
- return 0;
1061
- }
1062
-
1063
1150
  /************************************************************
1064
1151
  *
1065
1152
  ************************************************************/
@@ -1110,8 +1197,6 @@ function ac_close(gobj, event, kw, src)
1110
1197
  *---------------------------------------------*/
1111
1198
  const gmt = {
1112
1199
  mt_create: mt_create,
1113
- mt_start: mt_start,
1114
- mt_stop: mt_stop,
1115
1200
  mt_destroy: mt_destroy
1116
1201
  };
1117
1202
 
@@ -1130,7 +1215,6 @@ function create_gclass(gclass_name)
1130
1215
  *---------------------------------------------*/
1131
1216
  const states = [
1132
1217
  ["ST_IDLE", [
1133
- ["EV_RESIZE", ac_resize, null],
1134
1218
  ["EV_REFRESH", ac_refresh, null],
1135
1219
  ["EV_SHOW", ac_show, null],
1136
1220
  ["EV_HIDE", ac_hide, null],
@@ -1145,7 +1229,6 @@ function create_gclass(gclass_name)
1145
1229
  ["EV_WINDOW_TO_CLOSE", event_flag_t.EVF_OUTPUT_EVENT|event_flag_t.EVF_NO_WARN_SUBS],
1146
1230
  ["EV_WINDOW_MOVED", event_flag_t.EVF_OUTPUT_EVENT|event_flag_t.EVF_NO_WARN_SUBS],
1147
1231
  ["EV_WINDOW_RESIZED", event_flag_t.EVF_OUTPUT_EVENT|event_flag_t.EVF_NO_WARN_SUBS],
1148
- ["EV_RESIZE", 0],
1149
1232
  ["EV_REFRESH", 0],
1150
1233
  ["EV_SHOW", 0],
1151
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();