@yuneta/gobj-ui 7.26.29 → 7.26.30

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
@@ -23365,18 +23528,34 @@ function register_c_yui_service_view() {
23365
23528
  * contract as every other nav item (data-route/data-item-id/
23366
23529
  * data-disabled), so C_YUI_NAV's delegated click handler and
23367
23530
  * active-route highlight work unchanged.
23531
+ *
23532
+ * `badge_of(it, pinned)`, optional, answers the description of
23533
+ * the card's badge (C_YUI_NAV's, which knows what was set at
23534
+ * runtime). A card is an icon over its label, so the badge goes
23535
+ * on the corner of the icon (`pinned`), or after the label of a
23536
+ * card that has none. Without it a card has no badge.
23368
23537
  ************************************************************/
23369
- function card_descriptor(it, show_label) {
23538
+ function card_descriptor(it, show_label, badge_of) {
23370
23539
  let children = [];
23371
23540
  let label = it.name || "";
23372
- if (it.icon) children.push([
23373
- "span",
23374
- { class: "icon is-medium" },
23375
- ["i", {
23541
+ let pinned = !!it.icon;
23542
+ let badge = badge_of ? badge_of(it, pinned) : null;
23543
+ if (it.icon) {
23544
+ let glyph = ["i", {
23376
23545
  class: it.icon,
23377
23546
  "aria-hidden": "true"
23378
- }]
23379
- ]);
23547
+ }];
23548
+ if (badge) children.push([
23549
+ "span",
23550
+ { class: "icon is-medium yui-nav-icon" },
23551
+ [glyph, badge]
23552
+ ]);
23553
+ else children.push([
23554
+ "span",
23555
+ { class: "icon is-medium" },
23556
+ glyph
23557
+ ]);
23558
+ }
23380
23559
  if (show_label && label) children.push([
23381
23560
  "span",
23382
23561
  {
@@ -23385,6 +23564,7 @@ function card_descriptor(it, show_label) {
23385
23564
  },
23386
23565
  text_node(label)
23387
23566
  ]);
23567
+ if (badge && !pinned) children.push(badge);
23388
23568
  let a_attrs = {
23389
23569
  class: "yui-nav-item yui-nav-card",
23390
23570
  href: it.route ? "#" + it.route : "#",
@@ -23412,12 +23592,13 @@ function card_descriptor(it, show_label) {
23412
23592
  /************************************************************
23413
23593
  * Descriptor of the whole grid. Decorative items (`header`/
23414
23594
  * `divider`) are dropped, same policy as the tabs layout.
23595
+ * `badge_of`: as card_descriptor().
23415
23596
  ************************************************************/
23416
- function cards_grid_descriptor(items, show_label) {
23597
+ function cards_grid_descriptor(items, show_label, badge_of) {
23417
23598
  let cards = [];
23418
23599
  for (let it of items || []) {
23419
23600
  if (!it || it.type === "header" || it.type === "divider") continue;
23420
- cards.push(card_descriptor(it, show_label));
23601
+ cards.push(card_descriptor(it, show_label, badge_of));
23421
23602
  }
23422
23603
  return [
23423
23604
  "div",
@@ -23462,11 +23643,29 @@ function cards_grid_descriptor(items, show_label) {
23462
23643
  *
23463
23644
  * Each item supports:
23464
23645
  * 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.)
23646
+ * badge — a count, or a short text, carried by the item:
23647
+ * the toolbar badge's rules, helper and live
23648
+ * region (yui_badge.js) -- 0 / "" / null show
23649
+ * none, over 99 reads "99+". It sits on the
23650
+ * corner of the icon where the item is an icon
23651
+ * over its label (icon-bar, cards, a stacked
23652
+ * item) or an icon alone, and after the label
23653
+ * where the item reads as a line (vertical,
23654
+ * drawer, submenu, tabs, an accordion head).
23655
+ * The field only seeds the paint: a count is a
23656
+ * RUNTIME fact, set by
23657
+ * yui_shell_set_nav_item_badge(shell, id, value,
23658
+ * tone), which reaches every nav drawing the id.
23659
+ * Not drawn by "breadcrumb", deliberately: a
23660
+ * crumb names a step of the way in, and the
23661
+ * count belongs to the menu entry that leads
23662
+ * there -- a trail with counts on it reads as a
23663
+ * list of destinations, which is what a
23664
+ * breadcrumb is not. ("backbar" has no items.)
23665
+ * badge_tone — "danger" (the default look), "warning",
23666
+ * "info", "success", "primary" or "link":
23667
+ * Bulma's is-<tone> on the badge. An unknown
23668
+ * one is logged and drawn with the default look.
23470
23669
  * class — extra CSS class(es) on the item (tabs layout), e.g.
23471
23670
  * a per-item state colour like "yui-nav-disconnected"
23472
23671
  * closable — render a trailing ✕ that emits EV_NAV_ITEM_CLOSE
@@ -23477,6 +23676,13 @@ function cards_grid_descriptor(items, show_label) {
23477
23676
  * active highlight restored. The app_config path is simply the
23478
23677
  * first, startup caller of the same mechanism.
23479
23678
  *
23679
+ * One item's badge is set at runtime by sending EV_SET_ITEM_BADGE
23680
+ * {item_id, badge, badge_tone}: painted in place, no rebuild, and
23681
+ * KEPT in the `badges` attr, which wins over the item's own fields
23682
+ * in every later rebuild. The shell sends it to all its navs
23683
+ * (yui_shell_set_nav_item_badge) and hands `badges` to every nav
23684
+ * it creates.
23685
+ *
23480
23686
  * In addition to navigable items, secondary navs accept two
23481
23687
  * decorative item kinds for in-place visual grouping:
23482
23688
  *
@@ -23524,6 +23730,7 @@ var attrs_table$7 = [
23524
23730
  (0, _yuneta_gobj_js.SDATA)(_yuneta_gobj_js.data_type_t.DTP_STRING, "level", 0, "primary", "primary|secondary"),
23525
23731
  (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
23732
  (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"),
23733
+ (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
23734
  (0, _yuneta_gobj_js.SDATA)(_yuneta_gobj_js.data_type_t.DTP_POINTER, "shell", 0, null, "C_YUI_SHELL gobj (for navigation calls)"),
23528
23735
  (0, _yuneta_gobj_js.SDATA)(_yuneta_gobj_js.data_type_t.DTP_POINTER, "$container", 0, null, "Root HTMLElement of this nav"),
23529
23736
  (0, _yuneta_gobj_js.SDATA)(_yuneta_gobj_js.data_type_t.DTP_STRING, "active_route", 0, "", "Currently active route"),
@@ -23738,19 +23945,16 @@ function render_tabs(gobj, items) {
23738
23945
  for (let it of items) {
23739
23946
  if (is_decorative(it)) continue;
23740
23947
  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([
23948
+ let has_label = show_label && !(0, _yuneta_gobj_js.empty_string)(it.name);
23949
+ let pinned = !(0, _yuneta_gobj_js.empty_string)(it.icon) && !has_label;
23950
+ let badge_el = item_badge_node(gobj, it, pinned);
23951
+ if (!(0, _yuneta_gobj_js.empty_string)(it.icon)) children.push(icon_span(it.icon, "icon is-small", pinned ? badge_el : null));
23952
+ if (has_label) children.push([
23750
23953
  "span",
23751
23954
  { i18n: it.name },
23752
23955
  text_node(it.name)
23753
23956
  ]);
23957
+ if (!pinned) children.push(badge_el);
23754
23958
  if (it.closable) children.push([
23755
23959
  "span",
23756
23960
  {
@@ -23808,7 +24012,7 @@ function render_tabs(gobj, items) {
23808
24012
  }
23809
24013
  function render_cards(gobj, items) {
23810
24014
  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));
24015
+ return (0, _yuneta_gobj_js.createElement2)(cards_grid_descriptor(items, show_label, (it, pinned) => item_badge_node(gobj, it, pinned)));
23812
24016
  }
23813
24017
  /************************************************************
23814
24018
  * "backbar": a single "← <section>" link back to the section
@@ -23826,6 +24030,9 @@ function render_cards(gobj, items) {
23826
24030
  * other layout — so the delegated click handler and the active
23827
24031
  * highlight work unchanged, and the last crumb marks itself through
23828
24032
  * `active_route` like a tab does.
24033
+ *
24034
+ * No badge, deliberately: a crumb names a step of the way in, and a
24035
+ * count belongs to the menu entry that leads there.
23829
24036
  ************************************************************/
23830
24037
  function render_breadcrumb(gobj, items) {
23831
24038
  let show_label = (0, _yuneta_gobj_js.gobj_read_attr)(gobj, "show_label");
@@ -24004,7 +24211,7 @@ function render_accordion(gobj, items) {
24004
24211
  let $hdr = (0, _yuneta_gobj_js.createElement2)([
24005
24212
  "button",
24006
24213
  head_attrs,
24007
- text_node(head_text)
24214
+ [text_node(head_text), item_badge_node(gobj, it, false)]
24008
24215
  ]);
24009
24216
  $container.appendChild($hdr);
24010
24217
  let $ul = (0, _yuneta_gobj_js.createElement2)(["ul", {
@@ -24051,14 +24258,7 @@ function item_li(gobj, it, opts) {
24051
24258
  let { icon_pos, show_label, stacked } = opts;
24052
24259
  let children = [];
24053
24260
  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;
24261
+ let is_stacked = !!(stacked || icon_pos === "top" || icon_pos === "bottom");
24062
24262
  let label_el = show_label && !(0, _yuneta_gobj_js.empty_string)(label) ? [
24063
24263
  "span",
24064
24264
  {
@@ -24067,14 +24267,19 @@ function item_li(gobj, it, opts) {
24067
24267
  },
24068
24268
  text_node(label)
24069
24269
  ] : null;
24270
+ let pinned = !(0, _yuneta_gobj_js.empty_string)(it.icon) && (is_stacked || !label_el);
24271
+ let badge_el = item_badge_node(gobj, it, pinned);
24272
+ let icon_el = !(0, _yuneta_gobj_js.empty_string)(it.icon) ? icon_span(it.icon, "icon", pinned ? badge_el : null) : null;
24070
24273
  let a_class = "yui-nav-item";
24071
- if (stacked || icon_pos === "top" || icon_pos === "bottom") a_class += " yui-nav-stacked";
24274
+ if (is_stacked) a_class += " yui-nav-stacked";
24072
24275
  if (icon_pos === "right" || icon_pos === "bottom") {
24073
24276
  if (label_el) children.push(label_el);
24277
+ if (!pinned) children.push(badge_el);
24074
24278
  if (icon_el) children.push(icon_el);
24075
24279
  } else {
24076
24280
  if (icon_el) children.push(icon_el);
24077
24281
  if (label_el) children.push(label_el);
24282
+ if (!pinned) children.push(badge_el);
24078
24283
  }
24079
24284
  let aria_key = label || it.id;
24080
24285
  let a_attrs = {
@@ -24108,14 +24313,9 @@ function item_li(gobj, it, opts) {
24108
24313
  function item_iconbar(gobj, it, opts) {
24109
24314
  let { icon_pos, show_label } = opts;
24110
24315
  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;
24316
+ let pinned = !(0, _yuneta_gobj_js.empty_string)(it.icon);
24317
+ let badge_el = item_badge_node(gobj, it, pinned);
24318
+ let icon_el = pinned ? icon_span(it.icon, "icon is-medium", badge_el) : null;
24119
24319
  let label_el = show_label && !(0, _yuneta_gobj_js.empty_string)(label) ? [
24120
24320
  "span",
24121
24321
  {
@@ -24132,6 +24332,7 @@ function item_iconbar(gobj, it, opts) {
24132
24332
  if (icon_el) children.push(icon_el);
24133
24333
  if (label_el) children.push(label_el);
24134
24334
  }
24335
+ if (!pinned) children.push(badge_el);
24135
24336
  let a_attrs = {
24136
24337
  class: "yui-nav-item yui-nav-stacked",
24137
24338
  href: it.route ? "#" + it.route : "#",
@@ -24155,6 +24356,40 @@ function item_iconbar(gobj, it, opts) {
24155
24356
  ]
24156
24357
  ];
24157
24358
  }
24359
+ /************************************************************
24360
+ * The badge of an item (yui_badge.js), always drawn -- hidden
24361
+ * when empty -- so EV_SET_ITEM_BADGE has a node to write into.
24362
+ * What was set at runtime for its id (`badges`) wins over the
24363
+ * item's own `badge` / `badge_tone`. `pinned`: on the corner of
24364
+ * the icon instead of after the label.
24365
+ ************************************************************/
24366
+ function item_badge_node(gobj, it, pinned) {
24367
+ let badges = (0, _yuneta_gobj_js.gobj_read_attr)(gobj, "badges");
24368
+ let state = it;
24369
+ 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] || {};
24370
+ let classes = "NAV_ITEM_BADGE yui-badge yui-nav-badge";
24371
+ if (pinned) classes += " yui-nav-badge-pinned";
24372
+ 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}'`);
24373
+ }
24374
+ /************************************************************
24375
+ * An item's icon, with its badge on the corner when given one.
24376
+ ************************************************************/
24377
+ function icon_span(icon, classes, badge_el) {
24378
+ let glyph = ["i", {
24379
+ class: icon,
24380
+ "aria-hidden": "true"
24381
+ }];
24382
+ if (!badge_el) return [
24383
+ "span",
24384
+ { class: classes },
24385
+ glyph
24386
+ ];
24387
+ return [
24388
+ "span",
24389
+ { class: `${classes} yui-nav-icon` },
24390
+ [glyph, badge_el]
24391
+ ];
24392
+ }
24158
24393
  function wire_clicks(gobj, $container) {
24159
24394
  let priv = gobj.priv;
24160
24395
  let handler = (ev) => {
@@ -24231,6 +24466,36 @@ function ac_set_items(gobj, event, kw, src) {
24231
24466
  return 0;
24232
24467
  }
24233
24468
  /************************************************************
24469
+ * EV_SET_ITEM_BADGE {item_id, badge, badge_tone} — the badge of
24470
+ * one item, set at runtime (yui_shell_set_nav_item_badge).
24471
+ *
24472
+ * Kept in `badges`, so every later rebuild draws it, and painted
24473
+ * in place: no rebuild for a count that moves on every tick. An
24474
+ * id this nav does not draw is kept all the same — a later
24475
+ * EV_SET_ITEMS may bring the item.
24476
+ ************************************************************/
24477
+ function ac_set_item_badge(gobj, event, kw, src) {
24478
+ let item_id = kw.item_id;
24479
+ if ((0, _yuneta_gobj_js.empty_string)(item_id)) {
24480
+ (0, _yuneta_gobj_js.log_error)(`C_YUI_NAV: ${event} without item_id, from ${(0, _yuneta_gobj_js.gobj_short_name)(src)}`);
24481
+ return -1;
24482
+ }
24483
+ let menu_id = (0, _yuneta_gobj_js.gobj_read_attr)(gobj, "menu_id") || "";
24484
+ let tone = badge_tone(kw.badge_tone, `C_YUI_NAV '${menu_id}': item '${item_id}'`);
24485
+ let badge = kw.badge === void 0 ? null : kw.badge;
24486
+ let badges = Object.assign({}, (0, _yuneta_gobj_js.gobj_read_attr)(gobj, "badges"));
24487
+ badges[item_id] = {
24488
+ badge,
24489
+ badge_tone: tone
24490
+ };
24491
+ (0, _yuneta_gobj_js.gobj_write_attr)(gobj, "badges", badges);
24492
+ let $c = (0, _yuneta_gobj_js.gobj_read_attr)(gobj, "$container");
24493
+ if (!$c) return 0;
24494
+ let $badges = $c.querySelectorAll(`[data-item-id="${css_escape(item_id)}"] .NAV_ITEM_BADGE`);
24495
+ for (let $badge of $badges) paint_badge($badge, badge, tone);
24496
+ return 0;
24497
+ }
24498
+ /************************************************************
24234
24499
  * EV_ROUTE_CHANGED from the shell — refresh active highlight
24235
24500
  ************************************************************/
24236
24501
  function ac_route_changed(gobj, event, kw, src) {
@@ -24286,18 +24551,27 @@ function create_gclass$7(gclass_name) {
24286
24551
  (0, _yuneta_gobj_js.log_error)(`GClass ALREADY created: ${gclass_name}`);
24287
24552
  return -1;
24288
24553
  }
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
- ]]]];
24554
+ const states = [["ST_IDLE", [
24555
+ [
24556
+ "EV_ROUTE_CHANGED",
24557
+ ac_route_changed,
24558
+ null
24559
+ ],
24560
+ [
24561
+ "EV_SET_ITEMS",
24562
+ ac_set_items,
24563
+ null
24564
+ ],
24565
+ [
24566
+ "EV_SET_ITEM_BADGE",
24567
+ ac_set_item_badge,
24568
+ null
24569
+ ]
24570
+ ]]];
24298
24571
  const event_types = [
24299
24572
  ["EV_ROUTE_CHANGED", 0],
24300
24573
  ["EV_SET_ITEMS", 0],
24574
+ ["EV_SET_ITEM_BADGE", 0],
24301
24575
  ["EV_NAV_CLICKED", _yuneta_gobj_js.event_flag_t.EVF_OUTPUT_EVENT | _yuneta_gobj_js.event_flag_t.EVF_PUBLIC_EVENT],
24302
24576
  ["EV_NAV_ITEM_CLOSE", _yuneta_gobj_js.event_flag_t.EVF_OUTPUT_EVENT | _yuneta_gobj_js.event_flag_t.EVF_PUBLIC_EVENT],
24303
24577
  ["EV_DRAWER_CLOSE_REQUESTED", _yuneta_gobj_js.event_flag_t.EVF_OUTPUT_EVENT | _yuneta_gobj_js.event_flag_t.EVF_PUBLIC_EVENT]
@@ -83309,6 +83583,7 @@ exports.yui_shell_register_event_handler = yui_shell_register_event_handler;
83309
83583
  exports.yui_shell_register_overlay = yui_shell_register_overlay;
83310
83584
  exports.yui_shell_set_avatar_provider = yui_shell_set_avatar_provider;
83311
83585
  exports.yui_shell_set_connection_state = yui_shell_set_connection_state;
83586
+ exports.yui_shell_set_nav_item_badge = yui_shell_set_nav_item_badge;
83312
83587
  exports.yui_shell_set_sub_routes = yui_shell_set_sub_routes;
83313
83588
  exports.yui_shell_set_toolbar_item_badge = yui_shell_set_toolbar_item_badge;
83314
83589
  exports.yui_shell_set_toolbar_item_icon = yui_shell_set_toolbar_item_icon;