@selvajs/ui 4.12.4 → 5.0.0-beta.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.
@@ -120,24 +120,53 @@
120
120
  // — which throws NREs in downstream geometry components (e.g. Bounding Rectangle) and nulls
121
121
  // every output beyond them. So every terminal state below resolves to a valid option; there
122
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
128
+ // system-initiated picks; any real user commit resets the budget.
129
+ const MAX_CONSECUTIVE_AUTO_PICKS = 3;
130
+ let autoPickCount = 0;
131
+
123
132
  $effect(() => {
124
133
  if (!isDynamicValueListWidget(item) || !dynamicListHasOptions) return;
125
134
  const validValues = new Set(Object.values(dynamicListOptions));
126
135
  const firstOption = Object.values(dynamicListOptions)[0];
136
+ // Already valid → stable; refill the auto-pick budget and stop.
137
+ const isValid = Array.isArray(value)
138
+ ? value.length > 0 && value.every((v) => typeof v === 'string' && validValues.has(v))
139
+ : typeof value === 'string' && value !== '' && validValues.has(value);
140
+ if (isValid) {
141
+ autoPickCount = 0;
142
+ return;
143
+ }
144
+ if (autoPickCount >= MAX_CONSECUTIVE_AUTO_PICKS) {
145
+ console.warn(
146
+ `[InputControl] dynamic value list "${item.paramId}": options keep invalidating the ` +
147
+ `auto-picked selection (${autoPickCount} consecutive system picks) — the definition's ` +
148
+ `options likely depend on the selection. Stopping auto-reconciliation to avoid a solve loop.`
149
+ );
150
+ return;
151
+ }
127
152
  if (Array.isArray(value)) {
128
153
  const pruned = value.filter((v) => typeof v === 'string' && validValues.has(v));
129
154
  // Empty (never selected or fully pruned) always falls back to the first option —
130
155
  // a checklist that solves empty produces the same null cascade as a single value.
131
156
  if (pruned.length !== value.length || value.length === 0) {
157
+ autoPickCount++;
132
158
  onChange(item.paramId, pruned.length > 0 ? pruned : [firstOption], true);
133
159
  }
134
160
  } else if (typeof value !== 'string' || value === '' || !validValues.has(value)) {
135
161
  // Never-selected or stale single value — fall back to the first option.
162
+ autoPickCount++;
136
163
  onChange(item.paramId, firstOption, true);
137
164
  }
138
165
  });
139
166
 
140
167
  function commit(newValue: SupportedTypes) {
168
+ // A real user pick re-arms the system fallback (see autoPickCount above).
169
+ autoPickCount = 0;
141
170
  value = newValue;
142
171
  onChange(item.paramId, newValue);
143
172
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@selvajs/ui",
3
- "version": "4.12.4",
3
+ "version": "5.0.0-beta.0",
4
4
  "description": "Shared UI components and utilities for Selva applications",
5
5
  "license": "MIT",
6
6
  "publishConfig": {
@@ -37,13 +37,13 @@
37
37
  "**/*.css"
38
38
  ],
39
39
  "peerDependencies": {
40
- "@selvajs/compute": "^3.0.1",
40
+ "@selvajs/compute": "^3.1.0-beta.3",
41
41
  "@sveltejs/kit": "^2",
42
42
  "bits-ui": "^2.18.0",
43
43
  "svelte": "^5",
44
44
  "tailwind-variants": "^3.2.2",
45
45
  "three": "^0.184.0",
46
- "@selvajs/schemas": "^4.6.1"
46
+ "@selvajs/schemas": "^4.7.0-beta.0"
47
47
  },
48
48
  "peerDependenciesMeta": {
49
49
  "three": {
@@ -63,17 +63,17 @@
63
63
  },
64
64
  "devDependencies": {
65
65
  "@internationalized/date": "^3.12.1",
66
- "@sveltejs/kit": "2.58.0",
66
+ "@sveltejs/kit": "2.69.1",
67
67
  "@sveltejs/vite-plugin-svelte": "^6.2.4",
68
68
  "@types/three": "^0.184.0",
69
69
  "bits-ui": "^2.18.0",
70
70
  "rhino3dm": "8.17.0",
71
71
  "rimraf": "^6.0.1",
72
- "svelte": "5.55.5",
72
+ "svelte": "5.56.4",
73
73
  "tailwind-variants": "^3.2.2",
74
74
  "vitest": "^3.2.6",
75
75
  "@selvajs/config": "0.0.2",
76
- "@selvajs/schemas": "4.6.1"
76
+ "@selvajs/schemas": "4.7.0-beta.0"
77
77
  },
78
78
  "scripts": {
79
79
  "dev": "vite dev",
@@ -120,24 +120,53 @@
120
120
  // — which throws NREs in downstream geometry components (e.g. Bounding Rectangle) and nulls
121
121
  // every output beyond them. So every terminal state below resolves to a valid option; there
122
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
128
+ // system-initiated picks; any real user commit resets the budget.
129
+ const MAX_CONSECUTIVE_AUTO_PICKS = 3;
130
+ let autoPickCount = 0;
131
+
123
132
  $effect(() => {
124
133
  if (!isDynamicValueListWidget(item) || !dynamicListHasOptions) return;
125
134
  const validValues = new Set(Object.values(dynamicListOptions));
126
135
  const firstOption = Object.values(dynamicListOptions)[0];
136
+ // Already valid → stable; refill the auto-pick budget and stop.
137
+ const isValid = Array.isArray(value)
138
+ ? value.length > 0 && value.every((v) => typeof v === 'string' && validValues.has(v))
139
+ : typeof value === 'string' && value !== '' && validValues.has(value);
140
+ if (isValid) {
141
+ autoPickCount = 0;
142
+ return;
143
+ }
144
+ if (autoPickCount >= MAX_CONSECUTIVE_AUTO_PICKS) {
145
+ console.warn(
146
+ `[InputControl] dynamic value list "${item.paramId}": options keep invalidating the ` +
147
+ `auto-picked selection (${autoPickCount} consecutive system picks) — the definition's ` +
148
+ `options likely depend on the selection. Stopping auto-reconciliation to avoid a solve loop.`
149
+ );
150
+ return;
151
+ }
127
152
  if (Array.isArray(value)) {
128
153
  const pruned = value.filter((v) => typeof v === 'string' && validValues.has(v));
129
154
  // Empty (never selected or fully pruned) always falls back to the first option —
130
155
  // a checklist that solves empty produces the same null cascade as a single value.
131
156
  if (pruned.length !== value.length || value.length === 0) {
157
+ autoPickCount++;
132
158
  onChange(item.paramId, pruned.length > 0 ? pruned : [firstOption], true);
133
159
  }
134
160
  } else if (typeof value !== 'string' || value === '' || !validValues.has(value)) {
135
161
  // Never-selected or stale single value — fall back to the first option.
162
+ autoPickCount++;
136
163
  onChange(item.paramId, firstOption, true);
137
164
  }
138
165
  });
139
166
 
140
167
  function commit(newValue: SupportedTypes) {
168
+ // A real user pick re-arms the system fallback (see autoPickCount above).
169
+ autoPickCount = 0;
141
170
  value = newValue;
142
171
  onChange(item.paramId, newValue);
143
172
  }