@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
@@ -0,0 +1,478 @@
1
+ /***********************************************************************
2
+ * c_yui_treedb_schema.js
3
+ *
4
+ * Schema-graph landing (prototype): the treedb drawn as a GRAPH OF
5
+ * TOPICS — one node per topic, one edge per hook/fkey relationship
6
+ * — from the schema `descs` alone (no data, no backend calls). A
7
+ * node click opens that topic's table (a real hash navigation via
8
+ * the host-supplied `node_route`). An alternate landing to the
9
+ * topic cards, in the spirit of "every treedb is a graph".
10
+ *
11
+ * Copyright (c) 2026, ArtGins.
12
+ * All Rights Reserved.
13
+ ***********************************************************************/
14
+
15
+ import {
16
+ SDATA,
17
+ SDATA_END,
18
+ data_type_t,
19
+ gclass_create,
20
+ log_error,
21
+ gobj_read_pointer_attr,
22
+ gobj_subscribe_event,
23
+ gobj_parent,
24
+ gobj_read_attr,
25
+ createElement2,
26
+ gobj_write_attr,
27
+ gobj_short_name,
28
+ gobj_read_bool_attr,
29
+ gobj_read_str_attr,
30
+ is_object,
31
+ } from "@yuneta/gobj-js";
32
+
33
+ import {Graph, NodeEvent} from "@antv/g6";
34
+
35
+ import {yui_is_dark, yui_watch_theme} from "./yui_theme.js";
36
+
37
+ import {t} from "i18next";
38
+
39
+ /***************************************************************
40
+ * Constants
41
+ ***************************************************************/
42
+ const GCLASS_NAME = "C_YUI_TREEDB_SCHEMA";
43
+
44
+ /***************************************************************
45
+ * Data
46
+ ***************************************************************/
47
+ const attrs_table = [
48
+ SDATA(data_type_t.DTP_POINTER, "subscriber", 0, null, "Subscriber of output events"),
49
+ SDATA(data_type_t.DTP_JSON, "descs", 0, null, "Treedb schema: {topic_name: desc}"),
50
+ SDATA(data_type_t.DTP_STRING, "node_route", 0, "", "Hash-route template with a {topic} placeholder: a node click opens that topic (e.g. '#/topics/db/<sel>/{topic}')"),
51
+ SDATA(data_type_t.DTP_BOOLEAN, "system", 0, false, "Include system topics (__*__) too"),
52
+ SDATA(data_type_t.DTP_POINTER, "$container", 0, null, "Root HTML element"),
53
+ SDATA_END()
54
+ ];
55
+
56
+ let PRIVATE_DATA = {
57
+ $container: null,
58
+ graph: null,
59
+ theme_observer: null, // MutationObserver on <html data-theme>
60
+ };
61
+
62
+ let __gclass__ = null;
63
+
64
+
65
+
66
+
67
+ /******************************
68
+ * Framework Methods
69
+ ******************************/
70
+
71
+
72
+
73
+
74
+ function mt_create(gobj)
75
+ {
76
+ build_ui(gobj);
77
+
78
+ /*
79
+ * CHILD subscription model
80
+ */
81
+ let subscriber = gobj_read_pointer_attr(gobj, "subscriber");
82
+ if(!subscriber) {
83
+ subscriber = gobj_parent(gobj);
84
+ }
85
+ gobj_subscribe_event(gobj, null, {}, subscriber);
86
+ }
87
+
88
+ function mt_start(gobj)
89
+ {
90
+ /* The graph picks its colours from the theme. Translate the DOM
91
+ * mutation into EV_THEME and let the action restyle the live graph
92
+ * (ac_theme) — a rebuild would reset the camera. */
93
+ gobj.priv.theme_observer = yui_watch_theme(gobj);
94
+
95
+ build_graph(gobj);
96
+ }
97
+
98
+ function mt_stop(gobj)
99
+ {
100
+ let priv = gobj.priv;
101
+
102
+ if(priv.theme_observer) {
103
+ priv.theme_observer.disconnect();
104
+ priv.theme_observer = null;
105
+ }
106
+ destroy_graph(gobj);
107
+ }
108
+
109
+ function mt_destroy(gobj)
110
+ {
111
+ let $container = gobj_read_attr(gobj, "$container");
112
+ if($container && $container.parentNode) {
113
+ $container.parentNode.removeChild($container);
114
+ }
115
+ gobj_write_attr(gobj, "$container", null);
116
+ }
117
+
118
+
119
+
120
+
121
+ /***************************
122
+ * Local Methods
123
+ ***************************/
124
+
125
+
126
+
127
+
128
+ /************************************************************
129
+ * Build UI
130
+ ************************************************************/
131
+ function build_ui(gobj)
132
+ {
133
+ let $container = createElement2(
134
+ ['div', {class: 'C_YUI_TREEDB_SCHEMA TREEDB_SCHEMA_VIEW',
135
+ style: 'position:relative; height:100%; min-height:0;'}, []]
136
+ );
137
+ gobj_write_attr(gobj, "$container", $container);
138
+ }
139
+
140
+ /************************************************************
141
+ * Derive {nodes, edges} from the schema.
142
+ * Node = a topic. Edge = a hook (parent -> child) or an
143
+ * fkey (child -> parent, reversed to parent -> child), deduped.
144
+ * Left-to-right dagre follows the parent -> child data flow.
145
+ ************************************************************/
146
+ function schema_to_graph(gobj)
147
+ {
148
+ let descs = gobj_read_attr(gobj, "descs");
149
+ let system = gobj_read_bool_attr(gobj, "system");
150
+ let nodes = [];
151
+ let topic_set = {};
152
+
153
+ if(!is_object(descs)) {
154
+ return {nodes: nodes, edges: []};
155
+ }
156
+
157
+ for(let topic of Object.keys(descs)) {
158
+ if(!system && topic.substring(0, 2) === "__") {
159
+ continue;
160
+ }
161
+ topic_set[topic] = true;
162
+ nodes.push({id: topic, data: {topic_name: topic}});
163
+ }
164
+
165
+ let edge_seen = {};
166
+ let edges = [];
167
+ let add_edge = (source, target) => {
168
+ if(!topic_set[source] || !topic_set[target] || source === target) {
169
+ return;
170
+ }
171
+ let key = source + "" + target;
172
+ if(edge_seen[key]) {
173
+ return;
174
+ }
175
+ edge_seen[key] = true;
176
+ edges.push({id: key, source: source, target: target});
177
+ };
178
+
179
+ for(let topic of Object.keys(descs)) {
180
+ let desc = descs[topic];
181
+ if(!is_object(desc) || !Array.isArray(desc.cols)) {
182
+ continue;
183
+ }
184
+ for(let col of desc.cols) {
185
+ if(!col) {
186
+ continue;
187
+ }
188
+ if(is_object(col.hook)) {
189
+ for(let child of Object.keys(col.hook)) {
190
+ add_edge(topic, child); /* parent -> child */
191
+ }
192
+ } else if(is_object(col.fkey)) {
193
+ for(let parent of Object.keys(col.fkey)) {
194
+ add_edge(parent, topic); /* parent -> child */
195
+ }
196
+ }
197
+ }
198
+ }
199
+
200
+ return {nodes: nodes, edges: edges};
201
+ }
202
+
203
+ /************************************************************
204
+ * The theme-dependent styles, in ONE place: they are applied
205
+ * as the graph is built and re-applied on a theme switch
206
+ * (ac_theme), which restyles the LIVE graph — rebuilding it
207
+ * would drop the user's zoom/pan and any dragged node.
208
+ ************************************************************/
209
+ function node_style(dark)
210
+ {
211
+ return {
212
+ size: 40,
213
+ fill: "#5B8FF9",
214
+ labelText: (d) => d.id,
215
+ labelPlacement: "bottom",
216
+ labelFill: dark ? "#e6e6e6" : "#333333",
217
+ labelBackground: true,
218
+ labelBackgroundFill: dark ? "rgba(0,0,0,0.5)" : "rgba(255,255,255,0.7)",
219
+ cursor: "pointer",
220
+ };
221
+ }
222
+
223
+ function edge_style(dark)
224
+ {
225
+ return {
226
+ stroke: dark ? "#666666" : "#bbbbbb",
227
+ endArrow: true,
228
+ };
229
+ }
230
+
231
+ /************************************************************
232
+ * Build the G6 schema graph.
233
+ ************************************************************/
234
+ function build_graph(gobj)
235
+ {
236
+ let priv = gobj.priv;
237
+ let $container = gobj_read_attr(gobj, "$container");
238
+ if(!$container) {
239
+ return;
240
+ }
241
+ destroy_graph(gobj);
242
+
243
+ let data = schema_to_graph(gobj);
244
+ if(data.nodes.length === 0) {
245
+ let $empty = createElement2(
246
+ ['div', {class: 'TREEDB_SCHEMA_EMPTY p-4 has-text-grey',
247
+ i18n: 'no topics'}, t('no topics', {defaultValue: 'No topics'})]
248
+ );
249
+ $container.appendChild($empty);
250
+ return;
251
+ }
252
+
253
+ let dark = yui_is_dark();
254
+ let graph;
255
+ try {
256
+ graph = new Graph({
257
+ container: $container,
258
+ autoResize: true,
259
+ data: data,
260
+ node: {
261
+ style: node_style(dark),
262
+ },
263
+ edge: {
264
+ style: edge_style(dark),
265
+ },
266
+ layout: {
267
+ type: "antv-dagre",
268
+ rankdir: "LR",
269
+ nodesep: 24,
270
+ ranksep: 60,
271
+ },
272
+ behaviors: ["zoom-canvas", "drag-canvas", "drag-element"],
273
+ });
274
+ } catch(e) {
275
+ log_error(`${gobj_short_name(gobj)}: schema graph create failed: ${e}`);
276
+ return;
277
+ }
278
+ priv.graph = graph;
279
+
280
+ graph.setTheme(dark ? "dark" : "light");
281
+
282
+ /* A node click opens that topic — a real hash navigation, so it is
283
+ * deep-linkable and Back-friendly like the cards. Crosses the FSM. */
284
+ graph.on(NodeEvent.CLICK, (evt) => {
285
+ let node_id = evt && evt.target && evt.target.id;
286
+ gobj_send_event(gobj, "EV_NODE_CLICK", {node_id: node_id}, gobj);
287
+ });
288
+
289
+ graph.render().then(() => {
290
+ try {
291
+ graph.fitView();
292
+ } catch(e) {
293
+ // best-effort centring
294
+ }
295
+ }).catch((e) => {
296
+ log_error(`${gobj_short_name(gobj)}: schema graph render failed: ${e}`);
297
+ });
298
+ }
299
+
300
+ /************************************************************
301
+ * Destroy the G6 graph.
302
+ ************************************************************/
303
+ function destroy_graph(gobj)
304
+ {
305
+ let priv = gobj.priv;
306
+ if(priv.graph) {
307
+ try {
308
+ priv.graph.destroy();
309
+ } catch(e) {
310
+ // already gone
311
+ }
312
+ priv.graph = null;
313
+ }
314
+ let $container = gobj_read_attr(gobj, "$container");
315
+ if($container) {
316
+ while($container.firstChild) {
317
+ $container.removeChild($container.firstChild);
318
+ }
319
+ }
320
+ }
321
+
322
+
323
+
324
+
325
+ /***************************
326
+ * Actions
327
+ ***************************/
328
+
329
+
330
+
331
+
332
+ /************************************************************
333
+ * A topic node was clicked: open its table via the host route.
334
+ ************************************************************/
335
+ function ac_node_click(gobj, event, kw, src)
336
+ {
337
+ let topic = kw && kw.node_id;
338
+ let route = gobj_read_str_attr(gobj, "node_route");
339
+ if(!topic || !route) {
340
+ return 0;
341
+ }
342
+ let href = route.replace("{topic}", topic);
343
+ if(typeof window !== "undefined") {
344
+ window.location.hash = href;
345
+ }
346
+ return 0;
347
+ }
348
+
349
+ /************************************************************
350
+ * Shown by the host: (re)fit the view now the container is
351
+ * visible and sized (G6 renders at 0×0 while display:none).
352
+ ************************************************************/
353
+ function ac_show(gobj, event, kw, src)
354
+ {
355
+ let priv = gobj.priv;
356
+ if(!priv.graph) {
357
+ build_graph(gobj);
358
+ return 0;
359
+ }
360
+ try {
361
+ priv.graph.fitView();
362
+ } catch(e) {
363
+ // best-effort
364
+ }
365
+ return 0;
366
+ }
367
+
368
+ /************************************************************
369
+ * {theme: "dark"|"light"} — the app switched theme.
370
+ * A restyle, not new data: repaint the LIVE graph and redraw,
371
+ * so the user's zoom/pan and dragged nodes survive. A rebuild
372
+ * is only right when there is no graph to restyle — the "no
373
+ * topics" empty state, whose message is built, not drawn.
374
+ ************************************************************/
375
+ function ac_theme(gobj, event, kw, src)
376
+ {
377
+ let priv = gobj.priv;
378
+ let graph = priv.graph;
379
+
380
+ if(!graph) {
381
+ build_graph(gobj);
382
+ return 0;
383
+ }
384
+
385
+ let dark = (kw && kw.theme) ? (kw.theme === "dark") : yui_is_dark();
386
+ try {
387
+ graph.setTheme(dark ? "dark" : "light");
388
+ graph.setNode({style: node_style(dark)});
389
+ graph.setEdge({style: edge_style(dark)});
390
+ graph.draw().catch((e) => {
391
+ log_error(`${gobj_short_name(gobj)}: schema graph redraw failed: ${e}`);
392
+ });
393
+ } catch(e) {
394
+ log_error(`${gobj_short_name(gobj)}: schema graph restyle failed: ${e}`);
395
+ }
396
+
397
+ return 0;
398
+ }
399
+
400
+ /************************************************************
401
+ * Rebuild from a fresh schema (descs arrived / changed).
402
+ ************************************************************/
403
+ function ac_rebuild(gobj, event, kw, src)
404
+ {
405
+ if(kw && is_object(kw.descs)) {
406
+ gobj_write_attr(gobj, "descs", kw.descs);
407
+ }
408
+ build_graph(gobj);
409
+ return 0;
410
+ }
411
+
412
+
413
+
414
+
415
+ /***************************
416
+ * FSM
417
+ ***************************/
418
+
419
+
420
+
421
+
422
+ const gmt = {
423
+ mt_create: mt_create,
424
+ mt_start: mt_start,
425
+ mt_stop: mt_stop,
426
+ mt_destroy: mt_destroy,
427
+ };
428
+
429
+ function create_gclass(gclass_name)
430
+ {
431
+ if(__gclass__) {
432
+ log_error(`GClass ALREADY created: ${gclass_name}`);
433
+ return -1;
434
+ }
435
+
436
+ const states = [
437
+ ["ST_IDLE", [
438
+ ["EV_NODE_CLICK", ac_node_click, null],
439
+ ["EV_SHOW", ac_show, null],
440
+ ["EV_THEME", ac_theme, null],
441
+ ["EV_REBUILD", ac_rebuild, null],
442
+ ]]
443
+ ];
444
+
445
+ const event_types = [
446
+ ["EV_NODE_CLICK", 0],
447
+ ["EV_SHOW", 0],
448
+ ["EV_THEME", 0],
449
+ ["EV_REBUILD", 0],
450
+ ];
451
+
452
+ __gclass__ = gclass_create(
453
+ gclass_name,
454
+ event_types,
455
+ states,
456
+ gmt,
457
+ 0, // lmt
458
+ attrs_table,
459
+ PRIVATE_DATA,
460
+ 0, // authz_table
461
+ 0, // command_table
462
+ 0, // s_user_trace_level
463
+ 0 // gclass_flag
464
+ );
465
+
466
+ if(!__gclass__) {
467
+ return -1;
468
+ }
469
+
470
+ return 0;
471
+ }
472
+
473
+ function register_c_yui_treedb_schema()
474
+ {
475
+ return create_gclass(GCLASS_NAME);
476
+ }
477
+
478
+ export {register_c_yui_treedb_schema};
@@ -46,52 +46,17 @@
46
46
  min-width: 0;
47
47
  }
48
48
 
49
- /* Moved from c_yui_main.css (2.6.1): the responsive edit-dialog card
50
- * and the utilities this gclass tags on its DOM.
51
- * Self-contained here so every consumer gets it without the
52
- * legacy stack's stylesheet. */
53
-
54
- /* Custom class to adjust the modal width */
55
- .modal-card.modal-is-responsive {
56
- width: 100%;
57
- max-width: 100%;
58
- }
59
-
60
- @media screen and (min-width: 768px) {
61
- .modal-card.modal-is-responsive {
62
- width: 50rem;
49
+ /* The edit/create form runs in the shell's standardized adaptive dialog
50
+ * (yui_shell_show_modal dialog:true, logical_class TREEDB_FORM_SHEET). The
51
+ * shell CSS already gives it the centred card / full-screen-mobile sheet;
52
+ * here we only widen it on desktop — a label+field record form wants more
53
+ * room than the default 640px. Mobile stays edge-to-edge (shell rule). */
54
+ @media screen and (min-width: 769px) {
55
+ .TREEDB_FORM_SHEET .yui-dialog-content {
63
56
  max-width: 50rem;
64
57
  }
65
58
  }
66
59
 
67
- /*
68
- * Mobile (< 768px, dovetails with the desktop rule above): a
69
- * `modal-is-responsive` card (the C_YUI_TREEDB edit/delete form
70
- * popup, and any other consumer of this opt-in class) takes the
71
- * WHOLE screen instead of a centred sheet — more room to edit on a
72
- * phone. Confirm dialogs do not carry `modal-is-responsive`, so
73
- * they stay compact. Edge-to-edge: full size, no margin, flat
74
- * corners; the body keeps Bulma's own scroll.
75
- */
76
- @media screen and (max-width: 767px) {
77
- .modal-card.modal-is-responsive {
78
- width: 100%;
79
- max-width: 100%;
80
- height: 100%;
81
- max-height: 100%;
82
- margin: 0;
83
- border-radius: 0;
84
- }
85
- .modal-card.modal-is-responsive .modal-card-head,
86
- .modal-card.modal-is-responsive .modal-card-foot {
87
- border-radius: 0;
88
- }
89
- .modal-card.modal-is-responsive .modal-card-body {
90
- flex: 1 1 auto;
91
- overflow-y: auto;
92
- }
93
- }
94
-
95
60
  .without-border {
96
61
  border: none !important;
97
62
  box-shadow: none !important;