@yuneta/gobj-ui 2.2.4 → 2.3.0

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_map.js CHANGED
@@ -559,10 +559,13 @@ function onClick(gobj, e)
559
559
  const coordinates = e.features[0].geometry.coordinates.slice();
560
560
  const properties = e.features[0].properties;
561
561
 
562
- const gobj_service = gobj_find_service(
563
- properties.gobj_service_name,
564
- true
565
- );
562
+ /* A marker may or may not be backed by a gobj service. Only look it up
563
+ * when the feature carries a name (verbose=false: a marker without a
564
+ * service is a normal case handled by the popup branch below, not an
565
+ * error). gobj_find_service() would otherwise crash on undefined. */
566
+ const gobj_service = properties.gobj_service_name
567
+ ? gobj_find_service(properties.gobj_service_name, false)
568
+ : null;
566
569
  if(gobj_service) {
567
570
  let name = clean_name(gobj_name(gobj_service));
568
571
  let window_service_name = `window-map-${name}`;
package/src/c_yui_nav.js CHANGED
@@ -12,6 +12,17 @@
12
12
  * "drawer" — off-canvas vertical (toggled by hamburger)
13
13
  * "submenu" — Bulma .menu nested under a heading
14
14
  * "accordion" — Bulma .menu list with collapsible group
15
+ * "cards" — grid of tappable cards (section-index landing;
16
+ * mounted as a stage view by C_YUI_SHELL when a
17
+ * primary item declares submenu.index)
18
+ * "backbar" — single "← <section>" link back to the section
19
+ * index; the mobile replacement of the tab strip
20
+ * for submenu.index sections (the shell collapses
21
+ * the secondary zone while the index is on stage)
22
+ *
23
+ * The `show_on` attr (same expressions as the shell's zone
24
+ * `show_on`) applies breakpoint visibility classes to the nav
25
+ * container; build_ui re-applies them on every rebuild.
15
26
  *
16
27
  * Each item supports:
17
28
  * id, name, icon (CSS class or svg id), route, badge, disabled
@@ -51,13 +62,21 @@ import {
51
62
  createElement2, empty_string, is_array, is_object, is_string,
52
63
  } from "@yuneta/gobj-js";
53
64
 
65
+ import { cards_grid_descriptor } from "./nav_cards_helpers.js";
66
+ import {
67
+ BULMA_BP_ORDER,
68
+ breakpoints_from_expr,
69
+ bulma_hidden_class,
70
+ } from "./shell_show_on.js";
71
+
54
72
  /***************************************************************
55
73
  * Constants
56
74
  ***************************************************************/
57
75
  const GCLASS_NAME = "C_YUI_NAV";
58
76
 
59
77
  const SUPPORTED_LAYOUTS = [
60
- "vertical", "icon-bar", "tabs", "drawer", "submenu", "accordion"
78
+ "vertical", "icon-bar", "tabs", "drawer", "submenu", "accordion", "cards",
79
+ "backbar"
61
80
  ];
62
81
 
63
82
  /* Decorative items have no `route` and never participate in
@@ -82,6 +101,8 @@ SDATA(data_type_t.DTP_STRING, "layout", 0, "vertical", "vertical|ico
82
101
  SDATA(data_type_t.DTP_STRING, "icon_pos", 0, "left", "left|right|top|bottom"),
83
102
  SDATA(data_type_t.DTP_BOOLEAN, "show_label", 0, true, "Show item label next to/under the icon"),
84
103
  SDATA(data_type_t.DTP_STRING, "level", 0, "primary", "primary|secondary"),
104
+ SDATA(data_type_t.DTP_STRING, "show_on", 0, "", "Breakpoint visibility expr (shell show_on syntax); empty = always"),
105
+ SDATA(data_type_t.DTP_STRING, "back_route", 0, "", "backbar layout: route of the section index the ← returns to"),
85
106
 
86
107
  SDATA(data_type_t.DTP_POINTER, "shell", 0, null, "C_YUI_SHELL gobj (for navigation calls)"),
87
108
  SDATA(data_type_t.DTP_POINTER, "$container", 0, null, "Root HTMLElement of this nav"),
@@ -200,12 +221,29 @@ function build_ui(gobj)
200
221
  case "drawer": $container = render_drawer(gobj, items); break;
201
222
  case "submenu": $container = render_submenu(gobj, items); break;
202
223
  case "accordion": $container = render_accordion(gobj, items); break;
224
+ case "cards": $container = render_cards(gobj, items); break;
225
+ case "backbar": $container = render_backbar(gobj); break;
203
226
  default: $container = render_vertical(gobj, items);
204
227
  }
205
228
  $container.setAttribute("data-nav-zone", zone);
206
229
  $container.setAttribute("data-nav-layout", layout);
207
230
  $container.classList.add("C_YUI_NAV", "yui-nav", `yui-nav-${layout}`);
208
231
 
232
+ /* Breakpoint visibility (same expressions as the shell's zone
233
+ * show_on). Applied here, attr-driven, so a rebuild via
234
+ * EV_SET_ITEMS re-applies it — the shell sets it only once at
235
+ * creation time. */
236
+ let show_on = gobj_read_attr(gobj, "show_on") || "";
237
+ if(!empty_string(show_on) && show_on !== "*") {
238
+ let visible = breakpoints_from_expr(show_on);
239
+ for(let bp of BULMA_BP_ORDER) {
240
+ if(!visible[bp]) {
241
+ $container.classList.add(bulma_hidden_class(bp));
242
+ }
243
+ }
244
+ $container.setAttribute("data-show-on", show_on);
245
+ }
246
+
209
247
  /* Accessibility: every nav root is a landmark. For the drawer we
210
248
  * tag the wrapper as role=dialog and the panel as role=navigation. */
211
249
  if(layout !== "drawer") {
@@ -383,6 +421,50 @@ function render_tabs(gobj, items)
383
421
  );
384
422
  }
385
423
 
424
+ function render_cards(gobj, items)
425
+ {
426
+ let show_label = gobj_read_attr(gobj, "show_label");
427
+ return createElement2(cards_grid_descriptor(items, show_label));
428
+ }
429
+
430
+ /************************************************************
431
+ * "backbar": a single "← <section>" link back to the section
432
+ * index route. Static DOM — the active child view names itself
433
+ * in its own content, so the bar only carries the way back plus
434
+ * section context. While the index itself is on stage the shell
435
+ * collapses the whole secondary zone (tabs and cards never
436
+ * coexist), so the bar needs no self-hiding logic.
437
+ ************************************************************/
438
+ function render_backbar(gobj)
439
+ {
440
+ let back_route = gobj_read_attr(gobj, "back_route") || "";
441
+ let label = gobj_read_attr(gobj, "nav_label") || "";
442
+
443
+ let label_attrs = {class: "yui-nav-label"};
444
+ if(label) {
445
+ label_attrs.i18n = label;
446
+ }
447
+ let a_attrs = {
448
+ class: "yui-nav-item yui-nav-back",
449
+ href: back_route ? "#" + back_route : "#",
450
+ "data-item-id": "__index__",
451
+ "data-route": back_route,
452
+ "aria-label": label || "back"
453
+ };
454
+ if(label) {
455
+ a_attrs["data-i18n-aria-label"] = label;
456
+ }
457
+ return createElement2(
458
+ ["div", {},
459
+ ["a", a_attrs, [
460
+ ["span", {class: "icon is-small"},
461
+ ["i", {class: "yi-arrow-left", "aria-hidden": "true"}]],
462
+ ["span", label_attrs, label]
463
+ ]]
464
+ ]
465
+ );
466
+ }
467
+
386
468
  function render_drawer(gobj, items)
387
469
  {
388
470
  /* Off-canvas drawer: initially hidden; toggled via .is-active on the
@@ -757,6 +839,13 @@ function ac_route_changed(gobj, event, kw, src)
757
839
  return 0;
758
840
  }
759
841
 
842
+ /* backbar: no per-item highlight. Its visibility while the index
843
+ * itself is on stage is the shell's job (the whole secondary zone
844
+ * collapses at the index route — tabs and cards never coexist). */
845
+ if(gobj_read_attr(gobj, "layout") === "backbar") {
846
+ return 0;
847
+ }
848
+
760
849
  /* Remove prior active class on any children */
761
850
  let previouslyActive = $c.querySelectorAll(".is-active");
762
851
  previouslyActive.forEach(n => n.classList.remove("is-active"));
@@ -167,13 +167,21 @@
167
167
  }
168
168
  .yui-nav-iconbar .yui-nav-item.is-active,
169
169
  .yui-nav-iconbar li.is-active > .yui-nav-item {
170
- color: var(--bulma-link, #485fc7);
171
- background-color: var(--bulma-link-light, rgba(72,95,199,0.1));
170
+ background-color: var(--bulma-link, #485fc7);
171
+ color: var(--bulma-link-invert, #fff);
172
172
  }
173
173
 
174
174
  /*--- tabs (Bulma .tabs) ---*/
175
175
  .yui-nav-tabs .tabs ul { flex-wrap: wrap; }
176
176
 
177
+ /* Secondary-nav tab strip (top-sub zone): Bulma gives `.tabs:not(:last-child)`
178
+ * a 1.5rem block-spacing margin-bottom, which steals content height on every
179
+ * view (worst on mobile). The zone already has its own bottom border for
180
+ * separation, so drop the margin. Specificity (3 classes) beats Bulma's rule. */
181
+ .yui-zone-top-sub .yui-nav-tabs.tabs {
182
+ margin-bottom: 0.25rem;
183
+ }
184
+
177
185
  /*--- closable tab: trailing ✕ affordance (item.closable) ---*/
178
186
  .yui-nav-close {
179
187
  opacity: 0.55;
@@ -189,6 +197,64 @@
189
197
  /*--- per-item state class (item.class) — generic disconnected tint ---*/
190
198
  .yui-nav-tabs .tabs li.yui-nav-disconnected a { color: var(--bulma-danger, #f14668); }
191
199
 
200
+ /*--- cards (section-index landing grid, mounted as a stage view) ---*/
201
+ .yui-nav-cards {
202
+ display: grid;
203
+ grid-template-columns: repeat(auto-fill, minmax(9rem, 1fr));
204
+ gap: 0.75rem;
205
+ padding: 1rem;
206
+ width: 100%;
207
+ align-content: start;
208
+ }
209
+ .yui-nav-cards .yui-nav-card {
210
+ display: flex;
211
+ flex-direction: column;
212
+ align-items: center;
213
+ justify-content: center;
214
+ gap: 0.5rem;
215
+ padding: 1.25rem 0.75rem;
216
+ border: 1px solid var(--bulma-border-weak, #dbdbdb);
217
+ border-radius: 0.5rem;
218
+ background: var(--bulma-scheme-main, #fff);
219
+ color: var(--bulma-text, inherit);
220
+ text-decoration: none;
221
+ text-align: center;
222
+ line-height: 1.2;
223
+ word-break: break-word;
224
+ }
225
+ /* yui_icons are 1em CSS masks: font-size scales them. */
226
+ .yui-nav-cards .yui-nav-card .icon {
227
+ font-size: 1.5rem;
228
+ }
229
+ .yui-nav-cards .yui-nav-card:hover,
230
+ .yui-nav-cards .yui-nav-card:focus-visible {
231
+ border-color: var(--bulma-link, #485fc7);
232
+ color: var(--bulma-link, #485fc7);
233
+ }
234
+ .yui-nav-cards .yui-nav-card.is-active {
235
+ border-color: var(--bulma-link, #485fc7);
236
+ }
237
+ .yui-nav-cards .yui-nav-card[data-disabled="1"] {
238
+ opacity: 0.5;
239
+ }
240
+
241
+ /*--- backbar (mobile "← <section>" back-to-index, submenu.index) ---*/
242
+ .yui-nav-backbar {
243
+ display: flex;
244
+ align-items: center;
245
+ padding: 0.25rem 0.5rem;
246
+ }
247
+ .yui-nav-backbar .yui-nav-back {
248
+ display: inline-flex;
249
+ align-items: center;
250
+ gap: 0.4rem;
251
+ padding: 0.25rem 0.5rem;
252
+ border-radius: 0.375rem;
253
+ color: var(--bulma-text, inherit);
254
+ text-decoration: none;
255
+ font-weight: 600;
256
+ }
257
+
192
258
  /*--- stacked item (icon above label) ---*/
193
259
  .yui-nav-stacked {
194
260
  display: inline-flex;
@@ -16,6 +16,11 @@
16
16
  * $container in its stage, hide the previous one. Lifecycle per item
17
17
  * decides when the gobj is created/destroyed.
18
18
  *
19
+ * A primary item with `submenu.index` gets a section-index landing:
20
+ * its own route mounts the submenu as a "cards" C_YUI_NAV in the
21
+ * stage instead of redirecting to the default child (see
22
+ * shell_section_index.js for the contract).
23
+ *
19
24
  * Hash-based 2-level routing (#/primary/secondary). No dependency on
20
25
  * C_YUI_ROUTING.
21
26
  *
@@ -53,6 +58,10 @@ import {
53
58
  } from "./shell_toolbar_helpers.js";
54
59
 
55
60
  import { resolve_route } from "./route_resolver.js";
61
+ import {
62
+ section_index_target,
63
+ secondary_nav_renders,
64
+ } from "./shell_section_index.js";
56
65
 
57
66
  /***************************************************************
58
67
  * Constants
@@ -621,18 +630,28 @@ function build_item_index(gobj, config)
621
630
  let items = (menu && menu.items) || [];
622
631
  for(let item of items) {
623
632
  if(item.route) {
633
+ /* Section-index landing (submenu.index): the section
634
+ * route gets a synthesized target — the submenu itself
635
+ * rendered as a "cards" C_YUI_NAV in the stage — so it
636
+ * becomes a real resting, deep-linkable route instead
637
+ * of redirecting to the default child (navigate_to()
638
+ * only redirects while the entry has NO target).
639
+ * Explicit inline targets keep precedence (the helper
640
+ * returns null then). */
641
+ let target = item.target || section_index_target(menu_id, item);
642
+
624
643
  /* A later menu must NOT clobber an earlier entry that
625
644
  * has a valid target with one that has none. This is
626
645
  * the common case where a `quick` drawer just reuses
627
646
  * routes declared (with target) in `primary.submenu`.
628
647
  * Rule: prefer the first entry with a target. */
629
648
  let prev = priv.item_index[item.route];
630
- if(!prev || (!prev.target && item.target)) {
649
+ if(!prev || (!prev.target && target)) {
631
650
  priv.item_index[item.route] = {
632
651
  item: item,
633
652
  parent_item: null,
634
- stage: item.target && item.target.stage || null,
635
- target: item.target || null,
653
+ stage: target && target.stage || null,
654
+ target: target,
636
655
  menu_id: menu_id
637
656
  };
638
657
  }
@@ -761,19 +780,27 @@ function instantiate_menus(gobj, config)
761
780
  );
762
781
  continue;
763
782
  }
764
- let submenu_def = {
765
- items: sub.items,
766
- render: { [zone_id]: render_to_obj(layout) }
767
- };
783
+ /* Index sections (submenu.index) get a render PLAN:
784
+ * the declared nav constrained to ">=tablet" plus a
785
+ * mobile "backbar" (← <section>) — tabs and cards
786
+ * never coexist, and a child view on mobile offers
787
+ * the way back instead of a duplicated tab strip. */
788
+ let renders = secondary_nav_renders(item, render_to_obj(layout));
768
789
  let sub_menu_id = `secondary.${menu_id}.${item.id}`;
769
- let nav = instantiate_nav_in_zone(
770
- gobj, submenu_def, sub_menu_id, zone_id, "secondary",
771
- item.name || ""
772
- );
773
- /* Hidden until owning primary is active. */
774
- let $c = gobj_read_attr(nav, "$container");
775
- if($c) {
776
- $c.classList.add("is-hidden");
790
+ for(let render_cfg of renders) {
791
+ let submenu_def = {
792
+ items: sub.items,
793
+ render: { [zone_id]: render_cfg }
794
+ };
795
+ let nav = instantiate_nav_in_zone(
796
+ gobj, submenu_def, sub_menu_id, zone_id, "secondary",
797
+ item.name || ""
798
+ );
799
+ /* Hidden until owning primary is active. */
800
+ let $c = gobj_read_attr(nav, "$container");
801
+ if($c) {
802
+ $c.classList.add("is-hidden");
803
+ }
777
804
  }
778
805
  }
779
806
  }
@@ -801,8 +828,11 @@ function instantiate_nav_in_zone(gobj, menu, menu_id, zone_id, level, nav_label)
801
828
  render_cfg = { layout: render_cfg };
802
829
  }
803
830
 
804
- let nav_name = `nav_${menu_id.replace(/\./g, "_")}_${zone_id}`;
805
831
  let layout = render_cfg.layout || "vertical";
832
+ /* A backbar shares menu_id and zone with the nav it complements —
833
+ * suffix the gobj name to avoid a sibling-name collision. */
834
+ let nav_name = `nav_${menu_id.replace(/\./g, "_")}_${zone_id}` +
835
+ (layout === "backbar" ? "_backbar" : "");
806
836
  let nav = gobj_create(
807
837
  nav_name,
808
838
  "C_YUI_NAV",
@@ -815,6 +845,8 @@ function instantiate_nav_in_zone(gobj, menu, menu_id, zone_id, level, nav_label)
815
845
  icon_pos: render_cfg.icon_pos || default_icon_pos(zone_id),
816
846
  show_label: render_cfg.show_label !== false,
817
847
  level: level,
848
+ show_on: render_cfg.show_on || "",
849
+ back_route: render_cfg.back_route || "",
818
850
  shell: gobj
819
851
  },
820
852
  gobj
@@ -1174,6 +1206,16 @@ function build_view_gobj(gobj, entry, route, stage)
1174
1206
  }
1175
1207
  stage.el.appendChild($view);
1176
1208
  gobj_start(view);
1209
+
1210
+ /* App view gclasses translate their own DOM; a section-index view
1211
+ * (synthesized "cards" C_YUI_NAV) is SHELL-owned DOM built after
1212
+ * the host's one-shot refresh_language — apply the registered
1213
+ * translator, same policy as lazily-built dropdown panels. */
1214
+ let priv = gobj_read_attr(gobj, "priv");
1215
+ if(priv && typeof priv.translator === "function" &&
1216
+ target.gclass === "C_YUI_NAV") {
1217
+ refresh_language($view, priv.translator);
1218
+ }
1177
1219
  return view;
1178
1220
  }
1179
1221
 
@@ -2002,7 +2044,19 @@ function update_secondary_nav_visibility(gobj, entry)
2002
2044
  * secondary navs and a nav-derived check would never collapse
2003
2045
  * the zone (empty white strip under the toolbar). */
2004
2046
  let active_primary = entry.parent_item || entry.item || null;
2005
- let has_secondary = !!(
2047
+
2048
+ /* At the section-index route itself (a level-1 item with
2049
+ * submenu.index) the cards ARE the navigation: showing the tab
2050
+ * strip too would duplicate it (confusing — either tabs or
2051
+ * cards, never both). Collapse the secondary zone there. */
2052
+ let at_section_index = !!(
2053
+ !entry.parent_item &&
2054
+ active_primary &&
2055
+ active_primary.submenu &&
2056
+ active_primary.submenu.index
2057
+ );
2058
+
2059
+ let has_secondary = !at_section_index && !!(
2006
2060
  active_primary &&
2007
2061
  active_primary.submenu &&
2008
2062
  Array.isArray(active_primary.submenu.items) &&
@@ -2037,7 +2091,7 @@ function update_secondary_nav_visibility(gobj, entry)
2037
2091
  if(!$c) {
2038
2092
  continue;
2039
2093
  }
2040
- if(nav_menu_id === target_secondary_id) {
2094
+ if(nav_menu_id === target_secondary_id && has_secondary) {
2041
2095
  $c.classList.remove("is-hidden");
2042
2096
  } else {
2043
2097
  $c.classList.add("is-hidden");
@@ -2130,6 +2184,11 @@ function find_secondary_nav(priv, parent_item_id)
2130
2184
  for(let nav of priv.navs) {
2131
2185
  let menu_id = gobj_read_attr(nav, "menu_id") || "";
2132
2186
  if(menu_id.startsWith("secondary.") && menu_id.endsWith(suffix)) {
2187
+ /* A backbar shares the menu_id but renders no items —
2188
+ * EV_SET_ITEMS must reach the item-based nav. */
2189
+ if(gobj_read_attr(nav, "layout") === "backbar") {
2190
+ continue;
2191
+ }
2133
2192
  return nav;
2134
2193
  }
2135
2194
  }
@@ -2237,6 +2296,32 @@ function yui_shell_set_submenu(shell_gobj, parent_item_id, items)
2237
2296
 
2238
2297
  /* Push the new items into the nav (rebuilds its DOM in place). */
2239
2298
  gobj_send_event(nav, "EV_SET_ITEMS", {items: items}, shell_gobj);
2299
+
2300
+ /* Section-index landing (submenu.index): keep the synthesized
2301
+ * target and any mounted index view (a "cards" C_YUI_NAV in the
2302
+ * stage) in sync with the new items — otherwise a later mount,
2303
+ * or the already mounted view, would render the stale set. */
2304
+ if(parent_item && parent_item.route &&
2305
+ parent_item.submenu && parent_item.submenu.index) {
2306
+ let entry = priv.item_index[parent_item.route];
2307
+ if(entry && entry.target &&
2308
+ entry.target.gclass === "C_YUI_NAV" && entry.target.kw) {
2309
+ entry.target.kw.menu_items = items;
2310
+ }
2311
+ for(let stage_name in priv.stages) {
2312
+ let stage = priv.stages[stage_name];
2313
+ let view = stage.items && stage.items[parent_item.route];
2314
+ if(view) {
2315
+ gobj_send_event(view, "EV_SET_ITEMS", {items: items}, shell_gobj);
2316
+ /* EV_SET_ITEMS rebuilt the DOM: re-apply the translator
2317
+ * (shell-owned DOM, same policy as build_view_gobj). */
2318
+ let $view = gobj_read_attr(view, "$container");
2319
+ if($view && typeof priv.translator === "function") {
2320
+ refresh_language($view, priv.translator);
2321
+ }
2322
+ }
2323
+ }
2324
+ }
2240
2325
  return 0;
2241
2326
  }
2242
2327