@selvajs/ui 4.12.3 → 4.12.5

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.
@@ -110,20 +110,21 @@
110
110
  );
111
111
 
112
112
  // When a dynamic value list recomputes, a previously-selected value may no longer be an
113
- // available option. Prune the stale selection so the control shows a valid option (or empty)
114
- // instead of rendering the orphaned raw value as its own label. This is a system-initiated
115
- // change (the user didn't pick the new option), so force a solve — otherwise manual-solve
116
- // schemas would keep the prior output on screen, making it look like the auto-picked option
117
- // produced it.
118
- // Distinguishes "never selected" from "user deliberately cleared": the empty→
119
- // first-option fallback below must not fight a real user action (e.g. unchecking
120
- // every checklist entry), only fill the initial void.
121
- let userTouched = false;
122
-
123
- // Loop breaker for the system fallback. A definition whose computed options
124
- // DEPEND on the selection can oscillate: auto-pick A → solve → new options
125
- // exclude A → auto-pick B → solve → … Each cycle force-solves and re-renders
126
- // the option list, which can run a tab out of memory. Bound consecutive
113
+ // available option. Prune the stale selection so the control shows a valid option instead
114
+ // of rendering the orphaned raw value as its own label. This is a system-initiated change
115
+ // (the user didn't pick the new option), so force a solve — otherwise manual-solve schemas
116
+ // would keep the prior output on screen, making it look like the auto-picked option produced it.
117
+ //
118
+ // INVARIANT: a dynamic value list must never dispatch an empty/null value to solve. There
119
+ // is always at least one option, and an empty selection reaches the definition as null/""
120
+ // which throws NREs in downstream geometry components (e.g. Bounding Rectangle) and nulls
121
+ // every output beyond them. So every terminal state below resolves to a valid option; there
122
+ // is deliberately no "user cleared it, stay empty" path.
123
+ //
124
+ // LOOP BREAKER: a definition whose computed options DEPEND on the selection can oscillate
125
+ // auto-pick A → solve → new options exclude A → auto-pick B → solve → … Each cycle force-
126
+ // solves and re-parses/re-renders the (possibly multi-MB) options, which FREEZES the tab
127
+ // (each solve result blocks the main thread; the loop never yields). Bound consecutive
127
128
  // system-initiated picks; any real user commit resets the budget.
128
129
  const MAX_CONSECUTIVE_AUTO_PICKS = 3;
129
130
  let autoPickCount = 0;
@@ -132,7 +133,7 @@
132
133
  if (!isDynamicValueListWidget(item) || !dynamicListHasOptions) return;
133
134
  const validValues = new Set(Object.values(dynamicListOptions));
134
135
  const firstOption = Object.values(dynamicListOptions)[0];
135
- // Selection is valid → stable state reached; refill the auto-pick budget.
136
+ // Already valid → stable; refill the auto-pick budget and stop.
136
137
  const isValid = Array.isArray(value)
137
138
  ? value.length > 0 && value.every((v) => typeof v === 'string' && validValues.has(v))
138
139
  : typeof value === 'string' && value !== '' && validValues.has(value);
@@ -148,42 +149,22 @@
148
149
  );
149
150
  return;
150
151
  }
151
- // Route through onChange (not commit) — value is a one-way prop here, so writing it
152
- // directly from an effect trips Svelte's binding-ownership check.
153
- // A NEVER-made selection (empty string/array, e.g. no default on a fresh page)
154
- // gets the same first-option fallback as a stale one: an empty selection solves
155
- // as an empty string, which cascades through the definition as null-data errors
156
- // ("File not found: .dmf", Text→Number conversion failures, …) and the empty
157
- // result then gets replayed by the solve caches.
158
- // Diagnostic: each system pick logs itself — a runaway sequence of these lines
159
- // (pick #1, #2, #3 + the loop-breaker warning) is the solve-loop signature.
160
- const logAutoPick = (picked: string) =>
161
- console.info(
162
- `[DVL] auto-pick #${autoPickCount} on "${item.paramId}": "${picked.slice(0, 60)}" ` +
163
- `(was ${JSON.stringify(Array.isArray(value) ? value.slice(0, 3) : value)?.slice(0, 80)})`
164
- );
165
152
  if (Array.isArray(value)) {
166
153
  const pruned = value.filter((v) => typeof v === 'string' && validValues.has(v));
167
- if (pruned.length !== value.length || (value.length === 0 && !userTouched)) {
168
- // Fall back to first option when the checklist is fully pruned or was
169
- // never selected; a user-cleared checklist stays empty.
154
+ // Empty (never selected or fully pruned) always falls back to the first option
155
+ // a checklist that solves empty produces the same null cascade as a single value.
156
+ if (pruned.length !== value.length || value.length === 0) {
170
157
  autoPickCount++;
171
- logAutoPick(pruned.length > 0 ? String(pruned[0]) : firstOption);
172
158
  onChange(item.paramId, pruned.length > 0 ? pruned : [firstOption], true);
173
159
  }
174
- } else if (
175
- (typeof value === 'string' && value && !validValues.has(value)) ||
176
- ((value == null || value === '') && !userTouched)
177
- ) {
178
- // Stale single value, or never-selected — fall back to the first option.
160
+ } else if (typeof value !== 'string' || value === '' || !validValues.has(value)) {
161
+ // Never-selected or stale single value fall back to the first option.
179
162
  autoPickCount++;
180
- logAutoPick(firstOption);
181
163
  onChange(item.paramId, firstOption, true);
182
164
  }
183
165
  });
184
166
 
185
167
  function commit(newValue: SupportedTypes) {
186
- userTouched = true;
187
168
  // A real user pick re-arms the system fallback (see autoPickCount above).
188
169
  autoPickCount = 0;
189
170
  value = newValue;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@selvajs/ui",
3
- "version": "4.12.3",
3
+ "version": "4.12.5",
4
4
  "description": "Shared UI components and utilities for Selva applications",
5
5
  "license": "MIT",
6
6
  "publishConfig": {
@@ -110,20 +110,21 @@
110
110
  );
111
111
 
112
112
  // When a dynamic value list recomputes, a previously-selected value may no longer be an
113
- // available option. Prune the stale selection so the control shows a valid option (or empty)
114
- // instead of rendering the orphaned raw value as its own label. This is a system-initiated
115
- // change (the user didn't pick the new option), so force a solve — otherwise manual-solve
116
- // schemas would keep the prior output on screen, making it look like the auto-picked option
117
- // produced it.
118
- // Distinguishes "never selected" from "user deliberately cleared": the empty→
119
- // first-option fallback below must not fight a real user action (e.g. unchecking
120
- // every checklist entry), only fill the initial void.
121
- let userTouched = false;
122
-
123
- // Loop breaker for the system fallback. A definition whose computed options
124
- // DEPEND on the selection can oscillate: auto-pick A → solve → new options
125
- // exclude A → auto-pick B → solve → … Each cycle force-solves and re-renders
126
- // the option list, which can run a tab out of memory. Bound consecutive
113
+ // available option. Prune the stale selection so the control shows a valid option instead
114
+ // of rendering the orphaned raw value as its own label. This is a system-initiated change
115
+ // (the user didn't pick the new option), so force a solve — otherwise manual-solve schemas
116
+ // would keep the prior output on screen, making it look like the auto-picked option produced it.
117
+ //
118
+ // INVARIANT: a dynamic value list must never dispatch an empty/null value to solve. There
119
+ // is always at least one option, and an empty selection reaches the definition as null/""
120
+ // which throws NREs in downstream geometry components (e.g. Bounding Rectangle) and nulls
121
+ // every output beyond them. So every terminal state below resolves to a valid option; there
122
+ // is deliberately no "user cleared it, stay empty" path.
123
+ //
124
+ // LOOP BREAKER: a definition whose computed options DEPEND on the selection can oscillate
125
+ // auto-pick A → solve → new options exclude A → auto-pick B → solve → … Each cycle force-
126
+ // solves and re-parses/re-renders the (possibly multi-MB) options, which FREEZES the tab
127
+ // (each solve result blocks the main thread; the loop never yields). Bound consecutive
127
128
  // system-initiated picks; any real user commit resets the budget.
128
129
  const MAX_CONSECUTIVE_AUTO_PICKS = 3;
129
130
  let autoPickCount = 0;
@@ -132,7 +133,7 @@
132
133
  if (!isDynamicValueListWidget(item) || !dynamicListHasOptions) return;
133
134
  const validValues = new Set(Object.values(dynamicListOptions));
134
135
  const firstOption = Object.values(dynamicListOptions)[0];
135
- // Selection is valid → stable state reached; refill the auto-pick budget.
136
+ // Already valid → stable; refill the auto-pick budget and stop.
136
137
  const isValid = Array.isArray(value)
137
138
  ? value.length > 0 && value.every((v) => typeof v === 'string' && validValues.has(v))
138
139
  : typeof value === 'string' && value !== '' && validValues.has(value);
@@ -148,42 +149,22 @@
148
149
  );
149
150
  return;
150
151
  }
151
- // Route through onChange (not commit) — value is a one-way prop here, so writing it
152
- // directly from an effect trips Svelte's binding-ownership check.
153
- // A NEVER-made selection (empty string/array, e.g. no default on a fresh page)
154
- // gets the same first-option fallback as a stale one: an empty selection solves
155
- // as an empty string, which cascades through the definition as null-data errors
156
- // ("File not found: .dmf", Text→Number conversion failures, …) and the empty
157
- // result then gets replayed by the solve caches.
158
- // Diagnostic: each system pick logs itself — a runaway sequence of these lines
159
- // (pick #1, #2, #3 + the loop-breaker warning) is the solve-loop signature.
160
- const logAutoPick = (picked: string) =>
161
- console.info(
162
- `[DVL] auto-pick #${autoPickCount} on "${item.paramId}": "${picked.slice(0, 60)}" ` +
163
- `(was ${JSON.stringify(Array.isArray(value) ? value.slice(0, 3) : value)?.slice(0, 80)})`
164
- );
165
152
  if (Array.isArray(value)) {
166
153
  const pruned = value.filter((v) => typeof v === 'string' && validValues.has(v));
167
- if (pruned.length !== value.length || (value.length === 0 && !userTouched)) {
168
- // Fall back to first option when the checklist is fully pruned or was
169
- // never selected; a user-cleared checklist stays empty.
154
+ // Empty (never selected or fully pruned) always falls back to the first option
155
+ // a checklist that solves empty produces the same null cascade as a single value.
156
+ if (pruned.length !== value.length || value.length === 0) {
170
157
  autoPickCount++;
171
- logAutoPick(pruned.length > 0 ? String(pruned[0]) : firstOption);
172
158
  onChange(item.paramId, pruned.length > 0 ? pruned : [firstOption], true);
173
159
  }
174
- } else if (
175
- (typeof value === 'string' && value && !validValues.has(value)) ||
176
- ((value == null || value === '') && !userTouched)
177
- ) {
178
- // Stale single value, or never-selected — fall back to the first option.
160
+ } else if (typeof value !== 'string' || value === '' || !validValues.has(value)) {
161
+ // Never-selected or stale single value fall back to the first option.
179
162
  autoPickCount++;
180
- logAutoPick(firstOption);
181
163
  onChange(item.paramId, firstOption, true);
182
164
  }
183
165
  });
184
166
 
185
167
  function commit(newValue: SupportedTypes) {
186
- userTouched = true;
187
168
  // A real user pick re-arms the system fallback (see autoPickCount above).
188
169
  autoPickCount = 0;
189
170
  value = newValue;