@selvajs/ui 4.12.3 → 4.12.4

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,82 +110,34 @@
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
127
- // system-initiated picks; any real user commit resets the budget.
128
- const MAX_CONSECUTIVE_AUTO_PICKS = 3;
129
- let autoPickCount = 0;
130
-
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.
131
123
  $effect(() => {
132
124
  if (!isDynamicValueListWidget(item) || !dynamicListHasOptions) return;
133
125
  const validValues = new Set(Object.values(dynamicListOptions));
134
126
  const firstOption = Object.values(dynamicListOptions)[0];
135
- // Selection is valid → stable state reached; refill the auto-pick budget.
136
- const isValid = Array.isArray(value)
137
- ? value.length > 0 && value.every((v) => typeof v === 'string' && validValues.has(v))
138
- : typeof value === 'string' && value !== '' && validValues.has(value);
139
- if (isValid) {
140
- autoPickCount = 0;
141
- return;
142
- }
143
- if (autoPickCount >= MAX_CONSECUTIVE_AUTO_PICKS) {
144
- console.warn(
145
- `[InputControl] dynamic value list "${item.paramId}": options keep invalidating the ` +
146
- `auto-picked selection (${autoPickCount} consecutive system picks) — the definition's ` +
147
- `options likely depend on the selection. Stopping auto-reconciliation to avoid a solve loop.`
148
- );
149
- return;
150
- }
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
127
  if (Array.isArray(value)) {
166
128
  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.
170
- autoPickCount++;
171
- logAutoPick(pruned.length > 0 ? String(pruned[0]) : firstOption);
129
+ // Empty (never selected or fully pruned) always falls back to the first option
130
+ // a checklist that solves empty produces the same null cascade as a single value.
131
+ if (pruned.length !== value.length || value.length === 0) {
172
132
  onChange(item.paramId, pruned.length > 0 ? pruned : [firstOption], true);
173
133
  }
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.
179
- autoPickCount++;
180
- logAutoPick(firstOption);
134
+ } else if (typeof value !== 'string' || value === '' || !validValues.has(value)) {
135
+ // Never-selected or stale single value fall back to the first option.
181
136
  onChange(item.paramId, firstOption, true);
182
137
  }
183
138
  });
184
139
 
185
140
  function commit(newValue: SupportedTypes) {
186
- userTouched = true;
187
- // A real user pick re-arms the system fallback (see autoPickCount above).
188
- autoPickCount = 0;
189
141
  value = newValue;
190
142
  onChange(item.paramId, newValue);
191
143
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@selvajs/ui",
3
- "version": "4.12.3",
3
+ "version": "4.12.4",
4
4
  "description": "Shared UI components and utilities for Selva applications",
5
5
  "license": "MIT",
6
6
  "publishConfig": {
@@ -110,82 +110,34 @@
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
127
- // system-initiated picks; any real user commit resets the budget.
128
- const MAX_CONSECUTIVE_AUTO_PICKS = 3;
129
- let autoPickCount = 0;
130
-
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.
131
123
  $effect(() => {
132
124
  if (!isDynamicValueListWidget(item) || !dynamicListHasOptions) return;
133
125
  const validValues = new Set(Object.values(dynamicListOptions));
134
126
  const firstOption = Object.values(dynamicListOptions)[0];
135
- // Selection is valid → stable state reached; refill the auto-pick budget.
136
- const isValid = Array.isArray(value)
137
- ? value.length > 0 && value.every((v) => typeof v === 'string' && validValues.has(v))
138
- : typeof value === 'string' && value !== '' && validValues.has(value);
139
- if (isValid) {
140
- autoPickCount = 0;
141
- return;
142
- }
143
- if (autoPickCount >= MAX_CONSECUTIVE_AUTO_PICKS) {
144
- console.warn(
145
- `[InputControl] dynamic value list "${item.paramId}": options keep invalidating the ` +
146
- `auto-picked selection (${autoPickCount} consecutive system picks) — the definition's ` +
147
- `options likely depend on the selection. Stopping auto-reconciliation to avoid a solve loop.`
148
- );
149
- return;
150
- }
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
127
  if (Array.isArray(value)) {
166
128
  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.
170
- autoPickCount++;
171
- logAutoPick(pruned.length > 0 ? String(pruned[0]) : firstOption);
129
+ // Empty (never selected or fully pruned) always falls back to the first option
130
+ // a checklist that solves empty produces the same null cascade as a single value.
131
+ if (pruned.length !== value.length || value.length === 0) {
172
132
  onChange(item.paramId, pruned.length > 0 ? pruned : [firstOption], true);
173
133
  }
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.
179
- autoPickCount++;
180
- logAutoPick(firstOption);
134
+ } else if (typeof value !== 'string' || value === '' || !validValues.has(value)) {
135
+ // Never-selected or stale single value fall back to the first option.
181
136
  onChange(item.paramId, firstOption, true);
182
137
  }
183
138
  });
184
139
 
185
140
  function commit(newValue: SupportedTypes) {
186
- userTouched = true;
187
- // A real user pick re-arms the system fallback (see autoPickCount above).
188
- autoPickCount = 0;
189
141
  value = newValue;
190
142
  onChange(item.paramId, newValue);
191
143
  }