@yuneta/gobj-ui 7.26.29 → 7.26.31

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.
@@ -524,6 +524,127 @@ function text_list_item_formatter(label) {
524
524
  return span;
525
525
  }
526
526
  //#endregion
527
+ //#region src/yui_badge.js
528
+ /***********************************************************************
529
+ * yui_badge.js
530
+ *
531
+ * The BADGE: the little count a control carries -- unread
532
+ * notifications, active alarms. The toolbar items of C_YUI_SHELL
533
+ * and the menu items of C_YUI_NAV build and paint theirs here, so
534
+ * the two follow the same rules and cannot drift apart:
535
+ *
536
+ * EMPTY IS 0, "" AND null ALIKE (false and undefined too): a badge
537
+ * reading "0" is worse than no badge -- it draws the eye to say
538
+ * nothing. A count is only worth a pixel when there IS something.
539
+ *
540
+ * CAPPED AT 99+: a badge sits in a row of fixed width (the app bar,
541
+ * a bottom icon bar), and a four digit pill pushes the items next
542
+ * to it off a phone screen. A string passes through, for the
543
+ * states that are not counts ("!", "…").
544
+ *
545
+ * ALWAYS RENDERED, hidden when empty, so the runtime setters have a
546
+ * node to write into and never rebuild the control that holds it.
547
+ *
548
+ * role="status" and NOT aria-hidden, deliberately. The control
549
+ * carries an explicit aria-label, and an explicit label REPLACES
550
+ * the element's content for a screen reader -- so a badge inside
551
+ * it would be silent. As its own live region it is both read and
552
+ * announced when the count changes, which is the whole point of a
553
+ * badge that means "something needs you". For the same reason a
554
+ * write that changes nothing touches no DOM: rewriting the same
555
+ * number would have it announced again on every tick.
556
+ *
557
+ * A TONE, optional: one of BADGE_TONES, drawn as Bulma's
558
+ * `is-<tone>` on the badge. It says HOW BAD the count is -- the
559
+ * alarms of an app coloured by the worst state among them. No
560
+ * tone is the default look (the danger colour: a badge exists to
561
+ * pull the eye). An unknown tone is the caller's error: it is
562
+ * logged, and the default look is drawn instead.
563
+ *
564
+ * The text is a Text node, never a string: a value that came from
565
+ * outside must not reach createElement2() as markup.
566
+ *
567
+ * Copyright (c) 2026, ArtGins.
568
+ * All Rights Reserved.
569
+ ***********************************************************************/
570
+ /***************************************************************
571
+ * Constants
572
+ ***************************************************************/
573
+ var BADGE_TONES = Object.freeze([
574
+ "danger",
575
+ "warning",
576
+ "info",
577
+ "success",
578
+ "primary",
579
+ "link"
580
+ ]);
581
+ /************************************************************
582
+ * The text a badge shows for `value`, "" when it shows none.
583
+ ************************************************************/
584
+ function badge_text(value) {
585
+ if (value === null || value === void 0 || value === false) return "";
586
+ if (typeof value === "number") {
587
+ if (!isFinite(value) || value <= 0) return "";
588
+ return value > 99 ? "99+" : String(Math.floor(value));
589
+ }
590
+ return String(value).trim();
591
+ }
592
+ /************************************************************
593
+ * The tone to draw: `tone` when it is one of BADGE_TONES, ""
594
+ * (the default look) when it is empty. Anything else is logged,
595
+ * naming `who`, and answers "".
596
+ ************************************************************/
597
+ function badge_tone(tone, who) {
598
+ if (tone === void 0 || tone === null || tone === "") return "";
599
+ if (BADGE_TONES.indexOf(tone) < 0) {
600
+ (0, _yuneta_gobj_js.log_error)(`${who}: unknown badge tone '${tone}', drawn with the default look (the tones: ${BADGE_TONES.join(", ")})`);
601
+ return "";
602
+ }
603
+ return tone;
604
+ }
605
+ /************************************************************
606
+ * The createElement2() description of a badge. `classes` are
607
+ * the host's (its logical name, `yui-badge` for the look, and
608
+ * where it sits); `who` names the item in the log of an unknown
609
+ * tone.
610
+ ************************************************************/
611
+ function badge_node(value, tone, classes, who) {
612
+ let text = badge_text(value);
613
+ let known = badge_tone(tone, who);
614
+ let attrs = {
615
+ class: known ? `${classes} is-${known}` : classes,
616
+ role: "status"
617
+ };
618
+ if (!text) attrs.hidden = "hidden";
619
+ return [
620
+ "span",
621
+ attrs,
622
+ text_node(text)
623
+ ];
624
+ }
625
+ /************************************************************
626
+ * Write a badge that is already in the DOM, as badge_node()
627
+ * draws it. `tone` is one badge_tone() answered: the setter
628
+ * checks it once for all the badges it paints.
629
+ ************************************************************/
630
+ function paint_badge($badge, value, tone) {
631
+ for (let known of BADGE_TONES) {
632
+ let cls = `is-${known}`;
633
+ if (known === tone) {
634
+ if (!$badge.classList.contains(cls)) $badge.classList.add(cls);
635
+ } else if ($badge.classList.contains(cls)) $badge.classList.remove(cls);
636
+ }
637
+ let text = badge_text(value);
638
+ if (!text) {
639
+ $badge.textContent = "";
640
+ $badge.setAttribute("hidden", "hidden");
641
+ return;
642
+ }
643
+ if ($badge.textContent === text && !$badge.hasAttribute("hidden")) return;
644
+ $badge.textContent = text;
645
+ $badge.removeAttribute("hidden");
646
+ }
647
+ //#endregion
527
648
  //#region src/route_map_model.js
528
649
  /***********************************************************************
529
650
  * route_map_model.js
@@ -962,6 +1083,8 @@ var PRIVATE_DATA$22 = {
962
1083
  stages: {},
963
1084
  navs: [],
964
1085
  item_index: {},
1086
+ nav_badges: {},
1087
+ dynamic_items: {},
965
1088
  section_pos: {},
966
1089
  route_mru: [],
967
1090
  sub_routes: {},
@@ -1585,6 +1708,7 @@ function instantiate_nav_in_zone(gobj, menu, menu_id, zone_id, level, nav_label)
1585
1708
  level,
1586
1709
  show_on: render_cfg.show_on || "",
1587
1710
  back_route: render_cfg.back_route || "",
1711
+ badges: Object.assign({}, priv.nav_badges),
1588
1712
  shell: gobj
1589
1713
  }, gobj);
1590
1714
  let $c = (0, _yuneta_gobj_js.gobj_read_attr)(nav, "$container");
@@ -1774,8 +1898,10 @@ function navigate_to(gobj, route, depth, no_drain) {
1774
1898
  });
1775
1899
  }
1776
1900
  function build_view_gobj(gobj, entry, route, stage) {
1901
+ let priv = gobj.priv;
1777
1902
  let target = entry.target;
1778
1903
  let kw = target.kw || {};
1904
+ if (target.gclass === "C_YUI_NAV") kw = Object.assign({}, kw, { badges: Object.assign({}, priv.nav_badges) });
1779
1905
  let name = target.name || `view_${safe_id(route)}`;
1780
1906
  let view;
1781
1907
  try {
@@ -1876,48 +2002,6 @@ function build_toolbar$1(gobj, config) {
1876
2002
  $zone.appendChild($bar);
1877
2003
  }
1878
2004
  /************************************************************
1879
- * The badge node of a toolbar item — the little count pinned to
1880
- * the corner of its icon (unread notifications, active alarms).
1881
- *
1882
- * ALWAYS rendered, hidden when empty, so the runtime setter has a
1883
- * node to write into and does not have to rebuild the button.
1884
- *
1885
- * EMPTY IS 0, "" AND null ALIKE: a badge reading "0" is worse than
1886
- * no badge — it draws the eye to say nothing. A count is only
1887
- * worth a pixel when there IS something.
1888
- *
1889
- * Capped at 99+: the toolbar is a fixed-width row, and a four
1890
- * digit pill pushes the items next to it off a phone screen.
1891
- *
1892
- * role="status" and NOT aria-hidden, deliberately. The button
1893
- * carries an explicit aria-label, and an explicit label REPLACES
1894
- * the element's content for a screen reader — so a badge inside
1895
- * it would be silent. As its own live region it is both read and
1896
- * announced when the count changes, which is the whole point of a
1897
- * badge that means "something needs you".
1898
- ************************************************************/
1899
- function badge_text(value) {
1900
- if (value === null || value === void 0 || value === false) return "";
1901
- if (typeof value === "number") {
1902
- if (!isFinite(value) || value <= 0) return "";
1903
- return value > 99 ? "99+" : String(Math.floor(value));
1904
- }
1905
- return String(value).trim();
1906
- }
1907
- function badge_node(value) {
1908
- let text = badge_text(value);
1909
- let attrs = {
1910
- class: "yui-toolbar-badge",
1911
- role: "status"
1912
- };
1913
- if (!text) attrs.hidden = "hidden";
1914
- return [
1915
- "span",
1916
- attrs,
1917
- text
1918
- ];
1919
- }
1920
- /************************************************************
1921
2005
  * The attributes that turn a toolbar item into a real link, or
1922
2006
  * null when this item is not one. Answering null is what keeps
1923
2007
  * every caller's default (a <button> that dispatches an action)
@@ -1948,7 +2032,7 @@ function build_toolbar_action_item(gobj, it) {
1948
2032
  [["i", {
1949
2033
  class: it.icon,
1950
2034
  "aria-hidden": "true"
1951
- }], badge_node(it.badge)]
2035
+ }], badge_node(it.badge, it.badge_tone, "TOOLBAR_ITEM_BADGE yui-badge yui-toolbar-badge", `C_YUI_SHELL: toolbar item '${it.id || "?"}'`)]
1952
2036
  ]);
1953
2037
  if (!(0, _yuneta_gobj_js.empty_string)(it.name)) children.push([
1954
2038
  "span",
@@ -2759,6 +2843,41 @@ function update_secondary_nav_visibility(gobj, entry) {
2759
2843
  else $c.classList.add("is-hidden");
2760
2844
  }
2761
2845
  }
2846
+ /************************************************************
2847
+ * Does a menu of this shell declare an item `item_id`: an item
2848
+ * of a menu of app_config.json, of one of their submenus, or
2849
+ * one given to yui_shell_set_submenu() since.
2850
+ ************************************************************/
2851
+ function nav_item_declared(gobj, item_id) {
2852
+ let priv = gobj.priv;
2853
+ let config = (0, _yuneta_gobj_js.gobj_read_attr)(gobj, "config") || {};
2854
+ let menus = (0, _yuneta_gobj_js.is_object)(config.menu) ? config.menu : {};
2855
+ let holds = (items) => (0, _yuneta_gobj_js.is_array)(items) && items.some((it) => it && it.id === item_id);
2856
+ for (let menu_id in menus) {
2857
+ let items = menus[menu_id] && menus[menu_id].items;
2858
+ if (!(0, _yuneta_gobj_js.is_array)(items)) continue;
2859
+ if (holds(items)) return true;
2860
+ for (let it of items) if (it && it.submenu && holds(it.submenu.items)) return true;
2861
+ }
2862
+ for (let parent_item_id in priv.dynamic_items) if (holds(priv.dynamic_items[parent_item_id])) return true;
2863
+ return false;
2864
+ }
2865
+ /************************************************************
2866
+ * Every C_YUI_NAV of this shell: the navs of its menus, and the
2867
+ * section-index landings mounted in a stage.
2868
+ ************************************************************/
2869
+ function shell_navs(gobj) {
2870
+ let priv = gobj.priv;
2871
+ let navs = priv.navs.slice();
2872
+ for (let stage_name in priv.stages) {
2873
+ let views = priv.stages[stage_name].items || {};
2874
+ for (let route in views) {
2875
+ let view = views[route];
2876
+ if (view && (0, _yuneta_gobj_js.gobj_gclass_name)(view) === "C_YUI_NAV") navs.push(view);
2877
+ }
2878
+ }
2879
+ return navs;
2880
+ }
2762
2881
  /***************************
2763
2882
  * Actions
2764
2883
  ***************************/
@@ -3255,35 +3374,79 @@ function yui_shell_set_toolbar_item_icon(shell_gobj, item_id, icon_class) {
3255
3374
  * its icon.
3256
3375
  *
3257
3376
  * yui_shell_set_toolbar_item_badge(shell, "alarms", 3);
3377
+ * yui_shell_set_toolbar_item_badge(shell, "alarms", 3, "warning");
3258
3378
  * yui_shell_set_toolbar_item_badge(shell, "alarms", 0); // hidden
3259
3379
  *
3260
3380
  * This is the API that matters: a count is a RUNTIME fact. The
3261
- * `badge` field of a toolbar item only seeds the first paint, and
3262
- * an app whose number never changes did not need a badge.
3381
+ * `badge` / `badge_tone` fields of a toolbar item only seed the
3382
+ * first paint, and an app whose number never changes did not need
3383
+ * a badge.
3263
3384
  *
3264
3385
  * 0 / "" / null / false all clear it, and a number over 99 renders
3265
- * "99+" — see badge_node(). Accepts a string for the states that
3266
- * are not counts ("!", "…").
3386
+ * "99+" — see yui_badge.js. Accepts a string for the states that
3387
+ * are not counts ("!", "…"). `tone`, optional, is one of
3388
+ * BADGE_TONES ("danger" | "warning" | "info" | "success" |
3389
+ * "primary" | "link"): Bulma's is-<tone> on the badge. Each call
3390
+ * states the WHOLE badge: no tone is the default look, not the
3391
+ * tone it had. The twin of yui_shell_set_nav_item_badge().
3267
3392
  *
3268
3393
  * Silent no-op on an unknown item id: a toolbar that does not
3269
3394
  * declare the item (an app without alarms) must not be an error
3270
- * at every tick of whatever feeds the number.
3395
+ * at every tick of whatever feeds the number. An unknown TONE is
3396
+ * the caller's error: logged, and drawn with the default look.
3271
3397
  ************************************************************/
3272
- function yui_shell_set_toolbar_item_badge(shell_gobj, item_id, value) {
3398
+ function yui_shell_set_toolbar_item_badge(shell_gobj, item_id, value, tone) {
3399
+ tone = badge_tone(tone, `C_YUI_SHELL: yui_shell_set_toolbar_item_badge('${item_id}')`);
3273
3400
  if (!shell_gobj || !(0, _yuneta_gobj_js.is_gobj)(shell_gobj) || (0, _yuneta_gobj_js.empty_string)(item_id)) return;
3274
3401
  let $container = (0, _yuneta_gobj_js.gobj_read_attr)(shell_gobj, "$container");
3275
3402
  if (!$container) return;
3276
3403
  let $badge = $container.querySelector(`[data-toolbar-item-id="${item_id}"] .yui-toolbar-badge`);
3277
3404
  if (!$badge) return;
3278
- let text = badge_text(value);
3279
- if (!text) {
3280
- $badge.textContent = "";
3281
- $badge.setAttribute("hidden", "hidden");
3282
- return;
3283
- }
3284
- if ($badge.textContent === text && !$badge.hasAttribute("hidden")) return;
3285
- $badge.textContent = text;
3286
- $badge.removeAttribute("hidden");
3405
+ paint_badge($badge, value, tone);
3406
+ }
3407
+ /************************************************************
3408
+ * Set (or clear) the badge of a MENU item — the twin of
3409
+ * yui_shell_set_toolbar_item_badge(), for an entry of a menu of
3410
+ * app_config.json, of one of their submenus, or one given to
3411
+ * yui_shell_set_submenu().
3412
+ *
3413
+ * yui_shell_set_nav_item_badge(shell, "alarms", 3);
3414
+ * yui_shell_set_nav_item_badge(shell, "alarms", 3, "warning");
3415
+ * yui_shell_set_nav_item_badge(shell, "alarms", 0); // hidden
3416
+ *
3417
+ * The same value, tone and rules as the toolbar's (yui_badge.js),
3418
+ * each call stating the whole badge.
3419
+ *
3420
+ * EVERY instance of the item gets it: the same entry is drawn by
3421
+ * as many navs as there are zones hosting its menu (a rail on
3422
+ * desktop, an icon bar on a phone), and by a drawer that repeats
3423
+ * it. Each nav repaints it in place (EV_SET_ITEM_BADGE), and
3424
+ * KEEPS it: a rebuild of the nav (a yui_shell_set_submenu()) draws
3425
+ * it again. The shell keeps it too, for the navs it creates
3426
+ * afterwards (a section-index landing, mounted when the user walks
3427
+ * into it). From then on it is the badge of that id: the `badge`
3428
+ * field of an item only seeds the paint until this is called.
3429
+ *
3430
+ * Silent no-op on an id no menu declares, as the toolbar's: a menu
3431
+ * without the entry (an app without alarms) must not be an error
3432
+ * at every tick of whatever feeds the number. An unknown TONE is
3433
+ * the caller's error: logged, and drawn with the default look.
3434
+ ************************************************************/
3435
+ function yui_shell_set_nav_item_badge(shell_gobj, item_id, value, tone) {
3436
+ tone = badge_tone(tone, `C_YUI_SHELL: yui_shell_set_nav_item_badge('${item_id}')`);
3437
+ if (!shell_gobj || !(0, _yuneta_gobj_js.is_gobj)(shell_gobj) || (0, _yuneta_gobj_js.gobj_is_destroying)(shell_gobj) || (0, _yuneta_gobj_js.empty_string)(item_id)) return;
3438
+ let priv = shell_gobj.priv;
3439
+ if (!priv || !nav_item_declared(shell_gobj, item_id)) return;
3440
+ let badge = value === void 0 ? null : value;
3441
+ priv.nav_badges[item_id] = {
3442
+ badge,
3443
+ badge_tone: tone
3444
+ };
3445
+ for (let nav of shell_navs(shell_gobj)) (0, _yuneta_gobj_js.gobj_send_event)(nav, "EV_SET_ITEM_BADGE", {
3446
+ item_id,
3447
+ badge,
3448
+ badge_tone: tone
3449
+ }, shell_gobj);
3287
3450
  }
3288
3451
  /************************************************************
3289
3452
  * Programmatic close of any open toolbar dropdown. Useful for
@@ -3887,7 +4050,7 @@ var MOBILE_MAX = 768;
3887
4050
  ***************************************************************/
3888
4051
  var attrs_table$21 = [
3889
4052
  (0, _yuneta_gobj_js.SDATA)(_yuneta_gobj_js.data_type_t.DTP_POINTER, "subscriber", 0, null, "Subscriber of output events"),
3890
- (0, _yuneta_gobj_js.SDATA)(_yuneta_gobj_js.data_type_t.DTP_POINTER, "$parent", 0, null, "$container will be appended to $parent if not null, else to document.body"),
4053
+ (0, _yuneta_gobj_js.SDATA)(_yuneta_gobj_js.data_type_t.DTP_POINTER, "$parent", 0, null, "$container will be appended to $parent if not null, else to the overlay layer of the shell (document.body when there is none)"),
3891
4054
  (0, _yuneta_gobj_js.SDATA)(_yuneta_gobj_js.data_type_t.DTP_INTEGER, "x", 0, "300", "X position"),
3892
4055
  (0, _yuneta_gobj_js.SDATA)(_yuneta_gobj_js.data_type_t.DTP_INTEGER, "y", 0, "100", "Y position"),
3893
4056
  (0, _yuneta_gobj_js.SDATA)(_yuneta_gobj_js.data_type_t.DTP_INTEGER, "width", 0, "700", "Width of the window"),
@@ -4382,8 +4545,8 @@ function build_ui$18(gobj) {
4382
4545
  }
4383
4546
  (0, _yuneta_gobj_js.refresh_language)($container, i18next.t);
4384
4547
  let $parent = (0, _yuneta_gobj_js.gobj_read_attr)(gobj, "$parent");
4385
- if ($parent) $parent.appendChild($container);
4386
- else document.body.appendChild($container);
4548
+ if (!$parent) $parent = default_parent(gobj);
4549
+ $parent.appendChild($container);
4387
4550
  if ((0, _yuneta_gobj_js.gobj_read_bool_attr)(gobj, "openMaximized")) max$2(gobj);
4388
4551
  else if (!mobile) {
4389
4552
  if ((0, _yuneta_gobj_js.gobj_read_bool_attr)(gobj, "content_size")) $container.style.height = "auto";
@@ -4469,6 +4632,30 @@ function close_window(gobj) {
4469
4632
  };
4470
4633
  }
4471
4634
  /************************************************************
4635
+ * Where a window lives when its host names no `$parent`: the
4636
+ * OVERLAY layer of the shell -- above the content, below the
4637
+ * toolbar's dropdowns, the dialogs and the toasts.
4638
+ *
4639
+ * It used to be document.body, and that put every window above
4640
+ * the WHOLE shell: `.yui-shell` is position:fixed, a stacking
4641
+ * context of its own, so the z-index of its modal layer (99)
4642
+ * counted only inside it, and a window at z-index 11 on the body
4643
+ * covered it. The confirmation a window's own close asks ("all
4644
+ * changes will be lost") opened UNDER the window that asked it,
4645
+ * where no click could reach it; a toast, a dropdown of the
4646
+ * toolbar, any dialog opened with a window on screen, the same.
4647
+ *
4648
+ * Inside the layer the dock's z-index still orders the windows
4649
+ * among themselves. No shell (a page with windows and no shell):
4650
+ * the body, as before.
4651
+ ************************************************************/
4652
+ function default_parent(gobj) {
4653
+ let shell = yui_shell_of(gobj);
4654
+ let layers = shell && shell.priv && shell.priv.layers;
4655
+ if (layers && layers.overlay) return layers.overlay;
4656
+ return document.body;
4657
+ }
4658
+ /************************************************************
4472
4659
  *
4473
4660
  ************************************************************/
4474
4661
  function toggle(gobj) {
@@ -6571,7 +6758,9 @@ function yui_tabulator_name_filters(table, t) {
6571
6758
  let $title = $el.querySelector(".tabulator-col-title");
6572
6759
  if (!title && $title) title = $title.textContent.trim();
6573
6760
  if (!title) continue;
6574
- $input.setAttribute("aria-label", t("filter by column", { column: title }));
6761
+ let name = t("filter by column", { column: title });
6762
+ $input.setAttribute("aria-label", name);
6763
+ $input.setAttribute("title", name);
6575
6764
  }
6576
6765
  } catch (e) {}
6577
6766
  }
@@ -6602,6 +6791,8 @@ function yui_tabulator_name_row_selects(table, t) {
6602
6791
  for (let $box of boxes) {
6603
6792
  $box.setAttribute("aria-label", t("select row"));
6604
6793
  $box.setAttribute("data-i18n-aria-label", "select row");
6794
+ $box.setAttribute("title", t("select row"));
6795
+ $box.setAttribute("data-i18n-title", "select row");
6605
6796
  }
6606
6797
  } catch (e) {}
6607
6798
  }
@@ -9000,11 +9191,19 @@ function name_form_control$1($field, conf) {
9000
9191
  $control.setAttribute("aria-label", (0, i18next.t)(key));
9001
9192
  $control.setAttribute("data-i18n-aria-label", key);
9002
9193
  }
9194
+ if ($control && !$control.getAttribute("title")) {
9195
+ $control.setAttribute("title", (0, i18next.t)(key));
9196
+ $control.setAttribute("data-i18n-title", key);
9197
+ }
9003
9198
  let $ts = $field.querySelector(".ts-control input");
9004
9199
  if ($ts && !$ts.getAttribute("aria-label")) {
9005
9200
  $ts.setAttribute("aria-label", (0, i18next.t)(key));
9006
9201
  $ts.setAttribute("data-i18n-aria-label", key);
9007
9202
  }
9203
+ if ($ts && !$ts.getAttribute("title")) {
9204
+ $ts.setAttribute("title", (0, i18next.t)(key));
9205
+ $ts.setAttribute("data-i18n-title", key);
9206
+ }
9008
9207
  let $clear = $field.querySelector(".ts-wrapper .clear-button");
9009
9208
  if ($clear) {
9010
9209
  const CLEAR_KEY = "remove all selected options";
@@ -10094,6 +10293,8 @@ function create_coordinates(gobj, attrs) {
10094
10293
  type: "text",
10095
10294
  placeholder: (0, i18next.t)("coordinates..."),
10096
10295
  "data-i18n-placeholder": "coordinates...",
10296
+ title: (0, i18next.t)("coordinates"),
10297
+ "data-i18n-title": "coordinates",
10097
10298
  "aria-label": (0, i18next.t)("coordinates"),
10098
10299
  "data-i18n-aria-label": "coordinates"
10099
10300
  },
@@ -23365,18 +23566,34 @@ function register_c_yui_service_view() {
23365
23566
  * contract as every other nav item (data-route/data-item-id/
23366
23567
  * data-disabled), so C_YUI_NAV's delegated click handler and
23367
23568
  * active-route highlight work unchanged.
23569
+ *
23570
+ * `badge_of(it, pinned)`, optional, answers the description of
23571
+ * the card's badge (C_YUI_NAV's, which knows what was set at
23572
+ * runtime). A card is an icon over its label, so the badge goes
23573
+ * on the corner of the icon (`pinned`), or after the label of a
23574
+ * card that has none. Without it a card has no badge.
23368
23575
  ************************************************************/
23369
- function card_descriptor(it, show_label) {
23576
+ function card_descriptor(it, show_label, badge_of) {
23370
23577
  let children = [];
23371
23578
  let label = it.name || "";
23372
- if (it.icon) children.push([
23373
- "span",
23374
- { class: "icon is-medium" },
23375
- ["i", {
23579
+ let pinned = !!it.icon;
23580
+ let badge = badge_of ? badge_of(it, pinned) : null;
23581
+ if (it.icon) {
23582
+ let glyph = ["i", {
23376
23583
  class: it.icon,
23377
23584
  "aria-hidden": "true"
23378
- }]
23379
- ]);
23585
+ }];
23586
+ if (badge) children.push([
23587
+ "span",
23588
+ { class: "icon is-medium yui-nav-icon" },
23589
+ [glyph, badge]
23590
+ ]);
23591
+ else children.push([
23592
+ "span",
23593
+ { class: "icon is-medium" },
23594
+ glyph
23595
+ ]);
23596
+ }
23380
23597
  if (show_label && label) children.push([
23381
23598
  "span",
23382
23599
  {
@@ -23385,6 +23602,7 @@ function card_descriptor(it, show_label) {
23385
23602
  },
23386
23603
  text_node(label)
23387
23604
  ]);
23605
+ if (badge && !pinned) children.push(badge);
23388
23606
  let a_attrs = {
23389
23607
  class: "yui-nav-item yui-nav-card",
23390
23608
  href: it.route ? "#" + it.route : "#",
@@ -23412,12 +23630,13 @@ function card_descriptor(it, show_label) {
23412
23630
  /************************************************************
23413
23631
  * Descriptor of the whole grid. Decorative items (`header`/
23414
23632
  * `divider`) are dropped, same policy as the tabs layout.
23633
+ * `badge_of`: as card_descriptor().
23415
23634
  ************************************************************/
23416
- function cards_grid_descriptor(items, show_label) {
23635
+ function cards_grid_descriptor(items, show_label, badge_of) {
23417
23636
  let cards = [];
23418
23637
  for (let it of items || []) {
23419
23638
  if (!it || it.type === "header" || it.type === "divider") continue;
23420
- cards.push(card_descriptor(it, show_label));
23639
+ cards.push(card_descriptor(it, show_label, badge_of));
23421
23640
  }
23422
23641
  return [
23423
23642
  "div",
@@ -23462,11 +23681,29 @@ function cards_grid_descriptor(items, show_label) {
23462
23681
  *
23463
23682
  * Each item supports:
23464
23683
  * id, name, icon (CSS class or svg id), route, disabled
23465
- * (`badge` was listed here for a long time and NOTHING ever
23466
- * rendered it — a menu item cannot carry a count today. The
23467
- * toolbar can: `badge` on a toolbar item plus
23468
- * yui_shell_set_toolbar_item_badge(). Implement it here the
23469
- * day a menu entry needs one, rather than re-listing it.)
23684
+ * badge — a count, or a short text, carried by the item:
23685
+ * the toolbar badge's rules, helper and live
23686
+ * region (yui_badge.js) -- 0 / "" / null show
23687
+ * none, over 99 reads "99+". It sits on the
23688
+ * corner of the icon where the item is an icon
23689
+ * over its label (icon-bar, cards, a stacked
23690
+ * item) or an icon alone, and after the label
23691
+ * where the item reads as a line (vertical,
23692
+ * drawer, submenu, tabs, an accordion head).
23693
+ * The field only seeds the paint: a count is a
23694
+ * RUNTIME fact, set by
23695
+ * yui_shell_set_nav_item_badge(shell, id, value,
23696
+ * tone), which reaches every nav drawing the id.
23697
+ * Not drawn by "breadcrumb", deliberately: a
23698
+ * crumb names a step of the way in, and the
23699
+ * count belongs to the menu entry that leads
23700
+ * there -- a trail with counts on it reads as a
23701
+ * list of destinations, which is what a
23702
+ * breadcrumb is not. ("backbar" has no items.)
23703
+ * badge_tone — "danger" (the default look), "warning",
23704
+ * "info", "success", "primary" or "link":
23705
+ * Bulma's is-<tone> on the badge. An unknown
23706
+ * one is logged and drawn with the default look.
23470
23707
  * class — extra CSS class(es) on the item (tabs layout), e.g.
23471
23708
  * a per-item state colour like "yui-nav-disconnected"
23472
23709
  * closable — render a trailing ✕ that emits EV_NAV_ITEM_CLOSE
@@ -23477,6 +23714,13 @@ function cards_grid_descriptor(items, show_label) {
23477
23714
  * active highlight restored. The app_config path is simply the
23478
23715
  * first, startup caller of the same mechanism.
23479
23716
  *
23717
+ * One item's badge is set at runtime by sending EV_SET_ITEM_BADGE
23718
+ * {item_id, badge, badge_tone}: painted in place, no rebuild, and
23719
+ * KEPT in the `badges` attr, which wins over the item's own fields
23720
+ * in every later rebuild. The shell sends it to all its navs
23721
+ * (yui_shell_set_nav_item_badge) and hands `badges` to every nav
23722
+ * it creates.
23723
+ *
23480
23724
  * In addition to navigable items, secondary navs accept two
23481
23725
  * decorative item kinds for in-place visual grouping:
23482
23726
  *
@@ -23524,6 +23768,7 @@ var attrs_table$7 = [
23524
23768
  (0, _yuneta_gobj_js.SDATA)(_yuneta_gobj_js.data_type_t.DTP_STRING, "level", 0, "primary", "primary|secondary"),
23525
23769
  (0, _yuneta_gobj_js.SDATA)(_yuneta_gobj_js.data_type_t.DTP_STRING, "show_on", 0, "", "Breakpoint visibility expr (shell show_on syntax); empty = always"),
23526
23770
  (0, _yuneta_gobj_js.SDATA)(_yuneta_gobj_js.data_type_t.DTP_STRING, "back_route", 0, "", "backbar layout: route of the section index the ← returns to"),
23771
+ (0, _yuneta_gobj_js.SDATA)(_yuneta_gobj_js.data_type_t.DTP_DICT, "badges", 0, "{}", "Item id -> {badge, badge_tone} set at runtime (EV_SET_ITEM_BADGE); wins over the item's own fields"),
23527
23772
  (0, _yuneta_gobj_js.SDATA)(_yuneta_gobj_js.data_type_t.DTP_POINTER, "shell", 0, null, "C_YUI_SHELL gobj (for navigation calls)"),
23528
23773
  (0, _yuneta_gobj_js.SDATA)(_yuneta_gobj_js.data_type_t.DTP_POINTER, "$container", 0, null, "Root HTMLElement of this nav"),
23529
23774
  (0, _yuneta_gobj_js.SDATA)(_yuneta_gobj_js.data_type_t.DTP_STRING, "active_route", 0, "", "Currently active route"),
@@ -23738,19 +23983,16 @@ function render_tabs(gobj, items) {
23738
23983
  for (let it of items) {
23739
23984
  if (is_decorative(it)) continue;
23740
23985
  let children = [];
23741
- if (!(0, _yuneta_gobj_js.empty_string)(it.icon)) children.push([
23742
- "span",
23743
- { class: "icon is-small" },
23744
- ["i", {
23745
- class: it.icon,
23746
- "aria-hidden": "true"
23747
- }]
23748
- ]);
23749
- if (show_label && !(0, _yuneta_gobj_js.empty_string)(it.name)) children.push([
23986
+ let has_label = show_label && !(0, _yuneta_gobj_js.empty_string)(it.name);
23987
+ let pinned = !(0, _yuneta_gobj_js.empty_string)(it.icon) && !has_label;
23988
+ let badge_el = item_badge_node(gobj, it, pinned);
23989
+ if (!(0, _yuneta_gobj_js.empty_string)(it.icon)) children.push(icon_span(it.icon, "icon is-small", pinned ? badge_el : null));
23990
+ if (has_label) children.push([
23750
23991
  "span",
23751
23992
  { i18n: it.name },
23752
23993
  text_node(it.name)
23753
23994
  ]);
23995
+ if (!pinned) children.push(badge_el);
23754
23996
  if (it.closable) children.push([
23755
23997
  "span",
23756
23998
  {
@@ -23808,7 +24050,7 @@ function render_tabs(gobj, items) {
23808
24050
  }
23809
24051
  function render_cards(gobj, items) {
23810
24052
  let show_label = (0, _yuneta_gobj_js.gobj_read_attr)(gobj, "show_label");
23811
- return (0, _yuneta_gobj_js.createElement2)(cards_grid_descriptor(items, show_label));
24053
+ return (0, _yuneta_gobj_js.createElement2)(cards_grid_descriptor(items, show_label, (it, pinned) => item_badge_node(gobj, it, pinned)));
23812
24054
  }
23813
24055
  /************************************************************
23814
24056
  * "backbar": a single "← <section>" link back to the section
@@ -23826,6 +24068,9 @@ function render_cards(gobj, items) {
23826
24068
  * other layout — so the delegated click handler and the active
23827
24069
  * highlight work unchanged, and the last crumb marks itself through
23828
24070
  * `active_route` like a tab does.
24071
+ *
24072
+ * No badge, deliberately: a crumb names a step of the way in, and a
24073
+ * count belongs to the menu entry that leads there.
23829
24074
  ************************************************************/
23830
24075
  function render_breadcrumb(gobj, items) {
23831
24076
  let show_label = (0, _yuneta_gobj_js.gobj_read_attr)(gobj, "show_label");
@@ -24004,7 +24249,7 @@ function render_accordion(gobj, items) {
24004
24249
  let $hdr = (0, _yuneta_gobj_js.createElement2)([
24005
24250
  "button",
24006
24251
  head_attrs,
24007
- text_node(head_text)
24252
+ [text_node(head_text), item_badge_node(gobj, it, false)]
24008
24253
  ]);
24009
24254
  $container.appendChild($hdr);
24010
24255
  let $ul = (0, _yuneta_gobj_js.createElement2)(["ul", {
@@ -24051,14 +24296,7 @@ function item_li(gobj, it, opts) {
24051
24296
  let { icon_pos, show_label, stacked } = opts;
24052
24297
  let children = [];
24053
24298
  let label = it.name || "";
24054
- let icon_el = !(0, _yuneta_gobj_js.empty_string)(it.icon) ? [
24055
- "span",
24056
- { class: "icon" },
24057
- ["i", {
24058
- class: it.icon,
24059
- "aria-hidden": "true"
24060
- }]
24061
- ] : null;
24299
+ let is_stacked = !!(stacked || icon_pos === "top" || icon_pos === "bottom");
24062
24300
  let label_el = show_label && !(0, _yuneta_gobj_js.empty_string)(label) ? [
24063
24301
  "span",
24064
24302
  {
@@ -24067,14 +24305,19 @@ function item_li(gobj, it, opts) {
24067
24305
  },
24068
24306
  text_node(label)
24069
24307
  ] : null;
24308
+ let pinned = !(0, _yuneta_gobj_js.empty_string)(it.icon) && (is_stacked || !label_el);
24309
+ let badge_el = item_badge_node(gobj, it, pinned);
24310
+ let icon_el = !(0, _yuneta_gobj_js.empty_string)(it.icon) ? icon_span(it.icon, "icon", pinned ? badge_el : null) : null;
24070
24311
  let a_class = "yui-nav-item";
24071
- if (stacked || icon_pos === "top" || icon_pos === "bottom") a_class += " yui-nav-stacked";
24312
+ if (is_stacked) a_class += " yui-nav-stacked";
24072
24313
  if (icon_pos === "right" || icon_pos === "bottom") {
24073
24314
  if (label_el) children.push(label_el);
24315
+ if (!pinned) children.push(badge_el);
24074
24316
  if (icon_el) children.push(icon_el);
24075
24317
  } else {
24076
24318
  if (icon_el) children.push(icon_el);
24077
24319
  if (label_el) children.push(label_el);
24320
+ if (!pinned) children.push(badge_el);
24078
24321
  }
24079
24322
  let aria_key = label || it.id;
24080
24323
  let a_attrs = {
@@ -24108,14 +24351,9 @@ function item_li(gobj, it, opts) {
24108
24351
  function item_iconbar(gobj, it, opts) {
24109
24352
  let { icon_pos, show_label } = opts;
24110
24353
  let label = it.name || "";
24111
- let icon_el = !(0, _yuneta_gobj_js.empty_string)(it.icon) ? [
24112
- "span",
24113
- { class: "icon is-medium" },
24114
- ["i", {
24115
- class: it.icon,
24116
- "aria-hidden": "true"
24117
- }]
24118
- ] : null;
24354
+ let pinned = !(0, _yuneta_gobj_js.empty_string)(it.icon);
24355
+ let badge_el = item_badge_node(gobj, it, pinned);
24356
+ let icon_el = pinned ? icon_span(it.icon, "icon is-medium", badge_el) : null;
24119
24357
  let label_el = show_label && !(0, _yuneta_gobj_js.empty_string)(label) ? [
24120
24358
  "span",
24121
24359
  {
@@ -24132,6 +24370,7 @@ function item_iconbar(gobj, it, opts) {
24132
24370
  if (icon_el) children.push(icon_el);
24133
24371
  if (label_el) children.push(label_el);
24134
24372
  }
24373
+ if (!pinned) children.push(badge_el);
24135
24374
  let a_attrs = {
24136
24375
  class: "yui-nav-item yui-nav-stacked",
24137
24376
  href: it.route ? "#" + it.route : "#",
@@ -24155,6 +24394,40 @@ function item_iconbar(gobj, it, opts) {
24155
24394
  ]
24156
24395
  ];
24157
24396
  }
24397
+ /************************************************************
24398
+ * The badge of an item (yui_badge.js), always drawn -- hidden
24399
+ * when empty -- so EV_SET_ITEM_BADGE has a node to write into.
24400
+ * What was set at runtime for its id (`badges`) wins over the
24401
+ * item's own `badge` / `badge_tone`. `pinned`: on the corner of
24402
+ * the icon instead of after the label.
24403
+ ************************************************************/
24404
+ function item_badge_node(gobj, it, pinned) {
24405
+ let badges = (0, _yuneta_gobj_js.gobj_read_attr)(gobj, "badges");
24406
+ let state = it;
24407
+ if ((0, _yuneta_gobj_js.is_object)(badges) && (0, _yuneta_gobj_js.is_string)(it.id) && Object.prototype.hasOwnProperty.call(badges, it.id)) state = badges[it.id] || {};
24408
+ let classes = "NAV_ITEM_BADGE yui-badge yui-nav-badge";
24409
+ if (pinned) classes += " yui-nav-badge-pinned";
24410
+ return badge_node(state.badge, state.badge_tone, classes, `C_YUI_NAV '${(0, _yuneta_gobj_js.gobj_read_attr)(gobj, "menu_id") || ""}': item '${it.id}'`);
24411
+ }
24412
+ /************************************************************
24413
+ * An item's icon, with its badge on the corner when given one.
24414
+ ************************************************************/
24415
+ function icon_span(icon, classes, badge_el) {
24416
+ let glyph = ["i", {
24417
+ class: icon,
24418
+ "aria-hidden": "true"
24419
+ }];
24420
+ if (!badge_el) return [
24421
+ "span",
24422
+ { class: classes },
24423
+ glyph
24424
+ ];
24425
+ return [
24426
+ "span",
24427
+ { class: `${classes} yui-nav-icon` },
24428
+ [glyph, badge_el]
24429
+ ];
24430
+ }
24158
24431
  function wire_clicks(gobj, $container) {
24159
24432
  let priv = gobj.priv;
24160
24433
  let handler = (ev) => {
@@ -24231,6 +24504,36 @@ function ac_set_items(gobj, event, kw, src) {
24231
24504
  return 0;
24232
24505
  }
24233
24506
  /************************************************************
24507
+ * EV_SET_ITEM_BADGE {item_id, badge, badge_tone} — the badge of
24508
+ * one item, set at runtime (yui_shell_set_nav_item_badge).
24509
+ *
24510
+ * Kept in `badges`, so every later rebuild draws it, and painted
24511
+ * in place: no rebuild for a count that moves on every tick. An
24512
+ * id this nav does not draw is kept all the same — a later
24513
+ * EV_SET_ITEMS may bring the item.
24514
+ ************************************************************/
24515
+ function ac_set_item_badge(gobj, event, kw, src) {
24516
+ let item_id = kw.item_id;
24517
+ if ((0, _yuneta_gobj_js.empty_string)(item_id)) {
24518
+ (0, _yuneta_gobj_js.log_error)(`C_YUI_NAV: ${event} without item_id, from ${(0, _yuneta_gobj_js.gobj_short_name)(src)}`);
24519
+ return -1;
24520
+ }
24521
+ let menu_id = (0, _yuneta_gobj_js.gobj_read_attr)(gobj, "menu_id") || "";
24522
+ let tone = badge_tone(kw.badge_tone, `C_YUI_NAV '${menu_id}': item '${item_id}'`);
24523
+ let badge = kw.badge === void 0 ? null : kw.badge;
24524
+ let badges = Object.assign({}, (0, _yuneta_gobj_js.gobj_read_attr)(gobj, "badges"));
24525
+ badges[item_id] = {
24526
+ badge,
24527
+ badge_tone: tone
24528
+ };
24529
+ (0, _yuneta_gobj_js.gobj_write_attr)(gobj, "badges", badges);
24530
+ let $c = (0, _yuneta_gobj_js.gobj_read_attr)(gobj, "$container");
24531
+ if (!$c) return 0;
24532
+ let $badges = $c.querySelectorAll(`[data-item-id="${css_escape(item_id)}"] .NAV_ITEM_BADGE`);
24533
+ for (let $badge of $badges) paint_badge($badge, badge, tone);
24534
+ return 0;
24535
+ }
24536
+ /************************************************************
24234
24537
  * EV_ROUTE_CHANGED from the shell — refresh active highlight
24235
24538
  ************************************************************/
24236
24539
  function ac_route_changed(gobj, event, kw, src) {
@@ -24286,18 +24589,27 @@ function create_gclass$7(gclass_name) {
24286
24589
  (0, _yuneta_gobj_js.log_error)(`GClass ALREADY created: ${gclass_name}`);
24287
24590
  return -1;
24288
24591
  }
24289
- const states = [["ST_IDLE", [[
24290
- "EV_ROUTE_CHANGED",
24291
- ac_route_changed,
24292
- null
24293
- ], [
24294
- "EV_SET_ITEMS",
24295
- ac_set_items,
24296
- null
24297
- ]]]];
24592
+ const states = [["ST_IDLE", [
24593
+ [
24594
+ "EV_ROUTE_CHANGED",
24595
+ ac_route_changed,
24596
+ null
24597
+ ],
24598
+ [
24599
+ "EV_SET_ITEMS",
24600
+ ac_set_items,
24601
+ null
24602
+ ],
24603
+ [
24604
+ "EV_SET_ITEM_BADGE",
24605
+ ac_set_item_badge,
24606
+ null
24607
+ ]
24608
+ ]]];
24298
24609
  const event_types = [
24299
24610
  ["EV_ROUTE_CHANGED", 0],
24300
24611
  ["EV_SET_ITEMS", 0],
24612
+ ["EV_SET_ITEM_BADGE", 0],
24301
24613
  ["EV_NAV_CLICKED", _yuneta_gobj_js.event_flag_t.EVF_OUTPUT_EVENT | _yuneta_gobj_js.event_flag_t.EVF_PUBLIC_EVENT],
24302
24614
  ["EV_NAV_ITEM_CLOSE", _yuneta_gobj_js.event_flag_t.EVF_OUTPUT_EVENT | _yuneta_gobj_js.event_flag_t.EVF_PUBLIC_EVENT],
24303
24615
  ["EV_DRAWER_CLOSE_REQUESTED", _yuneta_gobj_js.event_flag_t.EVF_OUTPUT_EVENT | _yuneta_gobj_js.event_flag_t.EVF_PUBLIC_EVENT]
@@ -83309,6 +83621,7 @@ exports.yui_shell_register_event_handler = yui_shell_register_event_handler;
83309
83621
  exports.yui_shell_register_overlay = yui_shell_register_overlay;
83310
83622
  exports.yui_shell_set_avatar_provider = yui_shell_set_avatar_provider;
83311
83623
  exports.yui_shell_set_connection_state = yui_shell_set_connection_state;
83624
+ exports.yui_shell_set_nav_item_badge = yui_shell_set_nav_item_badge;
83312
83625
  exports.yui_shell_set_sub_routes = yui_shell_set_sub_routes;
83313
83626
  exports.yui_shell_set_toolbar_item_badge = yui_shell_set_toolbar_item_badge;
83314
83627
  exports.yui_shell_set_toolbar_item_icon = yui_shell_set_toolbar_item_icon;