@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.
@@ -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
@@ -3882,7 +4045,7 @@ var MOBILE_MAX = 768;
3882
4045
  ***************************************************************/
3883
4046
  var attrs_table$21 = [
3884
4047
  SDATA(data_type_t.DTP_POINTER, "subscriber", 0, null, "Subscriber of output events"),
3885
- SDATA(data_type_t.DTP_POINTER, "$parent", 0, null, "$container will be appended to $parent if not null, else to document.body"),
4048
+ SDATA(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)"),
3886
4049
  SDATA(data_type_t.DTP_INTEGER, "x", 0, "300", "X position"),
3887
4050
  SDATA(data_type_t.DTP_INTEGER, "y", 0, "100", "Y position"),
3888
4051
  SDATA(data_type_t.DTP_INTEGER, "width", 0, "700", "Width of the window"),
@@ -4377,8 +4540,8 @@ function build_ui$18(gobj) {
4377
4540
  }
4378
4541
  refresh_language($container, t);
4379
4542
  let $parent = gobj_read_attr(gobj, "$parent");
4380
- if ($parent) $parent.appendChild($container);
4381
- else document.body.appendChild($container);
4543
+ if (!$parent) $parent = default_parent(gobj);
4544
+ $parent.appendChild($container);
4382
4545
  if (gobj_read_bool_attr(gobj, "openMaximized")) max$2(gobj);
4383
4546
  else if (!mobile) {
4384
4547
  if (gobj_read_bool_attr(gobj, "content_size")) $container.style.height = "auto";
@@ -4464,6 +4627,30 @@ function close_window(gobj) {
4464
4627
  };
4465
4628
  }
4466
4629
  /************************************************************
4630
+ * Where a window lives when its host names no `$parent`: the
4631
+ * OVERLAY layer of the shell -- above the content, below the
4632
+ * toolbar's dropdowns, the dialogs and the toasts.
4633
+ *
4634
+ * It used to be document.body, and that put every window above
4635
+ * the WHOLE shell: `.yui-shell` is position:fixed, a stacking
4636
+ * context of its own, so the z-index of its modal layer (99)
4637
+ * counted only inside it, and a window at z-index 11 on the body
4638
+ * covered it. The confirmation a window's own close asks ("all
4639
+ * changes will be lost") opened UNDER the window that asked it,
4640
+ * where no click could reach it; a toast, a dropdown of the
4641
+ * toolbar, any dialog opened with a window on screen, the same.
4642
+ *
4643
+ * Inside the layer the dock's z-index still orders the windows
4644
+ * among themselves. No shell (a page with windows and no shell):
4645
+ * the body, as before.
4646
+ ************************************************************/
4647
+ function default_parent(gobj) {
4648
+ let shell = yui_shell_of(gobj);
4649
+ let layers = shell && shell.priv && shell.priv.layers;
4650
+ if (layers && layers.overlay) return layers.overlay;
4651
+ return document.body;
4652
+ }
4653
+ /************************************************************
4467
4654
  *
4468
4655
  ************************************************************/
4469
4656
  function toggle(gobj) {
@@ -6566,7 +6753,9 @@ function yui_tabulator_name_filters(table, t) {
6566
6753
  let $title = $el.querySelector(".tabulator-col-title");
6567
6754
  if (!title && $title) title = $title.textContent.trim();
6568
6755
  if (!title) continue;
6569
- $input.setAttribute("aria-label", t("filter by column", { column: title }));
6756
+ let name = t("filter by column", { column: title });
6757
+ $input.setAttribute("aria-label", name);
6758
+ $input.setAttribute("title", name);
6570
6759
  }
6571
6760
  } catch (e) {}
6572
6761
  }
@@ -6597,6 +6786,8 @@ function yui_tabulator_name_row_selects(table, t) {
6597
6786
  for (let $box of boxes) {
6598
6787
  $box.setAttribute("aria-label", t("select row"));
6599
6788
  $box.setAttribute("data-i18n-aria-label", "select row");
6789
+ $box.setAttribute("title", t("select row"));
6790
+ $box.setAttribute("data-i18n-title", "select row");
6600
6791
  }
6601
6792
  } catch (e) {}
6602
6793
  }
@@ -8995,11 +9186,19 @@ function name_form_control$1($field, conf) {
8995
9186
  $control.setAttribute("aria-label", t(key));
8996
9187
  $control.setAttribute("data-i18n-aria-label", key);
8997
9188
  }
9189
+ if ($control && !$control.getAttribute("title")) {
9190
+ $control.setAttribute("title", t(key));
9191
+ $control.setAttribute("data-i18n-title", key);
9192
+ }
8998
9193
  let $ts = $field.querySelector(".ts-control input");
8999
9194
  if ($ts && !$ts.getAttribute("aria-label")) {
9000
9195
  $ts.setAttribute("aria-label", t(key));
9001
9196
  $ts.setAttribute("data-i18n-aria-label", key);
9002
9197
  }
9198
+ if ($ts && !$ts.getAttribute("title")) {
9199
+ $ts.setAttribute("title", t(key));
9200
+ $ts.setAttribute("data-i18n-title", key);
9201
+ }
9003
9202
  let $clear = $field.querySelector(".ts-wrapper .clear-button");
9004
9203
  if ($clear) {
9005
9204
  const CLEAR_KEY = "remove all selected options";
@@ -10089,6 +10288,8 @@ function create_coordinates(gobj, attrs) {
10089
10288
  type: "text",
10090
10289
  placeholder: t("coordinates..."),
10091
10290
  "data-i18n-placeholder": "coordinates...",
10291
+ title: t("coordinates"),
10292
+ "data-i18n-title": "coordinates",
10092
10293
  "aria-label": t("coordinates"),
10093
10294
  "data-i18n-aria-label": "coordinates"
10094
10295
  },
@@ -23360,18 +23561,34 @@ function register_c_yui_service_view() {
23360
23561
  * contract as every other nav item (data-route/data-item-id/
23361
23562
  * data-disabled), so C_YUI_NAV's delegated click handler and
23362
23563
  * active-route highlight work unchanged.
23564
+ *
23565
+ * `badge_of(it, pinned)`, optional, answers the description of
23566
+ * the card's badge (C_YUI_NAV's, which knows what was set at
23567
+ * runtime). A card is an icon over its label, so the badge goes
23568
+ * on the corner of the icon (`pinned`), or after the label of a
23569
+ * card that has none. Without it a card has no badge.
23363
23570
  ************************************************************/
23364
- function card_descriptor(it, show_label) {
23571
+ function card_descriptor(it, show_label, badge_of) {
23365
23572
  let children = [];
23366
23573
  let label = it.name || "";
23367
- if (it.icon) children.push([
23368
- "span",
23369
- { class: "icon is-medium" },
23370
- ["i", {
23574
+ let pinned = !!it.icon;
23575
+ let badge = badge_of ? badge_of(it, pinned) : null;
23576
+ if (it.icon) {
23577
+ let glyph = ["i", {
23371
23578
  class: it.icon,
23372
23579
  "aria-hidden": "true"
23373
- }]
23374
- ]);
23580
+ }];
23581
+ if (badge) children.push([
23582
+ "span",
23583
+ { class: "icon is-medium yui-nav-icon" },
23584
+ [glyph, badge]
23585
+ ]);
23586
+ else children.push([
23587
+ "span",
23588
+ { class: "icon is-medium" },
23589
+ glyph
23590
+ ]);
23591
+ }
23375
23592
  if (show_label && label) children.push([
23376
23593
  "span",
23377
23594
  {
@@ -23380,6 +23597,7 @@ function card_descriptor(it, show_label) {
23380
23597
  },
23381
23598
  text_node(label)
23382
23599
  ]);
23600
+ if (badge && !pinned) children.push(badge);
23383
23601
  let a_attrs = {
23384
23602
  class: "yui-nav-item yui-nav-card",
23385
23603
  href: it.route ? "#" + it.route : "#",
@@ -23407,12 +23625,13 @@ function card_descriptor(it, show_label) {
23407
23625
  /************************************************************
23408
23626
  * Descriptor of the whole grid. Decorative items (`header`/
23409
23627
  * `divider`) are dropped, same policy as the tabs layout.
23628
+ * `badge_of`: as card_descriptor().
23410
23629
  ************************************************************/
23411
- function cards_grid_descriptor(items, show_label) {
23630
+ function cards_grid_descriptor(items, show_label, badge_of) {
23412
23631
  let cards = [];
23413
23632
  for (let it of items || []) {
23414
23633
  if (!it || it.type === "header" || it.type === "divider") continue;
23415
- cards.push(card_descriptor(it, show_label));
23634
+ cards.push(card_descriptor(it, show_label, badge_of));
23416
23635
  }
23417
23636
  return [
23418
23637
  "div",
@@ -23457,11 +23676,29 @@ function cards_grid_descriptor(items, show_label) {
23457
23676
  *
23458
23677
  * Each item supports:
23459
23678
  * 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.)
23679
+ * badge — a count, or a short text, carried by the item:
23680
+ * the toolbar badge's rules, helper and live
23681
+ * region (yui_badge.js) -- 0 / "" / null show
23682
+ * none, over 99 reads "99+". It sits on the
23683
+ * corner of the icon where the item is an icon
23684
+ * over its label (icon-bar, cards, a stacked
23685
+ * item) or an icon alone, and after the label
23686
+ * where the item reads as a line (vertical,
23687
+ * drawer, submenu, tabs, an accordion head).
23688
+ * The field only seeds the paint: a count is a
23689
+ * RUNTIME fact, set by
23690
+ * yui_shell_set_nav_item_badge(shell, id, value,
23691
+ * tone), which reaches every nav drawing the id.
23692
+ * Not drawn by "breadcrumb", deliberately: a
23693
+ * crumb names a step of the way in, and the
23694
+ * count belongs to the menu entry that leads
23695
+ * there -- a trail with counts on it reads as a
23696
+ * list of destinations, which is what a
23697
+ * breadcrumb is not. ("backbar" has no items.)
23698
+ * badge_tone — "danger" (the default look), "warning",
23699
+ * "info", "success", "primary" or "link":
23700
+ * Bulma's is-<tone> on the badge. An unknown
23701
+ * one is logged and drawn with the default look.
23465
23702
  * class — extra CSS class(es) on the item (tabs layout), e.g.
23466
23703
  * a per-item state colour like "yui-nav-disconnected"
23467
23704
  * closable — render a trailing ✕ that emits EV_NAV_ITEM_CLOSE
@@ -23472,6 +23709,13 @@ function cards_grid_descriptor(items, show_label) {
23472
23709
  * active highlight restored. The app_config path is simply the
23473
23710
  * first, startup caller of the same mechanism.
23474
23711
  *
23712
+ * One item's badge is set at runtime by sending EV_SET_ITEM_BADGE
23713
+ * {item_id, badge, badge_tone}: painted in place, no rebuild, and
23714
+ * KEPT in the `badges` attr, which wins over the item's own fields
23715
+ * in every later rebuild. The shell sends it to all its navs
23716
+ * (yui_shell_set_nav_item_badge) and hands `badges` to every nav
23717
+ * it creates.
23718
+ *
23475
23719
  * In addition to navigable items, secondary navs accept two
23476
23720
  * decorative item kinds for in-place visual grouping:
23477
23721
  *
@@ -23519,6 +23763,7 @@ var attrs_table$7 = [
23519
23763
  SDATA(data_type_t.DTP_STRING, "level", 0, "primary", "primary|secondary"),
23520
23764
  SDATA(data_type_t.DTP_STRING, "show_on", 0, "", "Breakpoint visibility expr (shell show_on syntax); empty = always"),
23521
23765
  SDATA(data_type_t.DTP_STRING, "back_route", 0, "", "backbar layout: route of the section index the ← returns to"),
23766
+ 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
23767
  SDATA(data_type_t.DTP_POINTER, "shell", 0, null, "C_YUI_SHELL gobj (for navigation calls)"),
23523
23768
  SDATA(data_type_t.DTP_POINTER, "$container", 0, null, "Root HTMLElement of this nav"),
23524
23769
  SDATA(data_type_t.DTP_STRING, "active_route", 0, "", "Currently active route"),
@@ -23733,19 +23978,16 @@ function render_tabs(gobj, items) {
23733
23978
  for (let it of items) {
23734
23979
  if (is_decorative(it)) continue;
23735
23980
  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([
23981
+ let has_label = show_label && !empty_string(it.name);
23982
+ let pinned = !empty_string(it.icon) && !has_label;
23983
+ let badge_el = item_badge_node(gobj, it, pinned);
23984
+ if (!empty_string(it.icon)) children.push(icon_span(it.icon, "icon is-small", pinned ? badge_el : null));
23985
+ if (has_label) children.push([
23745
23986
  "span",
23746
23987
  { i18n: it.name },
23747
23988
  text_node(it.name)
23748
23989
  ]);
23990
+ if (!pinned) children.push(badge_el);
23749
23991
  if (it.closable) children.push([
23750
23992
  "span",
23751
23993
  {
@@ -23803,7 +24045,7 @@ function render_tabs(gobj, items) {
23803
24045
  }
23804
24046
  function render_cards(gobj, items) {
23805
24047
  let show_label = gobj_read_attr(gobj, "show_label");
23806
- return createElement2(cards_grid_descriptor(items, show_label));
24048
+ return createElement2(cards_grid_descriptor(items, show_label, (it, pinned) => item_badge_node(gobj, it, pinned)));
23807
24049
  }
23808
24050
  /************************************************************
23809
24051
  * "backbar": a single "← <section>" link back to the section
@@ -23821,6 +24063,9 @@ function render_cards(gobj, items) {
23821
24063
  * other layout — so the delegated click handler and the active
23822
24064
  * highlight work unchanged, and the last crumb marks itself through
23823
24065
  * `active_route` like a tab does.
24066
+ *
24067
+ * No badge, deliberately: a crumb names a step of the way in, and a
24068
+ * count belongs to the menu entry that leads there.
23824
24069
  ************************************************************/
23825
24070
  function render_breadcrumb(gobj, items) {
23826
24071
  let show_label = gobj_read_attr(gobj, "show_label");
@@ -23999,7 +24244,7 @@ function render_accordion(gobj, items) {
23999
24244
  let $hdr = createElement2([
24000
24245
  "button",
24001
24246
  head_attrs,
24002
- text_node(head_text)
24247
+ [text_node(head_text), item_badge_node(gobj, it, false)]
24003
24248
  ]);
24004
24249
  $container.appendChild($hdr);
24005
24250
  let $ul = createElement2(["ul", {
@@ -24046,14 +24291,7 @@ function item_li(gobj, it, opts) {
24046
24291
  let { icon_pos, show_label, stacked } = opts;
24047
24292
  let children = [];
24048
24293
  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;
24294
+ let is_stacked = !!(stacked || icon_pos === "top" || icon_pos === "bottom");
24057
24295
  let label_el = show_label && !empty_string(label) ? [
24058
24296
  "span",
24059
24297
  {
@@ -24062,14 +24300,19 @@ function item_li(gobj, it, opts) {
24062
24300
  },
24063
24301
  text_node(label)
24064
24302
  ] : null;
24303
+ let pinned = !empty_string(it.icon) && (is_stacked || !label_el);
24304
+ let badge_el = item_badge_node(gobj, it, pinned);
24305
+ let icon_el = !empty_string(it.icon) ? icon_span(it.icon, "icon", pinned ? badge_el : null) : null;
24065
24306
  let a_class = "yui-nav-item";
24066
- if (stacked || icon_pos === "top" || icon_pos === "bottom") a_class += " yui-nav-stacked";
24307
+ if (is_stacked) a_class += " yui-nav-stacked";
24067
24308
  if (icon_pos === "right" || icon_pos === "bottom") {
24068
24309
  if (label_el) children.push(label_el);
24310
+ if (!pinned) children.push(badge_el);
24069
24311
  if (icon_el) children.push(icon_el);
24070
24312
  } else {
24071
24313
  if (icon_el) children.push(icon_el);
24072
24314
  if (label_el) children.push(label_el);
24315
+ if (!pinned) children.push(badge_el);
24073
24316
  }
24074
24317
  let aria_key = label || it.id;
24075
24318
  let a_attrs = {
@@ -24103,14 +24346,9 @@ function item_li(gobj, it, opts) {
24103
24346
  function item_iconbar(gobj, it, opts) {
24104
24347
  let { icon_pos, show_label } = opts;
24105
24348
  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;
24349
+ let pinned = !empty_string(it.icon);
24350
+ let badge_el = item_badge_node(gobj, it, pinned);
24351
+ let icon_el = pinned ? icon_span(it.icon, "icon is-medium", badge_el) : null;
24114
24352
  let label_el = show_label && !empty_string(label) ? [
24115
24353
  "span",
24116
24354
  {
@@ -24127,6 +24365,7 @@ function item_iconbar(gobj, it, opts) {
24127
24365
  if (icon_el) children.push(icon_el);
24128
24366
  if (label_el) children.push(label_el);
24129
24367
  }
24368
+ if (!pinned) children.push(badge_el);
24130
24369
  let a_attrs = {
24131
24370
  class: "yui-nav-item yui-nav-stacked",
24132
24371
  href: it.route ? "#" + it.route : "#",
@@ -24150,6 +24389,40 @@ function item_iconbar(gobj, it, opts) {
24150
24389
  ]
24151
24390
  ];
24152
24391
  }
24392
+ /************************************************************
24393
+ * The badge of an item (yui_badge.js), always drawn -- hidden
24394
+ * when empty -- so EV_SET_ITEM_BADGE has a node to write into.
24395
+ * What was set at runtime for its id (`badges`) wins over the
24396
+ * item's own `badge` / `badge_tone`. `pinned`: on the corner of
24397
+ * the icon instead of after the label.
24398
+ ************************************************************/
24399
+ function item_badge_node(gobj, it, pinned) {
24400
+ let badges = gobj_read_attr(gobj, "badges");
24401
+ let state = it;
24402
+ if (is_object(badges) && is_string(it.id) && Object.prototype.hasOwnProperty.call(badges, it.id)) state = badges[it.id] || {};
24403
+ let classes = "NAV_ITEM_BADGE yui-badge yui-nav-badge";
24404
+ if (pinned) classes += " yui-nav-badge-pinned";
24405
+ return badge_node(state.badge, state.badge_tone, classes, `C_YUI_NAV '${gobj_read_attr(gobj, "menu_id") || ""}': item '${it.id}'`);
24406
+ }
24407
+ /************************************************************
24408
+ * An item's icon, with its badge on the corner when given one.
24409
+ ************************************************************/
24410
+ function icon_span(icon, classes, badge_el) {
24411
+ let glyph = ["i", {
24412
+ class: icon,
24413
+ "aria-hidden": "true"
24414
+ }];
24415
+ if (!badge_el) return [
24416
+ "span",
24417
+ { class: classes },
24418
+ glyph
24419
+ ];
24420
+ return [
24421
+ "span",
24422
+ { class: `${classes} yui-nav-icon` },
24423
+ [glyph, badge_el]
24424
+ ];
24425
+ }
24153
24426
  function wire_clicks(gobj, $container) {
24154
24427
  let priv = gobj.priv;
24155
24428
  let handler = (ev) => {
@@ -24226,6 +24499,36 @@ function ac_set_items(gobj, event, kw, src) {
24226
24499
  return 0;
24227
24500
  }
24228
24501
  /************************************************************
24502
+ * EV_SET_ITEM_BADGE {item_id, badge, badge_tone} — the badge of
24503
+ * one item, set at runtime (yui_shell_set_nav_item_badge).
24504
+ *
24505
+ * Kept in `badges`, so every later rebuild draws it, and painted
24506
+ * in place: no rebuild for a count that moves on every tick. An
24507
+ * id this nav does not draw is kept all the same — a later
24508
+ * EV_SET_ITEMS may bring the item.
24509
+ ************************************************************/
24510
+ function ac_set_item_badge(gobj, event, kw, src) {
24511
+ let item_id = kw.item_id;
24512
+ if (empty_string(item_id)) {
24513
+ log_error(`C_YUI_NAV: ${event} without item_id, from ${gobj_short_name(src)}`);
24514
+ return -1;
24515
+ }
24516
+ let menu_id = gobj_read_attr(gobj, "menu_id") || "";
24517
+ let tone = badge_tone(kw.badge_tone, `C_YUI_NAV '${menu_id}': item '${item_id}'`);
24518
+ let badge = kw.badge === void 0 ? null : kw.badge;
24519
+ let badges = Object.assign({}, gobj_read_attr(gobj, "badges"));
24520
+ badges[item_id] = {
24521
+ badge,
24522
+ badge_tone: tone
24523
+ };
24524
+ gobj_write_attr(gobj, "badges", badges);
24525
+ let $c = gobj_read_attr(gobj, "$container");
24526
+ if (!$c) return 0;
24527
+ let $badges = $c.querySelectorAll(`[data-item-id="${css_escape(item_id)}"] .NAV_ITEM_BADGE`);
24528
+ for (let $badge of $badges) paint_badge($badge, badge, tone);
24529
+ return 0;
24530
+ }
24531
+ /************************************************************
24229
24532
  * EV_ROUTE_CHANGED from the shell — refresh active highlight
24230
24533
  ************************************************************/
24231
24534
  function ac_route_changed(gobj, event, kw, src) {
@@ -24281,18 +24584,27 @@ function create_gclass$7(gclass_name) {
24281
24584
  log_error(`GClass ALREADY created: ${gclass_name}`);
24282
24585
  return -1;
24283
24586
  }
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
- ]]]];
24587
+ const states = [["ST_IDLE", [
24588
+ [
24589
+ "EV_ROUTE_CHANGED",
24590
+ ac_route_changed,
24591
+ null
24592
+ ],
24593
+ [
24594
+ "EV_SET_ITEMS",
24595
+ ac_set_items,
24596
+ null
24597
+ ],
24598
+ [
24599
+ "EV_SET_ITEM_BADGE",
24600
+ ac_set_item_badge,
24601
+ null
24602
+ ]
24603
+ ]]];
24293
24604
  const event_types = [
24294
24605
  ["EV_ROUTE_CHANGED", 0],
24295
24606
  ["EV_SET_ITEMS", 0],
24607
+ ["EV_SET_ITEM_BADGE", 0],
24296
24608
  ["EV_NAV_CLICKED", event_flag_t.EVF_OUTPUT_EVENT | event_flag_t.EVF_PUBLIC_EVENT],
24297
24609
  ["EV_NAV_ITEM_CLOSE", event_flag_t.EVF_OUTPUT_EVENT | event_flag_t.EVF_PUBLIC_EVENT],
24298
24610
  ["EV_DRAWER_CLOSE_REQUESTED", event_flag_t.EVF_OUTPUT_EVENT | event_flag_t.EVF_PUBLIC_EVENT]
@@ -83199,6 +83511,6 @@ function setup_json_pad(self) {
83199
83511
  return win;
83200
83512
  }
83201
83513
  //#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 };
83514
+ 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
83515
 
83204
83516
  //# sourceMappingURL=gobj-ui.es.js.map