@yuneta/gobj-ui 3.0.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 (45) hide show
  1. package/README.md +324 -6
  2. package/dist/gobj-ui.cjs.js +10891 -5006
  3. package/dist/gobj-ui.es.js +10915 -5064
  4. package/index.js +45 -1
  5. package/package.json +4 -4
  6. package/src/c_g6_nodes_tree.js +128 -40
  7. package/src/c_yui_form.js +112 -53
  8. package/src/c_yui_gobj_tree_js.js +58 -36
  9. package/src/c_yui_json.css +139 -0
  10. package/src/c_yui_json.js +928 -0
  11. package/src/c_yui_json_graph.js +110 -20
  12. package/src/c_yui_map.js +18 -36
  13. package/src/c_yui_nav.js +6 -9
  14. package/src/c_yui_period.css +184 -0
  15. package/src/c_yui_period.js +1441 -0
  16. package/src/c_yui_shell.css +33 -0
  17. package/src/c_yui_shell.js +672 -111
  18. package/src/c_yui_treedb_graph.js +639 -37
  19. package/src/c_yui_treedb_schema.js +478 -0
  20. package/src/c_yui_treedb_topic_with_form.css +7 -42
  21. package/src/c_yui_treedb_topic_with_form.js +152 -103
  22. package/src/c_yui_treedb_topics.css +73 -0
  23. package/src/c_yui_treedb_topics.js +1070 -29
  24. package/src/c_yui_window.js +180 -97
  25. package/src/c_yui_window_manager.js +38 -4
  26. package/src/json_view_helpers.js +214 -0
  27. package/src/json_view_helpers.test.js +135 -0
  28. package/src/route_map_model.js +317 -0
  29. package/src/route_map_model.test.js +209 -0
  30. package/src/route_resolver.js +24 -1
  31. package/src/route_resolver.test.js +40 -1
  32. package/src/shell_modals.js +117 -39
  33. package/src/shell_route_map.css +225 -0
  34. package/src/shell_route_map.js +363 -0
  35. package/src/tabulator.css +67 -0
  36. package/src/yui_dev.js +130 -8
  37. package/src/yui_frontend_view.js +106 -0
  38. package/src/yui_icons.css +62 -0
  39. package/src/yui_inputs.css +8 -3
  40. package/src/yui_inputs.js +51 -8
  41. package/src/yui_tabulator_i18n.js +128 -0
  42. package/src/yui_theme.js +147 -0
  43. package/src/yui_time.js +666 -0
  44. package/src/yui_time.test.js +330 -0
  45. package/src/yui_toolbar.js +86 -43
@@ -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,15 +61,17 @@ 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
 
@@ -119,7 +122,7 @@ SDATA(data_type_t.DTP_JSON, "tabulator_settings", 0, {
119
122
  pagination: true,
120
123
  paginationSize: 25,
121
124
  paginationSizeSelector: [25, 50, 100, true],
122
- placeholder: "No data available",
125
+ placeholder: "No data available", /* overridden per-language at create */
123
126
  maxHeight: "100%"
124
127
  }, "Default settings for Tabulator"),
125
128
 
@@ -139,10 +142,7 @@ let PRIVATE_DATA = {
139
142
  topic_name: "",
140
143
  tabulator: null,
141
144
  form: null, // hosted C_YUI_FORM child (while dialog open)
142
- $popup: null, // dialog DOM element
143
- on_keydown: null, // document Escape handler (no-shell fallback)
144
- escape_shell: null, // shell whose Escape chain holds escape_handler
145
- escape_handler: null, // close handler pushed on the shell chain
145
+ form_modal: null, // { close } handle of the adaptive dialog
146
146
  };
147
147
 
148
148
  let __gclass__ = null;
@@ -200,6 +200,14 @@ function mt_create(gobj)
200
200
  ***************************************************************/
201
201
  function mt_start(gobj)
202
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
+ }
203
211
  create_tabulator(gobj);
204
212
  }
205
213
 
@@ -208,6 +216,10 @@ function mt_start(gobj)
208
216
  ***************************************************************/
209
217
  function mt_stop(gobj)
210
218
  {
219
+ let shell = yui_shell_of(gobj);
220
+ if(shell) {
221
+ gobj_unsubscribe_event(shell, "EV_LANGUAGE_CHANGED", {}, gobj);
222
+ }
211
223
  close_form_dialog(gobj);
212
224
  table__destroy(gobj);
213
225
  }
@@ -459,7 +471,7 @@ function build_ui(gobj)
459
471
  if(with_search_button) {
460
472
  let search_id = `${toolbar_id}_search`;
461
473
  let $search_box = createElement2(
462
- ['div', {class: 'control has-icons-left has-icons-right mr-1'}, [
474
+ ['div', {class: 'control has-icons-left mr-1'}, [
463
475
  ['input', {
464
476
  id: search_id,
465
477
  class: 'input',
@@ -469,19 +481,13 @@ function build_ui(gobj)
469
481
  }],
470
482
  ['span', {class: 'icon is-left'}, [
471
483
  ['i', {class: 'yi-magnifying-glass'}]
472
- ]],
473
- ['span', {class: 'icon is-right', style: 'cursor:pointer; pointer-events:all;', title: 'clear search'}, [
474
- ['i', {class: 'yi-xmark'}]
475
- ], {
476
- 'click': (event) => {
477
- event.stopPropagation();
478
- let $input = document.getElementById(search_id);
479
- if($input) { $input.value = ''; $input.dispatchEvent(new Event('input')); }
480
- }
481
- }]
484
+ ]]
482
485
  ]]
483
486
  );
484
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);
485
491
  if($search_input) {
486
492
  $search_input.addEventListener('input', (event) => {
487
493
  let searchText = event.target.value.trim().toLowerCase();
@@ -511,7 +517,7 @@ function build_ui(gobj)
511
517
 
512
518
  if(with_refresh_button) {
513
519
  let $refresh = createElement2(
514
- ['button', {class: 'button mr-1', title: 'refresh'}, [
520
+ ['button', {class: 'button mr-1', title: t('refresh'), 'data-i18n-title': 'refresh'}, [
515
521
  ['i', {class: 'yi-arrows-rotate'}],
516
522
  ['span', {class: 'is-hidden-mobile', i18n: 'refresh', style: 'padding-left:5px;'}, 'refresh']
517
523
  ], {
@@ -820,7 +826,12 @@ function create_tabulator(gobj)
820
826
  }
821
827
 
822
828
  let pkey = desc.pkey || "id";
823
- 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);
824
835
 
825
836
  let tabulator_settings = json_deep_copy(gobj_read_attr(gobj, "tabulator_settings"));
826
837
 
@@ -848,7 +859,26 @@ function create_tabulator(gobj)
848
859
  log_error(`${gobj_short_name(gobj)}: table element '#${table_id}' not found in $container`);
849
860
  return;
850
861
  }
851
- 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
+ }
852
882
 
853
883
  /* Keep the footer in sync with the visible (active) row count,
854
884
  * and hide the pagination chrome while everything fits in one
@@ -1134,9 +1164,14 @@ function open_form_dialog(gobj, mode, record)
1134
1164
 
1135
1165
  let priv = gobj.priv;
1136
1166
  let desc = gobj_read_attr(gobj, "desc");
1137
- let popup_id = gobj_read_str_attr(gobj, "popup_id");
1138
1167
  let topic_name = gobj_read_str_attr(gobj, "topic_name");
1139
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
+
1140
1175
  /* Title says what you are doing: "new <topic>" on create,
1141
1176
  * "<topic> — <pkey>" on update. */
1142
1177
  let pkey = desc.pkey || "id";
@@ -1148,36 +1183,12 @@ function open_form_dialog(gobj, mode, record)
1148
1183
  title = t(topic_name) + (empty_string(rid) ? "" : " — " + rid);
1149
1184
  }
1150
1185
 
1151
- let $element = createElement2([
1152
- 'div', {id: popup_id, class: 'modal is-active'}, [
1153
- ['div', {class: 'modal-background'}, ''],
1154
- ['div', {class: 'modal-card modal-is-responsive'}, [
1155
- ['header', {class: 'modal-card-head p-3'}, [
1156
- ['p', {class: 'modal-card-title', style: 'text-align:center;'}, title],
1157
- ['button', {class: 'delete is-large', 'aria-label': 'close'}, '', {
1158
- click: function(evt) {
1159
- evt.stopPropagation();
1160
- request_close_form_dialog(gobj);
1161
- }
1162
- }]
1163
- ]],
1164
- /* flex column so the hosted form takes the dialog
1165
- * height: fields scroll internally and the form's
1166
- * bottom toolbar stays visible (dialog footer) */
1167
- ['section', {
1168
- class: 'modal-card-body p-2',
1169
- style: 'display:flex; flex-direction:column;'
1170
- }, []]
1171
- ]]
1172
- ], {
1173
- click: function(evt) {
1174
- // Capture bubble events
1175
- evt.stopPropagation();
1176
- }
1177
- }
1178
- ]);
1179
-
1180
- 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
+ );
1181
1192
 
1182
1193
  let form = gobj_create_pure_child(
1183
1194
  "form_" + clean_name(gobj_name(gobj)),
@@ -1187,6 +1198,10 @@ function open_form_dialog(gobj, mode, record)
1187
1198
  record: record,
1188
1199
  fkey_options: build_fkey_options(gobj),
1189
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",
1190
1205
  pkey: desc.pkey || "id",
1191
1206
  topic_name: topic_name,
1192
1207
  editable: true,
@@ -1195,7 +1210,6 @@ function open_form_dialog(gobj, mode, record)
1195
1210
  gobj
1196
1211
  );
1197
1212
  priv.form = form;
1198
- priv.$popup = $element;
1199
1213
 
1200
1214
  let $form_container = gobj_read_attr(form, "$container");
1201
1215
  if($form_container) {
@@ -1203,38 +1217,33 @@ function open_form_dialog(gobj, mode, record)
1203
1217
  $form_container.style.minHeight = "0";
1204
1218
  }
1205
1219
 
1206
- popup_mount_layer(gobj).appendChild($element);
1207
- refresh_language($element, t);
1208
-
1209
- /* Escape closes through the same unsaved-changes guard as the X.
1210
- * Under a shell, ride its Escape priority chain LIFO with the
1211
- * shell confirm dialogs, so Escape on an open confirm never
1212
- * reaches this dialog and stacks a second one. Without a shell
1213
- * (bare unit tests) fall back to a document listener. */
1214
- let shell = yui_shell_of(gobj);
1215
- if(shell) {
1216
- priv.escape_shell = shell;
1217
- priv.escape_handler = function() {
1218
- request_close_form_dialog(gobj);
1219
- };
1220
- yui_shell_push_escape(shell, "popup", priv.escape_handler);
1221
- } else {
1222
- priv.on_keydown = function(evt) {
1223
- if(evt.key === "Escape") {
1224
- evt.stopPropagation();
1225
- request_close_form_dialog(gobj);
1226
- }
1227
- };
1228
- document.addEventListener("keydown", priv.on_keydown);
1229
- }
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
+ });
1230
1238
 
1231
- 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
1232
1241
 
1233
1242
  /* Focus the pkey unless the mode made it readonly (update) —
1234
1243
  * then the first editable field. */
1235
- let $with_focus = $element.querySelector('.with-focus:not([readonly])');
1244
+ let $with_focus = $body.querySelector('.with-focus:not([readonly])');
1236
1245
  if(!$with_focus) {
1237
- $with_focus = $element.querySelector(
1246
+ $with_focus = $body.querySelector(
1238
1247
  'input.input:not([readonly]), textarea, select'
1239
1248
  );
1240
1249
  }
@@ -1244,10 +1253,12 @@ function open_form_dialog(gobj, mode, record)
1244
1253
  }
1245
1254
 
1246
1255
  /************************************************************
1247
- * Close request from the dialog X: if the form carries
1248
- * 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).
1249
1260
  ************************************************************/
1250
- function request_close_form_dialog(gobj)
1261
+ function form_may_close(gobj)
1251
1262
  {
1252
1263
  let priv = gobj.priv;
1253
1264
  if(priv.form) {
@@ -1259,31 +1270,24 @@ function request_close_form_dialog(gobj)
1259
1270
  'All changes will be lost. Are you sure?',
1260
1271
  {t: t, yes_label: "yes", no_label: "no", cancel_label: "cancel"}
1261
1272
  ).then(function(answer) {
1262
- if(answer === "yes") {
1263
- close_form_dialog(gobj);
1273
+ if(answer === "yes" && priv.form_modal) {
1274
+ priv.form_modal.close();
1264
1275
  }
1265
1276
  });
1266
- return;
1277
+ return false;
1267
1278
  }
1268
1279
  }
1269
- close_form_dialog(gobj);
1280
+ return true;
1270
1281
  }
1271
1282
 
1272
1283
  /************************************************************
1273
- * 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.
1274
1287
  ************************************************************/
1275
- function close_form_dialog(gobj)
1288
+ function teardown_form_child(gobj)
1276
1289
  {
1277
1290
  let priv = gobj.priv;
1278
- if(priv.escape_handler) {
1279
- yui_shell_pop_escape(priv.escape_shell, priv.escape_handler);
1280
- priv.escape_shell = null;
1281
- priv.escape_handler = null;
1282
- }
1283
- if(priv.on_keydown) {
1284
- document.removeEventListener("keydown", priv.on_keydown);
1285
- priv.on_keydown = null;
1286
- }
1287
1291
  if(priv.form) {
1288
1292
  if(gobj_is_running(priv.form)) {
1289
1293
  gobj_stop(priv.form);
@@ -1291,12 +1295,24 @@ function close_form_dialog(gobj)
1291
1295
  gobj_destroy(priv.form);
1292
1296
  priv.form = null;
1293
1297
  }
1294
- if(priv.$popup) {
1295
- if(priv.$popup.parentNode) {
1296
- priv.$popup.parentNode.removeChild(priv.$popup);
1297
- }
1298
- 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;
1299
1314
  }
1315
+ teardown_form_child(gobj);
1300
1316
  }
1301
1317
 
1302
1318
  /************************************************************
@@ -1720,6 +1736,37 @@ function show_dropdown_popup_menu(gobj, x, y, items, callback)
1720
1736
  /************************************************************
1721
1737
  * From external, at the beginning, load all topic data
1722
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
+
1723
1770
  function ac_load_nodes(gobj, event, kw, src)
1724
1771
  {
1725
1772
  let data = kw;
@@ -2280,6 +2327,7 @@ function create_gclass(gclass_name)
2280
2327
  *---------------------------------------------*/
2281
2328
  const states = [
2282
2329
  ["ST_IDLE", [
2330
+ ["EV_LANGUAGE_CHANGED", ac_language_changed, null],
2283
2331
  ["EV_LOAD_NODES", ac_load_nodes, null],
2284
2332
  ["EV_LOAD_NODE_CREATED", ac_load_node_created, null],
2285
2333
  ["EV_LOAD_NODE_UPDATED", ac_load_node_updated, null],
@@ -2306,6 +2354,7 @@ function create_gclass(gclass_name)
2306
2354
  * Events
2307
2355
  *---------------------------------------------*/
2308
2356
  const event_types = [
2357
+ ["EV_LANGUAGE_CHANGED", 0],
2309
2358
  ["EV_LOAD_NODES", 0],
2310
2359
  ["EV_LOAD_NODE_CREATED", 0],
2311
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
+ }