@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
@@ -41,6 +41,8 @@ import {
41
41
  import {
42
42
  yui_shell_push_escape,
43
43
  yui_shell_pop_escape,
44
+ yui_shell_register_overlay,
45
+ yui_shell_overlay_dismissed,
44
46
  } from "./c_yui_shell.js";
45
47
 
46
48
 
@@ -52,7 +54,7 @@ function notification_layer(shell)
52
54
  if(!shell) {
53
55
  return null;
54
56
  }
55
- let priv = gobj_read_attr(shell, "priv");
57
+ let priv = shell.priv;
56
58
  return priv && priv.layers && priv.layers.notification;
57
59
  }
58
60
 
@@ -61,7 +63,7 @@ function modal_layer(shell)
61
63
  if(!shell) {
62
64
  return null;
63
65
  }
64
- let priv = gobj_read_attr(shell, "priv");
66
+ let priv = shell.priv;
65
67
  return priv && priv.layers && priv.layers.modal;
66
68
  }
67
69
 
@@ -75,7 +77,7 @@ export function yui_shell_popup_layer(shell)
75
77
  if(!shell) {
76
78
  return null;
77
79
  }
78
- let priv = gobj_read_attr(shell, "priv");
80
+ let priv = shell.priv;
79
81
  return (priv && priv.layers && priv.layers.popup) || null;
80
82
  }
81
83
 
@@ -119,11 +121,11 @@ function show_notification(shell, kind, message, opts)
119
121
  ? {i18n: message}
120
122
  : {};
121
123
  let $note = createElement2(
122
- ["div", {class: `notification yui-notification is-${kind} is-light`,
124
+ ["div", {class: `TOAST notification yui-notification is-${kind} is-light`,
123
125
  role: kind === "danger" ? "alert" : "status"},
124
126
  [
125
- ["button", {class: "delete", "aria-label": "close"}],
126
- ["p", p_attrs, message]
127
+ ["button", {class: "TOAST_CLOSE delete", "aria-label": "close"}],
128
+ ["p", {...p_attrs, class: "TOAST_MSG"}, message]
127
129
  ]
128
130
  ]
129
131
  );
@@ -205,6 +207,14 @@ export function yui_shell_show_modal(shell, content, opts)
205
207
  * history.back()), so gobj-ui stays routing-agnostic. */
206
208
  let dialog = !!(opts && opts.dialog);
207
209
  let title = (opts && opts.title) || "";
210
+ /* Optional DATA half of the title (a topic/service name), never
211
+ * translated, shown before `title`. Same contract as C_YUI_WINDOW's
212
+ * `title_prefix`, and for the same reason: composing
213
+ * `${topic} · ${t("keys")}` into `title` yields a string that is not
214
+ * an i18n key, so it never re-translates. Split, only the kind half
215
+ * carries a key. The separator is CSS — createElement2 trims text
216
+ * nodes and would eat the spaces around it. */
217
+ let title_prefix = (opts && opts.title_prefix) || "";
208
218
 
209
219
  /* The external Bulma `.modal-close is-large` sits at the
210
220
  * top-right of the viewport, outside the content box. Callers
@@ -214,37 +224,47 @@ export function yui_shell_show_modal(shell, content, opts)
214
224
  let with_close = !(opts && opts.with_close_button === false);
215
225
  let modal_children;
216
226
  if(dialog) {
217
- let header = ["div", {class: "yui-dialog-header"}, [
218
- ["button", {class: "yui-dialog-back", type: "button", "aria-label": "back"},
227
+ let header = ["div", {class: "MODAL_HEADER yui-dialog-header"}, [
228
+ ["button", {class: "MODAL_BACK yui-dialog-back", type: "button", "aria-label": "back"},
219
229
  [["i", {class: "yi-arrow-left"}]]],
220
- ["span", {class: "yui-dialog-title", i18n: title}, title],
221
- ["button", {class: "yui-dialog-x", type: "button", "aria-label": "close"},
230
+ ["span", {class: "MODAL_TITLE yui-dialog-title"},
231
+ (title_prefix ? [["span", {class: "MODAL_TITLE_PREFIX"}, title_prefix]] : [])
232
+ .concat(title
233
+ ? [["span", {class: "MODAL_TITLE_KIND", i18n: title}, title]]
234
+ : [])],
235
+ ["button", {class: "MODAL_CLOSE yui-dialog-x", type: "button", "aria-label": "close"},
222
236
  [["i", {class: "yi-xmark"}]]],
223
237
  ]];
224
- let body = ["div", {class: "yui-dialog-body"}, inner ? [inner] : []];
238
+ let body = ["div", {class: "MODAL_BODY yui-dialog-body"}, inner ? [inner] : []];
225
239
  modal_children = [
226
- ["div", {class: "modal-background"}],
227
- ["div", {class: "modal-content yui-dialog-content"}, [header, body]]
240
+ ["div", {class: "MODAL_BACKDROP modal-background"}],
241
+ ["div", {class: "MODAL_CONTENT modal-content yui-dialog-content"}, [header, body]]
228
242
  ];
229
243
  /* The header X replaces the external Bulma close button. */
230
244
  with_close = false;
231
245
  } else {
232
246
  modal_children = [
233
- ["div", {class: "modal-background"}],
234
- ["div", {class: "modal-content"},
247
+ ["div", {class: "MODAL_BACKDROP modal-background"}],
248
+ ["div", {class: "MODAL_CONTENT modal-content"},
235
249
  inner ? [inner] : []
236
250
  ]
237
251
  ];
238
252
  if(with_close) {
239
253
  modal_children.push(
240
- ["button", {class: "modal-close is-large",
254
+ ["button", {class: "MODAL_CLOSE modal-close is-large",
241
255
  "aria-label": "close"}]
242
256
  );
243
257
  }
244
258
  }
245
259
 
260
+ /* `opts.logical_class`: the CALLER's UPPER_SNAKE name for THIS modal
261
+ * (e.g. TRANGER_KEYS_SHEET). Every modal shares the MODAL block names,
262
+ * only this tells one popup from another in the Inspector or a selector. */
263
+ let logical = (opts && opts.logical_class) || "";
264
+
246
265
  let $modal = createElement2(
247
- ["div", {class: "modal yui-modal" + (dialog ? " yui-dialog" : "") + " is-active",
266
+ ["div", {class: "MODAL" + (logical ? " " + logical : "") +
267
+ " modal yui-modal" + (dialog ? " yui-dialog" : "") + " is-active",
248
268
  role: "dialog", "aria-modal": "true"},
249
269
  modal_children
250
270
  ]
@@ -261,10 +281,13 @@ export function yui_shell_show_modal(shell, content, opts)
261
281
  maybe_apply_translator($modal, opts);
262
282
 
263
283
  let closed = false;
264
- let close_fn = null;
265
284
  let release_focus = null;
285
+ let overlay = null;
286
+ let escape_handler = null;
266
287
 
267
- let close = function() {
288
+ /* Unconditional teardown. The returned close() maps here: a
289
+ * programmatic close always closes, bypassing before_close. */
290
+ let do_close = function() {
268
291
  if(closed) {
269
292
  return;
270
293
  }
@@ -273,13 +296,16 @@ export function yui_shell_show_modal(shell, content, opts)
273
296
  release_focus();
274
297
  release_focus = null;
275
298
  }
276
- if(close_fn) {
277
- yui_shell_pop_escape(shell, close_fn);
278
- close_fn = null;
299
+ if(escape_handler) {
300
+ yui_shell_pop_escape(shell, escape_handler);
301
+ escape_handler = null;
279
302
  }
280
303
  if($modal.parentNode) {
281
304
  $modal.parentNode.removeChild($modal);
282
305
  }
306
+ /* Retire the browser-history entry (no-op when Back triggered
307
+ * this close — the entry is already gone). */
308
+ yui_shell_overlay_dismissed(shell, overlay);
283
309
  if(opts && typeof opts.on_close === "function") {
284
310
  try {
285
311
  opts.on_close();
@@ -288,9 +314,50 @@ export function yui_shell_show_modal(shell, content, opts)
288
314
  }
289
315
  }
290
316
  };
291
- close_fn = close;
292
317
 
293
- yui_shell_push_escape(shell, "modal", close);
318
+ /* `opts.before_close`: a guard consulted on EVERY user-driven dismiss
319
+ * (Escape, backdrop, the X / back-arrow, browser Back). Return false to
320
+ * VETO — the modal stays up and the caller takes over (e.g. an
321
+ * unsaved-changes prompt that calls close() itself on confirm). Return
322
+ * true/undefined to let it close. Absent guard ⇒ always closes, so
323
+ * existing callers are unaffected. */
324
+ let guarded_close = function(on_veto) {
325
+ if(closed) {
326
+ return;
327
+ }
328
+ let allow = true;
329
+ if(opts && typeof opts.before_close === "function") {
330
+ try {
331
+ allow = opts.before_close();
332
+ } catch(e) {
333
+ log_warning(`yui_shell_show_modal: before_close threw: ${e}`);
334
+ allow = true;
335
+ }
336
+ }
337
+ if(allow === false) {
338
+ if(typeof on_veto === "function") {
339
+ on_veto();
340
+ }
341
+ return;
342
+ }
343
+ do_close();
344
+ };
345
+ let request_close = function() {
346
+ guarded_close(null);
347
+ };
348
+ /* Browser Back already consumed the history entry; if the guard vetoes,
349
+ * re-arm a fresh entry so a later Back still targets this modal. */
350
+ let on_back = function() {
351
+ guarded_close(function() {
352
+ overlay = yui_shell_register_overlay(shell, on_back);
353
+ });
354
+ };
355
+
356
+ escape_handler = request_close;
357
+ yui_shell_push_escape(shell, "modal", request_close);
358
+ /* Browser Back closes the top-most modal (see overlay history in
359
+ * c_yui_shell.js). Null when history integration is off. */
360
+ overlay = yui_shell_register_overlay(shell, on_back);
294
361
  /* Trap on $modal (not on .modal-content) so the .modal-close
295
362
  * button — rendered as a SIBLING of .modal-content — is
296
363
  * reachable via Tab. Without this, Tab/Shift+Tab can only
@@ -299,35 +366,50 @@ export function yui_shell_show_modal(shell, content, opts)
299
366
  release_focus = activate_focus_trap_on($modal);
300
367
 
301
368
  if((opts == null || opts.dismiss_on_background !== false)) {
302
- $modal.querySelector(".modal-background").addEventListener("click", close);
369
+ $modal.querySelector(".modal-background").addEventListener("click", request_close);
303
370
  }
304
371
  let $close_btn = $modal.querySelector(".modal-close");
305
372
  if($close_btn) {
306
- $close_btn.addEventListener("click", close);
373
+ $close_btn.addEventListener("click", request_close);
307
374
  }
308
375
  if(dialog) {
309
376
  let $back = $modal.querySelector(".yui-dialog-back");
310
377
  if($back) {
311
- $back.addEventListener("click", close);
378
+ $back.addEventListener("click", request_close);
312
379
  }
313
380
  let $x = $modal.querySelector(".yui-dialog-x");
314
381
  if($x) {
315
- $x.addEventListener("click", close);
382
+ $x.addEventListener("click", request_close);
316
383
  }
317
384
  }
318
385
 
319
- return { close };
386
+ return { close: do_close };
320
387
  }
321
388
 
322
389
 
323
390
  /***************************************************************
324
- * Blocking dialogs (Bulma .modal-card)
391
+ * Blocking dialogs (icon-centric .modal-card)
325
392
  *
326
393
  * build_dialog returns a Promise that resolves with the
327
394
  * clicked button's `value`. Escape, the close button and
328
395
  * the dismiss action all resolve with the LAST button's value
329
396
  * (cancel/no/ok by convention — the safe-default action).
397
+ *
398
+ * Layout ported from the legacy volatil modals (2.5.0
399
+ * redesign, removed in 3.0.0): a narrow rounded card with a
400
+ * tinted round icon of the `opts.type`
401
+ * (question/success/info/warning/error, `danger` aliases
402
+ * error), optional capitalized title, centered message and
403
+ * buttons. Everything maps to Bulma vars (light and dark).
330
404
  ***************************************************************/
405
+ const CONFIRM_TYPE_ICONS = {
406
+ "info": "yi-circle-info",
407
+ "question": "yi-question",
408
+ "success": "yi-square-check",
409
+ "warning": "yi-triangle-exclamation",
410
+ "error": "yi-circle-exclamation",
411
+ };
412
+
331
413
  function build_dialog(shell, message, buttons, opts)
332
414
  {
333
415
  let $layer = modal_layer(shell);
@@ -339,16 +421,18 @@ function build_dialog(shell, message, buttons, opts)
339
421
  let title = (opts && opts.title) || "";
340
422
  let dismiss_value = buttons[buttons.length - 1].value;
341
423
 
424
+ let type = ((opts && opts.type) || "question").toLowerCase();
425
+ if(type === "danger") {
426
+ type = "error";
427
+ }
428
+ let icon = CONFIRM_TYPE_ICONS[type] || CONFIRM_TYPE_ICONS["question"];
429
+
342
430
  let $body_children = (typeof message === "string")
343
- ? [["p", {i18n: message}, message]]
431
+ ? [["p", {class: "CONFIRM_MSG yui-confirm-msg", i18n: message}, message]]
344
432
  : [message];
345
433
 
346
- let title_attrs = !empty_string(title)
347
- ? {class: "modal-card-title", i18n: title}
348
- : {class: "modal-card-title"};
349
-
350
434
  let $footer_children = buttons.map(b => {
351
- let cls = "button";
435
+ let cls = "CONFIRM_BTN button px-5";
352
436
  if(b.kind === "primary") {
353
437
  cls += " is-link";
354
438
  } else if(b.kind === "danger") {
@@ -362,24 +446,34 @@ function build_dialog(shell, message, buttons, opts)
362
446
  return ["button", btn_attrs, b.label];
363
447
  });
364
448
 
449
+ let $card_children = [
450
+ ["div", {class: "CONFIRM_ICON yui-confirm-icon"},
451
+ [["i", {class: icon, "aria-hidden": "true"}]]
452
+ ]
453
+ ];
454
+ if(!empty_string(title)) {
455
+ $card_children.push(
456
+ ["p", {class: "CONFIRM_TITLE yui-confirm-title has-text-centered",
457
+ i18n: title}, title]
458
+ );
459
+ }
460
+ $card_children.push(
461
+ ["button", {class: "CONFIRM_CLOSE delete yui-confirm-x", "aria-label": "close"}],
462
+ ["section", {class: "CONFIRM_BODY modal-card-body has-text-centered"},
463
+ $body_children],
464
+ ["footer", {class: "CONFIRM_FOOT modal-card-foot"}, $footer_children]
465
+ );
466
+
467
+ /* Caller's own UPPER_SNAKE name for THIS confirm, same contract as
468
+ * yui_shell_show_modal's `logical_class`. */
469
+ let logical = (opts && opts.logical_class) ? " " + opts.logical_class : "";
470
+
365
471
  let $modal = createElement2(
366
- ["div", {class: "modal yui-modal yui-confirm is-active",
472
+ ["div", {class: `CONFIRM${logical} modal yui-modal yui-confirm is-active is-${type}`,
367
473
  role: "dialog", "aria-modal": "true"},
368
474
  [
369
- ["div", {class: "modal-background"}],
370
- ["div", {class: "modal-card"},
371
- [
372
- ["header", {class: "modal-card-head"},
373
- [
374
- ["p", title_attrs, title],
375
- ["button", {class: "delete",
376
- "aria-label": "close"}]
377
- ]
378
- ],
379
- ["section", {class: "modal-card-body"}, $body_children],
380
- ["footer", {class: "modal-card-foot"}, $footer_children]
381
- ]
382
- ]
475
+ ["div", {class: "CONFIRM_BACKDROP modal-background"}],
476
+ ["div", {class: "CONFIRM_CARD modal-card"}, $card_children]
383
477
  ]
384
478
  ]
385
479
  );
@@ -390,6 +484,7 @@ function build_dialog(shell, message, buttons, opts)
390
484
  let resolved = false;
391
485
  let close_fn = null;
392
486
  let release_focus = null;
487
+ let overlay = null;
393
488
 
394
489
  let close = function(value) {
395
490
  if(resolved) {
@@ -407,11 +502,17 @@ function build_dialog(shell, message, buttons, opts)
407
502
  if($modal.parentNode) {
408
503
  $modal.parentNode.removeChild($modal);
409
504
  }
505
+ /* Retire the browser-history entry (no-op when Back
506
+ * triggered this close). */
507
+ yui_shell_overlay_dismissed(shell, overlay);
410
508
  resolve(value);
411
509
  };
412
510
  close_fn = () => close(dismiss_value);
413
511
 
414
512
  yui_shell_push_escape(shell, "modal", close_fn);
513
+ /* Browser Back dismisses the dialog with the safe-default value,
514
+ * exactly like Escape. */
515
+ overlay = yui_shell_register_overlay(shell, close_fn);
415
516
  release_focus = activate_focus_trap_on(
416
517
  $modal.querySelector(".modal-card")
417
518
  );
@@ -419,7 +520,7 @@ function build_dialog(shell, message, buttons, opts)
419
520
  $modal.querySelector(".modal-background").addEventListener(
420
521
  "click", () => close(dismiss_value)
421
522
  );
422
- $modal.querySelector(".modal-card-head .delete").addEventListener(
523
+ $modal.querySelector(".modal-card .delete").addEventListener(
423
524
  "click", () => close(dismiss_value)
424
525
  );
425
526
  let footer_buttons = $modal.querySelectorAll(
@@ -430,6 +531,12 @@ function build_dialog(shell, message, buttons, opts)
430
531
  close($btn.getAttribute("data-modal-button-value"));
431
532
  });
432
533
  });
534
+
535
+ /* Enter answers the primary action, like the volatil modals. */
536
+ let $primary = $modal.querySelector(".modal-card-foot button");
537
+ if($primary) {
538
+ $primary.focus();
539
+ }
433
540
  });
434
541
  }
435
542
 
@@ -437,6 +544,9 @@ function build_dialog(shell, message, buttons, opts)
437
544
  export function yui_shell_confirm_ok(shell, message, opts)
438
545
  {
439
546
  let label = (opts && opts.ok_label) || "OK";
547
+ if(!opts || !opts.type) {
548
+ opts = Object.assign({}, opts, {type: "success"});
549
+ }
440
550
  return build_dialog(shell, message, [
441
551
  {label: label, value: "ok", kind: "primary"}
442
552
  ], opts).then(() => undefined);
@@ -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
+ }