@yuneta/gobj-ui 7.23.186 → 7.23.188

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.
@@ -335,12 +335,19 @@ function build_ui(gobj)
335
335
  }
336
336
  }],
337
337
  /* Landing view toggle: cards grid <-> schema graph
338
- * (topics as nodes). Shown only on the landing. */
338
+ * (topics as nodes). Shown only on the landing.
339
+ *
340
+ * The glyph is the draughtsman's compass, not the
341
+ * `hexagon-nodes` of the graph: the button one place
342
+ * to its left opens the graph of the DATA, and the
343
+ * two wore the same shape with only their labels
344
+ * telling them apart -- which on a narrow toolbar is
345
+ * nothing. A schema is the PLAN of the treedb. */
339
346
  ['button', {class: 'button TREEDB_LANDING_TOGGLE is-hidden',
340
347
  title: t('schema graph'), 'aria-label': t('schema graph'),
341
348
  'data-i18n-title': 'schema graph',
342
349
  'data-i18n-aria-label': 'schema graph'}, [
343
- ['span', {class: 'icon'}, [yui_toolbar_icon('yi-hexagon-nodes')]],
350
+ ['span', {class: 'icon'}, [yui_toolbar_icon('yi-compass-drafting')]],
344
351
  ['span', {i18n: 'schema'}, 'schema']
345
352
  ], {
346
353
  click: (evt) => {
@@ -348,6 +355,24 @@ function build_ui(gobj)
348
355
  gobj_send_event(gobj, "EV_TOGGLE_LANDING_VIEW", {}, gobj);
349
356
  }
350
357
  }],
358
+ /* The schema as JSON -- the `descs` the view is drawing,
359
+ * which is what a person writing a schema literal, or
360
+ * chasing a flag that did not take, actually needs to
361
+ * read. Landing-only, like the toggle it stands next
362
+ * to; the `raw json` button further right is another
363
+ * thing entirely (the tranger's content). */
364
+ ['button', {class: 'button TREEDB_SCHEMA_JSON_BTN is-hidden',
365
+ title: t('schema json'), 'aria-label': t('schema json'),
366
+ 'data-i18n-title': 'schema json',
367
+ 'data-i18n-aria-label': 'schema json'}, [
368
+ ['span', {class: 'icon'}, [yui_toolbar_icon('yi-code')]],
369
+ ['span', {i18n: 'schema json'}, 'schema json']
370
+ ], {
371
+ click: (evt) => {
372
+ evt.stopPropagation();
373
+ gobj_send_event(gobj, "EV_OPEN_SCHEMA_JSON", {}, gobj);
374
+ }
375
+ }],
351
376
  /* The backend this view browses. The tab that hosts this
352
377
  * view is labelled with the TREEDB name, and the same
353
378
  * treedb name lives on more than one node — two tabs
@@ -817,6 +842,7 @@ function show_topics_landing(gobj)
817
842
  let $info = $container.querySelector(".TREEDB_TOPIC_INFO");
818
843
  let $back = $container.querySelector(".TREEDB_TOPICS_BACK");
819
844
  let $toggle = $container.querySelector(".TREEDB_LANDING_TOGGLE");
845
+ let $schema_json = $container.querySelector(".TREEDB_SCHEMA_JSON_BTN");
820
846
  if($tabs) {
821
847
  $tabs.classList.add("is-hidden");
822
848
  }
@@ -835,6 +861,11 @@ function show_topics_landing(gobj)
835
861
  $toggle.classList.remove("is-hidden"); /* the toggle lives on the landing */
836
862
  $toggle.classList.toggle("is-primary", schema_mode);
837
863
  }
864
+ if($schema_json) {
865
+ /* The schema's json is worth reading when the schema is what is
866
+ * on screen; on the cards grid it is one more button in the way. */
867
+ $schema_json.classList.toggle("is-hidden", !schema_mode);
868
+ }
838
869
  if(schema_mode) {
839
870
  if($landing) {
840
871
  $landing.classList.add("is-hidden");
@@ -946,6 +977,7 @@ function show_topic_detail(gobj)
946
977
  let $info = $container.querySelector(".TREEDB_TOPIC_INFO");
947
978
  let $back = $container.querySelector(".TREEDB_TOPICS_BACK");
948
979
  let $toggle = $container.querySelector(".TREEDB_LANDING_TOGGLE");
980
+ let $schema_json = $container.querySelector(".TREEDB_SCHEMA_JSON_BTN");
949
981
  if($landing) {
950
982
  $landing.classList.add("is-hidden");
951
983
  }
@@ -955,6 +987,9 @@ function show_topic_detail(gobj)
955
987
  if($info) {
956
988
  $info.classList.add("is-hidden");
957
989
  }
990
+ if($schema_json) {
991
+ $schema_json.classList.add("is-hidden");
992
+ }
958
993
  if($toggle) {
959
994
  $toggle.classList.add("is-hidden"); /* landing-only */
960
995
  }
@@ -1010,12 +1045,16 @@ function show_topic_info(gobj, topic)
1010
1045
  let $schema = $container.querySelector(".TREEDB_TOPICS_SCHEMA");
1011
1046
  let $back = $container.querySelector(".TREEDB_TOPICS_BACK");
1012
1047
  let $toggle = $container.querySelector(".TREEDB_LANDING_TOGGLE");
1048
+ let $schema_json = $container.querySelector(".TREEDB_SCHEMA_JSON_BTN");
1013
1049
  if($landing) {
1014
1050
  $landing.classList.add("is-hidden");
1015
1051
  }
1016
1052
  if($schema) {
1017
1053
  $schema.classList.add("is-hidden");
1018
1054
  }
1055
+ if($schema_json) {
1056
+ $schema_json.classList.add("is-hidden");
1057
+ }
1019
1058
  if($toggle) {
1020
1059
  $toggle.classList.add("is-hidden");
1021
1060
  }
@@ -1307,7 +1346,7 @@ function register_sub_routes(gobj)
1307
1346
  let nodes = [];
1308
1347
  if(is_object(gobj_read_attr(gobj, "landing_routes"))) {
1309
1348
  nodes.push({route: base + "/schema", label: "schema",
1310
- icon: "yi-hexagon-nodes", gclass: "C_YUI_TREEDB_SCHEMA"});
1349
+ icon: "yi-compass-drafting", gclass: "C_YUI_TREEDB_SCHEMA"});
1311
1350
  }
1312
1351
  if(is_object(descs)) {
1313
1352
  for(let topic of Object.keys(descs)) {
@@ -1655,13 +1694,18 @@ function is_mobile()
1655
1694
  * whole service (lazy drill). CHILD model: the viewer publishes
1656
1695
  * EV_EXPAND_PATH to us.
1657
1696
  ************************************************************/
1658
- function open_json_viewer(gobj)
1697
+ function open_json_viewer(gobj, opts)
1659
1698
  {
1660
1699
  let priv = gobj.priv;
1700
+ let schema = !!(opts && opts.schema);
1661
1701
 
1662
- /* Already open: just re-fetch. */
1702
+ /* Already open: just re-fetch. The schema needs no fetch -- it is
1703
+ * the `descs` this view already holds -- so a second click on a
1704
+ * viewer that is already showing it does nothing. */
1663
1705
  if(priv.json_win || priv.json_modal) {
1664
- request_print_tranger(gobj, "");
1706
+ if(!schema) {
1707
+ request_print_tranger(gobj, "");
1708
+ }
1665
1709
  return;
1666
1710
  }
1667
1711
 
@@ -1681,7 +1725,11 @@ function open_json_viewer(gobj)
1681
1725
  /* No `title`: the host titles it — the window's title bar on
1682
1726
  * desktop, the dialog's header on mobile. The viewer's own
1683
1727
  * title would land INSIDE that host, doubling it. */
1684
- subscriber: gobj /* publishes EV_EXPAND_PATH to us */
1728
+ subscriber: gobj, /* publishes EV_EXPAND_PATH to us */
1729
+ /* The schema is here already and it is small: it goes in
1730
+ * whole, and no drill-down happens because nothing is
1731
+ * collapsed. */
1732
+ json_data: schema? gobj_read_attr(gobj, "descs") : null
1685
1733
  },
1686
1734
  gobj
1687
1735
  );
@@ -1703,7 +1751,7 @@ function open_json_viewer(gobj)
1703
1751
  dialog: true,
1704
1752
  logical_class: "TREEDB_JSON_SHEET",
1705
1753
  title_prefix: priv.treedb_name,
1706
- title: "raw json",
1754
+ title: schema? "schema json" : "raw json",
1707
1755
  t: t,
1708
1756
  on_close: () => {
1709
1757
  if(gobj_is_destroying(gobj)) {
@@ -1733,8 +1781,8 @@ function open_json_viewer(gobj)
1733
1781
  height: 620,
1734
1782
  logical_class: "TREEDB_JSON_WINDOW",
1735
1783
  title_prefix: priv.treedb_name,
1736
- title: "raw json",
1737
- icon: "yi-eye",
1784
+ title: schema? "schema json" : "raw json",
1785
+ icon: schema? "yi-code" : "yi-eye",
1738
1786
  body: $box,
1739
1787
  manager: null,
1740
1788
  on_close: () => {
@@ -1754,7 +1802,9 @@ function open_json_viewer(gobj)
1754
1802
  gobj_start(priv.json_win);
1755
1803
  }
1756
1804
 
1757
- request_print_tranger(gobj, "");
1805
+ if(!schema) {
1806
+ request_print_tranger(gobj, "");
1807
+ }
1758
1808
  }
1759
1809
 
1760
1810
  /************************************************************
@@ -2718,6 +2768,24 @@ function ac_open_json(gobj, event, kw, src)
2718
2768
  return 0;
2719
2769
  }
2720
2770
 
2771
+ /********************************************
2772
+ * The schema, as the json it is: the `descs` the schema graph is
2773
+ * drawing. Same viewer and same host as the tranger's raw json --
2774
+ * one at a time, so it closes whatever was open first.
2775
+ ********************************************/
2776
+ function ac_open_schema_json(gobj, event, kw, src)
2777
+ {
2778
+ if(!is_object(gobj_read_attr(gobj, "descs"))) {
2779
+ /* Not reachable by hand -- the button only exists while the
2780
+ * schema is on screen, which needs the descs -- so this is a
2781
+ * broken caller, not something to tell the reader about. */
2782
+ log_error(`${gobj_short_name(gobj)}: schema json with no descs`);
2783
+ return -1;
2784
+ }
2785
+ open_json_viewer(gobj, {schema: true});
2786
+ return 0;
2787
+ }
2788
+
2721
2789
  /********************************************
2722
2790
  * Open the graph where the reader left it: its last visited
2723
2791
  * route under the graph's base -- `<graph>/<topic>` when a topic
@@ -2823,6 +2891,7 @@ function create_gclass(gclass_name)
2823
2891
  ["EV_DELETE_RECORD", ac_delete_record, null],
2824
2892
  ["EV_REFRESH_TOPIC", ac_refresh_topic, null],
2825
2893
  ["EV_OPEN_JSON", ac_open_json, null],
2894
+ ["EV_OPEN_SCHEMA_JSON", ac_open_schema_json, null],
2826
2895
  ["EV_OPEN_GRAPH", ac_open_graph, null],
2827
2896
  ["EV_EXPAND_PATH", ac_json_expand_path, null],
2828
2897
  ["EV_JSON_CLOSED", ac_json_closed, null],
@@ -2857,6 +2926,7 @@ function create_gclass(gclass_name)
2857
2926
  event_flag_t.EVF_NO_WARN_SUBS],
2858
2927
  ["EV_REFRESH_TOPIC", 0],
2859
2928
  ["EV_OPEN_JSON", 0],
2929
+ ["EV_OPEN_SCHEMA_JSON", 0],
2860
2930
  ["EV_OPEN_GRAPH", 0],
2861
2931
  ["EV_EXPAND_PATH", 0],
2862
2932
  ["EV_JSON_CLOSED", 0],
@@ -2,78 +2,78 @@
2
2
  * treedb_node_label.js
3
3
  *
4
4
  * Pure, testable logic behind the label of a node in the treedb
5
- * graph: WHAT A RECORD IS CALLED, which is not always what it is
6
- * keyed by. Kept out of the gclass so it can be unit-tested with
7
- * no DOM and no G6.
5
+ * graph: WHAT A RECORD IS CALLED. Kept out of the gclass so it can
6
+ * be unit-tested with no DOM and no G6.
8
7
  *
9
8
  * Copyright (c) 2026, ArtGins.
10
9
  * All Rights Reserved.
11
10
  ***********************************************************************/
12
11
 
12
+ const SEP = " · "; /* the middle dot the shell titles use */
13
+
13
14
  /************************************************************
14
- * What a node is CALLED, which is not always what it is
15
- * keyed by.
15
+ * What a node is CALLED: **its id, and the secondary keys it
16
+ * carries**.
16
17
  *
17
- * A topic whose id column is flagged `rowid`, `uuid` or
18
- * `qualified` keys its records by something that is not the
19
- * name — a counter, a random string, or the name with every
20
- * ancestor in front of it — and the name a human knows the
21
- * record by lives in the secondary key the topic declares
22
- * (`pkey2s`, carried in the desc since SDK > 7.13.0). The
23
- * `topics` and `cols` topics of treedb_system_schema are the
24
- * case that forced this: named in `value`, so every card in
25
- * the graph read "181", "225", "193" while they were keyed by
26
- * rowid, and reads the whole path now that they are
27
- * qualified.
18
+ * A record is one thing and an INSTANCE of it is another: the
19
+ * topic keys the record by `id` and the instance by the
20
+ * columns it declares in `pkey2s`. A card that shows one
21
+ * without the other is a card that cannot be identified --
22
+ * and it used to show one INSTEAD of the other, so the yunos
23
+ * of an agent read `7.23.0-1`, `7.23.0-1`, `7.23.0-1` (the
24
+ * release, which they share) and never said which yuno each
25
+ * one was, while a `configurations` card read `1`.
28
26
  *
29
- * The pkey stays reachable: it is the card's tooltip.
27
+ * It came from the opposite case, and that one still works:
28
+ * the `topics` and `cols` of treedb_system_schema are keyed
29
+ * by a rowid, so the card said "181" and the name lived in
30
+ * the pkey2. Now it says both -- `181 · yuno_role` -- which
31
+ * is what the record is: that name, under that key.
30
32
  *
31
- * Falls back to the id whenever the schema does not say
32
- * otherwise — an older node whose desc carries no `pkey2s`
33
- * included.
33
+ * Rules:
34
+ * - the id always opens the label;
35
+ * - every pkey2 the record carries follows it, in the
36
+ * order the topic declares them, separated by `·`;
37
+ * - a pkey2 whose value is empty, absent or equal to the
38
+ * id adds nothing;
39
+ * - a desc with no `pkey2s` (an older node's schema
40
+ * included) gives the bare id.
34
41
  ************************************************************/
35
42
  export function node_label(desc, record)
36
43
  {
37
- /* treedb stores every record under `id` (the pkey name is fixed in
38
- * tranger2_create_topic), so the fallback reads `id` while the FLAGS
39
- * are read from whatever column the desc names as the pkey. */
44
+ /* treedb stores every record under `id`: the pkey name is fixed in
45
+ * tranger2_create_topic, whatever the desc calls it. */
40
46
  let id = record.id;
41
47
 
42
- let id_col = null;
43
- if(Array.isArray(desc.cols)) {
44
- for(let col of desc.cols) {
45
- if(col && col.id === (desc.pkey || "id")) {
46
- id_col = col;
47
- break;
48
- }
49
- }
50
- }
51
- if(!id_col || !Array.isArray(id_col.flag)) {
52
- return id;
53
- }
54
- if(id_col.flag.indexOf("rowid") < 0 &&
55
- id_col.flag.indexOf("uuid") < 0 &&
56
- id_col.flag.indexOf("qualified") < 0
57
- ) {
58
- return id;
59
- }
60
-
48
+ let pkey2s = desc? desc.pkey2s : null;
61
49
  /* `pkey2s` is a list in the desc, but the schema literal may
62
- * declare it as a bare string — take either. */
63
- let pkey2s = desc.pkey2s;
50
+ * declare it as a bare string -- take either. */
64
51
  if(typeof pkey2s === "string") {
65
52
  pkey2s = pkey2s? [pkey2s] : [];
66
53
  }
67
- if(!Array.isArray(pkey2s)) {
54
+ if(!Array.isArray(pkey2s) || pkey2s.length === 0) {
68
55
  return id;
69
56
  }
57
+
58
+ let parts = [];
70
59
  for(let name of pkey2s) {
60
+ if(!name) {
61
+ continue;
62
+ }
71
63
  let value = record[name];
72
- if(typeof value === "string" && value.length > 0) {
73
- return value;
64
+ if(typeof value === "number") {
65
+ value = String(value);
66
+ }
67
+ if(typeof value !== "string" || value.length === 0) {
68
+ continue;
69
+ }
70
+ if(value === String(id)) {
71
+ continue; /* the same thing twice says nothing */
74
72
  }
73
+ parts.push(value);
75
74
  }
76
-
77
- return id;
75
+ if(parts.length === 0) {
76
+ return id;
77
+ }
78
+ return [id, ...parts].join(SEP);
78
79
  }
79
-
@@ -1,8 +1,10 @@
1
1
  /***********************************************************************
2
2
  * treedb_node_label.test.js
3
3
  *
4
- * The label of a treedb graph node: the pkey when it names the
5
- * record, the secondary key when the pkey is synthetic.
4
+ * The label of a treedb graph node: its id, and the secondary
5
+ * keys it carries. A record is one thing and an INSTANCE of it
6
+ * another, and a card that shows one without the other cannot be
7
+ * identified.
6
8
  *
7
9
  * Copyright (c) 2026, ArtGins.
8
10
  * All Rights Reserved.
@@ -35,44 +37,67 @@ const REALMS_DESC = {
35
37
  };
36
38
 
37
39
  describe("node_label", () => {
38
- it("uses the secondary key when the pkey is a rowid", () => {
40
+ const DOT = " \u00b7 ";
41
+
42
+ it("says the id AND the secondary key that names the record", () => {
39
43
  expect(node_label(COLS_DESC, {id: "181", value: "yuno_role"}))
40
- .toBe("yuno_role");
44
+ .toBe("181" + DOT + "yuno_role");
45
+ });
46
+
47
+ it("says the id AND the instance: the case that brought it", () => {
48
+ /* Three yunos of one agent, all of release 7.23.0-1: the cards
49
+ * read `7.23.0-1` three times and never said which yuno. */
50
+ let yunos = {
51
+ topic_name: "yunos",
52
+ pkey: "id",
53
+ pkey2s: ["yuno_release"],
54
+ cols: [
55
+ {id: "id", type: "string", flag: ["persistent", "rowid"]},
56
+ {id: "yuno_release", type: "string", flag: ["persistent"]},
57
+ ],
58
+ };
59
+ expect(node_label(yunos, {id: "1", yuno_release: "7.23.0-1"}))
60
+ .toBe("1" + DOT + "7.23.0-1");
61
+ expect(node_label(yunos, {id: "1600", yuno_release: "1.8.1.0-2"}))
62
+ .toBe("1600" + DOT + "1.8.1.0-2");
41
63
  });
42
64
 
43
- it("uses the pkey when it names the record", () => {
65
+ it("gives the bare id when the topic declares no secondary key", () => {
44
66
  expect(node_label(REALMS_DESC, {id: "artgins.utilities.all", realm_name: "all"}))
45
67
  .toBe("artgins.utilities.all");
46
68
  });
47
69
 
48
70
  it("accepts a pkey2s declared as a bare string", () => {
49
71
  let desc = {...COLS_DESC, pkey2s: "value"};
50
- expect(node_label(desc, {id: "181", value: "yuno_role"})).toBe("yuno_role");
72
+ expect(node_label(desc, {id: "181", value: "yuno_role"}))
73
+ .toBe("181" + DOT + "yuno_role");
51
74
  });
52
75
 
53
- it("treats a qualified pkey like a rowid one", () => {
54
- let desc = {
55
- ...COLS_DESC,
56
- cols: [
57
- {id: "id", type: "string", flag: ["persistent", "qualified"]},
58
- {id: "value", type: "string", flag: ["persistent", "required"]},
59
- ],
60
- };
61
- expect(node_label(desc, {
62
- id: "treedb_yunovatioscodb.yunos.yuno_role",
63
- value: "yuno_role",
64
- })).toBe("yuno_role");
76
+ it("does not care what the id column is flagged", () => {
77
+ /* rowid, uuid, qualified or nothing at all: the id is shown
78
+ * either way, and the flags decide nothing here any more. */
79
+ for(let flag of [["persistent", "uuid"], ["persistent", "qualified"], ["persistent"]]) {
80
+ let desc = {
81
+ ...COLS_DESC,
82
+ cols: [
83
+ {id: "id", type: "string", flag: flag},
84
+ {id: "value", type: "string", flag: ["persistent", "required"]},
85
+ ],
86
+ };
87
+ expect(node_label(desc, {id: "k", value: "named"})).toBe("k" + DOT + "named");
88
+ }
65
89
  });
66
90
 
67
- it("treats a uuid pkey like a rowid one", () => {
68
- let desc = {
69
- ...COLS_DESC,
70
- cols: [
71
- {id: "id", type: "string", flag: ["persistent", "uuid"]},
72
- {id: "value", type: "string", flag: ["persistent", "required"]},
73
- ],
74
- };
75
- expect(node_label(desc, {id: "3f2a…", value: "named"})).toBe("named");
91
+ it("says every secondary key the topic declares, in its order", () => {
92
+ let desc = {topic_name: "x", pkey: "id", pkey2s: ["a", "b"], cols: []};
93
+ expect(node_label(desc, {id: "7", a: "one", b: "two"}))
94
+ .toBe("7" + DOT + "one" + DOT + "two");
95
+ });
96
+
97
+ it("skips a secondary key that is empty, absent or the id again", () => {
98
+ expect(node_label(COLS_DESC, {id: "181", value: ""})).toBe("181");
99
+ expect(node_label(COLS_DESC, {id: "181"})).toBe("181");
100
+ expect(node_label(COLS_DESC, {id: "181", value: "181"})).toBe("181");
76
101
  });
77
102
 
78
103
  it("falls back to the id when the desc carries no pkey2s (older node)", () => {
@@ -80,27 +105,13 @@ describe("node_label", () => {
80
105
  expect(node_label(desc, {id: "181", value: "yuno_role"})).toBe("181");
81
106
  });
82
107
 
83
- it("falls back to the id when the secondary key is empty in the record", () => {
84
- expect(node_label(COLS_DESC, {id: "181", value: ""})).toBe("181");
85
- expect(node_label(COLS_DESC, {id: "181"})).toBe("181");
86
- });
87
-
88
- it("honours a pkey that is not called 'id'", () => {
89
- let desc = {
90
- topic_name: "x",
91
- pkey: "key",
92
- pkey2s: ["name"],
93
- cols: [
94
- {id: "key", type: "string", flag: ["persistent", "rowid"]},
95
- {id: "name", type: "string", flag: ["persistent", "required"]},
96
- /* A col called `id` that is NOT the pkey must not decide. */
97
- {id: "id", type: "string", flag: ["persistent"]},
98
- ],
99
- };
100
- expect(node_label(desc, {id: "7", key: "7", name: "seven"})).toBe("seven");
108
+ it("takes a numeric secondary key as the text it is", () => {
109
+ expect(node_label(COLS_DESC, {id: "emailsender.artgins", value: 1}))
110
+ .toBe("emailsender.artgins" + DOT + "1");
101
111
  });
102
112
 
103
- it("survives a desc with no cols", () => {
113
+ it("survives a desc with no cols, and no desc at all", () => {
104
114
  expect(node_label({topic_name: "x", pkey: "id"}, {id: "42"})).toBe("42");
115
+ expect(node_label(null, {id: "42"})).toBe("42");
105
116
  });
106
117
  });
package/src/yui_icons.css CHANGED
@@ -176,6 +176,15 @@
176
176
  mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 576 512'%3E%3Cpath d='M41-24.9c-9.4-9.4-24.6-9.4-33.9 0S-2.3-.3 7 9.1l528 528c9.4 9.4 24.6 9.4 33.9 0s9.4-24.6 0-33.9l-96.4-96.4c2.7-2.4 5.4-4.8 8-7.2 46.8-43.5 78.1-95.4 93-131.1 3.3-7.9 3.3-16.7 0-24.6-14.9-35.7-46.2-87.7-93-131.1-47.1-43.7-111.8-80.6-192.6-80.6-56.8 0-105.6 18.2-146 44.2L41-24.9zM204.5 138.7c23.5-16.8 52.4-26.7 83.5-26.7 79.5 0 144 64.5 144 144 0 31.1-9.9 59.9-26.7 83.5l-34.7-34.7c12.7-21.4 17-47.7 10.1-73.7-13.7-51.2-66.4-81.6-117.6-67.9-8.6 2.3-16.7 5.7-24 10l-34.7-34.7zM325.3 395.1c-11.9 3.2-24.4 4.9-37.3 4.9-79.5 0-144-64.5-144-144 0-12.9 1.7-25.4 4.9-37.3L69.4 139.2c-32.6 36.8-55 75.8-66.9 104.5-3.3 7.9-3.3 16.7 0 24.6 14.9 35.7 46.2 87.7 93 131.1 47.1 43.7 111.8 80.6 192.6 80.6 37.3 0 71.2-7.9 101.5-20.6l-64.2-64.2z'/%3E%3C/svg%3E");
177
177
  }
178
178
 
179
+ /* The SCHEMA of a treedb -- its plan, as against the `hexagon-nodes`
180
+ * of the DATA graph. The two sat on the same glyph in the same
181
+ * toolbar, one button apart, and nothing but the label told them
182
+ * apart. */
183
+ .yi-compass-drafting::before {
184
+ -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3E%3Cpath d='M99.3 256.1l69.4-119.9c-5.6-12.2-8.8-25.8-8.8-40.2 0-53 43-96 96-96s96 43 96 96c0 14.3-3.1 27.9-8.8 40.2l44.4 76.7c-23.1 26-53.7 45.1-88.4 53.8L256 191.9 187.9 309.5c21.5 6.8 44.3 10.5 68.1 10.5 70.7 0 133.8-32.7 174.9-84 11.1-13.8 31.2-16 45-5s16 31.2 5 45c-52.7 65.8-133.9 108-224.8 108-35.4 0-69.4-6.4-100.7-18.1L98.7 463.7C94 471.8 87 478.4 78.6 482.6L23.2 510.3c-5 2.5-10.9 2.2-15.6-.7S0 501.5 0 496l0-55.4c0-8.4 2.2-16.7 6.5-24.1l60-103.7c-12.8-11.2-24.6-23.5-35.3-36.8-11.1-13.8-8.8-33.9 5-45s33.9-8.8 45 5c5.7 7.1 11.8 13.8 18.2 20.1zM381.1 407.9c32.5-13 62.4-31 88.9-52.9l35.6 61.5c4.2 7.3 6.5 15.6 6.5 24.1l0 55.4c0 5.5-2.9 10.7-7.6 13.6s-10.6 3.2-15.6 .7l-55.4-27.7c-8.4-4.2-15.4-10.8-20.1-18.9l-32.3-55.8zM256 128a32 32 0 1 0 0-64 32 32 0 1 0 0 64z'/%3E%3C/svg%3E");
185
+ mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3E%3Cpath d='M99.3 256.1l69.4-119.9c-5.6-12.2-8.8-25.8-8.8-40.2 0-53 43-96 96-96s96 43 96 96c0 14.3-3.1 27.9-8.8 40.2l44.4 76.7c-23.1 26-53.7 45.1-88.4 53.8L256 191.9 187.9 309.5c21.5 6.8 44.3 10.5 68.1 10.5 70.7 0 133.8-32.7 174.9-84 11.1-13.8 31.2-16 45-5s16 31.2 5 45c-52.7 65.8-133.9 108-224.8 108-35.4 0-69.4-6.4-100.7-18.1L98.7 463.7C94 471.8 87 478.4 78.6 482.6L23.2 510.3c-5 2.5-10.9 2.2-15.6-.7S0 501.5 0 496l0-55.4c0-8.4 2.2-16.7 6.5-24.1l60-103.7c-12.8-11.2-24.6-23.5-35.3-36.8-11.1-13.8-8.8-33.9 5-45s33.9-8.8 45 5c5.7 7.1 11.8 13.8 18.2 20.1zM381.1 407.9c32.5-13 62.4-31 88.9-52.9l35.6 61.5c4.2 7.3 6.5 15.6 6.5 24.1l0 55.4c0 5.5-2.9 10.7-7.6 13.6s-10.6 3.2-15.6 .7l-55.4-27.7c-8.4-4.2-15.4-10.8-20.1-18.9l-32.3-55.8zM256 128a32 32 0 1 0 0-64 32 32 0 1 0 0 64z'/%3E%3C/svg%3E");
186
+ }
187
+
179
188
  .yi-hexagon-nodes::before {
180
189
  -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 448 512'%3E%3Cpath d='M248 106.6c18.9-9 32-28.3 32-50.6c0-30.9-25.1-56-56-56s-56 25.1-56 56c0 22.3 13.1 41.6 32 50.6l0 98.8c-2.8 1.3-5.5 2.9-8 4.7l-80.1-45.8c1.6-20.8-8.6-41.6-27.9-52.8C57.2 96 23 105.2 7.5 132S1.2 193 28 208.5c1.3 .8 2.6 1.5 4 2.1l0 90.8c-1.3 .6-2.7 1.3-4 2.1C1.2 319-8 353.2 7.5 380S57.2 416 84 400.5c19.3-11.1 29.4-32 27.8-52.8l50.5-28.9c-11.5-11.2-19.9-25.6-23.8-41.7L88 306.1c-2.6-1.8-5.2-3.3-8-4.7l0-90.8c2.8-1.3 5.5-2.9 8-4.7l80.1 45.8c-.1 1.4-.2 2.8-.2 4.3c0 22.3 13.1 41.6 32 50.6l0 98.8c-18.9 9-32 28.3-32 50.6c0 30.9 25.1 56 56 56s56-25.1 56-56c0-22.3-13.1-41.6-32-50.6l0-98.8c2.8-1.3 5.5-2.9 8-4.7l80.1 45.8c-1.6 20.8 8.6 41.6 27.8 52.8c26.8 15.5 61 6.3 76.5-20.5s6.3-61-20.5-76.5c-1.3-.8-2.7-1.5-4-2.1l0-90.8c1.4-.6 2.7-1.3 4-2.1c26.8-15.5 36-49.7 20.5-76.5S390.8 96 364 111.5c-19.3 11.1-29.4 32-27.8 52.8l-50.6 28.9c11.5 11.2 19.9 25.6 23.8 41.7L360 205.9c2.6 1.8 5.2 3.3 8 4.7l0 90.8c-2.8 1.3-5.5 2.9-8 4.6l-80.1-45.8c.1-1.4 .2-2.8 .2-4.3c0-22.3-13.1-41.6-32-50.6l0-98.8z'/%3E%3C/svg%3E");
181
190
  mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 448 512'%3E%3Cpath d='M248 106.6c18.9-9 32-28.3 32-50.6c0-30.9-25.1-56-56-56s-56 25.1-56 56c0 22.3 13.1 41.6 32 50.6l0 98.8c-2.8 1.3-5.5 2.9-8 4.7l-80.1-45.8c1.6-20.8-8.6-41.6-27.9-52.8C57.2 96 23 105.2 7.5 132S1.2 193 28 208.5c1.3 .8 2.6 1.5 4 2.1l0 90.8c-1.3 .6-2.7 1.3-4 2.1C1.2 319-8 353.2 7.5 380S57.2 416 84 400.5c19.3-11.1 29.4-32 27.8-52.8l50.5-28.9c-11.5-11.2-19.9-25.6-23.8-41.7L88 306.1c-2.6-1.8-5.2-3.3-8-4.7l0-90.8c2.8-1.3 5.5-2.9 8-4.7l80.1 45.8c-.1 1.4-.2 2.8-.2 4.3c0 22.3 13.1 41.6 32 50.6l0 98.8c-18.9 9-32 28.3-32 50.6c0 30.9 25.1 56 56 56s56-25.1 56-56c0-22.3-13.1-41.6-32-50.6l0-98.8c2.8-1.3 5.5-2.9 8-4.7l80.1 45.8c-1.6 20.8 8.6 41.6 27.8 52.8c26.8 15.5 61 6.3 76.5-20.5s6.3-61-20.5-76.5c-1.3-.8-2.7-1.5-4-2.1l0-90.8c1.4-.6 2.7-1.3 4-2.1c26.8-15.5 36-49.7 20.5-76.5S390.8 96 364 111.5c-19.3 11.1-29.4 32-27.8 52.8l-50.6 28.9c11.5 11.2 19.9 25.6 23.8 41.7L360 205.9c2.6 1.8 5.2 3.3 8 4.7l0 90.8c-2.8 1.3-5.5 2.9-8 4.6l-80.1-45.8c.1-1.4 .2-2.8 .2-4.3c0-22.3-13.1-41.6-32-50.6l0-98.8z'/%3E%3C/svg%3E");