@morscherlab/mint-sdk 1.0.0 → 1.0.1

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 (68) hide show
  1. package/dist/BaseModal-B9UA8Y_I.js +165 -0
  2. package/dist/BaseModal-B9UA8Y_I.js.map +1 -0
  3. package/dist/BaseSelect-DksaKYq_.js +176 -0
  4. package/dist/BaseSelect-DksaKYq_.js.map +1 -0
  5. package/dist/ExperimentPopover-CCYB1oWp.js +361 -0
  6. package/dist/ExperimentPopover-CCYB1oWp.js.map +1 -0
  7. package/dist/ExperimentPopover-D0bg_fqM.js +3 -0
  8. package/dist/ExperimentSelectorModal-B_kPbXcg.js +4 -0
  9. package/dist/ExperimentSelectorModal-wm7yUdAr.js +720 -0
  10. package/dist/ExperimentSelectorModal-wm7yUdAr.js.map +1 -0
  11. package/dist/SettingsModal-L7Ejny45.js +5 -0
  12. package/dist/SettingsModal-LEKI6Ebl.js +521 -0
  13. package/dist/SettingsModal-LEKI6Ebl.js.map +1 -0
  14. package/dist/{auth-BulIv_km.js → auth-D9q2GIcv.js} +3 -80
  15. package/dist/auth-D9q2GIcv.js.map +1 -0
  16. package/dist/components/DataFrame.vue.d.ts +3 -0
  17. package/dist/components/ExperimentDataViewer.vue.d.ts +2 -0
  18. package/dist/components/PluginWorkspaceView.vue.d.ts +2 -2
  19. package/dist/components/index.js +7 -2
  20. package/dist/{components-DtX3LDLq.js → components-CdjRzHI2.js} +533 -2025
  21. package/dist/components-CdjRzHI2.js.map +1 -0
  22. package/dist/composables/index.js +9 -3
  23. package/dist/composables/usePluginClient.d.ts +2 -1
  24. package/dist/{composables-wNt7VtkF.js → composables-DJgqPrlR.js} +7 -12
  25. package/dist/{composables-wNt7VtkF.js.map → composables-DJgqPrlR.js.map} +1 -1
  26. package/dist/experiment-utils-hGXMHlAc.js +109 -0
  27. package/dist/experiment-utils-hGXMHlAc.js.map +1 -0
  28. package/dist/index.js +16 -5
  29. package/dist/index.js.map +1 -1
  30. package/dist/install.js +7 -2
  31. package/dist/install.js.map +1 -1
  32. package/dist/permissions.js +81 -0
  33. package/dist/permissions.js.map +1 -0
  34. package/dist/stores/index.js +1 -1
  35. package/dist/styles.css +3233 -3185
  36. package/dist/templates/index.js +3 -1
  37. package/dist/templates-Do43ZIMb.js +5065 -0
  38. package/dist/templates-Do43ZIMb.js.map +1 -0
  39. package/dist/{templates-DSbHJC4v.js → useControlSchema-0n8Bcftq.js} +10 -5335
  40. package/dist/useControlSchema-0n8Bcftq.js.map +1 -0
  41. package/dist/useDropdownState-Ben4DnjJ.js +47 -0
  42. package/dist/useDropdownState-Ben4DnjJ.js.map +1 -0
  43. package/dist/useEventListener-CfVkP9Xz.js +57 -0
  44. package/dist/useEventListener-CfVkP9Xz.js.map +1 -0
  45. package/dist/useExperimentSelector-BpZklTbV.js +469 -0
  46. package/dist/useExperimentSelector-BpZklTbV.js.map +1 -0
  47. package/dist/useFormBuilder-COfYWDuC.js +729 -0
  48. package/dist/useFormBuilder-COfYWDuC.js.map +1 -0
  49. package/dist/{useProtocolTemplates-DwBhEPPU.js → useProtocolTemplates-TUQO_F3n.js} +8 -1298
  50. package/dist/useProtocolTemplates-TUQO_F3n.js.map +1 -0
  51. package/dist/utils/pluginIcon.d.ts +29 -2
  52. package/package.json +5 -1
  53. package/src/__tests__/components/DataFrame.test.ts +37 -0
  54. package/src/__tests__/components/PluginIcon.test.ts +77 -0
  55. package/src/__tests__/composables/usePluginClient.test.ts +11 -10
  56. package/src/components/AppTopBar.vue +7 -6
  57. package/src/components/DataFrame.vue +27 -2
  58. package/src/components/ExperimentDataViewer.vue +5 -1
  59. package/src/components/PluginIcon.story.vue +31 -1
  60. package/src/components/PluginIcon.vue +94 -4
  61. package/src/composables/usePluginClient.ts +3 -12
  62. package/src/styles/components/dataframe.css +26 -0
  63. package/src/styles/components/plugin-icon.css +5 -0
  64. package/src/utils/pluginIcon.ts +159 -2
  65. package/dist/auth-BulIv_km.js.map +0 -1
  66. package/dist/components-DtX3LDLq.js.map +0 -1
  67. package/dist/templates-DSbHJC4v.js.map +0 -1
  68. package/dist/useProtocolTemplates-DwBhEPPU.js.map +0 -1
@@ -0,0 +1,729 @@
1
+ import { D as getTypeDefault, E as getFieldRegistryEntry } from "./useControlSchema-0n8Bcftq.js";
2
+ import { computed, onMounted, onUnmounted, reactive, ref, shallowRef, toRaw, watch } from "vue";
3
+ //#region src/composables/usePlatformContext.ts
4
+ var DEFAULT_CONTEXT = {
5
+ isIntegrated: false,
6
+ theme: "system"
7
+ };
8
+ var platformContext = ref({ ...DEFAULT_CONTEXT });
9
+ var inferredOrigins = /* @__PURE__ */ new Set();
10
+ var allowedOrigins = /* @__PURE__ */ new Set();
11
+ var allowAnyOrigin = false;
12
+ var initialized = false;
13
+ var listenerCount = 0;
14
+ var nextConsumerId = 0;
15
+ var currentHandler = null;
16
+ var consumerOrigins = /* @__PURE__ */ new Map();
17
+ var allowAnyOriginConsumers = /* @__PURE__ */ new Set();
18
+ /**
19
+ * Derive origin from URL (protocol + host)
20
+ */
21
+ function getOriginFromUrl(url) {
22
+ try {
23
+ return new URL(url).origin;
24
+ } catch {
25
+ return null;
26
+ }
27
+ }
28
+ function normalizeAllowedOrigins(origins) {
29
+ const normalized = /* @__PURE__ */ new Set();
30
+ if (!origins) return normalized;
31
+ for (const origin of origins) normalized.add(getOriginFromUrl(origin) || origin);
32
+ return normalized;
33
+ }
34
+ function recomputeOriginPolicy() {
35
+ allowedOrigins = new Set(inferredOrigins);
36
+ for (const origins of consumerOrigins.values()) for (const origin of origins) allowedOrigins.add(origin);
37
+ allowAnyOrigin = allowAnyOriginConsumers.size > 0;
38
+ }
39
+ function resetPlatformContextState() {
40
+ inferredOrigins = /* @__PURE__ */ new Set();
41
+ allowedOrigins = /* @__PURE__ */ new Set();
42
+ allowAnyOrigin = false;
43
+ initialized = false;
44
+ listenerCount = 0;
45
+ currentHandler = null;
46
+ consumerOrigins.clear();
47
+ allowAnyOriginConsumers.clear();
48
+ platformContext.value = { ...DEFAULT_CONTEXT };
49
+ }
50
+ /**
51
+ * Check if an origin is allowed for postMessage communication
52
+ */
53
+ function isOriginAllowed(origin) {
54
+ if (allowAnyOrigin) {
55
+ console.warn("[MINT SDK] postMessage origin validation disabled - only use in development");
56
+ return true;
57
+ }
58
+ if (origin === window.location.origin) return true;
59
+ return allowedOrigins.has(origin);
60
+ }
61
+ /**
62
+ * Platform context composable for plugin integration with MINT Platform.
63
+ *
64
+ * Provides secure communication with the parent platform via postMessage.
65
+ *
66
+ * @param options - Configuration options
67
+ * @param options.allowedOrigins - List of allowed origins for postMessage
68
+ * @param options.allowAnyOrigin - Allow any origin (UNSAFE, development only)
69
+ *
70
+ * @example
71
+ * ```typescript
72
+ * // Basic usage - derives origin from platform injection
73
+ * const { isIntegrated, user, theme } = usePlatformContext()
74
+ *
75
+ * // With explicit allowed origins
76
+ * const { isIntegrated } = usePlatformContext({
77
+ * allowedOrigins: ['https://mint.example.com']
78
+ * })
79
+ *
80
+ * // Development mode (UNSAFE)
81
+ * const { isIntegrated } = usePlatformContext({
82
+ * allowAnyOrigin: import.meta.env.DEV
83
+ * })
84
+ * ```
85
+ */
86
+ /** Connects a plugin to the MINT platform via postMessage, exposing user, theme, and experiment context. */
87
+ function usePlatformContext(options = {}) {
88
+ const consumerId = ++nextConsumerId;
89
+ const instanceOrigins = normalizeAllowedOrigins(options.allowedOrigins);
90
+ const instanceAllowAnyOrigin = options.allowAnyOrigin === true;
91
+ function detectPlatform() {
92
+ const detectedOrigins = /* @__PURE__ */ new Set();
93
+ const platformData = window.__MINT_PLATFORM__;
94
+ if (platformData) {
95
+ platformContext.value = {
96
+ ...platformData,
97
+ isIntegrated: true
98
+ };
99
+ if (platformData.platformOrigin) detectedOrigins.add(platformData.platformOrigin);
100
+ else if (platformData.platformApiUrl) {
101
+ const origin = getOriginFromUrl(platformData.platformApiUrl);
102
+ if (origin) detectedOrigins.add(origin);
103
+ }
104
+ } else {
105
+ const urlParams = new URLSearchParams(window.location.search);
106
+ const hasPluginParam = urlParams.has("mint-plugin");
107
+ const platformOrigin = urlParams.get("mint-origin");
108
+ if (platformOrigin) {
109
+ const origin = getOriginFromUrl(platformOrigin);
110
+ if (origin) detectedOrigins.add(origin);
111
+ }
112
+ platformContext.value = {
113
+ isIntegrated: hasPluginParam,
114
+ theme: (() => {
115
+ try {
116
+ const s = localStorage.getItem("mint-settings");
117
+ if (s) return JSON.parse(s).theme || "system";
118
+ } catch {}
119
+ return "system";
120
+ })(),
121
+ platformOrigin: platformOrigin || void 0
122
+ };
123
+ }
124
+ inferredOrigins = detectedOrigins;
125
+ }
126
+ function handlePlatformMessage(event) {
127
+ if (event.source !== window.parent) return;
128
+ if (!isOriginAllowed(event.origin)) {
129
+ console.warn(`[MINT SDK] Rejected postMessage from untrusted origin: ${event.origin}`);
130
+ return;
131
+ }
132
+ try {
133
+ const platformEvent = event.data;
134
+ if (!platformEvent.type?.startsWith("mint:")) return;
135
+ switch (platformEvent.type) {
136
+ case "mint:theme-changed":
137
+ platformContext.value.theme = platformEvent.payload;
138
+ break;
139
+ case "mint:user-changed":
140
+ platformContext.value.user = platformEvent.payload;
141
+ break;
142
+ }
143
+ } catch {}
144
+ }
145
+ /**
146
+ * Send a message to the parent platform.
147
+ * Uses validated target origin for security.
148
+ */
149
+ function sendToPlatform(event) {
150
+ if (!platformContext.value.isIntegrated || window.parent === window) return;
151
+ let targetOrigin;
152
+ if (platformContext.value.platformOrigin) targetOrigin = platformContext.value.platformOrigin;
153
+ else if (allowedOrigins.size > 0) targetOrigin = allowedOrigins.values().next().value;
154
+ else if (allowAnyOrigin) {
155
+ targetOrigin = "*";
156
+ console.warn("[MINT SDK] Using wildcard origin for postMessage - only use in development");
157
+ } else {
158
+ console.warn("[MINT SDK] Cannot send postMessage: no platform origin configured");
159
+ return;
160
+ }
161
+ window.parent.postMessage(event, targetOrigin);
162
+ }
163
+ /**
164
+ * Request navigation to a path in the platform.
165
+ */
166
+ function navigate(path) {
167
+ sendToPlatform({
168
+ type: "mint:navigate",
169
+ payload: path
170
+ });
171
+ }
172
+ /**
173
+ * Show a notification in the platform.
174
+ */
175
+ function notify(message, type = "info") {
176
+ sendToPlatform({
177
+ type: "mint:notification",
178
+ payload: {
179
+ message,
180
+ type
181
+ }
182
+ });
183
+ }
184
+ onMounted(() => {
185
+ consumerOrigins.set(consumerId, instanceOrigins);
186
+ if (instanceAllowAnyOrigin) allowAnyOriginConsumers.add(consumerId);
187
+ if (!initialized) {
188
+ detectPlatform();
189
+ currentHandler = handlePlatformMessage;
190
+ window.addEventListener("message", handlePlatformMessage);
191
+ initialized = true;
192
+ }
193
+ listenerCount++;
194
+ recomputeOriginPolicy();
195
+ });
196
+ onUnmounted(() => {
197
+ consumerOrigins.delete(consumerId);
198
+ allowAnyOriginConsumers.delete(consumerId);
199
+ listenerCount = Math.max(0, listenerCount - 1);
200
+ if (listenerCount === 0) {
201
+ if (currentHandler) window.removeEventListener("message", currentHandler);
202
+ resetPlatformContextState();
203
+ return;
204
+ }
205
+ recomputeOriginPolicy();
206
+ });
207
+ return {
208
+ context: platformContext,
209
+ isIntegrated: computed(() => platformContext.value.isIntegrated),
210
+ plugin: computed(() => platformContext.value.plugin),
211
+ user: computed(() => platformContext.value.user),
212
+ theme: computed(() => platformContext.value.theme),
213
+ features: computed(() => platformContext.value.features),
214
+ navigate,
215
+ notify,
216
+ sendToPlatform
217
+ };
218
+ }
219
+ //#endregion
220
+ //#region src/composables/useFormValidation.ts
221
+ var validators = {
222
+ required: (value, message = "This field is required") => {
223
+ if (value === null || value === void 0 || value === "") return message;
224
+ if (Array.isArray(value) && value.length === 0) return message;
225
+ return null;
226
+ },
227
+ minLength: (value, min, message) => {
228
+ if (typeof value !== "string") return null;
229
+ if (value.length < min) return message || `Must be at least ${min} characters`;
230
+ return null;
231
+ },
232
+ maxLength: (value, max, message) => {
233
+ if (typeof value !== "string") return null;
234
+ if (value.length > max) return message || `Must be at most ${max} characters`;
235
+ return null;
236
+ },
237
+ min: (value, min, message) => {
238
+ if (typeof value !== "number") return null;
239
+ if (value < min) return message || `Must be at least ${min}`;
240
+ return null;
241
+ },
242
+ max: (value, max, message) => {
243
+ if (typeof value !== "number") return null;
244
+ if (value > max) return message || `Must be at most ${max}`;
245
+ return null;
246
+ },
247
+ pattern: (value, pattern, message) => {
248
+ if (typeof value !== "string") return null;
249
+ if (!pattern.test(value)) return message || "Invalid format";
250
+ return null;
251
+ },
252
+ email: (value, message = "Invalid email address") => {
253
+ if (typeof value !== "string" || !value) return null;
254
+ if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) return message;
255
+ return null;
256
+ }
257
+ };
258
+ function validateFieldValue(value, fieldRules, formData) {
259
+ if (!fieldRules) return null;
260
+ if (fieldRules.required) {
261
+ const message = typeof fieldRules.required === "string" ? fieldRules.required : void 0;
262
+ const error = validators.required(value, message);
263
+ if (error) return error;
264
+ }
265
+ if (value === null || value === void 0 || value === "") return null;
266
+ if (fieldRules.minLength !== void 0) {
267
+ const config = typeof fieldRules.minLength === "number" ? {
268
+ value: fieldRules.minLength,
269
+ message: void 0
270
+ } : fieldRules.minLength;
271
+ const error = validators.minLength(value, config.value, config.message);
272
+ if (error) return error;
273
+ }
274
+ if (fieldRules.maxLength !== void 0) {
275
+ const config = typeof fieldRules.maxLength === "number" ? {
276
+ value: fieldRules.maxLength,
277
+ message: void 0
278
+ } : fieldRules.maxLength;
279
+ const error = validators.maxLength(value, config.value, config.message);
280
+ if (error) return error;
281
+ }
282
+ if (fieldRules.min !== void 0) {
283
+ const config = typeof fieldRules.min === "number" ? {
284
+ value: fieldRules.min,
285
+ message: void 0
286
+ } : fieldRules.min;
287
+ const error = validators.min(value, config.value, config.message);
288
+ if (error) return error;
289
+ }
290
+ if (fieldRules.max !== void 0) {
291
+ const config = typeof fieldRules.max === "number" ? {
292
+ value: fieldRules.max,
293
+ message: void 0
294
+ } : fieldRules.max;
295
+ const error = validators.max(value, config.value, config.message);
296
+ if (error) return error;
297
+ }
298
+ if (fieldRules.pattern !== void 0) {
299
+ const config = fieldRules.pattern instanceof RegExp ? {
300
+ value: fieldRules.pattern,
301
+ message: void 0
302
+ } : fieldRules.pattern;
303
+ const error = validators.pattern(value, config.value, config.message);
304
+ if (error) return error;
305
+ }
306
+ if (fieldRules.email) {
307
+ const message = typeof fieldRules.email === "string" ? fieldRules.email : void 0;
308
+ const error = validators.email(value, message);
309
+ if (error) return error;
310
+ }
311
+ if (fieldRules.custom) {
312
+ const customRules = Array.isArray(fieldRules.custom) ? fieldRules.custom : [fieldRules.custom];
313
+ for (const rule of customRules) {
314
+ const error = rule(value, formData);
315
+ if (error) return error;
316
+ }
317
+ }
318
+ return null;
319
+ }
320
+ //#endregion
321
+ //#region src/composables/useForm.ts
322
+ /**
323
+ * Form state management composable with validation.
324
+ *
325
+ * @param initialValues - Initial form values
326
+ * @param rules - Validation rules for each field
327
+ *
328
+ * @example
329
+ * ```typescript
330
+ * const { data, errors, isValid, handleSubmit, getFieldProps } = useForm(
331
+ * { email: '', password: '' },
332
+ * {
333
+ * email: { required: true, email: true },
334
+ * password: { required: true, minLength: 8 },
335
+ * }
336
+ * )
337
+ *
338
+ * // In template
339
+ * <BaseInput v-bind="getFieldProps('email')" label="Email" />
340
+ * <BaseInput v-bind="getFieldProps('password')" type="password" label="Password" />
341
+ * <BaseButton @click="handleSubmit(onSubmit)" :disabled="!isValid">Submit</BaseButton>
342
+ * ```
343
+ */
344
+ /** Reactive form state with field-level validation, dirty tracking, and submit handling. */
345
+ function useForm(initialValues, rules = {}) {
346
+ const cloneableInitialValues = deepToRaw(initialValues);
347
+ let _initialValues = structuredClone(cloneableInitialValues);
348
+ const data = reactive(structuredClone(cloneableInitialValues));
349
+ const errors = reactive(Object.keys(initialValues).reduce((acc, key) => {
350
+ acc[key] = null;
351
+ return acc;
352
+ }, {}));
353
+ const touched = reactive(Object.keys(initialValues).reduce((acc, key) => {
354
+ acc[key] = false;
355
+ return acc;
356
+ }, {}));
357
+ const dirty = reactive(Object.keys(initialValues).reduce((acc, key) => {
358
+ acc[key] = false;
359
+ return acc;
360
+ }, {}));
361
+ const isSubmitting = ref(false);
362
+ watch(() => ({ ...data }), (newData) => {
363
+ for (const key of Object.keys(newData)) dirty[key] = newData[key] !== _initialValues[key];
364
+ }, { deep: true });
365
+ function validateField(field) {
366
+ const value = data[field];
367
+ const fieldRules = rules[field];
368
+ const error = validateFieldValue(value, fieldRules, data);
369
+ errors[field] = error;
370
+ return error === null;
371
+ }
372
+ function validate() {
373
+ let isAllValid = true;
374
+ for (const field of Object.keys(data)) if (!validateField(field)) isAllValid = false;
375
+ return isAllValid;
376
+ }
377
+ const isValid = computed(() => {
378
+ return Object.values(errors).every((error) => error === null);
379
+ });
380
+ const isDirty = computed(() => {
381
+ return Object.values(dirty).some((d) => d);
382
+ });
383
+ function setFieldValue(field, value) {
384
+ data[field] = value;
385
+ if (touched[field]) validateField(field);
386
+ }
387
+ function setFieldError(field, error) {
388
+ errors[field] = error;
389
+ }
390
+ function setFieldTouched(field, isTouched = true) {
391
+ touched[field] = isTouched;
392
+ if (isTouched) validateField(field);
393
+ }
394
+ function reset(values) {
395
+ const resetValues = values ? {
396
+ ..._initialValues,
397
+ ...values
398
+ } : _initialValues;
399
+ for (const key of Object.keys(data)) {
400
+ data[key] = structuredClone(deepToRaw(resetValues[key]));
401
+ errors[key] = null;
402
+ touched[key] = false;
403
+ dirty[key] = false;
404
+ }
405
+ }
406
+ function replaceState(values) {
407
+ const nextValues = structuredClone(deepToRaw(values));
408
+ _initialValues = structuredClone(nextValues);
409
+ for (const key of Object.keys(data)) {
410
+ if (key in nextValues) continue;
411
+ delete data[key];
412
+ delete errors[key];
413
+ delete touched[key];
414
+ delete dirty[key];
415
+ }
416
+ for (const key of Object.keys(nextValues)) {
417
+ data[key] = structuredClone(deepToRaw(nextValues[key]));
418
+ errors[key] = null;
419
+ touched[key] = false;
420
+ dirty[key] = false;
421
+ }
422
+ }
423
+ function handleSubmit(onSubmit) {
424
+ return async (e) => {
425
+ e?.preventDefault();
426
+ for (const field of Object.keys(data)) touched[field] = true;
427
+ if (!validate()) return;
428
+ isSubmitting.value = true;
429
+ try {
430
+ await onSubmit(data);
431
+ } finally {
432
+ isSubmitting.value = false;
433
+ }
434
+ };
435
+ }
436
+ function getFieldProps(field) {
437
+ const fieldStr = field;
438
+ return {
439
+ modelValue: data[field],
440
+ "onUpdate:modelValue": (value) => setFieldValue(field, value),
441
+ onBlur: () => setFieldTouched(fieldStr),
442
+ error: touched[fieldStr] ? errors[fieldStr] : null
443
+ };
444
+ }
445
+ return {
446
+ data,
447
+ errors,
448
+ touched,
449
+ dirty,
450
+ isValid,
451
+ isDirty,
452
+ isSubmitting,
453
+ setFieldValue,
454
+ setFieldError,
455
+ setFieldTouched,
456
+ validateField,
457
+ validate,
458
+ reset,
459
+ replaceState,
460
+ handleSubmit,
461
+ getFieldProps
462
+ };
463
+ }
464
+ function deepToRaw(value) {
465
+ const raw = toRaw(value);
466
+ if (Array.isArray(raw)) return raw.map((item) => deepToRaw(item));
467
+ if (isPlainRecord(raw)) return Object.fromEntries(Object.entries(raw).map(([key, item]) => [key, deepToRaw(item)]));
468
+ return raw;
469
+ }
470
+ function isPlainRecord(value) {
471
+ return Object.prototype.toString.call(value) === "[object Object]";
472
+ }
473
+ //#endregion
474
+ //#region src/composables/formBuilderSchema.ts
475
+ /**
476
+ * Evaluate a JSON-serializable field condition against the current form data.
477
+ *
478
+ * Supports logical operators (`and`, `or`, `not`) and comparison operators
479
+ * (`eq`, `neq`, `gt`, `lt`, `gte`, `lte`, `in`, `notIn`, `truthy`, `falsy`,
480
+ * `contains`). Returns `true` if the condition passes.
481
+ */
482
+ function evaluateCondition(condition, data) {
483
+ if ("and" in condition) return condition.and.every((c) => evaluateCondition(c, data));
484
+ if ("or" in condition) return condition.or.some((c) => evaluateCondition(c, data));
485
+ if ("not" in condition) return !evaluateCondition(condition.not, data);
486
+ const value = data[condition.field];
487
+ if ("eq" in condition) return value === condition.eq;
488
+ if ("neq" in condition) return value !== condition.neq;
489
+ if ("gt" in condition) return typeof value === "number" && value > condition.gt;
490
+ if ("lt" in condition) return typeof value === "number" && value < condition.lt;
491
+ if ("gte" in condition) return typeof value === "number" && value >= condition.gte;
492
+ if ("lte" in condition) return typeof value === "number" && value <= condition.lte;
493
+ if ("in" in condition) return condition.in.includes(value);
494
+ if ("notIn" in condition) return !condition.notIn.includes(value);
495
+ if ("truthy" in condition) return !!value;
496
+ if ("falsy" in condition) return !value;
497
+ if ("contains" in condition) {
498
+ if (typeof value === "string") return value.includes(condition.contains);
499
+ if (Array.isArray(value)) return value.includes(condition.contains);
500
+ return false;
501
+ }
502
+ return true;
503
+ }
504
+ /** Return all sections across steps (wizard) or directly from a flat schema. */
505
+ function collectSections(schema) {
506
+ return schema.steps ? schema.steps.flatMap((step) => step.sections) : schema.sections;
507
+ }
508
+ /** Return all field schemas in schema order, flattening sections and steps. */
509
+ function flattenFields(schema) {
510
+ return collectSections(schema).flatMap((section) => section.fields);
511
+ }
512
+ /** Convert a JSON-safe `FieldValidation` descriptor to a runtime `FieldRules` object. */
513
+ function convertValidation(v) {
514
+ const rules = {};
515
+ if (v.required !== void 0) rules.required = v.required;
516
+ if (v.minLength !== void 0) rules.minLength = v.minLength;
517
+ if (v.maxLength !== void 0) rules.maxLength = v.maxLength;
518
+ if (v.min !== void 0) rules.min = v.min;
519
+ if (v.max !== void 0) rules.max = v.max;
520
+ if (v.email !== void 0) rules.email = v.email;
521
+ if (v.pattern !== void 0) rules.pattern = typeof v.pattern === "string" ? new RegExp(v.pattern) : {
522
+ value: new RegExp(v.pattern.value),
523
+ message: v.pattern.message
524
+ };
525
+ return rules;
526
+ }
527
+ function buildInitialValues(fields, initialData) {
528
+ const initialValues = {};
529
+ for (const field of fields) {
530
+ const key = field.name;
531
+ if (initialData && hasOwnKey(initialData, key)) initialValues[key] = initialData[key];
532
+ else if (field.defaultValue !== void 0) initialValues[key] = field.defaultValue;
533
+ else initialValues[key] = getTypeDefault(field.type);
534
+ }
535
+ return initialValues;
536
+ }
537
+ function buildRules(fields, enhancements) {
538
+ const rules = {};
539
+ for (const field of fields) {
540
+ const base = field.validation ? convertValidation(field.validation) : {};
541
+ const enhancement = enhancements?.fields?.[field.name];
542
+ const customValidators = [];
543
+ if (enhancement?.validate) {
544
+ const fn = enhancement.validate;
545
+ customValidators.push((value, formData) => fn(value, formData));
546
+ }
547
+ if (customValidators.length > 0) base.custom = customValidators;
548
+ if (Object.keys(base).length > 0) rules[field.name] = base;
549
+ }
550
+ return rules;
551
+ }
552
+ function replaceArray(target, values) {
553
+ target.splice(0, target.length, ...values);
554
+ }
555
+ function replaceRecord(target, source) {
556
+ for (const key of Object.keys(target)) delete target[key];
557
+ Object.assign(target, source);
558
+ }
559
+ function hasOwnKey(source, key) {
560
+ return Object.prototype.hasOwnProperty.call(source, key);
561
+ }
562
+ //#endregion
563
+ //#region src/composables/useFormBuilder.ts
564
+ /**
565
+ * Drive a `FormSchema` as reactive form state.
566
+ *
567
+ * Builds initial values from schema defaults and `initialData`, derives
568
+ * validation rules from `FieldValidation` descriptors and enhancement
569
+ * validators, evaluates `FieldCondition` expressions for field/section
570
+ * visibility, and wires wizard step navigation when `schema.steps` is set.
571
+ *
572
+ * @param schema - Declarative form or wizard schema.
573
+ * @param initialData - Values that override schema defaults.
574
+ * @param enhancements - TypeScript-only callbacks (dynamic options, validators,
575
+ * submit handler, transform, field-change watcher).
576
+ */
577
+ /** Drives a declarative FormSchema as reactive state with conditional fields, wizard steps, and validation. */
578
+ function useFormBuilder(schema, initialData, enhancements) {
579
+ const currentSchema = shallowRef(schema);
580
+ const fields = reactive(flattenFields(schema));
581
+ const sections = reactive(collectSections(schema));
582
+ const rules = reactive(buildRules(fields, enhancements));
583
+ const form = useForm(buildInitialValues(fields, initialData), rules);
584
+ function isFieldVisible(name) {
585
+ const enhancement = enhancements?.fields?.[name];
586
+ if (enhancement?.visible) return enhancement.visible(form.data);
587
+ const field = fields.find((f) => f.name === name);
588
+ if (field?.condition) return evaluateCondition(field.condition, form.data);
589
+ return true;
590
+ }
591
+ function isSectionVisible(id) {
592
+ const section = sections.find((s) => s.id === id);
593
+ if (section?.condition) return evaluateCondition(section.condition, form.data);
594
+ return true;
595
+ }
596
+ function getResolvedFieldProps(field) {
597
+ const entry = getFieldRegistryEntry(field.type);
598
+ const formProps = form.getFieldProps(field.name);
599
+ const merged = {
600
+ ...entry.defaults,
601
+ ...field.props ?? {}
602
+ };
603
+ const enhancement = enhancements?.fields?.[field.name];
604
+ if (enhancement?.props) Object.assign(merged, enhancement.props(form.data));
605
+ const options = getFieldOptions(field.name);
606
+ if (options) merged.options = options;
607
+ if (entry.vModel) {
608
+ merged.modelValue = formProps.modelValue;
609
+ merged["onUpdate:modelValue"] = formProps["onUpdate:modelValue"];
610
+ }
611
+ merged.onBlur = formProps.onBlur;
612
+ if (formProps.error) merged.error = true;
613
+ if (field.placeholder) merged.placeholder = field.placeholder;
614
+ if (field.size) merged.size = field.size;
615
+ if (field.disabled) merged.disabled = true;
616
+ if (field.readonly) merged.readonly = true;
617
+ if (field.type === "radio" && !merged.name) merged.name = field.name;
618
+ return merged;
619
+ }
620
+ function getFieldOptions(name) {
621
+ const enhancement = enhancements?.fields?.[name];
622
+ if (enhancement?.options) return enhancement.options(form.data);
623
+ const field = fields.find((f) => f.name === name);
624
+ if (field?.props?.options) return field.props.options;
625
+ }
626
+ const currentStep = ref(0);
627
+ const isCurrentStepValid = computed(() => {
628
+ if (!currentSchema.value.steps) return form.isValid.value;
629
+ const step = currentSchema.value.steps[currentStep.value];
630
+ if (!step) return true;
631
+ for (const section of step.sections) {
632
+ if (!isSectionVisible(section.id)) continue;
633
+ for (const field of section.fields) {
634
+ if (!isFieldVisible(field.name)) continue;
635
+ if (!form.validateField(field.name)) return false;
636
+ }
637
+ }
638
+ return true;
639
+ });
640
+ function goNext() {
641
+ if (!currentSchema.value.steps) return false;
642
+ const step = currentSchema.value.steps[currentStep.value];
643
+ if (!step) return false;
644
+ let valid = true;
645
+ for (const section of step.sections) {
646
+ if (!isSectionVisible(section.id)) continue;
647
+ for (const field of section.fields) {
648
+ if (!isFieldVisible(field.name)) continue;
649
+ form.setFieldTouched(field.name, true);
650
+ if (!form.validateField(field.name)) valid = false;
651
+ }
652
+ }
653
+ if (!valid) return false;
654
+ if (currentStep.value < currentSchema.value.steps.length - 1) currentStep.value++;
655
+ return true;
656
+ }
657
+ function goBack() {
658
+ if (currentStep.value > 0) currentStep.value--;
659
+ }
660
+ function goToStep(index) {
661
+ if (!currentSchema.value.steps) return;
662
+ if (index >= 0 && index < currentSchema.value.steps.length) currentStep.value = index;
663
+ }
664
+ function validate() {
665
+ let allValid = true;
666
+ for (const field of fields) {
667
+ if (!isFieldVisible(field.name)) continue;
668
+ form.setFieldTouched(field.name, true);
669
+ if (!form.validateField(field.name)) allValid = false;
670
+ }
671
+ return allValid;
672
+ }
673
+ async function submit() {
674
+ if (!validate()) return;
675
+ let submitData = {};
676
+ for (const field of fields) if (isFieldVisible(field.name)) submitData[field.name] = form.data[field.name];
677
+ if (enhancements?.transform) submitData = enhancements.transform(submitData);
678
+ if (enhancements?.onSubmit) {
679
+ form.isSubmitting.value = true;
680
+ try {
681
+ await enhancements.onSubmit(submitData);
682
+ } finally {
683
+ form.isSubmitting.value = false;
684
+ }
685
+ }
686
+ }
687
+ function reset(values) {
688
+ form.reset(values);
689
+ currentStep.value = 0;
690
+ }
691
+ function updateSchema(nextSchema, nextData) {
692
+ currentSchema.value = nextSchema;
693
+ const nextFields = flattenFields(nextSchema);
694
+ replaceArray(fields, nextFields);
695
+ replaceArray(sections, collectSections(nextSchema));
696
+ replaceRecord(rules, buildRules(nextFields, enhancements));
697
+ form.replaceState(buildInitialValues(nextFields, nextData));
698
+ currentStep.value = 0;
699
+ }
700
+ if (enhancements?.onFieldChange) {
701
+ const callback = enhancements.onFieldChange;
702
+ watch(() => ({ ...form.data }), (newData, oldData) => {
703
+ if (!oldData) return;
704
+ for (const key of Object.keys(newData)) if (newData[key] !== oldData[key]) callback(key, newData[key], newData);
705
+ }, { deep: true });
706
+ }
707
+ return {
708
+ form,
709
+ rules,
710
+ isFieldVisible,
711
+ isSectionVisible,
712
+ fields,
713
+ getResolvedFieldProps,
714
+ getFieldOptions,
715
+ currentStep,
716
+ isCurrentStepValid,
717
+ goNext,
718
+ goBack,
719
+ goToStep,
720
+ validate,
721
+ reset,
722
+ submit,
723
+ updateSchema
724
+ };
725
+ }
726
+ //#endregion
727
+ export { usePlatformContext as i, evaluateCondition as n, useForm as r, useFormBuilder as t };
728
+
729
+ //# sourceMappingURL=useFormBuilder-COfYWDuC.js.map