@allhailai/formfoundry-core 1.2.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/dist/index.js ADDED
@@ -0,0 +1,2905 @@
1
+ import React4, { createContext, useContext, useId, useRef, useState, useCallback, useEffect, useMemo, useReducer, useImperativeHandle } from 'react';
2
+ import { jsx, Fragment, jsxs } from 'react/jsx-runtime';
3
+
4
+ // src/node/events.ts
5
+ function createEventEmitter() {
6
+ const listeners2 = /* @__PURE__ */ new Map();
7
+ const emitter = {
8
+ on(event, listener) {
9
+ if (!listeners2.has(event)) {
10
+ listeners2.set(event, /* @__PURE__ */ new Set());
11
+ }
12
+ listeners2.get(event).add(listener);
13
+ return () => {
14
+ listeners2.get(event)?.delete(listener);
15
+ };
16
+ },
17
+ once(event, listener) {
18
+ const wrapper = (payload) => {
19
+ listener(payload);
20
+ listeners2.get(event)?.delete(wrapper);
21
+ };
22
+ return emitter.on(event, wrapper);
23
+ },
24
+ emit(event, payload) {
25
+ const eventListeners = listeners2.get(event);
26
+ if (!eventListeners) return;
27
+ for (const listener of eventListeners) {
28
+ try {
29
+ listener(payload);
30
+ } catch (e) {
31
+ if (process.env.NODE_ENV !== "production") {
32
+ console.error("[FormFoundry] Event listener error:", e);
33
+ }
34
+ }
35
+ }
36
+ },
37
+ off(event) {
38
+ if (event) {
39
+ listeners2.delete(event);
40
+ } else {
41
+ listeners2.clear();
42
+ }
43
+ }
44
+ };
45
+ return emitter;
46
+ }
47
+
48
+ // src/node/globalRegistry.ts
49
+ var globalNodeMap = /* @__PURE__ */ new Map();
50
+ var listeners = /* @__PURE__ */ new Set();
51
+ function subscribeToRegistry(fn) {
52
+ listeners.add(fn);
53
+ return () => listeners.delete(fn);
54
+ }
55
+ function getNode(id) {
56
+ return globalNodeMap.get(id);
57
+ }
58
+ function registerNode(node) {
59
+ if (process.env.NODE_ENV !== "production" && globalNodeMap.has(node.id)) {
60
+ const existing = globalNodeMap.get(node.id);
61
+ if (existing !== node) {
62
+ console.warn(
63
+ `[FormFoundry] Node with id "${node.id}" is already registered globally. The previous node will be overwritten.`
64
+ );
65
+ }
66
+ }
67
+ globalNodeMap.set(node.id, node);
68
+ listeners.forEach((fn) => fn("registered", node.id, node));
69
+ }
70
+ function unregisterNode(id) {
71
+ const node = globalNodeMap.get(id);
72
+ globalNodeMap.delete(id);
73
+ if (node) {
74
+ listeners.forEach((fn) => fn("unregistered", id, node));
75
+ }
76
+ }
77
+ function clearGlobalRegistry() {
78
+ const entries = [...globalNodeMap.entries()];
79
+ globalNodeMap.clear();
80
+ for (const [id, node] of entries) {
81
+ listeners.forEach((fn) => fn("unregistered", id, node));
82
+ }
83
+ }
84
+ function clearRegistryListeners() {
85
+ listeners.clear();
86
+ }
87
+ function getRegisteredNodeIds() {
88
+ return Array.from(globalNodeMap.keys());
89
+ }
90
+
91
+ // src/node/hooks.ts
92
+ function createHookDispatcher() {
93
+ const middlewares = [];
94
+ return {
95
+ use(middleware) {
96
+ middlewares.push(middleware);
97
+ return () => {
98
+ const index = middlewares.indexOf(middleware);
99
+ if (index !== -1) {
100
+ middlewares.splice(index, 1);
101
+ }
102
+ };
103
+ },
104
+ dispatch(payload) {
105
+ if (middlewares.length === 0) return payload;
106
+ let index = 0;
107
+ const next = (current) => {
108
+ if (index >= middlewares.length) return current;
109
+ const middleware = middlewares[index++];
110
+ return middleware(current, next);
111
+ };
112
+ return next(payload);
113
+ },
114
+ // BUG-34 fix: async dispatch that awaits each middleware result
115
+ async dispatchAsync(payload) {
116
+ if (middlewares.length === 0) return payload;
117
+ let current = payload;
118
+ for (const middleware of middlewares) {
119
+ const result = middleware(current, (p) => p);
120
+ current = result instanceof Promise ? await result : result;
121
+ }
122
+ return current;
123
+ }
124
+ };
125
+ }
126
+ function createHooks() {
127
+ return {
128
+ init: createHookDispatcher(),
129
+ input: createHookDispatcher(),
130
+ commit: createHookDispatcher(),
131
+ prop: createHookDispatcher(),
132
+ message: createHookDispatcher(),
133
+ submit: createHookDispatcher()
134
+ };
135
+ }
136
+
137
+ // src/node/ledger.ts
138
+ function createLedger(store) {
139
+ const counters = [];
140
+ let unsubscribe = null;
141
+ function recalculate() {
142
+ for (const counter of counters) {
143
+ counter.count = 0;
144
+ for (const msg of store.messages.values()) {
145
+ if (counter.filter(msg)) {
146
+ counter.count++;
147
+ }
148
+ }
149
+ }
150
+ }
151
+ unsubscribe = store.on(() => {
152
+ recalculate();
153
+ });
154
+ const ledger = {
155
+ count(name, filter) {
156
+ const counter = { name, filter, count: 0 };
157
+ for (const msg of store.messages.values()) {
158
+ if (filter(msg)) {
159
+ counter.count++;
160
+ }
161
+ }
162
+ counters.push(counter);
163
+ return counter;
164
+ },
165
+ counters() {
166
+ return counters;
167
+ },
168
+ recalculate,
169
+ destroy() {
170
+ if (unsubscribe) {
171
+ unsubscribe();
172
+ unsubscribe = null;
173
+ }
174
+ counters.length = 0;
175
+ }
176
+ };
177
+ return ledger;
178
+ }
179
+
180
+ // src/node/messages.ts
181
+ function createMessage(partial) {
182
+ return {
183
+ type: "validation",
184
+ blocking: true,
185
+ visible: true,
186
+ meta: {},
187
+ ...partial
188
+ };
189
+ }
190
+ function createMessageStore() {
191
+ const messages = /* @__PURE__ */ new Map();
192
+ const listeners2 = /* @__PURE__ */ new Set();
193
+ let batchDepth = 0;
194
+ let batchDirty = false;
195
+ const notify = () => {
196
+ if (batchDepth > 0) {
197
+ batchDirty = true;
198
+ return;
199
+ }
200
+ for (const listener of listeners2) {
201
+ listener(store);
202
+ }
203
+ };
204
+ const store = {
205
+ get messages() {
206
+ return messages;
207
+ },
208
+ set(message) {
209
+ messages.set(message.key, message);
210
+ notify();
211
+ },
212
+ remove(key) {
213
+ if (messages.delete(key)) {
214
+ notify();
215
+ }
216
+ },
217
+ get(key) {
218
+ return messages.get(key);
219
+ },
220
+ has(key) {
221
+ return messages.has(key);
222
+ },
223
+ clear(type) {
224
+ if (type) {
225
+ for (const [key, msg] of messages) {
226
+ if (msg.type === type) {
227
+ messages.delete(key);
228
+ }
229
+ }
230
+ } else {
231
+ messages.clear();
232
+ }
233
+ notify();
234
+ },
235
+ filter(type) {
236
+ const result = [];
237
+ for (const msg of messages.values()) {
238
+ if (msg.type === type) {
239
+ result.push(msg);
240
+ }
241
+ }
242
+ return result;
243
+ },
244
+ visible() {
245
+ const result = [];
246
+ for (const msg of messages.values()) {
247
+ if (msg.visible) {
248
+ result.push(msg);
249
+ }
250
+ }
251
+ return result;
252
+ },
253
+ hasBlocking() {
254
+ for (const msg of messages.values()) {
255
+ if (msg.blocking) return true;
256
+ }
257
+ return false;
258
+ },
259
+ blocking() {
260
+ const result = [];
261
+ for (const msg of messages.values()) {
262
+ if (msg.blocking) {
263
+ result.push(msg);
264
+ }
265
+ }
266
+ return result;
267
+ },
268
+ on(listener) {
269
+ listeners2.add(listener);
270
+ return () => {
271
+ listeners2.delete(listener);
272
+ };
273
+ },
274
+ batch(fn) {
275
+ batchDepth++;
276
+ try {
277
+ fn();
278
+ } finally {
279
+ batchDepth--;
280
+ if (batchDepth === 0 && batchDirty) {
281
+ batchDirty = false;
282
+ notify();
283
+ }
284
+ }
285
+ }
286
+ };
287
+ return store;
288
+ }
289
+
290
+ // src/node/index.ts
291
+ var nodeCounter = 0;
292
+ function generateNodeId(type) {
293
+ return `${type}_${++nodeCounter}`;
294
+ }
295
+ function recomputeValue(node) {
296
+ if (node.type === "group") {
297
+ const obj = {};
298
+ for (const child of node.children) {
299
+ obj[child.name] = child.value;
300
+ }
301
+ node.value = obj;
302
+ node._value = obj;
303
+ } else if (node.type === "list") {
304
+ node.value = node.children.map((child) => child.value);
305
+ node._value = node.value;
306
+ }
307
+ if (node.parent) {
308
+ recomputeValue(node.parent);
309
+ }
310
+ }
311
+ async function hydrateChildren(node, initialValue, emitEvents = false) {
312
+ if (node.type === "group" && initialValue !== null && typeof initialValue === "object" && !Array.isArray(initialValue)) {
313
+ const values = initialValue;
314
+ for (const child of node.children) {
315
+ if (child.name in values) {
316
+ if (emitEvents) {
317
+ await child.input(values[child.name]);
318
+ } else {
319
+ child.value = values[child.name];
320
+ child._value = values[child.name];
321
+ if (child.config.defaultValue === void 0) {
322
+ child.config.defaultValue = values[child.name];
323
+ }
324
+ if (child.type !== "input") {
325
+ hydrateChildren(child, child.value);
326
+ }
327
+ }
328
+ }
329
+ }
330
+ } else if (node.type === "list" && Array.isArray(initialValue)) {
331
+ for (let i = 0; i < node.children.length && i < initialValue.length; i++) {
332
+ const child = node.children[i];
333
+ if (emitEvents) {
334
+ await child.input(initialValue[i]);
335
+ } else {
336
+ child.value = initialValue[i];
337
+ child._value = initialValue[i];
338
+ if (child.config.defaultValue === void 0) {
339
+ child.config.defaultValue = initialValue[i];
340
+ }
341
+ if (child.type !== "input") {
342
+ hydrateChildren(child, child.value);
343
+ }
344
+ }
345
+ }
346
+ }
347
+ }
348
+ function resolveConfig(node, key) {
349
+ if (key in node.config) {
350
+ return node.config[key];
351
+ }
352
+ if (node.parent) {
353
+ return resolveConfig(node.parent, key);
354
+ }
355
+ return void 0;
356
+ }
357
+ function createNode(options = {}) {
358
+ const {
359
+ type = "input",
360
+ name,
361
+ value,
362
+ id,
363
+ config = {},
364
+ children: initialChildren = [],
365
+ parent = null,
366
+ plugins = [],
367
+ props = {}
368
+ } = options;
369
+ const nodeId = id ?? generateNodeId(type);
370
+ const nodeName = name ?? nodeId;
371
+ const store = createMessageStore();
372
+ const hooks = createHooks();
373
+ const events = createEventEmitter();
374
+ const ledger = createLedger(store);
375
+ const pluginCleanups = [];
376
+ const pluginCleanupsByChild = /* @__PURE__ */ new Map();
377
+ let pendingCount = 0;
378
+ let settleResolvers = [];
379
+ function trackPending() {
380
+ pendingCount++;
381
+ }
382
+ function resolvePending() {
383
+ pendingCount--;
384
+ if (pendingCount <= 0) {
385
+ pendingCount = 0;
386
+ const resolvers = settleResolvers;
387
+ settleResolvers = [];
388
+ queueMicrotask(() => {
389
+ if (pendingCount <= 0) {
390
+ for (const resolve of resolvers) {
391
+ resolve();
392
+ }
393
+ } else {
394
+ settleResolvers = [...resolvers, ...settleResolvers];
395
+ }
396
+ });
397
+ }
398
+ }
399
+ const node = {
400
+ id: nodeId,
401
+ name: nodeName,
402
+ type,
403
+ // Tree
404
+ parent,
405
+ children: [],
406
+ // Value
407
+ value: value ?? (type === "group" ? {} : type === "list" ? [] : void 0),
408
+ _value: value ?? (type === "group" ? {} : type === "list" ? [] : void 0),
409
+ // State
410
+ touched: false,
411
+ dirty: false,
412
+ disabled: false,
413
+ // Derived (computed from message store)
414
+ get valid() {
415
+ return !store.hasBlocking();
416
+ },
417
+ get errors() {
418
+ return store.filter("validation").filter((m) => m.visible).map((m) => m.value);
419
+ },
420
+ // Subsystems
421
+ store,
422
+ hooks,
423
+ events,
424
+ ledger,
425
+ config,
426
+ props,
427
+ // Methods
428
+ async input(newValue) {
429
+ trackPending();
430
+ try {
431
+ const transformedValue = await hooks.input.dispatchAsync(newValue);
432
+ node._value = transformedValue;
433
+ const committedValue = await hooks.commit.dispatchAsync(transformedValue);
434
+ node.value = committedValue;
435
+ if (node.type !== "input" && committedValue != null) {
436
+ await hydrateChildren(node, committedValue, true);
437
+ }
438
+ if (node.parent) {
439
+ recomputeValue(node.parent);
440
+ }
441
+ events.emit("input", committedValue);
442
+ events.emit("commit", committedValue);
443
+ } finally {
444
+ resolvePending();
445
+ }
446
+ },
447
+ addChild(child, index) {
448
+ child.parent = node;
449
+ if (index !== void 0 && index >= 0 && index <= node.children.length) {
450
+ node.children.splice(index, 0, child);
451
+ } else {
452
+ node.children.push(child);
453
+ }
454
+ const parentPlugins = config.plugins ?? plugins;
455
+ const childCleanups = [];
456
+ for (const plugin of parentPlugins) {
457
+ const cleanup = plugin(child);
458
+ if (cleanup) childCleanups.push(cleanup);
459
+ }
460
+ if (childCleanups.length > 0) {
461
+ pluginCleanupsByChild.set(child.id, childCleanups);
462
+ }
463
+ const preAddValue = node.type === "group" ? node.value : null;
464
+ recomputeValue(node);
465
+ if (preAddValue && typeof preAddValue === "object" && child.name in preAddValue) {
466
+ const existingVal = preAddValue[child.name];
467
+ if (existingVal !== void 0) {
468
+ const shouldHydrate = child.type !== "input" || child.value === void 0;
469
+ if (shouldHydrate) {
470
+ child.value = existingVal;
471
+ child._value = existingVal;
472
+ if (child.type !== "input") {
473
+ hydrateChildren(child, existingVal);
474
+ }
475
+ recomputeValue(node);
476
+ }
477
+ }
478
+ }
479
+ },
480
+ removeChild(child) {
481
+ const index = node.children.indexOf(child);
482
+ if (index !== -1) {
483
+ node.children.splice(index, 1);
484
+ child.parent = null;
485
+ const childCleanups = pluginCleanupsByChild.get(child.id);
486
+ if (childCleanups) {
487
+ for (const cleanup of childCleanups) {
488
+ cleanup();
489
+ }
490
+ pluginCleanupsByChild.delete(child.id);
491
+ }
492
+ recomputeValue(node);
493
+ }
494
+ },
495
+ destroy() {
496
+ events.emit("destroying", void 0);
497
+ for (const cleanup of pluginCleanups) {
498
+ cleanup();
499
+ }
500
+ if (node.parent) {
501
+ node.parent.removeChild(node);
502
+ }
503
+ const childrenToDestroy = [...node.children];
504
+ node.children = [];
505
+ for (const child of childrenToDestroy) {
506
+ child.destroy();
507
+ child.parent = null;
508
+ }
509
+ for (const cleanups of pluginCleanupsByChild.values()) {
510
+ for (const cleanup of cleanups) {
511
+ cleanup();
512
+ }
513
+ }
514
+ pluginCleanupsByChild.clear();
515
+ ledger.destroy();
516
+ events.off();
517
+ unregisterNode(node.id);
518
+ },
519
+ walk(fn) {
520
+ fn(node);
521
+ for (const child of node.children) {
522
+ child.walk(fn);
523
+ }
524
+ },
525
+ at(path) {
526
+ const segments = path.split(".");
527
+ let current = node;
528
+ for (const segment of segments) {
529
+ if (!current || current.type === "input") return void 0;
530
+ current = current.children.find((c) => c.name === segment);
531
+ }
532
+ return current;
533
+ },
534
+ reset(resetValue) {
535
+ node.touched = false;
536
+ node.dirty = false;
537
+ node.store.clear("validation");
538
+ node.store.clear("error");
539
+ if (node.type === "group" && node.children.length > 0) {
540
+ const obj = typeof resetValue === "object" && resetValue !== null && !Array.isArray(resetValue) ? resetValue : void 0;
541
+ for (const child of node.children) {
542
+ child.reset(obj?.[child.name]);
543
+ }
544
+ recomputeValue(node);
545
+ } else if (node.type === "list" && node.children.length > 0) {
546
+ const arr = Array.isArray(resetValue) ? resetValue : void 0;
547
+ for (let i = 0; i < node.children.length; i++) {
548
+ node.children[i].reset(arr?.[i]);
549
+ }
550
+ recomputeValue(node);
551
+ } else {
552
+ const defaultVal = resetValue ?? node.config.defaultValue ?? (node.type === "group" ? {} : node.type === "list" ? [] : void 0);
553
+ node.value = defaultVal;
554
+ node._value = defaultVal;
555
+ }
556
+ if (node.parent) {
557
+ recomputeValue(node.parent);
558
+ }
559
+ events.emit("reset", node.value);
560
+ },
561
+ /**
562
+ * Wait for all currently in-flight `input()` calls to complete.
563
+ *
564
+ * **Design note (G1):** If no `input()` calls are pending, `settle()`
565
+ * resolves immediately. This is intentional — it means "wait for
566
+ * current async work", not "wait for the next input". If you need to
567
+ * wait for a future input, subscribe to the `'commit'` event instead.
568
+ *
569
+ * Uses a microtask-deferred resolver (G2 fix) so that re-entrant
570
+ * `input()` calls triggered during resolver callbacks are captured
571
+ * before settlement completes.
572
+ */
573
+ async settle() {
574
+ if (pendingCount <= 0) return;
575
+ return new Promise((resolve) => {
576
+ settleResolvers.push(resolve);
577
+ });
578
+ },
579
+ async submit() {
580
+ }
581
+ };
582
+ for (const child of initialChildren) {
583
+ node.addChild(child);
584
+ }
585
+ if (value !== void 0 && type !== "input") {
586
+ hydrateChildren(node, value);
587
+ } else if (type !== "input") {
588
+ recomputeValue(node);
589
+ }
590
+ const allPlugins = [...config.plugins ?? [], ...plugins];
591
+ for (const plugin of allPlugins) {
592
+ const cleanup = plugin(node);
593
+ if (cleanup) pluginCleanups.push(cleanup);
594
+ }
595
+ hooks.init.dispatch(node);
596
+ registerNode(node);
597
+ events.emit("created", node);
598
+ return node;
599
+ }
600
+
601
+ // src/i18n/index.ts
602
+ function interpolate(template, ctx) {
603
+ return template.replace(/\{label\}/g, ctx.label).replace(/\{name\}/g, ctx.name).replace(/\{args\.(\d+)\}/g, (_, i) => String(ctx.args[Number(i)] ?? "")).replace(/\{(\d+)\}/g, (_, i) => String(ctx.args[Number(i)] ?? ""));
604
+ }
605
+ function resolveLocaleMessage(locale, ruleName, ctx) {
606
+ const msg = locale.messages[ruleName];
607
+ if (!msg) return `${ctx.label} is invalid.`;
608
+ if (typeof msg === "function") {
609
+ return msg(ctx);
610
+ }
611
+ return interpolate(msg, ctx);
612
+ }
613
+ var localeRegistry = /* @__PURE__ */ new Map();
614
+ var activeLocaleCode = "en";
615
+ function registerLocale(locale) {
616
+ localeRegistry.set(locale.code, locale);
617
+ }
618
+ function setActiveLocale(code) {
619
+ if (!localeRegistry.has(code)) {
620
+ if (process.env.NODE_ENV !== "production") {
621
+ console.warn(
622
+ `[FormFoundry i18n] Locale "${code}" is not registered. Available: ${Array.from(localeRegistry.keys()).join(", ") || "(none)"}`
623
+ );
624
+ }
625
+ }
626
+ activeLocaleCode = code;
627
+ }
628
+ function getActiveLocale() {
629
+ return localeRegistry.get(activeLocaleCode);
630
+ }
631
+ function getLocale(code) {
632
+ return localeRegistry.get(code);
633
+ }
634
+ function getRegisteredLocales() {
635
+ return Array.from(localeRegistry.keys());
636
+ }
637
+
638
+ // src/validation/messages.ts
639
+ var defaultMessages = {
640
+ required: "This field is required.",
641
+ email: "Please enter a valid email address.",
642
+ url: "Please enter a valid URL.",
643
+ min: (args) => `Must be at least ${args[0] ?? "?"}.`,
644
+ max: (args) => `Must be at most ${args[0] ?? "?"}.`,
645
+ minLength: (args) => `Must be at least ${args[0] ?? "?"} characters.`,
646
+ maxLength: (args) => `Must be at most ${args[0] ?? "?"} characters.`,
647
+ matches: "This field does not match the required pattern.",
648
+ confirm: "This field does not match.",
649
+ alpha: "This field must contain only alphabetical characters.",
650
+ alphanumeric: "This field must contain only alphanumeric characters.",
651
+ date: "Please enter a valid date.",
652
+ between: (args) => `Must be between ${args[0] ?? "?"} and ${args[1] ?? "?"}.`,
653
+ number: "This field must be a number.",
654
+ accepted: "This field must be accepted.",
655
+ alpha_spaces: "This field must contain only letters and spaces.",
656
+ contains_alpha: "This field must contain at least one letter.",
657
+ contains_alphanumeric: "This field must contain at least one letter or number.",
658
+ contains_alpha_spaces: "This field must contain at least one letter or space.",
659
+ contains_lowercase: "This field must contain at least one lowercase letter.",
660
+ contains_uppercase: "This field must contain at least one uppercase letter.",
661
+ contains_numeric: "This field must contain at least one number.",
662
+ contains_symbol: "This field must contain at least one symbol.",
663
+ date_after: (args) => `Must be after ${args[0] ?? "today"}.`,
664
+ date_before: (args) => `Must be before ${args[0] ?? "today"}.`,
665
+ date_between: (args) => `Must be between ${args[0] ?? "?"} and ${args[1] ?? "?"}.`,
666
+ date_format: (args) => `Must match the format ${args[0] ?? "YYYY-MM-DD"}.`,
667
+ ends_with: (args) => `Must end with ${args.join(", ") || "?"}.`,
668
+ is: (args) => `Must be one of: ${args.join(", ") || "?"}.`,
669
+ length: (args) => args[1] ? `Must be between ${args[0]} and ${args[1]} characters.` : `Must be exactly ${args[0] ?? "?"} characters.`,
670
+ lowercase: "This field must be all lowercase.",
671
+ not: (args) => `Must not be: ${args.join(", ") || "?"}.`,
672
+ require_one: "At least one of these fields is required.",
673
+ starts_with: (args) => `Must start with ${args.join(", ") || "?"}.`,
674
+ symbol: "This field must contain only symbols.",
675
+ uppercase: "This field must be all uppercase."
676
+ };
677
+ function resolveMessage(ruleName, args, customMessages, label, name) {
678
+ const custom = customMessages?.[ruleName];
679
+ if (custom) {
680
+ return typeof custom === "function" ? custom(args) : custom;
681
+ }
682
+ const activeLocale = getActiveLocale();
683
+ if (activeLocale) {
684
+ const ctx = {
685
+ label: label ?? name ?? "This field",
686
+ name: name ?? "",
687
+ args,
688
+ values: {}
689
+ };
690
+ const localeMsg = activeLocale.messages[ruleName];
691
+ if (localeMsg) {
692
+ return resolveLocaleMessage(activeLocale, ruleName, ctx);
693
+ }
694
+ }
695
+ const source = defaultMessages[ruleName];
696
+ if (typeof source === "function") return source(args);
697
+ if (typeof source === "string") return source;
698
+ return "This field is invalid.";
699
+ }
700
+
701
+ // src/validation/parseRules.ts
702
+ function parseRules(spec) {
703
+ if (typeof spec === "string") {
704
+ return parseStringRules(spec);
705
+ }
706
+ if (Array.isArray(spec)) {
707
+ return parseArrayRules(spec);
708
+ }
709
+ return [];
710
+ }
711
+ function parseStringRules(spec) {
712
+ if (!spec.trim()) return [];
713
+ const rules = spec.split("|");
714
+ return rules.map(parseStringRule).filter(Boolean);
715
+ }
716
+ function parseStringRule(rule) {
717
+ let remaining = rule.trim();
718
+ if (!remaining) return null;
719
+ const hints = {};
720
+ while (remaining.length > 0) {
721
+ const debounceMatch = remaining.match(/^\((\d+)\)/);
722
+ if (debounceMatch) {
723
+ hints.debounce = parseInt(debounceMatch[1], 10);
724
+ remaining = remaining.slice(debounceMatch[0].length);
725
+ continue;
726
+ }
727
+ if (remaining.startsWith("+")) {
728
+ hints.empty = true;
729
+ remaining = remaining.slice(1);
730
+ continue;
731
+ }
732
+ if (remaining.startsWith("*")) {
733
+ hints.force = true;
734
+ remaining = remaining.slice(1);
735
+ continue;
736
+ }
737
+ if (remaining.startsWith("?")) {
738
+ hints.optional = true;
739
+ remaining = remaining.slice(1);
740
+ continue;
741
+ }
742
+ break;
743
+ }
744
+ const colonIndex = remaining.indexOf(":");
745
+ if (colonIndex === -1) {
746
+ return { name: remaining, args: [], hints };
747
+ }
748
+ const name = remaining.slice(0, colonIndex);
749
+ const argsString = remaining.slice(colonIndex + 1);
750
+ const args = argsString.split(",").map((a) => a.trim());
751
+ return { name, args, hints };
752
+ }
753
+ function parseArrayRules(spec) {
754
+ const rules = [];
755
+ for (const item of spec) {
756
+ if (typeof item === "string") {
757
+ rules.push(...parseStringRules(item));
758
+ } else if (typeof item === "function") {
759
+ rules.push({
760
+ name: item.name || `custom_${rules.length}`,
761
+ args: [],
762
+ hints: {},
763
+ handler: item
764
+ });
765
+ } else if (Array.isArray(item)) {
766
+ const [name, ...args] = item;
767
+ if (typeof name === "string") {
768
+ rules.push({ name, args, hints: {} });
769
+ }
770
+ }
771
+ }
772
+ return rules;
773
+ }
774
+
775
+ // src/validation/rules.ts
776
+ function isEmpty(value) {
777
+ if (value === null || value === void 0 || value === "") return true;
778
+ if (Array.isArray(value) && value.length === 0) return true;
779
+ return false;
780
+ }
781
+ function toNumber(value) {
782
+ if (typeof value === "number") return value;
783
+ if (typeof value === "string") {
784
+ const parsed = Number(value);
785
+ return isNaN(parsed) ? null : parsed;
786
+ }
787
+ return null;
788
+ }
789
+ function toString(value) {
790
+ if (typeof value === "string") return value;
791
+ if (value === null || value === void 0) return "";
792
+ return String(value);
793
+ }
794
+ function getMessage(ruleName, args) {
795
+ const msgFactory = defaultMessages[ruleName];
796
+ if (typeof msgFactory === "function") {
797
+ return msgFactory(args.map(String));
798
+ }
799
+ if (typeof msgFactory === "string") {
800
+ return msgFactory;
801
+ }
802
+ return `This field is invalid.`;
803
+ }
804
+ var required = (value, args) => {
805
+ if (isEmpty(value)) return getMessage("required", args);
806
+ if (typeof value === "boolean" && !value) return getMessage("required", args);
807
+ return null;
808
+ };
809
+ var email = (value, args) => {
810
+ const str = toString(value);
811
+ if (!str) return null;
812
+ const re = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
813
+ return re.test(str) ? null : getMessage("email", args);
814
+ };
815
+ var url = (value, args) => {
816
+ const str = toString(value);
817
+ if (!str) return null;
818
+ try {
819
+ new URL(str);
820
+ return null;
821
+ } catch {
822
+ return getMessage("url", args);
823
+ }
824
+ };
825
+ var min = (value, args) => {
826
+ const num = toNumber(value);
827
+ const threshold = toNumber(args[0]);
828
+ if (num === null || threshold === null) return null;
829
+ return num >= threshold ? null : getMessage("min", args);
830
+ };
831
+ var max = (value, args) => {
832
+ const num = toNumber(value);
833
+ const threshold = toNumber(args[0]);
834
+ if (num === null || threshold === null) return null;
835
+ return num <= threshold ? null : getMessage("max", args);
836
+ };
837
+ var minLength = (value, args) => {
838
+ const str = toString(value);
839
+ if (!str) return null;
840
+ const threshold = toNumber(args[0]);
841
+ if (threshold === null) return null;
842
+ return str.length >= threshold ? null : getMessage("minLength", args);
843
+ };
844
+ var maxLength = (value, args) => {
845
+ const str = toString(value);
846
+ if (!str) return null;
847
+ const threshold = toNumber(args[0]);
848
+ if (threshold === null) return null;
849
+ return str.length <= threshold ? null : getMessage("maxLength", args);
850
+ };
851
+ var matches = (value, args) => {
852
+ const str = toString(value);
853
+ if (!str) return null;
854
+ const pattern = args[0];
855
+ if (pattern instanceof RegExp) {
856
+ return pattern.test(str) ? null : getMessage("matches", args);
857
+ }
858
+ if (typeof pattern === "string") {
859
+ const re = new RegExp(pattern);
860
+ return re.test(str) ? null : getMessage("matches", args);
861
+ }
862
+ return null;
863
+ };
864
+ var confirm = (value, args, context) => {
865
+ const siblingName = typeof args[0] === "string" ? args[0] : `${context.name}_confirm`;
866
+ const siblingValue = context.values[siblingName];
867
+ return value === siblingValue ? null : getMessage("confirm", args);
868
+ };
869
+ var alpha = (value, args) => {
870
+ const str = toString(value);
871
+ if (!str) return null;
872
+ return /^[a-zA-Z]+$/.test(str) ? null : getMessage("alpha", args);
873
+ };
874
+ var alphanumeric = (value, args) => {
875
+ const str = toString(value);
876
+ if (!str) return null;
877
+ return /^[a-zA-Z0-9]+$/.test(str) ? null : getMessage("alphanumeric", args);
878
+ };
879
+ var date = (value, args) => {
880
+ const str = toString(value);
881
+ if (!str) return null;
882
+ const d = new Date(str);
883
+ return isNaN(d.getTime()) ? getMessage("date", args) : null;
884
+ };
885
+ var between = (value, args) => {
886
+ const num = toNumber(value);
887
+ const lo = toNumber(args[0]);
888
+ const hi = toNumber(args[1]);
889
+ if (num === null || lo === null || hi === null) return null;
890
+ return num >= lo && num <= hi ? null : getMessage("between", args);
891
+ };
892
+ var number = (value, args) => {
893
+ const str = toString(value);
894
+ if (!str) return null;
895
+ return isNaN(Number(str)) ? getMessage("number", args) : null;
896
+ };
897
+ var accepted = (value, args) => {
898
+ const v = typeof value === "string" ? value.toLowerCase() : value;
899
+ if (v === true || v === "yes" || v === "on" || v === "1" || v === 1) return null;
900
+ return getMessage("accepted", args);
901
+ };
902
+ var alpha_spaces = (value, args) => {
903
+ const str = toString(value);
904
+ if (!str) return null;
905
+ return /^[a-zA-Z\s]+$/.test(str) ? null : getMessage("alpha_spaces", args);
906
+ };
907
+ var contains_alpha = (value, args) => {
908
+ const str = toString(value);
909
+ if (!str) return null;
910
+ return /[a-zA-Z]/.test(str) ? null : getMessage("contains_alpha", args);
911
+ };
912
+ var contains_alphanumeric = (value, args) => {
913
+ const str = toString(value);
914
+ if (!str) return null;
915
+ return /[a-zA-Z0-9]/.test(str) ? null : getMessage("contains_alphanumeric", args);
916
+ };
917
+ var contains_alpha_spaces = (value, args) => {
918
+ const str = toString(value);
919
+ if (!str) return null;
920
+ return /[a-zA-Z\s]/.test(str) ? null : getMessage("contains_alpha_spaces", args);
921
+ };
922
+ var contains_lowercase = (value, args) => {
923
+ const str = toString(value);
924
+ if (!str) return null;
925
+ return /[a-z]/.test(str) ? null : getMessage("contains_lowercase", args);
926
+ };
927
+ var contains_uppercase = (value, args) => {
928
+ const str = toString(value);
929
+ if (!str) return null;
930
+ return /[A-Z]/.test(str) ? null : getMessage("contains_uppercase", args);
931
+ };
932
+ var contains_numeric = (value, args) => {
933
+ const str = toString(value);
934
+ if (!str) return null;
935
+ return /\d/.test(str) ? null : getMessage("contains_numeric", args);
936
+ };
937
+ var contains_symbol = (value, args) => {
938
+ const str = toString(value);
939
+ if (!str) return null;
940
+ return /[^a-zA-Z0-9\s]/.test(str) ? null : getMessage("contains_symbol", args);
941
+ };
942
+ var date_after = (value, args) => {
943
+ const str = toString(value);
944
+ if (!str) return null;
945
+ const d = new Date(str);
946
+ if (isNaN(d.getTime())) return getMessage("date_after", args);
947
+ const after = args[0] ? new Date(String(args[0])) : /* @__PURE__ */ new Date();
948
+ if (isNaN(after.getTime())) return null;
949
+ return d > after ? null : getMessage("date_after", args);
950
+ };
951
+ var date_before = (value, args) => {
952
+ const str = toString(value);
953
+ if (!str) return null;
954
+ const d = new Date(str);
955
+ if (isNaN(d.getTime())) return getMessage("date_before", args);
956
+ const before = args[0] ? new Date(String(args[0])) : /* @__PURE__ */ new Date();
957
+ if (isNaN(before.getTime())) return null;
958
+ return d < before ? null : getMessage("date_before", args);
959
+ };
960
+ var date_between = (value, args) => {
961
+ const str = toString(value);
962
+ if (!str) return null;
963
+ const d = new Date(str);
964
+ if (isNaN(d.getTime())) return getMessage("date_between", args);
965
+ const lo = args[0] ? new Date(String(args[0])) : null;
966
+ const hi = args[1] ? new Date(String(args[1])) : null;
967
+ if (!lo || !hi || isNaN(lo.getTime()) || isNaN(hi.getTime())) return null;
968
+ return d >= lo && d <= hi ? null : getMessage("date_between", args);
969
+ };
970
+ var date_format = (value, args) => {
971
+ const str = toString(value);
972
+ if (!str) return null;
973
+ const format = typeof args[0] === "string" ? args[0] : "YYYY-MM-DD";
974
+ const regex = format.replace(/YYYY/g, "\\d{4}").replace(/MM/g, "\\d{2}").replace(/DD/g, "\\d{2}").replace(/HH/g, "\\d{2}").replace(/mm/g, "\\d{2}").replace(/ss/g, "\\d{2}");
975
+ return new RegExp(`^${regex}$`).test(str) ? null : getMessage("date_format", args);
976
+ };
977
+ var ends_with = (value, args) => {
978
+ const str = toString(value);
979
+ if (!str) return null;
980
+ const suffixes = args.map(String);
981
+ return suffixes.some((s) => str.endsWith(s)) ? null : getMessage("ends_with", args);
982
+ };
983
+ var is = (value, args) => {
984
+ const str = toString(value);
985
+ return args.some((arg) => String(arg) === str) ? null : getMessage("is", args);
986
+ };
987
+ var length = (value, args) => {
988
+ const str = toString(value);
989
+ if (!str) return null;
990
+ const len = str.length;
991
+ const lo = toNumber(args[0]);
992
+ const hi = args[1] !== void 0 ? toNumber(args[1]) : lo;
993
+ if (lo === null) return null;
994
+ if (hi === null) return len === lo ? null : getMessage("length", args);
995
+ return len >= lo && len <= hi ? null : getMessage("length", args);
996
+ };
997
+ var lowercase = (value, args) => {
998
+ const str = toString(value);
999
+ if (!str) return null;
1000
+ return str === str.toLowerCase() ? null : getMessage("lowercase", args);
1001
+ };
1002
+ var not = (value, args) => {
1003
+ const str = toString(value);
1004
+ return args.some((arg) => String(arg) === str) ? getMessage("not", args) : null;
1005
+ };
1006
+ var require_one = (value, args, context) => {
1007
+ const fieldNames = args.map(String);
1008
+ const hasSome = fieldNames.some((name) => {
1009
+ const v = context.values[name];
1010
+ return v !== null && v !== void 0 && v !== "";
1011
+ });
1012
+ return hasSome ? null : getMessage("require_one", args);
1013
+ };
1014
+ var starts_with = (value, args) => {
1015
+ const str = toString(value);
1016
+ if (!str) return null;
1017
+ const prefixes = args.map(String);
1018
+ return prefixes.some((p) => str.startsWith(p)) ? null : getMessage("starts_with", args);
1019
+ };
1020
+ var symbol = (value, args) => {
1021
+ const str = toString(value);
1022
+ if (!str) return null;
1023
+ return /^[^a-zA-Z0-9\s]+$/.test(str) ? null : getMessage("symbol", args);
1024
+ };
1025
+ var uppercase = (value, args) => {
1026
+ const str = toString(value);
1027
+ if (!str) return null;
1028
+ return str === str.toUpperCase() ? null : getMessage("uppercase", args);
1029
+ };
1030
+ var builtInRules = {
1031
+ required,
1032
+ email,
1033
+ url,
1034
+ min,
1035
+ max,
1036
+ minLength,
1037
+ maxLength,
1038
+ matches,
1039
+ confirm,
1040
+ alpha,
1041
+ alphanumeric,
1042
+ date,
1043
+ between,
1044
+ number,
1045
+ accepted,
1046
+ alpha_spaces,
1047
+ contains_alpha,
1048
+ contains_alphanumeric,
1049
+ contains_alpha_spaces,
1050
+ contains_lowercase,
1051
+ contains_uppercase,
1052
+ contains_numeric,
1053
+ contains_symbol,
1054
+ date_after,
1055
+ date_before,
1056
+ date_between,
1057
+ date_format,
1058
+ ends_with,
1059
+ is,
1060
+ length,
1061
+ lowercase,
1062
+ not,
1063
+ require_one,
1064
+ starts_with,
1065
+ symbol,
1066
+ uppercase
1067
+ };
1068
+
1069
+ // src/validation/registry.ts
1070
+ var customRules = /* @__PURE__ */ new Map();
1071
+ function registerRule(name, rule) {
1072
+ customRules.set(name, rule);
1073
+ }
1074
+ function getRule(name) {
1075
+ return customRules.get(name) ?? builtInRules[name];
1076
+ }
1077
+ function hasRule(name) {
1078
+ return customRules.has(name) || name in builtInRules;
1079
+ }
1080
+ function unregisterRule(name) {
1081
+ customRules.delete(name);
1082
+ }
1083
+
1084
+ // src/validation/zodAdapter.ts
1085
+ function validateWithZod(schema, value) {
1086
+ const result = schema.safeParse(value);
1087
+ if (result.success) return [];
1088
+ return result.error.issues.map((issue) => {
1089
+ const path = issue.path.join(".");
1090
+ return createMessage({
1091
+ key: `zod_${path || "root"}_${issue.code}`,
1092
+ type: "validation",
1093
+ value: issue.message,
1094
+ blocking: true,
1095
+ visible: true,
1096
+ meta: {
1097
+ zodCode: issue.code,
1098
+ path: issue.path
1099
+ }
1100
+ });
1101
+ });
1102
+ }
1103
+ function validateFormWithZod(schema, values) {
1104
+ const result = schema.safeParse(values);
1105
+ if (result.success) return {};
1106
+ const errors = {};
1107
+ for (const issue of result.error.issues) {
1108
+ const fieldName = issue.path[0];
1109
+ if (typeof fieldName === "string") {
1110
+ if (!errors[fieldName]) {
1111
+ errors[fieldName] = [];
1112
+ }
1113
+ errors[fieldName].push(issue.message);
1114
+ }
1115
+ }
1116
+ return errors;
1117
+ }
1118
+
1119
+ // src/validation/index.ts
1120
+ async function runValidation(spec, value, context, customMessages) {
1121
+ const rules = parseRules(spec);
1122
+ const messages = [];
1123
+ const valueIsEmpty = isEmpty2(value);
1124
+ for (const rule of rules) {
1125
+ if (valueIsEmpty && !rule.hints.empty && rule.name !== "required") {
1126
+ continue;
1127
+ }
1128
+ const error = await executeRule(rule, value, context, customMessages);
1129
+ if (error) {
1130
+ messages.push(error);
1131
+ if (!rule.hints.force) {
1132
+ break;
1133
+ }
1134
+ }
1135
+ }
1136
+ return messages;
1137
+ }
1138
+ async function executeRule(rule, value, context, customMessages) {
1139
+ const handler = rule.handler ?? getRule(rule.name);
1140
+ if (!handler) {
1141
+ console.warn(`[FormFoundry] Unknown validation rule: "${rule.name}"`);
1142
+ return null;
1143
+ }
1144
+ const errorString = await handler(value, rule.args, context);
1145
+ if (errorString === null) return null;
1146
+ const source = customMessages?.[rule.name];
1147
+ let messageValue = errorString;
1148
+ if (source) {
1149
+ messageValue = typeof source === "function" ? source(rule.args.map(String)) : source;
1150
+ }
1151
+ return createMessage({
1152
+ key: `rule_${rule.name}`,
1153
+ type: "validation",
1154
+ value: messageValue,
1155
+ blocking: !rule.hints.optional,
1156
+ visible: true,
1157
+ meta: {
1158
+ ruleName: rule.name,
1159
+ ruleArgs: rule.args
1160
+ }
1161
+ });
1162
+ }
1163
+ function isEmpty2(value) {
1164
+ if (value === null || value === void 0 || value === "") return true;
1165
+ if (Array.isArray(value) && value.length === 0) return true;
1166
+ return false;
1167
+ }
1168
+ var defaultMeta = {
1169
+ isSubmitting: false,
1170
+ isValid: true,
1171
+ errors: {},
1172
+ register: () => {
1173
+ if (process.env.NODE_ENV !== "production") {
1174
+ console.warn("[FormFoundry] register called outside of a <Form> component");
1175
+ }
1176
+ },
1177
+ unregister: () => {
1178
+ },
1179
+ getNode: () => void 0,
1180
+ triggerValidation: async () => true,
1181
+ showErrors: false,
1182
+ config: {},
1183
+ formNode: null,
1184
+ reset: () => {
1185
+ },
1186
+ submitCount: 0,
1187
+ setExternalErrors: () => {
1188
+ },
1189
+ clearExternalErrors: () => {
1190
+ },
1191
+ isDisabled: false
1192
+ };
1193
+ var FormMetaContext = createContext(defaultMeta);
1194
+ function useFormMeta() {
1195
+ return useContext(FormMetaContext);
1196
+ }
1197
+ var defaultValues = {
1198
+ values: {},
1199
+ getValue: () => void 0,
1200
+ setValue: () => {
1201
+ if (process.env.NODE_ENV !== "production") {
1202
+ console.warn("[FormFoundry] setValue called outside of a <Form> component");
1203
+ }
1204
+ }
1205
+ };
1206
+ var FormValuesContext = createContext(defaultValues);
1207
+ function useFormValues() {
1208
+ return useContext(FormValuesContext);
1209
+ }
1210
+
1211
+ // src/context/useFormContext.ts
1212
+ function useFormContext() {
1213
+ return {
1214
+ values: useFormValues(),
1215
+ meta: useFormMeta()
1216
+ };
1217
+ }
1218
+ function useFormFoundryField(options) {
1219
+ const {
1220
+ name,
1221
+ validation,
1222
+ validationBehavior: fieldBehavior,
1223
+ defaultValue,
1224
+ plugins,
1225
+ disabled = false,
1226
+ label,
1227
+ inputType,
1228
+ selectOptions,
1229
+ dependsOn
1230
+ } = options;
1231
+ const reactId = useId();
1232
+ const fieldId = `formfoundry-${name}-${reactId}`;
1233
+ const { values, getValue, setValue } = useFormValues();
1234
+ const meta = useFormMeta();
1235
+ const validationBehavior = fieldBehavior ?? meta.config.validationBehavior ?? "blur";
1236
+ const nodeRef = useRef(null);
1237
+ if (!nodeRef.current) {
1238
+ nodeRef.current = createNode({
1239
+ type: "input",
1240
+ name,
1241
+ id: fieldId,
1242
+ value: getValue(name) ?? defaultValue,
1243
+ config: {
1244
+ validation,
1245
+ validationBehavior,
1246
+ defaultValue,
1247
+ plugins,
1248
+ // Store label and inputType for print renderer access
1249
+ label,
1250
+ inputType: inputType ?? "text",
1251
+ options: selectOptions
1252
+ }
1253
+ });
1254
+ }
1255
+ const node = nodeRef.current;
1256
+ if (label !== void 0) {
1257
+ node.config.label = label;
1258
+ }
1259
+ if (inputType !== void 0) {
1260
+ node.config.inputType = inputType;
1261
+ }
1262
+ if (selectOptions !== void 0) {
1263
+ node.config.options = selectOptions;
1264
+ }
1265
+ if (node.name !== name) {
1266
+ const oldName = node.name;
1267
+ node.name = name;
1268
+ meta.unregister(oldName);
1269
+ meta.register(name, node);
1270
+ }
1271
+ const [touched, setTouched] = useState(false);
1272
+ const [dirty, setDirty] = useState(false);
1273
+ const [errorStrings, setErrorStrings] = useState([]);
1274
+ const hasBlurredRef = useRef(false);
1275
+ const debounceTimerRef = useRef(null);
1276
+ const valuesRef = useRef(values);
1277
+ valuesRef.current = values;
1278
+ const getValueRef = useRef(getValue);
1279
+ getValueRef.current = getValue;
1280
+ const validationGenRef = useRef(0);
1281
+ const defaultValueRef = useRef(defaultValue);
1282
+ defaultValueRef.current = defaultValue;
1283
+ const defaultValueJsonRef = useRef(JSON.stringify(defaultValue));
1284
+ defaultValueJsonRef.current = JSON.stringify(defaultValue);
1285
+ const validate = useCallback(async () => {
1286
+ const gen = ++validationGenRef.current;
1287
+ if (!validation) {
1288
+ setErrorStrings([]);
1289
+ node.store.clear("validation");
1290
+ return;
1291
+ }
1292
+ if (isZodSchema(validation)) {
1293
+ const msgs2 = validateWithZod(validation, getValueRef.current(name));
1294
+ if (gen !== validationGenRef.current) return;
1295
+ node.store.batch(() => {
1296
+ node.store.clear("validation");
1297
+ for (const msg of msgs2) {
1298
+ node.store.set(msg);
1299
+ }
1300
+ });
1301
+ setErrorStrings(msgs2.filter((m) => m.visible).map((m) => m.value));
1302
+ return;
1303
+ }
1304
+ const msgs = await runValidation(
1305
+ validation,
1306
+ getValueRef.current(name),
1307
+ {
1308
+ values: valuesRef.current,
1309
+ name,
1310
+ label
1311
+ },
1312
+ meta.config.messages
1313
+ );
1314
+ if (gen !== validationGenRef.current) return;
1315
+ node.store.batch(() => {
1316
+ node.store.clear("validation");
1317
+ for (const msg of msgs) {
1318
+ node.store.set(msg);
1319
+ }
1320
+ });
1321
+ setErrorStrings(msgs.filter((m) => m.visible).map((m) => m.value));
1322
+ }, [validation, name, label, meta.config.messages, node]);
1323
+ useEffect(() => {
1324
+ meta.register(name, node);
1325
+ return () => {
1326
+ meta.unregister(name);
1327
+ };
1328
+ }, [name, node, meta]);
1329
+ useEffect(() => {
1330
+ meta.updateValidateCallback?.(name, validate);
1331
+ }, [name, validate, meta]);
1332
+ const onChange = useCallback(
1333
+ (newValue) => {
1334
+ setValue(name, newValue);
1335
+ void node.input(newValue).then(() => {
1336
+ const committedValue = node.value;
1337
+ if (committedValue !== newValue) {
1338
+ setValue(name, committedValue);
1339
+ }
1340
+ const def = defaultValueRef.current ?? void 0;
1341
+ let isDirty;
1342
+ if (def !== null && typeof def === "object") {
1343
+ isDirty = JSON.stringify(committedValue) !== defaultValueJsonRef.current;
1344
+ } else {
1345
+ isDirty = committedValue !== def;
1346
+ }
1347
+ setDirty(isDirty);
1348
+ node.dirty = isDirty;
1349
+ });
1350
+ if (validationBehavior === "live" || validationBehavior === "blur" && hasBlurredRef.current || validationBehavior === "dirty" && newValue !== (defaultValueRef.current ?? void 0) || validationBehavior === "submit" && meta.submitCount > 0) {
1351
+ if (debounceTimerRef.current) {
1352
+ clearTimeout(debounceTimerRef.current);
1353
+ }
1354
+ const debounceMs = meta.config.validationDebounce ?? 150;
1355
+ debounceTimerRef.current = setTimeout(() => {
1356
+ void validate();
1357
+ }, debounceMs);
1358
+ }
1359
+ },
1360
+ [name, setValue, validationBehavior, validate, meta.config.validationDebounce, meta.submitCount, node]
1361
+ );
1362
+ const onBlur = useCallback(() => {
1363
+ setTouched(true);
1364
+ node.touched = true;
1365
+ hasBlurredRef.current = true;
1366
+ if (validationBehavior === "blur") {
1367
+ void validate();
1368
+ }
1369
+ }, [validationBehavior, validate, node]);
1370
+ const depsKey = dependsOn?.join(",") ?? "";
1371
+ const depsValues = dependsOn?.map((dep) => JSON.stringify(getValue(dep))).join("|") ?? "";
1372
+ useEffect(() => {
1373
+ if (!dependsOn || dependsOn.length === 0) return;
1374
+ if (touched || hasBlurredRef.current) {
1375
+ void validate();
1376
+ }
1377
+ }, [depsKey, depsValues, validate]);
1378
+ useEffect(() => {
1379
+ return () => {
1380
+ if (debounceTimerRef.current) {
1381
+ clearTimeout(debounceTimerRef.current);
1382
+ }
1383
+ };
1384
+ }, []);
1385
+ const value = getValue(name) ?? defaultValue;
1386
+ const valid = errorStrings.length === 0;
1387
+ const error = errorStrings[0] ?? null;
1388
+ const showErrors = touched || meta.showErrors;
1389
+ const fieldDisabled = disabled || meta.isDisabled;
1390
+ const fieldProps = useMemo(
1391
+ () => ({
1392
+ value: value ?? "",
1393
+ onChange,
1394
+ onBlur,
1395
+ "aria-invalid": showErrors && !valid ? true : void 0,
1396
+ "aria-describedby": showErrors && error ? `${fieldId}-error` : void 0,
1397
+ id: fieldId,
1398
+ name,
1399
+ disabled: fieldDisabled || void 0
1400
+ }),
1401
+ [value, onChange, onBlur, showErrors, valid, error, fieldId, name, fieldDisabled]
1402
+ );
1403
+ return {
1404
+ value,
1405
+ error: showErrors ? error : null,
1406
+ errors: showErrors ? errorStrings : [],
1407
+ touched,
1408
+ dirty,
1409
+ valid,
1410
+ onChange,
1411
+ onBlur,
1412
+ fieldProps,
1413
+ id: fieldId,
1414
+ node
1415
+ };
1416
+ }
1417
+ function isZodSchema(validation) {
1418
+ return typeof validation === "object" && validation !== null && "_def" in validation && "safeParse" in validation;
1419
+ }
1420
+ function subscribeToNode(node, onChange) {
1421
+ const unsubs = [
1422
+ node.events.on("commit", onChange),
1423
+ node.events.on("reset", onChange),
1424
+ node.events.on("message-added", onChange),
1425
+ node.events.on("message-removed", onChange)
1426
+ ];
1427
+ return () => unsubs.forEach((u) => u());
1428
+ }
1429
+ function useFormNode(id) {
1430
+ const [, forceRender] = useState(0);
1431
+ const nodeUnsubRef = useRef(null);
1432
+ const triggerRender = useCallback(() => {
1433
+ forceRender((n) => n + 1);
1434
+ }, []);
1435
+ const subscribeToNodeEvents = useCallback(
1436
+ (node) => {
1437
+ nodeUnsubRef.current?.();
1438
+ nodeUnsubRef.current = subscribeToNode(node, triggerRender);
1439
+ },
1440
+ [triggerRender]
1441
+ );
1442
+ useEffect(() => {
1443
+ if (!id) return;
1444
+ const node = getNode(id);
1445
+ if (node) {
1446
+ subscribeToNodeEvents(node);
1447
+ }
1448
+ const unsubRegistry = subscribeToRegistry((event, registeredId, registeredNode) => {
1449
+ if (registeredId !== id) return;
1450
+ if (event === "registered") {
1451
+ subscribeToNodeEvents(registeredNode);
1452
+ triggerRender();
1453
+ } else if (event === "unregistered") {
1454
+ nodeUnsubRef.current?.();
1455
+ nodeUnsubRef.current = null;
1456
+ triggerRender();
1457
+ }
1458
+ });
1459
+ return () => {
1460
+ unsubRegistry();
1461
+ nodeUnsubRef.current?.();
1462
+ nodeUnsubRef.current = null;
1463
+ };
1464
+ }, [id, triggerRender, subscribeToNodeEvents]);
1465
+ return id ? getNode(id) : void 0;
1466
+ }
1467
+ function useFormNodes(ids) {
1468
+ const [, forceRender] = useState(0);
1469
+ const nodeUnsubsRef = useRef(/* @__PURE__ */ new Map());
1470
+ const triggerRender = useCallback(() => {
1471
+ forceRender((n) => n + 1);
1472
+ }, []);
1473
+ const idsKey = ids.join(",");
1474
+ useEffect(() => {
1475
+ for (const unsub of nodeUnsubsRef.current.values()) {
1476
+ unsub();
1477
+ }
1478
+ nodeUnsubsRef.current.clear();
1479
+ const idSet = new Set(ids);
1480
+ for (const id of ids) {
1481
+ const node = getNode(id);
1482
+ if (node) {
1483
+ nodeUnsubsRef.current.set(id, subscribeToNode(node, triggerRender));
1484
+ }
1485
+ }
1486
+ const unsubRegistry = subscribeToRegistry((event, registeredId, registeredNode) => {
1487
+ if (!idSet.has(registeredId)) return;
1488
+ if (event === "registered") {
1489
+ nodeUnsubsRef.current.get(registeredId)?.();
1490
+ nodeUnsubsRef.current.set(
1491
+ registeredId,
1492
+ subscribeToNode(registeredNode, triggerRender)
1493
+ );
1494
+ triggerRender();
1495
+ } else if (event === "unregistered") {
1496
+ nodeUnsubsRef.current.get(registeredId)?.();
1497
+ nodeUnsubsRef.current.delete(registeredId);
1498
+ triggerRender();
1499
+ }
1500
+ });
1501
+ return () => {
1502
+ unsubRegistry();
1503
+ for (const unsub of nodeUnsubsRef.current.values()) {
1504
+ unsub();
1505
+ }
1506
+ nodeUnsubsRef.current.clear();
1507
+ };
1508
+ }, [idsKey, triggerRender]);
1509
+ const result = /* @__PURE__ */ new Map();
1510
+ for (const id of ids) {
1511
+ result.set(id, getNode(id));
1512
+ }
1513
+ return result;
1514
+ }
1515
+ function useFormPrint(defaultOptions) {
1516
+ const { values } = useFormValues();
1517
+ const meta = useFormMeta();
1518
+ const printContainerRef = useRef(null);
1519
+ const getFieldDescriptors = useCallback(
1520
+ (_mode) => {
1521
+ const descriptors = [];
1522
+ const formNode = meta.formNode;
1523
+ if (!formNode) return descriptors;
1524
+ for (const child of formNode.children) {
1525
+ const name = child.name;
1526
+ const config = child.config ?? {};
1527
+ const label = config.label;
1528
+ const type = child.type === "input" ? config.inputType ?? "text" : child.type;
1529
+ if (child.type !== "input") continue;
1530
+ const descriptor = {
1531
+ name,
1532
+ label: label ?? name,
1533
+ type,
1534
+ value: values[name],
1535
+ options: config.options
1536
+ };
1537
+ descriptors.push(descriptor);
1538
+ }
1539
+ return descriptors;
1540
+ },
1541
+ [meta.formNode, values]
1542
+ );
1543
+ const printForm = useCallback(
1544
+ (overrides) => {
1545
+ const options = { ...defaultOptions, ...overrides };
1546
+ const mode = options.mode ?? "filled";
1547
+ const fields = getFieldDescriptors(mode);
1548
+ injectPrintCSS();
1549
+ const container = document.createElement("div");
1550
+ container.setAttribute("data-formfoundry-print-root", "");
1551
+ container.style.display = "none";
1552
+ const resolvedMode = mode === "current" ? "filled" : mode;
1553
+ container.innerHTML = buildPrintHTML(fields, resolvedMode, options);
1554
+ document.body.appendChild(container);
1555
+ printContainerRef.current = container;
1556
+ container.style.display = "block";
1557
+ requestAnimationFrame(() => {
1558
+ window.print();
1559
+ setTimeout(() => {
1560
+ if (printContainerRef.current) {
1561
+ document.body.removeChild(printContainerRef.current);
1562
+ printContainerRef.current = null;
1563
+ }
1564
+ }, 100);
1565
+ });
1566
+ },
1567
+ [defaultOptions, getFieldDescriptors]
1568
+ );
1569
+ return { printForm, getFieldDescriptors };
1570
+ }
1571
+ function buildPrintHTML(fields, mode, options) {
1572
+ const header = options.title || options.subtitle ? `<div class="ff-print__header">
1573
+ ${options.title ? `<h1 class="ff-print__title">${escapeHtml(options.title)}</h1>` : ""}
1574
+ ${options.subtitle ? `<p class="ff-print__subtitle">${escapeHtml(options.subtitle)}</p>` : ""}
1575
+ </div>` : "";
1576
+ const fieldRows = fields.map((field) => {
1577
+ const label = escapeHtml(field.label || field.name);
1578
+ const isTextarea = field.type === "textarea";
1579
+ const fieldClass = `ff-print__field${isTextarea ? " ff-print__field--textarea" : ""}`;
1580
+ const valueHtml = mode === "blank" ? buildBlankValue(field) : buildFilledValue(field);
1581
+ return `<li class="${fieldClass}">
1582
+ <span class="ff-print__label">${label}:</span>
1583
+ <span class="ff-print__value">${valueHtml}</span>
1584
+ </li>`;
1585
+ }).join("\n");
1586
+ const footer = options.footer ? `<div class="ff-print__footer">${escapeHtml(options.footer)}</div>` : "";
1587
+ return `<div class="ff-print${options.className ? ` ${options.className}` : ""}">
1588
+ ${header}
1589
+ <ul class="ff-print__fields">${fieldRows}</ul>
1590
+ ${footer}
1591
+ </div>`;
1592
+ }
1593
+ function buildBlankValue(field) {
1594
+ switch (field.type) {
1595
+ case "checkbox":
1596
+ case "switch":
1597
+ return '<span class="ff-print__blank-box"></span>';
1598
+ case "radio":
1599
+ return `<span class="ff-print__blank-options">
1600
+ ${(field.options ?? []).map(
1601
+ (opt) => `<span class="ff-print__blank-option"><span class="ff-print__blank-radio"></span>${escapeHtml(opt.label)}</span>`
1602
+ ).join("")}
1603
+ </span>`;
1604
+ case "select":
1605
+ case "checkboxgroup":
1606
+ return `<span class="ff-print__blank-options">
1607
+ ${(field.options ?? []).map(
1608
+ (opt) => `<span class="ff-print__blank-option"><span class="ff-print__blank-box"></span>${escapeHtml(opt.label)}</span>`
1609
+ ).join("")}
1610
+ </span>`;
1611
+ case "textarea":
1612
+ return '<div class="ff-print__blank-textarea"></div>';
1613
+ default:
1614
+ return '<span class="ff-print__blank-line"></span>';
1615
+ }
1616
+ }
1617
+ function buildFilledValue(field) {
1618
+ const { value, type } = field;
1619
+ switch (type) {
1620
+ case "checkbox":
1621
+ case "switch":
1622
+ return `<span class="ff-print__filled-check">${value ? "\u2611" : "\u2610"}</span>`;
1623
+ case "radio":
1624
+ case "select": {
1625
+ const selected = field.options?.find(
1626
+ (opt) => String(opt.value) === String(value)
1627
+ );
1628
+ return `<span class="ff-print__filled-value">${escapeHtml(selected?.label ?? String(value ?? "\u2014"))}</span>`;
1629
+ }
1630
+ case "checkboxgroup": {
1631
+ const selectedValues = Array.isArray(value) ? value : [];
1632
+ const labels = (field.options ?? []).filter((opt) => selectedValues.includes(opt.value)).map((opt) => opt.label);
1633
+ return `<span class="ff-print__filled-value">${escapeHtml(labels.length > 0 ? labels.join(", ") : "\u2014")}</span>`;
1634
+ }
1635
+ case "textarea":
1636
+ return `<div class="ff-print__filled-textarea">${escapeHtml(String(value ?? ""))}</div>`;
1637
+ case "file":
1638
+ return `<span class="ff-print__filled-value">${value ? escapeHtml(String(value)) : "No file selected"}</span>`;
1639
+ case "hidden":
1640
+ return "";
1641
+ default:
1642
+ return `<span class="ff-print__filled-value">${value != null && value !== "" ? escapeHtml(String(value)) : "\u2014"}</span>`;
1643
+ }
1644
+ }
1645
+ function escapeHtml(str) {
1646
+ return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
1647
+ }
1648
+ var PRINT_STYLE_ID = "formfoundry-print-css";
1649
+ function injectPrintCSS() {
1650
+ if (typeof document === "undefined") return;
1651
+ if (document.getElementById(PRINT_STYLE_ID)) return;
1652
+ const style = document.createElement("style");
1653
+ style.id = PRINT_STYLE_ID;
1654
+ style.textContent = PRINT_CSS;
1655
+ document.head.appendChild(style);
1656
+ }
1657
+ var PRINT_CSS = `
1658
+ @media print {
1659
+ body > *:not([data-formfoundry-print-root]) {
1660
+ display: none !important;
1661
+ }
1662
+ [data-formfoundry-print-root] {
1663
+ display: block !important;
1664
+ position: static !important;
1665
+ width: 100% !important;
1666
+ background: white !important;
1667
+ color: black !important;
1668
+ }
1669
+ }
1670
+ .ff-print {
1671
+ font-family: 'Georgia', 'Times New Roman', serif;
1672
+ font-size: 12pt;
1673
+ line-height: 1.5;
1674
+ color: #000;
1675
+ background: #fff;
1676
+ padding: 0.5in;
1677
+ max-width: 8.5in;
1678
+ margin: 0 auto;
1679
+ }
1680
+ .ff-print__header {
1681
+ text-align: center;
1682
+ margin-bottom: 1.5rem;
1683
+ padding-bottom: 0.75rem;
1684
+ border-bottom: 2px solid #000;
1685
+ }
1686
+ .ff-print__title {
1687
+ font-size: 18pt;
1688
+ font-weight: 700;
1689
+ margin: 0 0 0.25rem 0;
1690
+ }
1691
+ .ff-print__subtitle {
1692
+ font-size: 10pt;
1693
+ color: #555;
1694
+ margin: 0;
1695
+ font-style: italic;
1696
+ }
1697
+ .ff-print__fields {
1698
+ list-style: none;
1699
+ padding: 0;
1700
+ margin: 0;
1701
+ }
1702
+ .ff-print__field {
1703
+ display: flex;
1704
+ align-items: baseline;
1705
+ padding: 0.4rem 0;
1706
+ border-bottom: 1px solid #e5e5e5;
1707
+ break-inside: avoid;
1708
+ page-break-inside: avoid;
1709
+ }
1710
+ .ff-print__field--textarea {
1711
+ flex-direction: column;
1712
+ align-items: stretch;
1713
+ }
1714
+ .ff-print__label {
1715
+ font-weight: 600;
1716
+ font-size: 10pt;
1717
+ min-width: 160px;
1718
+ flex-shrink: 0;
1719
+ color: #333;
1720
+ }
1721
+ .ff-print__value {
1722
+ flex: 1;
1723
+ font-size: 11pt;
1724
+ }
1725
+ .ff-print__blank-line {
1726
+ flex: 1;
1727
+ border-bottom: 1px solid #999;
1728
+ min-height: 1.2em;
1729
+ margin-left: 0.5rem;
1730
+ }
1731
+ .ff-print__blank-box {
1732
+ display: inline-block;
1733
+ width: 14px;
1734
+ height: 14px;
1735
+ border: 1.5px solid #666;
1736
+ vertical-align: middle;
1737
+ margin-right: 0.25rem;
1738
+ border-radius: 2px;
1739
+ }
1740
+ .ff-print__blank-radio {
1741
+ display: inline-block;
1742
+ width: 14px;
1743
+ height: 14px;
1744
+ border: 1.5px solid #666;
1745
+ vertical-align: middle;
1746
+ margin-right: 0.25rem;
1747
+ border-radius: 50%;
1748
+ }
1749
+ .ff-print__blank-textarea {
1750
+ border: 1px solid #999;
1751
+ min-height: 4em;
1752
+ margin-top: 0.35rem;
1753
+ border-radius: 2px;
1754
+ }
1755
+ .ff-print__blank-options {
1756
+ display: flex;
1757
+ flex-direction: column;
1758
+ gap: 0.25rem;
1759
+ margin-left: 0.5rem;
1760
+ }
1761
+ .ff-print__blank-option {
1762
+ display: flex;
1763
+ align-items: center;
1764
+ gap: 0.35rem;
1765
+ font-size: 10pt;
1766
+ }
1767
+ .ff-print__filled-value {
1768
+ font-size: 11pt;
1769
+ margin-left: 0.5rem;
1770
+ }
1771
+ .ff-print__filled-check {
1772
+ font-size: 13pt;
1773
+ margin-left: 0.5rem;
1774
+ }
1775
+ .ff-print__filled-textarea {
1776
+ white-space: pre-wrap;
1777
+ font-size: 10pt;
1778
+ margin-top: 0.35rem;
1779
+ padding: 0.35rem;
1780
+ background: #fafafa;
1781
+ border: 1px solid #e0e0e0;
1782
+ border-radius: 2px;
1783
+ min-height: 2em;
1784
+ }
1785
+ .ff-print__footer {
1786
+ margin-top: 2rem;
1787
+ padding-top: 0.75rem;
1788
+ border-top: 1px solid #ccc;
1789
+ font-size: 9pt;
1790
+ color: #888;
1791
+ text-align: center;
1792
+ }
1793
+ .ff-print--preview {
1794
+ border: 1px solid #ddd;
1795
+ border-radius: 8px;
1796
+ box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
1797
+ margin: 1rem 0;
1798
+ }
1799
+ `;
1800
+ var InputRegistryContext = createContext(null);
1801
+ function useInputRegistry() {
1802
+ const registry = useContext(InputRegistryContext);
1803
+ if (!registry) {
1804
+ throw new Error(
1805
+ "[FormFoundry] useInputRegistry must be used within a <FormFoundryProvider>. Wrap your application or form tree with <FormFoundryProvider>."
1806
+ );
1807
+ }
1808
+ return registry;
1809
+ }
1810
+ function createRegistry(entries) {
1811
+ const map = /* @__PURE__ */ new Map();
1812
+ if (entries) {
1813
+ for (const [type, component] of Object.entries(entries)) {
1814
+ map.set(type, component);
1815
+ }
1816
+ }
1817
+ return {
1818
+ register(type, component) {
1819
+ map.set(type, component);
1820
+ },
1821
+ get(type) {
1822
+ return map.get(type);
1823
+ },
1824
+ has(type) {
1825
+ return map.has(type);
1826
+ },
1827
+ keys() {
1828
+ return [...map.keys()];
1829
+ }
1830
+ };
1831
+ }
1832
+ function createDefaultRegistry() {
1833
+ return createRegistry();
1834
+ }
1835
+ var FormFoundryProviderContext = createContext({});
1836
+ function useFormFoundryProvider() {
1837
+ return useContext(FormFoundryProviderContext);
1838
+ }
1839
+ function FormFoundryProvider({
1840
+ config,
1841
+ locale,
1842
+ registry,
1843
+ plugins,
1844
+ children
1845
+ }) {
1846
+ const inputRegistry = useMemo(
1847
+ () => registry ?? createDefaultRegistry(),
1848
+ [registry]
1849
+ );
1850
+ const providerValue = useMemo(
1851
+ () => ({
1852
+ config: config ? { ...config, locale } : locale ? { locale } : void 0,
1853
+ plugins
1854
+ }),
1855
+ [config, locale, plugins]
1856
+ );
1857
+ return /* @__PURE__ */ jsx(FormFoundryProviderContext.Provider, { value: providerValue, children: /* @__PURE__ */ jsx(InputRegistryContext.Provider, { value: inputRegistry, children }) });
1858
+ }
1859
+ function formReducer(state, action) {
1860
+ switch (action.type) {
1861
+ case "SET_VALUE":
1862
+ return {
1863
+ ...state,
1864
+ values: { ...state.values, [action.name]: action.value }
1865
+ };
1866
+ case "SET_VALUES":
1867
+ return { ...state, values: action.values };
1868
+ case "SET_SUBMITTING":
1869
+ return { ...state, isSubmitting: action.isSubmitting };
1870
+ case "SHOW_ERRORS":
1871
+ return { ...state, showErrors: true };
1872
+ case "RESET":
1873
+ return {
1874
+ values: action.values,
1875
+ isSubmitting: false,
1876
+ showErrors: false,
1877
+ submitCount: 0,
1878
+ externalErrors: {}
1879
+ };
1880
+ case "INCREMENT_SUBMIT_COUNT":
1881
+ return { ...state, submitCount: state.submitCount + 1 };
1882
+ case "SET_EXTERNAL_ERRORS":
1883
+ return { ...state, externalErrors: action.errors };
1884
+ case "CLEAR_EXTERNAL_ERRORS":
1885
+ return { ...state, externalErrors: {} };
1886
+ default:
1887
+ return state;
1888
+ }
1889
+ }
1890
+ function Form({
1891
+ id: formId,
1892
+ ref,
1893
+ onSubmit,
1894
+ onSubmitRaw,
1895
+ defaultValues: defaultValues2 = {},
1896
+ schema,
1897
+ resolver,
1898
+ plugins,
1899
+ validationBehavior,
1900
+ showErrors: showErrorsProp,
1901
+ disableOnSubmit = true,
1902
+ as: Component = "form",
1903
+ children,
1904
+ className
1905
+ }) {
1906
+ const providerConfig = useFormFoundryProvider();
1907
+ const [state, dispatch] = useReducer(formReducer, {
1908
+ values: defaultValues2,
1909
+ isSubmitting: false,
1910
+ showErrors: false,
1911
+ submitCount: 0,
1912
+ externalErrors: {}
1913
+ });
1914
+ const [validationVersion, setValidationVersion] = useState(0);
1915
+ const nodesRef = useRef(/* @__PURE__ */ new Map());
1916
+ const validateCallbacksRef = useRef(/* @__PURE__ */ new Map());
1917
+ const formNodeRef = useRef(null);
1918
+ if (!formNodeRef.current) {
1919
+ formNodeRef.current = createNode({
1920
+ type: "group",
1921
+ name: "__form__",
1922
+ id: formId,
1923
+ value: defaultValues2,
1924
+ config: {
1925
+ validationBehavior: validationBehavior ?? providerConfig.config?.validationBehavior,
1926
+ plugins: [...providerConfig.plugins ?? [], ...plugins ?? []]
1927
+ }
1928
+ });
1929
+ }
1930
+ const defaultValuesRef = useRef(defaultValues2);
1931
+ defaultValuesRef.current = defaultValues2;
1932
+ const valuesContext = useMemo(
1933
+ () => ({
1934
+ values: state.values,
1935
+ getValue: (name) => state.values[name],
1936
+ setValue: (name, value) => {
1937
+ dispatch({ type: "SET_VALUE", name, value });
1938
+ }
1939
+ }),
1940
+ [state.values]
1941
+ );
1942
+ const valuesRef = useRef(state.values);
1943
+ valuesRef.current = state.values;
1944
+ const externalErrorsRef = useRef(state.externalErrors);
1945
+ externalErrorsRef.current = state.externalErrors;
1946
+ const register = useCallback((name, node) => {
1947
+ nodesRef.current.set(name, node);
1948
+ if (formNodeRef.current && !formNodeRef.current.children.includes(node)) {
1949
+ formNodeRef.current.addChild(node);
1950
+ }
1951
+ }, []);
1952
+ const updateValidateCallback = useCallback((name, validate) => {
1953
+ const wrappedValidate = async () => {
1954
+ await validate();
1955
+ setValidationVersion((v) => v + 1);
1956
+ };
1957
+ validateCallbacksRef.current.set(name, wrappedValidate);
1958
+ }, []);
1959
+ const unregister = useCallback((name) => {
1960
+ const node = nodesRef.current.get(name);
1961
+ if (node) {
1962
+ formNodeRef.current?.removeChild(node);
1963
+ }
1964
+ nodesRef.current.delete(name);
1965
+ validateCallbacksRef.current.delete(name);
1966
+ }, []);
1967
+ const getNode2 = useCallback(
1968
+ (name) => nodesRef.current.get(name),
1969
+ []
1970
+ );
1971
+ const triggerValidation = useCallback(
1972
+ async (name) => {
1973
+ if (name) {
1974
+ const validateFn = validateCallbacksRef.current.get(name);
1975
+ if (validateFn) {
1976
+ await validateFn();
1977
+ }
1978
+ const node = nodesRef.current.get(name);
1979
+ return node ? node.valid : true;
1980
+ }
1981
+ const validationPromises = [];
1982
+ for (const validateFn of validateCallbacksRef.current.values()) {
1983
+ validationPromises.push(validateFn());
1984
+ }
1985
+ await Promise.all(validationPromises);
1986
+ let allValid = true;
1987
+ for (const node of nodesRef.current.values()) {
1988
+ if (!node.valid) {
1989
+ allValid = false;
1990
+ }
1991
+ }
1992
+ if (schema) {
1993
+ const formErrors = validateFormWithZod(schema, valuesRef.current);
1994
+ if (Object.keys(formErrors).length > 0) {
1995
+ allValid = false;
1996
+ }
1997
+ }
1998
+ if (resolver) {
1999
+ const result = await resolver(valuesRef.current);
2000
+ if (Object.keys(result.errors).length > 0) {
2001
+ allValid = false;
2002
+ }
2003
+ }
2004
+ return allValid;
2005
+ },
2006
+ [schema, resolver]
2007
+ );
2008
+ const reset = useCallback((values) => {
2009
+ const resetValues = values ?? defaultValuesRef.current;
2010
+ for (const [fieldName, node] of nodesRef.current) {
2011
+ node.reset(resetValues[fieldName]);
2012
+ }
2013
+ dispatch({ type: "RESET", values: resetValues });
2014
+ }, []);
2015
+ const setExternalErrors = useCallback((errors) => {
2016
+ const normalized = {};
2017
+ for (const [key, val] of Object.entries(errors)) {
2018
+ normalized[key] = Array.isArray(val) ? val : [val];
2019
+ }
2020
+ dispatch({ type: "SET_EXTERNAL_ERRORS", errors: normalized });
2021
+ }, []);
2022
+ const clearExternalErrors = useCallback(() => {
2023
+ dispatch({ type: "CLEAR_EXTERNAL_ERRORS" });
2024
+ }, []);
2025
+ const aggregatedErrors = useMemo(() => {
2026
+ const errors = {};
2027
+ for (const [name, node] of nodesRef.current) {
2028
+ if (node.errors.length > 0) {
2029
+ errors[name] = [...node.errors];
2030
+ }
2031
+ }
2032
+ for (const [name, extErrors] of Object.entries(state.externalErrors)) {
2033
+ if (!errors[name]) {
2034
+ errors[name] = [];
2035
+ }
2036
+ errors[name].push(...extErrors);
2037
+ }
2038
+ return errors;
2039
+ }, [state.values, state.externalErrors, validationVersion]);
2040
+ const isValid = useMemo(() => {
2041
+ for (const node of nodesRef.current.values()) {
2042
+ if (!node.valid) return false;
2043
+ }
2044
+ return Object.keys(state.externalErrors).length === 0;
2045
+ }, [state.values, state.externalErrors, validationVersion]);
2046
+ const isDisabled = disableOnSubmit && state.isSubmitting;
2047
+ const metaContext = useMemo(
2048
+ () => ({
2049
+ isSubmitting: state.isSubmitting,
2050
+ isValid,
2051
+ errors: aggregatedErrors,
2052
+ register,
2053
+ unregister,
2054
+ updateValidateCallback,
2055
+ getNode: getNode2,
2056
+ triggerValidation,
2057
+ showErrors: state.showErrors || (showErrorsProp ?? false),
2058
+ config: {
2059
+ ...providerConfig.config,
2060
+ validationBehavior: validationBehavior ?? providerConfig.config?.validationBehavior
2061
+ },
2062
+ formNode: formNodeRef.current,
2063
+ reset,
2064
+ submitCount: state.submitCount,
2065
+ setExternalErrors,
2066
+ clearExternalErrors,
2067
+ isDisabled
2068
+ }),
2069
+ [
2070
+ state.isSubmitting,
2071
+ state.showErrors,
2072
+ state.submitCount,
2073
+ state.externalErrors,
2074
+ isValid,
2075
+ isDisabled,
2076
+ aggregatedErrors,
2077
+ register,
2078
+ unregister,
2079
+ updateValidateCallback,
2080
+ getNode2,
2081
+ triggerValidation,
2082
+ showErrorsProp,
2083
+ providerConfig.config,
2084
+ validationBehavior,
2085
+ reset,
2086
+ setExternalErrors,
2087
+ clearExternalErrors
2088
+ ]
2089
+ );
2090
+ const performSubmit = useCallback(
2091
+ async () => {
2092
+ dispatch({ type: "INCREMENT_SUBMIT_COUNT" });
2093
+ onSubmitRaw?.(valuesRef.current);
2094
+ dispatch({ type: "SHOW_ERRORS" });
2095
+ const valid = await triggerValidation();
2096
+ if (!valid) return;
2097
+ dispatch({ type: "SET_SUBMITTING", isSubmitting: true });
2098
+ try {
2099
+ const formNode = formNodeRef.current;
2100
+ const currentValues = valuesRef.current;
2101
+ const values = formNode ? formNode.hooks.submit.dispatch({ ...currentValues }) : currentValues;
2102
+ await onSubmit(values);
2103
+ } finally {
2104
+ dispatch({ type: "SET_SUBMITTING", isSubmitting: false });
2105
+ }
2106
+ },
2107
+ [triggerValidation, onSubmit, onSubmitRaw]
2108
+ );
2109
+ const handleSubmit = useCallback(
2110
+ async (e) => {
2111
+ e.preventDefault();
2112
+ await performSubmit();
2113
+ },
2114
+ [performSubmit]
2115
+ );
2116
+ useEffect(() => {
2117
+ const formNode = formNodeRef.current;
2118
+ if (formNode) {
2119
+ formNode.submit = performSubmit;
2120
+ }
2121
+ }, [performSubmit]);
2122
+ const { printForm } = useFormPrint();
2123
+ useImperativeHandle(ref, () => ({
2124
+ submit: performSubmit,
2125
+ reset,
2126
+ getValues: () => ({ ...valuesRef.current }),
2127
+ isValid: () => {
2128
+ for (const node of nodesRef.current.values()) {
2129
+ if (!node.valid) return false;
2130
+ }
2131
+ return Object.keys(externalErrorsRef.current).length === 0;
2132
+ },
2133
+ printForm
2134
+ }), [performSubmit, reset, printForm]);
2135
+ return /* @__PURE__ */ jsx(FormValuesContext.Provider, { value: valuesContext, children: /* @__PURE__ */ jsx(FormMetaContext.Provider, { value: metaContext, children: /* @__PURE__ */ jsx(
2136
+ Component,
2137
+ {
2138
+ onSubmit: Component === "form" ? handleSubmit : void 0,
2139
+ className,
2140
+ noValidate: Component === "form" ? true : void 0,
2141
+ children
2142
+ }
2143
+ ) }) });
2144
+ }
2145
+
2146
+ // src/schema/expressions.ts
2147
+ var EXPRESSION_PREFIX = "$: ";
2148
+ function isExpression(value) {
2149
+ return typeof value === "string" && value.startsWith(EXPRESSION_PREFIX);
2150
+ }
2151
+ function isVariableRef(value) {
2152
+ return typeof value === "string" && value.startsWith("$") && !value.startsWith("$: ") && !value.startsWith("$el") && !value.startsWith("$cmp") && !value.startsWith("$formfoundry");
2153
+ }
2154
+ function tokenize(expr) {
2155
+ const tokens = [];
2156
+ let i = 0;
2157
+ while (i < expr.length) {
2158
+ const ch = expr[i];
2159
+ if (/\s/.test(ch)) {
2160
+ i++;
2161
+ continue;
2162
+ }
2163
+ if (/[0-9]/.test(ch) || ch === "." && i + 1 < expr.length && /[0-9]/.test(expr[i + 1])) {
2164
+ let num = "";
2165
+ while (i < expr.length && /[0-9.]/.test(expr[i])) {
2166
+ num += expr[i];
2167
+ i++;
2168
+ }
2169
+ tokens.push({ type: "number", value: parseFloat(num) });
2170
+ continue;
2171
+ }
2172
+ if (ch === "'" || ch === '"') {
2173
+ const quote = ch;
2174
+ i++;
2175
+ let str = "";
2176
+ while (i < expr.length && expr[i] !== quote) {
2177
+ if (expr[i] === "\\" && i + 1 < expr.length) {
2178
+ i++;
2179
+ str += expr[i];
2180
+ } else {
2181
+ str += expr[i];
2182
+ }
2183
+ i++;
2184
+ }
2185
+ i++;
2186
+ tokens.push({ type: "string", value: str });
2187
+ continue;
2188
+ }
2189
+ if (ch === "$") {
2190
+ i++;
2191
+ let varName = "";
2192
+ while (i < expr.length && /[a-zA-Z0-9_]/.test(expr[i])) {
2193
+ varName += expr[i];
2194
+ i++;
2195
+ }
2196
+ while (i < expr.length && expr[i] === "." && i + 1 < expr.length && /[a-zA-Z_]/.test(expr[i + 1])) {
2197
+ varName += ".";
2198
+ i++;
2199
+ while (i < expr.length && /[a-zA-Z0-9_]/.test(expr[i])) {
2200
+ varName += expr[i];
2201
+ i++;
2202
+ }
2203
+ }
2204
+ tokens.push({ type: "variable", value: varName });
2205
+ continue;
2206
+ }
2207
+ if (expr.slice(i, i + 4) === "true" && (i + 4 >= expr.length || !/[a-zA-Z0-9_]/.test(expr[i + 4]))) {
2208
+ tokens.push({ type: "boolean", value: true });
2209
+ i += 4;
2210
+ continue;
2211
+ }
2212
+ if (expr.slice(i, i + 5) === "false" && (i + 5 >= expr.length || !/[a-zA-Z0-9_]/.test(expr[i + 5]))) {
2213
+ tokens.push({ type: "boolean", value: false });
2214
+ i += 5;
2215
+ continue;
2216
+ }
2217
+ if (expr.slice(i, i + 3) === "===" || expr.slice(i, i + 3) === "!==") {
2218
+ tokens.push({ type: "operator", value: expr.slice(i, i + 3) });
2219
+ i += 3;
2220
+ continue;
2221
+ }
2222
+ if (expr.slice(i, i + 2) === "&&" || expr.slice(i, i + 2) === "||" || expr.slice(i, i + 2) === ">=" || expr.slice(i, i + 2) === "<=") {
2223
+ tokens.push({ type: "operator", value: expr.slice(i, i + 2) });
2224
+ i += 2;
2225
+ continue;
2226
+ }
2227
+ if (ch === "+" || ch === "-" || ch === "*" || ch === "/" || ch === "%" || ch === ">" || ch === "<") {
2228
+ tokens.push({ type: "operator", value: ch });
2229
+ i++;
2230
+ continue;
2231
+ }
2232
+ if (ch === ".") {
2233
+ tokens.push({ type: "dot", value: "." });
2234
+ i++;
2235
+ continue;
2236
+ }
2237
+ if (ch === "?") {
2238
+ tokens.push({ type: "ternary_q", value: "?" });
2239
+ i++;
2240
+ continue;
2241
+ }
2242
+ if (ch === ":") {
2243
+ tokens.push({ type: "ternary_c", value: ":" });
2244
+ i++;
2245
+ continue;
2246
+ }
2247
+ if (ch === "(" || ch === ")") {
2248
+ tokens.push({ type: "paren", value: ch });
2249
+ i++;
2250
+ continue;
2251
+ }
2252
+ if (ch === "!" && (i + 1 >= expr.length || expr[i + 1] !== "=")) {
2253
+ tokens.push({ type: "not", value: "!" });
2254
+ i++;
2255
+ continue;
2256
+ }
2257
+ i++;
2258
+ }
2259
+ return tokens;
2260
+ }
2261
+ var Parser = class {
2262
+ constructor(tokens) {
2263
+ this.tokens = tokens;
2264
+ }
2265
+ pos = 0;
2266
+ parse() {
2267
+ const ast = this.parseTernary();
2268
+ return ast;
2269
+ }
2270
+ peek() {
2271
+ return this.tokens[this.pos];
2272
+ }
2273
+ consume() {
2274
+ return this.tokens[this.pos++];
2275
+ }
2276
+ parseTernary() {
2277
+ let node = this.parseOr();
2278
+ if (this.peek()?.type === "ternary_q") {
2279
+ this.consume();
2280
+ const consequent = this.parseTernary();
2281
+ if (this.peek()?.type === "ternary_c") {
2282
+ this.consume();
2283
+ }
2284
+ const alternate = this.parseTernary();
2285
+ node = { kind: "ternary", condition: node, consequent, alternate };
2286
+ }
2287
+ return node;
2288
+ }
2289
+ parseOr() {
2290
+ let node = this.parseAnd();
2291
+ while (this.peek()?.type === "operator" && this.peek()?.value === "||") {
2292
+ const op = this.consume().value;
2293
+ const right = this.parseAnd();
2294
+ node = { kind: "binary", op, left: node, right };
2295
+ }
2296
+ return node;
2297
+ }
2298
+ parseAnd() {
2299
+ let node = this.parseEquality();
2300
+ while (this.peek()?.type === "operator" && this.peek()?.value === "&&") {
2301
+ const op = this.consume().value;
2302
+ const right = this.parseEquality();
2303
+ node = { kind: "binary", op, left: node, right };
2304
+ }
2305
+ return node;
2306
+ }
2307
+ parseEquality() {
2308
+ let node = this.parseComparison();
2309
+ while (this.peek()?.type === "operator" && (this.peek()?.value === "===" || this.peek()?.value === "!==")) {
2310
+ const op = this.consume().value;
2311
+ const right = this.parseComparison();
2312
+ node = { kind: "binary", op, left: node, right };
2313
+ }
2314
+ return node;
2315
+ }
2316
+ parseComparison() {
2317
+ let node = this.parseAdditive();
2318
+ while (this.peek()?.type === "operator" && (this.peek()?.value === ">" || this.peek()?.value === "<" || this.peek()?.value === ">=" || this.peek()?.value === "<=")) {
2319
+ const op = this.consume().value;
2320
+ const right = this.parseAdditive();
2321
+ node = { kind: "binary", op, left: node, right };
2322
+ }
2323
+ return node;
2324
+ }
2325
+ parseAdditive() {
2326
+ let node = this.parseMultiplicative();
2327
+ while (this.peek()?.type === "operator" && (this.peek()?.value === "+" || this.peek()?.value === "-")) {
2328
+ const op = this.consume().value;
2329
+ const right = this.parseMultiplicative();
2330
+ node = { kind: "binary", op, left: node, right };
2331
+ }
2332
+ return node;
2333
+ }
2334
+ parseMultiplicative() {
2335
+ let node = this.parseUnary();
2336
+ while (this.peek()?.type === "operator" && (this.peek()?.value === "*" || this.peek()?.value === "/" || this.peek()?.value === "%")) {
2337
+ const op = this.consume().value;
2338
+ const right = this.parseUnary();
2339
+ node = { kind: "binary", op, left: node, right };
2340
+ }
2341
+ return node;
2342
+ }
2343
+ parseUnary() {
2344
+ if (this.peek()?.type === "not") {
2345
+ this.consume();
2346
+ const operand = this.parseUnary();
2347
+ return { kind: "unary", op: "!", operand };
2348
+ }
2349
+ return this.parsePrimary();
2350
+ }
2351
+ parsePrimary() {
2352
+ const token = this.peek();
2353
+ if (!token) {
2354
+ return { kind: "literal", value: void 0 };
2355
+ }
2356
+ if (token.type === "paren" && token.value === "(") {
2357
+ this.consume();
2358
+ const node = this.parseTernary();
2359
+ if (this.peek()?.type === "paren" && this.peek()?.value === ")") {
2360
+ this.consume();
2361
+ }
2362
+ return node;
2363
+ }
2364
+ if (token.type === "number" || token.type === "string" || token.type === "boolean") {
2365
+ this.consume();
2366
+ return { kind: "literal", value: token.value };
2367
+ }
2368
+ if (token.type === "variable") {
2369
+ this.consume();
2370
+ const path = token.value.split(".");
2371
+ return { kind: "variable", path };
2372
+ }
2373
+ this.consume();
2374
+ return { kind: "literal", value: void 0 };
2375
+ }
2376
+ };
2377
+ function resolvePath(data, path) {
2378
+ let current = data;
2379
+ for (const segment of path) {
2380
+ if (current == null || typeof current !== "object") return void 0;
2381
+ current = current[segment];
2382
+ }
2383
+ return current;
2384
+ }
2385
+ function evaluate(ast, data) {
2386
+ switch (ast.kind) {
2387
+ case "literal":
2388
+ return ast.value;
2389
+ case "variable":
2390
+ return resolvePath(data, ast.path);
2391
+ case "unary": {
2392
+ const val = evaluate(ast.operand, data);
2393
+ if (ast.op === "!") return !val;
2394
+ return val;
2395
+ }
2396
+ case "binary": {
2397
+ const left = evaluate(ast.left, data);
2398
+ const right = evaluate(ast.right, data);
2399
+ switch (ast.op) {
2400
+ case "+": {
2401
+ if (typeof left === "string" || typeof right === "string") {
2402
+ return String(left ?? "") + String(right ?? "");
2403
+ }
2404
+ return left + right;
2405
+ }
2406
+ case "-":
2407
+ return left - right;
2408
+ case "*":
2409
+ return left * right;
2410
+ case "/":
2411
+ return right !== 0 ? left / right : 0;
2412
+ case "%":
2413
+ return right !== 0 ? left % right : 0;
2414
+ case "===":
2415
+ return left === right;
2416
+ case "!==":
2417
+ return left !== right;
2418
+ case ">":
2419
+ return left > right;
2420
+ case "<":
2421
+ return left < right;
2422
+ case ">=":
2423
+ return left >= right;
2424
+ case "<=":
2425
+ return left <= right;
2426
+ case "&&":
2427
+ return left && right;
2428
+ case "||":
2429
+ return left || right;
2430
+ default:
2431
+ return void 0;
2432
+ }
2433
+ }
2434
+ case "ternary": {
2435
+ const condition = evaluate(ast.condition, data);
2436
+ return condition ? evaluate(ast.consequent, data) : evaluate(ast.alternate, data);
2437
+ }
2438
+ }
2439
+ }
2440
+ function evaluateExpression(expression, data) {
2441
+ const raw = expression.startsWith(EXPRESSION_PREFIX) ? expression.slice(EXPRESSION_PREFIX.length) : expression;
2442
+ const tokens = tokenize(raw);
2443
+ const parser = new Parser(tokens);
2444
+ const ast = parser.parse();
2445
+ return evaluate(ast, data);
2446
+ }
2447
+ function resolveVariableRef(ref, data) {
2448
+ const path = ref.slice(1).split(".");
2449
+ return resolvePath(data, path);
2450
+ }
2451
+ function resolveSchemaValue(value, data) {
2452
+ if (isExpression(value)) {
2453
+ return evaluateExpression(value, data);
2454
+ }
2455
+ if (isVariableRef(value)) {
2456
+ return resolveVariableRef(value, data);
2457
+ }
2458
+ return value;
2459
+ }
2460
+ var SchemaDataContext = createContext({
2461
+ values: {},
2462
+ data: {},
2463
+ merged: {}
2464
+ });
2465
+ function useSchemaData() {
2466
+ return useContext(SchemaDataContext);
2467
+ }
2468
+ function SchemaRenderer({ schema, library, data = {} }) {
2469
+ const registry = useInputRegistry();
2470
+ const { values } = useFormValues();
2471
+ const schemaDataContext = useMemo(
2472
+ () => ({
2473
+ values,
2474
+ data,
2475
+ merged: { ...values, ...data }
2476
+ }),
2477
+ [values, data]
2478
+ );
2479
+ return /* @__PURE__ */ jsx(SchemaDataContext.Provider, { value: schemaDataContext, children: schema.map((node, index) => /* @__PURE__ */ jsx(
2480
+ SchemaNodeRenderer,
2481
+ {
2482
+ node,
2483
+ registry,
2484
+ library
2485
+ },
2486
+ getNodeKey(node, index)
2487
+ )) });
2488
+ }
2489
+ function SchemaNodeRenderer({
2490
+ node,
2491
+ registry,
2492
+ library
2493
+ }) {
2494
+ const { merged } = useSchemaData();
2495
+ if ("for" in node && Array.isArray(node.for)) {
2496
+ return renderLoop(node, registry, library, merged);
2497
+ }
2498
+ if ("then" in node && "if" in node && !("$formfoundry" in node) && !("$el" in node) && !("$cmp" in node)) {
2499
+ return renderConditional(
2500
+ node,
2501
+ registry,
2502
+ library,
2503
+ merged
2504
+ );
2505
+ }
2506
+ if ("if" in node && typeof node.if === "function" && !node.if(merged)) {
2507
+ return null;
2508
+ }
2509
+ if ("$formfoundry" in node) {
2510
+ return renderFormFoundryInput(node, registry, merged);
2511
+ }
2512
+ if ("$el" in node) {
2513
+ return renderHtmlElement(
2514
+ node,
2515
+ registry,
2516
+ library,
2517
+ merged
2518
+ );
2519
+ }
2520
+ if ("$cmp" in node) {
2521
+ return renderComponent(
2522
+ node,
2523
+ registry,
2524
+ library,
2525
+ merged
2526
+ );
2527
+ }
2528
+ return null;
2529
+ }
2530
+ function renderFormFoundryInput(node, registry, merged = {}) {
2531
+ const { $formfoundry, if: _if, ...rawProps } = node;
2532
+ const Component = registry.get($formfoundry);
2533
+ if (!Component) {
2534
+ if (process.env.NODE_ENV !== "production") {
2535
+ console.warn(
2536
+ `[FormFoundry] No input registered for type "${$formfoundry}". Available types: ${registry.keys().join(", ") || "(none)"}`
2537
+ );
2538
+ }
2539
+ return null;
2540
+ }
2541
+ const props = resolveObjectExpressions(rawProps, merged);
2542
+ return /* @__PURE__ */ jsx(Component, { ...props });
2543
+ }
2544
+ function renderHtmlElement(node, registry, library, merged = {}) {
2545
+ const { $el, attrs: rawAttrs, children, if: _if } = node;
2546
+ const attrs = rawAttrs ? resolveObjectExpressions(rawAttrs, merged) : void 0;
2547
+ const childContent = resolveChildren(children, registry, library, merged);
2548
+ return React4.createElement($el, attrs, childContent);
2549
+ }
2550
+ function renderComponent(node, registry, library, merged = {}) {
2551
+ const { $cmp, props: rawProps, children, if: _if } = node;
2552
+ const Component = library?.[$cmp];
2553
+ if (!Component) {
2554
+ if (process.env.NODE_ENV !== "production") {
2555
+ console.warn(
2556
+ `[FormFoundry] No component found for "$cmp: ${$cmp}" in library.`
2557
+ );
2558
+ }
2559
+ return null;
2560
+ }
2561
+ const resolvedProps = rawProps ? resolveObjectExpressions(rawProps, merged) : {};
2562
+ const childContent = resolveChildren(children, registry, library, merged);
2563
+ return /* @__PURE__ */ jsx(Component, { ...resolvedProps, children: childContent });
2564
+ }
2565
+ function renderConditional(node, registry, library, merged) {
2566
+ const branch = node.if(merged) ? node.then : node.else;
2567
+ if (!branch) return null;
2568
+ return /* @__PURE__ */ jsx(Fragment, { children: branch.map((child, index) => /* @__PURE__ */ jsx(
2569
+ SchemaNodeRenderer,
2570
+ {
2571
+ node: child,
2572
+ registry,
2573
+ library
2574
+ },
2575
+ getNodeKey(child, index)
2576
+ )) });
2577
+ }
2578
+ function renderLoop(node, registry, library, merged) {
2579
+ const [itemName, indexName, source] = node.for;
2580
+ const items = typeof source === "string" ? merged[source] ?? [] : source ?? [];
2581
+ if (!Array.isArray(items)) return null;
2582
+ return /* @__PURE__ */ jsx(Fragment, { children: items.map((item, idx) => {
2583
+ const loopData = {
2584
+ ...merged,
2585
+ [itemName]: item,
2586
+ [indexName]: idx
2587
+ };
2588
+ return /* @__PURE__ */ jsx(
2589
+ SchemaDataContext.Provider,
2590
+ {
2591
+ value: { values: {}, data: loopData, merged: loopData },
2592
+ children: node.children.map((child, childIdx) => /* @__PURE__ */ jsx(
2593
+ SchemaNodeRenderer,
2594
+ {
2595
+ node: child,
2596
+ registry,
2597
+ library
2598
+ },
2599
+ getNodeKey(child, childIdx)
2600
+ ))
2601
+ },
2602
+ `loop_${idx}`
2603
+ );
2604
+ }) });
2605
+ }
2606
+ function resolveObjectExpressions(obj, data) {
2607
+ const resolved = {};
2608
+ for (const [key, value] of Object.entries(obj)) {
2609
+ resolved[key] = resolveSchemaValue(value, data);
2610
+ }
2611
+ return resolved;
2612
+ }
2613
+ function resolveChildren(children, registry, library, merged) {
2614
+ if (children === void 0) return null;
2615
+ if (typeof children === "string") {
2616
+ if (isExpression(children) || isVariableRef(children)) {
2617
+ return String(resolveSchemaValue(children, merged) ?? "");
2618
+ }
2619
+ return children;
2620
+ }
2621
+ return children.map((child, index) => /* @__PURE__ */ jsx(
2622
+ SchemaNodeRenderer,
2623
+ {
2624
+ node: child,
2625
+ registry,
2626
+ library
2627
+ },
2628
+ getNodeKey(child, index)
2629
+ ));
2630
+ }
2631
+ function getNodeKey(node, index) {
2632
+ if ("name" in node && typeof node.name === "string") {
2633
+ return node.name;
2634
+ }
2635
+ if ("$el" in node) {
2636
+ return `el_${index}`;
2637
+ }
2638
+ if ("$cmp" in node) {
2639
+ return `cmp_${node.$cmp}_${index}`;
2640
+ }
2641
+ return `node_${index}`;
2642
+ }
2643
+
2644
+ // src/plugins/index.ts
2645
+ function applyPlugins(node, plugins) {
2646
+ const cleanups = [];
2647
+ for (const plugin of plugins) {
2648
+ const cleanup = plugin(node);
2649
+ if (cleanup) {
2650
+ cleanups.push(cleanup);
2651
+ }
2652
+ }
2653
+ return cleanups;
2654
+ }
2655
+ function createDebugPlugin(prefix = "[FormFoundry]") {
2656
+ return (node) => {
2657
+ const unsubscribe = node.hooks.commit.use((value, next) => {
2658
+ console.log(`${prefix} ${node.name} committed:`, value);
2659
+ return next(value);
2660
+ });
2661
+ return unsubscribe;
2662
+ };
2663
+ }
2664
+ function createAutoTrimPlugin() {
2665
+ return (node) => {
2666
+ const unsubscribe = node.hooks.commit.use((value, next) => {
2667
+ if (typeof value === "string") {
2668
+ return next(value.trim());
2669
+ }
2670
+ return next(value);
2671
+ });
2672
+ return unsubscribe;
2673
+ };
2674
+ }
2675
+
2676
+ // src/resolvers/zodResolver.ts
2677
+ function zodResolver(schema) {
2678
+ return async (values) => {
2679
+ const result = schema.safeParse(values);
2680
+ if (result.success) {
2681
+ return { errors: {} };
2682
+ }
2683
+ const errors = {};
2684
+ for (const issue of result.error.issues) {
2685
+ const fieldName = issue.path[0];
2686
+ if (typeof fieldName === "string") {
2687
+ if (!errors[fieldName]) {
2688
+ errors[fieldName] = [];
2689
+ }
2690
+ errors[fieldName].push(issue.message);
2691
+ }
2692
+ }
2693
+ return { errors };
2694
+ };
2695
+ }
2696
+
2697
+ // src/i18n/en.ts
2698
+ var en = {
2699
+ code: "en",
2700
+ name: "English",
2701
+ messages: {
2702
+ // ─── Core rules ────────────────────────────────────────────────────
2703
+ required: "{label} is required.",
2704
+ email: "{label} must be a valid email address.",
2705
+ url: "{label} must be a valid URL.",
2706
+ min: "{label} must be at least {0}.",
2707
+ max: "{label} must be at most {0}.",
2708
+ minLength: "{label} must be at least {0} characters.",
2709
+ maxLength: "{label} must be at most {0} characters.",
2710
+ matches: "{label} does not match the required pattern.",
2711
+ confirm: "{label} does not match.",
2712
+ alpha: "{label} must contain only alphabetical characters.",
2713
+ alphanumeric: "{label} must contain only alphanumeric characters.",
2714
+ date: "{label} must be a valid date.",
2715
+ between: "{label} must be between {0} and {1}.",
2716
+ number: "{label} must be a number.",
2717
+ // ─── Extended rules ────────────────────────────────────────────────
2718
+ accepted: "{label} must be accepted.",
2719
+ alpha_spaces: "{label} must contain only letters and spaces.",
2720
+ contains_alpha: "{label} must contain at least one letter.",
2721
+ contains_alphanumeric: "{label} must contain at least one letter or number.",
2722
+ contains_alpha_spaces: "{label} must contain at least one letter or space.",
2723
+ contains_lowercase: "{label} must contain at least one lowercase letter.",
2724
+ contains_uppercase: "{label} must contain at least one uppercase letter.",
2725
+ contains_numeric: "{label} must contain at least one number.",
2726
+ contains_symbol: "{label} must contain at least one symbol.",
2727
+ date_after: "{label} must be after {0}.",
2728
+ date_before: "{label} must be before {0}.",
2729
+ date_between: "{label} must be between {0} and {1}.",
2730
+ date_format: "{label} must match the format {0}.",
2731
+ ends_with: ({ label, args }) => `${label} must end with ${args.join(", ") || "?"}.`,
2732
+ is: ({ label, args }) => `${label} must be one of: ${args.join(", ") || "?"}.`,
2733
+ length: ({ label, args }) => args[1] ? `${label} must be between ${args[0]} and ${args[1]} characters.` : `${label} must be exactly ${args[0] ?? "?"} characters.`,
2734
+ lowercase: "{label} must be all lowercase.",
2735
+ not: ({ label, args }) => `${label} must not be: ${args.join(", ") || "?"}.`,
2736
+ require_one: "At least one of these fields is required.",
2737
+ starts_with: ({ label, args }) => `${label} must start with ${args.join(", ") || "?"}.`,
2738
+ symbol: "{label} must contain only symbols.",
2739
+ uppercase: "{label} must be all uppercase."
2740
+ }
2741
+ };
2742
+ function FormPrintButton({
2743
+ options,
2744
+ children = "Print",
2745
+ className,
2746
+ disabled
2747
+ }) {
2748
+ const { printForm } = useFormPrint(options);
2749
+ return /* @__PURE__ */ jsx(
2750
+ "button",
2751
+ {
2752
+ type: "button",
2753
+ onClick: () => printForm(),
2754
+ className,
2755
+ disabled,
2756
+ "data-formfoundry": "print-button",
2757
+ children
2758
+ }
2759
+ );
2760
+ }
2761
+ function FormPrintView({
2762
+ fields,
2763
+ mode,
2764
+ title,
2765
+ subtitle,
2766
+ footer,
2767
+ className,
2768
+ preview = false
2769
+ }) {
2770
+ const resolvedMode = mode === "current" ? "filled" : mode;
2771
+ return /* @__PURE__ */ jsxs("div", { className: `ff-print${preview ? " ff-print--preview" : ""}${className ? ` ${className}` : ""}`, children: [
2772
+ (title || subtitle) && /* @__PURE__ */ jsxs("div", { className: "ff-print__header", children: [
2773
+ title && /* @__PURE__ */ jsx("h1", { className: "ff-print__title", children: title }),
2774
+ subtitle && /* @__PURE__ */ jsx("p", { className: "ff-print__subtitle", children: subtitle })
2775
+ ] }),
2776
+ /* @__PURE__ */ jsx("ul", { className: "ff-print__fields", children: fields.map((field) => /* @__PURE__ */ jsx(
2777
+ PrintField,
2778
+ {
2779
+ field,
2780
+ mode: resolvedMode
2781
+ },
2782
+ field.name
2783
+ )) }),
2784
+ footer && /* @__PURE__ */ jsx("div", { className: "ff-print__footer", children: footer })
2785
+ ] });
2786
+ }
2787
+ function PrintField({ field, mode }) {
2788
+ const isTextarea = field.type === "textarea";
2789
+ const label = field.label || field.name;
2790
+ return /* @__PURE__ */ jsxs("li", { className: `ff-print__field${isTextarea ? " ff-print__field--textarea" : ""}`, children: [
2791
+ /* @__PURE__ */ jsxs("span", { className: "ff-print__label", children: [
2792
+ label,
2793
+ ":"
2794
+ ] }),
2795
+ /* @__PURE__ */ jsx("span", { className: "ff-print__value", children: mode === "blank" ? renderBlank(field) : renderFilled(field) })
2796
+ ] });
2797
+ }
2798
+ function renderBlank(field) {
2799
+ switch (field.type) {
2800
+ case "checkbox":
2801
+ case "switch":
2802
+ return /* @__PURE__ */ jsx("span", { className: "ff-print__blank-box" });
2803
+ case "radio":
2804
+ return /* @__PURE__ */ jsx("span", { className: "ff-print__blank-options", children: (field.options ?? []).map((opt) => /* @__PURE__ */ jsxs("span", { className: "ff-print__blank-option", children: [
2805
+ /* @__PURE__ */ jsx("span", { className: "ff-print__blank-radio" }),
2806
+ opt.label
2807
+ ] }, opt.value)) });
2808
+ case "select":
2809
+ return /* @__PURE__ */ jsx("span", { className: "ff-print__blank-options", children: (field.options ?? []).map((opt) => /* @__PURE__ */ jsxs("span", { className: "ff-print__blank-option", children: [
2810
+ /* @__PURE__ */ jsx("span", { className: "ff-print__blank-box" }),
2811
+ opt.label
2812
+ ] }, opt.value)) });
2813
+ case "textarea":
2814
+ return /* @__PURE__ */ jsx("div", { className: "ff-print__blank-textarea" });
2815
+ case "checkboxgroup":
2816
+ return /* @__PURE__ */ jsx("span", { className: "ff-print__blank-options", children: (field.options ?? []).map((opt) => /* @__PURE__ */ jsxs("span", { className: "ff-print__blank-option", children: [
2817
+ /* @__PURE__ */ jsx("span", { className: "ff-print__blank-box" }),
2818
+ opt.label
2819
+ ] }, opt.value)) });
2820
+ // text, email, password, url, tel, number, date, color, slider, file, hidden
2821
+ default:
2822
+ return /* @__PURE__ */ jsx("span", { className: "ff-print__blank-line" });
2823
+ }
2824
+ }
2825
+ function renderFilled(field) {
2826
+ const { value, type } = field;
2827
+ switch (type) {
2828
+ case "checkbox":
2829
+ case "switch":
2830
+ return /* @__PURE__ */ jsx("span", { className: "ff-print__filled-check", children: value ? "\u2611" : "\u2610" });
2831
+ case "radio":
2832
+ case "select": {
2833
+ const selected = field.options?.find(
2834
+ (opt) => String(opt.value) === String(value)
2835
+ );
2836
+ return /* @__PURE__ */ jsx("span", { className: "ff-print__filled-value", children: selected?.label ?? String(value ?? "\u2014") });
2837
+ }
2838
+ case "checkboxgroup": {
2839
+ const selectedValues = Array.isArray(value) ? value : [];
2840
+ const selectedLabels = (field.options ?? []).filter((opt) => selectedValues.includes(opt.value)).map((opt) => opt.label);
2841
+ return /* @__PURE__ */ jsx("span", { className: "ff-print__filled-value", children: selectedLabels.length > 0 ? selectedLabels.join(", ") : "\u2014" });
2842
+ }
2843
+ case "textarea":
2844
+ return /* @__PURE__ */ jsx("div", { className: "ff-print__filled-textarea", children: String(value ?? "") });
2845
+ case "file":
2846
+ return /* @__PURE__ */ jsx("span", { className: "ff-print__filled-value", children: value ? String(value) : "No file selected" });
2847
+ case "color":
2848
+ return /* @__PURE__ */ jsx("span", { className: "ff-print__filled-value", children: value ? /* @__PURE__ */ jsxs(Fragment, { children: [
2849
+ /* @__PURE__ */ jsx(
2850
+ "span",
2851
+ {
2852
+ style: {
2853
+ display: "inline-block",
2854
+ width: 14,
2855
+ height: 14,
2856
+ backgroundColor: String(value),
2857
+ border: "1px solid #999",
2858
+ verticalAlign: "middle",
2859
+ marginRight: 6,
2860
+ borderRadius: 2
2861
+ }
2862
+ }
2863
+ ),
2864
+ String(value)
2865
+ ] }) : "\u2014" });
2866
+ case "hidden":
2867
+ return null;
2868
+ // text, email, password, url, tel, number, date, slider
2869
+ default:
2870
+ return /* @__PURE__ */ jsx("span", { className: "ff-print__filled-value", children: value != null && value !== "" ? String(value) : "\u2014" });
2871
+ }
2872
+ }
2873
+ function PrintOverlay({
2874
+ fields,
2875
+ mode,
2876
+ title,
2877
+ subtitle,
2878
+ footer,
2879
+ className,
2880
+ open = true
2881
+ }) {
2882
+ if (!open || fields.length === 0) return null;
2883
+ return /* @__PURE__ */ jsx(
2884
+ "div",
2885
+ {
2886
+ "data-formfoundry-print-root": "",
2887
+ style: { display: "none" },
2888
+ children: /* @__PURE__ */ jsx(
2889
+ FormPrintView,
2890
+ {
2891
+ fields,
2892
+ mode,
2893
+ title,
2894
+ subtitle,
2895
+ footer,
2896
+ className
2897
+ }
2898
+ )
2899
+ }
2900
+ );
2901
+ }
2902
+
2903
+ export { EXPRESSION_PREFIX, Form, FormFoundryProvider, FormMetaContext, FormPrintButton, FormPrintView, FormValuesContext, InputRegistryContext, PrintOverlay, SchemaRenderer, applyPlugins, builtInRules, clearGlobalRegistry, clearRegistryListeners, createAutoTrimPlugin, createDebugPlugin, createDefaultRegistry, createEventEmitter, createHooks, createLedger, createMessage, createMessageStore, createNode, createRegistry, defaultMessages, en as enLocale, evaluateExpression, getActiveLocale, getLocale, getNode, getRegisteredLocales, getRegisteredNodeIds, getRule, hasRule, interpolate, isExpression, isVariableRef, parseRules, registerLocale, registerNode, registerRule, resolveConfig, resolveLocaleMessage, resolveMessage, resolveSchemaValue, resolveVariableRef, runValidation, setActiveLocale, subscribeToRegistry, unregisterNode, unregisterRule, useFormContext, useFormFoundryField, useFormMeta, useFormNode, useFormNodes, useFormPrint, useFormValues, useInputRegistry, validateFormWithZod, validateWithZod, zodResolver };
2904
+ //# sourceMappingURL=index.js.map
2905
+ //# sourceMappingURL=index.js.map