@yuneta/gobj-ui 1.0.1 → 2.2.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.
- package/README.md +40 -363
- package/dist/gobj-ui.cjs.js +11154 -5506
- package/dist/gobj-ui.es.js +11131 -5508
- package/index.js +41 -14
- package/package.json +11 -9
- package/src/c_g6_nodes_tree.js +6 -1
- package/src/c_yui_form.js +1 -1
- package/src/c_yui_gobj_tree_js.js +1 -1
- package/src/c_yui_json_graph.js +1 -1
- package/src/c_yui_main.js +3 -3
- package/src/c_yui_map.js +6 -1
- package/src/c_yui_nav.js +881 -0
- package/src/c_yui_pager.js +545 -0
- package/src/c_yui_routing.css +1 -1
- package/src/c_yui_routing.js +1 -1
- package/src/c_yui_shell.css +571 -0
- package/src/c_yui_shell.js +2474 -0
- package/src/c_yui_tabs.js +1 -1
- package/src/c_yui_treedb_graph.js +35 -9
- package/src/c_yui_treedb_topic_with_form.js +1 -1
- package/src/c_yui_treedb_topics.js +36 -8
- package/src/c_yui_uplot.js +1 -1
- package/src/c_yui_window.js +242 -21
- package/src/c_yui_window_manager.js +602 -0
- package/src/c_yui_wizard.js +612 -0
- package/src/pager_helpers.js +138 -0
- package/src/pager_helpers.test.js +140 -0
- package/src/route_resolver.js +53 -0
- package/src/route_resolver.test.js +82 -0
- package/src/shell_focus_trap.js +123 -0
- package/src/shell_focus_trap.test.js +299 -0
- package/src/shell_modals.js +445 -0
- package/src/shell_show_on.js +91 -0
- package/src/shell_show_on.test.js +86 -0
- package/src/shell_toolbar_helpers.js +221 -0
- package/src/shell_toolbar_helpers.test.js +207 -0
- package/src/tabulator.css +53 -0
- package/src/wizard_helpers.js +117 -0
- package/src/wizard_helpers.test.js +122 -0
- package/src/yui_dev.js +1257 -362
- package/src/yui_icons.css +5 -0
- package/src/yui_inputs.css +32 -0
- package/src/yui_inputs.js +71 -0
- package/vite-plugin-yuneta-html.js +2 -2
- package/skeleton/config.json +0 -20
- package/skeleton/index.html +0 -37
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
/***********************************************************************
|
|
2
|
+
* shell_toolbar_helpers.js
|
|
3
|
+
*
|
|
4
|
+
* Pure helpers for the C_YUI_SHELL declarative toolbar. Split out
|
|
5
|
+
* of c_yui_shell.js so the additive item types ("brand", "avatar")
|
|
6
|
+
* and the new "dropdown" action type can be unit-tested without a
|
|
7
|
+
* DOM.
|
|
8
|
+
*
|
|
9
|
+
* Three responsibilities:
|
|
10
|
+
* - classify_toolbar_item() — pick the renderer kind
|
|
11
|
+
* - validate_toolbar_item() — shape check, returns warnings
|
|
12
|
+
* - validate_dropdown_action() — shape check for dropdowns
|
|
13
|
+
*
|
|
14
|
+
* Item kinds:
|
|
15
|
+
* "brand" — logo (img) + wordmark (text); default action navigate
|
|
16
|
+
* "avatar" — circular initials, populated by an app-registered
|
|
17
|
+
* provider callback (yui_shell_set_avatar_provider)
|
|
18
|
+
* "action" — every other toolbar item: icon and/or label that
|
|
19
|
+
* triggers an action.type on click
|
|
20
|
+
*
|
|
21
|
+
* Action types accepted on a toolbar item or on a dropdown sub-item:
|
|
22
|
+
* "navigate" | "drawer" | "event" | "dropdown"
|
|
23
|
+
*
|
|
24
|
+
* Copyright (c) 2026, ArtGins.
|
|
25
|
+
* All Rights Reserved.
|
|
26
|
+
***********************************************************************/
|
|
27
|
+
|
|
28
|
+
export const TOOLBAR_ITEM_KINDS = ["brand", "avatar", "connection", "action"];
|
|
29
|
+
export const TOOLBAR_ACTION_TYPES = ["navigate", "drawer", "event", "dropdown"];
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
/************************************************************
|
|
33
|
+
* Decide which renderer to use for a toolbar item.
|
|
34
|
+
*
|
|
35
|
+
* Returns one of "brand" | "avatar" | "action". Items with
|
|
36
|
+
* no `type` (legacy contract) classify as "action" so existing
|
|
37
|
+
* configs keep working unchanged.
|
|
38
|
+
************************************************************/
|
|
39
|
+
export function classify_toolbar_item(item)
|
|
40
|
+
{
|
|
41
|
+
if(!item || typeof item !== "object") {
|
|
42
|
+
return "action";
|
|
43
|
+
}
|
|
44
|
+
let t = item.type;
|
|
45
|
+
if(t === "brand" || t === "avatar" || t === "connection") {
|
|
46
|
+
return t;
|
|
47
|
+
}
|
|
48
|
+
return "action";
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
/************************************************************
|
|
53
|
+
* Validate one toolbar item. Returns { ok, warnings }.
|
|
54
|
+
*
|
|
55
|
+
* Lenient by design: unknown fields are ignored so apps can
|
|
56
|
+
* carry extra metadata. We only flag shapes that would render
|
|
57
|
+
* visibly broken (brand without logo/wordmark, dropdown without
|
|
58
|
+
* items, unknown action.type, etc.).
|
|
59
|
+
************************************************************/
|
|
60
|
+
export function validate_toolbar_item(item)
|
|
61
|
+
{
|
|
62
|
+
let warnings = [];
|
|
63
|
+
if(!item || typeof item !== "object") {
|
|
64
|
+
warnings.push("toolbar item must be an object");
|
|
65
|
+
return { ok: false, warnings: warnings };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
let kind = classify_toolbar_item(item);
|
|
69
|
+
let id = item.id || "<no-id>";
|
|
70
|
+
|
|
71
|
+
if(kind === "brand") {
|
|
72
|
+
if(!_is_non_empty_string(item.logo)) {
|
|
73
|
+
warnings.push(
|
|
74
|
+
`toolbar item '${id}' (type:brand) requires 'logo' (image URL)`
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
if(!_is_non_empty_string(item.wordmark)) {
|
|
78
|
+
warnings.push(
|
|
79
|
+
`toolbar item '${id}' (type:brand) requires 'wordmark' (text)`
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/* avatar has no required fields beyond the optional action — the
|
|
85
|
+
* initials come from a runtime provider, not the JSON. */
|
|
86
|
+
|
|
87
|
+
/* Validate the action shape, including the new "dropdown" type. */
|
|
88
|
+
if(item.action !== undefined && item.action !== null) {
|
|
89
|
+
let nested = validate_action(item.action, id);
|
|
90
|
+
for(let w of nested) {
|
|
91
|
+
warnings.push(w);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/* Optional secondary (right-click) action — same shape as action,
|
|
96
|
+
* used e.g. by a connection indicator to open a dev panel. */
|
|
97
|
+
if(item.context_action !== undefined && item.context_action !== null) {
|
|
98
|
+
let nested = validate_action(item.context_action, id);
|
|
99
|
+
for(let w of nested) {
|
|
100
|
+
warnings.push(w);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return { ok: warnings.length === 0, warnings: warnings };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
/************************************************************
|
|
109
|
+
* Validate an `action` object (toolbar item action or
|
|
110
|
+
* dropdown sub-item action). Returns an array of warnings.
|
|
111
|
+
************************************************************/
|
|
112
|
+
export function validate_action(action, owner_id)
|
|
113
|
+
{
|
|
114
|
+
let warnings = [];
|
|
115
|
+
if(!action || typeof action !== "object") {
|
|
116
|
+
return warnings;
|
|
117
|
+
}
|
|
118
|
+
let id = owner_id || "<no-id>";
|
|
119
|
+
|
|
120
|
+
if(action.type === undefined || action.type === null) {
|
|
121
|
+
/* No type at all is also caught at runtime by handle_toolbar_action,
|
|
122
|
+
* but flag it here so config errors surface during validate_config. */
|
|
123
|
+
warnings.push(`toolbar item '${id}' has action without 'type'`);
|
|
124
|
+
return warnings;
|
|
125
|
+
}
|
|
126
|
+
if(TOOLBAR_ACTION_TYPES.indexOf(action.type) < 0) {
|
|
127
|
+
warnings.push(
|
|
128
|
+
`toolbar item '${id}' has unknown action.type '${action.type}'; ` +
|
|
129
|
+
`valid: ${TOOLBAR_ACTION_TYPES.join(", ")}`
|
|
130
|
+
);
|
|
131
|
+
return warnings;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if(action.type === "navigate" && !_is_non_empty_string(action.route)) {
|
|
135
|
+
warnings.push(
|
|
136
|
+
`toolbar item '${id}' (action.type:navigate) requires 'route'`
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
if(action.type === "event" && !_is_non_empty_string(action.event)) {
|
|
140
|
+
warnings.push(
|
|
141
|
+
`toolbar item '${id}' (action.type:event) requires 'event' name`
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
if(action.type === "dropdown") {
|
|
145
|
+
let nested = validate_dropdown_action(action, id);
|
|
146
|
+
for(let w of nested) {
|
|
147
|
+
warnings.push(w);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return warnings;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
/************************************************************
|
|
156
|
+
* Validate the items of a dropdown action.
|
|
157
|
+
*
|
|
158
|
+
* Each entry is either:
|
|
159
|
+
* { type: "divider" [, show_on] }
|
|
160
|
+
* { id, name?, icon?, action, show_on? }
|
|
161
|
+
*
|
|
162
|
+
* Nested dropdowns are NOT supported: an "action.type": "dropdown"
|
|
163
|
+
* inside a dropdown sub-item is flagged.
|
|
164
|
+
************************************************************/
|
|
165
|
+
export function validate_dropdown_action(action, owner_id)
|
|
166
|
+
{
|
|
167
|
+
let warnings = [];
|
|
168
|
+
let id = owner_id || "<no-id>";
|
|
169
|
+
|
|
170
|
+
if(!Array.isArray(action.items)) {
|
|
171
|
+
warnings.push(
|
|
172
|
+
`toolbar item '${id}' (action.type:dropdown) requires items[]`
|
|
173
|
+
);
|
|
174
|
+
return warnings;
|
|
175
|
+
}
|
|
176
|
+
if(action.items.length === 0) {
|
|
177
|
+
warnings.push(
|
|
178
|
+
`toolbar item '${id}' (action.type:dropdown) has empty items[]`
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
for(let i = 0; i < action.items.length; i++) {
|
|
183
|
+
let it = action.items[i];
|
|
184
|
+
let where = `${id}[${i}]`;
|
|
185
|
+
if(!it || typeof it !== "object") {
|
|
186
|
+
warnings.push(`dropdown item ${where} must be an object`);
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
if(it.type === "divider") {
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
if(!it.action || typeof it.action !== "object") {
|
|
193
|
+
warnings.push(
|
|
194
|
+
`dropdown item ${where} requires an action object`
|
|
195
|
+
);
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
if(it.action.type === "dropdown") {
|
|
199
|
+
warnings.push(
|
|
200
|
+
`dropdown item ${where} has action.type:dropdown — ` +
|
|
201
|
+
`nested dropdowns are not supported`
|
|
202
|
+
);
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
let nested = validate_action(it.action, where);
|
|
206
|
+
for(let w of nested) {
|
|
207
|
+
warnings.push(w);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return warnings;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
/************************************************************
|
|
216
|
+
* Internal — non-empty trimmed string check.
|
|
217
|
+
************************************************************/
|
|
218
|
+
function _is_non_empty_string(v)
|
|
219
|
+
{
|
|
220
|
+
return typeof v === "string" && v.trim().length > 0;
|
|
221
|
+
}
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
/***********************************************************************
|
|
2
|
+
* shell_toolbar_helpers.test.mjs
|
|
3
|
+
*
|
|
4
|
+
* Unit tests for the pure toolbar helpers used by C_YUI_SHELL.
|
|
5
|
+
* Run with: node --test tests/
|
|
6
|
+
***********************************************************************/
|
|
7
|
+
import { test, expect } from "vitest";
|
|
8
|
+
import {
|
|
9
|
+
TOOLBAR_ITEM_KINDS,
|
|
10
|
+
TOOLBAR_ACTION_TYPES,
|
|
11
|
+
classify_toolbar_item,
|
|
12
|
+
validate_toolbar_item,
|
|
13
|
+
validate_action,
|
|
14
|
+
validate_dropdown_action,
|
|
15
|
+
} from "./shell_toolbar_helpers.js";
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
/*============================================================
|
|
19
|
+
* classify_toolbar_item
|
|
20
|
+
*============================================================*/
|
|
21
|
+
test("classify: legacy item without 'type' is 'action'", () => {
|
|
22
|
+
expect(classify_toolbar_item({ id: "home", icon: "yi-home" })).toBe("action");
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test("classify: type:'brand' classifies as 'brand'", () => {
|
|
26
|
+
expect(classify_toolbar_item({ id: "b", type: "brand", logo: "/x.svg", wordmark: "X" })).toBe("brand");
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test("classify: type:'avatar' classifies as 'avatar'", () => {
|
|
30
|
+
expect(classify_toolbar_item({ id: "u", type: "avatar" })).toBe("avatar");
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test("classify: type:'connection' classifies as 'connection'", () => {
|
|
34
|
+
expect(classify_toolbar_item({ id: "conn", type: "connection" })).toBe("connection");
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test("validate: bad context_action surfaces a warning", () => {
|
|
38
|
+
let r = validate_toolbar_item({
|
|
39
|
+
id: "conn", type: "connection",
|
|
40
|
+
context_action: { type: "bogus" }
|
|
41
|
+
});
|
|
42
|
+
expect(r.ok).toBe(false);
|
|
43
|
+
expect(r.warnings.some(w => w.includes("unknown action.type"))).toBeTruthy();
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test("classify: unknown type falls back to 'action'", () => {
|
|
47
|
+
expect(classify_toolbar_item({ id: "?", type: "phantom" })).toBe("action");
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test("classify: null/undefined are 'action'", () => {
|
|
51
|
+
expect(classify_toolbar_item(null)).toBe("action");
|
|
52
|
+
expect(classify_toolbar_item(undefined)).toBe("action");
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
/*============================================================
|
|
57
|
+
* validate_toolbar_item — brand
|
|
58
|
+
*============================================================*/
|
|
59
|
+
test("validate brand: ok with logo + wordmark", () => {
|
|
60
|
+
let r = validate_toolbar_item({
|
|
61
|
+
id: "brand", type: "brand",
|
|
62
|
+
logo: "/wm.svg", wordmark: "Wattyzer",
|
|
63
|
+
action: { type: "navigate", route: "/welcome" }
|
|
64
|
+
});
|
|
65
|
+
expect(r.ok).toBe(true);
|
|
66
|
+
expect(r.warnings).toEqual([]);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test("validate brand: missing logo flags warning", () => {
|
|
70
|
+
let r = validate_toolbar_item({
|
|
71
|
+
id: "brand", type: "brand", wordmark: "X"
|
|
72
|
+
});
|
|
73
|
+
expect(r.ok).toBe(false);
|
|
74
|
+
expect(r.warnings.join("\n")).toMatch(/requires 'logo'/);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test("validate brand: missing wordmark flags warning", () => {
|
|
78
|
+
let r = validate_toolbar_item({
|
|
79
|
+
id: "brand", type: "brand", logo: "/x.svg"
|
|
80
|
+
});
|
|
81
|
+
expect(r.ok).toBe(false);
|
|
82
|
+
expect(r.warnings.join("\n")).toMatch(/requires 'wordmark'/);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test("validate brand: empty-string fields count as missing", () => {
|
|
86
|
+
let r = validate_toolbar_item({
|
|
87
|
+
id: "brand", type: "brand", logo: " ", wordmark: ""
|
|
88
|
+
});
|
|
89
|
+
expect(r.ok).toBe(false);
|
|
90
|
+
expect(r.warnings.length).toBe(2);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
/*============================================================
|
|
95
|
+
* validate_toolbar_item — avatar
|
|
96
|
+
*============================================================*/
|
|
97
|
+
test("validate avatar: ok with no extra fields", () => {
|
|
98
|
+
let r = validate_toolbar_item({ id: "user", type: "avatar" });
|
|
99
|
+
expect(r.ok).toBe(true);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test("validate avatar: ok with dropdown action", () => {
|
|
103
|
+
let r = validate_toolbar_item({
|
|
104
|
+
id: "user", type: "avatar",
|
|
105
|
+
action: {
|
|
106
|
+
type: "dropdown",
|
|
107
|
+
items: [
|
|
108
|
+
{ id: "p", name: "Profile",
|
|
109
|
+
action: { type: "navigate", route: "/me" } }
|
|
110
|
+
]
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
expect(r.ok).toBe(true);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
/*============================================================
|
|
118
|
+
* validate_action
|
|
119
|
+
*============================================================*/
|
|
120
|
+
test("action: navigate without route flags warning", () => {
|
|
121
|
+
let w = validate_action({ type: "navigate" }, "x");
|
|
122
|
+
expect(w.join("\n")).toMatch(/requires 'route'/);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
test("action: event without event name flags warning", () => {
|
|
126
|
+
let w = validate_action({ type: "event" }, "x");
|
|
127
|
+
expect(w.join("\n")).toMatch(/requires 'event'/);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test("action: unknown action.type flags warning", () => {
|
|
131
|
+
let w = validate_action({ type: "teleport" }, "x");
|
|
132
|
+
expect(w.join("\n")).toMatch(/unknown action\.type/);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
test("action: missing type flags warning", () => {
|
|
136
|
+
let w = validate_action({}, "x");
|
|
137
|
+
expect(w.join("\n")).toMatch(/action without 'type'/);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
test("action: drawer with op only is accepted", () => {
|
|
141
|
+
let w = validate_action({ type: "drawer", op: "toggle" }, "x");
|
|
142
|
+
expect(w).toEqual([]);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
/*============================================================
|
|
147
|
+
* validate_dropdown_action
|
|
148
|
+
*============================================================*/
|
|
149
|
+
test("dropdown: missing items[] flags warning", () => {
|
|
150
|
+
let w = validate_dropdown_action({ type: "dropdown" }, "user");
|
|
151
|
+
expect(w.join("\n")).toMatch(/requires items\[\]/);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
test("dropdown: empty items[] flags warning", () => {
|
|
155
|
+
let w = validate_dropdown_action({ type: "dropdown", items: [] }, "user");
|
|
156
|
+
expect(w.join("\n")).toMatch(/empty items\[\]/);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
test("dropdown: dividers are accepted without action", () => {
|
|
160
|
+
let w = validate_dropdown_action({
|
|
161
|
+
type: "dropdown",
|
|
162
|
+
items: [
|
|
163
|
+
{ id: "p", name: "Profile",
|
|
164
|
+
action: { type: "navigate", route: "/me" } },
|
|
165
|
+
{ type: "divider" },
|
|
166
|
+
{ id: "out", name: "Logout",
|
|
167
|
+
action: { type: "event", event: "EV_LOGOUT" } }
|
|
168
|
+
]
|
|
169
|
+
}, "user");
|
|
170
|
+
expect(w).toEqual([]);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
test("dropdown: sub-item without action flags warning", () => {
|
|
174
|
+
let w = validate_dropdown_action({
|
|
175
|
+
type: "dropdown",
|
|
176
|
+
items: [{ id: "x", name: "X" }]
|
|
177
|
+
}, "user");
|
|
178
|
+
expect(w.join("\n")).toMatch(/requires an action object/);
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
test("dropdown: nested dropdowns rejected", () => {
|
|
182
|
+
let w = validate_dropdown_action({
|
|
183
|
+
type: "dropdown",
|
|
184
|
+
items: [{
|
|
185
|
+
id: "x", name: "X",
|
|
186
|
+
action: { type: "dropdown", items: [] }
|
|
187
|
+
}]
|
|
188
|
+
}, "user");
|
|
189
|
+
expect(w.join("\n")).toMatch(/nested dropdowns are not supported/);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
test("dropdown: invalid sub-action surfaces underlying warning", () => {
|
|
193
|
+
let w = validate_dropdown_action({
|
|
194
|
+
type: "dropdown",
|
|
195
|
+
items: [{ id: "x", name: "X", action: { type: "navigate" } }]
|
|
196
|
+
}, "user");
|
|
197
|
+
expect(w.join("\n")).toMatch(/requires 'route'/);
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
/*============================================================
|
|
202
|
+
* Constants exported are stable
|
|
203
|
+
*============================================================*/
|
|
204
|
+
test("kinds + action-type constants are exposed", () => {
|
|
205
|
+
expect(TOOLBAR_ITEM_KINDS).toEqual(["brand", "avatar", "connection", "action"]);
|
|
206
|
+
expect(TOOLBAR_ACTION_TYPES).toEqual(["navigate", "drawer", "event", "dropdown"]);
|
|
207
|
+
});
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/***********************************************************************
|
|
2
|
+
* tabulator.css
|
|
3
|
+
*
|
|
4
|
+
* Cross-app Tabulator theme fixes and reusable helpers.
|
|
5
|
+
*
|
|
6
|
+
* Tabulator is a first-class element across the yunos, so its
|
|
7
|
+
* styling belongs to the library — not duplicated per app.
|
|
8
|
+
* Import this once (after `tabulator.min.css` +
|
|
9
|
+
* `tabulator_bulma.css`) so every consumer gets the same
|
|
10
|
+
* theme-aware behaviour.
|
|
11
|
+
*
|
|
12
|
+
* Copyright (c) 2026, ArtGins.
|
|
13
|
+
* All Rights Reserved.
|
|
14
|
+
***********************************************************************/
|
|
15
|
+
|
|
16
|
+
/* ====================================================================
|
|
17
|
+
* Active / selected row highlight.
|
|
18
|
+
* Green wash + left accent, theme-aware, with enough specificity
|
|
19
|
+
* (three classes) to beat the striped-row !important backgrounds.
|
|
20
|
+
* Add `yui-row-active` to a `.tabulator-row` element to mark it.
|
|
21
|
+
* ==================================================================== */
|
|
22
|
+
.tabulator-row.yui-row-active,
|
|
23
|
+
.tabulator-row.yui-row-active.tabulator-row-odd,
|
|
24
|
+
.tabulator-row.yui-row-active.tabulator-row-even {
|
|
25
|
+
background-color: #E8F6EE !important;
|
|
26
|
+
box-shadow: inset 4px 0 0 #2BB673;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
[data-theme=dark] .tabulator-row.yui-row-active,
|
|
30
|
+
[data-theme=dark] .tabulator-row.yui-row-active.tabulator-row-odd,
|
|
31
|
+
[data-theme=dark] .tabulator-row.yui-row-active.tabulator-row-even {
|
|
32
|
+
background-color: rgba(43, 182, 115, 0.22) !important;
|
|
33
|
+
box-shadow: inset 4px 0 0 #2BB673;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/* ====================================================================
|
|
37
|
+
* Tree expand/collapse control (the +/- box) in dark theme.
|
|
38
|
+
* Tabulator hardcodes the box border and the +/- strokes to #333 on
|
|
39
|
+
* a near-black wash — a dark glyph in a dark box on a dark background,
|
|
40
|
+
* effectively invisible. Repaint border, wash and strokes light.
|
|
41
|
+
* ==================================================================== */
|
|
42
|
+
[data-theme=dark] .tabulator-data-tree-control {
|
|
43
|
+
background: rgba(255, 255, 255, 0.14) !important;
|
|
44
|
+
border-color: rgba(255, 255, 255, 0.85) !important;
|
|
45
|
+
}
|
|
46
|
+
[data-theme=dark] .tabulator-data-tree-control:hover {
|
|
47
|
+
background: rgba(255, 255, 255, 0.28) !important;
|
|
48
|
+
}
|
|
49
|
+
[data-theme=dark] .tabulator-data-tree-control .tabulator-data-tree-control-expand,
|
|
50
|
+
[data-theme=dark] .tabulator-data-tree-control .tabulator-data-tree-control-expand:after,
|
|
51
|
+
[data-theme=dark] .tabulator-data-tree-control .tabulator-data-tree-control-collapse:after {
|
|
52
|
+
background: #ffffff !important;
|
|
53
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/***********************************************************************
|
|
2
|
+
* wizard_helpers.js
|
|
3
|
+
*
|
|
4
|
+
* Pure (DOM-free) step logic for C_YUI_WIZARD.
|
|
5
|
+
* Kept apart so it is unit-testable with `node --test`
|
|
6
|
+
* (same split as shell_toolbar_helpers.js / pager_helpers.js).
|
|
7
|
+
*
|
|
8
|
+
* Copyright (c) 2026, ArtGins.
|
|
9
|
+
* All Rights Reserved.
|
|
10
|
+
***********************************************************************/
|
|
11
|
+
|
|
12
|
+
/***************************************************************
|
|
13
|
+
* A step is a plain object: { id, title } (content/gobj are
|
|
14
|
+
* tracked by the gclass, never here).
|
|
15
|
+
***************************************************************/
|
|
16
|
+
|
|
17
|
+
/***************************************************************
|
|
18
|
+
* Clamp an index into [0, count-1] (0 when there are no steps).
|
|
19
|
+
***************************************************************/
|
|
20
|
+
function wizard_clamp_index(idx, count)
|
|
21
|
+
{
|
|
22
|
+
if(count <= 0) {
|
|
23
|
+
return 0;
|
|
24
|
+
}
|
|
25
|
+
if(idx < 0) {
|
|
26
|
+
return 0;
|
|
27
|
+
}
|
|
28
|
+
if(idx > count - 1) {
|
|
29
|
+
return count - 1;
|
|
30
|
+
}
|
|
31
|
+
return idx;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/***************************************************************
|
|
35
|
+
* Next / previous index (clamped, never wrap).
|
|
36
|
+
***************************************************************/
|
|
37
|
+
function wizard_next_index(idx, count)
|
|
38
|
+
{
|
|
39
|
+
return wizard_clamp_index(idx + 1, count);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function wizard_prev_index(idx, count)
|
|
43
|
+
{
|
|
44
|
+
return wizard_clamp_index(idx - 1, count);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/***************************************************************
|
|
48
|
+
* Chrome model for the current step.
|
|
49
|
+
*
|
|
50
|
+
* opts = { allow_back, confirm_label, next_label }
|
|
51
|
+
*
|
|
52
|
+
* Returns { idx, count, id, title, is_first, is_last,
|
|
53
|
+
* show_back, primary_label }.
|
|
54
|
+
***************************************************************/
|
|
55
|
+
function wizard_step_model(steps, idx, opts)
|
|
56
|
+
{
|
|
57
|
+
opts = opts || {};
|
|
58
|
+
let count = steps.length;
|
|
59
|
+
let i = wizard_clamp_index(idx, count);
|
|
60
|
+
let s = steps[i] || {};
|
|
61
|
+
|
|
62
|
+
let is_first = i <= 0;
|
|
63
|
+
let is_last = i >= count - 1;
|
|
64
|
+
|
|
65
|
+
let primary_label;
|
|
66
|
+
if(is_last) {
|
|
67
|
+
primary_label = opts.confirm_label || "confirm";
|
|
68
|
+
} else {
|
|
69
|
+
primary_label = opts.next_label || "next";
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return {
|
|
73
|
+
idx: i,
|
|
74
|
+
count: count,
|
|
75
|
+
id: s.id || "",
|
|
76
|
+
title: s.title || "",
|
|
77
|
+
is_first: is_first,
|
|
78
|
+
is_last: is_last,
|
|
79
|
+
show_back: !!opts.allow_back && i > 0,
|
|
80
|
+
primary_label: primary_label,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/***************************************************************
|
|
85
|
+
* Accumulate a step's validated kw.
|
|
86
|
+
* Returns a NEW { merged, by_step } (never mutates).
|
|
87
|
+
***************************************************************/
|
|
88
|
+
function wizard_accumulate(acc, step_id, kw)
|
|
89
|
+
{
|
|
90
|
+
if(!acc) {
|
|
91
|
+
acc = { merged: {}, by_step: {} };
|
|
92
|
+
}
|
|
93
|
+
kw = kw || {};
|
|
94
|
+
let merged = Object.assign({}, acc.merged, kw);
|
|
95
|
+
let by_step = Object.assign({}, acc.by_step);
|
|
96
|
+
by_step[step_id] = kw;
|
|
97
|
+
return { merged: merged, by_step: by_step };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/***************************************************************
|
|
101
|
+
* Whether the current step must be asked to validate before
|
|
102
|
+
* advancing: only linear wizards, and only when the step has a
|
|
103
|
+
* gobj that can answer EV_STEP_VALIDATE.
|
|
104
|
+
***************************************************************/
|
|
105
|
+
function wizard_should_validate(linear, has_gobj)
|
|
106
|
+
{
|
|
107
|
+
return !!linear && !!has_gobj;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export {
|
|
111
|
+
wizard_clamp_index,
|
|
112
|
+
wizard_next_index,
|
|
113
|
+
wizard_prev_index,
|
|
114
|
+
wizard_step_model,
|
|
115
|
+
wizard_accumulate,
|
|
116
|
+
wizard_should_validate,
|
|
117
|
+
};
|