@yuneta/gobj-ui 1.0.1 → 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 (46) hide show
  1. package/README.md +40 -363
  2. package/dist/gobj-ui.cjs.js +11154 -5506
  3. package/dist/gobj-ui.es.js +11131 -5508
  4. package/index.js +41 -14
  5. package/package.json +11 -9
  6. package/src/c_g6_nodes_tree.js +6 -1
  7. package/src/c_yui_form.js +1 -1
  8. package/src/c_yui_gobj_tree_js.js +1 -1
  9. package/src/c_yui_json_graph.js +1 -1
  10. package/src/c_yui_main.js +3 -3
  11. package/src/c_yui_map.js +6 -1
  12. package/src/c_yui_nav.js +881 -0
  13. package/src/c_yui_pager.js +545 -0
  14. package/src/c_yui_routing.css +1 -1
  15. package/src/c_yui_routing.js +1 -1
  16. package/src/c_yui_shell.css +571 -0
  17. package/src/c_yui_shell.js +2474 -0
  18. package/src/c_yui_tabs.js +1 -1
  19. package/src/c_yui_treedb_graph.js +35 -9
  20. package/src/c_yui_treedb_topic_with_form.js +1 -1
  21. package/src/c_yui_treedb_topics.js +36 -8
  22. package/src/c_yui_uplot.js +1 -1
  23. package/src/c_yui_window.js +242 -21
  24. package/src/c_yui_window_manager.js +602 -0
  25. package/src/c_yui_wizard.js +612 -0
  26. package/src/pager_helpers.js +138 -0
  27. package/src/pager_helpers.test.js +140 -0
  28. package/src/route_resolver.js +53 -0
  29. package/src/route_resolver.test.js +82 -0
  30. package/src/shell_focus_trap.js +123 -0
  31. package/src/shell_focus_trap.test.js +299 -0
  32. package/src/shell_modals.js +445 -0
  33. package/src/shell_show_on.js +91 -0
  34. package/src/shell_show_on.test.js +86 -0
  35. package/src/shell_toolbar_helpers.js +221 -0
  36. package/src/shell_toolbar_helpers.test.js +207 -0
  37. package/src/tabulator.css +53 -0
  38. package/src/wizard_helpers.js +117 -0
  39. package/src/wizard_helpers.test.js +122 -0
  40. package/src/yui_dev.js +1257 -362
  41. package/src/yui_icons.css +5 -0
  42. package/src/yui_inputs.css +32 -0
  43. package/src/yui_inputs.js +71 -0
  44. package/vite-plugin-yuneta-html.js +2 -2
  45. package/skeleton/config.json +0 -20
  46. package/skeleton/index.html +0 -37
package/src/c_yui_tabs.js CHANGED
@@ -183,7 +183,7 @@ function build_ui(gobj)
183
183
  let tabs_style = gobj_read_attr(gobj, "tabs_style");
184
184
  let $container = createElement2(
185
185
  // Don't use is-flex, don't work well with is-hidden
186
- ['div', {class: `yui_tabs`, style: 'height:100%; display:flex; flex-direction:column;'}, [
186
+ ['div', {class: `C_YUI_TABS`, style: 'height:100%; display:flex; flex-direction:column;'}, [
187
187
  ['div', {class: 'is-flex-grow-0'}, [
188
188
  ['div', {class: `tabs ${tabs_style}`, style: ''}, [
189
189
  ['ul', {}]
@@ -319,7 +319,7 @@ function build_ui(gobj)
319
319
 
320
320
  let $container = createElement2(
321
321
  // Don't use is-flex, don't work well with is-hidden
322
- ['div', {class: 'graphs', style: `height:100%; display:flex; flex-direction:column;`}, [
322
+ ['div', {class: 'C_YUI_TREEDB_GRAPH', style: `height:100%; display:flex; flex-direction:column;`}, [
323
323
  ['div', {class: 'is-flex-grow-0 is-flex is-align-items-center toolbar_yui_treedb_graph'}, $toolbar],
324
324
  ['div', {class: `is-flex-grow-1 ${padding}`, style: 'height:100%; min-height:0; overflow:hidden;'}, [
325
325
  ['div', {id: priv.canvas_id, class: `graph-container`, style: 'height:100%; min-height:0;border: 1px solid var(--bulma-border-weak);border-radius:0.2rem;'}, [
@@ -333,6 +333,28 @@ function build_ui(gobj)
333
333
  return $container;
334
334
  }
335
335
 
336
+ /************************************************************
337
+ * Show a non-blocking inline error banner at the top of the view
338
+ * (used when the treedb schema `descs` cannot load). Reuses a single
339
+ * banner so retries don't stack.
340
+ ************************************************************/
341
+ function show_load_error(gobj, message)
342
+ {
343
+ let $container = gobj_read_attr(gobj, "$container");
344
+ if(!$container) {
345
+ return;
346
+ }
347
+ let $err = $container.querySelector(".treedb-load-error");
348
+ if(!$err) {
349
+ $err = createElement2(
350
+ ["div", {class: "notification is-danger is-light m-3 treedb-load-error"}, []]
351
+ );
352
+ $container.insertBefore($err, $container.firstChild);
353
+ }
354
+ let treedb = gobj_read_attr(gobj, "treedb_name") || "";
355
+ $err.textContent = (treedb ? `${treedb}: ` : "") + (message || "cannot load treedb");
356
+ }
357
+
336
358
  /************************************************************
337
359
  * Destroy UI
338
360
  ************************************************************/
@@ -908,18 +930,22 @@ function ac_mt_command_answer(gobj, event, kw, src)
908
930
  log_error(e);
909
931
  return;
910
932
  }
911
- if(result < 0) {
912
- display_error_message(
913
- "Error",
914
- t(comment)
915
- );
916
- // HACK don't return, pass errors when need it.
917
- }
918
-
919
933
  let __command__ = msg_iev_get_stack(gobj, kw, "command_stack", true);
920
934
  let command = kw_get_str(gobj, __command__, "command", "", kw_flag_t.KW_REQUIRED);
921
935
  let kw_command = kw_get_dict(gobj, __command__, "kw", {}, kw_flag_t.KW_REQUIRED);
922
936
 
937
+ if(result < 0) {
938
+ if(command === "descs") {
939
+ /* The schema couldn't load (not a treedb, no authz for it, backend
940
+ * down…). Show an inline banner in the view rather than a blocking
941
+ * app-modal that wedges the whole SPA behind an empty tab. */
942
+ show_load_error(gobj, t(comment));
943
+ } else {
944
+ display_error_message("Error", t(comment));
945
+ }
946
+ // HACK don't return for non-descs, pass errors when need it.
947
+ }
948
+
923
949
  switch(command) {
924
950
  case "descs":
925
951
  if(result >= 0) {
@@ -506,7 +506,7 @@ function build_ui(gobj)
506
506
  *----------------------------------------------*/
507
507
  let table_id = gobj_read_str_attr(gobj, "table_id");
508
508
  let $container = createElement2(
509
- ['div', {class: 'container-treedb-topic-with-form', style: 'height:100%;'}, [
509
+ ['div', {class: 'C_YUI_TREEDB_TOPIC_WITH_FORM', style: 'height:100%;'}, [
510
510
  ['div', {class: 'toolbar_tabulator_table m-1', style: 'display:flex; justify-content:space-between; align-items:center; flex-wrap:wrap;'}],
511
511
  ['div',
512
512
  {
@@ -216,7 +216,7 @@ function build_ui(gobj)
216
216
  * Layout Schema
217
217
  *----------------------------------------------*/
218
218
  let $container = createElement2(
219
- ['div', {class: `${gobj_read_attr(gobj, "treedb_name")}`, style: 'height:100%; display:flex; flex-direction:column;'}, [
219
+ ['div', {class: `C_YUI_TREEDB_TOPICS ${gobj_read_attr(gobj, "treedb_name")}`, style: 'height:100%; display:flex; flex-direction:column;'}, [
220
220
  ['div', {class: 'is-flex-grow-0'}, [
221
221
  ['div', {class: `tabs ${gobj_read_attr(gobj, "tabs_style")}`, style: ''}, [
222
222
  ['ul', {}]
@@ -229,6 +229,28 @@ function build_ui(gobj)
229
229
  gobj_write_attr(gobj, "$container", $container);
230
230
  }
231
231
 
232
+ /************************************************************
233
+ * Show a non-blocking inline error banner at the top of the view
234
+ * (used when the treedb schema `descs` cannot load). Reuses a single
235
+ * banner so retries don't stack.
236
+ ************************************************************/
237
+ function show_load_error(gobj, message)
238
+ {
239
+ let $container = gobj_read_attr(gobj, "$container");
240
+ if(!$container) {
241
+ return;
242
+ }
243
+ let $err = $container.querySelector(".treedb-load-error");
244
+ if(!$err) {
245
+ $err = createElement2(
246
+ ["div", {class: "notification is-danger is-light m-3 treedb-load-error"}, []]
247
+ );
248
+ $container.insertBefore($err, $container.firstChild);
249
+ }
250
+ let treedb = gobj_read_attr(gobj, "treedb_name") || "";
251
+ $err.textContent = (treedb ? `${treedb}: ` : "") + (message || "cannot load treedb");
252
+ }
253
+
232
254
  /************************************************************
233
255
  * Destroy UI
234
256
  ************************************************************/
@@ -786,16 +808,22 @@ function ac_mt_command_answer(gobj, event, kw, src)
786
808
  log_error(e);
787
809
  return;
788
810
  }
789
- if(result < 0) {
790
- display_error_message(
791
- "Error",
792
- t(comment)
793
- );
794
- }
795
-
796
811
  let __command__ = msg_iev_get_stack(gobj, kw, "command_stack", true);
797
812
  let command = kw_get_str(gobj, __command__, "command", "", kw_flag_t.KW_REQUIRED);
798
813
  let kw_command = kw_get_dict(gobj, __command__, "kw", {}, kw_flag_t.KW_REQUIRED);
814
+
815
+ if(result < 0) {
816
+ if(command === "descs") {
817
+ /* The schema couldn't load (not a treedb, no authz for it, backend
818
+ * down…). Show an inline banner in the view rather than a blocking
819
+ * app-modal that wedges the whole SPA behind an empty tab. */
820
+ show_load_error(gobj, t(comment));
821
+ } else {
822
+ display_error_message("Error", t(comment));
823
+ }
824
+ return 0;
825
+ }
826
+
799
827
  switch(command) {
800
828
  case "descs":
801
829
  if(result >= 0) {
@@ -145,7 +145,7 @@ function build_ui(gobj)
145
145
  $container = createElement2(
146
146
  ['div',
147
147
  {
148
- class: '',
148
+ class: 'C_YUI_UPLOT',
149
149
  style: 'overflow:auto;'
150
150
  }
151
151
  ]
@@ -19,6 +19,7 @@ import {
19
19
  gobj_write_attr,
20
20
  gobj_short_name,
21
21
  clean_name,
22
+ gobj_send_event,
22
23
  gobj_find_service,
23
24
  createElement2,
24
25
  is_gobj,
@@ -49,6 +50,19 @@ import {get_yesnocancel} from "./c_yui_main.js";
49
50
  ***************************************************************/
50
51
  const GCLASS_NAME = "C_YUI_WINDOW";
51
52
 
53
+ /* Window-control glyphs as inline SVG (currentColor → theme-aware,
54
+ * pixel-consistent). There is no minimize/restore glyph in the
55
+ * yui_icons.css mask set, and chrome affordances read better as
56
+ * crisp SVG than as content icons. */
57
+ const WC_MIN = '<svg viewBox="0 0 16 16" aria-hidden="true"><rect x="3" y="11" width="10" height="1.6" rx="0.8" fill="currentColor"/></svg>';
58
+ const WC_MAX = '<svg viewBox="0 0 16 16" aria-hidden="true"><rect x="3.3" y="3.3" width="9.4" height="9.4" rx="1.6" fill="none" stroke="currentColor" stroke-width="1.5"/></svg>';
59
+ const WC_RESTORE = '<svg viewBox="0 0 16 16" aria-hidden="true"><rect x="5.2" y="3" width="7.8" height="7.8" rx="1.3" fill="none" stroke="currentColor" stroke-width="1.4"/><rect x="3" y="5.2" width="7.8" height="7.8" rx="1.3" fill="var(--bulma-scheme-main)" stroke="currentColor" stroke-width="1.4"/></svg>';
60
+ const WC_CLOSE = '<svg viewBox="0 0 16 16" aria-hidden="true"><path d="M4 4 L12 12 M12 4 L4 12" stroke="currentColor" stroke-width="1.7" stroke-linecap="round"/></svg>';
61
+
62
+ /* Bulma mobile breakpoint: at or below this a window becomes a
63
+ * full-screen sheet (no float / drag / resize). */
64
+ const MOBILE_MAX = 768;
65
+
52
66
  /***************************************************************
53
67
  * Data
54
68
  ***************************************************************/
@@ -70,9 +84,13 @@ SDATA(data_type_t.DTP_BOOLEAN, "resizable", 0, true, "Allow resizing"),
70
84
  SDATA(data_type_t.DTP_BOOLEAN, "showFooter", 0, true, "Show footer"),
71
85
  SDATA(data_type_t.DTP_BOOLEAN, "openMaximized",0, false, "Open the window maximized"),
72
86
  SDATA(data_type_t.DTP_BOOLEAN, "showMax", 0, true, "Show maximize button"),
87
+ SDATA(data_type_t.DTP_BOOLEAN, "showMin", 0, true, "Show minimize (roll-up / shade) button"),
73
88
  SDATA(data_type_t.DTP_BOOLEAN, "maximized", 0, false, "Flag to indicate if maximized"),
74
89
  SDATA(data_type_t.DTP_JSON, "window_style", 0, "{}", "Override window style"),
75
90
  SDATA(data_type_t.DTP_POINTER, "on_close", 0, null, "Callback on destroy"),
91
+ SDATA(data_type_t.DTP_POINTER, "manager", 0, null, "Optional C_YUI_WINDOW_MANAGER (gobj or service name) for dock/taskbar"),
92
+ SDATA(data_type_t.DTP_STRING, "title", 0, "", "Window title, shown on the dock chip"),
93
+ SDATA(data_type_t.DTP_STRING, "icon", 0, "", "Dock-chip icon: a yi-* class name or inline SVG (by window type)"),
76
94
  // TODO pendiente focus modal keyboard
77
95
  SDATA(data_type_t.DTP_POINTER, "focus", 0, null, "Brings focus to the element, can be a number or selector"),
78
96
  SDATA(data_type_t.DTP_BOOLEAN, "modal", 0, false, "Enable modal mode"),
@@ -85,6 +103,7 @@ SDATA_END()
85
103
 
86
104
  let PRIVATE_DATA = {
87
105
  prevSize: 0,
106
+ preShadeHeight: 0,
88
107
  };
89
108
 
90
109
  let __gclass__ = null;
@@ -116,6 +135,27 @@ function mt_create(gobj)
116
135
  gobj_write_attr(gobj, "window_id", window_id);
117
136
  build_ui(gobj);
118
137
 
138
+ /* Optional window manager (dock/taskbar): register, and raise/
139
+ * highlight on any pointer press. Absent → the window is a plain
140
+ * floating window and minimize falls back to shade in place. */
141
+ let manager = resolve_manager(gobj);
142
+ if(manager) {
143
+ let $c = gobj_read_attr(gobj, "$container");
144
+ if($c) {
145
+ $c.addEventListener("pointerdown", function() {
146
+ let m = gobj_read_pointer_attr(gobj, "manager");
147
+ if(m) {
148
+ gobj_send_event(m, "EV_FOCUS_WINDOW", {window: gobj}, gobj);
149
+ }
150
+ }, true);
151
+ }
152
+ gobj_send_event(manager, "EV_REGISTER_WINDOW", {
153
+ window: gobj,
154
+ title: gobj_read_attr(gobj, "title") || gobj_short_name(gobj),
155
+ icon: gobj_read_attr(gobj, "icon") || "",
156
+ }, gobj);
157
+ }
158
+
119
159
  /* Keep the window inside the viewport on a breakpoint change.
120
160
  * Wired in mt_create (NOT mt_start) on purpose: windows are
121
161
  * often created via gobj_create_service WITHOUT being started
@@ -159,6 +199,11 @@ function mt_stop(gobj)
159
199
  ***************************************************************/
160
200
  function mt_destroy(gobj)
161
201
  {
202
+ let manager = gobj_read_pointer_attr(gobj, "manager");
203
+ if(manager) {
204
+ gobj_send_event(manager, "EV_UNREGISTER_WINDOW", {window: gobj}, gobj);
205
+ }
206
+
162
207
  let on_win_resize = gobj_read_attr(gobj, "win_resize_handler");
163
208
  if(on_win_resize) {
164
209
  window.removeEventListener("resize", on_win_resize);
@@ -177,11 +222,122 @@ function mt_destroy(gobj)
177
222
 
178
223
 
179
224
 
225
+ /************************************************************
226
+ * Inject the window-chrome stylesheet once. Theme-aware via
227
+ * Bulma scheme vars (flips on <html data-theme>); replaces the
228
+ * old saturated `has-background-info` bar + forced black text.
229
+ ************************************************************/
230
+ function ensure_window_style()
231
+ {
232
+ if(document.getElementById('yui-window-style')) {
233
+ return;
234
+ }
235
+ let css = `
236
+ .yui-window-header {
237
+ background: var(--bulma-scheme-main-bis);
238
+ color: var(--bulma-text-strong);
239
+ -webkit-user-select: none; user-select: none;
240
+ }
241
+ .yui-window-titlebar-controls { display: flex; align-items: center; gap: 2px; padding-left: 6px; }
242
+ .yui-wc {
243
+ width: 30px; height: 26px; display: inline-flex; align-items: center; justify-content: center;
244
+ padding: 0; border: 0; background: transparent; color: var(--bulma-text); cursor: pointer;
245
+ border-radius: 5px; -webkit-tap-highlight-color: transparent;
246
+ }
247
+ .yui-wc:hover { background: var(--bulma-scheme-main-ter); color: var(--bulma-text-strong); }
248
+ .yui-wc.wc-close:hover { background: #e0364a; color: #fff; }
249
+ .yui-wc svg { width: 15px; height: 15px; display: block; }
250
+ .C_YUI_WINDOW.is-shaded .yui-window-body,
251
+ .C_YUI_WINDOW.is-shaded .yui-window-footer { display: none !important; }
252
+ .C_YUI_WINDOW.is-shaded { height: auto !important; }
253
+ .yui-window-resize { color: var(--bulma-text-weak); opacity: 0.55; }
254
+ .yui-window-resize:hover { opacity: 1; color: var(--bulma-text); }
255
+ @media (max-width: ${MOBILE_MAX}px) {
256
+ .C_YUI_WINDOW.is-mobile-sheet { border-radius: 0 !important; box-shadow: none !important; }
257
+ .C_YUI_WINDOW.is-mobile-sheet .yui-window-resize { display: none !important; }
258
+ .yui-wc { width: 42px; height: 38px; }
259
+ .yui-wc.wc-max { display: none !important; }
260
+ .yui-wc svg { width: 18px; height: 18px; }
261
+ }
262
+ `;
263
+ let $style = document.createElement('style');
264
+ $style.id = 'yui-window-style';
265
+ $style.textContent = css;
266
+ document.head.appendChild($style);
267
+ }
268
+
269
+ /************************************************************
270
+ * Below the mobile breakpoint the window is a full-screen
271
+ * sheet (no float / drag / resize).
272
+ ************************************************************/
273
+ function is_mobile()
274
+ {
275
+ let w = window.innerWidth || document.documentElement.offsetWidth;
276
+ return w <= MOBILE_MAX;
277
+ }
278
+
279
+ /************************************************************
280
+ * Resolve the optional `manager` attr (a gobj or a service
281
+ * name) to a gobj, caching the resolution back into the attr.
282
+ ************************************************************/
283
+ function resolve_manager(gobj)
284
+ {
285
+ let m = gobj_read_pointer_attr(gobj, "manager");
286
+ if(!m) {
287
+ return null;
288
+ }
289
+ if(typeof m === "string") {
290
+ m = gobj_find_service(m, false);
291
+ gobj_write_attr(gobj, "manager", m);
292
+ }
293
+ return m;
294
+ }
295
+
296
+ /************************************************************
297
+ * Minimize: with a manager, send the window to the dock;
298
+ * otherwise "shade" it in place. Called by the minimize button.
299
+ ************************************************************/
300
+ function do_minimize(gobj)
301
+ {
302
+ let manager = gobj_read_pointer_attr(gobj, "manager");
303
+ if(manager) {
304
+ gobj_send_event(manager, "EV_MINIMIZE_WINDOW", {window: gobj}, gobj);
305
+ } else {
306
+ toggle_shade(gobj);
307
+ }
308
+ }
309
+
310
+ /************************************************************
311
+ * Minimize as "shade": roll the window up to its title bar
312
+ * in place (self-contained; no window manager needed). Toggles.
313
+ ************************************************************/
314
+ function toggle_shade(gobj)
315
+ {
316
+ let priv = gobj.priv;
317
+ let $container = gobj_read_attr(gobj, "$container");
318
+ if(!$container) {
319
+ return;
320
+ }
321
+ if($container.classList.contains("is-shaded")) {
322
+ $container.classList.remove("is-shaded");
323
+ if(priv.preShadeHeight) {
324
+ $container.style.height = parseInt(priv.preShadeHeight) + 'px';
325
+ }
326
+ } else {
327
+ priv.preShadeHeight = $container.getBoundingClientRect().height;
328
+ $container.classList.add("is-shaded");
329
+ $container.style.height = "auto";
330
+ }
331
+ }
332
+
180
333
  /************************************************************
181
334
  * Build UI
182
335
  ************************************************************/
183
336
  function build_ui(gobj)
184
337
  {
338
+ ensure_window_style();
339
+ let mobile = is_mobile();
340
+
185
341
  let header = gobj_read_attr(gobj, "header");
186
342
  if(is_gobj(header)) {
187
343
  header = gobj_read_attr(header, "$container");
@@ -224,6 +380,16 @@ function build_ui(gobj)
224
380
  rect = do_center(gobj, rect.x, rect.y, rect.width, rect.height);
225
381
  }
226
382
 
383
+ /* On mobile the window is a full-screen sheet: ignore the saved
384
+ * rect / centering and fill the viewport. */
385
+ if(mobile) {
386
+ rect = {
387
+ x: 0, y: 0,
388
+ width: window.innerWidth || document.documentElement.offsetWidth,
389
+ height: window.innerHeight || document.documentElement.offsetHeight,
390
+ };
391
+ }
392
+
227
393
  let window_style = {
228
394
  position: "fixed",
229
395
  "z-index": 3,
@@ -245,14 +411,14 @@ function build_ui(gobj)
245
411
 
246
412
  let $container = createElement2(
247
413
  ['div', {
248
- class: 'yui-window strong-shadow is-flex is-flex-direction-column',
414
+ class: 'C_YUI_WINDOW strong-shadow is-flex is-flex-direction-column' + (mobile ? ' is-mobile-sheet' : ''),
249
415
  style: window_style}, [
250
416
 
251
417
  /*----------------------------*
252
418
  * Header
253
419
  *----------------------------*/
254
420
  ['div', {
255
- class: 'yui-window-header p-1 is-flex-shrink-0 is-flex is-flex-nowrap is-justify-content-space-between is-align-items-flex-start has-text-black has-background-info', style: 'border-bottom:1px solid var(--bulma-border); cursor:move; box-sizing: border-box;'
421
+ 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;'
256
422
  }, [
257
423
  /* Custom header content: a single-row, horizontally
258
424
  * scrollable strip. It takes the remaining width and
@@ -272,33 +438,40 @@ function build_ui(gobj)
272
438
  * width and kept wrapping. */
273
439
  ['div', {class: '', style: 'width:max-content; display:flex; flex-wrap:nowrap; align-items:center;'}, header]
274
440
  ]],
275
- /* Max/close: pinned top-right, never shrink, never
276
- * wrap, always on top so the click always lands. */
277
- ['div', {class: 'is-flex-shrink-0 is-flex is-flex-nowrap is-align-items-flex-start', style: 'position:relative; z-index:1; cursor:default;'}, [
441
+ /* Window controls: pinned top-right, never shrink,
442
+ * never wrap, always on top so the click always lands.
443
+ * Minimize (shade) · maximize/restore · close. */
444
+ ['div', {class: 'yui-window-titlebar-controls is-flex-shrink-0', style: 'position:relative; z-index:1; cursor:default;'}, [
278
445
  /*----------------------------*
279
- * Max/min button
446
+ * Minimize (shade)
280
447
  *----------------------------*/
281
448
  ['button', {
282
- class: 'without-border pr-4',
283
- style: {
284
- color: 'var(--bulma-text)',
285
- "font-size": "1.4em",
286
- "cursor": "pointer",
287
- "display": gobj_read_bool_attr(gobj, "showMax")?'inline-block':'none',
449
+ class: 'yui-wc wc-min', type: 'button', 'aria-label': 'minimize',
450
+ style: gobj_read_bool_attr(gobj, "showMin") ? '' : 'display:none;',
451
+ }, WC_MIN, {
452
+ click: (evt) => {
453
+ evt.stopPropagation();
454
+ do_minimize(gobj);
288
455
  }
289
- }, '<i class="yi-square"></i>', {
456
+ }],
457
+ /*----------------------------*
458
+ * Maximize / restore
459
+ *----------------------------*/
460
+ ['button', {
461
+ class: 'yui-wc wc-max', type: 'button', 'aria-label': 'maximize',
462
+ style: gobj_read_bool_attr(gobj, "showMax") ? '' : 'display:none;',
463
+ }, WC_MAX, {
290
464
  click: (evt) => {
291
465
  evt.stopPropagation();
292
466
  toggle(gobj);
293
467
  }
294
468
  }],
295
469
  /*----------------------------*
296
- * Close button
470
+ * Close
297
471
  *----------------------------*/
298
472
  ['button', {
299
- class: 'without-border pr-2',
300
- style: 'color:var(--bulma-text);font-size:1.6em;cursor:pointer;',
301
- }, '<i class="yi-xmark"></i>', {
473
+ class: 'yui-wc wc-close', type: 'button', 'aria-label': 'close',
474
+ }, WC_CLOSE, {
302
475
  click: (evt) => {
303
476
  evt.stopPropagation();
304
477
  close_window(gobj);
@@ -409,7 +582,7 @@ function build_ui(gobj)
409
582
  *----------------------------*/
410
583
  if(gobj_read_bool_attr(gobj, "resizable") && !gobj_read_bool_attr(gobj, "showFooter")) {
411
584
  let $resizable_btn = createElement2(['div', {
412
- class: 'without-border',
585
+ class: 'without-border yui-window-resize',
413
586
  style: {
414
587
  cursor: "nwse-resize",
415
588
  display: "flex",
@@ -447,7 +620,7 @@ function build_ui(gobj)
447
620
 
448
621
  if (gobj_read_bool_attr(gobj, "openMaximized")) {
449
622
  max(gobj);
450
- } else {
623
+ } else if(!mobile) {
451
624
  if(gobj_read_bool_attr(gobj, "content_size")) {
452
625
  $container.style.height = "auto";
453
626
  }
@@ -538,10 +711,27 @@ function max(gobj)
538
711
  $container.style.height = parseInt(rect.height) +'px';
539
712
 
540
713
  gobj_write_bool_attr(gobj, "maximized", true);
714
+ set_max_icon(gobj, true);
541
715
 
542
716
  return rect;
543
717
  }
544
718
 
719
+ /************************************************************
720
+ * Swap the maximize/restore glyph to match the state.
721
+ ************************************************************/
722
+ function set_max_icon(gobj, maximized)
723
+ {
724
+ let $container = gobj_read_attr(gobj, "$container");
725
+ if(!$container) {
726
+ return;
727
+ }
728
+ let $btn = $container.querySelector('.wc-max');
729
+ if($btn) {
730
+ $btn.innerHTML = maximized ? WC_RESTORE : WC_MAX;
731
+ $btn.setAttribute('aria-label', maximized ? 'restore' : 'maximize');
732
+ }
733
+ }
734
+
545
735
  /************************************************************
546
736
  *
547
737
  ************************************************************/
@@ -562,6 +752,7 @@ function min(gobj)
562
752
  $container.style.height = parseInt(rect.height) +'px';
563
753
 
564
754
  gobj_write_bool_attr(gobj, "maximized", false);
755
+ set_max_icon(gobj, false);
565
756
 
566
757
  return rect;
567
758
  }
@@ -571,6 +762,11 @@ function min(gobj)
571
762
  ************************************************************/
572
763
  function mvStart(gobj, evt)
573
764
  {
765
+ /* No window-dragging on a mobile full-screen sheet. */
766
+ if(is_mobile()) {
767
+ return;
768
+ }
769
+
574
770
  let $container = gobj_read_attr(gobj, "$container");
575
771
 
576
772
  let window_rect = $container.getBoundingClientRect();
@@ -776,6 +972,19 @@ function handleResize(gobj)
776
972
  return;
777
973
  }
778
974
 
975
+ /* Mobile: full-screen sheet, refit to the viewport and stop. */
976
+ if(is_mobile()) {
977
+ $container.classList.add("is-mobile-sheet");
978
+ let vw = window.innerWidth || document.documentElement.offsetWidth;
979
+ let vh = window.innerHeight || document.documentElement.offsetHeight;
980
+ $container.style.left = '0px';
981
+ $container.style.top = '0px';
982
+ $container.style.width = vw + 'px';
983
+ $container.style.height = vh + 'px';
984
+ return;
985
+ }
986
+ $container.classList.remove("is-mobile-sheet");
987
+
779
988
  /* Maximized: just refit to the (new) viewport. */
780
989
  if(gobj_read_bool_attr(gobj, "maximized") === true) {
781
990
  let r = do_fix_dimension_to_screen(gobj, 0, 0, 10000, 10000);
@@ -853,6 +1062,16 @@ function ac_hide(gobj, event, kw, src)
853
1062
  return 0;
854
1063
  }
855
1064
 
1065
+ /************************************************************
1066
+ * EV_CLOSE_WINDOW — close from outside (e.g. the dock chip ✕),
1067
+ * the same teardown as the title-bar close button.
1068
+ ************************************************************/
1069
+ function ac_close(gobj, event, kw, src)
1070
+ {
1071
+ close_window(gobj);
1072
+ return 0;
1073
+ }
1074
+
856
1075
 
857
1076
 
858
1077
 
@@ -891,7 +1110,8 @@ function create_gclass(gclass_name)
891
1110
  ["EV_RESIZE", ac_resize, null],
892
1111
  ["EV_REFRESH", ac_refresh, null],
893
1112
  ["EV_SHOW", ac_show, null],
894
- ["EV_HIDE", ac_hide, null]
1113
+ ["EV_HIDE", ac_hide, null],
1114
+ ["EV_CLOSE_WINDOW", ac_close, null]
895
1115
  ]]
896
1116
  ];
897
1117
 
@@ -905,7 +1125,8 @@ function create_gclass(gclass_name)
905
1125
  ["EV_RESIZE", 0],
906
1126
  ["EV_REFRESH", 0],
907
1127
  ["EV_SHOW", 0],
908
- ["EV_HIDE", 0]
1128
+ ["EV_HIDE", 0],
1129
+ ["EV_CLOSE_WINDOW", 0]
909
1130
  ];
910
1131
 
911
1132
  __gclass__ = gclass_create(