@yuneta/gobj-ui 7.26.28 → 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.
package/src/c_yui_form.js CHANGED
@@ -805,11 +805,13 @@ function build_html_form(gobj, $form, prefix, template)
805
805
  }
806
806
 
807
807
  /*
808
- * Build the html field and add to form, if <tag> is not-null
808
+ * Build the html field and add to form, if <tag> is not-null.
809
+ * Its label and options are the schema's: column names and
810
+ * headers, data.
809
811
  */
810
812
  if(html_field_conf.tag) {
811
- let $field = create_form_field(gobj, $form, prefix, field_desc, html_field_conf);
812
- name_form_control($field, html_field_conf);
813
+ let $field = create_form_field(gobj, $form, prefix, field_desc, /* i18n: data */ html_field_conf);
814
+ name_form_control($field, /* i18n: data */ html_field_conf);
813
815
  $form.appendChild($field);
814
816
  }
815
817
  });
@@ -2862,7 +2862,7 @@ function ac_open_gclass(gobj, event, kw, src)
2862
2862
  close_gclass_view(priv.gclass_view);
2863
2863
  priv.gclass_view = null;
2864
2864
 
2865
- priv.gclass_view = open_gclass_view(gobj, node_data.gclass, {
2865
+ priv.gclass_view = open_gclass_view(gobj, /* i18n: data */ node_data.gclass, {
2866
2866
  title_prefix: gobj_yuno_name() || "",
2867
2867
  /* Which state THIS gobj is in. The description describes the
2868
2868
  * CLASS and cannot carry it, and the tree is the only party
package/src/c_yui_nav.js CHANGED
@@ -33,11 +33,29 @@
33
33
  *
34
34
  * Each item supports:
35
35
  * id, name, icon (CSS class or svg id), route, disabled
36
- * (`badge` was listed here for a long time and NOTHING ever
37
- * rendered it — a menu item cannot carry a count today. The
38
- * toolbar can: `badge` on a toolbar item plus
39
- * yui_shell_set_toolbar_item_badge(). Implement it here the
40
- * day a menu entry needs one, rather than re-listing it.)
36
+ * badge — a count, or a short text, carried by the item:
37
+ * the toolbar badge's rules, helper and live
38
+ * region (yui_badge.js) -- 0 / "" / null show
39
+ * none, over 99 reads "99+". It sits on the
40
+ * corner of the icon where the item is an icon
41
+ * over its label (icon-bar, cards, a stacked
42
+ * item) or an icon alone, and after the label
43
+ * where the item reads as a line (vertical,
44
+ * drawer, submenu, tabs, an accordion head).
45
+ * The field only seeds the paint: a count is a
46
+ * RUNTIME fact, set by
47
+ * yui_shell_set_nav_item_badge(shell, id, value,
48
+ * tone), which reaches every nav drawing the id.
49
+ * Not drawn by "breadcrumb", deliberately: a
50
+ * crumb names a step of the way in, and the
51
+ * count belongs to the menu entry that leads
52
+ * there -- a trail with counts on it reads as a
53
+ * list of destinations, which is what a
54
+ * breadcrumb is not. ("backbar" has no items.)
55
+ * badge_tone — "danger" (the default look), "warning",
56
+ * "info", "success", "primary" or "link":
57
+ * Bulma's is-<tone> on the badge. An unknown
58
+ * one is logged and drawn with the default look.
41
59
  * class — extra CSS class(es) on the item (tabs layout), e.g.
42
60
  * a per-item state colour like "yui-nav-disconnected"
43
61
  * closable — render a trailing ✕ that emits EV_NAV_ITEM_CLOSE
@@ -48,6 +66,13 @@
48
66
  * active highlight restored. The app_config path is simply the
49
67
  * first, startup caller of the same mechanism.
50
68
  *
69
+ * One item's badge is set at runtime by sending EV_SET_ITEM_BADGE
70
+ * {item_id, badge, badge_tone}: painted in place, no rebuild, and
71
+ * KEPT in the `badges` attr, which wins over the item's own fields
72
+ * in every later rebuild. The shell sends it to all its navs
73
+ * (yui_shell_set_nav_item_badge) and hands `badges` to every nav
74
+ * it creates.
75
+ *
51
76
  * In addition to navigable items, secondary navs accept two
52
77
  * decorative item kinds for in-place visual grouping:
53
78
  *
@@ -71,6 +96,7 @@ import {
71
96
  gobj_parent,
72
97
  gobj_read_attr, gobj_read_pointer_attr, gobj_write_attr,
73
98
  gobj_publish_event,
99
+ gobj_short_name,
74
100
  createElement2, empty_string, is_array, is_object, is_string,
75
101
  } from "@yuneta/gobj-js";
76
102
 
@@ -83,6 +109,7 @@ import {
83
109
  bulma_hidden_class,
84
110
  } from "./shell_show_on.js";
85
111
  import {text_node} from "./yui_text.js";
112
+ import {badge_node, badge_tone, paint_badge} from "./yui_badge.js";
86
113
 
87
114
  /***************************************************************
88
115
  * Constants
@@ -118,6 +145,7 @@ SDATA(data_type_t.DTP_BOOLEAN, "show_label", 0, true, "Show item la
118
145
  SDATA(data_type_t.DTP_STRING, "level", 0, "primary", "primary|secondary"),
119
146
  SDATA(data_type_t.DTP_STRING, "show_on", 0, "", "Breakpoint visibility expr (shell show_on syntax); empty = always"),
120
147
  SDATA(data_type_t.DTP_STRING, "back_route", 0, "", "backbar layout: route of the section index the ← returns to"),
148
+ 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"),
121
149
 
122
150
  SDATA(data_type_t.DTP_POINTER, "shell", 0, null, "C_YUI_SHELL gobj (for navigation calls)"),
123
151
  SDATA(data_type_t.DTP_POINTER, "$container", 0, null, "Root HTMLElement of this nav"),
@@ -230,17 +258,19 @@ function build_ui(gobj)
230
258
  let menu_id = gobj_read_attr(gobj, "menu_id") || "nav";
231
259
  let $container;
232
260
 
261
+ /* The items are the app's nav declaration: the keys of their names
262
+ * are read there (app_config.json), not here. */
233
263
  switch(layout) {
234
- case "vertical": $container = render_vertical(gobj, items); break;
235
- case "icon-bar": $container = render_icon_bar(gobj, items); break;
236
- case "tabs": $container = render_tabs(gobj, items); break;
237
- case "drawer": $container = render_drawer(gobj, items); break;
238
- case "submenu": $container = render_submenu(gobj, items); break;
239
- case "accordion": $container = render_accordion(gobj, items); break;
240
- case "cards": $container = render_cards(gobj, items); break;
241
- case "backbar": $container = render_backbar(gobj); break;
242
- case "breadcrumb": $container = render_breadcrumb(gobj, items); break;
243
- default: $container = render_vertical(gobj, items);
264
+ case "vertical": $container = render_vertical(gobj, /* i18n: data */ items); break;
265
+ case "icon-bar": $container = render_icon_bar(gobj, /* i18n: data */ items); break;
266
+ case "tabs": $container = render_tabs(gobj, /* i18n: data */ items); break;
267
+ case "drawer": $container = render_drawer(gobj, /* i18n: data */ items); break;
268
+ case "submenu": $container = render_submenu(gobj, /* i18n: data */ items); break;
269
+ case "accordion": $container = render_accordion(gobj, /* i18n: data */ items); break;
270
+ case "cards": $container = render_cards(gobj, /* i18n: data */ items); break;
271
+ case "backbar": $container = render_backbar(gobj); break;
272
+ case "breadcrumb": $container = render_breadcrumb(gobj, /* i18n: data */ items); break;
273
+ default: $container = render_vertical(gobj, /* i18n: data */ items);
244
274
  }
245
275
  $container.setAttribute("data-nav-zone", zone);
246
276
  $container.setAttribute("data-nav-layout", layout);
@@ -407,13 +437,20 @@ function render_tabs(gobj, items)
407
437
  continue; /* tab strips have no room for section labels */
408
438
  }
409
439
  let children = [];
440
+ let has_label = show_label && !empty_string(it.name);
441
+ /* An icon-only tab carries its badge on the icon; a labelled
442
+ * one after the label, before the ✕. */
443
+ let pinned = !empty_string(it.icon) && !has_label;
444
+ let badge_el = item_badge_node(gobj, it, pinned);
410
445
  if(!empty_string(it.icon)) {
411
- children.push(["span", {class: "icon is-small"},
412
- ["i", {class: it.icon, "aria-hidden":"true"}]]);
446
+ children.push(icon_span(it.icon, "icon is-small", pinned ? badge_el : null));
413
447
  }
414
- if(show_label && !empty_string(it.name)) {
448
+ if(has_label) {
415
449
  children.push(["span", {i18n: it.name}, text_node(it.name)]);
416
450
  }
451
+ if(!pinned) {
452
+ children.push(badge_el);
453
+ }
417
454
  /* Optional close affordance: a trailing ✕ that emits
418
455
  * EV_NAV_ITEM_CLOSE instead of navigating (caught first in
419
456
  * wire_clicks by its data-close-item marker). */
@@ -460,7 +497,8 @@ function render_tabs(gobj, items)
460
497
  function render_cards(gobj, items)
461
498
  {
462
499
  let show_label = gobj_read_attr(gobj, "show_label");
463
- return createElement2(cards_grid_descriptor(items, show_label));
500
+ return createElement2(cards_grid_descriptor(items, show_label,
501
+ (it, pinned) => item_badge_node(gobj, it, pinned)));
464
502
  }
465
503
 
466
504
  /************************************************************
@@ -479,6 +517,9 @@ function render_cards(gobj, items)
479
517
  * other layout — so the delegated click handler and the active
480
518
  * highlight work unchanged, and the last crumb marks itself through
481
519
  * `active_route` like a tab does.
520
+ *
521
+ * No badge, deliberately: a crumb names a step of the way in, and a
522
+ * count belongs to the menu entry that leads there.
482
523
  ************************************************************/
483
524
  function render_breadcrumb(gobj, items)
484
525
  {
@@ -642,7 +683,9 @@ function render_accordion(gobj, items)
642
683
  head_attrs["aria-label"] = head_text;
643
684
  head_attrs["data-i18n-aria-label"] = head_text;
644
685
  }
645
- let $hdr = createElement2(["button", head_attrs, text_node(head_text)]);
686
+ /* A section head reads as a line: its badge follows the label */
687
+ let $hdr = createElement2(["button", head_attrs,
688
+ [text_node(head_text), item_badge_node(gobj, it, false)]]);
646
689
  $container.appendChild($hdr);
647
690
 
648
691
  let $ul = createElement2(
@@ -694,16 +737,21 @@ function item_li(gobj, it, opts)
694
737
  let { icon_pos, show_label, stacked } = opts;
695
738
  let children = [];
696
739
  let label = it.name || "";
740
+ let is_stacked = !!(stacked || icon_pos === "top" || icon_pos === "bottom");
697
741
 
698
- let icon_el = !empty_string(it.icon)
699
- ? ["span", {class: "icon"}, ["i", {class: it.icon, "aria-hidden":"true"}]]
700
- : null;
701
742
  let label_el = (show_label && !empty_string(label))
702
743
  ? ["span", {class: "yui-nav-label", i18n: label}, text_node(label)]
703
744
  : null;
745
+ /* On the corner of the icon where the item is an icon over its
746
+ * label or an icon alone; after the label where it reads as a line. */
747
+ let pinned = !empty_string(it.icon) && (is_stacked || !label_el);
748
+ let badge_el = item_badge_node(gobj, it, pinned);
749
+ let icon_el = !empty_string(it.icon)
750
+ ? icon_span(it.icon, "icon", pinned ? badge_el : null)
751
+ : null;
704
752
 
705
753
  let a_class = "yui-nav-item";
706
- if(stacked || icon_pos === "top" || icon_pos === "bottom") {
754
+ if(is_stacked) {
707
755
  a_class += " yui-nav-stacked";
708
756
  }
709
757
 
@@ -711,6 +759,9 @@ function item_li(gobj, it, opts)
711
759
  if(label_el) {
712
760
  children.push(label_el);
713
761
  }
762
+ if(!pinned) {
763
+ children.push(badge_el);
764
+ }
714
765
  if(icon_el) {
715
766
  children.push(icon_el);
716
767
  }
@@ -721,6 +772,9 @@ function item_li(gobj, it, opts)
721
772
  if(label_el) {
722
773
  children.push(label_el);
723
774
  }
775
+ if(!pinned) {
776
+ children.push(badge_el);
777
+ }
724
778
  }
725
779
 
726
780
  let aria_key = label || it.id;
@@ -763,9 +817,12 @@ function item_iconbar(gobj, it, opts)
763
817
  {
764
818
  let { icon_pos, show_label } = opts;
765
819
  let label = it.name || "";
766
- let icon_el = !empty_string(it.icon)
767
- ? ["span", {class: "icon is-medium"},
768
- ["i", {class: it.icon, "aria-hidden":"true"}]]
820
+ /* An icon bar item is an icon over its label: the badge goes on
821
+ * the corner of the icon, or after the label of one that has none. */
822
+ let pinned = !empty_string(it.icon);
823
+ let badge_el = item_badge_node(gobj, it, pinned);
824
+ let icon_el = pinned
825
+ ? icon_span(it.icon, "icon is-medium", badge_el)
769
826
  : null;
770
827
  let label_el = (show_label && !empty_string(label))
771
828
  ? ["span", {class: "yui-nav-label is-size-7", i18n: label}, text_node(label)]
@@ -787,6 +844,9 @@ function item_iconbar(gobj, it, opts)
787
844
  children.push(label_el);
788
845
  }
789
846
  }
847
+ if(!pinned) {
848
+ children.push(badge_el);
849
+ }
790
850
 
791
851
  let a_attrs = {
792
852
  class: "yui-nav-item yui-nav-stacked",
@@ -808,6 +868,41 @@ function item_iconbar(gobj, it, opts)
808
868
  ];
809
869
  }
810
870
 
871
+ /************************************************************
872
+ * The badge of an item (yui_badge.js), always drawn -- hidden
873
+ * when empty -- so EV_SET_ITEM_BADGE has a node to write into.
874
+ * What was set at runtime for its id (`badges`) wins over the
875
+ * item's own `badge` / `badge_tone`. `pinned`: on the corner of
876
+ * the icon instead of after the label.
877
+ ************************************************************/
878
+ function item_badge_node(gobj, it, pinned)
879
+ {
880
+ let badges = gobj_read_attr(gobj, "badges");
881
+ let state = it;
882
+ if(is_object(badges) && is_string(it.id) &&
883
+ Object.prototype.hasOwnProperty.call(badges, it.id)) {
884
+ state = badges[it.id] || {};
885
+ }
886
+ let classes = "NAV_ITEM_BADGE yui-badge yui-nav-badge";
887
+ if(pinned) {
888
+ classes += " yui-nav-badge-pinned";
889
+ }
890
+ return badge_node(state.badge, state.badge_tone, classes,
891
+ `C_YUI_NAV '${gobj_read_attr(gobj, "menu_id") || ""}': item '${it.id}'`);
892
+ }
893
+
894
+ /************************************************************
895
+ * An item's icon, with its badge on the corner when given one.
896
+ ************************************************************/
897
+ function icon_span(icon, classes, badge_el)
898
+ {
899
+ let glyph = ["i", {class: icon, "aria-hidden": "true"}];
900
+ if(!badge_el) {
901
+ return ["span", {class: classes}, glyph];
902
+ }
903
+ return ["span", {class: `${classes} yui-nav-icon`}, [glyph, badge_el]];
904
+ }
905
+
811
906
  function wire_clicks(gobj, $container)
812
907
  {
813
908
  let priv = gobj.priv;
@@ -930,6 +1025,44 @@ function ac_set_items(gobj, event, kw, src)
930
1025
  return 0;
931
1026
  }
932
1027
 
1028
+ /************************************************************
1029
+ * EV_SET_ITEM_BADGE {item_id, badge, badge_tone} — the badge of
1030
+ * one item, set at runtime (yui_shell_set_nav_item_badge).
1031
+ *
1032
+ * Kept in `badges`, so every later rebuild draws it, and painted
1033
+ * in place: no rebuild for a count that moves on every tick. An
1034
+ * id this nav does not draw is kept all the same — a later
1035
+ * EV_SET_ITEMS may bring the item.
1036
+ ************************************************************/
1037
+ function ac_set_item_badge(gobj, event, kw, src)
1038
+ {
1039
+ let item_id = kw.item_id;
1040
+ if(empty_string(item_id)) {
1041
+ log_error(`C_YUI_NAV: ${event} without item_id, from ${gobj_short_name(src)}`);
1042
+ return -1;
1043
+ }
1044
+ let menu_id = gobj_read_attr(gobj, "menu_id") || "";
1045
+ let tone = badge_tone(kw.badge_tone, `C_YUI_NAV '${menu_id}': item '${item_id}'`);
1046
+ let badge = (kw.badge === undefined) ? null : kw.badge;
1047
+
1048
+ /* A new dict: the one in the attr may be the creator's. */
1049
+ let badges = Object.assign({}, gobj_read_attr(gobj, "badges"));
1050
+ badges[item_id] = {badge: badge, badge_tone: tone};
1051
+ gobj_write_attr(gobj, "badges", badges);
1052
+
1053
+ let $c = gobj_read_attr(gobj, "$container");
1054
+ if(!$c) {
1055
+ return 0;
1056
+ }
1057
+ let $badges = $c.querySelectorAll(
1058
+ `[data-item-id="${css_escape(item_id)}"] .NAV_ITEM_BADGE`
1059
+ );
1060
+ for(let $badge of $badges) {
1061
+ paint_badge($badge, badge, tone);
1062
+ }
1063
+ return 0;
1064
+ }
1065
+
933
1066
  /************************************************************
934
1067
  * EV_ROUTE_CHANGED from the shell — refresh active highlight
935
1068
  ************************************************************/
@@ -1041,14 +1174,16 @@ function create_gclass(gclass_name)
1041
1174
 
1042
1175
  const states = [
1043
1176
  ["ST_IDLE", [
1044
- ["EV_ROUTE_CHANGED", ac_route_changed, null],
1045
- ["EV_SET_ITEMS", ac_set_items, null]
1177
+ ["EV_ROUTE_CHANGED", ac_route_changed, null],
1178
+ ["EV_SET_ITEMS", ac_set_items, null],
1179
+ ["EV_SET_ITEM_BADGE", ac_set_item_badge, null]
1046
1180
  ]]
1047
1181
  ];
1048
1182
 
1049
1183
  const event_types = [
1050
1184
  ["EV_ROUTE_CHANGED", 0],
1051
1185
  ["EV_SET_ITEMS", 0],
1186
+ ["EV_SET_ITEM_BADGE", 0],
1052
1187
  ["EV_NAV_CLICKED", event_flag_t.EVF_OUTPUT_EVENT|event_flag_t.EVF_PUBLIC_EVENT],
1053
1188
  ["EV_NAV_ITEM_CLOSE", event_flag_t.EVF_OUTPUT_EVENT|event_flag_t.EVF_PUBLIC_EVENT],
1054
1189
  ["EV_DRAWER_CLOSE_REQUESTED", event_flag_t.EVF_OUTPUT_EVENT|event_flag_t.EVF_PUBLIC_EVENT]
@@ -386,7 +386,7 @@ function mode_name(mode)
386
386
  return t(mode.roll.label || mode.id);
387
387
  }
388
388
  if(mode.kind === "bucket") {
389
- return period_name(/* i18n: YUI_TIME_I18N_KEYS */ mode.spec, t);
389
+ return period_name(mode.spec, t);
390
390
  }
391
391
  return t(mode.id); /* span / custom */
392
392
  }
@@ -482,7 +482,7 @@ function build_ui(gobj)
482
482
  if(mode.overflow || mode.hidden) {
483
483
  continue;
484
484
  }
485
- priv.$modes.appendChild(build_mode_button(gobj, mode));
485
+ priv.$modes.appendChild(build_mode_button(gobj, /* i18n: C_YUI_PERIOD_I18N_KEYS, YUI_TIME_I18N_KEYS */ mode));
486
486
  }
487
487
 
488
488
  /* Scroll plumbing, not an action: it changes nothing outside the strip.
@@ -572,7 +572,7 @@ function build_mode_button(gobj, mode)
572
572
  {
573
573
  /* The name is COMPOSED for a bucket (period_name), so it carries no
574
574
  * key: ac_language_changed() rewrites the text and both names. */
575
- let name = mode_name(/* i18n: C_YUI_PERIOD_I18N_KEYS, YUI_TIME_I18N_KEYS */ mode);
575
+ let name = mode_name(mode);
576
576
  let $btn = createElement2(
577
577
  ["button", {class: `button YUI_PERIOD_MODE YUI_PERIOD_MODE_${mode.id.toUpperCase()}`,
578
578
  type: "button", title: name, "aria-label": name},
@@ -383,7 +383,7 @@ function mt_destroy(gobj)
383
383
  function refuse_bad_name(gobj, $message)
384
384
  {
385
385
  mark_name_field(gobj, true);
386
- yui_shell_show_error(yui_shell_of(gobj), /* i18n: data */ $message, {t: t});
386
+ yui_shell_show_error(yui_shell_of(gobj), $message, {t: t});
387
387
  return -1;
388
388
  }
389
389
 
@@ -1258,7 +1258,7 @@ function render(gobj)
1258
1258
  }
1259
1259
  if(state === "ST_IDLE") {
1260
1260
  show_notice(gobj, priv.load_error ? "cannot load the schemas" : "not connected",
1261
- priv.load_error, true);
1261
+ /* i18n: data */ priv.load_error, true);
1262
1262
  destroy_diagram(gobj);
1263
1263
  clear($body);
1264
1264
  return;
@@ -1362,16 +1362,16 @@ function render_treedbs(gobj, $body)
1362
1362
  let $cards = [];
1363
1363
 
1364
1364
  for(let treedb of priv.model.treedbs) {
1365
- let tags = [
1365
+ const from_c = treedb.c_schema_version !== undefined &&
1366
+ String(treedb.c_schema_version) !== String(treedb.schema_version);
1367
+ const tags = [
1366
1368
  {i18n: "topics", text: treedb.topics.length},
1367
1369
  {i18n: "version", text: treedb.schema_version === undefined
1368
- ? "?" : treedb.schema_version}
1370
+ ? "?" : treedb.schema_version},
1371
+ ...(from_c
1372
+ ? [{i18n: "from c", text: treedb.c_schema_version, style: "is-warning is-light"}]
1373
+ : [])
1369
1374
  ];
1370
- if(treedb.c_schema_version !== undefined &&
1371
- String(treedb.c_schema_version) !== String(treedb.schema_version)
1372
- ) {
1373
- tags.push({i18n: "from c", text: treedb.c_schema_version, style: "is-warning is-light"});
1374
- }
1375
1375
  $cards.push(open_card(gobj, "SCHEMA_TREEDB_CARD", treedb.id, "yi-database",
1376
1376
  tags, "EV_SELECT_TREEDB", {treedb: treedb.id}));
1377
1377
  }
@@ -1406,7 +1406,7 @@ function render_topics(gobj, $body)
1406
1406
  let treedb = current_treedb(gobj);
1407
1407
 
1408
1408
  if(!treedb) {
1409
- show_notice(gobj, "that treedb is not here any more", priv.treedb_id);
1409
+ show_notice(gobj, "that treedb is not here any more", /* i18n: data */ priv.treedb_id);
1410
1410
  return;
1411
1411
  }
1412
1412
 
@@ -1554,7 +1554,7 @@ function render_columns(gobj, $body)
1554
1554
  let topic = current_topic(gobj);
1555
1555
 
1556
1556
  if(!topic) {
1557
- show_notice(gobj, "that topic is not here any more", priv.topic_name);
1557
+ show_notice(gobj, "that topic is not here any more", /* i18n: data */ priv.topic_name);
1558
1558
  return;
1559
1559
  }
1560
1560
 
@@ -1815,7 +1815,7 @@ function render_diagram(gobj)
1815
1815
  if(!treedb) {
1816
1816
  destroy_diagram(gobj);
1817
1817
  clear($body);
1818
- show_notice(gobj, "that treedb is not here any more", priv.treedb_id);
1818
+ show_notice(gobj, "that treedb is not here any more", /* i18n: data */ priv.treedb_id);
1819
1819
  return;
1820
1820
  }
1821
1821
  if(!gclass_find_by_name("C_YUI_TREEDB_SCHEMA")) {
@@ -1921,7 +1921,7 @@ function open_dialog(gobj, $content, title, logical, title_prefix)
1921
1921
  priv.dialog_gen = priv.model_gen;
1922
1922
  priv.dialog_kind = logical;
1923
1923
  priv.dialog_content = $content;
1924
- priv.dialog = yui_shell_show_modal(yui_shell_of(gobj), /* i18n: data */ $content, {
1924
+ priv.dialog = yui_shell_show_modal(yui_shell_of(gobj), $content, {
1925
1925
  dialog: true,
1926
1926
  title: title,
1927
1927
  title_prefix: title_prefix || "",
@@ -2435,8 +2435,8 @@ function open_report(gobj, findings, treedb_id)
2435
2435
  return ["tr", {class: `SCHEMA_REPORT_ROW SCHEMA_REPORT_${finding.severity.toUpperCase()}`}, [
2436
2436
  ["td", {class: "SCHEMA_REPORT_SEVERITY"}, [
2437
2437
  ["span", {class: `tag ${finding.severity === "error" ? "is-danger" : "is-warning"}` +
2438
- " is-light", i18n: /* i18n: C_YUI_SCHEMA_EDITOR_I18N_KEYS */ finding.severity},
2439
- t(/* i18n: C_YUI_SCHEMA_EDITOR_I18N_KEYS */ finding.severity)]
2438
+ " is-light", i18n: finding.severity},
2439
+ t(finding.severity)]
2440
2440
  ]],
2441
2441
  ["td", {class: "SCHEMA_REPORT_WHAT", i18n: finding.code}, t(finding.code)],
2442
2442
  ["td", {class: "SCHEMA_REPORT_TOPIC"}, text_node(finding.topic || "")],
@@ -4256,7 +4256,7 @@ function confirm_column_delete(gobj, verdict)
4256
4256
  }
4257
4257
  $warning = createElement2(["div", {class: "SCHEMA_COLUMN_DATA_WARNING"}, children]);
4258
4258
  }
4259
- confirm_then(gobj, $warning? /* i18n: data */ $warning : message, `${c.topic}.${c.col}`,
4259
+ confirm_then(gobj, $warning? $warning : message, `${c.topic}.${c.col}`,
4260
4260
  {what: "column", topic: c.topic, col: c.col});
4261
4261
  return 0;
4262
4262
  }
@@ -4494,7 +4494,9 @@ function ac_validate(gobj, event, kw, src)
4494
4494
  written_topics: Object.keys(priv.written),
4495
4495
  baseline: priv.baseline
4496
4496
  });
4497
- open_report(gobj, findings, treedb.id);
4497
+ /* The severities are C_YUI_SCHEMA_EDITOR_I18N_KEYS; the codes are the
4498
+ * checks' own keys, read where schema_validate.js writes them. */
4499
+ open_report(gobj, /* i18n: C_YUI_SCHEMA_EDITOR_I18N_KEYS */ findings, treedb.id);
4498
4500
  if(has_errors(findings)) {
4499
4501
  log_warning(`${gobj_short_name(gobj)}: ${treedb.id} would be refused as it is`);
4500
4502
  }
@@ -4578,7 +4580,9 @@ function ac_preview_import(gobj, event, kw, src)
4578
4580
 
4579
4581
  let plan = plan_import(treedb, incoming, {prune: !!kw.prune});
4580
4582
  priv.import_plan = plan;
4581
- render_import_plan(gobj, plan);
4583
+ /* Its writes are SCHEMA_IMPORT_I18N_KEYS; its conflicts are keys of
4584
+ * their own, read where schema_import.js writes them. */
4585
+ render_import_plan(gobj, /* i18n: SCHEMA_IMPORT_I18N_KEYS */ plan);
4582
4586
  return 0;
4583
4587
  }
4584
4588
 
@@ -459,29 +459,30 @@
459
459
  }
460
460
 
461
461
  /*------------------------------------------------------------
462
- * Toolbar badge — the count pinned to an item's icon
462
+ * Badge — the count of a toolbar item or a menu item
463
+ * (yui_badge.js)
463
464
  *
464
- * Anchored to the ICON and not to the button: with its label on
465
- * (desktop) the button is wide, and a badge in its corner would
466
- * float away from the glyph it counts.
465
+ * `.yui-badge` is the LOOK, one for both. Where it SITS is the
466
+ * host's: `.yui-toolbar-badge` on the corner of a toolbar icon,
467
+ * `.yui-nav-badge` after a menu item's label, or on the corner
468
+ * of its icon (`.yui-nav-badge-pinned`).
469
+ *
470
+ * Anchored to the ICON and not to the control: with its label on
471
+ * (desktop) a toolbar button is wide, and a badge in its corner
472
+ * would float away from the glyph it counts.
467
473
  *
468
474
  * `hidden` is honoured explicitly: the element is always in the
469
475
  * DOM (so the runtime setter has somewhere to write) and
470
476
  * [hidden] loses to `display:inline-flex` without this rule.
471
477
  *
472
- * Danger colour on purpose — a badge exists to pull the eye. It
473
- * is its own colour pair in both schemes rather than a Bulma
474
- * helper, so it keeps its contrast when the toolbar background
475
- * changes with the theme. No transition: house rule.
478
+ * Danger colour by default, on purpose — a badge exists to pull
479
+ * the eye. A TONE (`is-warning`, `is-info`, …) is a FILL: the
480
+ * raw Bulma token under its `-invert` ink, its own colour pair in
481
+ * both schemes rather than a Bulma helper, so the badge keeps its
482
+ * contrast when the bar under it changes with the theme (README,
483
+ * "Colour"). No transition: house rule.
476
484
  *------------------------------------------------------------*/
477
- .yui-toolbar-icon {
478
- position: relative;
479
- overflow: visible;
480
- }
481
- .yui-toolbar-badge {
482
- position: absolute;
483
- top: -0.35em;
484
- right: -0.55em;
485
+ .yui-badge {
485
486
  display: inline-flex;
486
487
  align-items: center;
487
488
  justify-content: center;
@@ -491,19 +492,84 @@
491
492
  height: 1.15em;
492
493
  padding: 0 0.3em;
493
494
  border-radius: 999px;
494
- font-size: 0.62em;
495
495
  font-weight: 700;
496
496
  line-height: 1;
497
497
  font-family: inherit;
498
+ white-space: nowrap;
498
499
  background-color: var(--bulma-danger, #f14668);
499
500
  color: var(--bulma-danger-invert, #fff);
500
501
  /* Separates the pill from the glyph when they overlap. */
501
502
  box-shadow: 0 0 0 0.12em var(--bulma-scheme-main, #fff);
502
503
  pointer-events: none;
503
504
  }
504
- .yui-toolbar-badge[hidden] {
505
+ .yui-badge[hidden] {
505
506
  display: none;
506
507
  }
508
+ .yui-badge.is-danger {
509
+ background-color: var(--bulma-danger, #f14668);
510
+ color: var(--bulma-danger-invert, #fff);
511
+ }
512
+ .yui-badge.is-warning {
513
+ background-color: var(--bulma-warning, #ffb70f);
514
+ color: var(--bulma-warning-invert, rgba(0, 0, 0, 0.7));
515
+ }
516
+ .yui-badge.is-info {
517
+ background-color: var(--bulma-info, #66d1ff);
518
+ color: var(--bulma-info-invert, rgba(0, 0, 0, 0.7));
519
+ }
520
+ .yui-badge.is-success {
521
+ background-color: var(--bulma-success, #48c78e);
522
+ color: var(--bulma-success-invert, rgba(0, 0, 0, 0.7));
523
+ }
524
+ .yui-badge.is-primary {
525
+ background-color: var(--bulma-primary, #00d1b2);
526
+ color: var(--bulma-primary-invert, rgba(0, 0, 0, 0.7));
527
+ }
528
+ .yui-badge.is-link {
529
+ background-color: var(--bulma-link, #485fc7);
530
+ color: var(--bulma-link-invert, #fff);
531
+ }
532
+
533
+ /* Toolbar: on the corner of the icon, sized by the icon. */
534
+ .yui-toolbar-icon {
535
+ position: relative;
536
+ overflow: visible;
537
+ }
538
+ .yui-toolbar-badge {
539
+ position: absolute;
540
+ top: -0.35em;
541
+ right: -0.55em;
542
+ font-size: 0.62em;
543
+ }
544
+
545
+ /* Menu item: after the label. Sized in rem, so it is the same
546
+ * pill in every layout whatever the item's own font size. */
547
+ .yui-nav-badge {
548
+ flex: 0 0 auto;
549
+ margin-inline-start: 0.4rem;
550
+ font-size: 0.75rem;
551
+ vertical-align: middle;
552
+ }
553
+ /* Where the item already spaces its children (a flex row or column
554
+ * with a gap), the gap is the space. */
555
+ .yui-nav-vertical .menu-list a > .yui-nav-badge,
556
+ .yui-nav-stacked > .yui-nav-badge,
557
+ .yui-nav-cards .yui-nav-card > .yui-nav-badge {
558
+ margin-inline-start: 0;
559
+ }
560
+ /* ...or on the corner of the icon, where the item is an icon over
561
+ * its label (icon bar, cards, a stacked item) or an icon alone. */
562
+ .yui-nav-icon {
563
+ position: relative;
564
+ overflow: visible;
565
+ }
566
+ .yui-nav-badge.yui-nav-badge-pinned {
567
+ position: absolute;
568
+ top: -0.35em;
569
+ inset-inline-end: -0.55em;
570
+ margin: 0;
571
+ font-size: 0.7rem;
572
+ }
507
573
 
508
574
  /*============================================================
509
575
  * Toolbar — type:"brand"