partforge 0.48.0 → 0.49.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/package.json +1 -1
- package/src/framework/app.css +89 -21
- package/src/framework/controls.js +1 -1
- package/src/framework/mount.js +10 -2
- package/src/framework/panel/info.js +10 -1
- package/src/framework/panel/render.js +39 -13
- package/src/framework/panel/widgets/checkbox.js +2 -1
- package/src/framework/panel/widgets/numeric.js +3 -1
- package/src/framework/panel/widgets/select.js +4 -2
- package/src/framework/panel/widgets/text.js +2 -1
package/package.json
CHANGED
package/src/framework/app.css
CHANGED
|
@@ -53,7 +53,10 @@ canvas { display: block; }
|
|
|
53
53
|
sits at the rail's own padding, so each slider gains the ~22px the old box
|
|
54
54
|
border + padding used to take from both sides. */
|
|
55
55
|
.section {
|
|
56
|
-
padding:
|
|
56
|
+
/* No vertical padding at the top: the header band (below) owns the space
|
|
57
|
+
around the title so a section's title sits at the same offset whether
|
|
58
|
+
the section is open or collapsed — toggling must not move it. */
|
|
59
|
+
padding: 0 var(--pf-rail-pad) 11px;
|
|
57
60
|
}
|
|
58
61
|
/* Divider BETWEEN visible sections. `~` walks all preceding siblings, and a
|
|
59
62
|
relevance-hidden section (.section-hidden, display:none) fails the :not(),
|
|
@@ -65,28 +68,70 @@ canvas { display: block; }
|
|
|
65
68
|
.section:not(.section-hidden):not(.hidden) ~ .section:not(.section-hidden):not(.hidden) {
|
|
66
69
|
border-top: 1px solid var(--pf-border);
|
|
67
70
|
}
|
|
68
|
-
|
|
71
|
+
/* Section disclosure header: the whole row is the click target (render.js
|
|
72
|
+
puts the toggle listener on the header; the title button's click bubbles
|
|
73
|
+
to it and the ⓘ stops propagation). Full-bleed: negative margins cancel
|
|
74
|
+
the rail padding so the hover band and the collapsed rules run edge to
|
|
75
|
+
edge while the text keeps the rail's alignment. Row order: title (flex:1),
|
|
76
|
+
ⓘ, chevron on the far right. */
|
|
77
|
+
.sec-header {
|
|
78
|
+
display: flex; align-items: center; gap: 6px;
|
|
79
|
+
margin: 0 calc(-1 * var(--pf-rail-pad)) 6px;
|
|
80
|
+
/* The same 13px band open or collapsed: the title never moves on toggle
|
|
81
|
+
and the hover target keeps one size. */
|
|
82
|
+
padding: 13px var(--pf-rail-pad);
|
|
83
|
+
cursor: pointer;
|
|
84
|
+
}
|
|
85
|
+
.sec-header:hover { background: var(--pf-surface-2); }
|
|
69
86
|
.sec-title {
|
|
70
|
-
flex: 1; display: flex; align-items: center;
|
|
71
|
-
|
|
87
|
+
flex: 1; display: flex; align-items: center;
|
|
88
|
+
margin: 0; padding: 0; border: 0; background: transparent; cursor: pointer;
|
|
72
89
|
text-align: left;
|
|
73
|
-
font-family: var(--pf-mono); font-size:
|
|
74
|
-
letter-spacing: 0.
|
|
75
|
-
}
|
|
76
|
-
.sec-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
90
|
+
font-family: var(--pf-mono); font-size: 12px; font-weight: 600;
|
|
91
|
+
letter-spacing: 0.12em; text-transform: uppercase; color: var(--pf-text-2);
|
|
92
|
+
}
|
|
93
|
+
.sec-header:hover .sec-title { color: var(--pf-text); }
|
|
94
|
+
/* Disclosure triangle: closes the header row on the right, after the ⓘ.
|
|
95
|
+
Glyph via ::before — the span must stay text-free (tests match .sec-title
|
|
96
|
+
by exact textContent). Rotation keys off the collapsed class because the
|
|
97
|
+
chevron sits beside, not inside, the aria-carrying button. */
|
|
98
|
+
/* A real equilateral triangle (clip-path, not a font glyph): the box is
|
|
99
|
+
exactly the triangle's bounds, so rotating about the center is wobble-free
|
|
100
|
+
— a font's ▾ sits off-center in its em box and lurches when rotated.
|
|
101
|
+
Height = width × √3/2 keeps it equilateral. */
|
|
102
|
+
.sec-header .chev, .adv-header .chev {
|
|
103
|
+
display: inline-block; width: 8px; height: 7px; color: var(--pf-muted-2);
|
|
104
|
+
transition: transform 0.15s ease;
|
|
105
|
+
}
|
|
106
|
+
.sec-header .chev::before, .adv-header .chev::before {
|
|
107
|
+
content: ""; display: block; width: 100%; height: 100%;
|
|
108
|
+
background: currentColor;
|
|
109
|
+
clip-path: polygon(50% 100%, 0 0, 100% 0);
|
|
110
|
+
}
|
|
111
|
+
.section.collapsed > .sec-header .chev, .adv-wrap.collapsed > .adv-header .chev { transform: rotate(-90deg); }
|
|
112
|
+
/* Collapsed section: the header band sits flush between the section's top
|
|
113
|
+
divider and its own bottom rule — text vertically centered, hover and
|
|
114
|
+
click covering the entire band. The -1px bottom margin overlaps this rule
|
|
115
|
+
with the next section's border-top so they read as one line. */
|
|
116
|
+
.section.collapsed {
|
|
117
|
+
padding-bottom: 0;
|
|
118
|
+
border-bottom: 1px solid var(--pf-border); margin-bottom: -1px;
|
|
119
|
+
}
|
|
120
|
+
.section.collapsed .sec-header { margin-bottom: 0; }
|
|
80
121
|
.sec-body.hidden { display: none; }
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
select.select-input {
|
|
122
|
+
/* Native menulist arrows hug the right border and ignore padding, so the
|
|
123
|
+
panel's selects draw their own — the header chevrons' equilateral triangle
|
|
124
|
+
as a data-URI, inset 10px from the edge. Fixed slate fill: url() can't
|
|
125
|
+
read theme variables, and this shade reads on both themes. */
|
|
126
|
+
select.preset, select.select-input {
|
|
87
127
|
width: 100%; background: var(--pf-input-bg); color: var(--pf-text-2);
|
|
88
|
-
border: 1px solid var(--pf-border); border-radius: var(--pf-radius-control);
|
|
128
|
+
border: 1px solid var(--pf-border); border-radius: var(--pf-radius-control);
|
|
129
|
+
padding: 7px 26px 7px 9px;
|
|
89
130
|
font-family: var(--pf-mono); font-size: 11px;
|
|
131
|
+
appearance: none;
|
|
132
|
+
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='8' height='7'%3E%3Cpolygon points='0,0 8,0 4,7' fill='%238b8b94'/%3E%3C/svg%3E");
|
|
133
|
+
background-repeat: no-repeat;
|
|
134
|
+
background-position: right 10px center;
|
|
90
135
|
}
|
|
91
136
|
.feat { display: flex; align-items: center; gap: 8px; margin: 6px 0;
|
|
92
137
|
color: var(--pf-text-2); cursor: pointer; }
|
|
@@ -97,12 +142,35 @@ select.select-input {
|
|
|
97
142
|
`.sec-body` (rules above); everything else carrying `.hidden` inside the
|
|
98
143
|
panel is a `when` that evaluated false. */
|
|
99
144
|
.section.hidden, .slider.hidden, .feat.hidden, select.preset.hidden { display: none; }
|
|
145
|
+
/* Inner fold header: same row anatomy as .sec-header (title left, chevron
|
|
146
|
+
right, whole row clickable) at the subordinate scale. Spans the fold's own
|
|
147
|
+
width — not full-bleed — so it never crosses a feat-group's left border;
|
|
148
|
+
small negative margins let the hover band breathe past the text without
|
|
149
|
+
moving it. */
|
|
150
|
+
.adv-header {
|
|
151
|
+
display: flex; align-items: center; gap: 6px;
|
|
152
|
+
margin: 8px -6px 0; padding: 8px 6px;
|
|
153
|
+
/* Transparent borders reserve the collapsed rules' pixels so toggling
|
|
154
|
+
swaps only their color — the title never moves and the hover target
|
|
155
|
+
keeps one size, matching the section-header treatment. */
|
|
156
|
+
border-top: 1px solid transparent; border-bottom: 1px solid transparent;
|
|
157
|
+
border-radius: 6px; cursor: pointer;
|
|
158
|
+
}
|
|
159
|
+
.adv-header:hover { background: var(--pf-surface-2); }
|
|
100
160
|
.adv-toggle {
|
|
101
|
-
|
|
161
|
+
flex: 1; margin: 0; padding: 0; border: 0;
|
|
102
162
|
background: transparent; color: var(--pf-muted); cursor: pointer;
|
|
103
|
-
font-family: var(--pf-mono); font-size:
|
|
163
|
+
font-family: var(--pf-mono); font-size: 12px; font-weight: 600;
|
|
164
|
+
letter-spacing: 0.08em; text-transform: uppercase; text-align: left;
|
|
165
|
+
}
|
|
166
|
+
.adv-header:hover .adv-toggle { color: var(--pf-muted-2); }
|
|
167
|
+
/* Collapsed fold: the reserved borders take color — rules above and below,
|
|
168
|
+
text vertically centered between them (the section-band treatment at fold
|
|
169
|
+
scale). Square corners while the rules show. */
|
|
170
|
+
.adv-wrap.collapsed > .adv-header {
|
|
171
|
+
border-top-color: var(--pf-border); border-bottom-color: var(--pf-border);
|
|
172
|
+
border-radius: 0;
|
|
104
173
|
}
|
|
105
|
-
.adv-toggle:hover { color: var(--pf-muted-2); }
|
|
106
174
|
.adv.hidden { display: none; }
|
|
107
175
|
.adv { margin-top: 4px; }
|
|
108
176
|
|
|
@@ -10,6 +10,6 @@
|
|
|
10
10
|
// This file stays at its path because animation-controls.js imports the popover
|
|
11
11
|
// helpers from it, and because it is the documented import site.
|
|
12
12
|
export { buildControls } from "./panel/render.js";
|
|
13
|
-
export { popoverTop, createInfoPopover, attachInfo } from "./panel/info.js";
|
|
13
|
+
export { popoverTop, popoverLeft, createInfoPopover, attachInfo } from "./panel/info.js";
|
|
14
14
|
export { clampToRange } from "./panel/widgets/numeric.js";
|
|
15
15
|
export { visibleAdvanced, visibleFeatures, visibleToggles, sectionRenders } from "./panel/legacy.js";
|
package/src/framework/mount.js
CHANGED
|
@@ -139,10 +139,16 @@ function createCleanupStack() {
|
|
|
139
139
|
// onViewChange fires once synchronously during mount with the initial resolved
|
|
140
140
|
// view (before ready), then again on every subsequent view change (user click
|
|
141
141
|
// or a programmatic setView) — always the new view name.
|
|
142
|
+
// onParamsCommit({ changed, params }) // the user FINISHED editing a panel control (slider
|
|
143
|
+
// // released, box committed, checkbox ticked, preset
|
|
144
|
+
// // applied): `changed` lists the keys written, `params`
|
|
145
|
+
// // is a snapshot copy. Never fired by setParams or
|
|
146
|
+
// // animation playback — hosts call setParams from their
|
|
147
|
+
// // own undo/reset, and firing here would loop.
|
|
142
148
|
// Every `elements` entry defaults to the legacy global-ID lookup (below), resolved
|
|
143
149
|
// exactly once here — submodules take element refs and never query the document.
|
|
144
150
|
// `container`/`controls` remain as deprecated aliases for elements.viewer/.controls.
|
|
145
|
-
export function mount(part, { createWorker, elements = {}, onBuild, onPick, onDownload, onViewChange,
|
|
151
|
+
export function mount(part, { createWorker, elements = {}, onBuild, onPick, onDownload, onViewChange, onParamsCommit,
|
|
146
152
|
container: legacyContainer, controls: legacyControls } = {}) {
|
|
147
153
|
// --- element resolution (the only getElementById calls in the framework, save the ?pickserver client's optional #viewbar lookup) ----
|
|
148
154
|
const byId = (id) => document.getElementById(id);
|
|
@@ -487,7 +493,9 @@ export function mount(part, { createWorker, elements = {}, onBuild, onPick, onDo
|
|
|
487
493
|
const panel = buildControls(els.controls, part.parameters, params, () => {
|
|
488
494
|
animCtl?.notifyUserEdit();
|
|
489
495
|
onParamChange();
|
|
490
|
-
}
|
|
496
|
+
}, onParamsCommit
|
|
497
|
+
? (changed) => onParamsCommit({ changed, params: { ...params } })
|
|
498
|
+
: undefined);
|
|
491
499
|
cleanup.defer(() => panel.dispose());
|
|
492
500
|
const updateRelevance = () => {
|
|
493
501
|
// A throwing derive() must not break every slider drag — mount's pick
|
|
@@ -20,6 +20,15 @@ export function popoverTop({ glyphTop, glyphBottom, popHeight, viewportHeight })
|
|
|
20
20
|
return Math.max(8, glyphTop - 6 - popHeight);
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
+
// Popover left edge: aligned 8px left of the glyph when that fits, pulled
|
|
24
|
+
// left so the popover's right edge keeps a 10px margin from the viewport
|
|
25
|
+
// edge, and never past a 10px margin on the left (left margin wins when both
|
|
26
|
+
// would be violated). Pure, for direct unit testing — happy-dom reports zero
|
|
27
|
+
// layout metrics, same as popoverTop above.
|
|
28
|
+
export function popoverLeft({ glyphLeft, popWidth, viewportWidth }) {
|
|
29
|
+
return Math.max(10, Math.min(glyphLeft - 8, viewportWidth - 10 - popWidth));
|
|
30
|
+
}
|
|
31
|
+
|
|
23
32
|
// One popover element per panel, shared by all its glyphs (only one open at a
|
|
24
33
|
// time). Document-level dismiss listeners are registered per panel and removed
|
|
25
34
|
// by panel.dispose().
|
|
@@ -51,7 +60,7 @@ export function createInfoPopover() {
|
|
|
51
60
|
glyph.setAttribute("aria-expanded", "true");
|
|
52
61
|
const r = glyph.getBoundingClientRect();
|
|
53
62
|
pop.style.top = `${popoverTop({ glyphTop: r.top, glyphBottom: r.bottom, popHeight: pop.offsetHeight, viewportHeight: window.innerHeight })}px`;
|
|
54
|
-
pop.style.left = `${
|
|
63
|
+
pop.style.left = `${popoverLeft({ glyphLeft: r.left, popWidth: pop.offsetWidth, viewportWidth: window.innerWidth })}px`;
|
|
55
64
|
},
|
|
56
65
|
dispose() {
|
|
57
66
|
document.removeEventListener("click", onDocClick);
|
|
@@ -24,7 +24,7 @@ function indexNodes(nodes, map) {
|
|
|
24
24
|
}
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
-
export function buildControls(root, parameters, params, onDirty) {
|
|
27
|
+
export function buildControls(root, parameters, params, onDirty, onCommit) {
|
|
28
28
|
const info = createInfoPopover();
|
|
29
29
|
const tree = buildTree(desugar(parameters));
|
|
30
30
|
|
|
@@ -38,9 +38,11 @@ export function buildControls(root, parameters, params, onDirty) {
|
|
|
38
38
|
const lastVisible = new Map(); // id -> previous `visible`, to detect a reveal
|
|
39
39
|
const lastDisabled = new Map(); // id -> previous `disabled`, to skip a no-op input pass
|
|
40
40
|
// Containers that own a disclosure: sections, and titled inner groups (the
|
|
41
|
-
// legacy "Advanced" fold).
|
|
42
|
-
// button
|
|
43
|
-
|
|
41
|
+
// legacy "Advanced" fold). Both share the same anatomy — a header row with
|
|
42
|
+
// the aria-carrying button and a text-free chevron span — so `el` (the
|
|
43
|
+
// section element / the fold wrapper) mirrors the disclosure with a
|
|
44
|
+
// `.collapsed` class and CSS draws the closed-band affordance off that.
|
|
45
|
+
const disclosures = new Map(); // id -> { body, button, el }
|
|
44
46
|
indexNodes(tree, nodeById);
|
|
45
47
|
let relevant = null;
|
|
46
48
|
|
|
@@ -62,7 +64,7 @@ export function buildControls(root, parameters, params, onDirty) {
|
|
|
62
64
|
const open = state.get(id)?.open ?? true;
|
|
63
65
|
d.body.classList.toggle("hidden", !open);
|
|
64
66
|
d.button.setAttribute("aria-expanded", String(open));
|
|
65
|
-
|
|
67
|
+
d.el.classList.toggle("collapsed", !open);
|
|
66
68
|
}
|
|
67
69
|
};
|
|
68
70
|
|
|
@@ -112,6 +114,15 @@ export function buildControls(root, parameters, params, onDirty) {
|
|
|
112
114
|
|
|
113
115
|
const onEdit = () => { applyState(); onDirty?.(); };
|
|
114
116
|
|
|
117
|
+
// A commit = the user FINISHED an interaction (slider released, box
|
|
118
|
+
// committed, checkbox ticked, preset applied). Distinct from onDirty, which
|
|
119
|
+
// fires on every input event mid-drag. Wrapped: a throwing host handler
|
|
120
|
+
// must never break the panel.
|
|
121
|
+
const commit = (keys) => {
|
|
122
|
+
if (!onCommit) return;
|
|
123
|
+
try { onCommit(keys); } catch { /* host's problem, not the panel's */ }
|
|
124
|
+
};
|
|
125
|
+
|
|
115
126
|
// --- build ---------------------------------------------------------------
|
|
116
127
|
//
|
|
117
128
|
// TWO DIFFERENT THINGS USE `.hidden`, and conflating them is a real bug:
|
|
@@ -136,17 +147,24 @@ export function buildControls(root, parameters, params, onDirty) {
|
|
|
136
147
|
const wrap = el("div", "adv-wrap");
|
|
137
148
|
const body = el("div", "adv hidden"); // starts closed — legacy parity
|
|
138
149
|
body.id = `pf-fold-${node.id.replaceAll("/", "-")}`;
|
|
139
|
-
|
|
150
|
+
// Same row anatomy as a section header — title button (text-free of any
|
|
151
|
+
// arrow, same exact-textContent reasoning), chevron span on the right,
|
|
152
|
+
// whole row clickable — at the fold's subordinate scale.
|
|
153
|
+
const foldHeader = el("div", "adv-header");
|
|
154
|
+
const toggle = el("button", "adv-toggle");
|
|
155
|
+
toggle.type = "button";
|
|
156
|
+
toggle.append(el("span", "adv-name", node.title));
|
|
140
157
|
toggle.setAttribute("aria-controls", body.id);
|
|
141
|
-
|
|
158
|
+
foldHeader.append(toggle, el("span", "chev"));
|
|
159
|
+
foldHeader.addEventListener("click", () => {
|
|
142
160
|
const nowHidden = body.classList.toggle("hidden");
|
|
143
|
-
toggle.textContent = nowHidden ? `${node.title} ▾` : `${node.title} ▴`;
|
|
144
161
|
toggle.setAttribute("aria-expanded", String(!nowHidden));
|
|
162
|
+
wrap.classList.toggle("collapsed", nowHidden);
|
|
145
163
|
});
|
|
146
164
|
for (const child of node.children) renderNode(child, body, sectionCtx);
|
|
147
|
-
wrap.append(
|
|
165
|
+
wrap.append(foldHeader, body);
|
|
148
166
|
nodeEls.set(node.id, wrap); // conditions act on the wrapper
|
|
149
|
-
disclosures.set(node.id, { body, button: toggle,
|
|
167
|
+
disclosures.set(node.id, { body, button: toggle, el: wrap });
|
|
150
168
|
container.append(wrap);
|
|
151
169
|
}
|
|
152
170
|
|
|
@@ -173,6 +191,7 @@ export function buildControls(root, parameters, params, onDirty) {
|
|
|
173
191
|
Object.assign(params, bundle);
|
|
174
192
|
for (const { key, sync } of rawSyncs.get(sectionCtx.id)) if (key in params) sync();
|
|
175
193
|
onEdit();
|
|
194
|
+
commit(Object.keys(bundle));
|
|
176
195
|
});
|
|
177
196
|
// The section's controls need a handle on the picker to drop it to Custom
|
|
178
197
|
// when one of them is edited. First picker in the section wins.
|
|
@@ -203,6 +222,7 @@ export function buildControls(root, parameters, params, onDirty) {
|
|
|
203
222
|
};
|
|
204
223
|
const widget = factory(node, params, {
|
|
205
224
|
onChange: () => { markCustom(); onEdit(); },
|
|
225
|
+
onCommit: () => commit([node.key]),
|
|
206
226
|
info,
|
|
207
227
|
});
|
|
208
228
|
nodeEls.set(node.id, widget.el);
|
|
@@ -233,12 +253,14 @@ export function buildControls(root, parameters, params, onDirty) {
|
|
|
233
253
|
// because sectionByTitle-style lookups match `.sec-title` by exact
|
|
234
254
|
// textContent === title (controls.test.js:210), and a text chevron here
|
|
235
255
|
// would break that match.
|
|
236
|
-
title.append(el("span", "sec-name", section.title ?? "")
|
|
256
|
+
title.append(el("span", "sec-name", section.title ?? ""));
|
|
237
257
|
header.append(title);
|
|
258
|
+
// Row order: title (flex:1), then ⓘ, then the chevron on the far right.
|
|
238
259
|
// The ⓘ is a SIBLING of the button, never a child: attachInfo appends a
|
|
239
260
|
// <button>, and a button nested in a button is invalid HTML that never
|
|
240
261
|
// receives clicks.
|
|
241
262
|
attachInfo(header, section.description, info);
|
|
263
|
+
header.append(el("span", "chev"));
|
|
242
264
|
secEl.append(header);
|
|
243
265
|
|
|
244
266
|
const body = el("div", "sec-body");
|
|
@@ -246,11 +268,15 @@ export function buildControls(root, parameters, params, onDirty) {
|
|
|
246
268
|
title.setAttribute("aria-controls", body.id);
|
|
247
269
|
secEl.append(body);
|
|
248
270
|
|
|
249
|
-
title
|
|
271
|
+
// The whole header row toggles: the title button's own click bubbles up
|
|
272
|
+
// here, the chevron and the empty row space hit it directly, and the ⓘ
|
|
273
|
+
// stops propagation in attachInfo. aria state stays on the title button.
|
|
274
|
+
header.addEventListener("click", () => {
|
|
250
275
|
const nowHidden = body.classList.toggle("hidden");
|
|
251
276
|
title.setAttribute("aria-expanded", String(!nowHidden));
|
|
277
|
+
secEl.classList.toggle("collapsed", nowHidden);
|
|
252
278
|
});
|
|
253
|
-
disclosures.set(section.id, { body, button: title,
|
|
279
|
+
disclosures.set(section.id, { body, button: title, el: secEl });
|
|
254
280
|
|
|
255
281
|
// `preset` is filled in when a preset node renders. Controls read it late, so
|
|
256
282
|
// one appearing after them in the children array still works.
|
|
@@ -14,7 +14,7 @@ function el(tag, className, text) {
|
|
|
14
14
|
return node;
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
-
export function makeCheckbox(node, params, { onChange, info }) {
|
|
17
|
+
export function makeCheckbox(node, params, { onChange, onCommit, info }) {
|
|
18
18
|
const row = el("label", "feat");
|
|
19
19
|
const box = document.createElement("input");
|
|
20
20
|
box.type = "checkbox";
|
|
@@ -30,6 +30,7 @@ export function makeCheckbox(node, params, { onChange, info }) {
|
|
|
30
30
|
params[node.key] = 0;
|
|
31
31
|
}
|
|
32
32
|
onChange?.();
|
|
33
|
+
onCommit?.();
|
|
33
34
|
});
|
|
34
35
|
|
|
35
36
|
const sync = () => { box.checked = params[node.key] > 0; };
|
|
@@ -19,7 +19,7 @@ function el(tag, className, text) {
|
|
|
19
19
|
return node;
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
export function makeNumeric(node, params, { onChange, info }) {
|
|
22
|
+
export function makeNumeric(node, params, { onChange, onCommit, info }) {
|
|
23
23
|
const numeric = node.type === "number";
|
|
24
24
|
const wrap = el("div", "slider");
|
|
25
25
|
const row = el("div", "row");
|
|
@@ -68,6 +68,7 @@ export function makeNumeric(node, params, { onChange, info }) {
|
|
|
68
68
|
paintWarn();
|
|
69
69
|
onChange?.();
|
|
70
70
|
});
|
|
71
|
+
slider.addEventListener("change", () => onCommit?.());
|
|
71
72
|
wrap.append(slider);
|
|
72
73
|
}
|
|
73
74
|
|
|
@@ -124,6 +125,7 @@ export function makeNumeric(node, params, { onChange, info }) {
|
|
|
124
125
|
if (slider) slider.value = log ? toPosSafe(v) : v;
|
|
125
126
|
paintWarn();
|
|
126
127
|
onChange?.();
|
|
128
|
+
onCommit?.();
|
|
127
129
|
});
|
|
128
130
|
|
|
129
131
|
const sync = () => {
|
|
@@ -23,7 +23,7 @@ function labeledRow(node, info) {
|
|
|
23
23
|
return wrap;
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
export function makeSelect(node, params, { onChange, info }) {
|
|
26
|
+
export function makeSelect(node, params, { onChange, onCommit, info }) {
|
|
27
27
|
const wrap = labeledRow(node, info);
|
|
28
28
|
const opts = normalizeOptions(node.options);
|
|
29
29
|
const byString = new Map(opts.map((o) => [String(o.value), o.value]));
|
|
@@ -40,13 +40,14 @@ export function makeSelect(node, params, { onChange, info }) {
|
|
|
40
40
|
select.addEventListener("change", () => {
|
|
41
41
|
params[node.key] = byString.get(select.value);
|
|
42
42
|
onChange?.();
|
|
43
|
+
onCommit?.();
|
|
43
44
|
});
|
|
44
45
|
wrap.append(select);
|
|
45
46
|
const sync = () => { select.value = String(params[node.key]); };
|
|
46
47
|
return { el: wrap, sync };
|
|
47
48
|
}
|
|
48
49
|
|
|
49
|
-
export function makeRadio(node, params, { onChange, info }) {
|
|
50
|
+
export function makeRadio(node, params, { onChange, onCommit, info }) {
|
|
50
51
|
const wrap = labeledRow(node, info);
|
|
51
52
|
const opts = normalizeOptions(node.options);
|
|
52
53
|
const seg = el("div", "seg");
|
|
@@ -58,6 +59,7 @@ export function makeRadio(node, params, { onChange, info }) {
|
|
|
58
59
|
params[node.key] = o.value;
|
|
59
60
|
paint();
|
|
60
61
|
onChange?.();
|
|
62
|
+
onCommit?.();
|
|
61
63
|
});
|
|
62
64
|
seg.append(b);
|
|
63
65
|
return { b, value: o.value };
|
|
@@ -9,7 +9,7 @@ function el(tag, className, text) {
|
|
|
9
9
|
return node;
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
-
export function makeText(node, params, { onChange, info }) {
|
|
12
|
+
export function makeText(node, params, { onChange, onCommit, info }) {
|
|
13
13
|
const multiline = node.type === "textarea";
|
|
14
14
|
const wrap = el("div", "slider");
|
|
15
15
|
const row = el("div", "row");
|
|
@@ -26,6 +26,7 @@ export function makeText(node, params, { onChange, info }) {
|
|
|
26
26
|
params[node.key] = field.value;
|
|
27
27
|
onChange?.();
|
|
28
28
|
});
|
|
29
|
+
field.addEventListener("change", () => onCommit?.());
|
|
29
30
|
wrap.append(field);
|
|
30
31
|
|
|
31
32
|
const sync = () => { field.value = String(params[node.key] ?? ""); };
|