@yuneta/gobj-ui 2.6.0 → 4.0.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.
Files changed (56) hide show
  1. package/README.md +331 -11
  2. package/dist/gobj-ui.cjs.js +21386 -17919
  3. package/dist/gobj-ui.es.js +26059 -22613
  4. package/index.js +45 -9
  5. package/package.json +4 -4
  6. package/src/c_g6_nodes_tree.js +128 -40
  7. package/src/c_yui_form.css +9 -0
  8. package/src/c_yui_form.js +113 -53
  9. package/src/c_yui_gobj_tree_js.js +58 -36
  10. package/src/c_yui_json.css +139 -0
  11. package/src/c_yui_json.js +928 -0
  12. package/src/c_yui_json_graph.js +110 -20
  13. package/src/c_yui_map.js +18 -36
  14. package/src/c_yui_nav.js +6 -9
  15. package/src/c_yui_period.css +184 -0
  16. package/src/c_yui_period.js +1441 -0
  17. package/src/c_yui_shell.css +120 -0
  18. package/src/c_yui_shell.js +672 -111
  19. package/src/c_yui_treedb_graph.js +639 -37
  20. package/src/c_yui_treedb_schema.js +478 -0
  21. package/src/c_yui_treedb_topic_with_form.css +18 -0
  22. package/src/c_yui_treedb_topic_with_form.js +153 -103
  23. package/src/c_yui_treedb_topics.css +73 -0
  24. package/src/c_yui_treedb_topics.js +1070 -29
  25. package/src/c_yui_window.css +22 -0
  26. package/src/c_yui_window.js +182 -97
  27. package/src/c_yui_window_manager.js +38 -4
  28. package/src/json_view_helpers.js +214 -0
  29. package/src/json_view_helpers.test.js +135 -0
  30. package/src/route_map_model.js +317 -0
  31. package/src/route_map_model.test.js +209 -0
  32. package/src/route_resolver.js +24 -1
  33. package/src/route_resolver.test.js +40 -1
  34. package/src/shell_modals.js +162 -52
  35. package/src/shell_route_map.css +225 -0
  36. package/src/shell_route_map.js +363 -0
  37. package/src/tabulator.css +245 -0
  38. package/src/yui_dev.js +130 -8
  39. package/src/yui_frontend_view.js +106 -0
  40. package/src/yui_icons.css +62 -0
  41. package/src/yui_inputs.css +8 -3
  42. package/src/yui_inputs.js +51 -8
  43. package/src/yui_tabulator_i18n.js +128 -0
  44. package/src/yui_theme.js +147 -0
  45. package/src/yui_time.js +666 -0
  46. package/src/yui_time.test.js +330 -0
  47. package/src/yui_toolbar.css +28 -0
  48. package/src/yui_toolbar.js +86 -43
  49. package/src/c_yui_main.css +0 -501
  50. package/src/c_yui_main.js +0 -1623
  51. package/src/c_yui_routing.css +0 -18
  52. package/src/c_yui_routing.js +0 -837
  53. package/src/c_yui_tabs.js +0 -487
  54. package/src/themes.js +0 -215
  55. package/src/ytable.css +0 -63
  56. package/src/ytable.js +0 -505
@@ -59,6 +59,7 @@ import {
59
59
  } from '@antv/g6';
60
60
 
61
61
  import { ensure_drag_canvas_patch } from "./g6_drag_canvas_touch.js";
62
+ import { yui_is_dark, yui_theme_now, yui_watch_theme } from "./yui_theme.js";
62
63
 
63
64
  /***************************************************************
64
65
  * Constants
@@ -91,6 +92,7 @@ let PRIVATE_DATA = {
91
92
  graph: null,
92
93
  node_counter: 0,
93
94
  theme: null,
95
+ theme_observer: null, // MutationObserver on <html data-theme>
94
96
  resize_observer: null, // ResizeObserver on the canvas mount element
95
97
  _resize_raf: 0, // rAF id debouncing resize -> EV_RESIZE
96
98
  };
@@ -111,11 +113,55 @@ const TYPE_COLORS = {
111
113
  dict: "#4C0099", // purple
112
114
  };
113
115
 
116
+ /***************************************************************
117
+ * Group (card) colours. `fill` is the light-theme card; `tint`
118
+ * is the hue that fill hints at, used to derive the dark card —
119
+ * dict and list share a stroke, so the tint is what keeps them
120
+ * apart at a glance (with the dashed/solid border).
121
+ ***************************************************************/
114
122
  const GROUP_COLORS = {
115
- dict: {fill: "#FBFBFB", stroke: "#006658"},
116
- list: {fill: "#fffbd1", stroke: "#006658"},
123
+ dict: {fill: "#FBFBFB", tint: "#006658", stroke: "#006658"},
124
+ list: {fill: "#fffbd1", tint: "#ffd54a", stroke: "#006658"},
117
125
  };
118
126
 
127
+ /***************************************************************
128
+ * Soft, theme-aware card palette, same visual language as the
129
+ * gobj-tree's role_card_style(): tinted fill + group-colour
130
+ * border, lifted off the near-black canvas on dark (a faint tint
131
+ * on the canvas colour makes the cards invisible).
132
+ *
133
+ * The header bar is always the border colour, so its text flips:
134
+ * white on the dark-teal light bar, near-black on the brightened
135
+ * dark one.
136
+ ***************************************************************/
137
+ function json_card_style(group, dark)
138
+ {
139
+ return {
140
+ bg: dark
141
+ ? `color-mix(in srgb, ${group.tint} 30%, #2c3542)`
142
+ : group.fill,
143
+ border: dark
144
+ ? `color-mix(in srgb, ${group.stroke} 85%, #ffffff)`
145
+ : group.stroke,
146
+ header_fg: dark ? "#12181f" : "#ffffff",
147
+ key: dark ? "#c9cfd8" : "#1A1A1A",
148
+ };
149
+ }
150
+
151
+ /***************************************************************
152
+ * A scalar's colour by type. The palette is tuned for a light
153
+ * card; on a dark one every one of them (dark green #006000,
154
+ * blue #475ED0, …) sinks into the background, so brighten it.
155
+ ***************************************************************/
156
+ function type_color(type, dark)
157
+ {
158
+ let color = TYPE_COLORS[type] || (dark ? "#e8eaed" : "black");
159
+ if(!dark) {
160
+ return color;
161
+ }
162
+ return `color-mix(in srgb, ${color} 55%, #ffffff)`;
163
+ }
164
+
119
165
 
120
166
 
121
167
 
@@ -146,10 +192,14 @@ function mt_create(gobj)
146
192
  }
147
193
  gobj_subscribe_event(gobj, null, {}, subscriber);
148
194
 
149
- let __yui_main__ = gobj_find_service("__yui_main__", true);
150
- if(__yui_main__) {
151
- priv.theme = gobj_read_str_attr(__yui_main__, "theme");
152
- }
195
+ /* Follow the app theme. This used to read a legacy C_YUI_MAIN
196
+ * "__yui_main__" service's `theme` attr — and looked it up with
197
+ * verbose=true, so under C_YUI_SHELL (which has no such service) it
198
+ * logged "gobj service not found: __yui_main__" on every mount. It
199
+ * never watched either, so the theme was read once. Watch
200
+ * <html data-theme> and restyle in ac_theme. */
201
+ priv.theme = yui_theme_now();
202
+ priv.theme_observer = yui_watch_theme(gobj);
153
203
 
154
204
  build_ui(gobj);
155
205
 
@@ -184,6 +234,10 @@ function mt_destroy(gobj)
184
234
  {
185
235
  let priv = gobj.priv;
186
236
 
237
+ if(priv.theme_observer) {
238
+ priv.theme_observer.disconnect();
239
+ priv.theme_observer = null;
240
+ }
187
241
  if(priv.resize_observer) {
188
242
  priv.resize_observer.disconnect();
189
243
  priv.resize_observer = null;
@@ -385,9 +439,10 @@ function build_graph(gobj)
385
439
  /************************************************************
386
440
  * Build cell value HTML (matching old mx_json_viewer colors)
387
441
  ************************************************************/
388
- function build_cell_html(key, value, type)
442
+ function build_cell_html(key, value, type, dark)
389
443
  {
390
- let color = TYPE_COLORS[type] || "black";
444
+ let color = type_color(type, dark);
445
+ let key_color = json_card_style(GROUP_COLORS.dict, dark).key;
391
446
  let display_value = "";
392
447
 
393
448
  switch(type) {
@@ -414,7 +469,7 @@ function build_cell_html(key, value, type)
414
469
  break;
415
470
  }
416
471
 
417
- return `<span style="color:#1A1A1A">• ${escapeHtml(String(key))}: </span><span style="color:${color}">${display_value}</span>`;
472
+ return `<span style="color:${key_color}">• ${escapeHtml(String(key))}: </span><span style="color:${color}">${display_value}</span>`;
418
473
  }
419
474
 
420
475
  /************************************************************
@@ -481,8 +536,11 @@ function build_json_nodes(gobj, path, kw, nodes, edges, parent_id)
481
536
  }
482
537
 
483
538
  /*
484
- * Build HTML content for this group
539
+ * Build HTML content for this group.
540
+ * The theme is read live as the card is drawn (like the gobj-tree's
541
+ * cards): ac_theme rebuilds on a switch, so this is always current.
485
542
  */
543
+ let dark = yui_is_dark();
486
544
  let lines = [];
487
545
  let pending_complex = [];
488
546
 
@@ -490,7 +548,7 @@ function build_json_nodes(gobj, path, kw, nodes, edges, parent_id)
490
548
 
491
549
  for(let [key, value] of entries) {
492
550
  let type = get_json_type(value);
493
- let html = build_cell_html(key, value, type);
551
+ let html = build_cell_html(key, value, type, dark);
494
552
  lines.push(html);
495
553
 
496
554
  if((is_object(value) && json_object_size(value) > 0) ||
@@ -500,7 +558,9 @@ function build_json_nodes(gobj, path, kw, nodes, edges, parent_id)
500
558
  }
501
559
 
502
560
  let label = get_group_label(path) || (is_dict ? "{}" : "[]");
503
- let colors = is_dict ? GROUP_COLORS.dict : GROUP_COLORS.list;
561
+ let colors = json_card_style(
562
+ is_dict ? GROUP_COLORS.dict : GROUP_COLORS.list, dark
563
+ );
504
564
 
505
565
  /*
506
566
  * Build node HTML content
@@ -514,15 +574,15 @@ function build_json_nodes(gobj, path, kw, nodes, edges, parent_id)
514
574
  let node_html = `
515
575
  <div style="
516
576
  min-width: ${min_width}px;
517
- background: ${colors.fill};
518
- border: 1px ${is_dict ? 'dashed' : 'solid'} ${colors.stroke};
577
+ background: ${colors.bg};
578
+ border: 1px ${is_dict ? 'dashed' : 'solid'} ${colors.border};
519
579
  border-radius: ${is_dict ? '6px' : '0'};
520
580
  opacity: 0.9;
521
581
  overflow: hidden;
522
582
  ">
523
583
  <div style="
524
- background: ${colors.stroke};
525
- color: white;
584
+ background: ${colors.border};
585
+ color: ${colors.header_fg};
526
586
  font-weight: bold;
527
587
  font-size: 12px;
528
588
  padding: 3px 8px;
@@ -555,7 +615,7 @@ function build_json_nodes(gobj, path, kw, nodes, edges, parent_id)
555
615
  source: parent_id,
556
616
  target: group_id,
557
617
  style: {
558
- stroke: colors.stroke,
618
+ stroke: colors.border,
559
619
  lineWidth: 1,
560
620
  }
561
621
  });
@@ -608,23 +668,34 @@ function load_json(gobj)
608
668
 
609
669
  build_json_nodes(gobj, path, json, nodes, edges, null);
610
670
 
671
+ let preserve_view = !!priv.pending_preserve_view;
672
+ priv.pending_preserve_view = false;
673
+
611
674
  if(nodes.length > 0) {
612
675
  graph.setData({nodes: nodes, edges: edges});
613
676
  graph.render().then(() => {
614
- graph.fitView();
677
+ if(!preserve_view) {
678
+ graph.fitView();
679
+ }
615
680
  });
616
681
  }
617
682
  }
618
683
 
619
684
  /************************************************************
620
- * Clear and reload
685
+ * Clear and reload.
686
+ * {preserve_view: true}
687
+ * keep the current zoom and translate (no fitView) —
688
+ * a restyle (theme change) must not move the camera.
689
+ * Default: fit the whole graph to the viewport.
621
690
  ************************************************************/
622
- function refresh_json(gobj)
691
+ function refresh_json(gobj, opts)
623
692
  {
624
693
  let priv = gobj.priv;
625
694
  let graph = priv.graph;
695
+ opts = opts || {};
626
696
 
627
697
  if(graph) {
698
+ priv.pending_preserve_view = !!opts.preserve_view;
628
699
  graph.clear();
629
700
  load_json(gobj);
630
701
  }
@@ -789,6 +860,23 @@ function ac_refresh(gobj, event, kw, src)
789
860
  return 0;
790
861
  }
791
862
 
863
+ /************************************************************
864
+ * {theme: "dark"|"light"} — the app switched theme.
865
+ ************************************************************/
866
+ function ac_theme(gobj, event, kw, src)
867
+ {
868
+ let priv = gobj.priv;
869
+
870
+ priv.theme = kw.theme || yui_theme_now();
871
+ if(priv.graph) {
872
+ priv.graph.setTheme(priv.theme);
873
+ }
874
+ /* A restyle, not new data: keep the user's zoom/pan. */
875
+ refresh_json(gobj, {preserve_view: true});
876
+
877
+ return 0;
878
+ }
879
+
792
880
  /************************************************************
793
881
  *
794
882
  ************************************************************/
@@ -965,6 +1053,7 @@ function create_gclass(gclass_name)
965
1053
  ["ST_IDLE", [
966
1054
  ["EV_LOAD_DATA", ac_load_data, null],
967
1055
  ["EV_REFRESH", ac_refresh, null],
1056
+ ["EV_THEME", ac_theme, null],
968
1057
  ["EV_ZOOM_IN", ac_zoom_in, null],
969
1058
  ["EV_ZOOM_OUT", ac_zoom_out, null],
970
1059
  ["EV_ZOOM_RESET", ac_zoom_reset, null],
@@ -982,6 +1071,7 @@ function create_gclass(gclass_name)
982
1071
  const event_types = [
983
1072
  ["EV_LOAD_DATA", 0],
984
1073
  ["EV_REFRESH", 0],
1074
+ ["EV_THEME", 0],
985
1075
  ["EV_ZOOM_IN", 0],
986
1076
  ["EV_ZOOM_OUT", 0],
987
1077
  ["EV_ZOOM_RESET", 0],
package/src/c_yui_map.js CHANGED
@@ -27,6 +27,7 @@ import {
27
27
  gobj_read_bool_attr,
28
28
  gobj_find_service,
29
29
  gobj_create_service,
30
+ gobj_start,
30
31
  gobj_name,
31
32
  } from "@yuneta/gobj-js";
32
33
 
@@ -205,24 +206,6 @@ function mt_create(gobj)
205
206
  }
206
207
  }
207
208
 
208
- /***************************************************************
209
- * Framework Method: Start
210
- ***************************************************************/
211
- function mt_start(gobj)
212
- {
213
- let __yui_main__ = gobj_find_service("__yui_main__", true);
214
- if(__yui_main__) {
215
- gobj_subscribe_event(__yui_main__, "EV_RESIZE", {}, gobj);
216
- }
217
- }
218
-
219
- /***************************************************************
220
- * Framework Method: Stop
221
- ***************************************************************/
222
- function mt_stop(gobj)
223
- {
224
- }
225
-
226
209
  /***************************************************************
227
210
  * Framework Method: Destroy
228
211
  ***************************************************************/
@@ -590,10 +573,25 @@ function onClick(gobj, e)
590
573
  center: true,
591
574
  showMax: false,
592
575
  content_size: true,
576
+ /* The marker's own name: this window is one marker's
577
+ * detail, and several can be open at once, so the bar
578
+ * must say WHICH. It is DATA, so it travels in
579
+ * title_prefix (never translated) — in `title` it
580
+ * would get a data-i18n and a marker named like a
581
+ * locale key ("status") would render translated. */
582
+ title_prefix: gobj_name(gobj_service),
583
+ title: "",
584
+ icon: "yi-location-dot",
585
+ logical_class: "MAP_MARKER_WINDOW",
593
586
  body: popupContent
594
587
  },
595
588
  gobj
596
589
  );
590
+ if(!gobj_window) {
591
+ log_error(`${gobj_name(gobj)}: cannot create the marker window`);
592
+ return;
593
+ }
594
+ gobj_start(gobj_window);
597
595
 
598
596
  } else {
599
597
  // Initialize a popup
@@ -823,18 +821,6 @@ function ac_hide(gobj, event, kw, src)
823
821
  return 0;
824
822
  }
825
823
 
826
- /************************************************************
827
- *
828
- ************************************************************/
829
- function ac_resize(gobj, event, kw, src)
830
- {
831
- if(!gobj_read_bool_attr(gobj, "dimensions_with_parent_observer")) {
832
- // TODO
833
- }
834
-
835
- return 0;
836
- }
837
-
838
824
 
839
825
 
840
826
 
@@ -850,8 +836,6 @@ function ac_resize(gobj, event, kw, src)
850
836
  *---------------------------------------------*/
851
837
  const gmt = {
852
838
  mt_create: mt_create,
853
- mt_start: mt_start,
854
- mt_stop: mt_stop,
855
839
  mt_destroy: mt_destroy
856
840
  };
857
841
 
@@ -876,8 +860,7 @@ function create_gclass(gclass_name)
876
860
  ["EV_REFRESH", ac_refresh, null],
877
861
  ["EV_SELECT", ac_select, null],
878
862
  ["EV_SHOW", ac_show, null],
879
- ["EV_HIDE", ac_hide, null],
880
- ["EV_RESIZE", ac_resize, null]
863
+ ["EV_HIDE", ac_hide, null]
881
864
  ]]
882
865
  ];
883
866
 
@@ -891,8 +874,7 @@ function create_gclass(gclass_name)
891
874
  ["EV_REFRESH", 0],
892
875
  ["EV_SELECT", 0],
893
876
  ["EV_SHOW", 0],
894
- ["EV_HIDE", 0],
895
- ["EV_RESIZE", 0]
877
+ ["EV_HIDE", 0]
896
878
  ];
897
879
 
898
880
  __gclass__ = gclass_create(
package/src/c_yui_nav.js CHANGED
@@ -107,14 +107,15 @@ SDATA(data_type_t.DTP_STRING, "back_route", 0, "", "backbar layo
107
107
  SDATA(data_type_t.DTP_POINTER, "shell", 0, null, "C_YUI_SHELL gobj (for navigation calls)"),
108
108
  SDATA(data_type_t.DTP_POINTER, "$container", 0, null, "Root HTMLElement of this nav"),
109
109
  SDATA(data_type_t.DTP_STRING, "active_route", 0, "", "Currently active route"),
110
- SDATA(data_type_t.DTP_POINTER, "priv", 0, null, "Private runtime state (click listener, etc.)"),
111
110
  SDATA_END()
112
111
  ];
113
112
 
114
113
  /* Monotonic id generator for ARIA pairs (aria-controls etc.). */
115
114
  let __nav_aria_seq__ = 0;
116
115
 
117
- let PRIVATE_DATA = {};
116
+ let PRIVATE_DATA = {
117
+ click_handler: null
118
+ };
118
119
  let __gclass__ = null;
119
120
 
120
121
 
@@ -141,9 +142,6 @@ function mt_create(gobj)
141
142
  }
142
143
  gobj_subscribe_event(gobj, null, {}, subscriber);
143
144
 
144
- gobj_write_attr(gobj, "priv", {
145
- click_handler: null
146
- });
147
145
  build_ui(gobj);
148
146
  }
149
147
 
@@ -177,7 +175,7 @@ function mt_stop(gobj)
177
175
  function mt_destroy(gobj)
178
176
  {
179
177
  let $c = gobj_read_attr(gobj, "$container");
180
- let priv = gobj_read_attr(gobj, "priv");
178
+ let priv = gobj.priv;
181
179
  if($c && priv && priv.click_handler) {
182
180
  $c.removeEventListener("click", priv.click_handler);
183
181
  }
@@ -185,7 +183,6 @@ function mt_destroy(gobj)
185
183
  $c.parentNode.removeChild($c);
186
184
  }
187
185
  gobj_write_attr(gobj, "$container", null);
188
- gobj_write_attr(gobj, "priv", null);
189
186
  }
190
187
 
191
188
 
@@ -267,7 +264,7 @@ function build_ui(gobj)
267
264
  function rebuild(gobj)
268
265
  {
269
266
  let $old = gobj_read_attr(gobj, "$container");
270
- let priv = gobj_read_attr(gobj, "priv") || {};
267
+ let priv = gobj.priv;
271
268
  let parent = $old ? $old.parentNode : null;
272
269
  let next = $old ? $old.nextSibling : null;
273
270
  let was_hidden = !!($old && $old.classList.contains("is-hidden"));
@@ -718,7 +715,7 @@ function item_iconbar(gobj, it, opts)
718
715
 
719
716
  function wire_clicks(gobj, $container)
720
717
  {
721
- let priv = gobj_read_attr(gobj, "priv") || {};
718
+ let priv = gobj.priv;
722
719
 
723
720
  let handler = ev => {
724
721
  let target = ev.target;
@@ -0,0 +1,184 @@
1
+ /*
2
+ * c_yui_period.css
3
+ * The date navigator (C_YUI_PERIOD).
4
+ *
5
+ * Imported by c_yui_period.js, so it rides the component: a consumer that
6
+ * imports the gclass gets its styles, and one that does not, does not ship
7
+ * them.
8
+ */
9
+
10
+ .YUI_PERIOD {
11
+ max-width: 100%;
12
+ }
13
+
14
+ /*
15
+ * The granularities are a SEGMENTED control: one line of joined pills.
16
+ * When they do not fit — a phone, or an app that declared ten of them —
17
+ * the strip SCROLLS sideways. Wrapping would break the joined look mid-row,
18
+ * and letting it overflow would push the last modes out of the card (it did:
19
+ * "Custom" was cut off by the dialog's edge, unreachable).
20
+ */
21
+ .YUI_PERIOD_MODES_ROW {
22
+ max-width: 100%;
23
+ }
24
+
25
+ /* Two classes deep, again: Bulma's `.buttons` sets `justify-content:
26
+ * flex-start` and ships after this file, so a single-class rule of equal
27
+ * specificity loses — the strip stayed pinned to the left of the arrows it
28
+ * belongs to. When it does not fit it scrolls, and then it fills the row
29
+ * anyway. */
30
+ /* `safe center`, not plain `center`: a CENTRED flex row that overflows spills
31
+ * out of BOTH ends, and `scrollLeft` cannot go negative — so on a phone the
32
+ * first granularities were painted off the left edge and NO swipe could reach
33
+ * them (at 360px the browser exposed 28px of the 57px of overflow, and "All"
34
+ * sat at -28px). The `safe` keyword keeps the strip centred while it fits and
35
+ * falls back to start-alignment the moment it does not. */
36
+ .YUI_PERIOD .YUI_PERIOD_MODES {
37
+ flex: 1 1 auto;
38
+ min-width: 0;
39
+ overflow-x: auto;
40
+ scrollbar-width: thin;
41
+ justify-content: safe center;
42
+ }
43
+
44
+ .YUI_PERIOD_MODES::-webkit-scrollbar {
45
+ height: 4px;
46
+ }
47
+
48
+ /*
49
+ * The "there is more" hint: a 4px scrollbar is invisible to a thumb, so the
50
+ * edge that hides content fades out instead. The classes are toggled from
51
+ * update_modes_fade() (scroll + ResizeObserver); a strip that fits gets none.
52
+ */
53
+ .YUI_PERIOD_MODES.yui-period-fade-right {
54
+ -webkit-mask-image: linear-gradient(to right, #000 calc(100% - 2rem), transparent);
55
+ mask-image: linear-gradient(to right, #000 calc(100% - 2rem), transparent);
56
+ }
57
+
58
+ .YUI_PERIOD_MODES.yui-period-fade-left {
59
+ -webkit-mask-image: linear-gradient(to right, transparent, #000 2rem);
60
+ mask-image: linear-gradient(to right, transparent, #000 2rem);
61
+ }
62
+
63
+ .YUI_PERIOD_MODES.yui-period-fade-left.yui-period-fade-right {
64
+ -webkit-mask-image: linear-gradient(to right,
65
+ transparent, #000 2rem, #000 calc(100% - 2rem), transparent);
66
+ mask-image: linear-gradient(to right,
67
+ transparent, #000 2rem, #000 calc(100% - 2rem), transparent);
68
+ }
69
+
70
+ /* Bulma's .buttons gives every child a margin-bottom, for the rows it
71
+ * expects to wrap. This one never wraps, so that margin is just a gap. */
72
+ .YUI_PERIOD_MODES .button {
73
+ margin-bottom: 0;
74
+ white-space: nowrap;
75
+ }
76
+
77
+ /*
78
+ * The overflow menu lives OUTSIDE the scrolling strip on purpose: a dropdown
79
+ * inside an `overflow-x` container is clipped by it, so its items would open
80
+ * into a 4px-tall scrollbar.
81
+ */
82
+ .YUI_PERIOD_MORE {
83
+ flex: 0 0 auto;
84
+ margin-left: 0.25rem;
85
+ }
86
+
87
+ /*
88
+ * The navigator is the LOUDEST thing in the block: it is what the user
89
+ * actually operates ("show me yesterday"), and at Bulma's default button size
90
+ * it read as chrome. Big targets, a big label — a phone thumb hits them
91
+ * without aiming.
92
+ */
93
+ /* Two classes deep on purpose: Bulma's `.button` sets its own font-size and
94
+ * ships AFTER this file, so a single-class rule of equal specificity loses
95
+ * to it — the label stayed at 16px and read as chrome. */
96
+ .YUI_PERIOD .YUI_PERIOD_NAV .button {
97
+ height: auto;
98
+ min-width: 2.75rem;
99
+ padding: 0.5rem 0.75rem;
100
+ font-size: 1.25rem;
101
+ }
102
+
103
+ /* THREE classes deep, and the reason is the rule right above: the label is a
104
+ * `.button` INSIDE the navigator, so `.YUI_PERIOD .YUI_PERIOD_NAV .button`
105
+ * outranked a two-class label rule and pinned it to the arrows' 1.25rem — the
106
+ * row rendered flat, with nothing standing out, which is the opposite of what
107
+ * it is for. */
108
+ .YUI_PERIOD .YUI_PERIOD_NAV .YUI_PERIOD_LABEL {
109
+ font-size: 1.35rem;
110
+ font-weight: 700;
111
+ min-width: 9rem;
112
+ }
113
+
114
+ .YUI_PERIOD_LABEL .YUI_PERIOD_LABEL_TEXT {
115
+ margin-left: 0.4rem;
116
+ }
117
+
118
+ /* The calendar glyph is an affordance, not a headline: quieter and smaller
119
+ * than the label text it decorates. */
120
+ .YUI_PERIOD_LABEL .YUI_PERIOD_LABEL_ICON {
121
+ font-size: 1rem;
122
+ opacity: 0.55;
123
+ }
124
+
125
+ /* A name is for the user; these two timestamps are what the query carries.
126
+ * Quiet, but always there — nobody should have to guess what "Week 27"
127
+ * resolves to. */
128
+ .YUI_PERIOD_RESOLVED {
129
+ font-size: 0.75rem;
130
+ opacity: 0.7;
131
+ margin-top: 0.15rem;
132
+ }
133
+
134
+ /* Disabled, not gone (see repaint): the row keeps its place so the card does
135
+ * not re-flow under the cursor on every mode change. */
136
+ .YUI_PERIOD_NAV.yui-period-nav-off {
137
+ opacity: 0.45;
138
+ }
139
+
140
+ /* The calendar hangs off the navigator, centred under the label.
141
+ * `display: flex` lives HERE and not as Bulma's `.is-flex` helper: that one
142
+ * is `!important` and would beat `.is-hidden` (also `!important`), keeping
143
+ * the arrows on screen in the modes that have no bucket to walk. */
144
+ .YUI_PERIOD_NAV {
145
+ position: relative;
146
+ display: flex;
147
+ align-items: center;
148
+ justify-content: center;
149
+ }
150
+
151
+ .YUI_PERIOD_CALENDAR {
152
+ position: absolute;
153
+ top: 100%;
154
+ left: 50%;
155
+ transform: translateX(-50%);
156
+ z-index: 40;
157
+ min-width: 17rem;
158
+ max-width: 95vw;
159
+ }
160
+
161
+ .YUI_PERIOD_CAL_DAY,
162
+ .YUI_PERIOD_CAL_MONTH,
163
+ .YUI_PERIOD_CAL_YEAR,
164
+ .YUI_PERIOD_CAL_WEEKNUM {
165
+ padding-left: 0;
166
+ padding-right: 0;
167
+ }
168
+
169
+ /* The bucket a click would pick, while hovering: a quiet inset ring — the
170
+ * solid fill stays reserved for the bucket that IS selected.
171
+ * Three classes deep: Bulma's `.button.is-ghost` sets `box-shadow: none` at
172
+ * (0,2,0) and ships after this file, so a two-class rule loses to it.
173
+ * currentColor, so the ring follows the cell (link-blue, grey on the
174
+ * neighbouring months) and both themes for free. */
175
+ .YUI_PERIOD .YUI_PERIOD_CALENDAR .yui-period-preview {
176
+ box-shadow: inset 0 0 0 1px currentColor;
177
+ }
178
+
179
+ /* No transitions (framework rule): a granularity switches, it does not
180
+ * glide. */
181
+ .YUI_PERIOD .button,
182
+ .YUI_PERIOD_CALENDAR {
183
+ transition: none !important;
184
+ }