@yuneta/gobj-ui 2.6.0 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/README.md +331 -11
  2. package/dist/gobj-ui.cjs.js +21386 -17919
  3. package/dist/gobj-ui.es.js +26059 -22613
  4. package/index.js +45 -9
  5. package/package.json +4 -4
  6. package/src/c_g6_nodes_tree.js +128 -40
  7. package/src/c_yui_form.css +9 -0
  8. package/src/c_yui_form.js +113 -53
  9. package/src/c_yui_gobj_tree_js.js +58 -36
  10. package/src/c_yui_json.css +139 -0
  11. package/src/c_yui_json.js +928 -0
  12. package/src/c_yui_json_graph.js +110 -20
  13. package/src/c_yui_map.js +18 -36
  14. package/src/c_yui_nav.js +6 -9
  15. package/src/c_yui_period.css +184 -0
  16. package/src/c_yui_period.js +1441 -0
  17. package/src/c_yui_shell.css +120 -0
  18. package/src/c_yui_shell.js +672 -111
  19. package/src/c_yui_treedb_graph.js +639 -37
  20. package/src/c_yui_treedb_schema.js +478 -0
  21. package/src/c_yui_treedb_topic_with_form.css +18 -0
  22. package/src/c_yui_treedb_topic_with_form.js +153 -103
  23. package/src/c_yui_treedb_topics.css +73 -0
  24. package/src/c_yui_treedb_topics.js +1070 -29
  25. package/src/c_yui_window.css +22 -0
  26. package/src/c_yui_window.js +182 -97
  27. package/src/c_yui_window_manager.js +38 -4
  28. package/src/json_view_helpers.js +214 -0
  29. package/src/json_view_helpers.test.js +135 -0
  30. package/src/route_map_model.js +317 -0
  31. package/src/route_map_model.test.js +209 -0
  32. package/src/route_resolver.js +24 -1
  33. package/src/route_resolver.test.js +40 -1
  34. package/src/shell_modals.js +162 -52
  35. package/src/shell_route_map.css +225 -0
  36. package/src/shell_route_map.js +363 -0
  37. package/src/tabulator.css +245 -0
  38. package/src/yui_dev.js +130 -8
  39. package/src/yui_frontend_view.js +106 -0
  40. package/src/yui_icons.css +62 -0
  41. package/src/yui_inputs.css +8 -3
  42. package/src/yui_inputs.js +51 -8
  43. package/src/yui_tabulator_i18n.js +128 -0
  44. package/src/yui_theme.js +147 -0
  45. package/src/yui_time.js +666 -0
  46. package/src/yui_time.test.js +330 -0
  47. package/src/yui_toolbar.css +28 -0
  48. package/src/yui_toolbar.js +86 -43
  49. package/src/c_yui_main.css +0 -501
  50. package/src/c_yui_main.js +0 -1623
  51. package/src/c_yui_routing.css +0 -18
  52. package/src/c_yui_routing.js +0 -837
  53. package/src/c_yui_tabs.js +0 -487
  54. package/src/themes.js +0 -215
  55. package/src/ytable.css +0 -63
  56. package/src/ytable.js +0 -505
@@ -16,6 +16,7 @@ import {
16
16
  log_error,
17
17
  gobj_read_pointer_attr,
18
18
  gobj_subscribe_event,
19
+ gobj_unsubscribe_event,
19
20
  gobj_parent,
20
21
  sprintf,
21
22
  gobj_read_attr,
@@ -60,19 +61,22 @@ import {
60
61
  yui_shell_confirm_yesnocancel,
61
62
  yui_shell_confirm_ok,
62
63
  yui_shell_popup_layer,
64
+ yui_shell_show_modal,
63
65
  } from "./shell_modals.js";
64
66
 
65
67
  import {
66
68
  yui_shell_of,
67
- yui_shell_push_escape,
68
- yui_shell_pop_escape,
69
69
  } from "./c_yui_shell.js";
70
70
 
71
71
  import {register_c_yui_form} from "./c_yui_form.js";
72
+ import {attach_clear} from "./yui_inputs.js";
73
+
74
+ import {yui_tabulator_lang, yui_tabulator_relocalize} from "./yui_tabulator_i18n.js";
72
75
 
73
76
  import {t} from "i18next";
74
77
 
75
78
  import "./c_yui_treedb_topic_with_form.css";
79
+ import "./tabulator.css";
76
80
 
77
81
  import { TabulatorFull as Tabulator } from "tabulator-tables";
78
82
 
@@ -118,7 +122,7 @@ SDATA(data_type_t.DTP_JSON, "tabulator_settings", 0, {
118
122
  pagination: true,
119
123
  paginationSize: 25,
120
124
  paginationSizeSelector: [25, 50, 100, true],
121
- placeholder: "No data available",
125
+ placeholder: "No data available", /* overridden per-language at create */
122
126
  maxHeight: "100%"
123
127
  }, "Default settings for Tabulator"),
124
128
 
@@ -138,10 +142,7 @@ let PRIVATE_DATA = {
138
142
  topic_name: "",
139
143
  tabulator: null,
140
144
  form: null, // hosted C_YUI_FORM child (while dialog open)
141
- $popup: null, // dialog DOM element
142
- on_keydown: null, // document Escape handler (no-shell fallback)
143
- escape_shell: null, // shell whose Escape chain holds escape_handler
144
- escape_handler: null, // close handler pushed on the shell chain
145
+ form_modal: null, // { close } handle of the adaptive dialog
145
146
  };
146
147
 
147
148
  let __gclass__ = null;
@@ -199,6 +200,14 @@ function mt_create(gobj)
199
200
  ***************************************************************/
200
201
  function mt_start(gobj)
201
202
  {
203
+ /* The shell publishes the language switch (yui_shell_language_changed):
204
+ * the column headers re-translate themselves (they carry data-i18n — see
205
+ * col_label), but Tabulator's OWN chrome is drawn once and needs to be
206
+ * told. */
207
+ let shell = yui_shell_of(gobj);
208
+ if(shell) {
209
+ gobj_subscribe_event(shell, "EV_LANGUAGE_CHANGED", {}, gobj);
210
+ }
202
211
  create_tabulator(gobj);
203
212
  }
204
213
 
@@ -207,6 +216,10 @@ function mt_start(gobj)
207
216
  ***************************************************************/
208
217
  function mt_stop(gobj)
209
218
  {
219
+ let shell = yui_shell_of(gobj);
220
+ if(shell) {
221
+ gobj_unsubscribe_event(shell, "EV_LANGUAGE_CHANGED", {}, gobj);
222
+ }
210
223
  close_form_dialog(gobj);
211
224
  table__destroy(gobj);
212
225
  }
@@ -458,7 +471,7 @@ function build_ui(gobj)
458
471
  if(with_search_button) {
459
472
  let search_id = `${toolbar_id}_search`;
460
473
  let $search_box = createElement2(
461
- ['div', {class: 'control has-icons-left has-icons-right mr-1'}, [
474
+ ['div', {class: 'control has-icons-left mr-1'}, [
462
475
  ['input', {
463
476
  id: search_id,
464
477
  class: 'input',
@@ -468,19 +481,13 @@ function build_ui(gobj)
468
481
  }],
469
482
  ['span', {class: 'icon is-left'}, [
470
483
  ['i', {class: 'yi-magnifying-glass'}]
471
- ]],
472
- ['span', {class: 'icon is-right', style: 'cursor:pointer; pointer-events:all;', title: 'clear search'}, [
473
- ['i', {class: 'yi-xmark'}]
474
- ], {
475
- 'click': (event) => {
476
- event.stopPropagation();
477
- let $input = document.getElementById(search_id);
478
- if($input) { $input.value = ''; $input.dispatchEvent(new Event('input')); }
479
- }
480
- }]
484
+ ]]
481
485
  ]]
482
486
  );
483
487
  let $search_input = $search_box.querySelector(`#${search_id}`);
488
+ /* NORM clear (✕): dispatches a synthetic `input`, so the tabulator
489
+ * filter below re-runs (empty term → clearFilter). */
490
+ attach_clear($search_box, $search_input);
484
491
  if($search_input) {
485
492
  $search_input.addEventListener('input', (event) => {
486
493
  let searchText = event.target.value.trim().toLowerCase();
@@ -510,7 +517,7 @@ function build_ui(gobj)
510
517
 
511
518
  if(with_refresh_button) {
512
519
  let $refresh = createElement2(
513
- ['button', {class: 'button mr-1', title: 'refresh'}, [
520
+ ['button', {class: 'button mr-1', title: t('refresh'), 'data-i18n-title': 'refresh'}, [
514
521
  ['i', {class: 'yi-arrows-rotate'}],
515
522
  ['span', {class: 'is-hidden-mobile', i18n: 'refresh', style: 'padding-left:5px;'}, 'refresh']
516
523
  ], {
@@ -819,7 +826,12 @@ function create_tabulator(gobj)
819
826
  }
820
827
 
821
828
  let pkey = desc.pkey || "id";
822
- let selectable = with_checkbox ? true : (with_radio ? 1 : false);
829
+ /* Selection is driven ONLY by the checkbox column, never by clicking
830
+ * the row: "highlight" keeps the rowSelection checkbox fully working
831
+ * (it calls toggleSelect() directly) while disabling click-to-select,
832
+ * so opening the edit (yi-pen) form no longer implicitly ticks the
833
+ * row. Radio keeps single click-select (its own widget). */
834
+ let selectable = with_checkbox ? "highlight" : (with_radio ? 1 : false);
823
835
 
824
836
  let tabulator_settings = json_deep_copy(gobj_read_attr(gobj, "tabulator_settings"));
825
837
 
@@ -847,7 +859,26 @@ function create_tabulator(gobj)
847
859
  log_error(`${gobj_short_name(gobj)}: table element '#${table_id}' not found in $container`);
848
860
  return;
849
861
  }
850
- let tabulator = new Tabulator($table_el, tabulator_settings);
862
+ /* Tabulator renders its own chrome — the paginator, the placeholder, the
863
+ * loading/error notices — and nothing ever passed those through i18n: the
864
+ * table sat in English inside a Spanish view. Hand it the current
865
+ * language (every key falls back to what it used to render, so an app
866
+ * that defines none of them sees no change). */
867
+ let tabulator = new Tabulator($table_el,
868
+ Object.assign({}, tabulator_settings, yui_tabulator_lang(t), {
869
+ /* The placeholder ships as an English literal in the settings
870
+ * default; hand it the current language (falling back to exactly
871
+ * what it said before for an app that does not define the key). */
872
+ placeholder: t("no data available", {defaultValue: "No data available"})
873
+ }));
874
+
875
+ /* "highlight" still paints the whole-row hover wash (and a pointer
876
+ * cursor), which reads as a selection. This table selects only via the
877
+ * checkbox, so suppress the hover: rows stay visually static, and only
878
+ * a checkbox-selected row changes colour (see tabulator.css). */
879
+ if(with_checkbox) {
880
+ $table_el.classList.add("yui-no-row-hover");
881
+ }
851
882
 
852
883
  /* Keep the footer in sync with the visible (active) row count,
853
884
  * and hide the pagination chrome while everything fits in one
@@ -1133,9 +1164,14 @@ function open_form_dialog(gobj, mode, record)
1133
1164
 
1134
1165
  let priv = gobj.priv;
1135
1166
  let desc = gobj_read_attr(gobj, "desc");
1136
- let popup_id = gobj_read_str_attr(gobj, "popup_id");
1137
1167
  let topic_name = gobj_read_str_attr(gobj, "topic_name");
1138
1168
 
1169
+ let shell = yui_shell_of(gobj);
1170
+ if(!shell) {
1171
+ log_error(`${gobj_short_name(gobj)}: no shell, cannot open the edit form`);
1172
+ return;
1173
+ }
1174
+
1139
1175
  /* Title says what you are doing: "new <topic>" on create,
1140
1176
  * "<topic> — <pkey>" on update. */
1141
1177
  let pkey = desc.pkey || "id";
@@ -1147,36 +1183,12 @@ function open_form_dialog(gobj, mode, record)
1147
1183
  title = t(topic_name) + (empty_string(rid) ? "" : " — " + rid);
1148
1184
  }
1149
1185
 
1150
- let $element = createElement2([
1151
- 'div', {id: popup_id, class: 'modal is-active'}, [
1152
- ['div', {class: 'modal-background'}, ''],
1153
- ['div', {class: 'modal-card modal-is-responsive'}, [
1154
- ['header', {class: 'modal-card-head p-3'}, [
1155
- ['p', {class: 'modal-card-title', style: 'text-align:center;'}, title],
1156
- ['button', {class: 'delete is-large', 'aria-label': 'close'}, '', {
1157
- click: function(evt) {
1158
- evt.stopPropagation();
1159
- request_close_form_dialog(gobj);
1160
- }
1161
- }]
1162
- ]],
1163
- /* flex column so the hosted form takes the dialog
1164
- * height: fields scroll internally and the form's
1165
- * bottom toolbar stays visible (dialog footer) */
1166
- ['section', {
1167
- class: 'modal-card-body p-2',
1168
- style: 'display:flex; flex-direction:column;'
1169
- }, []]
1170
- ]]
1171
- ], {
1172
- click: function(evt) {
1173
- // Capture bubble events
1174
- evt.stopPropagation();
1175
- }
1176
- }
1177
- ]);
1178
-
1179
- let $body = $element.querySelector('.modal-card-body');
1186
+ /* A flex column so the hosted form takes the dialog height: fields
1187
+ * scroll internally and the form's bottom toolbar stays visible. */
1188
+ let $body = createElement2(
1189
+ ['div', {class: 'TREEDB_FORM_BODY',
1190
+ style: 'display:flex; flex-direction:column; height:100%;'}, []]
1191
+ );
1180
1192
 
1181
1193
  let form = gobj_create_pure_child(
1182
1194
  "form_" + clean_name(gobj_name(gobj)),
@@ -1186,6 +1198,10 @@ function open_form_dialog(gobj, mode, record)
1186
1198
  record: record,
1187
1199
  fkey_options: build_fkey_options(gobj),
1188
1200
  form_mode: mode,
1201
+ /* Editing the raw topic record: structured cols
1202
+ * (template / table / coordinates) are raw JSON editors,
1203
+ * not interpreted into sub-widgets (the pre-merge behaviour). */
1204
+ render_mode: "edit",
1189
1205
  pkey: desc.pkey || "id",
1190
1206
  topic_name: topic_name,
1191
1207
  editable: true,
@@ -1194,7 +1210,6 @@ function open_form_dialog(gobj, mode, record)
1194
1210
  gobj
1195
1211
  );
1196
1212
  priv.form = form;
1197
- priv.$popup = $element;
1198
1213
 
1199
1214
  let $form_container = gobj_read_attr(form, "$container");
1200
1215
  if($form_container) {
@@ -1202,38 +1217,33 @@ function open_form_dialog(gobj, mode, record)
1202
1217
  $form_container.style.minHeight = "0";
1203
1218
  }
1204
1219
 
1205
- popup_mount_layer(gobj).appendChild($element);
1206
- refresh_language($element, t);
1207
-
1208
- /* Escape closes through the same unsaved-changes guard as the X.
1209
- * Under a shell, ride its Escape priority chain LIFO with the
1210
- * shell confirm dialogs, so Escape on an open confirm never
1211
- * reaches this dialog and stacks a second one. Without a shell
1212
- * (bare unit tests) fall back to a document listener. */
1213
- let shell = yui_shell_of(gobj);
1214
- if(shell) {
1215
- priv.escape_shell = shell;
1216
- priv.escape_handler = function() {
1217
- request_close_form_dialog(gobj);
1218
- };
1219
- yui_shell_push_escape(shell, "popup", priv.escape_handler);
1220
- } else {
1221
- priv.on_keydown = function(evt) {
1222
- if(evt.key === "Escape") {
1223
- evt.stopPropagation();
1224
- request_close_form_dialog(gobj);
1225
- }
1226
- };
1227
- document.addEventListener("keydown", priv.on_keydown);
1228
- }
1220
+ /* The standardized adaptive dialog, like the sibling treedb views: a
1221
+ * centered card with the X at the top-right on desktop, a full-screen
1222
+ * sheet with a back arrow on mobile; Escape / browser Back / backdrop
1223
+ * are wired by the shell. `before_close` preserves the unsaved-changes
1224
+ * guard it vetoes the dismiss and, on confirm, closes the modal
1225
+ * itself. `on_close` tears the hosted form child down. */
1226
+ priv.form_modal = yui_shell_show_modal(shell, $body, {
1227
+ dialog: true,
1228
+ logical_class: "TREEDB_FORM_SHEET",
1229
+ title: title,
1230
+ t: t,
1231
+ before_close: function() {
1232
+ return form_may_close(gobj);
1233
+ },
1234
+ on_close: function() {
1235
+ teardown_form_child(gobj);
1236
+ }
1237
+ });
1229
1238
 
1230
- gobj_start(form); // mt_start loads `record` into the form
1239
+ gobj_start(form); // mt_start loads `record` into the form
1240
+ refresh_language($body, t); // translate anything the form emitted on start
1231
1241
 
1232
1242
  /* Focus the pkey unless the mode made it readonly (update) —
1233
1243
  * then the first editable field. */
1234
- let $with_focus = $element.querySelector('.with-focus:not([readonly])');
1244
+ let $with_focus = $body.querySelector('.with-focus:not([readonly])');
1235
1245
  if(!$with_focus) {
1236
- $with_focus = $element.querySelector(
1246
+ $with_focus = $body.querySelector(
1237
1247
  'input.input:not([readonly]), textarea, select'
1238
1248
  );
1239
1249
  }
@@ -1243,10 +1253,12 @@ function open_form_dialog(gobj, mode, record)
1243
1253
  }
1244
1254
 
1245
1255
  /************************************************************
1246
- * Close request from the dialog X: if the form carries
1247
- * unsaved changes ask first (EV_WINDOW_TO_CLOSE contract).
1256
+ * Unsaved-changes guard for the dialog's `before_close` hook.
1257
+ * Returns true to allow the dismiss; false to VETO it (the form
1258
+ * has pending edits) and pop the confirm — which, on "yes",
1259
+ * closes the modal itself (EV_WINDOW_TO_CLOSE contract).
1248
1260
  ************************************************************/
1249
- function request_close_form_dialog(gobj)
1261
+ function form_may_close(gobj)
1250
1262
  {
1251
1263
  let priv = gobj.priv;
1252
1264
  if(priv.form) {
@@ -1258,31 +1270,24 @@ function request_close_form_dialog(gobj)
1258
1270
  'All changes will be lost. Are you sure?',
1259
1271
  {t: t, yes_label: "yes", no_label: "no", cancel_label: "cancel"}
1260
1272
  ).then(function(answer) {
1261
- if(answer === "yes") {
1262
- close_form_dialog(gobj);
1273
+ if(answer === "yes" && priv.form_modal) {
1274
+ priv.form_modal.close();
1263
1275
  }
1264
1276
  });
1265
- return;
1277
+ return false;
1266
1278
  }
1267
1279
  }
1268
- close_form_dialog(gobj);
1280
+ return true;
1269
1281
  }
1270
1282
 
1271
1283
  /************************************************************
1272
- * Destroy the hosted form and remove the dialog DOM.
1284
+ * Destroy the hosted C_YUI_FORM child. Called from the modal's
1285
+ * on_close — the shell has already removed the dialog DOM and
1286
+ * retired its Escape / history entries.
1273
1287
  ************************************************************/
1274
- function close_form_dialog(gobj)
1288
+ function teardown_form_child(gobj)
1275
1289
  {
1276
1290
  let priv = gobj.priv;
1277
- if(priv.escape_handler) {
1278
- yui_shell_pop_escape(priv.escape_shell, priv.escape_handler);
1279
- priv.escape_shell = null;
1280
- priv.escape_handler = null;
1281
- }
1282
- if(priv.on_keydown) {
1283
- document.removeEventListener("keydown", priv.on_keydown);
1284
- priv.on_keydown = null;
1285
- }
1286
1291
  if(priv.form) {
1287
1292
  if(gobj_is_running(priv.form)) {
1288
1293
  gobj_stop(priv.form);
@@ -1290,12 +1295,24 @@ function close_form_dialog(gobj)
1290
1295
  gobj_destroy(priv.form);
1291
1296
  priv.form = null;
1292
1297
  }
1293
- if(priv.$popup) {
1294
- if(priv.$popup.parentNode) {
1295
- priv.$popup.parentNode.removeChild(priv.$popup);
1296
- }
1297
- priv.$popup = null;
1298
+ priv.form_modal = null;
1299
+ }
1300
+
1301
+ /************************************************************
1302
+ * Force the dialog down (bypasses the unsaved guard): used after a
1303
+ * successful save and on teardown. Closing the modal runs its
1304
+ * on_close, which tears the form child down.
1305
+ ************************************************************/
1306
+ function close_form_dialog(gobj)
1307
+ {
1308
+ let priv = gobj.priv;
1309
+ if(priv.form_modal) {
1310
+ let modal = priv.form_modal;
1311
+ priv.form_modal = null;
1312
+ modal.close(); // -> on_close -> teardown_form_child
1313
+ return;
1298
1314
  }
1315
+ teardown_form_child(gobj);
1299
1316
  }
1300
1317
 
1301
1318
  /************************************************************
@@ -1719,6 +1736,37 @@ function show_dropdown_popup_menu(gobj, x, y, items, callback)
1719
1736
  /************************************************************
1720
1737
  * From external, at the beginning, load all topic data
1721
1738
  ************************************************************/
1739
+ /***************************************************************
1740
+ * The language changed (the shell publishes it after the app switched).
1741
+ *
1742
+ * The column headers carry their key (col_label's title formatter emits a
1743
+ * data-i18n span), so refresh_language() has already re-translated them.
1744
+ * What it cannot reach is what TABULATOR itself renders: its paginator, its
1745
+ * placeholder, its loading/error notices — drawn once, from its own lang
1746
+ * dict. Re-apply it in the new language.
1747
+ ***************************************************************/
1748
+ function ac_language_changed(gobj, event, kw, src)
1749
+ {
1750
+ let tabulator = gobj_read_attr(gobj, "tabulator");
1751
+ if(!tabulator) {
1752
+ return 0;
1753
+ }
1754
+ yui_tabulator_relocalize(tabulator, t);
1755
+ try {
1756
+ tabulator.options.placeholder =
1757
+ t("no data available", {defaultValue: "No data available"});
1758
+ /* Re-setting the locale makes Tabulator re-run the title formatter on
1759
+ * the EXISTING header cell, which APPENDS its span to the one already
1760
+ * there ("Device GroupDevice Group"). Rebuilding the columns from
1761
+ * their own definitions renders each header once, from scratch. */
1762
+ tabulator.setColumns(tabulator.getColumnDefinitions());
1763
+ } catch(e) {
1764
+ log_error(`${gobj_short_name(gobj)}: cannot re-render the table: ${e}`);
1765
+ return -1;
1766
+ }
1767
+ return 0;
1768
+ }
1769
+
1722
1770
  function ac_load_nodes(gobj, event, kw, src)
1723
1771
  {
1724
1772
  let data = kw;
@@ -2279,6 +2327,7 @@ function create_gclass(gclass_name)
2279
2327
  *---------------------------------------------*/
2280
2328
  const states = [
2281
2329
  ["ST_IDLE", [
2330
+ ["EV_LANGUAGE_CHANGED", ac_language_changed, null],
2282
2331
  ["EV_LOAD_NODES", ac_load_nodes, null],
2283
2332
  ["EV_LOAD_NODE_CREATED", ac_load_node_created, null],
2284
2333
  ["EV_LOAD_NODE_UPDATED", ac_load_node_updated, null],
@@ -2305,6 +2354,7 @@ function create_gclass(gclass_name)
2305
2354
  * Events
2306
2355
  *---------------------------------------------*/
2307
2356
  const event_types = [
2357
+ ["EV_LANGUAGE_CHANGED", 0],
2308
2358
  ["EV_LOAD_NODES", 0],
2309
2359
  ["EV_LOAD_NODE_CREATED", 0],
2310
2360
  ["EV_LOAD_NODE_UPDATED", 0],
@@ -0,0 +1,73 @@
1
+ /***********************************************************************
2
+ * c_yui_treedb_topics.css
3
+ *
4
+ * Styling for C_YUI_TREEDB_TOPICS' cards-landing (list -> detail):
5
+ * the topic cards (three hash-routed icon actions) and the routed
6
+ * topic-info panel. Theme-aware via Bulma variables.
7
+ *
8
+ * Copyright (c) 2026, ArtGins.
9
+ * All Rights Reserved.
10
+ ***********************************************************************/
11
+
12
+ /* One topic card: name on top, a row of icon actions below.
13
+ * The grid itself is the shell's .yui-nav-cards. */
14
+ .TREEDB_TOPIC_CARD {
15
+ display: flex;
16
+ flex-direction: column;
17
+ align-items: center;
18
+ gap: 0.5rem;
19
+ padding: 1rem 0.75rem;
20
+ border: 1px solid var(--bulma-border-weak, #dbdbdb);
21
+ border-radius: 0.5rem;
22
+ background: var(--bulma-scheme-main, #fff);
23
+ color: var(--bulma-text, inherit);
24
+ }
25
+ .TREEDB_TOPIC_CARD {
26
+ cursor: pointer;
27
+ }
28
+ /* Selected (clicked) card: link-coloured border + soft ring. */
29
+ .TREEDB_TOPIC_CARD.is-selected {
30
+ border-color: var(--bulma-link, #485fc7);
31
+ box-shadow: 0 0 0 2px var(--bulma-link, #485fc7) inset;
32
+ }
33
+ .TREEDB_TOPIC_CARD .TREEDB_TOPIC_CARD_NAME {
34
+ font-weight: 600;
35
+ text-align: center;
36
+ line-height: 1.2;
37
+ word-break: break-word;
38
+ }
39
+ .TREEDB_TOPIC_CARD_ACTIONS {
40
+ display: flex;
41
+ justify-content: center;
42
+ gap: 0.25rem;
43
+ }
44
+ .TREEDB_TOPIC_CARD_ACTIONS > a {
45
+ display: inline-flex;
46
+ align-items: center;
47
+ justify-content: center;
48
+ width: 2.25rem;
49
+ height: 2.25rem;
50
+ border-radius: 0.375rem;
51
+ color: var(--bulma-text, inherit);
52
+ text-decoration: none;
53
+ }
54
+ .TREEDB_TOPIC_CARD_ACTIONS > a .icon {
55
+ font-size: 1.15rem;
56
+ }
57
+ .TREEDB_TOPIC_CARD_ACTIONS > a:hover,
58
+ .TREEDB_TOPIC_CARD_ACTIONS > a:focus-visible {
59
+ background: var(--bulma-background, #f5f5f5);
60
+ color: var(--bulma-link, #485fc7);
61
+ }
62
+
63
+ /* Routed topic-info panel. */
64
+ .TREEDB_TOPIC_INFO {
65
+ padding: 1rem;
66
+ }
67
+ .TREEDB_TOPIC_INFO .TREEDB_TOPIC_INFO_CARD {
68
+ max-width: 48rem;
69
+ }
70
+ .TREEDB_TOPIC_INFO table code {
71
+ background: transparent;
72
+ padding: 0;
73
+ }