@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.
@@ -519,6 +519,127 @@ function text_list_item_formatter(label) {
519
519
  return span;
520
520
  }
521
521
  //#endregion
522
+ //#region src/yui_badge.js
523
+ /***********************************************************************
524
+ * yui_badge.js
525
+ *
526
+ * The BADGE: the little count a control carries -- unread
527
+ * notifications, active alarms. The toolbar items of C_YUI_SHELL
528
+ * and the menu items of C_YUI_NAV build and paint theirs here, so
529
+ * the two follow the same rules and cannot drift apart:
530
+ *
531
+ * EMPTY IS 0, "" AND null ALIKE (false and undefined too): a badge
532
+ * reading "0" is worse than no badge -- it draws the eye to say
533
+ * nothing. A count is only worth a pixel when there IS something.
534
+ *
535
+ * CAPPED AT 99+: a badge sits in a row of fixed width (the app bar,
536
+ * a bottom icon bar), and a four digit pill pushes the items next
537
+ * to it off a phone screen. A string passes through, for the
538
+ * states that are not counts ("!", "…").
539
+ *
540
+ * ALWAYS RENDERED, hidden when empty, so the runtime setters have a
541
+ * node to write into and never rebuild the control that holds it.
542
+ *
543
+ * role="status" and NOT aria-hidden, deliberately. The control
544
+ * carries an explicit aria-label, and an explicit label REPLACES
545
+ * the element's content for a screen reader -- so a badge inside
546
+ * it would be silent. As its own live region it is both read and
547
+ * announced when the count changes, which is the whole point of a
548
+ * badge that means "something needs you". For the same reason a
549
+ * write that changes nothing touches no DOM: rewriting the same
550
+ * number would have it announced again on every tick.
551
+ *
552
+ * A TONE, optional: one of BADGE_TONES, drawn as Bulma's
553
+ * `is-<tone>` on the badge. It says HOW BAD the count is -- the
554
+ * alarms of an app coloured by the worst state among them. No
555
+ * tone is the default look (the danger colour: a badge exists to
556
+ * pull the eye). An unknown tone is the caller's error: it is
557
+ * logged, and the default look is drawn instead.
558
+ *
559
+ * The text is a Text node, never a string: a value that came from
560
+ * outside must not reach createElement2() as markup.
561
+ *
562
+ * Copyright (c) 2026, ArtGins.
563
+ * All Rights Reserved.
564
+ ***********************************************************************/
565
+ /***************************************************************
566
+ * Constants
567
+ ***************************************************************/
568
+ var BADGE_TONES = Object.freeze([
569
+ "danger",
570
+ "warning",
571
+ "info",
572
+ "success",
573
+ "primary",
574
+ "link"
575
+ ]);
576
+ /************************************************************
577
+ * The text a badge shows for `value`, "" when it shows none.
578
+ ************************************************************/
579
+ function badge_text(value) {
580
+ if (value === null || value === void 0 || value === false) return "";
581
+ if (typeof value === "number") {
582
+ if (!isFinite(value) || value <= 0) return "";
583
+ return value > 99 ? "99+" : String(Math.floor(value));
584
+ }
585
+ return String(value).trim();
586
+ }
587
+ /************************************************************
588
+ * The tone to draw: `tone` when it is one of BADGE_TONES, ""
589
+ * (the default look) when it is empty. Anything else is logged,
590
+ * naming `who`, and answers "".
591
+ ************************************************************/
592
+ function badge_tone(tone, who) {
593
+ if (tone === void 0 || tone === null || tone === "") return "";
594
+ if (BADGE_TONES.indexOf(tone) < 0) {
595
+ log_error(`${who}: unknown badge tone '${tone}', drawn with the default look (the tones: ${BADGE_TONES.join(", ")})`);
596
+ return "";
597
+ }
598
+ return tone;
599
+ }
600
+ /************************************************************
601
+ * The createElement2() description of a badge. `classes` are
602
+ * the host's (its logical name, `yui-badge` for the look, and
603
+ * where it sits); `who` names the item in the log of an unknown
604
+ * tone.
605
+ ************************************************************/
606
+ function badge_node(value, tone, classes, who) {
607
+ let text = badge_text(value);
608
+ let known = badge_tone(tone, who);
609
+ let attrs = {
610
+ class: known ? `${classes} is-${known}` : classes,
611
+ role: "status"
612
+ };
613
+ if (!text) attrs.hidden = "hidden";
614
+ return [
615
+ "span",
616
+ attrs,
617
+ text_node(text)
618
+ ];
619
+ }
620
+ /************************************************************
621
+ * Write a badge that is already in the DOM, as badge_node()
622
+ * draws it. `tone` is one badge_tone() answered: the setter
623
+ * checks it once for all the badges it paints.
624
+ ************************************************************/
625
+ function paint_badge($badge, value, tone) {
626
+ for (let known of BADGE_TONES) {
627
+ let cls = `is-${known}`;
628
+ if (known === tone) {
629
+ if (!$badge.classList.contains(cls)) $badge.classList.add(cls);
630
+ } else if ($badge.classList.contains(cls)) $badge.classList.remove(cls);
631
+ }
632
+ let text = badge_text(value);
633
+ if (!text) {
634
+ $badge.textContent = "";
635
+ $badge.setAttribute("hidden", "hidden");
636
+ return;
637
+ }
638
+ if ($badge.textContent === text && !$badge.hasAttribute("hidden")) return;
639
+ $badge.textContent = text;
640
+ $badge.removeAttribute("hidden");
641
+ }
642
+ //#endregion
522
643
  //#region src/route_map_model.js
523
644
  /***********************************************************************
524
645
  * route_map_model.js
@@ -957,6 +1078,8 @@ var PRIVATE_DATA$22 = {
957
1078
  stages: {},
958
1079
  navs: [],
959
1080
  item_index: {},
1081
+ nav_badges: {},
1082
+ dynamic_items: {},
960
1083
  section_pos: {},
961
1084
  route_mru: [],
962
1085
  sub_routes: {},
@@ -1580,6 +1703,7 @@ function instantiate_nav_in_zone(gobj, menu, menu_id, zone_id, level, nav_label)
1580
1703
  level,
1581
1704
  show_on: render_cfg.show_on || "",
1582
1705
  back_route: render_cfg.back_route || "",
1706
+ badges: Object.assign({}, priv.nav_badges),
1583
1707
  shell: gobj
1584
1708
  }, gobj);
1585
1709
  let $c = gobj_read_attr(nav, "$container");
@@ -1769,8 +1893,10 @@ function navigate_to(gobj, route, depth, no_drain) {
1769
1893
  });
1770
1894
  }
1771
1895
  function build_view_gobj(gobj, entry, route, stage) {
1896
+ let priv = gobj.priv;
1772
1897
  let target = entry.target;
1773
1898
  let kw = target.kw || {};
1899
+ if (target.gclass === "C_YUI_NAV") kw = Object.assign({}, kw, { badges: Object.assign({}, priv.nav_badges) });
1774
1900
  let name = target.name || `view_${safe_id(route)}`;
1775
1901
  let view;
1776
1902
  try {
@@ -1871,48 +1997,6 @@ function build_toolbar$1(gobj, config) {
1871
1997
  $zone.appendChild($bar);
1872
1998
  }
1873
1999
  /************************************************************
1874
- * The badge node of a toolbar item — the little count pinned to
1875
- * the corner of its icon (unread notifications, active alarms).
1876
- *
1877
- * ALWAYS rendered, hidden when empty, so the runtime setter has a
1878
- * node to write into and does not have to rebuild the button.
1879
- *
1880
- * EMPTY IS 0, "" AND null ALIKE: a badge reading "0" is worse than
1881
- * no badge — it draws the eye to say nothing. A count is only
1882
- * worth a pixel when there IS something.
1883
- *
1884
- * Capped at 99+: the toolbar is a fixed-width row, and a four
1885
- * digit pill pushes the items next to it off a phone screen.
1886
- *
1887
- * role="status" and NOT aria-hidden, deliberately. The button
1888
- * carries an explicit aria-label, and an explicit label REPLACES
1889
- * the element's content for a screen reader — so a badge inside
1890
- * it would be silent. As its own live region it is both read and
1891
- * announced when the count changes, which is the whole point of a
1892
- * badge that means "something needs you".
1893
- ************************************************************/
1894
- function badge_text(value) {
1895
- if (value === null || value === void 0 || value === false) return "";
1896
- if (typeof value === "number") {
1897
- if (!isFinite(value) || value <= 0) return "";
1898
- return value > 99 ? "99+" : String(Math.floor(value));
1899
- }
1900
- return String(value).trim();
1901
- }
1902
- function badge_node(value) {
1903
- let text = badge_text(value);
1904
- let attrs = {
1905
- class: "yui-toolbar-badge",
1906
- role: "status"
1907
- };
1908
- if (!text) attrs.hidden = "hidden";
1909
- return [
1910
- "span",
1911
- attrs,
1912
- text
1913
- ];
1914
- }
1915
- /************************************************************
1916
2000
  * The attributes that turn a toolbar item into a real link, or
1917
2001
  * null when this item is not one. Answering null is what keeps
1918
2002
  * every caller's default (a <button> that dispatches an action)
@@ -1943,7 +2027,7 @@ function build_toolbar_action_item(gobj, it) {
1943
2027
  [["i", {
1944
2028
  class: it.icon,
1945
2029
  "aria-hidden": "true"
1946
- }], badge_node(it.badge)]
2030
+ }], badge_node(it.badge, it.badge_tone, "TOOLBAR_ITEM_BADGE yui-badge yui-toolbar-badge", `C_YUI_SHELL: toolbar item '${it.id || "?"}'`)]
1947
2031
  ]);
1948
2032
  if (!empty_string(it.name)) children.push([
1949
2033
  "span",
@@ -2754,6 +2838,41 @@ function update_secondary_nav_visibility(gobj, entry) {
2754
2838
  else $c.classList.add("is-hidden");
2755
2839
  }
2756
2840
  }
2841
+ /************************************************************
2842
+ * Does a menu of this shell declare an item `item_id`: an item
2843
+ * of a menu of app_config.json, of one of their submenus, or
2844
+ * one given to yui_shell_set_submenu() since.
2845
+ ************************************************************/
2846
+ function nav_item_declared(gobj, item_id) {
2847
+ let priv = gobj.priv;
2848
+ let config = gobj_read_attr(gobj, "config") || {};
2849
+ let menus = is_object(config.menu) ? config.menu : {};
2850
+ let holds = (items) => is_array(items) && items.some((it) => it && it.id === item_id);
2851
+ for (let menu_id in menus) {
2852
+ let items = menus[menu_id] && menus[menu_id].items;
2853
+ if (!is_array(items)) continue;
2854
+ if (holds(items)) return true;
2855
+ for (let it of items) if (it && it.submenu && holds(it.submenu.items)) return true;
2856
+ }
2857
+ for (let parent_item_id in priv.dynamic_items) if (holds(priv.dynamic_items[parent_item_id])) return true;
2858
+ return false;
2859
+ }
2860
+ /************************************************************
2861
+ * Every C_YUI_NAV of this shell: the navs of its menus, and the
2862
+ * section-index landings mounted in a stage.
2863
+ ************************************************************/
2864
+ function shell_navs(gobj) {
2865
+ let priv = gobj.priv;
2866
+ let navs = priv.navs.slice();
2867
+ for (let stage_name in priv.stages) {
2868
+ let views = priv.stages[stage_name].items || {};
2869
+ for (let route in views) {
2870
+ let view = views[route];
2871
+ if (view && gobj_gclass_name(view) === "C_YUI_NAV") navs.push(view);
2872
+ }
2873
+ }
2874
+ return navs;
2875
+ }
2757
2876
  /***************************
2758
2877
  * Actions
2759
2878
  ***************************/
@@ -3250,35 +3369,79 @@ function yui_shell_set_toolbar_item_icon(shell_gobj, item_id, icon_class) {
3250
3369
  * its icon.
3251
3370
  *
3252
3371
  * yui_shell_set_toolbar_item_badge(shell, "alarms", 3);
3372
+ * yui_shell_set_toolbar_item_badge(shell, "alarms", 3, "warning");
3253
3373
  * yui_shell_set_toolbar_item_badge(shell, "alarms", 0); // hidden
3254
3374
  *
3255
3375
  * This is the API that matters: a count is a RUNTIME fact. The
3256
- * `badge` field of a toolbar item only seeds the first paint, and
3257
- * an app whose number never changes did not need a badge.
3376
+ * `badge` / `badge_tone` fields of a toolbar item only seed the
3377
+ * first paint, and an app whose number never changes did not need
3378
+ * a badge.
3258
3379
  *
3259
3380
  * 0 / "" / null / false all clear it, and a number over 99 renders
3260
- * "99+" — see badge_node(). Accepts a string for the states that
3261
- * are not counts ("!", "…").
3381
+ * "99+" — see yui_badge.js. Accepts a string for the states that
3382
+ * are not counts ("!", "…"). `tone`, optional, is one of
3383
+ * BADGE_TONES ("danger" | "warning" | "info" | "success" |
3384
+ * "primary" | "link"): Bulma's is-<tone> on the badge. Each call
3385
+ * states the WHOLE badge: no tone is the default look, not the
3386
+ * tone it had. The twin of yui_shell_set_nav_item_badge().
3262
3387
  *
3263
3388
  * Silent no-op on an unknown item id: a toolbar that does not
3264
3389
  * declare the item (an app without alarms) must not be an error
3265
- * at every tick of whatever feeds the number.
3390
+ * at every tick of whatever feeds the number. An unknown TONE is
3391
+ * the caller's error: logged, and drawn with the default look.
3266
3392
  ************************************************************/
3267
- function yui_shell_set_toolbar_item_badge(shell_gobj, item_id, value) {
3393
+ function yui_shell_set_toolbar_item_badge(shell_gobj, item_id, value, tone) {
3394
+ tone = badge_tone(tone, `C_YUI_SHELL: yui_shell_set_toolbar_item_badge('${item_id}')`);
3268
3395
  if (!shell_gobj || !is_gobj(shell_gobj) || empty_string(item_id)) return;
3269
3396
  let $container = gobj_read_attr(shell_gobj, "$container");
3270
3397
  if (!$container) return;
3271
3398
  let $badge = $container.querySelector(`[data-toolbar-item-id="${item_id}"] .yui-toolbar-badge`);
3272
3399
  if (!$badge) return;
3273
- let text = badge_text(value);
3274
- if (!text) {
3275
- $badge.textContent = "";
3276
- $badge.setAttribute("hidden", "hidden");
3277
- return;
3278
- }
3279
- if ($badge.textContent === text && !$badge.hasAttribute("hidden")) return;
3280
- $badge.textContent = text;
3281
- $badge.removeAttribute("hidden");
3400
+ paint_badge($badge, value, tone);
3401
+ }
3402
+ /************************************************************
3403
+ * Set (or clear) the badge of a MENU item — the twin of
3404
+ * yui_shell_set_toolbar_item_badge(), for an entry of a menu of
3405
+ * app_config.json, of one of their submenus, or one given to
3406
+ * yui_shell_set_submenu().
3407
+ *
3408
+ * yui_shell_set_nav_item_badge(shell, "alarms", 3);
3409
+ * yui_shell_set_nav_item_badge(shell, "alarms", 3, "warning");
3410
+ * yui_shell_set_nav_item_badge(shell, "alarms", 0); // hidden
3411
+ *
3412
+ * The same value, tone and rules as the toolbar's (yui_badge.js),
3413
+ * each call stating the whole badge.
3414
+ *
3415
+ * EVERY instance of the item gets it: the same entry is drawn by
3416
+ * as many navs as there are zones hosting its menu (a rail on
3417
+ * desktop, an icon bar on a phone), and by a drawer that repeats
3418
+ * it. Each nav repaints it in place (EV_SET_ITEM_BADGE), and
3419
+ * KEEPS it: a rebuild of the nav (a yui_shell_set_submenu()) draws
3420
+ * it again. The shell keeps it too, for the navs it creates
3421
+ * afterwards (a section-index landing, mounted when the user walks
3422
+ * into it). From then on it is the badge of that id: the `badge`
3423
+ * field of an item only seeds the paint until this is called.
3424
+ *
3425
+ * Silent no-op on an id no menu declares, as the toolbar's: a menu
3426
+ * without the entry (an app without alarms) must not be an error
3427
+ * at every tick of whatever feeds the number. An unknown TONE is
3428
+ * the caller's error: logged, and drawn with the default look.
3429
+ ************************************************************/
3430
+ function yui_shell_set_nav_item_badge(shell_gobj, item_id, value, tone) {
3431
+ tone = badge_tone(tone, `C_YUI_SHELL: yui_shell_set_nav_item_badge('${item_id}')`);
3432
+ if (!shell_gobj || !is_gobj(shell_gobj) || gobj_is_destroying(shell_gobj) || empty_string(item_id)) return;
3433
+ let priv = shell_gobj.priv;
3434
+ if (!priv || !nav_item_declared(shell_gobj, item_id)) return;
3435
+ let badge = value === void 0 ? null : value;
3436
+ priv.nav_badges[item_id] = {
3437
+ badge,
3438
+ badge_tone: tone
3439
+ };
3440
+ for (let nav of shell_navs(shell_gobj)) gobj_send_event(nav, "EV_SET_ITEM_BADGE", {
3441
+ item_id,
3442
+ badge,
3443
+ badge_tone: tone
3444
+ }, shell_gobj);
3282
3445
  }
3283
3446
  /************************************************************
3284
3447
  * Programmatic close of any open toolbar dropdown. Useful for
@@ -23360,18 +23523,34 @@ function register_c_yui_service_view() {
23360
23523
  * contract as every other nav item (data-route/data-item-id/
23361
23524
  * data-disabled), so C_YUI_NAV's delegated click handler and
23362
23525
  * active-route highlight work unchanged.
23526
+ *
23527
+ * `badge_of(it, pinned)`, optional, answers the description of
23528
+ * the card's badge (C_YUI_NAV's, which knows what was set at
23529
+ * runtime). A card is an icon over its label, so the badge goes
23530
+ * on the corner of the icon (`pinned`), or after the label of a
23531
+ * card that has none. Without it a card has no badge.
23363
23532
  ************************************************************/
23364
- function card_descriptor(it, show_label) {
23533
+ function card_descriptor(it, show_label, badge_of) {
23365
23534
  let children = [];
23366
23535
  let label = it.name || "";
23367
- if (it.icon) children.push([
23368
- "span",
23369
- { class: "icon is-medium" },
23370
- ["i", {
23536
+ let pinned = !!it.icon;
23537
+ let badge = badge_of ? badge_of(it, pinned) : null;
23538
+ if (it.icon) {
23539
+ let glyph = ["i", {
23371
23540
  class: it.icon,
23372
23541
  "aria-hidden": "true"
23373
- }]
23374
- ]);
23542
+ }];
23543
+ if (badge) children.push([
23544
+ "span",
23545
+ { class: "icon is-medium yui-nav-icon" },
23546
+ [glyph, badge]
23547
+ ]);
23548
+ else children.push([
23549
+ "span",
23550
+ { class: "icon is-medium" },
23551
+ glyph
23552
+ ]);
23553
+ }
23375
23554
  if (show_label && label) children.push([
23376
23555
  "span",
23377
23556
  {
@@ -23380,6 +23559,7 @@ function card_descriptor(it, show_label) {
23380
23559
  },
23381
23560
  text_node(label)
23382
23561
  ]);
23562
+ if (badge && !pinned) children.push(badge);
23383
23563
  let a_attrs = {
23384
23564
  class: "yui-nav-item yui-nav-card",
23385
23565
  href: it.route ? "#" + it.route : "#",
@@ -23407,12 +23587,13 @@ function card_descriptor(it, show_label) {
23407
23587
  /************************************************************
23408
23588
  * Descriptor of the whole grid. Decorative items (`header`/
23409
23589
  * `divider`) are dropped, same policy as the tabs layout.
23590
+ * `badge_of`: as card_descriptor().
23410
23591
  ************************************************************/
23411
- function cards_grid_descriptor(items, show_label) {
23592
+ function cards_grid_descriptor(items, show_label, badge_of) {
23412
23593
  let cards = [];
23413
23594
  for (let it of items || []) {
23414
23595
  if (!it || it.type === "header" || it.type === "divider") continue;
23415
- cards.push(card_descriptor(it, show_label));
23596
+ cards.push(card_descriptor(it, show_label, badge_of));
23416
23597
  }
23417
23598
  return [
23418
23599
  "div",
@@ -23457,11 +23638,29 @@ function cards_grid_descriptor(items, show_label) {
23457
23638
  *
23458
23639
  * Each item supports:
23459
23640
  * id, name, icon (CSS class or svg id), route, disabled
23460
- * (`badge` was listed here for a long time and NOTHING ever
23461
- * rendered it — a menu item cannot carry a count today. The
23462
- * toolbar can: `badge` on a toolbar item plus
23463
- * yui_shell_set_toolbar_item_badge(). Implement it here the
23464
- * day a menu entry needs one, rather than re-listing it.)
23641
+ * badge — a count, or a short text, carried by the item:
23642
+ * the toolbar badge's rules, helper and live
23643
+ * region (yui_badge.js) -- 0 / "" / null show
23644
+ * none, over 99 reads "99+". It sits on the
23645
+ * corner of the icon where the item is an icon
23646
+ * over its label (icon-bar, cards, a stacked
23647
+ * item) or an icon alone, and after the label
23648
+ * where the item reads as a line (vertical,
23649
+ * drawer, submenu, tabs, an accordion head).
23650
+ * The field only seeds the paint: a count is a
23651
+ * RUNTIME fact, set by
23652
+ * yui_shell_set_nav_item_badge(shell, id, value,
23653
+ * tone), which reaches every nav drawing the id.
23654
+ * Not drawn by "breadcrumb", deliberately: a
23655
+ * crumb names a step of the way in, and the
23656
+ * count belongs to the menu entry that leads
23657
+ * there -- a trail with counts on it reads as a
23658
+ * list of destinations, which is what a
23659
+ * breadcrumb is not. ("backbar" has no items.)
23660
+ * badge_tone — "danger" (the default look), "warning",
23661
+ * "info", "success", "primary" or "link":
23662
+ * Bulma's is-<tone> on the badge. An unknown
23663
+ * one is logged and drawn with the default look.
23465
23664
  * class — extra CSS class(es) on the item (tabs layout), e.g.
23466
23665
  * a per-item state colour like "yui-nav-disconnected"
23467
23666
  * closable — render a trailing ✕ that emits EV_NAV_ITEM_CLOSE
@@ -23472,6 +23671,13 @@ function cards_grid_descriptor(items, show_label) {
23472
23671
  * active highlight restored. The app_config path is simply the
23473
23672
  * first, startup caller of the same mechanism.
23474
23673
  *
23674
+ * One item's badge is set at runtime by sending EV_SET_ITEM_BADGE
23675
+ * {item_id, badge, badge_tone}: painted in place, no rebuild, and
23676
+ * KEPT in the `badges` attr, which wins over the item's own fields
23677
+ * in every later rebuild. The shell sends it to all its navs
23678
+ * (yui_shell_set_nav_item_badge) and hands `badges` to every nav
23679
+ * it creates.
23680
+ *
23475
23681
  * In addition to navigable items, secondary navs accept two
23476
23682
  * decorative item kinds for in-place visual grouping:
23477
23683
  *
@@ -23519,6 +23725,7 @@ var attrs_table$7 = [
23519
23725
  SDATA(data_type_t.DTP_STRING, "level", 0, "primary", "primary|secondary"),
23520
23726
  SDATA(data_type_t.DTP_STRING, "show_on", 0, "", "Breakpoint visibility expr (shell show_on syntax); empty = always"),
23521
23727
  SDATA(data_type_t.DTP_STRING, "back_route", 0, "", "backbar layout: route of the section index the ← returns to"),
23728
+ SDATA(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"),
23522
23729
  SDATA(data_type_t.DTP_POINTER, "shell", 0, null, "C_YUI_SHELL gobj (for navigation calls)"),
23523
23730
  SDATA(data_type_t.DTP_POINTER, "$container", 0, null, "Root HTMLElement of this nav"),
23524
23731
  SDATA(data_type_t.DTP_STRING, "active_route", 0, "", "Currently active route"),
@@ -23733,19 +23940,16 @@ function render_tabs(gobj, items) {
23733
23940
  for (let it of items) {
23734
23941
  if (is_decorative(it)) continue;
23735
23942
  let children = [];
23736
- if (!empty_string(it.icon)) children.push([
23737
- "span",
23738
- { class: "icon is-small" },
23739
- ["i", {
23740
- class: it.icon,
23741
- "aria-hidden": "true"
23742
- }]
23743
- ]);
23744
- if (show_label && !empty_string(it.name)) children.push([
23943
+ let has_label = show_label && !empty_string(it.name);
23944
+ let pinned = !empty_string(it.icon) && !has_label;
23945
+ let badge_el = item_badge_node(gobj, it, pinned);
23946
+ if (!empty_string(it.icon)) children.push(icon_span(it.icon, "icon is-small", pinned ? badge_el : null));
23947
+ if (has_label) children.push([
23745
23948
  "span",
23746
23949
  { i18n: it.name },
23747
23950
  text_node(it.name)
23748
23951
  ]);
23952
+ if (!pinned) children.push(badge_el);
23749
23953
  if (it.closable) children.push([
23750
23954
  "span",
23751
23955
  {
@@ -23803,7 +24007,7 @@ function render_tabs(gobj, items) {
23803
24007
  }
23804
24008
  function render_cards(gobj, items) {
23805
24009
  let show_label = gobj_read_attr(gobj, "show_label");
23806
- return createElement2(cards_grid_descriptor(items, show_label));
24010
+ return createElement2(cards_grid_descriptor(items, show_label, (it, pinned) => item_badge_node(gobj, it, pinned)));
23807
24011
  }
23808
24012
  /************************************************************
23809
24013
  * "backbar": a single "← <section>" link back to the section
@@ -23821,6 +24025,9 @@ function render_cards(gobj, items) {
23821
24025
  * other layout — so the delegated click handler and the active
23822
24026
  * highlight work unchanged, and the last crumb marks itself through
23823
24027
  * `active_route` like a tab does.
24028
+ *
24029
+ * No badge, deliberately: a crumb names a step of the way in, and a
24030
+ * count belongs to the menu entry that leads there.
23824
24031
  ************************************************************/
23825
24032
  function render_breadcrumb(gobj, items) {
23826
24033
  let show_label = gobj_read_attr(gobj, "show_label");
@@ -23999,7 +24206,7 @@ function render_accordion(gobj, items) {
23999
24206
  let $hdr = createElement2([
24000
24207
  "button",
24001
24208
  head_attrs,
24002
- text_node(head_text)
24209
+ [text_node(head_text), item_badge_node(gobj, it, false)]
24003
24210
  ]);
24004
24211
  $container.appendChild($hdr);
24005
24212
  let $ul = createElement2(["ul", {
@@ -24046,14 +24253,7 @@ function item_li(gobj, it, opts) {
24046
24253
  let { icon_pos, show_label, stacked } = opts;
24047
24254
  let children = [];
24048
24255
  let label = it.name || "";
24049
- let icon_el = !empty_string(it.icon) ? [
24050
- "span",
24051
- { class: "icon" },
24052
- ["i", {
24053
- class: it.icon,
24054
- "aria-hidden": "true"
24055
- }]
24056
- ] : null;
24256
+ let is_stacked = !!(stacked || icon_pos === "top" || icon_pos === "bottom");
24057
24257
  let label_el = show_label && !empty_string(label) ? [
24058
24258
  "span",
24059
24259
  {
@@ -24062,14 +24262,19 @@ function item_li(gobj, it, opts) {
24062
24262
  },
24063
24263
  text_node(label)
24064
24264
  ] : null;
24265
+ let pinned = !empty_string(it.icon) && (is_stacked || !label_el);
24266
+ let badge_el = item_badge_node(gobj, it, pinned);
24267
+ let icon_el = !empty_string(it.icon) ? icon_span(it.icon, "icon", pinned ? badge_el : null) : null;
24065
24268
  let a_class = "yui-nav-item";
24066
- if (stacked || icon_pos === "top" || icon_pos === "bottom") a_class += " yui-nav-stacked";
24269
+ if (is_stacked) a_class += " yui-nav-stacked";
24067
24270
  if (icon_pos === "right" || icon_pos === "bottom") {
24068
24271
  if (label_el) children.push(label_el);
24272
+ if (!pinned) children.push(badge_el);
24069
24273
  if (icon_el) children.push(icon_el);
24070
24274
  } else {
24071
24275
  if (icon_el) children.push(icon_el);
24072
24276
  if (label_el) children.push(label_el);
24277
+ if (!pinned) children.push(badge_el);
24073
24278
  }
24074
24279
  let aria_key = label || it.id;
24075
24280
  let a_attrs = {
@@ -24103,14 +24308,9 @@ function item_li(gobj, it, opts) {
24103
24308
  function item_iconbar(gobj, it, opts) {
24104
24309
  let { icon_pos, show_label } = opts;
24105
24310
  let label = it.name || "";
24106
- let icon_el = !empty_string(it.icon) ? [
24107
- "span",
24108
- { class: "icon is-medium" },
24109
- ["i", {
24110
- class: it.icon,
24111
- "aria-hidden": "true"
24112
- }]
24113
- ] : null;
24311
+ let pinned = !empty_string(it.icon);
24312
+ let badge_el = item_badge_node(gobj, it, pinned);
24313
+ let icon_el = pinned ? icon_span(it.icon, "icon is-medium", badge_el) : null;
24114
24314
  let label_el = show_label && !empty_string(label) ? [
24115
24315
  "span",
24116
24316
  {
@@ -24127,6 +24327,7 @@ function item_iconbar(gobj, it, opts) {
24127
24327
  if (icon_el) children.push(icon_el);
24128
24328
  if (label_el) children.push(label_el);
24129
24329
  }
24330
+ if (!pinned) children.push(badge_el);
24130
24331
  let a_attrs = {
24131
24332
  class: "yui-nav-item yui-nav-stacked",
24132
24333
  href: it.route ? "#" + it.route : "#",
@@ -24150,6 +24351,40 @@ function item_iconbar(gobj, it, opts) {
24150
24351
  ]
24151
24352
  ];
24152
24353
  }
24354
+ /************************************************************
24355
+ * The badge of an item (yui_badge.js), always drawn -- hidden
24356
+ * when empty -- so EV_SET_ITEM_BADGE has a node to write into.
24357
+ * What was set at runtime for its id (`badges`) wins over the
24358
+ * item's own `badge` / `badge_tone`. `pinned`: on the corner of
24359
+ * the icon instead of after the label.
24360
+ ************************************************************/
24361
+ function item_badge_node(gobj, it, pinned) {
24362
+ let badges = gobj_read_attr(gobj, "badges");
24363
+ let state = it;
24364
+ if (is_object(badges) && is_string(it.id) && Object.prototype.hasOwnProperty.call(badges, it.id)) state = badges[it.id] || {};
24365
+ let classes = "NAV_ITEM_BADGE yui-badge yui-nav-badge";
24366
+ if (pinned) classes += " yui-nav-badge-pinned";
24367
+ return badge_node(state.badge, state.badge_tone, classes, `C_YUI_NAV '${gobj_read_attr(gobj, "menu_id") || ""}': item '${it.id}'`);
24368
+ }
24369
+ /************************************************************
24370
+ * An item's icon, with its badge on the corner when given one.
24371
+ ************************************************************/
24372
+ function icon_span(icon, classes, badge_el) {
24373
+ let glyph = ["i", {
24374
+ class: icon,
24375
+ "aria-hidden": "true"
24376
+ }];
24377
+ if (!badge_el) return [
24378
+ "span",
24379
+ { class: classes },
24380
+ glyph
24381
+ ];
24382
+ return [
24383
+ "span",
24384
+ { class: `${classes} yui-nav-icon` },
24385
+ [glyph, badge_el]
24386
+ ];
24387
+ }
24153
24388
  function wire_clicks(gobj, $container) {
24154
24389
  let priv = gobj.priv;
24155
24390
  let handler = (ev) => {
@@ -24226,6 +24461,36 @@ function ac_set_items(gobj, event, kw, src) {
24226
24461
  return 0;
24227
24462
  }
24228
24463
  /************************************************************
24464
+ * EV_SET_ITEM_BADGE {item_id, badge, badge_tone} — the badge of
24465
+ * one item, set at runtime (yui_shell_set_nav_item_badge).
24466
+ *
24467
+ * Kept in `badges`, so every later rebuild draws it, and painted
24468
+ * in place: no rebuild for a count that moves on every tick. An
24469
+ * id this nav does not draw is kept all the same — a later
24470
+ * EV_SET_ITEMS may bring the item.
24471
+ ************************************************************/
24472
+ function ac_set_item_badge(gobj, event, kw, src) {
24473
+ let item_id = kw.item_id;
24474
+ if (empty_string(item_id)) {
24475
+ log_error(`C_YUI_NAV: ${event} without item_id, from ${gobj_short_name(src)}`);
24476
+ return -1;
24477
+ }
24478
+ let menu_id = gobj_read_attr(gobj, "menu_id") || "";
24479
+ let tone = badge_tone(kw.badge_tone, `C_YUI_NAV '${menu_id}': item '${item_id}'`);
24480
+ let badge = kw.badge === void 0 ? null : kw.badge;
24481
+ let badges = Object.assign({}, gobj_read_attr(gobj, "badges"));
24482
+ badges[item_id] = {
24483
+ badge,
24484
+ badge_tone: tone
24485
+ };
24486
+ gobj_write_attr(gobj, "badges", badges);
24487
+ let $c = gobj_read_attr(gobj, "$container");
24488
+ if (!$c) return 0;
24489
+ let $badges = $c.querySelectorAll(`[data-item-id="${css_escape(item_id)}"] .NAV_ITEM_BADGE`);
24490
+ for (let $badge of $badges) paint_badge($badge, badge, tone);
24491
+ return 0;
24492
+ }
24493
+ /************************************************************
24229
24494
  * EV_ROUTE_CHANGED from the shell — refresh active highlight
24230
24495
  ************************************************************/
24231
24496
  function ac_route_changed(gobj, event, kw, src) {
@@ -24281,18 +24546,27 @@ function create_gclass$7(gclass_name) {
24281
24546
  log_error(`GClass ALREADY created: ${gclass_name}`);
24282
24547
  return -1;
24283
24548
  }
24284
- const states = [["ST_IDLE", [[
24285
- "EV_ROUTE_CHANGED",
24286
- ac_route_changed,
24287
- null
24288
- ], [
24289
- "EV_SET_ITEMS",
24290
- ac_set_items,
24291
- null
24292
- ]]]];
24549
+ const states = [["ST_IDLE", [
24550
+ [
24551
+ "EV_ROUTE_CHANGED",
24552
+ ac_route_changed,
24553
+ null
24554
+ ],
24555
+ [
24556
+ "EV_SET_ITEMS",
24557
+ ac_set_items,
24558
+ null
24559
+ ],
24560
+ [
24561
+ "EV_SET_ITEM_BADGE",
24562
+ ac_set_item_badge,
24563
+ null
24564
+ ]
24565
+ ]]];
24293
24566
  const event_types = [
24294
24567
  ["EV_ROUTE_CHANGED", 0],
24295
24568
  ["EV_SET_ITEMS", 0],
24569
+ ["EV_SET_ITEM_BADGE", 0],
24296
24570
  ["EV_NAV_CLICKED", event_flag_t.EVF_OUTPUT_EVENT | event_flag_t.EVF_PUBLIC_EVENT],
24297
24571
  ["EV_NAV_ITEM_CLOSE", event_flag_t.EVF_OUTPUT_EVENT | event_flag_t.EVF_PUBLIC_EVENT],
24298
24572
  ["EV_DRAWER_CLOSE_REQUESTED", event_flag_t.EVF_OUTPUT_EVENT | event_flag_t.EVF_PUBLIC_EVENT]
@@ -83199,6 +83473,6 @@ function setup_json_pad(self) {
83199
83473
  return win;
83200
83474
  }
83201
83475
  //#endregion
83202
- export { EditControl, ICONS_TOPIC, MarkerControl, USER_ICON_PREFIX, YUI_FILE_ACCEPT, YUI_PERIODS, YUI_PERIODS_DEFAULT, YUI_ROLLING, addClasses, apply_dev_traces, attach_clear, build_dev_panel, dev_window_was_open, disableElements, enableElements, epoch_to_local_input, epoch_to_ms, expose_view_container, fmt_epoch, getStrokeColor, infer_period, info_traffic, inject_svg_icons, is_current_period, iso_week, local_input_to_epoch, ms_to_epoch, period_bounds, period_bounds_epoch, period_label, period_name, period_shift, period_spec, period_start, refresh_clear, register_c_g6_nodes_tree, register_c_yui_form, register_c_yui_fsm_graph, register_c_yui_gclass, register_c_yui_gobj_tree_js, register_c_yui_json, register_c_yui_json_graph, register_c_yui_json_pad, register_c_yui_map, register_c_yui_nav, register_c_yui_pager, register_c_yui_period, register_c_yui_schema_editor, register_c_yui_service_view, register_c_yui_shell, register_c_yui_treedb_graph, register_c_yui_treedb_schema, register_c_yui_treedb_topic_with_form, register_c_yui_treedb_topics, register_c_yui_uplot, register_c_yui_window, register_c_yui_window_manager, register_c_yui_wizard, removeChildElements, removeClasses, rolling_bounds, safe_locale, set_active_state, set_cancel_state, set_pressed_state, set_submit_state, setup_dev, setup_frontend_view, setup_json_pad, toggleClasses, yui_array_buffer_to_base64, yui_asset_element, yui_asset_id, yui_asset_ids, yui_asset_missing, yui_asset_src, yui_file_control, yui_file_id_label, yui_file_read, yui_file_sha256, yui_file_size_label, yui_files_manifest, yui_icon_is_defined, yui_icons_list, yui_icons_put_user, yui_icons_remove_user, yui_icons_set_user, yui_is_dark, yui_mount_service_view, yui_shell_close_drawer, yui_shell_close_dropdown, yui_shell_confirm_danger, yui_shell_confirm_ok, yui_shell_confirm_yesno, yui_shell_confirm_yesnocancel, yui_shell_navigate, yui_shell_open_drawer, yui_shell_overlay_dismissed, yui_shell_pop_escape, yui_shell_push_escape, yui_shell_refresh_avatars, yui_shell_register_event_handler, yui_shell_register_overlay, yui_shell_set_avatar_provider, yui_shell_set_connection_state, yui_shell_set_sub_routes, yui_shell_set_toolbar_item_badge, yui_shell_set_toolbar_item_icon, yui_shell_set_translator, yui_shell_show_error, yui_shell_show_info, yui_shell_show_modal, yui_shell_show_route_map, yui_shell_show_warning, yui_shell_toggle_drawer, yui_shell_unpark_route, yui_svg_icon_element, yui_svg_sanitize, yui_theme_now, yui_toolbar, yui_user_icon_class, yui_watch_theme };
83476
+ export { EditControl, ICONS_TOPIC, MarkerControl, USER_ICON_PREFIX, YUI_FILE_ACCEPT, YUI_PERIODS, YUI_PERIODS_DEFAULT, YUI_ROLLING, addClasses, apply_dev_traces, attach_clear, build_dev_panel, dev_window_was_open, disableElements, enableElements, epoch_to_local_input, epoch_to_ms, expose_view_container, fmt_epoch, getStrokeColor, infer_period, info_traffic, inject_svg_icons, is_current_period, iso_week, local_input_to_epoch, ms_to_epoch, period_bounds, period_bounds_epoch, period_label, period_name, period_shift, period_spec, period_start, refresh_clear, register_c_g6_nodes_tree, register_c_yui_form, register_c_yui_fsm_graph, register_c_yui_gclass, register_c_yui_gobj_tree_js, register_c_yui_json, register_c_yui_json_graph, register_c_yui_json_pad, register_c_yui_map, register_c_yui_nav, register_c_yui_pager, register_c_yui_period, register_c_yui_schema_editor, register_c_yui_service_view, register_c_yui_shell, register_c_yui_treedb_graph, register_c_yui_treedb_schema, register_c_yui_treedb_topic_with_form, register_c_yui_treedb_topics, register_c_yui_uplot, register_c_yui_window, register_c_yui_window_manager, register_c_yui_wizard, removeChildElements, removeClasses, rolling_bounds, safe_locale, set_active_state, set_cancel_state, set_pressed_state, set_submit_state, setup_dev, setup_frontend_view, setup_json_pad, toggleClasses, yui_array_buffer_to_base64, yui_asset_element, yui_asset_id, yui_asset_ids, yui_asset_missing, yui_asset_src, yui_file_control, yui_file_id_label, yui_file_read, yui_file_sha256, yui_file_size_label, yui_files_manifest, yui_icon_is_defined, yui_icons_list, yui_icons_put_user, yui_icons_remove_user, yui_icons_set_user, yui_is_dark, yui_mount_service_view, yui_shell_close_drawer, yui_shell_close_dropdown, yui_shell_confirm_danger, yui_shell_confirm_ok, yui_shell_confirm_yesno, yui_shell_confirm_yesnocancel, yui_shell_navigate, yui_shell_open_drawer, yui_shell_overlay_dismissed, yui_shell_pop_escape, yui_shell_push_escape, yui_shell_refresh_avatars, yui_shell_register_event_handler, yui_shell_register_overlay, yui_shell_set_avatar_provider, yui_shell_set_connection_state, yui_shell_set_nav_item_badge, yui_shell_set_sub_routes, yui_shell_set_toolbar_item_badge, yui_shell_set_toolbar_item_icon, yui_shell_set_translator, yui_shell_show_error, yui_shell_show_info, yui_shell_show_modal, yui_shell_show_route_map, yui_shell_show_warning, yui_shell_toggle_drawer, yui_shell_unpark_route, yui_svg_icon_element, yui_svg_sanitize, yui_theme_now, yui_toolbar, yui_user_icon_class, yui_watch_theme };
83203
83477
 
83204
83478
  //# sourceMappingURL=gobj-ui.es.js.map