@yuneta/gobj-ui 7.23.15 → 7.23.17

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.
@@ -10110,18 +10110,34 @@ var GROUP_COLORS = {
10110
10110
  };
10111
10111
  /***************************************************************
10112
10112
  * Soft, theme-aware card palette, same visual language as the
10113
- * gobj-tree's role_card_style(): tinted fill + group-colour
10114
- * border, lifted off the near-black canvas on dark (a faint tint
10115
- * on the canvas colour makes the cards invisible).
10113
+ * gobj-tree's role_card_style(): a near-neutral fill and a
10114
+ * group-colour border.
10115
+ *
10116
+ * The SURFACE stays out of the hue, on both themes, and that is
10117
+ * the whole rule here. The light card is near-white and lets
10118
+ * saturated dark text carry the colour; the dark card is its
10119
+ * mirror -- a near-neutral dark surface under bright text -- and
10120
+ * the group's hue lives in the border and the header bar, which
10121
+ * is where the light theme had always put it.
10122
+ *
10123
+ * It shipped tinting the dark surface 30% instead, and that put
10124
+ * green values on a green card. Worse, `list` (a yellow tint)
10125
+ * landed mid-luminance, a muddy olive where nothing contrasted
10126
+ * with anything: its purple values measured 1.60:1, against 11.48
10127
+ * for the same values on the light card. The 12% left is enough
10128
+ * to tell a dict card from a list one and not enough to fight the
10129
+ * text.
10116
10130
  *
10117
10131
  * The header bar is always the border colour, so its text flips:
10118
10132
  * white on the dark-teal light bar, near-black on the brightened
10119
- * dark one.
10133
+ * dark one. The dark bar is mixed lighter than the border used
10134
+ * to be so that near-black reads on it (6.87:1, which is what
10135
+ * white gets on the light bar).
10120
10136
  ***************************************************************/
10121
10137
  function json_card_style(group, dark) {
10122
10138
  return {
10123
- bg: dark ? `color-mix(in srgb, ${group.tint} 30%, #2c3542)` : group.fill,
10124
- border: dark ? `color-mix(in srgb, ${group.stroke} 85%, #ffffff)` : group.stroke,
10139
+ bg: dark ? `color-mix(in srgb, ${group.tint} 12%, #1E242E)` : group.fill,
10140
+ border: dark ? `color-mix(in srgb, ${group.stroke} 55%, #ffffff)` : group.stroke,
10125
10141
  header_fg: dark ? "#12181f" : "#ffffff",
10126
10142
  key: dark ? "#c9cfd8" : "#1A1A1A"
10127
10143
  };
@@ -10130,11 +10146,18 @@ function json_card_style(group, dark) {
10130
10146
  * A scalar's colour by type. The palette is tuned for a light
10131
10147
  * card; on a dark one every one of them (dark green #006000,
10132
10148
  * blue #475ED0, …) sinks into the background, so brighten it.
10149
+ *
10150
+ * 60% white and not the 45% it shipped with. The number is a
10151
+ * balance measured both ways: brighter reads better on the dark
10152
+ * card (the worst type goes from 2.88:1 to 4.74:1) and washes the
10153
+ * six types toward one pastel, and past this point red, orange
10154
+ * and purple stop being tellable apart, which is the only reason
10155
+ * they are coloured at all.
10133
10156
  ***************************************************************/
10134
10157
  function type_color(type, dark) {
10135
10158
  let color = TYPE_COLORS[type] || (dark ? "#e8eaed" : "black");
10136
10159
  if (!dark) return color;
10137
- return `color-mix(in srgb, ${color} 55%, #ffffff)`;
10160
+ return `color-mix(in srgb, ${color} 40%, #ffffff)`;
10138
10161
  }
10139
10162
  /******************************
10140
10163
  * Framework Methods
@@ -10431,8 +10454,9 @@ function build_graph$3(gobj) {
10431
10454
  * Build cell value HTML (matching old mx_json_viewer colors)
10432
10455
  ************************************************************/
10433
10456
  function build_cell_html(key, value, type, dark, matched) {
10434
- let color = type_color(type, dark);
10435
- let key_color = json_card_style(GROUP_COLORS.dict, dark).key;
10457
+ let on_light = matched ? false : dark;
10458
+ let color = type_color(type, on_light);
10459
+ let key_color = json_card_style(GROUP_COLORS.dict, on_light).key;
10436
10460
  let display_value = "";
10437
10461
  switch (type) {
10438
10462
  case "string":
@@ -10456,7 +10480,7 @@ function build_cell_html(key, value, type, dark, matched) {
10456
10480
  default: display_value = String(value);
10457
10481
  }
10458
10482
  let row = `<span style="color:${key_color}">• ${(0, _yuneta_gobj_js.escapeHtml)(String(key))}: </span><span style="color:${color}">${display_value}</span>`;
10459
- if (matched) row = `<span style="background:${dark ? "#7a5d00" : "#ffe082"}; border-radius:2px; padding:0 2px;">${row}</span>`;
10483
+ if (matched) row = `<span style="background:#ffe082; border-radius:2px; padding:0 2px;">${row}</span>`;
10460
10484
  return row;
10461
10485
  }
10462
10486
  /************************************************************
@@ -11341,6 +11365,56 @@ function json_text_dump(root, max_chars) {
11341
11365
  capped: false
11342
11366
  };
11343
11367
  }
11368
+ /************************************************************
11369
+ * Which view a freshly mounted viewer opens on.
11370
+ *
11371
+ * Three answers, in order of authority: what the HOST asked
11372
+ * for, what the READER chose last time, and `fallback`.
11373
+ *
11374
+ * The host wins because a viewer mounted to show a graph has
11375
+ * to show one. The memory comes next, because reopening a
11376
+ * document in the view you just left is the whole point of
11377
+ * remembering it. `fallback` is the floor.
11378
+ *
11379
+ * Anything not in `modes` is treated as unsaid, which covers
11380
+ * an absent value, an empty one, and a stored string from an
11381
+ * older release that named a view no longer offered.
11382
+ ************************************************************/
11383
+ function pick_view_mode(asked, remembered, modes, fallback) {
11384
+ let known = (m) => Array.isArray(modes) && modes.indexOf(m) >= 0;
11385
+ if (known(asked)) return asked;
11386
+ if (known(remembered)) return remembered;
11387
+ return fallback;
11388
+ }
11389
+ /************************************************************
11390
+ * The short label a container row carries beside its size.
11391
+ *
11392
+ * An array of dicts that carry an `id` is a list of RECORDS,
11393
+ * and a row that says only `2: {15}` makes you open all
11394
+ * fifteen fields of every one of them to find out WHICH
11395
+ * record it is. The id is the one field that answers that,
11396
+ * so the row says it without being opened.
11397
+ *
11398
+ * Only a SCALAR id, and only when it is really there: an `id`
11399
+ * that is itself a dict or a list is not a name, it is more
11400
+ * document, and printing `[object Object]` beside the size
11401
+ * would be worse than printing nothing.
11402
+ *
11403
+ * Cut at `max_chars`, because this rides on a row whose job
11404
+ * is to stay one line.
11405
+ ************************************************************/
11406
+ function container_label(value, max_chars) {
11407
+ if (value === null || typeof value !== "object" || Array.isArray(value)) return "";
11408
+ if (!Object.prototype.hasOwnProperty.call(value, "id")) return "";
11409
+ let id = value.id;
11410
+ let t = typeof id;
11411
+ if (t !== "string" && t !== "number" && t !== "boolean") return "";
11412
+ let text = String(id).trim();
11413
+ if (text === "") return "";
11414
+ let cap = typeof max_chars === "number" && max_chars > 0 ? max_chars : 0;
11415
+ if (cap && text.length > cap) return text.slice(0, cap) + "…";
11416
+ return text;
11417
+ }
11344
11418
  //#endregion
11345
11419
  //#region src/c_yui_json.js
11346
11420
  /***********************************************************************
@@ -11371,7 +11445,9 @@ function json_text_dump(root, max_chars) {
11371
11445
  *
11372
11446
  * THREE VIEWS over the same working document, chosen by the
11373
11447
  * `view_mode` attr ("tree" | "text" | "graph") and switched from the
11374
- * toolbar. They answer three different questions:
11448
+ * toolbar. The viewer opens on what the host asked for, else on the
11449
+ * view the reader chose last (kept in localStorage), else on the tree.
11450
+ * They answer three different questions:
11375
11451
  *
11376
11452
  * tree where is this value, and what is around it — the lazy
11377
11453
  * view above, the only one that can drill.
@@ -11405,6 +11481,7 @@ function json_text_dump(root, max_chars) {
11405
11481
  var GCLASS_NAME$10 = "C_YUI_JSON";
11406
11482
  var MAX_RENDER_ROWS = 5e3;
11407
11483
  var MAX_TEXT_CHARS = 2e6;
11484
+ var MAX_LABEL_CHARS = 40;
11408
11485
  var VIEWS = [
11409
11486
  {
11410
11487
  mode: "text",
@@ -11422,6 +11499,7 @@ var VIEWS = [
11422
11499
  key: "graph view"
11423
11500
  }
11424
11501
  ];
11502
+ var VIEW_MODE_STORAGE_KEY = "yui_json_view_mode";
11425
11503
  /***************************************************************
11426
11504
  * Data
11427
11505
  ***************************************************************/
@@ -11429,7 +11507,7 @@ var attrs_table$10 = [
11429
11507
  (0, _yuneta_gobj_js.SDATA)(_yuneta_gobj_js.data_type_t.DTP_POINTER, "subscriber", 0, null, "Subscriber of output events"),
11430
11508
  (0, _yuneta_gobj_js.SDATA)(_yuneta_gobj_js.data_type_t.DTP_STRING, "title", 0, "", "Optional header title (i18n key)"),
11431
11509
  (0, _yuneta_gobj_js.SDATA)(_yuneta_gobj_js.data_type_t.DTP_JSON, "json_data", 0, null, "Initial JSON to render (usually already collapsed)"),
11432
- (0, _yuneta_gobj_js.SDATA)(_yuneta_gobj_js.data_type_t.DTP_STRING, "view_mode", 0, "tree", "View: 'tree' (lazy tree), 'text' (raw dump) or 'graph'"),
11510
+ (0, _yuneta_gobj_js.SDATA)(_yuneta_gobj_js.data_type_t.DTP_STRING, "view_mode", 0, "", "View: 'tree' (lazy tree), 'text' (raw dump) or 'graph'. Empty means the host does not care: the reader's last choice wins, and the tree if there is none"),
11433
11511
  (0, _yuneta_gobj_js.SDATA)(_yuneta_gobj_js.data_type_t.DTP_POINTER, "$container", 0, null, "HTMLElement root, mounted by the parent"),
11434
11512
  (0, _yuneta_gobj_js.SDATA_END)()
11435
11513
  ];
@@ -11466,6 +11544,7 @@ function mt_create$10(gobj) {
11466
11544
  let json_data = (0, _yuneta_gobj_js.gobj_read_attr)(gobj, "json_data");
11467
11545
  if (json_data !== null && json_data !== void 0) priv.root = (0, _yuneta_gobj_js.json_deep_copy)(json_data);
11468
11546
  build_ui$9(gobj);
11547
+ resolve_initial_view_mode(gobj);
11469
11548
  apply_view_mode(gobj);
11470
11549
  let subscriber = (0, _yuneta_gobj_js.gobj_read_pointer_attr)(gobj, "subscriber");
11471
11550
  if (!subscriber) subscriber = (0, _yuneta_gobj_js.gobj_parent)(gobj);
@@ -11723,6 +11802,27 @@ function view_label(mode) {
11723
11802
  }
11724
11803
  }
11725
11804
  /************************************************************
11805
+ * The view the viewer OPENS on.
11806
+ *
11807
+ * Three answers, in order of authority: what the HOST asked for, what
11808
+ * the READER chose last time, and the tree. The host wins because a
11809
+ * viewer mounted to show a graph has to show one; the memory comes
11810
+ * next, because reopening a document in the view you just left is the
11811
+ * whole point of remembering it; the tree is the floor.
11812
+ *
11813
+ * This is why the attr no longer declares "tree" as its default: as a
11814
+ * default and as a host's explicit choice it was the same string, so
11815
+ * nothing could tell "show me the tree" from "I have no opinion", and
11816
+ * a memory that cannot see the difference has to lose to both.
11817
+ *
11818
+ * Resolved ONCE here and written into the attr, so every later read
11819
+ * is a plain attr read and no render touches the store.
11820
+ ************************************************************/
11821
+ function resolve_initial_view_mode(gobj) {
11822
+ let mode = pick_view_mode((0, _yuneta_gobj_js.gobj_read_str_attr)(gobj, "view_mode"), (0, _yuneta_gobj_js.kw_get_local_storage_value)(VIEW_MODE_STORAGE_KEY, "", false), VIEWS.map((v) => v.mode), "tree");
11823
+ (0, _yuneta_gobj_js.gobj_write_attr)(gobj, "view_mode", mode);
11824
+ }
11825
+ /************************************************************
11726
11826
  * The current view, normalised. Anything unknown is the tree.
11727
11827
  ************************************************************/
11728
11828
  function current_view_mode(gobj) {
@@ -11972,7 +12072,7 @@ function push_container_rows(ctx, key, value, segments, depth, rows, key_match)
11972
12072
  let searching_match = ctx.term && !key_match && subtree_matches(value, ctx.term);
11973
12073
  let open = priv.expanded.has(path) || searching_match;
11974
12074
  ctx.count++;
11975
- rows.push(toggle_row(ctx.gobj, key, size, is_object, depth, path, open));
12075
+ rows.push(toggle_row(ctx.gobj, key, size, is_object, depth, path, open, value));
11976
12076
  if (!open) return;
11977
12077
  if (is_object) for (let [k, v] of Object.entries(value)) push_entry_rows(ctx, k, v, segments.concat(k), depth + 1, rows);
11978
12078
  else value.forEach(function(v, i) {
@@ -12057,7 +12157,7 @@ function push_collapsed_row(ctx, value, segments, depth, key, rows) {
12057
12157
  /************************************************************
12058
12158
  * Expandable container header row
12059
12159
  ************************************************************/
12060
- function toggle_row(gobj, key, size, is_object, depth, path, open) {
12160
+ function toggle_row(gobj, key, size, is_object, depth, path, open, value) {
12061
12161
  let summary = (is_object ? "{" : "[") + String(size) + (is_object ? "}" : "]");
12062
12162
  let body = [];
12063
12163
  if (key !== null && key !== "") {
@@ -12077,6 +12177,12 @@ function toggle_row(gobj, key, size, is_object, depth, path, open) {
12077
12177
  { class: "JSON_SUMMARY has-text-grey" },
12078
12178
  summary
12079
12179
  ]);
12180
+ let label = container_label(value, MAX_LABEL_CHARS);
12181
+ if (label) body.push([
12182
+ "span",
12183
+ { class: "JSON_SUMMARY_ID ml-2" },
12184
+ label
12185
+ ]);
12080
12186
  return [
12081
12187
  "div",
12082
12188
  {
@@ -12330,6 +12436,8 @@ function ac_copy_all(gobj, event, kw, src) {
12330
12436
  * With no mode it ADVANCES to the next view in VIEWS order, which
12331
12437
  * is what the two-view toggle did when the list was two long. The
12332
12438
  * toolbar buttons always name their mode.
12439
+ *
12440
+ * Whatever lands here is REMEMBERED, so the next viewer opens on it.
12333
12441
  ************************************************************/
12334
12442
  function ac_set_view_mode(gobj, event, kw, src) {
12335
12443
  let mode = kw.mode;
@@ -12340,6 +12448,7 @@ function ac_set_view_mode(gobj, event, kw, src) {
12340
12448
  }
12341
12449
  if (mode === current_view_mode(gobj)) return 0;
12342
12450
  (0, _yuneta_gobj_js.gobj_write_attr)(gobj, "view_mode", mode);
12451
+ (0, _yuneta_gobj_js.kw_set_local_storage_value)(VIEW_MODE_STORAGE_KEY, mode);
12343
12452
  apply_view_mode(gobj);
12344
12453
  render_view(gobj);
12345
12454
  return 0;
@@ -10105,18 +10105,34 @@ var GROUP_COLORS = {
10105
10105
  };
10106
10106
  /***************************************************************
10107
10107
  * Soft, theme-aware card palette, same visual language as the
10108
- * gobj-tree's role_card_style(): tinted fill + group-colour
10109
- * border, lifted off the near-black canvas on dark (a faint tint
10110
- * on the canvas colour makes the cards invisible).
10108
+ * gobj-tree's role_card_style(): a near-neutral fill and a
10109
+ * group-colour border.
10110
+ *
10111
+ * The SURFACE stays out of the hue, on both themes, and that is
10112
+ * the whole rule here. The light card is near-white and lets
10113
+ * saturated dark text carry the colour; the dark card is its
10114
+ * mirror -- a near-neutral dark surface under bright text -- and
10115
+ * the group's hue lives in the border and the header bar, which
10116
+ * is where the light theme had always put it.
10117
+ *
10118
+ * It shipped tinting the dark surface 30% instead, and that put
10119
+ * green values on a green card. Worse, `list` (a yellow tint)
10120
+ * landed mid-luminance, a muddy olive where nothing contrasted
10121
+ * with anything: its purple values measured 1.60:1, against 11.48
10122
+ * for the same values on the light card. The 12% left is enough
10123
+ * to tell a dict card from a list one and not enough to fight the
10124
+ * text.
10111
10125
  *
10112
10126
  * The header bar is always the border colour, so its text flips:
10113
10127
  * white on the dark-teal light bar, near-black on the brightened
10114
- * dark one.
10128
+ * dark one. The dark bar is mixed lighter than the border used
10129
+ * to be so that near-black reads on it (6.87:1, which is what
10130
+ * white gets on the light bar).
10115
10131
  ***************************************************************/
10116
10132
  function json_card_style(group, dark) {
10117
10133
  return {
10118
- bg: dark ? `color-mix(in srgb, ${group.tint} 30%, #2c3542)` : group.fill,
10119
- border: dark ? `color-mix(in srgb, ${group.stroke} 85%, #ffffff)` : group.stroke,
10134
+ bg: dark ? `color-mix(in srgb, ${group.tint} 12%, #1E242E)` : group.fill,
10135
+ border: dark ? `color-mix(in srgb, ${group.stroke} 55%, #ffffff)` : group.stroke,
10120
10136
  header_fg: dark ? "#12181f" : "#ffffff",
10121
10137
  key: dark ? "#c9cfd8" : "#1A1A1A"
10122
10138
  };
@@ -10125,11 +10141,18 @@ function json_card_style(group, dark) {
10125
10141
  * A scalar's colour by type. The palette is tuned for a light
10126
10142
  * card; on a dark one every one of them (dark green #006000,
10127
10143
  * blue #475ED0, …) sinks into the background, so brighten it.
10144
+ *
10145
+ * 60% white and not the 45% it shipped with. The number is a
10146
+ * balance measured both ways: brighter reads better on the dark
10147
+ * card (the worst type goes from 2.88:1 to 4.74:1) and washes the
10148
+ * six types toward one pastel, and past this point red, orange
10149
+ * and purple stop being tellable apart, which is the only reason
10150
+ * they are coloured at all.
10128
10151
  ***************************************************************/
10129
10152
  function type_color(type, dark) {
10130
10153
  let color = TYPE_COLORS[type] || (dark ? "#e8eaed" : "black");
10131
10154
  if (!dark) return color;
10132
- return `color-mix(in srgb, ${color} 55%, #ffffff)`;
10155
+ return `color-mix(in srgb, ${color} 40%, #ffffff)`;
10133
10156
  }
10134
10157
  /******************************
10135
10158
  * Framework Methods
@@ -10426,8 +10449,9 @@ function build_graph$3(gobj) {
10426
10449
  * Build cell value HTML (matching old mx_json_viewer colors)
10427
10450
  ************************************************************/
10428
10451
  function build_cell_html(key, value, type, dark, matched) {
10429
- let color = type_color(type, dark);
10430
- let key_color = json_card_style(GROUP_COLORS.dict, dark).key;
10452
+ let on_light = matched ? false : dark;
10453
+ let color = type_color(type, on_light);
10454
+ let key_color = json_card_style(GROUP_COLORS.dict, on_light).key;
10431
10455
  let display_value = "";
10432
10456
  switch (type) {
10433
10457
  case "string":
@@ -10451,7 +10475,7 @@ function build_cell_html(key, value, type, dark, matched) {
10451
10475
  default: display_value = String(value);
10452
10476
  }
10453
10477
  let row = `<span style="color:${key_color}">• ${escapeHtml(String(key))}: </span><span style="color:${color}">${display_value}</span>`;
10454
- if (matched) row = `<span style="background:${dark ? "#7a5d00" : "#ffe082"}; border-radius:2px; padding:0 2px;">${row}</span>`;
10478
+ if (matched) row = `<span style="background:#ffe082; border-radius:2px; padding:0 2px;">${row}</span>`;
10455
10479
  return row;
10456
10480
  }
10457
10481
  /************************************************************
@@ -11336,6 +11360,56 @@ function json_text_dump(root, max_chars) {
11336
11360
  capped: false
11337
11361
  };
11338
11362
  }
11363
+ /************************************************************
11364
+ * Which view a freshly mounted viewer opens on.
11365
+ *
11366
+ * Three answers, in order of authority: what the HOST asked
11367
+ * for, what the READER chose last time, and `fallback`.
11368
+ *
11369
+ * The host wins because a viewer mounted to show a graph has
11370
+ * to show one. The memory comes next, because reopening a
11371
+ * document in the view you just left is the whole point of
11372
+ * remembering it. `fallback` is the floor.
11373
+ *
11374
+ * Anything not in `modes` is treated as unsaid, which covers
11375
+ * an absent value, an empty one, and a stored string from an
11376
+ * older release that named a view no longer offered.
11377
+ ************************************************************/
11378
+ function pick_view_mode(asked, remembered, modes, fallback) {
11379
+ let known = (m) => Array.isArray(modes) && modes.indexOf(m) >= 0;
11380
+ if (known(asked)) return asked;
11381
+ if (known(remembered)) return remembered;
11382
+ return fallback;
11383
+ }
11384
+ /************************************************************
11385
+ * The short label a container row carries beside its size.
11386
+ *
11387
+ * An array of dicts that carry an `id` is a list of RECORDS,
11388
+ * and a row that says only `2: {15}` makes you open all
11389
+ * fifteen fields of every one of them to find out WHICH
11390
+ * record it is. The id is the one field that answers that,
11391
+ * so the row says it without being opened.
11392
+ *
11393
+ * Only a SCALAR id, and only when it is really there: an `id`
11394
+ * that is itself a dict or a list is not a name, it is more
11395
+ * document, and printing `[object Object]` beside the size
11396
+ * would be worse than printing nothing.
11397
+ *
11398
+ * Cut at `max_chars`, because this rides on a row whose job
11399
+ * is to stay one line.
11400
+ ************************************************************/
11401
+ function container_label(value, max_chars) {
11402
+ if (value === null || typeof value !== "object" || Array.isArray(value)) return "";
11403
+ if (!Object.prototype.hasOwnProperty.call(value, "id")) return "";
11404
+ let id = value.id;
11405
+ let t = typeof id;
11406
+ if (t !== "string" && t !== "number" && t !== "boolean") return "";
11407
+ let text = String(id).trim();
11408
+ if (text === "") return "";
11409
+ let cap = typeof max_chars === "number" && max_chars > 0 ? max_chars : 0;
11410
+ if (cap && text.length > cap) return text.slice(0, cap) + "…";
11411
+ return text;
11412
+ }
11339
11413
  //#endregion
11340
11414
  //#region src/c_yui_json.js
11341
11415
  /***********************************************************************
@@ -11366,7 +11440,9 @@ function json_text_dump(root, max_chars) {
11366
11440
  *
11367
11441
  * THREE VIEWS over the same working document, chosen by the
11368
11442
  * `view_mode` attr ("tree" | "text" | "graph") and switched from the
11369
- * toolbar. They answer three different questions:
11443
+ * toolbar. The viewer opens on what the host asked for, else on the
11444
+ * view the reader chose last (kept in localStorage), else on the tree.
11445
+ * They answer three different questions:
11370
11446
  *
11371
11447
  * tree where is this value, and what is around it — the lazy
11372
11448
  * view above, the only one that can drill.
@@ -11400,6 +11476,7 @@ function json_text_dump(root, max_chars) {
11400
11476
  var GCLASS_NAME$10 = "C_YUI_JSON";
11401
11477
  var MAX_RENDER_ROWS = 5e3;
11402
11478
  var MAX_TEXT_CHARS = 2e6;
11479
+ var MAX_LABEL_CHARS = 40;
11403
11480
  var VIEWS = [
11404
11481
  {
11405
11482
  mode: "text",
@@ -11417,6 +11494,7 @@ var VIEWS = [
11417
11494
  key: "graph view"
11418
11495
  }
11419
11496
  ];
11497
+ var VIEW_MODE_STORAGE_KEY = "yui_json_view_mode";
11420
11498
  /***************************************************************
11421
11499
  * Data
11422
11500
  ***************************************************************/
@@ -11424,7 +11502,7 @@ var attrs_table$10 = [
11424
11502
  SDATA(data_type_t.DTP_POINTER, "subscriber", 0, null, "Subscriber of output events"),
11425
11503
  SDATA(data_type_t.DTP_STRING, "title", 0, "", "Optional header title (i18n key)"),
11426
11504
  SDATA(data_type_t.DTP_JSON, "json_data", 0, null, "Initial JSON to render (usually already collapsed)"),
11427
- SDATA(data_type_t.DTP_STRING, "view_mode", 0, "tree", "View: 'tree' (lazy tree), 'text' (raw dump) or 'graph'"),
11505
+ SDATA(data_type_t.DTP_STRING, "view_mode", 0, "", "View: 'tree' (lazy tree), 'text' (raw dump) or 'graph'. Empty means the host does not care: the reader's last choice wins, and the tree if there is none"),
11428
11506
  SDATA(data_type_t.DTP_POINTER, "$container", 0, null, "HTMLElement root, mounted by the parent"),
11429
11507
  SDATA_END()
11430
11508
  ];
@@ -11461,6 +11539,7 @@ function mt_create$10(gobj) {
11461
11539
  let json_data = gobj_read_attr(gobj, "json_data");
11462
11540
  if (json_data !== null && json_data !== void 0) priv.root = json_deep_copy(json_data);
11463
11541
  build_ui$9(gobj);
11542
+ resolve_initial_view_mode(gobj);
11464
11543
  apply_view_mode(gobj);
11465
11544
  let subscriber = gobj_read_pointer_attr(gobj, "subscriber");
11466
11545
  if (!subscriber) subscriber = gobj_parent(gobj);
@@ -11718,6 +11797,27 @@ function view_label(mode) {
11718
11797
  }
11719
11798
  }
11720
11799
  /************************************************************
11800
+ * The view the viewer OPENS on.
11801
+ *
11802
+ * Three answers, in order of authority: what the HOST asked for, what
11803
+ * the READER chose last time, and the tree. The host wins because a
11804
+ * viewer mounted to show a graph has to show one; the memory comes
11805
+ * next, because reopening a document in the view you just left is the
11806
+ * whole point of remembering it; the tree is the floor.
11807
+ *
11808
+ * This is why the attr no longer declares "tree" as its default: as a
11809
+ * default and as a host's explicit choice it was the same string, so
11810
+ * nothing could tell "show me the tree" from "I have no opinion", and
11811
+ * a memory that cannot see the difference has to lose to both.
11812
+ *
11813
+ * Resolved ONCE here and written into the attr, so every later read
11814
+ * is a plain attr read and no render touches the store.
11815
+ ************************************************************/
11816
+ function resolve_initial_view_mode(gobj) {
11817
+ let mode = pick_view_mode(gobj_read_str_attr(gobj, "view_mode"), kw_get_local_storage_value(VIEW_MODE_STORAGE_KEY, "", false), VIEWS.map((v) => v.mode), "tree");
11818
+ gobj_write_attr(gobj, "view_mode", mode);
11819
+ }
11820
+ /************************************************************
11721
11821
  * The current view, normalised. Anything unknown is the tree.
11722
11822
  ************************************************************/
11723
11823
  function current_view_mode(gobj) {
@@ -11967,7 +12067,7 @@ function push_container_rows(ctx, key, value, segments, depth, rows, key_match)
11967
12067
  let searching_match = ctx.term && !key_match && subtree_matches(value, ctx.term);
11968
12068
  let open = priv.expanded.has(path) || searching_match;
11969
12069
  ctx.count++;
11970
- rows.push(toggle_row(ctx.gobj, key, size, is_object, depth, path, open));
12070
+ rows.push(toggle_row(ctx.gobj, key, size, is_object, depth, path, open, value));
11971
12071
  if (!open) return;
11972
12072
  if (is_object) for (let [k, v] of Object.entries(value)) push_entry_rows(ctx, k, v, segments.concat(k), depth + 1, rows);
11973
12073
  else value.forEach(function(v, i) {
@@ -12052,7 +12152,7 @@ function push_collapsed_row(ctx, value, segments, depth, key, rows) {
12052
12152
  /************************************************************
12053
12153
  * Expandable container header row
12054
12154
  ************************************************************/
12055
- function toggle_row(gobj, key, size, is_object, depth, path, open) {
12155
+ function toggle_row(gobj, key, size, is_object, depth, path, open, value) {
12056
12156
  let summary = (is_object ? "{" : "[") + String(size) + (is_object ? "}" : "]");
12057
12157
  let body = [];
12058
12158
  if (key !== null && key !== "") {
@@ -12072,6 +12172,12 @@ function toggle_row(gobj, key, size, is_object, depth, path, open) {
12072
12172
  { class: "JSON_SUMMARY has-text-grey" },
12073
12173
  summary
12074
12174
  ]);
12175
+ let label = container_label(value, MAX_LABEL_CHARS);
12176
+ if (label) body.push([
12177
+ "span",
12178
+ { class: "JSON_SUMMARY_ID ml-2" },
12179
+ label
12180
+ ]);
12075
12181
  return [
12076
12182
  "div",
12077
12183
  {
@@ -12325,6 +12431,8 @@ function ac_copy_all(gobj, event, kw, src) {
12325
12431
  * With no mode it ADVANCES to the next view in VIEWS order, which
12326
12432
  * is what the two-view toggle did when the list was two long. The
12327
12433
  * toolbar buttons always name their mode.
12434
+ *
12435
+ * Whatever lands here is REMEMBERED, so the next viewer opens on it.
12328
12436
  ************************************************************/
12329
12437
  function ac_set_view_mode(gobj, event, kw, src) {
12330
12438
  let mode = kw.mode;
@@ -12335,6 +12443,7 @@ function ac_set_view_mode(gobj, event, kw, src) {
12335
12443
  }
12336
12444
  if (mode === current_view_mode(gobj)) return 0;
12337
12445
  gobj_write_attr(gobj, "view_mode", mode);
12446
+ kw_set_local_storage_value(VIEW_MODE_STORAGE_KEY, mode);
12338
12447
  apply_view_mode(gobj);
12339
12448
  render_view(gobj);
12340
12449
  return 0;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yuneta/gobj-ui",
3
- "version": "7.23.15",
3
+ "version": "7.23.17",
4
4
  "type": "module",
5
5
  "main": "dist/gobj-ui.cjs.js",
6
6
  "module": "dist/gobj-ui.es.js",
@@ -172,6 +172,19 @@
172
172
  .C_YUI_JSON .JSON_TYPE_BOOLEAN { color: var(--json-boolean); }
173
173
  .C_YUI_JSON .JSON_TYPE_NULL { color: var(--json-null); font-style: italic; }
174
174
 
175
+ /*
176
+ * The id a container row carries beside its size. It is a VALUE, so
177
+ * it takes the string colour and follows the theme with it; what sets
178
+ * it apart from the values under it is the WEIGHT, not a colour of its
179
+ * own -- a sixth colour in a row that already carries five would be
180
+ * one more thing to decode, for a field whose whole job is to be read
181
+ * at a glance while scrolling.
182
+ */
183
+ .C_YUI_JSON .JSON_SUMMARY_ID {
184
+ color: var(--json-string);
185
+ font-weight: 600;
186
+ }
187
+
175
188
  .C_YUI_JSON .JSON_STUB {
176
189
  color: var(--bulma-link, #3273dc);
177
190
  font-weight: 600;
package/src/c_yui_json.js CHANGED
@@ -26,7 +26,9 @@
26
26
  *
27
27
  * THREE VIEWS over the same working document, chosen by the
28
28
  * `view_mode` attr ("tree" | "text" | "graph") and switched from the
29
- * toolbar. They answer three different questions:
29
+ * toolbar. The viewer opens on what the host asked for, else on the
30
+ * view the reader chose last (kept in localStorage), else on the tree.
31
+ * They answer three different questions:
30
32
  *
31
33
  * tree where is this value, and what is around it — the lazy
32
34
  * view above, the only one that can drill.
@@ -82,6 +84,8 @@ import {
82
84
  json_deep_copy,
83
85
  json_object_size,
84
86
  refresh_language,
87
+ kw_get_local_storage_value,
88
+ kw_set_local_storage_value,
85
89
  } from "@yuneta/gobj-js";
86
90
 
87
91
  import {
@@ -94,6 +98,8 @@ import {
94
98
  is_time_field,
95
99
  format_epoch,
96
100
  json_text_dump,
101
+ pick_view_mode,
102
+ container_label,
97
103
  } from "./json_view_helpers.js";
98
104
 
99
105
  import {yui_toolbar} from "./yui_toolbar.js";
@@ -125,11 +131,19 @@ const MAX_RENDER_ROWS = 5000;
125
131
  */
126
132
  const MAX_TEXT_CHARS = 2000000;
127
133
 
134
+ /*
135
+ * How much of a container's id label fits beside its size. It
136
+ * rides on a row whose job is to stay one line, and an id is
137
+ * usually short; the ones that are not are uuids, where the head
138
+ * is what a person compares anyway.
139
+ */
140
+ const MAX_LABEL_CHARS = 40;
141
+
128
142
  /*
129
143
  * The three views, in switch order — this table IS the order of the
130
144
  * buttons, and EV_SET_VIEW_MODE with no mode advances along it.
131
- * Note the order is not the default: the viewer still OPENS on the
132
- * tree (the `view_mode` attr default), it just sits second in the row.
145
+ * Note the order is not the default: the viewer OPENS on the tree
146
+ * unless something says otherwise, it just sits second in the row.
133
147
  */
134
148
  const VIEWS = [
135
149
  {mode: "text", icon: "yi-code", key: "text view"},
@@ -137,6 +151,16 @@ const VIEWS = [
137
151
  {mode: "graph", icon: "yi-hexagon-nodes", key: "graph view"},
138
152
  ];
139
153
 
154
+ /*
155
+ * Where the reader's last choice of view is kept. ONE key for the
156
+ * whole library and not one per host: which of the three views someone
157
+ * reads JSON in is a habit of the PERSON, not a property of the
158
+ * document, so the next document they open opens the way they read the
159
+ * last one. It lives in localStorage, which is per browser and per
160
+ * artifact origin, and reaches nobody else.
161
+ */
162
+ const VIEW_MODE_STORAGE_KEY = "yui_json_view_mode";
163
+
140
164
  /***************************************************************
141
165
  * Data
142
166
  ***************************************************************/
@@ -147,7 +171,7 @@ SDATA(data_type_t.DTP_POINTER, "subscriber", 0, null, "Subscriber of outpu
147
171
  /*---------------- Config ----------------*/
148
172
  SDATA(data_type_t.DTP_STRING, "title", 0, "", "Optional header title (i18n key)"),
149
173
  SDATA(data_type_t.DTP_JSON, "json_data", 0, null, "Initial JSON to render (usually already collapsed)"),
150
- SDATA(data_type_t.DTP_STRING, "view_mode", 0, "tree", "View: 'tree' (lazy tree), 'text' (raw dump) or 'graph'"),
174
+ SDATA(data_type_t.DTP_STRING, "view_mode", 0, "", "View: 'tree' (lazy tree), 'text' (raw dump) or 'graph'. Empty means the host does not care: the reader's last choice wins, and the tree if there is none"),
151
175
 
152
176
  /*---------------- UI ----------------*/
153
177
  SDATA(data_type_t.DTP_POINTER, "$container", 0, null, "HTMLElement root, mounted by the parent"),
@@ -202,6 +226,7 @@ function mt_create(gobj)
202
226
  }
203
227
 
204
228
  build_ui(gobj);
229
+ resolve_initial_view_mode(gobj);
205
230
  apply_view_mode(gobj);
206
231
 
207
232
  /*
@@ -462,6 +487,34 @@ function view_label(mode)
462
487
  }
463
488
  }
464
489
 
490
+ /************************************************************
491
+ * The view the viewer OPENS on.
492
+ *
493
+ * Three answers, in order of authority: what the HOST asked for, what
494
+ * the READER chose last time, and the tree. The host wins because a
495
+ * viewer mounted to show a graph has to show one; the memory comes
496
+ * next, because reopening a document in the view you just left is the
497
+ * whole point of remembering it; the tree is the floor.
498
+ *
499
+ * This is why the attr no longer declares "tree" as its default: as a
500
+ * default and as a host's explicit choice it was the same string, so
501
+ * nothing could tell "show me the tree" from "I have no opinion", and
502
+ * a memory that cannot see the difference has to lose to both.
503
+ *
504
+ * Resolved ONCE here and written into the attr, so every later read
505
+ * is a plain attr read and no render touches the store.
506
+ ************************************************************/
507
+ function resolve_initial_view_mode(gobj)
508
+ {
509
+ let mode = pick_view_mode(
510
+ gobj_read_str_attr(gobj, "view_mode"),
511
+ kw_get_local_storage_value(VIEW_MODE_STORAGE_KEY, "", false),
512
+ VIEWS.map((v) => v.mode),
513
+ "tree"
514
+ );
515
+ gobj_write_attr(gobj, "view_mode", mode);
516
+ }
517
+
465
518
  /************************************************************
466
519
  * The current view, normalised. Anything unknown is the tree.
467
520
  ************************************************************/
@@ -779,7 +832,7 @@ function push_container_rows(ctx, key, value, segments, depth, rows, key_match)
779
832
  let open = priv.expanded.has(path) || searching_match;
780
833
 
781
834
  ctx.count++;
782
- rows.push(toggle_row(ctx.gobj, key, size, is_object, depth, path, open));
835
+ rows.push(toggle_row(ctx.gobj, key, size, is_object, depth, path, open, value));
783
836
 
784
837
  if(!open) {
785
838
  return;
@@ -853,7 +906,7 @@ function push_collapsed_row(ctx, value, segments, depth, key, rows)
853
906
  /************************************************************
854
907
  * Expandable container header row
855
908
  ************************************************************/
856
- function toggle_row(gobj, key, size, is_object, depth, path, open)
909
+ function toggle_row(gobj, key, size, is_object, depth, path, open, value)
857
910
  {
858
911
  let summary = (is_object ? "{" : "[") + String(size) + (is_object ? "}" : "]");
859
912
 
@@ -864,6 +917,21 @@ function toggle_row(gobj, key, size, is_object, depth, path, open)
864
917
  }
865
918
  body.push(['span', {class: 'JSON_SUMMARY has-text-grey'}, summary]);
866
919
 
920
+ /*
921
+ * A dict that carries an id says WHICH one it is, right here.
922
+ * Inside an array of records -- which is what a topic's nodes
923
+ * are -- the row read `2: {15}` and the only way to tell record
924
+ * 2 from record 9 was to open both.
925
+ *
926
+ * Shown open as well as closed: it is the row's label, and a
927
+ * label that disappears when you expand makes the row jump and
928
+ * costs you the name of the thing you just opened.
929
+ */
930
+ let label = container_label(value, MAX_LABEL_CHARS);
931
+ if(label) {
932
+ body.push(['span', {class: 'JSON_SUMMARY_ID ml-2'}, label]);
933
+ }
934
+
867
935
  return ['div', {class: 'JSON_ROW JSON_CONTAINER', style: row_indent(depth)}, [
868
936
  ['span', {class: 'JSON_TOGGLE' + (open ? ' is-open' : '')}, [
869
937
  ['i', {class: 'yi-chevron-right'}]
@@ -1168,6 +1236,8 @@ function ac_copy_all(gobj, event, kw, src)
1168
1236
  * With no mode it ADVANCES to the next view in VIEWS order, which
1169
1237
  * is what the two-view toggle did when the list was two long. The
1170
1238
  * toolbar buttons always name their mode.
1239
+ *
1240
+ * Whatever lands here is REMEMBERED, so the next viewer opens on it.
1171
1241
  ************************************************************/
1172
1242
  function ac_set_view_mode(gobj, event, kw, src)
1173
1243
  {
@@ -1187,6 +1257,14 @@ function ac_set_view_mode(gobj, event, kw, src)
1187
1257
 
1188
1258
  gobj_write_attr(gobj, "view_mode", mode);
1189
1259
 
1260
+ /*
1261
+ * Remembered here and not in resolve_initial_view_mode(): only a
1262
+ * view the READER picked is a preference. A mode a host pinned, or
1263
+ * the tree we fell back to, would otherwise be written back as if
1264
+ * somebody had chosen it.
1265
+ */
1266
+ kw_set_local_storage_value(VIEW_MODE_STORAGE_KEY, mode);
1267
+
1190
1268
  apply_view_mode(gobj);
1191
1269
  render_view(gobj);
1192
1270
  return 0;
@@ -221,22 +221,38 @@ const GROUP_COLORS = {
221
221
 
222
222
  /***************************************************************
223
223
  * Soft, theme-aware card palette, same visual language as the
224
- * gobj-tree's role_card_style(): tinted fill + group-colour
225
- * border, lifted off the near-black canvas on dark (a faint tint
226
- * on the canvas colour makes the cards invisible).
224
+ * gobj-tree's role_card_style(): a near-neutral fill and a
225
+ * group-colour border.
226
+ *
227
+ * The SURFACE stays out of the hue, on both themes, and that is
228
+ * the whole rule here. The light card is near-white and lets
229
+ * saturated dark text carry the colour; the dark card is its
230
+ * mirror -- a near-neutral dark surface under bright text -- and
231
+ * the group's hue lives in the border and the header bar, which
232
+ * is where the light theme had always put it.
233
+ *
234
+ * It shipped tinting the dark surface 30% instead, and that put
235
+ * green values on a green card. Worse, `list` (a yellow tint)
236
+ * landed mid-luminance, a muddy olive where nothing contrasted
237
+ * with anything: its purple values measured 1.60:1, against 11.48
238
+ * for the same values on the light card. The 12% left is enough
239
+ * to tell a dict card from a list one and not enough to fight the
240
+ * text.
227
241
  *
228
242
  * The header bar is always the border colour, so its text flips:
229
243
  * white on the dark-teal light bar, near-black on the brightened
230
- * dark one.
244
+ * dark one. The dark bar is mixed lighter than the border used
245
+ * to be so that near-black reads on it (6.87:1, which is what
246
+ * white gets on the light bar).
231
247
  ***************************************************************/
232
248
  function json_card_style(group, dark)
233
249
  {
234
250
  return {
235
251
  bg: dark
236
- ? `color-mix(in srgb, ${group.tint} 30%, #2c3542)`
252
+ ? `color-mix(in srgb, ${group.tint} 12%, #1E242E)`
237
253
  : group.fill,
238
254
  border: dark
239
- ? `color-mix(in srgb, ${group.stroke} 85%, #ffffff)`
255
+ ? `color-mix(in srgb, ${group.stroke} 55%, #ffffff)`
240
256
  : group.stroke,
241
257
  header_fg: dark ? "#12181f" : "#ffffff",
242
258
  key: dark ? "#c9cfd8" : "#1A1A1A",
@@ -247,6 +263,13 @@ function json_card_style(group, dark)
247
263
  * A scalar's colour by type. The palette is tuned for a light
248
264
  * card; on a dark one every one of them (dark green #006000,
249
265
  * blue #475ED0, …) sinks into the background, so brighten it.
266
+ *
267
+ * 60% white and not the 45% it shipped with. The number is a
268
+ * balance measured both ways: brighter reads better on the dark
269
+ * card (the worst type goes from 2.88:1 to 4.74:1) and washes the
270
+ * six types toward one pastel, and past this point red, orange
271
+ * and purple stop being tellable apart, which is the only reason
272
+ * they are coloured at all.
250
273
  ***************************************************************/
251
274
  function type_color(type, dark)
252
275
  {
@@ -254,7 +277,7 @@ function type_color(type, dark)
254
277
  if(!dark) {
255
278
  return color;
256
279
  }
257
- return `color-mix(in srgb, ${color} 55%, #ffffff)`;
280
+ return `color-mix(in srgb, ${color} 40%, #ffffff)`;
258
281
  }
259
282
 
260
283
 
@@ -698,8 +721,18 @@ function build_graph(gobj)
698
721
  ************************************************************/
699
722
  function build_cell_html(key, value, type, dark, matched)
700
723
  {
701
- let color = type_color(type, dark);
702
- let key_color = json_card_style(GROUP_COLORS.dict, dark).key;
724
+ /*
725
+ * A match is a LIGHT chip on BOTH themes, so what sits on it is
726
+ * light-surface text -- the same flip the header bar does.
727
+ *
728
+ * A dark chip has no room to exist: dark enough for the brightened
729
+ * values to read on it is also dark enough to be invisible against
730
+ * the card, and the amber it used to be (#7a5d00) ended up failing
731
+ * at both ends, 2.52:1 under the text and 2.34:1 over the card.
732
+ */
733
+ let on_light = matched? false: dark;
734
+ let color = type_color(type, on_light);
735
+ let key_color = json_card_style(GROUP_COLORS.dict, on_light).key;
703
736
  let display_value = "";
704
737
 
705
738
  switch(type) {
@@ -734,7 +767,7 @@ function build_cell_html(key, value, type, dark, matched)
734
767
  * a G6 node state: the key shape of an `html` node is a DOM
735
768
  * element, and G6 paints no state style on it. Setting
736
769
  * 'active' here would select correctly and show nothing. */
737
- row = `<span style="background:${dark? "#7a5d00": "#ffe082"}; ` +
770
+ row = `<span style="background:#ffe082; ` +
738
771
  `border-radius:2px; padding:0 2px;">${row}</span>`;
739
772
  }
740
773
  return row;
@@ -242,3 +242,75 @@ export function json_text_dump(root, max_chars)
242
242
  }
243
243
  return {text: text, capped: false};
244
244
  }
245
+
246
+ /************************************************************
247
+ * Which view a freshly mounted viewer opens on.
248
+ *
249
+ * Three answers, in order of authority: what the HOST asked
250
+ * for, what the READER chose last time, and `fallback`.
251
+ *
252
+ * The host wins because a viewer mounted to show a graph has
253
+ * to show one. The memory comes next, because reopening a
254
+ * document in the view you just left is the whole point of
255
+ * remembering it. `fallback` is the floor.
256
+ *
257
+ * Anything not in `modes` is treated as unsaid, which covers
258
+ * an absent value, an empty one, and a stored string from an
259
+ * older release that named a view no longer offered.
260
+ ************************************************************/
261
+ export function pick_view_mode(asked, remembered, modes, fallback)
262
+ {
263
+ let known = (m) => Array.isArray(modes) && modes.indexOf(m) >= 0;
264
+
265
+ if(known(asked)) {
266
+ return asked;
267
+ }
268
+ if(known(remembered)) {
269
+ return remembered;
270
+ }
271
+ return fallback;
272
+ }
273
+
274
+ /************************************************************
275
+ * The short label a container row carries beside its size.
276
+ *
277
+ * An array of dicts that carry an `id` is a list of RECORDS,
278
+ * and a row that says only `2: {15}` makes you open all
279
+ * fifteen fields of every one of them to find out WHICH
280
+ * record it is. The id is the one field that answers that,
281
+ * so the row says it without being opened.
282
+ *
283
+ * Only a SCALAR id, and only when it is really there: an `id`
284
+ * that is itself a dict or a list is not a name, it is more
285
+ * document, and printing `[object Object]` beside the size
286
+ * would be worse than printing nothing.
287
+ *
288
+ * Cut at `max_chars`, because this rides on a row whose job
289
+ * is to stay one line.
290
+ ************************************************************/
291
+ export function container_label(value, max_chars)
292
+ {
293
+ if(value === null || typeof value !== "object" || Array.isArray(value)) {
294
+ return "";
295
+ }
296
+ if(!Object.prototype.hasOwnProperty.call(value, "id")) {
297
+ return "";
298
+ }
299
+
300
+ let id = value.id;
301
+ let t = typeof id;
302
+ if(t !== "string" && t !== "number" && t !== "boolean") {
303
+ return "";
304
+ }
305
+
306
+ let text = String(id).trim();
307
+ if(text === "") {
308
+ return "";
309
+ }
310
+
311
+ let cap = (typeof max_chars === "number" && max_chars > 0)? max_chars: 0;
312
+ if(cap && text.length > cap) {
313
+ return text.slice(0, cap) + "…";
314
+ }
315
+ return text;
316
+ }
@@ -16,6 +16,8 @@ import {
16
16
  is_time_field,
17
17
  format_epoch,
18
18
  json_text_dump,
19
+ pick_view_mode,
20
+ container_label,
19
21
  } from "./json_view_helpers.js";
20
22
 
21
23
 
@@ -175,3 +177,69 @@ test("json_text_dump reports a document with no JSON form instead of printing no
175
177
  expect(d2.text).toBe("");
176
178
  expect(typeof d2.error).toBe("string");
177
179
  });
180
+
181
+ /***************************************************************
182
+ * pick_view_mode()
183
+ ***************************************************************/
184
+ const MODES = ["text", "tree", "graph"];
185
+
186
+ test("pick_view_mode: the tree when nothing is said", () => {
187
+ expect(pick_view_mode("", "", MODES, "tree")).toBe("tree");
188
+ expect(pick_view_mode(undefined, undefined, MODES, "tree")).toBe("tree");
189
+ expect(pick_view_mode(null, null, MODES, "tree")).toBe("tree");
190
+ });
191
+
192
+ test("pick_view_mode: what the reader chose last wins over the fallback", () => {
193
+ expect(pick_view_mode("", "graph", MODES, "tree")).toBe("graph");
194
+ expect(pick_view_mode("", "text", MODES, "tree")).toBe("text");
195
+ });
196
+
197
+ test("pick_view_mode: the host outranks the memory", () => {
198
+ expect(pick_view_mode("text", "graph", MODES, "tree")).toBe("text");
199
+ /* And "tree" asked for explicitly is a choice, not a default:
200
+ * it has to beat a remembered graph. */
201
+ expect(pick_view_mode("tree", "graph", MODES, "tree")).toBe("tree");
202
+ });
203
+
204
+ test("pick_view_mode: a mode no longer offered is treated as unsaid", () => {
205
+ /* A string left in the store by an older release. */
206
+ expect(pick_view_mode("", "webix", MODES, "tree")).toBe("tree");
207
+ expect(pick_view_mode("webix", "graph", MODES, "tree")).toBe("graph");
208
+ });
209
+
210
+ test("pick_view_mode: no mode table at all still answers the fallback", () => {
211
+ expect(pick_view_mode("graph", "text", undefined, "tree")).toBe("tree");
212
+ expect(pick_view_mode("graph", "text", [], "tree")).toBe("tree");
213
+ });
214
+
215
+ /***************************************************************
216
+ * container_label()
217
+ ***************************************************************/
218
+ test("container_label: a dict with a scalar id is labelled by it", () => {
219
+ expect(container_label({id: "LAB-001-001-E-001", name: "x"}, 40))
220
+ .toBe("LAB-001-001-E-001");
221
+ expect(container_label({id: 42}, 40)).toBe("42");
222
+ expect(container_label({id: false}, 40)).toBe("false");
223
+ });
224
+
225
+ test("container_label: nothing to say without a scalar id", () => {
226
+ expect(container_label({name: "x"}, 40)).toBe("");
227
+ expect(container_label({}, 40)).toBe("");
228
+ expect(container_label({id: {a: 1}}, 40)).toBe("");
229
+ expect(container_label({id: [1, 2]}, 40)).toBe("");
230
+ expect(container_label({id: null}, 40)).toBe("");
231
+ expect(container_label({id: " "}, 40)).toBe("");
232
+ });
233
+
234
+ test("container_label: an array is not a record", () => {
235
+ expect(container_label([{id: "a"}], 40)).toBe("");
236
+ expect(container_label(null, 40)).toBe("");
237
+ expect(container_label("id", 40)).toBe("");
238
+ });
239
+
240
+ test("container_label: cut at max_chars, and said with an ellipsis", () => {
241
+ let uuid = "ff14fc16-2cb2-472c-9c0a-89fa43837130";
242
+ expect(container_label({id: uuid}, 8)).toBe("ff14fc16…");
243
+ expect(container_label({id: uuid}, 0)).toBe(uuid);
244
+ expect(container_label({id: uuid}, undefined)).toBe(uuid);
245
+ });
@@ -6,6 +6,15 @@
6
6
  width: 100%;
7
7
  height: auto;
8
8
  position: relative;
9
+ /* The toolbar is often a flex item sharing its row with a pinned
10
+ * control (a back link, a title). A flex item's `min-width:auto`
11
+ * resolves to the CONTENT minimum, and the sections inside are
12
+ * `flex-shrink:0; white-space:nowrap` — so the toolbar refuses to
13
+ * shrink, the row overflows, and the scroll container never sees an
14
+ * overflow of its OWN: both arrows stay hidden while the items are
15
+ * clipped by an ancestor. Alone on its row nothing shrinks and the
16
+ * bug hides, which is why it only shows up once integrated. */
17
+ min-width: 0;
9
18
  }
10
19
 
11
20
  .yui-horizontal-toolbar-container {