@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.
Files changed (46) hide show
  1. package/README.md +40 -363
  2. package/dist/gobj-ui.cjs.js +11154 -5506
  3. package/dist/gobj-ui.es.js +11131 -5508
  4. package/index.js +41 -14
  5. package/package.json +11 -9
  6. package/src/c_g6_nodes_tree.js +6 -1
  7. package/src/c_yui_form.js +1 -1
  8. package/src/c_yui_gobj_tree_js.js +1 -1
  9. package/src/c_yui_json_graph.js +1 -1
  10. package/src/c_yui_main.js +3 -3
  11. package/src/c_yui_map.js +6 -1
  12. package/src/c_yui_nav.js +881 -0
  13. package/src/c_yui_pager.js +545 -0
  14. package/src/c_yui_routing.css +1 -1
  15. package/src/c_yui_routing.js +1 -1
  16. package/src/c_yui_shell.css +571 -0
  17. package/src/c_yui_shell.js +2474 -0
  18. package/src/c_yui_tabs.js +1 -1
  19. package/src/c_yui_treedb_graph.js +35 -9
  20. package/src/c_yui_treedb_topic_with_form.js +1 -1
  21. package/src/c_yui_treedb_topics.js +36 -8
  22. package/src/c_yui_uplot.js +1 -1
  23. package/src/c_yui_window.js +242 -21
  24. package/src/c_yui_window_manager.js +602 -0
  25. package/src/c_yui_wizard.js +612 -0
  26. package/src/pager_helpers.js +138 -0
  27. package/src/pager_helpers.test.js +140 -0
  28. package/src/route_resolver.js +53 -0
  29. package/src/route_resolver.test.js +82 -0
  30. package/src/shell_focus_trap.js +123 -0
  31. package/src/shell_focus_trap.test.js +299 -0
  32. package/src/shell_modals.js +445 -0
  33. package/src/shell_show_on.js +91 -0
  34. package/src/shell_show_on.test.js +86 -0
  35. package/src/shell_toolbar_helpers.js +221 -0
  36. package/src/shell_toolbar_helpers.test.js +207 -0
  37. package/src/tabulator.css +53 -0
  38. package/src/wizard_helpers.js +117 -0
  39. package/src/wizard_helpers.test.js +122 -0
  40. package/src/yui_dev.js +1257 -362
  41. package/src/yui_icons.css +5 -0
  42. package/src/yui_inputs.css +32 -0
  43. package/src/yui_inputs.js +71 -0
  44. package/vite-plugin-yuneta-html.js +2 -2
  45. package/skeleton/config.json +0 -20
  46. 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
+ };