@yuneta/gobj-ui 7.23.16 → 7.23.18

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.
@@ -10080,16 +10080,39 @@ var PRIVATE_DATA$11 = {
10080
10080
  var __gclass__$11 = null;
10081
10081
  var __layout_registered__$1 = false;
10082
10082
  /***************************************************************
10083
- * Type colors matching the original mx_json_viewer
10084
- ***************************************************************/
10085
- var TYPE_COLORS = {
10083
+ * Type colours, ONE EXPLICIT PALETTE PER THEME and not a
10084
+ * derivation, for the same reason the tree keeps two sets of
10085
+ * CSS tokens: a colour that reads on white is not the same
10086
+ * colour lightened, and deriving one from the other made the
10087
+ * two views of one document disagree about what a string is.
10088
+ * These ARE the tree's tokens (`c_yui_json.css`), so the tree
10089
+ * and the graph now say green with the same green.
10090
+ *
10091
+ * The light values are darker than the `mx_json_viewer` ones
10092
+ * they came from, because those could not be read: `#FF8C00`
10093
+ * is mid-luminance, so it reaches at most **2.33:1 against any
10094
+ * background that exists** -- no card colour and no highlight
10095
+ * could have fixed it, only the colour itself. Orange and blue
10096
+ * moved until every type clears 4.5:1 on all five surfaces this
10097
+ * viewer paints (two light cards, the match chip, two dark
10098
+ * cards); red and green were already there.
10099
+ ***************************************************************/
10100
+ var TYPE_COLORS_LIGHT = {
10086
10101
  string: "#006000",
10087
- number: "#EE422E",
10088
- boolean: "#FF8C00",
10089
- null: "#475ED0",
10102
+ number: "#b5330b",
10103
+ boolean: "#8C4D00",
10104
+ null: "#4359C6",
10090
10105
  list: "#4C0099",
10091
10106
  dict: "#4C0099"
10092
10107
  };
10108
+ var TYPE_COLORS_DARK = {
10109
+ string: "#7ec87e",
10110
+ number: "#ff8a65",
10111
+ boolean: "#ffb74d",
10112
+ null: "#90a4f4",
10113
+ list: "#B799D6",
10114
+ dict: "#B799D6"
10115
+ };
10093
10116
  /***************************************************************
10094
10117
  * Group (card) colours. `fill` is the light-theme card; `tint`
10095
10118
  * is the hue that fill hints at, used to derive the dark card —
@@ -10110,31 +10133,48 @@ var GROUP_COLORS = {
10110
10133
  };
10111
10134
  /***************************************************************
10112
10135
  * 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).
10136
+ * gobj-tree's role_card_style(): a near-neutral fill and a
10137
+ * group-colour border.
10138
+ *
10139
+ * The SURFACE stays out of the hue, on both themes, and that is
10140
+ * the whole rule here. The light card is near-white and lets
10141
+ * saturated dark text carry the colour; the dark card is its
10142
+ * mirror -- a near-neutral dark surface under bright text -- and
10143
+ * the group's hue lives in the border and the header bar, which
10144
+ * is where the light theme had always put it.
10145
+ *
10146
+ * It shipped tinting the dark surface 30% instead, and that put
10147
+ * green values on a green card. Worse, `list` (a yellow tint)
10148
+ * landed mid-luminance, a muddy olive where nothing contrasted
10149
+ * with anything: its purple values measured 1.60:1, against 11.48
10150
+ * for the same values on the light card. The 12% left is enough
10151
+ * to tell a dict card from a list one and not enough to fight the
10152
+ * text.
10116
10153
  *
10117
10154
  * The header bar is always the border colour, so its text flips:
10118
10155
  * white on the dark-teal light bar, near-black on the brightened
10119
- * dark one.
10156
+ * dark one. The dark bar is mixed lighter than the border used
10157
+ * to be so that near-black reads on it (6.87:1, which is what
10158
+ * white gets on the light bar).
10120
10159
  ***************************************************************/
10121
10160
  function json_card_style(group, dark) {
10122
10161
  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,
10162
+ bg: dark ? `color-mix(in srgb, ${group.tint} 12%, #1E242E)` : group.fill,
10163
+ border: dark ? `color-mix(in srgb, ${group.stroke} 55%, #ffffff)` : group.stroke,
10125
10164
  header_fg: dark ? "#12181f" : "#ffffff",
10126
10165
  key: dark ? "#c9cfd8" : "#1A1A1A"
10127
10166
  };
10128
10167
  }
10129
10168
  /***************************************************************
10130
- * A scalar's colour by type. The palette is tuned for a light
10131
- * card; on a dark one every one of them (dark green #006000,
10132
- * blue #475ED0, …) sinks into the background, so brighten it.
10169
+ * A scalar's colour by type, straight out of the palette for
10170
+ * the theme. It used to be the light colour mixed with white,
10171
+ * which is how a document ended up green in one view and a
10172
+ * paler green in the other, and how the amount of white became
10173
+ * a knob nobody could set: enough of it to read on a dark card
10174
+ * washed the six types into one pastel.
10133
10175
  ***************************************************************/
10134
10176
  function type_color(type, dark) {
10135
- let color = TYPE_COLORS[type] || (dark ? "#e8eaed" : "black");
10136
- if (!dark) return color;
10137
- return `color-mix(in srgb, ${color} 55%, #ffffff)`;
10177
+ return (dark ? TYPE_COLORS_DARK : TYPE_COLORS_LIGHT)[type] || (dark ? "#e8eaed" : "black");
10138
10178
  }
10139
10179
  /******************************
10140
10180
  * Framework Methods
@@ -10431,8 +10471,9 @@ function build_graph$3(gobj) {
10431
10471
  * Build cell value HTML (matching old mx_json_viewer colors)
10432
10472
  ************************************************************/
10433
10473
  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;
10474
+ let on_light = matched ? false : dark;
10475
+ let color = type_color(type, on_light);
10476
+ let key_color = json_card_style(GROUP_COLORS.dict, on_light).key;
10436
10477
  let display_value = "";
10437
10478
  switch (type) {
10438
10479
  case "string":
@@ -10456,7 +10497,7 @@ function build_cell_html(key, value, type, dark, matched) {
10456
10497
  default: display_value = String(value);
10457
10498
  }
10458
10499
  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>`;
10500
+ if (matched) row = `<span style="background:#ffe082; border-radius:2px; padding:0 2px;">${row}</span>`;
10460
10501
  return row;
10461
10502
  }
10462
10503
  /************************************************************
@@ -11341,6 +11382,56 @@ function json_text_dump(root, max_chars) {
11341
11382
  capped: false
11342
11383
  };
11343
11384
  }
11385
+ /************************************************************
11386
+ * Which view a freshly mounted viewer opens on.
11387
+ *
11388
+ * Three answers, in order of authority: what the HOST asked
11389
+ * for, what the READER chose last time, and `fallback`.
11390
+ *
11391
+ * The host wins because a viewer mounted to show a graph has
11392
+ * to show one. The memory comes next, because reopening a
11393
+ * document in the view you just left is the whole point of
11394
+ * remembering it. `fallback` is the floor.
11395
+ *
11396
+ * Anything not in `modes` is treated as unsaid, which covers
11397
+ * an absent value, an empty one, and a stored string from an
11398
+ * older release that named a view no longer offered.
11399
+ ************************************************************/
11400
+ function pick_view_mode(asked, remembered, modes, fallback) {
11401
+ let known = (m) => Array.isArray(modes) && modes.indexOf(m) >= 0;
11402
+ if (known(asked)) return asked;
11403
+ if (known(remembered)) return remembered;
11404
+ return fallback;
11405
+ }
11406
+ /************************************************************
11407
+ * The short label a container row carries beside its size.
11408
+ *
11409
+ * An array of dicts that carry an `id` is a list of RECORDS,
11410
+ * and a row that says only `2: {15}` makes you open all
11411
+ * fifteen fields of every one of them to find out WHICH
11412
+ * record it is. The id is the one field that answers that,
11413
+ * so the row says it without being opened.
11414
+ *
11415
+ * Only a SCALAR id, and only when it is really there: an `id`
11416
+ * that is itself a dict or a list is not a name, it is more
11417
+ * document, and printing `[object Object]` beside the size
11418
+ * would be worse than printing nothing.
11419
+ *
11420
+ * Cut at `max_chars`, because this rides on a row whose job
11421
+ * is to stay one line.
11422
+ ************************************************************/
11423
+ function container_label(value, max_chars) {
11424
+ if (value === null || typeof value !== "object" || Array.isArray(value)) return "";
11425
+ if (!Object.prototype.hasOwnProperty.call(value, "id")) return "";
11426
+ let id = value.id;
11427
+ let t = typeof id;
11428
+ if (t !== "string" && t !== "number" && t !== "boolean") return "";
11429
+ let text = String(id).trim();
11430
+ if (text === "") return "";
11431
+ let cap = typeof max_chars === "number" && max_chars > 0 ? max_chars : 0;
11432
+ if (cap && text.length > cap) return text.slice(0, cap) + "…";
11433
+ return text;
11434
+ }
11344
11435
  //#endregion
11345
11436
  //#region src/c_yui_json.js
11346
11437
  /***********************************************************************
@@ -11371,7 +11462,9 @@ function json_text_dump(root, max_chars) {
11371
11462
  *
11372
11463
  * THREE VIEWS over the same working document, chosen by the
11373
11464
  * `view_mode` attr ("tree" | "text" | "graph") and switched from the
11374
- * toolbar. They answer three different questions:
11465
+ * toolbar. The viewer opens on what the host asked for, else on the
11466
+ * view the reader chose last (kept in localStorage), else on the tree.
11467
+ * They answer three different questions:
11375
11468
  *
11376
11469
  * tree where is this value, and what is around it — the lazy
11377
11470
  * view above, the only one that can drill.
@@ -11405,6 +11498,7 @@ function json_text_dump(root, max_chars) {
11405
11498
  var GCLASS_NAME$10 = "C_YUI_JSON";
11406
11499
  var MAX_RENDER_ROWS = 5e3;
11407
11500
  var MAX_TEXT_CHARS = 2e6;
11501
+ var MAX_LABEL_CHARS = 40;
11408
11502
  var VIEWS = [
11409
11503
  {
11410
11504
  mode: "text",
@@ -11422,6 +11516,7 @@ var VIEWS = [
11422
11516
  key: "graph view"
11423
11517
  }
11424
11518
  ];
11519
+ var VIEW_MODE_STORAGE_KEY = "yui_json_view_mode";
11425
11520
  /***************************************************************
11426
11521
  * Data
11427
11522
  ***************************************************************/
@@ -11429,7 +11524,7 @@ var attrs_table$10 = [
11429
11524
  (0, _yuneta_gobj_js.SDATA)(_yuneta_gobj_js.data_type_t.DTP_POINTER, "subscriber", 0, null, "Subscriber of output events"),
11430
11525
  (0, _yuneta_gobj_js.SDATA)(_yuneta_gobj_js.data_type_t.DTP_STRING, "title", 0, "", "Optional header title (i18n key)"),
11431
11526
  (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'"),
11527
+ (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
11528
  (0, _yuneta_gobj_js.SDATA)(_yuneta_gobj_js.data_type_t.DTP_POINTER, "$container", 0, null, "HTMLElement root, mounted by the parent"),
11434
11529
  (0, _yuneta_gobj_js.SDATA_END)()
11435
11530
  ];
@@ -11466,6 +11561,7 @@ function mt_create$10(gobj) {
11466
11561
  let json_data = (0, _yuneta_gobj_js.gobj_read_attr)(gobj, "json_data");
11467
11562
  if (json_data !== null && json_data !== void 0) priv.root = (0, _yuneta_gobj_js.json_deep_copy)(json_data);
11468
11563
  build_ui$9(gobj);
11564
+ resolve_initial_view_mode(gobj);
11469
11565
  apply_view_mode(gobj);
11470
11566
  let subscriber = (0, _yuneta_gobj_js.gobj_read_pointer_attr)(gobj, "subscriber");
11471
11567
  if (!subscriber) subscriber = (0, _yuneta_gobj_js.gobj_parent)(gobj);
@@ -11723,6 +11819,27 @@ function view_label(mode) {
11723
11819
  }
11724
11820
  }
11725
11821
  /************************************************************
11822
+ * The view the viewer OPENS on.
11823
+ *
11824
+ * Three answers, in order of authority: what the HOST asked for, what
11825
+ * the READER chose last time, and the tree. The host wins because a
11826
+ * viewer mounted to show a graph has to show one; the memory comes
11827
+ * next, because reopening a document in the view you just left is the
11828
+ * whole point of remembering it; the tree is the floor.
11829
+ *
11830
+ * This is why the attr no longer declares "tree" as its default: as a
11831
+ * default and as a host's explicit choice it was the same string, so
11832
+ * nothing could tell "show me the tree" from "I have no opinion", and
11833
+ * a memory that cannot see the difference has to lose to both.
11834
+ *
11835
+ * Resolved ONCE here and written into the attr, so every later read
11836
+ * is a plain attr read and no render touches the store.
11837
+ ************************************************************/
11838
+ function resolve_initial_view_mode(gobj) {
11839
+ 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");
11840
+ (0, _yuneta_gobj_js.gobj_write_attr)(gobj, "view_mode", mode);
11841
+ }
11842
+ /************************************************************
11726
11843
  * The current view, normalised. Anything unknown is the tree.
11727
11844
  ************************************************************/
11728
11845
  function current_view_mode(gobj) {
@@ -11972,7 +12089,7 @@ function push_container_rows(ctx, key, value, segments, depth, rows, key_match)
11972
12089
  let searching_match = ctx.term && !key_match && subtree_matches(value, ctx.term);
11973
12090
  let open = priv.expanded.has(path) || searching_match;
11974
12091
  ctx.count++;
11975
- rows.push(toggle_row(ctx.gobj, key, size, is_object, depth, path, open));
12092
+ rows.push(toggle_row(ctx.gobj, key, size, is_object, depth, path, open, value));
11976
12093
  if (!open) return;
11977
12094
  if (is_object) for (let [k, v] of Object.entries(value)) push_entry_rows(ctx, k, v, segments.concat(k), depth + 1, rows);
11978
12095
  else value.forEach(function(v, i) {
@@ -12057,7 +12174,7 @@ function push_collapsed_row(ctx, value, segments, depth, key, rows) {
12057
12174
  /************************************************************
12058
12175
  * Expandable container header row
12059
12176
  ************************************************************/
12060
- function toggle_row(gobj, key, size, is_object, depth, path, open) {
12177
+ function toggle_row(gobj, key, size, is_object, depth, path, open, value) {
12061
12178
  let summary = (is_object ? "{" : "[") + String(size) + (is_object ? "}" : "]");
12062
12179
  let body = [];
12063
12180
  if (key !== null && key !== "") {
@@ -12077,6 +12194,12 @@ function toggle_row(gobj, key, size, is_object, depth, path, open) {
12077
12194
  { class: "JSON_SUMMARY has-text-grey" },
12078
12195
  summary
12079
12196
  ]);
12197
+ let label = container_label(value, MAX_LABEL_CHARS);
12198
+ if (label) body.push([
12199
+ "span",
12200
+ { class: "JSON_SUMMARY_ID ml-2" },
12201
+ label
12202
+ ]);
12080
12203
  return [
12081
12204
  "div",
12082
12205
  {
@@ -12330,6 +12453,8 @@ function ac_copy_all(gobj, event, kw, src) {
12330
12453
  * With no mode it ADVANCES to the next view in VIEWS order, which
12331
12454
  * is what the two-view toggle did when the list was two long. The
12332
12455
  * toolbar buttons always name their mode.
12456
+ *
12457
+ * Whatever lands here is REMEMBERED, so the next viewer opens on it.
12333
12458
  ************************************************************/
12334
12459
  function ac_set_view_mode(gobj, event, kw, src) {
12335
12460
  let mode = kw.mode;
@@ -12340,6 +12465,7 @@ function ac_set_view_mode(gobj, event, kw, src) {
12340
12465
  }
12341
12466
  if (mode === current_view_mode(gobj)) return 0;
12342
12467
  (0, _yuneta_gobj_js.gobj_write_attr)(gobj, "view_mode", mode);
12468
+ (0, _yuneta_gobj_js.kw_set_local_storage_value)(VIEW_MODE_STORAGE_KEY, mode);
12343
12469
  apply_view_mode(gobj);
12344
12470
  render_view(gobj);
12345
12471
  return 0;
@@ -10075,16 +10075,39 @@ var PRIVATE_DATA$11 = {
10075
10075
  var __gclass__$11 = null;
10076
10076
  var __layout_registered__$1 = false;
10077
10077
  /***************************************************************
10078
- * Type colors matching the original mx_json_viewer
10079
- ***************************************************************/
10080
- var TYPE_COLORS = {
10078
+ * Type colours, ONE EXPLICIT PALETTE PER THEME and not a
10079
+ * derivation, for the same reason the tree keeps two sets of
10080
+ * CSS tokens: a colour that reads on white is not the same
10081
+ * colour lightened, and deriving one from the other made the
10082
+ * two views of one document disagree about what a string is.
10083
+ * These ARE the tree's tokens (`c_yui_json.css`), so the tree
10084
+ * and the graph now say green with the same green.
10085
+ *
10086
+ * The light values are darker than the `mx_json_viewer` ones
10087
+ * they came from, because those could not be read: `#FF8C00`
10088
+ * is mid-luminance, so it reaches at most **2.33:1 against any
10089
+ * background that exists** -- no card colour and no highlight
10090
+ * could have fixed it, only the colour itself. Orange and blue
10091
+ * moved until every type clears 4.5:1 on all five surfaces this
10092
+ * viewer paints (two light cards, the match chip, two dark
10093
+ * cards); red and green were already there.
10094
+ ***************************************************************/
10095
+ var TYPE_COLORS_LIGHT = {
10081
10096
  string: "#006000",
10082
- number: "#EE422E",
10083
- boolean: "#FF8C00",
10084
- null: "#475ED0",
10097
+ number: "#b5330b",
10098
+ boolean: "#8C4D00",
10099
+ null: "#4359C6",
10085
10100
  list: "#4C0099",
10086
10101
  dict: "#4C0099"
10087
10102
  };
10103
+ var TYPE_COLORS_DARK = {
10104
+ string: "#7ec87e",
10105
+ number: "#ff8a65",
10106
+ boolean: "#ffb74d",
10107
+ null: "#90a4f4",
10108
+ list: "#B799D6",
10109
+ dict: "#B799D6"
10110
+ };
10088
10111
  /***************************************************************
10089
10112
  * Group (card) colours. `fill` is the light-theme card; `tint`
10090
10113
  * is the hue that fill hints at, used to derive the dark card —
@@ -10105,31 +10128,48 @@ var GROUP_COLORS = {
10105
10128
  };
10106
10129
  /***************************************************************
10107
10130
  * 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).
10131
+ * gobj-tree's role_card_style(): a near-neutral fill and a
10132
+ * group-colour border.
10133
+ *
10134
+ * The SURFACE stays out of the hue, on both themes, and that is
10135
+ * the whole rule here. The light card is near-white and lets
10136
+ * saturated dark text carry the colour; the dark card is its
10137
+ * mirror -- a near-neutral dark surface under bright text -- and
10138
+ * the group's hue lives in the border and the header bar, which
10139
+ * is where the light theme had always put it.
10140
+ *
10141
+ * It shipped tinting the dark surface 30% instead, and that put
10142
+ * green values on a green card. Worse, `list` (a yellow tint)
10143
+ * landed mid-luminance, a muddy olive where nothing contrasted
10144
+ * with anything: its purple values measured 1.60:1, against 11.48
10145
+ * for the same values on the light card. The 12% left is enough
10146
+ * to tell a dict card from a list one and not enough to fight the
10147
+ * text.
10111
10148
  *
10112
10149
  * The header bar is always the border colour, so its text flips:
10113
10150
  * white on the dark-teal light bar, near-black on the brightened
10114
- * dark one.
10151
+ * dark one. The dark bar is mixed lighter than the border used
10152
+ * to be so that near-black reads on it (6.87:1, which is what
10153
+ * white gets on the light bar).
10115
10154
  ***************************************************************/
10116
10155
  function json_card_style(group, dark) {
10117
10156
  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,
10157
+ bg: dark ? `color-mix(in srgb, ${group.tint} 12%, #1E242E)` : group.fill,
10158
+ border: dark ? `color-mix(in srgb, ${group.stroke} 55%, #ffffff)` : group.stroke,
10120
10159
  header_fg: dark ? "#12181f" : "#ffffff",
10121
10160
  key: dark ? "#c9cfd8" : "#1A1A1A"
10122
10161
  };
10123
10162
  }
10124
10163
  /***************************************************************
10125
- * A scalar's colour by type. The palette is tuned for a light
10126
- * card; on a dark one every one of them (dark green #006000,
10127
- * blue #475ED0, …) sinks into the background, so brighten it.
10164
+ * A scalar's colour by type, straight out of the palette for
10165
+ * the theme. It used to be the light colour mixed with white,
10166
+ * which is how a document ended up green in one view and a
10167
+ * paler green in the other, and how the amount of white became
10168
+ * a knob nobody could set: enough of it to read on a dark card
10169
+ * washed the six types into one pastel.
10128
10170
  ***************************************************************/
10129
10171
  function type_color(type, dark) {
10130
- let color = TYPE_COLORS[type] || (dark ? "#e8eaed" : "black");
10131
- if (!dark) return color;
10132
- return `color-mix(in srgb, ${color} 55%, #ffffff)`;
10172
+ return (dark ? TYPE_COLORS_DARK : TYPE_COLORS_LIGHT)[type] || (dark ? "#e8eaed" : "black");
10133
10173
  }
10134
10174
  /******************************
10135
10175
  * Framework Methods
@@ -10426,8 +10466,9 @@ function build_graph$3(gobj) {
10426
10466
  * Build cell value HTML (matching old mx_json_viewer colors)
10427
10467
  ************************************************************/
10428
10468
  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;
10469
+ let on_light = matched ? false : dark;
10470
+ let color = type_color(type, on_light);
10471
+ let key_color = json_card_style(GROUP_COLORS.dict, on_light).key;
10431
10472
  let display_value = "";
10432
10473
  switch (type) {
10433
10474
  case "string":
@@ -10451,7 +10492,7 @@ function build_cell_html(key, value, type, dark, matched) {
10451
10492
  default: display_value = String(value);
10452
10493
  }
10453
10494
  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>`;
10495
+ if (matched) row = `<span style="background:#ffe082; border-radius:2px; padding:0 2px;">${row}</span>`;
10455
10496
  return row;
10456
10497
  }
10457
10498
  /************************************************************
@@ -11336,6 +11377,56 @@ function json_text_dump(root, max_chars) {
11336
11377
  capped: false
11337
11378
  };
11338
11379
  }
11380
+ /************************************************************
11381
+ * Which view a freshly mounted viewer opens on.
11382
+ *
11383
+ * Three answers, in order of authority: what the HOST asked
11384
+ * for, what the READER chose last time, and `fallback`.
11385
+ *
11386
+ * The host wins because a viewer mounted to show a graph has
11387
+ * to show one. The memory comes next, because reopening a
11388
+ * document in the view you just left is the whole point of
11389
+ * remembering it. `fallback` is the floor.
11390
+ *
11391
+ * Anything not in `modes` is treated as unsaid, which covers
11392
+ * an absent value, an empty one, and a stored string from an
11393
+ * older release that named a view no longer offered.
11394
+ ************************************************************/
11395
+ function pick_view_mode(asked, remembered, modes, fallback) {
11396
+ let known = (m) => Array.isArray(modes) && modes.indexOf(m) >= 0;
11397
+ if (known(asked)) return asked;
11398
+ if (known(remembered)) return remembered;
11399
+ return fallback;
11400
+ }
11401
+ /************************************************************
11402
+ * The short label a container row carries beside its size.
11403
+ *
11404
+ * An array of dicts that carry an `id` is a list of RECORDS,
11405
+ * and a row that says only `2: {15}` makes you open all
11406
+ * fifteen fields of every one of them to find out WHICH
11407
+ * record it is. The id is the one field that answers that,
11408
+ * so the row says it without being opened.
11409
+ *
11410
+ * Only a SCALAR id, and only when it is really there: an `id`
11411
+ * that is itself a dict or a list is not a name, it is more
11412
+ * document, and printing `[object Object]` beside the size
11413
+ * would be worse than printing nothing.
11414
+ *
11415
+ * Cut at `max_chars`, because this rides on a row whose job
11416
+ * is to stay one line.
11417
+ ************************************************************/
11418
+ function container_label(value, max_chars) {
11419
+ if (value === null || typeof value !== "object" || Array.isArray(value)) return "";
11420
+ if (!Object.prototype.hasOwnProperty.call(value, "id")) return "";
11421
+ let id = value.id;
11422
+ let t = typeof id;
11423
+ if (t !== "string" && t !== "number" && t !== "boolean") return "";
11424
+ let text = String(id).trim();
11425
+ if (text === "") return "";
11426
+ let cap = typeof max_chars === "number" && max_chars > 0 ? max_chars : 0;
11427
+ if (cap && text.length > cap) return text.slice(0, cap) + "…";
11428
+ return text;
11429
+ }
11339
11430
  //#endregion
11340
11431
  //#region src/c_yui_json.js
11341
11432
  /***********************************************************************
@@ -11366,7 +11457,9 @@ function json_text_dump(root, max_chars) {
11366
11457
  *
11367
11458
  * THREE VIEWS over the same working document, chosen by the
11368
11459
  * `view_mode` attr ("tree" | "text" | "graph") and switched from the
11369
- * toolbar. They answer three different questions:
11460
+ * toolbar. The viewer opens on what the host asked for, else on the
11461
+ * view the reader chose last (kept in localStorage), else on the tree.
11462
+ * They answer three different questions:
11370
11463
  *
11371
11464
  * tree where is this value, and what is around it — the lazy
11372
11465
  * view above, the only one that can drill.
@@ -11400,6 +11493,7 @@ function json_text_dump(root, max_chars) {
11400
11493
  var GCLASS_NAME$10 = "C_YUI_JSON";
11401
11494
  var MAX_RENDER_ROWS = 5e3;
11402
11495
  var MAX_TEXT_CHARS = 2e6;
11496
+ var MAX_LABEL_CHARS = 40;
11403
11497
  var VIEWS = [
11404
11498
  {
11405
11499
  mode: "text",
@@ -11417,6 +11511,7 @@ var VIEWS = [
11417
11511
  key: "graph view"
11418
11512
  }
11419
11513
  ];
11514
+ var VIEW_MODE_STORAGE_KEY = "yui_json_view_mode";
11420
11515
  /***************************************************************
11421
11516
  * Data
11422
11517
  ***************************************************************/
@@ -11424,7 +11519,7 @@ var attrs_table$10 = [
11424
11519
  SDATA(data_type_t.DTP_POINTER, "subscriber", 0, null, "Subscriber of output events"),
11425
11520
  SDATA(data_type_t.DTP_STRING, "title", 0, "", "Optional header title (i18n key)"),
11426
11521
  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'"),
11522
+ 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
11523
  SDATA(data_type_t.DTP_POINTER, "$container", 0, null, "HTMLElement root, mounted by the parent"),
11429
11524
  SDATA_END()
11430
11525
  ];
@@ -11461,6 +11556,7 @@ function mt_create$10(gobj) {
11461
11556
  let json_data = gobj_read_attr(gobj, "json_data");
11462
11557
  if (json_data !== null && json_data !== void 0) priv.root = json_deep_copy(json_data);
11463
11558
  build_ui$9(gobj);
11559
+ resolve_initial_view_mode(gobj);
11464
11560
  apply_view_mode(gobj);
11465
11561
  let subscriber = gobj_read_pointer_attr(gobj, "subscriber");
11466
11562
  if (!subscriber) subscriber = gobj_parent(gobj);
@@ -11718,6 +11814,27 @@ function view_label(mode) {
11718
11814
  }
11719
11815
  }
11720
11816
  /************************************************************
11817
+ * The view the viewer OPENS on.
11818
+ *
11819
+ * Three answers, in order of authority: what the HOST asked for, what
11820
+ * the READER chose last time, and the tree. The host wins because a
11821
+ * viewer mounted to show a graph has to show one; the memory comes
11822
+ * next, because reopening a document in the view you just left is the
11823
+ * whole point of remembering it; the tree is the floor.
11824
+ *
11825
+ * This is why the attr no longer declares "tree" as its default: as a
11826
+ * default and as a host's explicit choice it was the same string, so
11827
+ * nothing could tell "show me the tree" from "I have no opinion", and
11828
+ * a memory that cannot see the difference has to lose to both.
11829
+ *
11830
+ * Resolved ONCE here and written into the attr, so every later read
11831
+ * is a plain attr read and no render touches the store.
11832
+ ************************************************************/
11833
+ function resolve_initial_view_mode(gobj) {
11834
+ 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");
11835
+ gobj_write_attr(gobj, "view_mode", mode);
11836
+ }
11837
+ /************************************************************
11721
11838
  * The current view, normalised. Anything unknown is the tree.
11722
11839
  ************************************************************/
11723
11840
  function current_view_mode(gobj) {
@@ -11967,7 +12084,7 @@ function push_container_rows(ctx, key, value, segments, depth, rows, key_match)
11967
12084
  let searching_match = ctx.term && !key_match && subtree_matches(value, ctx.term);
11968
12085
  let open = priv.expanded.has(path) || searching_match;
11969
12086
  ctx.count++;
11970
- rows.push(toggle_row(ctx.gobj, key, size, is_object, depth, path, open));
12087
+ rows.push(toggle_row(ctx.gobj, key, size, is_object, depth, path, open, value));
11971
12088
  if (!open) return;
11972
12089
  if (is_object) for (let [k, v] of Object.entries(value)) push_entry_rows(ctx, k, v, segments.concat(k), depth + 1, rows);
11973
12090
  else value.forEach(function(v, i) {
@@ -12052,7 +12169,7 @@ function push_collapsed_row(ctx, value, segments, depth, key, rows) {
12052
12169
  /************************************************************
12053
12170
  * Expandable container header row
12054
12171
  ************************************************************/
12055
- function toggle_row(gobj, key, size, is_object, depth, path, open) {
12172
+ function toggle_row(gobj, key, size, is_object, depth, path, open, value) {
12056
12173
  let summary = (is_object ? "{" : "[") + String(size) + (is_object ? "}" : "]");
12057
12174
  let body = [];
12058
12175
  if (key !== null && key !== "") {
@@ -12072,6 +12189,12 @@ function toggle_row(gobj, key, size, is_object, depth, path, open) {
12072
12189
  { class: "JSON_SUMMARY has-text-grey" },
12073
12190
  summary
12074
12191
  ]);
12192
+ let label = container_label(value, MAX_LABEL_CHARS);
12193
+ if (label) body.push([
12194
+ "span",
12195
+ { class: "JSON_SUMMARY_ID ml-2" },
12196
+ label
12197
+ ]);
12075
12198
  return [
12076
12199
  "div",
12077
12200
  {
@@ -12325,6 +12448,8 @@ function ac_copy_all(gobj, event, kw, src) {
12325
12448
  * With no mode it ADVANCES to the next view in VIEWS order, which
12326
12449
  * is what the two-view toggle did when the list was two long. The
12327
12450
  * toolbar buttons always name their mode.
12451
+ *
12452
+ * Whatever lands here is REMEMBERED, so the next viewer opens on it.
12328
12453
  ************************************************************/
12329
12454
  function ac_set_view_mode(gobj, event, kw, src) {
12330
12455
  let mode = kw.mode;
@@ -12335,6 +12460,7 @@ function ac_set_view_mode(gobj, event, kw, src) {
12335
12460
  }
12336
12461
  if (mode === current_view_mode(gobj)) return 0;
12337
12462
  gobj_write_attr(gobj, "view_mode", mode);
12463
+ kw_set_local_storage_value(VIEW_MODE_STORAGE_KEY, mode);
12338
12464
  apply_view_mode(gobj);
12339
12465
  render_view(gobj);
12340
12466
  return 0;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yuneta/gobj-ui",
3
- "version": "7.23.16",
3
+ "version": "7.23.18",
4
4
  "type": "module",
5
5
  "main": "dist/gobj-ui.cjs.js",
6
6
  "module": "dist/gobj-ui.es.js",
@@ -9,11 +9,20 @@
9
9
  * All Rights Reserved.
10
10
  ***********************************************************************/
11
11
 
12
+ /*
13
+ * Light values are darker than a naive orange/blue because they have to
14
+ * be READ, on the card and on the graph view's match chip alike. The
15
+ * ceiling is a fact of the colour, not of the surface: `#FF8C00`, which
16
+ * these came from, is mid-luminance and reaches at most 2.33:1 against
17
+ * ANY background that exists. The graph view carries the same two
18
+ * palettes (`c_yui_json_graph.js`), so one document says green with the
19
+ * same green in all three views.
20
+ */
12
21
  .C_YUI_JSON {
13
22
  --json-string: #006000;
14
23
  --json-number: #b5330b;
15
- --json-boolean: #b36b00;
16
- --json-null: #475ed0;
24
+ --json-boolean: #8C4D00;
25
+ --json-null: #4359C6;
17
26
  --json-key: var(--bulma-text-strong, #1a1a1a);
18
27
  --json-row-hover: rgba(0, 0, 0, 0.05);
19
28
  --json-guide: #b5b5b5;
@@ -172,6 +181,19 @@
172
181
  .C_YUI_JSON .JSON_TYPE_BOOLEAN { color: var(--json-boolean); }
173
182
  .C_YUI_JSON .JSON_TYPE_NULL { color: var(--json-null); font-style: italic; }
174
183
 
184
+ /*
185
+ * The id a container row carries beside its size. It is a VALUE, so
186
+ * it takes the string colour and follows the theme with it; what sets
187
+ * it apart from the values under it is the WEIGHT, not a colour of its
188
+ * own -- a sixth colour in a row that already carries five would be
189
+ * one more thing to decode, for a field whose whole job is to be read
190
+ * at a glance while scrolling.
191
+ */
192
+ .C_YUI_JSON .JSON_SUMMARY_ID {
193
+ color: var(--json-string);
194
+ font-weight: 600;
195
+ }
196
+
175
197
  .C_YUI_JSON .JSON_STUB {
176
198
  color: var(--bulma-link, #3273dc);
177
199
  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;
@@ -197,17 +197,41 @@ let __layout_registered__ = false;
197
197
 
198
198
 
199
199
  /***************************************************************
200
- * Type colors matching the original mx_json_viewer
200
+ * Type colours, ONE EXPLICIT PALETTE PER THEME and not a
201
+ * derivation, for the same reason the tree keeps two sets of
202
+ * CSS tokens: a colour that reads on white is not the same
203
+ * colour lightened, and deriving one from the other made the
204
+ * two views of one document disagree about what a string is.
205
+ * These ARE the tree's tokens (`c_yui_json.css`), so the tree
206
+ * and the graph now say green with the same green.
207
+ *
208
+ * The light values are darker than the `mx_json_viewer` ones
209
+ * they came from, because those could not be read: `#FF8C00`
210
+ * is mid-luminance, so it reaches at most **2.33:1 against any
211
+ * background that exists** -- no card colour and no highlight
212
+ * could have fixed it, only the colour itself. Orange and blue
213
+ * moved until every type clears 4.5:1 on all five surfaces this
214
+ * viewer paints (two light cards, the match chip, two dark
215
+ * cards); red and green were already there.
201
216
  ***************************************************************/
202
- const TYPE_COLORS = {
217
+ const TYPE_COLORS_LIGHT = {
203
218
  string: "#006000", // green
204
- number: "#EE422E", // red
205
- boolean: "#FF8C00", // orange
206
- null: "#475ED0", // blue
219
+ number: "#b5330b", // red
220
+ boolean: "#8C4D00", // orange
221
+ null: "#4359C6", // blue
207
222
  list: "#4C0099", // purple
208
223
  dict: "#4C0099", // purple
209
224
  };
210
225
 
226
+ const TYPE_COLORS_DARK = {
227
+ string: "#7ec87e",
228
+ number: "#ff8a65",
229
+ boolean: "#ffb74d",
230
+ null: "#90a4f4",
231
+ list: "#B799D6",
232
+ dict: "#B799D6",
233
+ };
234
+
211
235
  /***************************************************************
212
236
  * Group (card) colours. `fill` is the light-theme card; `tint`
213
237
  * is the hue that fill hints at, used to derive the dark card —
@@ -221,22 +245,38 @@ const GROUP_COLORS = {
221
245
 
222
246
  /***************************************************************
223
247
  * 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).
248
+ * gobj-tree's role_card_style(): a near-neutral fill and a
249
+ * group-colour border.
250
+ *
251
+ * The SURFACE stays out of the hue, on both themes, and that is
252
+ * the whole rule here. The light card is near-white and lets
253
+ * saturated dark text carry the colour; the dark card is its
254
+ * mirror -- a near-neutral dark surface under bright text -- and
255
+ * the group's hue lives in the border and the header bar, which
256
+ * is where the light theme had always put it.
257
+ *
258
+ * It shipped tinting the dark surface 30% instead, and that put
259
+ * green values on a green card. Worse, `list` (a yellow tint)
260
+ * landed mid-luminance, a muddy olive where nothing contrasted
261
+ * with anything: its purple values measured 1.60:1, against 11.48
262
+ * for the same values on the light card. The 12% left is enough
263
+ * to tell a dict card from a list one and not enough to fight the
264
+ * text.
227
265
  *
228
266
  * The header bar is always the border colour, so its text flips:
229
267
  * white on the dark-teal light bar, near-black on the brightened
230
- * dark one.
268
+ * dark one. The dark bar is mixed lighter than the border used
269
+ * to be so that near-black reads on it (6.87:1, which is what
270
+ * white gets on the light bar).
231
271
  ***************************************************************/
232
272
  function json_card_style(group, dark)
233
273
  {
234
274
  return {
235
275
  bg: dark
236
- ? `color-mix(in srgb, ${group.tint} 30%, #2c3542)`
276
+ ? `color-mix(in srgb, ${group.tint} 12%, #1E242E)`
237
277
  : group.fill,
238
278
  border: dark
239
- ? `color-mix(in srgb, ${group.stroke} 85%, #ffffff)`
279
+ ? `color-mix(in srgb, ${group.stroke} 55%, #ffffff)`
240
280
  : group.stroke,
241
281
  header_fg: dark ? "#12181f" : "#ffffff",
242
282
  key: dark ? "#c9cfd8" : "#1A1A1A",
@@ -244,17 +284,17 @@ function json_card_style(group, dark)
244
284
  }
245
285
 
246
286
  /***************************************************************
247
- * A scalar's colour by type. The palette is tuned for a light
248
- * card; on a dark one every one of them (dark green #006000,
249
- * blue #475ED0, …) sinks into the background, so brighten it.
287
+ * A scalar's colour by type, straight out of the palette for
288
+ * the theme. It used to be the light colour mixed with white,
289
+ * which is how a document ended up green in one view and a
290
+ * paler green in the other, and how the amount of white became
291
+ * a knob nobody could set: enough of it to read on a dark card
292
+ * washed the six types into one pastel.
250
293
  ***************************************************************/
251
294
  function type_color(type, dark)
252
295
  {
253
- let color = TYPE_COLORS[type] || (dark ? "#e8eaed" : "black");
254
- if(!dark) {
255
- return color;
256
- }
257
- return `color-mix(in srgb, ${color} 55%, #ffffff)`;
296
+ let palette = dark? TYPE_COLORS_DARK: TYPE_COLORS_LIGHT;
297
+ return palette[type] || (dark ? "#e8eaed" : "black");
258
298
  }
259
299
 
260
300
 
@@ -698,8 +738,18 @@ function build_graph(gobj)
698
738
  ************************************************************/
699
739
  function build_cell_html(key, value, type, dark, matched)
700
740
  {
701
- let color = type_color(type, dark);
702
- let key_color = json_card_style(GROUP_COLORS.dict, dark).key;
741
+ /*
742
+ * A match is a LIGHT chip on BOTH themes, so what sits on it is
743
+ * light-surface text -- the same flip the header bar does.
744
+ *
745
+ * A dark chip has no room to exist: dark enough for the brightened
746
+ * values to read on it is also dark enough to be invisible against
747
+ * the card, and the amber it used to be (#7a5d00) ended up failing
748
+ * at both ends, 2.52:1 under the text and 2.34:1 over the card.
749
+ */
750
+ let on_light = matched? false: dark;
751
+ let color = type_color(type, on_light);
752
+ let key_color = json_card_style(GROUP_COLORS.dict, on_light).key;
703
753
  let display_value = "";
704
754
 
705
755
  switch(type) {
@@ -734,7 +784,7 @@ function build_cell_html(key, value, type, dark, matched)
734
784
  * a G6 node state: the key shape of an `html` node is a DOM
735
785
  * element, and G6 paints no state style on it. Setting
736
786
  * 'active' here would select correctly and show nothing. */
737
- row = `<span style="background:${dark? "#7a5d00": "#ffe082"}; ` +
787
+ row = `<span style="background:#ffe082; ` +
738
788
  `border-radius:2px; padding:0 2px;">${row}</span>`;
739
789
  }
740
790
  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
+ });