@gravionlabs/helix-zod 22.0.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.
@@ -0,0 +1,974 @@
1
+ import { NgComponentOutlet } from '@angular/common';
2
+ import * as i0 from '@angular/core';
3
+ import { InjectionToken, inject, Injectable, input, computed, ChangeDetectionStrategy, Component, output, Injector, signal, untracked, Directive, model, makeEnvironmentProviders } from '@angular/core';
4
+ import { validateStandardSchema, hidden, disabled, readonly, required, applyEach, applyWhenValue, form, submit, FormField, transformedValue } from '@angular/forms/signals';
5
+ import { z, ZodIssueCode } from 'zod';
6
+ import { ValidatorKey } from '@gravionlabs/helix-core/validators';
7
+ import { HelixFormField, HelixSelect } from '@gravionlabs/helix-shell';
8
+
9
+ /**
10
+ * Multi-provider token holding widget registrations. Later registrations win,
11
+ * so consumer widgets registered after the built-ins override them.
12
+ */
13
+ const HELIX_DYNAMIC_FIELD_WIDGETS = new InjectionToken('HELIX_DYNAMIC_FIELD_WIDGETS');
14
+ /** Optional app-level error message resolver used by all built-in widgets. */
15
+ const HELIX_ERROR_MESSAGE_RESOLVER = new InjectionToken('HELIX_ERROR_MESSAGE_RESOLVER');
16
+ /** Resolves the widget component registered for a widget kind. */
17
+ class HelixFieldWidgetResolver {
18
+ #registrations = inject(HELIX_DYNAMIC_FIELD_WIDGETS, { optional: true }) ?? [];
19
+ resolve(widget) {
20
+ const flat = this.#registrations.flat();
21
+ for (let i = flat.length - 1; i >= 0; i--) {
22
+ if (flat[i].widget === widget)
23
+ return flat[i].component;
24
+ }
25
+ throw new Error(`[HelixFieldWidgetResolver] No widget registered for kind "${widget}". ` +
26
+ (typeof ngDevMode !== 'undefined' && ngDevMode
27
+ ? `Did you forget provideHelixDynamicForms() or a custom { widgets: [...] } registration?`
28
+ : ''));
29
+ }
30
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: HelixFieldWidgetResolver, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
31
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: HelixFieldWidgetResolver, providedIn: 'root' });
32
+ }
33
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: HelixFieldWidgetResolver, decorators: [{
34
+ type: Injectable,
35
+ args: [{ providedIn: 'root' }]
36
+ }] });
37
+
38
+ /**
39
+ * Dispatches one field descriptor to its registered widget component.
40
+ *
41
+ * Handles `hidden()` centrally: a hidden field renders nothing, per the
42
+ * signal-forms guidance to `@if` on the hidden state.
43
+ *
44
+ * Imports no widget statically — composite widgets (object/array/union) import
45
+ * this component, and built-in registrations live in
46
+ * `provideHelixDynamicForms()`, which breaks the import cycle.
47
+ */
48
+ class HelixDynamicField {
49
+ field = input.required(/* @ts-ignore */
50
+ ...(ngDevMode ? [{ debugName: "field" }] : /* istanbul ignore next */ []));
51
+ descriptor = input.required(/* @ts-ignore */
52
+ ...(ngDevMode ? [{ debugName: "descriptor" }] : /* istanbul ignore next */ []));
53
+ #resolver = inject(HelixFieldWidgetResolver);
54
+ component = computed(() => this.#resolver.resolve(this.descriptor().widget), /* @ts-ignore */
55
+ ...(ngDevMode ? [{ debugName: "component" }] : /* istanbul ignore next */ []));
56
+ outletInputs = computed(() => ({
57
+ field: this.field(),
58
+ descriptor: this.descriptor(),
59
+ }), /* @ts-ignore */
60
+ ...(ngDevMode ? [{ debugName: "outletInputs" }] : /* istanbul ignore next */ []));
61
+ hiddenState = computed(() => this.field()().hidden(), /* @ts-ignore */
62
+ ...(ngDevMode ? [{ debugName: "hiddenState" }] : /* istanbul ignore next */ []));
63
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: HelixDynamicField, deps: [], target: i0.ɵɵFactoryTarget.Component });
64
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.6", type: HelixDynamicField, isStandalone: true, selector: "helix-dynamic-field", inputs: { field: { classPropertyName: "field", publicName: "field", isSignal: true, isRequired: true, transformFunction: null }, descriptor: { classPropertyName: "descriptor", publicName: "descriptor", isSignal: true, isRequired: true, transformFunction: null } }, ngImport: i0, template: "@if (!hiddenState()) {\n <ng-container [ngComponentOutlet]=\"component()\" [ngComponentOutletInputs]=\"outletInputs()\" />\n}\n", styles: [":host{display:block;width:100%}\n"], dependencies: [{ kind: "directive", type: NgComponentOutlet, selector: "[ngComponentOutlet]", inputs: ["ngComponentOutlet", "ngComponentOutletInputs", "ngComponentOutletInjector", "ngComponentOutletEnvironmentInjector", "ngComponentOutletContent", "ngComponentOutletNgModule"], exportAs: ["ngComponentOutlet"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
65
+ }
66
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: HelixDynamicField, decorators: [{
67
+ type: Component,
68
+ args: [{ selector: 'helix-dynamic-field', standalone: true, imports: [NgComponentOutlet], changeDetection: ChangeDetectionStrategy.OnPush, template: "@if (!hiddenState()) {\n <ng-container [ngComponentOutlet]=\"component()\" [ngComponentOutletInputs]=\"outletInputs()\" />\n}\n", styles: [":host{display:block;width:100%}\n"] }]
69
+ }], propDecorators: { field: [{ type: i0.Input, args: [{ isSignal: true, alias: "field", required: true }] }], descriptor: [{ type: i0.Input, args: [{ isSignal: true, alias: "descriptor", required: true }] }] } });
70
+
71
+ /**
72
+ * Builds the signal-forms schema for a Zod-driven dynamic form:
73
+ *
74
+ * 1. Root-level `validateStandardSchema(rootPath, zodSchema)` — Zod issue
75
+ * paths route errors onto the matching child fields, including cross-field
76
+ * `.refine()` errors with a `path`.
77
+ * 2. Conditional field state from {@link HelixFieldMeta} predicates
78
+ * (`hiddenWhen`/`disabledWhen`/`readonlyWhen`/`requiredWhen`), evaluated
79
+ * against the root form value.
80
+ * 3. `meta.extraSchema` escape-hatch rules per field.
81
+ * 4. `applyEach` for array items and `applyWhenValue` for discriminated-union
82
+ * variants, so variant/item rules stay dormant unless active.
83
+ */
84
+ function buildHelixSchema(zodSchema, root) {
85
+ return (rootPath) => {
86
+ validateStandardSchema(rootPath, zodSchema);
87
+ applyDescriptorRules(rootPath, root, rootPath);
88
+ };
89
+ }
90
+ /** Reads the root form value from any nested logic context. */
91
+ function rootValueOf(ctx, rootPath) {
92
+ return ctx.valueOf(rootPath);
93
+ }
94
+ function applyConditionRules(path, descriptor, rootPath) {
95
+ const meta = descriptor.meta;
96
+ if (meta.hiddenWhen) {
97
+ hidden(path, (ctx) => !!meta.hiddenWhen?.(rootValueOf(ctx, rootPath)));
98
+ }
99
+ if (meta.disabledWhen) {
100
+ disabled(path, (ctx) => meta.disabledWhen?.(rootValueOf(ctx, rootPath)) ?? false);
101
+ }
102
+ if (meta.readonlyWhen) {
103
+ readonly(path, (ctx) => !!meta.readonlyWhen?.(rootValueOf(ctx, rootPath)));
104
+ }
105
+ if (meta.requiredWhen) {
106
+ required(path, {
107
+ when: (ctx) => !!meta.requiredWhen?.(rootValueOf(ctx, rootPath)),
108
+ });
109
+ }
110
+ meta.extraSchema?.(path);
111
+ }
112
+ function applyDescriptorRules(path, descriptor, rootPath) {
113
+ // Root-level conditions make no sense on the form itself — only on fields.
114
+ if (path !== rootPath)
115
+ applyConditionRules(path, descriptor, rootPath);
116
+ for (const child of descriptor.children ?? []) {
117
+ applyDescriptorRules(path[child.key], child, rootPath);
118
+ }
119
+ if (descriptor.itemDescriptor) {
120
+ const item = descriptor.itemDescriptor;
121
+ applyEach(path, ((itemPath) => {
122
+ applyDescriptorRules(itemPath, item, rootPath);
123
+ }));
124
+ }
125
+ if (descriptor.union) {
126
+ const { discriminator, variants } = descriptor.union;
127
+ for (const [tag, variant] of variants) {
128
+ applyWhenValue(path, ((value) => value?.[discriminator] === tag), ((variantPath) => {
129
+ // The union field itself carries the variant's children.
130
+ for (const child of variant.children ?? []) {
131
+ applyDescriptorRules(variantPath[child.key], child, rootPath);
132
+ }
133
+ }));
134
+ }
135
+ }
136
+ }
137
+
138
+ const defOf$1 = (schema) => schema._zod.def;
139
+ /**
140
+ * Builds a fully-populated initial model value for a Zod schema — signal forms
141
+ * require a concrete model shape up front.
142
+ *
143
+ * Rules: `.default()` wins; strings → `''`; numbers/dates → `null` (paired
144
+ * with parse-transforming widgets); booleans → `false`; enums → first option;
145
+ * arrays → `[]`; objects → recursed shape; discriminated unions → default of
146
+ * the first variant; `optional`/`nullable` leaves without a more specific
147
+ * default → `undefined` / `null`.
148
+ */
149
+ function buildDefaultValue(schema) {
150
+ const def = defOf$1(schema);
151
+ switch (def.type) {
152
+ case 'default': {
153
+ const dv = def['defaultValue'];
154
+ return typeof dv === 'function' ? dv() : dv;
155
+ }
156
+ case 'optional': {
157
+ // Optional leaves default to undefined; optional containers still need a
158
+ // concrete shape so the field tree has navigable paths.
159
+ const inner = schema.unwrap();
160
+ return isLeaf(inner) ? undefined : buildDefaultValue(inner);
161
+ }
162
+ case 'readonly':
163
+ case 'nonoptional':
164
+ return buildDefaultValue(schema.unwrap());
165
+ case 'nullable':
166
+ return null;
167
+ case 'string':
168
+ return '';
169
+ case 'number':
170
+ case 'int':
171
+ case 'date':
172
+ return null;
173
+ case 'boolean':
174
+ return false;
175
+ case 'enum': {
176
+ const options = schema.options;
177
+ return options[0];
178
+ }
179
+ case 'literal': {
180
+ const values = def['values'];
181
+ return values[0];
182
+ }
183
+ case 'array':
184
+ return [];
185
+ case 'object': {
186
+ const shape = schema.shape;
187
+ const value = {};
188
+ for (const [key, child] of Object.entries(shape)) {
189
+ value[key] = buildDefaultValue(child);
190
+ }
191
+ return value;
192
+ }
193
+ case 'union': {
194
+ const options = schema.options;
195
+ return buildDefaultValue(options[0]);
196
+ }
197
+ default:
198
+ return undefined;
199
+ }
200
+ }
201
+ function isLeaf(schema) {
202
+ const type = defOf$1(schema).type;
203
+ return type !== 'object' && type !== 'array' && type !== 'union';
204
+ }
205
+
206
+ /**
207
+ * Typed registry backing {@link helixMeta}. Checked before the zod global
208
+ * registry by {@link readHelixMeta}.
209
+ */
210
+ const helixFieldMetaRegistry = z.registry();
211
+ /**
212
+ * Attaches Helix UI metadata to a Zod schema and returns the **same** instance.
213
+ *
214
+ * Prefer this over `.meta()`, which clones the schema — metadata must live on
215
+ * the exact instance composed into the `z.object()` shape for the walker to
216
+ * find it.
217
+ *
218
+ * @example
219
+ * const UserSchema = z.object({
220
+ * email: helixMeta(z.email(), { label: 'E-mail', placeholder: 'you@example.com' }),
221
+ * });
222
+ */
223
+ function helixMeta(schema, meta) {
224
+ helixFieldMetaRegistry.add(schema, meta);
225
+ return schema;
226
+ }
227
+ const WRAPPER_TYPES$1 = new Set(['optional', 'nullable', 'default', 'readonly', 'nonoptional']);
228
+ function metaOf(schema) {
229
+ const registered = helixFieldMetaRegistry.get(schema);
230
+ if (registered)
231
+ return registered;
232
+ const globalMeta = schema.meta();
233
+ if (!globalMeta)
234
+ return undefined;
235
+ const { title, description, helix, ...rest } = globalMeta;
236
+ const mapped = { ...rest };
237
+ if (title !== undefined)
238
+ mapped.label = title;
239
+ if (description !== undefined)
240
+ mapped.hint = description;
241
+ return { ...mapped, ...helix };
242
+ }
243
+ /**
244
+ * Reads {@link HelixFieldMeta} from a Zod schema: the typed registry first,
245
+ * then the zod global registry (`.meta()` — `title` → `label`,
246
+ * `description` → `hint`, `helix` key merged on top).
247
+ *
248
+ * Wrapper types (`optional`, `nullable`, `default`, `readonly`) are unwrapped
249
+ * and their metadata merged — outermost wins.
250
+ */
251
+ function readHelixMeta(schema) {
252
+ const layers = [];
253
+ let current = schema;
254
+ while (current) {
255
+ const meta = metaOf(current);
256
+ if (meta)
257
+ layers.push(meta);
258
+ const def = current._zod.def;
259
+ current = WRAPPER_TYPES$1.has(def.type) ? current.unwrap() : undefined;
260
+ }
261
+ // Innermost first so outer layers overwrite inner ones.
262
+ return layers.reverse().reduce((acc, layer) => ({ ...acc, ...layer }), {});
263
+ }
264
+
265
+ const WRAPPER_TYPES = new Set(['optional', 'nullable', 'default', 'readonly', 'nonoptional']);
266
+ /** Native `<input type>` per input-based widget kind. */
267
+ const INPUT_TYPES = {
268
+ text: 'text',
269
+ email: 'email',
270
+ password: 'password',
271
+ number: 'number',
272
+ date: 'date',
273
+ };
274
+ function defOf(schema) {
275
+ return schema._zod.def;
276
+ }
277
+ /** Peels optional/nullable/default/readonly wrappers, tracking requiredness. */
278
+ function unwrap(schema) {
279
+ let inner = schema;
280
+ let required = true;
281
+ while (WRAPPER_TYPES.has(defOf(inner).type)) {
282
+ const type = defOf(inner).type;
283
+ if (type === 'optional' || type === 'nullable' || type === 'default')
284
+ required = false;
285
+ if (type === 'nonoptional')
286
+ required = true;
287
+ inner = inner.unwrap();
288
+ }
289
+ return { inner, required };
290
+ }
291
+ function normalizeOptions(options) {
292
+ if (!options)
293
+ return undefined;
294
+ return options.map((o) => typeof o === 'string' ? { label: o, value: o } : o);
295
+ }
296
+ function inferWidget(inner) {
297
+ const def = defOf(inner);
298
+ switch (def.type) {
299
+ case 'string':
300
+ return def['format'] === 'email' ? 'email' : 'text';
301
+ case 'number':
302
+ case 'int':
303
+ return 'number';
304
+ case 'boolean':
305
+ return 'checkbox';
306
+ case 'date':
307
+ return 'date';
308
+ case 'enum':
309
+ case 'literal':
310
+ return 'select';
311
+ case 'array':
312
+ return 'array';
313
+ case 'object':
314
+ return 'object';
315
+ case 'union':
316
+ return def['discriminator'] !== undefined ? 'union' : null;
317
+ default:
318
+ return null;
319
+ }
320
+ }
321
+ function enumOptions(inner) {
322
+ const def = defOf(inner);
323
+ if (def.type === 'enum') {
324
+ const values = inner.options;
325
+ return values.map((v) => ({ label: String(v), value: v }));
326
+ }
327
+ if (def.type === 'literal') {
328
+ const values = def['values'];
329
+ return values.map((v) => ({ label: String(v), value: v }));
330
+ }
331
+ return undefined;
332
+ }
333
+ /** Reads the discriminator literal of a union variant's tag field. */
334
+ function discriminatorValue(variant, discriminator) {
335
+ const shape = variant.shape;
336
+ const tagDef = defOf(unwrap(shape[discriminator]).inner);
337
+ const values = tagDef['values'];
338
+ if (!values?.length) {
339
+ throw new Error(`[zodToFieldDescriptors] Discriminated union variant has no literal value for ` +
340
+ `discriminator "${discriminator}".`);
341
+ }
342
+ return values[0];
343
+ }
344
+ function walk(schema, key, path) {
345
+ const meta = readHelixMeta(schema);
346
+ const { inner, required } = unwrap(schema);
347
+ const def = defOf(inner);
348
+ const inferred = inferWidget(inner);
349
+ const widget = meta.widget ?? inferred;
350
+ if (widget === null || widget === undefined) {
351
+ if (typeof ngDevMode !== 'undefined' && ngDevMode) {
352
+ throw new Error(`[zodToFieldDescriptors] Zod type "${def.type}" at path "${path.join('.')}" has no ` +
353
+ `automatic widget mapping. Set { widget: '...' } via helixMeta() to render it with a ` +
354
+ `registered (custom) widget.`);
355
+ }
356
+ // Production: render nothing rather than crash — fall through as plain text.
357
+ return {
358
+ key,
359
+ path,
360
+ widget: 'text',
361
+ meta,
362
+ zodType: inner,
363
+ zodSource: schema,
364
+ required,
365
+ inputType: 'text',
366
+ };
367
+ }
368
+ const descriptor = {
369
+ key,
370
+ path,
371
+ widget,
372
+ meta,
373
+ zodType: inner,
374
+ zodSource: schema,
375
+ required,
376
+ inputType: INPUT_TYPES[widget],
377
+ options: normalizeOptions(meta.options) ?? enumOptions(inner),
378
+ };
379
+ if (def.type === 'object') {
380
+ const shape = inner.shape;
381
+ const children = Object.entries(shape).map(([childKey, childSchema], index) => ({
382
+ index,
383
+ descriptor: walk(childSchema, childKey, [...path, childKey]),
384
+ }));
385
+ children.sort((a, b) => (a.descriptor.meta.order ?? Number.MAX_SAFE_INTEGER) -
386
+ (b.descriptor.meta.order ?? Number.MAX_SAFE_INTEGER) || a.index - b.index);
387
+ descriptor.children = children.map((c) => c.descriptor);
388
+ }
389
+ if (def.type === 'array') {
390
+ descriptor.itemDescriptor = walk(def['element'], '', path);
391
+ }
392
+ if (widget === 'union' && def.type === 'union') {
393
+ const discriminator = def['discriminator'];
394
+ const variants = new Map();
395
+ for (const variant of inner.options) {
396
+ variants.set(discriminatorValue(variant, discriminator), walk(variant, key, path));
397
+ }
398
+ descriptor.union = { discriminator, variants };
399
+ }
400
+ return descriptor;
401
+ }
402
+ /**
403
+ * Walks an annotated `z.object()` schema and produces the recursive
404
+ * {@link HelixFieldDescriptor} tree consumed by the dynamic form renderer.
405
+ *
406
+ * Widget inference: string→text (`z.email()`→email), number→number,
407
+ * boolean→checkbox, date→date, enum/literal→select, array→array,
408
+ * object→object, discriminated union→union. Anything else requires a
409
+ * `widget` override in {@link HelixFieldMeta} (throws in dev mode otherwise).
410
+ */
411
+ function zodToFieldDescriptors(schema) {
412
+ return walk(schema, '', []);
413
+ }
414
+
415
+ /**
416
+ * Renders a complete signal form from an annotated Zod object schema.
417
+ *
418
+ * - Validation: root-level `validateStandardSchema` — Zod issues are routed to
419
+ * the matching fields.
420
+ * - Model: pass your own `WritableSignal` via `model`, or let the component
421
+ * derive an initial value from the schema (`buildDefaultValue`).
422
+ * - Swapping the `schema` input recreates the form and resets its state.
423
+ * - `submitted` emits the **parsed** (`schema.parse`) value on valid submit.
424
+ *
425
+ * Requires `provideHelixDynamicForms()` in the injector chain.
426
+ */
427
+ class HelixDynamicForm {
428
+ schema = input.required(/* @ts-ignore */
429
+ ...(ngDevMode ? [{ debugName: "schema" }] : /* istanbul ignore next */ []));
430
+ /** Optional external model — omit to derive the initial value from the schema. */
431
+ model = input(/* @ts-ignore */
432
+ ...(ngDevMode ? [undefined, { debugName: "model" }] : /* istanbul ignore next */ []));
433
+ submitLabel = input('Submit', /* @ts-ignore */
434
+ ...(ngDevMode ? [{ debugName: "submitLabel" }] : /* istanbul ignore next */ []));
435
+ showSubmit = input(true, /* @ts-ignore */
436
+ ...(ngDevMode ? [{ debugName: "showSubmit" }] : /* istanbul ignore next */ []));
437
+ submitted = output();
438
+ #injector = inject(Injector);
439
+ descriptors = computed(() => zodToFieldDescriptors(this.schema()), /* @ts-ignore */
440
+ ...(ngDevMode ? [{ debugName: "descriptors" }] : /* istanbul ignore next */ []));
441
+ #model = computed(() => this.model() ?? signal(buildDefaultValue(this.schema())), /* @ts-ignore */
442
+ ...(ngDevMode ? [{ debugName: "#model" }] : /* istanbul ignore next */ []));
443
+ /** The form's `FieldTree` — recreated (state reset) when the schema changes. */
444
+ formTree = computed(() => {
445
+ const model = this.#model();
446
+ const schema = this.schema();
447
+ const root = this.descriptors();
448
+ return untracked(() => form(model, buildHelixSchema(schema, root), { injector: this.#injector }));
449
+ }, /* @ts-ignore */
450
+ ...(ngDevMode ? [{ debugName: "formTree" }] : /* istanbul ignore next */ []));
451
+ childField(descriptor) {
452
+ return this.formTree()[descriptor.key];
453
+ }
454
+ async handleSubmit(event) {
455
+ event.preventDefault();
456
+ await submit(this.formTree(), async (tree) => {
457
+ this.submitted.emit(this.schema().parse(tree().value()));
458
+ return undefined;
459
+ });
460
+ }
461
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: HelixDynamicForm, deps: [], target: i0.ɵɵFactoryTarget.Component });
462
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.6", type: HelixDynamicForm, isStandalone: true, selector: "helix-dynamic-form", inputs: { schema: { classPropertyName: "schema", publicName: "schema", isSignal: true, isRequired: true, transformFunction: null }, model: { classPropertyName: "model", publicName: "model", isSignal: true, isRequired: false, transformFunction: null }, submitLabel: { classPropertyName: "submitLabel", publicName: "submitLabel", isSignal: true, isRequired: false, transformFunction: null }, showSubmit: { classPropertyName: "showSubmit", publicName: "showSubmit", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { submitted: "submitted" }, ngImport: i0, template: "<form class=\"helix-dynamic-form\" novalidate (submit)=\"handleSubmit($event)\">\n @for (child of descriptors().children ?? []; track child.key) {\n <helix-dynamic-field [field]=\"childField(child)\" [descriptor]=\"child\" />\n }\n\n @if (showSubmit()) {\n <button\n type=\"submit\"\n class=\"helix-dynamic-form__submit\"\n [disabled]=\"formTree()().submitting()\"\n >\n {{ submitLabel() }}\n </button>\n }\n</form>\n", styles: [":host{display:block;width:100%}.helix-dynamic-form{display:flex;flex-direction:column;gap:1rem}.helix-dynamic-form__submit{align-self:flex-start;padding:.5rem 1.25rem;font-family:inherit;font-size:1rem;color:var(--h-primary-contrast-color);background-color:var(--h-primary-color);border:1px solid var(--h-primary-color);border-radius:var(--h-border-radius-md);cursor:pointer;transition:background-color .2s}.helix-dynamic-form__submit:hover:not(:disabled){background-color:var(--h-primary-hover-color)}.helix-dynamic-form__submit:disabled{cursor:default;opacity:.6}\n"], dependencies: [{ kind: "component", type: HelixDynamicField, selector: "helix-dynamic-field", inputs: ["field", "descriptor"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
463
+ }
464
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: HelixDynamicForm, decorators: [{
465
+ type: Component,
466
+ args: [{ selector: 'helix-dynamic-form', standalone: true, imports: [HelixDynamicField], changeDetection: ChangeDetectionStrategy.OnPush, template: "<form class=\"helix-dynamic-form\" novalidate (submit)=\"handleSubmit($event)\">\n @for (child of descriptors().children ?? []; track child.key) {\n <helix-dynamic-field [field]=\"childField(child)\" [descriptor]=\"child\" />\n }\n\n @if (showSubmit()) {\n <button\n type=\"submit\"\n class=\"helix-dynamic-form__submit\"\n [disabled]=\"formTree()().submitting()\"\n >\n {{ submitLabel() }}\n </button>\n }\n</form>\n", styles: [":host{display:block;width:100%}.helix-dynamic-form{display:flex;flex-direction:column;gap:1rem}.helix-dynamic-form__submit{align-self:flex-start;padding:.5rem 1.25rem;font-family:inherit;font-size:1rem;color:var(--h-primary-contrast-color);background-color:var(--h-primary-color);border:1px solid var(--h-primary-color);border-radius:var(--h-border-radius-md);cursor:pointer;transition:background-color .2s}.helix-dynamic-form__submit:hover:not(:disabled){background-color:var(--h-primary-hover-color)}.helix-dynamic-form__submit:disabled{cursor:default;opacity:.6}\n"] }]
467
+ }], propDecorators: { schema: [{ type: i0.Input, args: [{ isSignal: true, alias: "schema", required: true }] }], model: [{ type: i0.Input, args: [{ isSignal: true, alias: "model", required: false }] }], submitLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "submitLabel", required: false }] }], showSubmit: [{ type: i0.Input, args: [{ isSignal: true, alias: "showSubmit", required: false }] }], submitted: [{ type: i0.Output, args: ["submitted"] }] } });
468
+
469
+ // Mirrors Validators' internal EMPTY_VALUES — defined locally to avoid
470
+ // depending on unexported internals of @gravionlabs/helix-core.
471
+ const EMPTY_VALUES = ['', null, undefined];
472
+ const isEmptyValue = (v) => EMPTY_VALUES.includes(v);
473
+ /**
474
+ * Maps a single `ZodIssue` to the corresponding `ValidatorKey`.
475
+ *
476
+ * NOTE: Zod v4 breaking changes accounted for here:
477
+ * - `invalid_string` → `invalid_format` with `format` property (not `validation`)
478
+ * - `too_small`/`too_big` use `origin` instead of `type`
479
+ * - `not_integer` removed → `invalid_type` with `expected: 'int'`
480
+ * - `invalid_type` for null/undefined no longer has a `received` field;
481
+ * detected via the `value` parameter instead
482
+ *
483
+ * Returns `null` for issue codes with no automatic mapping — caller handles
484
+ * these via `fallbackKey` (Option B).
485
+ */
486
+ function zodIssueToHelixKey(issue, value) {
487
+ switch (issue.code) {
488
+ case ZodIssueCode.invalid_type:
489
+ // Zod v4 dropped the `received` field on invalid_type issues.
490
+ // Detect required (null/undefined) errors by checking the control value directly.
491
+ if (isEmptyValue(value))
492
+ return ValidatorKey.Required;
493
+ // integer: Zod v4 changed not_integer → invalid_type { expected: 'int' }
494
+ if (issue.expected === 'int')
495
+ return ValidatorKey.Integer;
496
+ // numeric type mismatch
497
+ if (issue.expected === 'number' ||
498
+ issue.expected === 'float') {
499
+ return ValidatorKey.Number;
500
+ }
501
+ return null;
502
+ case ZodIssueCode.invalid_format:
503
+ // Zod v4: replaces invalid_string for all format-based string validations.
504
+ // The `format` property holds the validation name.
505
+ {
506
+ const fmt = issue.format;
507
+ if (fmt === 'email')
508
+ return ValidatorKey.Email;
509
+ if (fmt === 'regex')
510
+ return ValidatorKey.Pattern;
511
+ if (fmt === 'datetime' || fmt === 'date' || fmt === 'time')
512
+ return ValidatorKey.Date;
513
+ }
514
+ return null;
515
+ case ZodIssueCode.too_small:
516
+ // Zod v4: uses `origin` instead of `type` to identify the validated construct.
517
+ {
518
+ const origin = issue.origin;
519
+ if (origin === 'string' || origin === 'array')
520
+ return ValidatorKey.MinLength;
521
+ if (origin === 'number')
522
+ return ValidatorKey.Min;
523
+ }
524
+ return null;
525
+ case ZodIssueCode.too_big:
526
+ {
527
+ const origin = issue.origin;
528
+ if (origin === 'string' || origin === 'array')
529
+ return ValidatorKey.MaxLength;
530
+ if (origin === 'number')
531
+ return ValidatorKey.Max;
532
+ }
533
+ return null;
534
+ case ZodIssueCode.custom:
535
+ return null; // must be handled via fallbackKey (Option B)
536
+ default:
537
+ return null;
538
+ }
539
+ }
540
+
541
+ /** Built-in signal-forms error kinds → ValidatorKey. */
542
+ const NG_KIND_TO_HELIX = {
543
+ required: ValidatorKey.Required,
544
+ min: ValidatorKey.Min,
545
+ max: ValidatorKey.Max,
546
+ minLength: ValidatorKey.MinLength,
547
+ maxLength: ValidatorKey.MaxLength,
548
+ pattern: ValidatorKey.Pattern,
549
+ email: ValidatorKey.Email,
550
+ };
551
+ /**
552
+ * Maps a signal-forms `ValidationError` to the `ValidatorKey` message
553
+ * convention shared with `HelixZodValidators.fromZod`.
554
+ *
555
+ * @param error The signal-forms validation error.
556
+ * @param value The current field value — used to distinguish Required from
557
+ * type errors on Zod `invalid_type` issues (Zod v4 dropped the
558
+ * `received` field).
559
+ */
560
+ function helixErrorKey(error, value) {
561
+ if (error.kind === 'standardSchema') {
562
+ const issue = error.issue;
563
+ return zodIssueToHelixKey(issue, value);
564
+ }
565
+ return NG_KIND_TO_HELIX[error.kind] ?? null;
566
+ }
567
+ function defaultMessage(error) {
568
+ if (error.kind === 'standardSchema') {
569
+ const issue = error.issue;
570
+ return issue.message ?? null;
571
+ }
572
+ return error.message ?? null;
573
+ }
574
+ /**
575
+ * Returns the message of the first displayable error, mirroring
576
+ * `HelixFormField`'s first-error convention.
577
+ *
578
+ * Order of precedence per error: `resolver` result → Zod issue message /
579
+ * signal-forms error message. Errors yielding no message are skipped.
580
+ */
581
+ function helixFirstErrorMessage(errors, options = {}) {
582
+ for (const error of errors) {
583
+ const resolved = options.resolver?.(error, helixErrorKey(error, options.value));
584
+ const message = resolved ?? defaultMessage(error);
585
+ if (message)
586
+ return message;
587
+ }
588
+ return null;
589
+ }
590
+
591
+ /**
592
+ * Base class for dynamic-form widgets. A widget is a standalone component with
593
+ * the two inputs below; `HelixDynamicField` instantiates it via
594
+ * `NgComponentOutlet` and feeds them in.
595
+ */
596
+ class HelixFieldWidgetBase {
597
+ field = input.required(/* @ts-ignore */
598
+ ...(ngDevMode ? [{ debugName: "field" }] : /* istanbul ignore next */ []));
599
+ descriptor = input.required(/* @ts-ignore */
600
+ ...(ngDevMode ? [{ debugName: "descriptor" }] : /* istanbul ignore next */ []));
601
+ #errorResolver = inject(HELIX_ERROR_MESSAGE_RESOLVER, { optional: true });
602
+ state = computed(() => this.field()(), /* @ts-ignore */
603
+ ...(ngDevMode ? [{ debugName: "state" }] : /* istanbul ignore next */ []));
604
+ /** Touched-gated first error message — mirrors `HelixFormField.activeError`. */
605
+ firstError = computed(() => {
606
+ const state = this.state();
607
+ if (!state.touched())
608
+ return null;
609
+ return helixFirstErrorMessage(state.errors(), {
610
+ value: state.value(),
611
+ resolver: this.#errorResolver ?? undefined,
612
+ });
613
+ }, /* @ts-ignore */
614
+ ...(ngDevMode ? [{ debugName: "firstError" }] : /* istanbul ignore next */ []));
615
+ label = computed(() => this.descriptor().meta.label ?? humanize(this.descriptor().key), /* @ts-ignore */
616
+ ...(ngDevMode ? [{ debugName: "label" }] : /* istanbul ignore next */ []));
617
+ hint = computed(() => this.descriptor().meta.hint, /* @ts-ignore */
618
+ ...(ngDevMode ? [{ debugName: "hint" }] : /* istanbul ignore next */ []));
619
+ placeholder = computed(() => this.descriptor().meta.placeholder ?? '', /* @ts-ignore */
620
+ ...(ngDevMode ? [{ debugName: "placeholder" }] : /* istanbul ignore next */ []));
621
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: HelixFieldWidgetBase, deps: [], target: i0.ɵɵFactoryTarget.Directive });
622
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.1.6", type: HelixFieldWidgetBase, isStandalone: true, inputs: { field: { classPropertyName: "field", publicName: "field", isSignal: true, isRequired: true, transformFunction: null }, descriptor: { classPropertyName: "descriptor", publicName: "descriptor", isSignal: true, isRequired: true, transformFunction: null } }, ngImport: i0 });
623
+ }
624
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: HelixFieldWidgetBase, decorators: [{
625
+ type: Directive
626
+ }], propDecorators: { field: [{ type: i0.Input, args: [{ isSignal: true, alias: "field", required: true }] }], descriptor: [{ type: i0.Input, args: [{ isSignal: true, alias: "descriptor", required: true }] }] } });
627
+ /** `firstName` → `First name` — fallback label when meta provides none. */
628
+ function humanize(key) {
629
+ if (!key)
630
+ return '';
631
+ const words = key
632
+ .replace(/([a-z0-9])([A-Z])/g, '$1 $2')
633
+ .replace(/[_-]+/g, ' ')
634
+ .toLowerCase();
635
+ return words.charAt(0).toUpperCase() + words.slice(1);
636
+ }
637
+
638
+ /** Built-in widget for `z.array()` fields — item list with add/remove. */
639
+ class HelixArrayWidget extends HelixFieldWidgetBase {
640
+ itemFields = computed(() => {
641
+ const length = (this.state().value() ?? []).length;
642
+ return Array.from({ length }, (_, index) => this.field()[index]);
643
+ }, /* @ts-ignore */
644
+ ...(ngDevMode ? [{ debugName: "itemFields" }] : /* istanbul ignore next */ []));
645
+ addLabel = computed(() => this.descriptor().meta.addLabel ?? 'Add', /* @ts-ignore */
646
+ ...(ngDevMode ? [{ debugName: "addLabel" }] : /* istanbul ignore next */ []));
647
+ removeLabel = computed(() => this.descriptor().meta.removeLabel ?? 'Remove', /* @ts-ignore */
648
+ ...(ngDevMode ? [{ debugName: "removeLabel" }] : /* istanbul ignore next */ []));
649
+ add() {
650
+ const item = this.descriptor().itemDescriptor;
651
+ if (!item)
652
+ return;
653
+ const value = buildDefaultValue(item.zodSource);
654
+ this.state().value.update((current) => [...(current ?? []), value]);
655
+ }
656
+ removeAt(index) {
657
+ this.state().value.update((current) => (current ?? []).filter((_, i) => i !== index));
658
+ }
659
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: HelixArrayWidget, deps: null, target: i0.ɵɵFactoryTarget.Component });
660
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.6", type: HelixArrayWidget, isStandalone: true, selector: "helix-array-widget", usesInheritance: true, ngImport: i0, template: "<div class=\"helix-array-widget\">\n @if (label(); as l) {\n <div class=\"helix-array-widget__label\">{{ l }}</div>\n }\n\n @for (itemField of itemFields(); track $index) {\n <div class=\"helix-array-widget__item\">\n <helix-dynamic-field [field]=\"itemField\" [descriptor]=\"descriptor().itemDescriptor!\" />\n <button\n type=\"button\"\n class=\"helix-array-widget__remove\"\n (click)=\"removeAt($index)\"\n >\n {{ removeLabel() }}\n </button>\n </div>\n }\n\n <button type=\"button\" class=\"helix-array-widget__add\" (click)=\"add()\">{{ addLabel() }}</button>\n\n @if (firstError(); as err) {\n <small class=\"helix-array-widget__message helix-array-widget__message--error\">{{ err }}</small>\n } @else if (hint(); as h) {\n <small class=\"helix-array-widget__message helix-array-widget__message--hint\">{{ h }}</small>\n }\n</div>\n", styles: [":host{display:block;width:100%}.helix-array-widget{display:flex;flex-direction:column;gap:.75rem}.helix-array-widget__label{font-size:.875rem;font-weight:500;color:var(--h-text-color)}.helix-array-widget__item{display:flex;align-items:flex-start;gap:.5rem}.helix-array-widget__item helix-dynamic-field{flex:1}.helix-array-widget__add,.helix-array-widget__remove{padding:.375rem .75rem;font-family:inherit;font-size:.875rem;color:var(--h-text-color);background-color:var(--h-content-background);border:1px solid var(--h-content-border-color);border-radius:var(--h-border-radius-md);cursor:pointer;transition:border-color .2s}.helix-array-widget__add:hover,.helix-array-widget__remove:hover{border-color:var(--h-primary-color)}.helix-array-widget__add{align-self:flex-start}.helix-array-widget__message{font-size:.75rem}.helix-array-widget__message--hint{color:var(--h-text-muted-color)}.helix-array-widget__message--error{color:var(--h-red-500)}\n"], dependencies: [{ kind: "component", type: HelixDynamicField, selector: "helix-dynamic-field", inputs: ["field", "descriptor"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
661
+ }
662
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: HelixArrayWidget, decorators: [{
663
+ type: Component,
664
+ args: [{ selector: 'helix-array-widget', standalone: true, imports: [HelixDynamicField], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"helix-array-widget\">\n @if (label(); as l) {\n <div class=\"helix-array-widget__label\">{{ l }}</div>\n }\n\n @for (itemField of itemFields(); track $index) {\n <div class=\"helix-array-widget__item\">\n <helix-dynamic-field [field]=\"itemField\" [descriptor]=\"descriptor().itemDescriptor!\" />\n <button\n type=\"button\"\n class=\"helix-array-widget__remove\"\n (click)=\"removeAt($index)\"\n >\n {{ removeLabel() }}\n </button>\n </div>\n }\n\n <button type=\"button\" class=\"helix-array-widget__add\" (click)=\"add()\">{{ addLabel() }}</button>\n\n @if (firstError(); as err) {\n <small class=\"helix-array-widget__message helix-array-widget__message--error\">{{ err }}</small>\n } @else if (hint(); as h) {\n <small class=\"helix-array-widget__message helix-array-widget__message--hint\">{{ h }}</small>\n }\n</div>\n", styles: [":host{display:block;width:100%}.helix-array-widget{display:flex;flex-direction:column;gap:.75rem}.helix-array-widget__label{font-size:.875rem;font-weight:500;color:var(--h-text-color)}.helix-array-widget__item{display:flex;align-items:flex-start;gap:.5rem}.helix-array-widget__item helix-dynamic-field{flex:1}.helix-array-widget__add,.helix-array-widget__remove{padding:.375rem .75rem;font-family:inherit;font-size:.875rem;color:var(--h-text-color);background-color:var(--h-content-background);border:1px solid var(--h-content-border-color);border-radius:var(--h-border-radius-md);cursor:pointer;transition:border-color .2s}.helix-array-widget__add:hover,.helix-array-widget__remove:hover{border-color:var(--h-primary-color)}.helix-array-widget__add{align-self:flex-start}.helix-array-widget__message{font-size:.75rem}.helix-array-widget__message--hint{color:var(--h-text-muted-color)}.helix-array-widget__message--error{color:var(--h-red-500)}\n"] }]
665
+ }] });
666
+
667
+ /** Built-in widget for boolean fields — checkbox with inline label. */
668
+ class HelixCheckboxWidget extends HelixFieldWidgetBase {
669
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: HelixCheckboxWidget, deps: null, target: i0.ɵɵFactoryTarget.Component });
670
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.6", type: HelixCheckboxWidget, isStandalone: true, selector: "helix-checkbox-widget", usesInheritance: true, ngImport: i0, template: "<div class=\"helix-checkbox-widget\">\n <label class=\"helix-checkbox-widget__label\">\n <input class=\"helix-checkbox-widget__input\" type=\"checkbox\" [formField]=\"field()\" />\n <span>{{ label() }}</span>\n </label>\n\n @if (firstError(); as err) {\n <small class=\"helix-checkbox-widget__message helix-checkbox-widget__message--error\">{{ err }}</small>\n } @else if (hint(); as h) {\n <small class=\"helix-checkbox-widget__message helix-checkbox-widget__message--hint\">{{ h }}</small>\n }\n</div>\n", styles: [":host{display:block;width:100%}.helix-checkbox-widget{display:flex;flex-direction:column;gap:.5rem}.helix-checkbox-widget__label{display:inline-flex;align-items:center;gap:.5rem;font-size:.875rem;color:var(--h-text-color);cursor:pointer}.helix-checkbox-widget__input{accent-color:var(--h-primary-color);width:1rem;height:1rem}.helix-checkbox-widget__message{font-size:.75rem}.helix-checkbox-widget__message--hint{color:var(--h-text-muted-color)}.helix-checkbox-widget__message--error{color:var(--h-red-500)}\n"], dependencies: [{ kind: "directive", type: FormField, selector: "[formField]", inputs: ["formField"], exportAs: ["formField"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
671
+ }
672
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: HelixCheckboxWidget, decorators: [{
673
+ type: Component,
674
+ args: [{ selector: 'helix-checkbox-widget', standalone: true, imports: [FormField], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"helix-checkbox-widget\">\n <label class=\"helix-checkbox-widget__label\">\n <input class=\"helix-checkbox-widget__input\" type=\"checkbox\" [formField]=\"field()\" />\n <span>{{ label() }}</span>\n </label>\n\n @if (firstError(); as err) {\n <small class=\"helix-checkbox-widget__message helix-checkbox-widget__message--error\">{{ err }}</small>\n } @else if (hint(); as h) {\n <small class=\"helix-checkbox-widget__message helix-checkbox-widget__message--hint\">{{ h }}</small>\n }\n</div>\n", styles: [":host{display:block;width:100%}.helix-checkbox-widget{display:flex;flex-direction:column;gap:.5rem}.helix-checkbox-widget__label{display:inline-flex;align-items:center;gap:.5rem;font-size:.875rem;color:var(--h-text-color);cursor:pointer}.helix-checkbox-widget__input{accent-color:var(--h-primary-color);width:1rem;height:1rem}.helix-checkbox-widget__message{font-size:.75rem}.helix-checkbox-widget__message--hint{color:var(--h-text-muted-color)}.helix-checkbox-widget__message--error{color:var(--h-red-500)}\n"] }]
675
+ }] });
676
+
677
+ function toIsoDate(value) {
678
+ const y = value.getFullYear().toString().padStart(4, '0');
679
+ const m = (value.getMonth() + 1).toString().padStart(2, '0');
680
+ const d = value.getDate().toString().padStart(2, '0');
681
+ return `${y}-${m}-${d}`;
682
+ }
683
+ /**
684
+ * Native `<input type="date">` exposed as a signal-forms `FormValueControl`
685
+ * with a `Date | null` model — string↔Date conversion via `transformedValue`.
686
+ */
687
+ class HelixDateInput {
688
+ value = model.required(/* @ts-ignore */
689
+ ...(ngDevMode ? [{ debugName: "value" }] : /* istanbul ignore next */ []));
690
+ disabled = input(false, /* @ts-ignore */
691
+ ...(ngDevMode ? [{ debugName: "disabled" }] : /* istanbul ignore next */ []));
692
+ readonly = input(false, /* @ts-ignore */
693
+ ...(ngDevMode ? [{ debugName: "readonly" }] : /* istanbul ignore next */ []));
694
+ touched = model(false, /* @ts-ignore */
695
+ ...(ngDevMode ? [{ debugName: "touched" }] : /* istanbul ignore next */ []));
696
+ rawValue = transformedValue(this.value, {
697
+ parse: (raw) => {
698
+ if (raw === '')
699
+ return { value: null };
700
+ const parsed = new Date(`${raw}T00:00:00`);
701
+ if (Number.isNaN(parsed.getTime())) {
702
+ return { error: { kind: 'parse', message: `"${raw}" is not a valid date` } };
703
+ }
704
+ return { value: parsed };
705
+ },
706
+ format: (value) => (value ? toIsoDate(value) : ''),
707
+ });
708
+ onInput(event) {
709
+ this.rawValue.set(event.target.value);
710
+ }
711
+ onBlur() {
712
+ this.touched.set(true);
713
+ }
714
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: HelixDateInput, deps: [], target: i0.ɵɵFactoryTarget.Component });
715
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "22.1.6", type: HelixDateInput, isStandalone: true, selector: "helix-date-input", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: true, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, readonly: { classPropertyName: "readonly", publicName: "readonly", isSignal: true, isRequired: false, transformFunction: null }, touched: { classPropertyName: "touched", publicName: "touched", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange", touched: "touchedChange" }, ngImport: i0, template: "<input\n class=\"helix-date-input__input\"\n type=\"date\"\n [value]=\"rawValue()\"\n [disabled]=\"disabled()\"\n [readonly]=\"readonly()\"\n (input)=\"onInput($event)\"\n (blur)=\"onBlur()\"\n/>\n", styles: [":host{display:block;width:100%}.helix-date-input__input{width:100%;padding:.5rem .75rem;font-family:inherit;font-size:1rem;color:var(--h-text-color);background-color:var(--h-content-background);border:1px solid var(--h-content-border-color);border-radius:var(--h-border-radius-md);transition:border-color .2s,box-shadow .2s}.helix-date-input__input:focus{outline:0 none;outline-offset:0;border-color:var(--h-primary-color);box-shadow:0 0 0 .2rem var(--h-primary-100)}.helix-date-input__input:disabled,.helix-date-input__input[readonly]{cursor:default;background-color:var(--h-content-disabled-background);color:var(--h-text-disabled-color);opacity:1}.helix-date-input__input::placeholder{color:var(--h-text-muted-color)}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
716
+ }
717
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: HelixDateInput, decorators: [{
718
+ type: Component,
719
+ args: [{ selector: 'helix-date-input', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, template: "<input\n class=\"helix-date-input__input\"\n type=\"date\"\n [value]=\"rawValue()\"\n [disabled]=\"disabled()\"\n [readonly]=\"readonly()\"\n (input)=\"onInput($event)\"\n (blur)=\"onBlur()\"\n/>\n", styles: [":host{display:block;width:100%}.helix-date-input__input{width:100%;padding:.5rem .75rem;font-family:inherit;font-size:1rem;color:var(--h-text-color);background-color:var(--h-content-background);border:1px solid var(--h-content-border-color);border-radius:var(--h-border-radius-md);transition:border-color .2s,box-shadow .2s}.helix-date-input__input:focus{outline:0 none;outline-offset:0;border-color:var(--h-primary-color);box-shadow:0 0 0 .2rem var(--h-primary-100)}.helix-date-input__input:disabled,.helix-date-input__input[readonly]{cursor:default;background-color:var(--h-content-disabled-background);color:var(--h-text-disabled-color);opacity:1}.helix-date-input__input::placeholder{color:var(--h-text-muted-color)}\n"] }]
720
+ }], propDecorators: { value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: true }] }, { type: i0.Output, args: ["valueChange"] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], readonly: [{ type: i0.Input, args: [{ isSignal: true, alias: "readonly", required: false }] }], touched: [{ type: i0.Input, args: [{ isSignal: true, alias: "touched", required: false }] }, { type: i0.Output, args: ["touchedChange"] }] } });
721
+
722
+ /** Built-in widget for `z.date()` fields. */
723
+ class HelixDateWidget extends HelixFieldWidgetBase {
724
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: HelixDateWidget, deps: null, target: i0.ɵɵFactoryTarget.Component });
725
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "22.1.6", type: HelixDateWidget, isStandalone: true, selector: "helix-date-widget", usesInheritance: true, ngImport: i0, template: "<helix-form-field [label]=\"label()\" [hint]=\"hint()\" [error]=\"firstError()\">\n <helix-date-input [formField]=\"field()\" />\n</helix-form-field>\n", styles: [":host{display:block;width:100%}\n"], dependencies: [{ kind: "directive", type: FormField, selector: "[formField]", inputs: ["formField"], exportAs: ["formField"] }, { kind: "component", type: HelixFormField, selector: "helix-form-field", inputs: ["label", "control", "hint", "error", "showLabel", "showHint"] }, { kind: "component", type: HelixDateInput, selector: "helix-date-input", inputs: ["value", "disabled", "readonly", "touched"], outputs: ["valueChange", "touchedChange"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
726
+ }
727
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: HelixDateWidget, decorators: [{
728
+ type: Component,
729
+ args: [{ selector: 'helix-date-widget', standalone: true, imports: [FormField, HelixFormField, HelixDateInput], changeDetection: ChangeDetectionStrategy.OnPush, template: "<helix-form-field [label]=\"label()\" [hint]=\"hint()\" [error]=\"firstError()\">\n <helix-date-input [formField]=\"field()\" />\n</helix-form-field>\n", styles: [":host{display:block;width:100%}\n"] }]
730
+ }] });
731
+
732
+ /** Built-in widget for numeric fields. */
733
+ class HelixNumberWidget extends HelixFieldWidgetBase {
734
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: HelixNumberWidget, deps: null, target: i0.ɵɵFactoryTarget.Component });
735
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "22.1.6", type: HelixNumberWidget, isStandalone: true, selector: "helix-number-widget", usesInheritance: true, ngImport: i0, template: "<helix-form-field [label]=\"label()\" [hint]=\"hint()\" [error]=\"firstError()\">\n <input\n class=\"helix-number-widget__input\"\n type=\"number\"\n [placeholder]=\"placeholder()\"\n [formField]=\"field()\"\n />\n</helix-form-field>\n", styles: [":host{display:block;width:100%}.helix-number-widget__input{width:100%;padding:.5rem .75rem;font-family:inherit;font-size:1rem;color:var(--h-text-color);background-color:var(--h-content-background);border:1px solid var(--h-content-border-color);border-radius:var(--h-border-radius-md);transition:border-color .2s,box-shadow .2s}.helix-number-widget__input:focus{outline:0 none;outline-offset:0;border-color:var(--h-primary-color);box-shadow:0 0 0 .2rem var(--h-primary-100)}.helix-number-widget__input:disabled,.helix-number-widget__input[readonly]{cursor:default;background-color:var(--h-content-disabled-background);color:var(--h-text-disabled-color);opacity:1}.helix-number-widget__input::placeholder{color:var(--h-text-muted-color)}\n"], dependencies: [{ kind: "directive", type: FormField, selector: "[formField]", inputs: ["formField"], exportAs: ["formField"] }, { kind: "component", type: HelixFormField, selector: "helix-form-field", inputs: ["label", "control", "hint", "error", "showLabel", "showHint"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
736
+ }
737
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: HelixNumberWidget, decorators: [{
738
+ type: Component,
739
+ args: [{ selector: 'helix-number-widget', standalone: true, imports: [FormField, HelixFormField], changeDetection: ChangeDetectionStrategy.OnPush, template: "<helix-form-field [label]=\"label()\" [hint]=\"hint()\" [error]=\"firstError()\">\n <input\n class=\"helix-number-widget__input\"\n type=\"number\"\n [placeholder]=\"placeholder()\"\n [formField]=\"field()\"\n />\n</helix-form-field>\n", styles: [":host{display:block;width:100%}.helix-number-widget__input{width:100%;padding:.5rem .75rem;font-family:inherit;font-size:1rem;color:var(--h-text-color);background-color:var(--h-content-background);border:1px solid var(--h-content-border-color);border-radius:var(--h-border-radius-md);transition:border-color .2s,box-shadow .2s}.helix-number-widget__input:focus{outline:0 none;outline-offset:0;border-color:var(--h-primary-color);box-shadow:0 0 0 .2rem var(--h-primary-100)}.helix-number-widget__input:disabled,.helix-number-widget__input[readonly]{cursor:default;background-color:var(--h-content-disabled-background);color:var(--h-text-disabled-color);opacity:1}.helix-number-widget__input::placeholder{color:var(--h-text-muted-color)}\n"] }]
740
+ }] });
741
+
742
+ /** Built-in widget for nested `z.object()` fields — fieldset + recursion. */
743
+ class HelixObjectWidget extends HelixFieldWidgetBase {
744
+ childField(key) {
745
+ return this.field()[key];
746
+ }
747
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: HelixObjectWidget, deps: null, target: i0.ɵɵFactoryTarget.Component });
748
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.6", type: HelixObjectWidget, isStandalone: true, selector: "helix-object-widget", usesInheritance: true, ngImport: i0, template: "<fieldset class=\"helix-object-widget\">\n @if (label(); as l) {\n <legend class=\"helix-object-widget__legend\">{{ l }}</legend>\n }\n @if (hint(); as h) {\n <small class=\"helix-object-widget__hint\">{{ h }}</small>\n }\n\n @for (child of descriptor().children ?? []; track child.key) {\n <helix-dynamic-field [field]=\"childField(child.key)\" [descriptor]=\"child\" />\n }\n</fieldset>\n", styles: [":host{display:block;width:100%}.helix-object-widget{display:flex;flex-direction:column;gap:1rem;margin:0;padding:1rem;border:1px solid var(--h-content-border-color);border-radius:var(--h-border-radius-md)}.helix-object-widget__legend{padding:0 .5rem;font-size:.875rem;font-weight:600;color:var(--h-text-color)}.helix-object-widget__hint{font-size:.75rem;color:var(--h-text-muted-color)}\n"], dependencies: [{ kind: "component", type: HelixDynamicField, selector: "helix-dynamic-field", inputs: ["field", "descriptor"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
749
+ }
750
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: HelixObjectWidget, decorators: [{
751
+ type: Component,
752
+ args: [{ selector: 'helix-object-widget', standalone: true, imports: [HelixDynamicField], changeDetection: ChangeDetectionStrategy.OnPush, template: "<fieldset class=\"helix-object-widget\">\n @if (label(); as l) {\n <legend class=\"helix-object-widget__legend\">{{ l }}</legend>\n }\n @if (hint(); as h) {\n <small class=\"helix-object-widget__hint\">{{ h }}</small>\n }\n\n @for (child of descriptor().children ?? []; track child.key) {\n <helix-dynamic-field [field]=\"childField(child.key)\" [descriptor]=\"child\" />\n }\n</fieldset>\n", styles: [":host{display:block;width:100%}.helix-object-widget{display:flex;flex-direction:column;gap:1rem;margin:0;padding:1rem;border:1px solid var(--h-content-border-color);border-radius:var(--h-border-radius-md)}.helix-object-widget__legend{padding:0 .5rem;font-size:.875rem;font-weight:600;color:var(--h-text-color)}.helix-object-widget__hint{font-size:.75rem;color:var(--h-text-muted-color)}\n"] }]
753
+ }] });
754
+
755
+ /**
756
+ * Built-in widget for enum/select fields — wraps `HelixSelect`, which the
757
+ * signal-forms `FormField` directive binds via its `ControlValueAccessor`.
758
+ */
759
+ class HelixSelectWidget extends HelixFieldWidgetBase {
760
+ selectOptions = computed(() => [...(this.descriptor().options ?? [])], /* @ts-ignore */
761
+ ...(ngDevMode ? [{ debugName: "selectOptions" }] : /* istanbul ignore next */ []));
762
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: HelixSelectWidget, deps: null, target: i0.ɵɵFactoryTarget.Component });
763
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "22.1.6", type: HelixSelectWidget, isStandalone: true, selector: "helix-select-widget", usesInheritance: true, ngImport: i0, template: "<helix-form-field [label]=\"label()\" [hint]=\"hint()\" [error]=\"firstError()\">\n <helix-select\n [options]=\"selectOptions()\"\n [placeholder]=\"placeholder()\"\n [disabled]=\"state().disabled()\"\n [formField]=\"field()\"\n />\n</helix-form-field>\n", styles: [":host{display:block;width:100%}\n"], dependencies: [{ kind: "directive", type: FormField, selector: "[formField]", inputs: ["formField"], exportAs: ["formField"] }, { kind: "component", type: HelixFormField, selector: "helix-form-field", inputs: ["label", "control", "hint", "error", "showLabel", "showHint"] }, { kind: "component", type: HelixSelect, selector: "helix-select", inputs: ["options", "optionLabel", "optionValue", "placeholder", "disabled"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
764
+ }
765
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: HelixSelectWidget, decorators: [{
766
+ type: Component,
767
+ args: [{ selector: 'helix-select-widget', standalone: true, imports: [FormField, HelixFormField, HelixSelect], changeDetection: ChangeDetectionStrategy.OnPush, template: "<helix-form-field [label]=\"label()\" [hint]=\"hint()\" [error]=\"firstError()\">\n <helix-select\n [options]=\"selectOptions()\"\n [placeholder]=\"placeholder()\"\n [disabled]=\"state().disabled()\"\n [formField]=\"field()\"\n />\n</helix-form-field>\n", styles: [":host{display:block;width:100%}\n"] }]
768
+ }] });
769
+
770
+ /** Built-in widget for `text`, `email` and `password` fields. */
771
+ class HelixTextWidget extends HelixFieldWidgetBase {
772
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: HelixTextWidget, deps: null, target: i0.ɵɵFactoryTarget.Component });
773
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "22.1.6", type: HelixTextWidget, isStandalone: true, selector: "helix-text-widget", usesInheritance: true, ngImport: i0, template: "<helix-form-field [label]=\"label()\" [hint]=\"hint()\" [error]=\"firstError()\">\n <input\n class=\"helix-text-widget__input\"\n [type]=\"descriptor().inputType ?? 'text'\"\n [placeholder]=\"placeholder()\"\n [formField]=\"field()\"\n />\n</helix-form-field>\n", styles: [":host{display:block;width:100%}.helix-text-widget__input{width:100%;padding:.5rem .75rem;font-family:inherit;font-size:1rem;color:var(--h-text-color);background-color:var(--h-content-background);border:1px solid var(--h-content-border-color);border-radius:var(--h-border-radius-md);transition:border-color .2s,box-shadow .2s}.helix-text-widget__input:focus{outline:0 none;outline-offset:0;border-color:var(--h-primary-color);box-shadow:0 0 0 .2rem var(--h-primary-100)}.helix-text-widget__input:disabled,.helix-text-widget__input[readonly]{cursor:default;background-color:var(--h-content-disabled-background);color:var(--h-text-disabled-color);opacity:1}.helix-text-widget__input::placeholder{color:var(--h-text-muted-color)}\n"], dependencies: [{ kind: "directive", type: FormField, selector: "[formField]", inputs: ["formField"], exportAs: ["formField"] }, { kind: "component", type: HelixFormField, selector: "helix-form-field", inputs: ["label", "control", "hint", "error", "showLabel", "showHint"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
774
+ }
775
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: HelixTextWidget, decorators: [{
776
+ type: Component,
777
+ args: [{ selector: 'helix-text-widget', standalone: true, imports: [FormField, HelixFormField], changeDetection: ChangeDetectionStrategy.OnPush, template: "<helix-form-field [label]=\"label()\" [hint]=\"hint()\" [error]=\"firstError()\">\n <input\n class=\"helix-text-widget__input\"\n [type]=\"descriptor().inputType ?? 'text'\"\n [placeholder]=\"placeholder()\"\n [formField]=\"field()\"\n />\n</helix-form-field>\n", styles: [":host{display:block;width:100%}.helix-text-widget__input{width:100%;padding:.5rem .75rem;font-family:inherit;font-size:1rem;color:var(--h-text-color);background-color:var(--h-content-background);border:1px solid var(--h-content-border-color);border-radius:var(--h-border-radius-md);transition:border-color .2s,box-shadow .2s}.helix-text-widget__input:focus{outline:0 none;outline-offset:0;border-color:var(--h-primary-color);box-shadow:0 0 0 .2rem var(--h-primary-100)}.helix-text-widget__input:disabled,.helix-text-widget__input[readonly]{cursor:default;background-color:var(--h-content-disabled-background);color:var(--h-text-disabled-color);opacity:1}.helix-text-widget__input::placeholder{color:var(--h-text-muted-color)}\n"] }]
778
+ }] });
779
+
780
+ /** Built-in widget for multiline text (`widget: 'textarea'`). */
781
+ class HelixTextareaWidget extends HelixFieldWidgetBase {
782
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: HelixTextareaWidget, deps: null, target: i0.ɵɵFactoryTarget.Component });
783
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "22.1.6", type: HelixTextareaWidget, isStandalone: true, selector: "helix-textarea-widget", usesInheritance: true, ngImport: i0, template: "<helix-form-field [label]=\"label()\" [hint]=\"hint()\" [error]=\"firstError()\">\n <textarea\n class=\"helix-textarea-widget__input\"\n rows=\"4\"\n [placeholder]=\"placeholder()\"\n [formField]=\"field()\"\n ></textarea>\n</helix-form-field>\n", styles: [":host{display:block;width:100%}.helix-textarea-widget__input{width:100%;padding:.5rem .75rem;font-family:inherit;font-size:1rem;color:var(--h-text-color);background-color:var(--h-content-background);border:1px solid var(--h-content-border-color);border-radius:var(--h-border-radius-md);transition:border-color .2s,box-shadow .2s}.helix-textarea-widget__input:focus{outline:0 none;outline-offset:0;border-color:var(--h-primary-color);box-shadow:0 0 0 .2rem var(--h-primary-100)}.helix-textarea-widget__input:disabled,.helix-textarea-widget__input[readonly]{cursor:default;background-color:var(--h-content-disabled-background);color:var(--h-text-disabled-color);opacity:1}.helix-textarea-widget__input::placeholder{color:var(--h-text-muted-color)}.helix-textarea-widget__input{resize:vertical}\n"], dependencies: [{ kind: "directive", type: FormField, selector: "[formField]", inputs: ["formField"], exportAs: ["formField"] }, { kind: "component", type: HelixFormField, selector: "helix-form-field", inputs: ["label", "control", "hint", "error", "showLabel", "showHint"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
784
+ }
785
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: HelixTextareaWidget, decorators: [{
786
+ type: Component,
787
+ args: [{ selector: 'helix-textarea-widget', standalone: true, imports: [FormField, HelixFormField], changeDetection: ChangeDetectionStrategy.OnPush, template: "<helix-form-field [label]=\"label()\" [hint]=\"hint()\" [error]=\"firstError()\">\n <textarea\n class=\"helix-textarea-widget__input\"\n rows=\"4\"\n [placeholder]=\"placeholder()\"\n [formField]=\"field()\"\n ></textarea>\n</helix-form-field>\n", styles: [":host{display:block;width:100%}.helix-textarea-widget__input{width:100%;padding:.5rem .75rem;font-family:inherit;font-size:1rem;color:var(--h-text-color);background-color:var(--h-content-background);border:1px solid var(--h-content-border-color);border-radius:var(--h-border-radius-md);transition:border-color .2s,box-shadow .2s}.helix-textarea-widget__input:focus{outline:0 none;outline-offset:0;border-color:var(--h-primary-color);box-shadow:0 0 0 .2rem var(--h-primary-100)}.helix-textarea-widget__input:disabled,.helix-textarea-widget__input[readonly]{cursor:default;background-color:var(--h-content-disabled-background);color:var(--h-text-disabled-color);opacity:1}.helix-textarea-widget__input::placeholder{color:var(--h-text-muted-color)}.helix-textarea-widget__input{resize:vertical}\n"] }]
788
+ }] });
789
+
790
+ /**
791
+ * Built-in widget for `z.discriminatedUnion()` fields: a discriminator select
792
+ * plus the active variant's fields.
793
+ *
794
+ * Switching the discriminator **resets** the union value to the new variant's
795
+ * default (preserving the discriminator) — mandatory so the model always
796
+ * matches the active variant's shape and all field paths exist.
797
+ */
798
+ class HelixUnionWidget extends HelixFieldWidgetBase {
799
+ discriminator = computed(() => this.descriptor().union?.discriminator ?? '', /* @ts-ignore */
800
+ ...(ngDevMode ? [{ debugName: "discriminator" }] : /* istanbul ignore next */ []));
801
+ tags = computed(() => [...(this.descriptor().union?.variants.keys() ?? [])], /* @ts-ignore */
802
+ ...(ngDevMode ? [{ debugName: "tags" }] : /* istanbul ignore next */ []));
803
+ activeTag = computed(() => this.state().value()?.[this.discriminator()], /* @ts-ignore */
804
+ ...(ngDevMode ? [{ debugName: "activeTag" }] : /* istanbul ignore next */ []));
805
+ variantChildren = computed(() => {
806
+ const variant = this.descriptor().union?.variants.get(this.activeTag());
807
+ return (variant?.children ?? []).filter((c) => c.key !== this.discriminator());
808
+ }, /* @ts-ignore */
809
+ ...(ngDevMode ? [{ debugName: "variantChildren" }] : /* istanbul ignore next */ []));
810
+ childField(key) {
811
+ return this.field()[key];
812
+ }
813
+ onTagChange(event) {
814
+ const index = Number.parseInt(event.target.value, 10);
815
+ const tag = this.tags()[index];
816
+ const variant = this.descriptor().union?.variants.get(tag);
817
+ if (variant === undefined || tag === this.activeTag())
818
+ return;
819
+ const next = buildDefaultValue(variant.zodSource);
820
+ next[this.discriminator()] = tag;
821
+ this.state().value.set(next);
822
+ }
823
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: HelixUnionWidget, deps: null, target: i0.ɵɵFactoryTarget.Component });
824
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.6", type: HelixUnionWidget, isStandalone: true, selector: "helix-union-widget", usesInheritance: true, ngImport: i0, template: "<div class=\"helix-union-widget\">\n <helix-form-field [label]=\"label()\" [hint]=\"hint()\" [error]=\"firstError()\">\n <div class=\"helix-union-widget__select-wrapper\">\n <select\n class=\"helix-union-widget__select\"\n [disabled]=\"state().disabled()\"\n (change)=\"onTagChange($event)\"\n >\n @for (tag of tags(); track tag) {\n <option [value]=\"$index\" [selected]=\"tag === activeTag()\">{{ tag }}</option>\n }\n </select>\n <i class=\"helix-union-widget__icon pi pi-chevron-down\"></i>\n </div>\n </helix-form-field>\n\n @for (child of variantChildren(); track child.key) {\n <helix-dynamic-field [field]=\"childField(child.key)\" [descriptor]=\"child\" />\n }\n</div>\n", styles: [":host{display:block;width:100%}.helix-union-widget{display:flex;flex-direction:column;gap:1rem}.helix-union-widget__select-wrapper{position:relative;display:flex;width:100%}.helix-union-widget__select{width:100%;padding:.5rem .75rem;font-family:inherit;font-size:1rem;color:var(--h-text-color);background-color:var(--h-content-background);border:1px solid var(--h-content-border-color);border-radius:var(--h-border-radius-md);transition:border-color .2s,box-shadow .2s}.helix-union-widget__select:focus{outline:0 none;outline-offset:0;border-color:var(--h-primary-color);box-shadow:0 0 0 .2rem var(--h-primary-100)}.helix-union-widget__select:disabled,.helix-union-widget__select[readonly]{cursor:default;background-color:var(--h-content-disabled-background);color:var(--h-text-disabled-color);opacity:1}.helix-union-widget__select::placeholder{color:var(--h-text-muted-color)}.helix-union-widget__select{padding-right:2.5rem;appearance:none;cursor:pointer}.helix-union-widget__icon{position:absolute;top:50%;right:.75rem;transform:translateY(-50%);pointer-events:none;color:var(--h-text-muted-color);font-size:.875rem}\n"], dependencies: [{ kind: "component", type: HelixDynamicField, selector: "helix-dynamic-field", inputs: ["field", "descriptor"] }, { kind: "component", type: HelixFormField, selector: "helix-form-field", inputs: ["label", "control", "hint", "error", "showLabel", "showHint"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
825
+ }
826
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: HelixUnionWidget, decorators: [{
827
+ type: Component,
828
+ args: [{ selector: 'helix-union-widget', standalone: true, imports: [HelixDynamicField, HelixFormField], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"helix-union-widget\">\n <helix-form-field [label]=\"label()\" [hint]=\"hint()\" [error]=\"firstError()\">\n <div class=\"helix-union-widget__select-wrapper\">\n <select\n class=\"helix-union-widget__select\"\n [disabled]=\"state().disabled()\"\n (change)=\"onTagChange($event)\"\n >\n @for (tag of tags(); track tag) {\n <option [value]=\"$index\" [selected]=\"tag === activeTag()\">{{ tag }}</option>\n }\n </select>\n <i class=\"helix-union-widget__icon pi pi-chevron-down\"></i>\n </div>\n </helix-form-field>\n\n @for (child of variantChildren(); track child.key) {\n <helix-dynamic-field [field]=\"childField(child.key)\" [descriptor]=\"child\" />\n }\n</div>\n", styles: [":host{display:block;width:100%}.helix-union-widget{display:flex;flex-direction:column;gap:1rem}.helix-union-widget__select-wrapper{position:relative;display:flex;width:100%}.helix-union-widget__select{width:100%;padding:.5rem .75rem;font-family:inherit;font-size:1rem;color:var(--h-text-color);background-color:var(--h-content-background);border:1px solid var(--h-content-border-color);border-radius:var(--h-border-radius-md);transition:border-color .2s,box-shadow .2s}.helix-union-widget__select:focus{outline:0 none;outline-offset:0;border-color:var(--h-primary-color);box-shadow:0 0 0 .2rem var(--h-primary-100)}.helix-union-widget__select:disabled,.helix-union-widget__select[readonly]{cursor:default;background-color:var(--h-content-disabled-background);color:var(--h-text-disabled-color);opacity:1}.helix-union-widget__select::placeholder{color:var(--h-text-muted-color)}.helix-union-widget__select{padding-right:2.5rem;appearance:none;cursor:pointer}.helix-union-widget__icon{position:absolute;top:50%;right:.75rem;transform:translateY(-50%);pointer-events:none;color:var(--h-text-muted-color);font-size:.875rem}\n"] }]
829
+ }] });
830
+
831
+ const BUILT_IN_WIDGETS = [
832
+ { widget: 'text', component: HelixTextWidget },
833
+ { widget: 'email', component: HelixTextWidget },
834
+ { widget: 'password', component: HelixTextWidget },
835
+ { widget: 'textarea', component: HelixTextareaWidget },
836
+ { widget: 'number', component: HelixNumberWidget },
837
+ { widget: 'checkbox', component: HelixCheckboxWidget },
838
+ { widget: 'select', component: HelixSelectWidget },
839
+ { widget: 'date', component: HelixDateWidget },
840
+ { widget: 'object', component: HelixObjectWidget },
841
+ { widget: 'array', component: HelixArrayWidget },
842
+ { widget: 'union', component: HelixUnionWidget },
843
+ ];
844
+ /**
845
+ * Provides the dynamic-form widget registry (built-in widgets included) and
846
+ * optional configuration. Add it to `ApplicationConfig.providers` or a route's
847
+ * `providers`.
848
+ */
849
+ function provideHelixDynamicForms(config = {}) {
850
+ const providers = [
851
+ { provide: HELIX_DYNAMIC_FIELD_WIDGETS, multi: true, useValue: BUILT_IN_WIDGETS },
852
+ // Re-provide the resolver here so it is created in THIS environment
853
+ // injector and sees the registrations above. The `providedIn: 'root'`
854
+ // fallback instance cannot see route-scoped providers.
855
+ HelixFieldWidgetResolver,
856
+ ];
857
+ if (config.widgets?.length) {
858
+ providers.push({ provide: HELIX_DYNAMIC_FIELD_WIDGETS, multi: true, useValue: config.widgets });
859
+ }
860
+ if (config.errorMessageResolver) {
861
+ providers.push({
862
+ provide: HELIX_ERROR_MESSAGE_RESOLVER,
863
+ useValue: config.errorMessageResolver,
864
+ });
865
+ }
866
+ return makeEnvironmentProviders(providers);
867
+ }
868
+
869
+ /**
870
+ * Navigates a `FieldTree` by a root-relative property path
871
+ * (e.g. `['address', 'city']`).
872
+ */
873
+ function fieldAtPath(tree, path) {
874
+ let current = tree;
875
+ for (const key of path) {
876
+ current = current[key];
877
+ if (current === undefined) {
878
+ throw new Error(`[fieldAtPath] No field at path "${path.join('.')}" (missing key "${key}").`);
879
+ }
880
+ }
881
+ return current;
882
+ }
883
+
884
+ const HelixZodValidators = {
885
+ /**
886
+ * Converts a Zod field schema into a Helix-compatible Angular `ValidatorFn`.
887
+ *
888
+ * Each `ZodIssue` is mapped to its `ValidatorKey`. `HelixFormField` reads
889
+ * those keys directly — no template changes required.
890
+ *
891
+ * All issues from a single `safeParse` are processed simultaneously, producing
892
+ * one `ValidationErrors` key per issue — equivalent to stacking multiple
893
+ * `Validators` calls.
894
+ *
895
+ * ### Known gaps
896
+ * - `ValidatorKey.OneOf` / `AllOf` — `z.enum()` produces `invalid_value`,
897
+ * which has no automatic mapping. Use `fallbackKey` or continue using
898
+ * `Validators.oneOf` / `Validators.allOf`.
899
+ * - `invalid_type` for `boolean` expected — Helix has no `Boolean` key.
900
+ * Use `fallbackKey` or `Validators.pattern`.
901
+ *
902
+ * @param schema A Zod field schema (e.g. `UserSchema.shape.email`).
903
+ * Do NOT pass schemas with `.transform()` — use the
904
+ * input-only (pre-transform) variant for form controls.
905
+ * @param options `allowEmpty` (default `true`) and optional `fallbackKey`
906
+ * for `.refine()` / `custom` validators.
907
+ *
908
+ * @example
909
+ * // Standard field — no fallbackKey needed
910
+ * HelixZodValidators.fromZod(UserSchema.shape.email)
911
+ *
912
+ * @example
913
+ * // Schema with .refine() — fallbackKey required (Option B)
914
+ * HelixZodValidators.fromZod(
915
+ * z.string().refine(v => !banned.includes(v), 'Username not allowed'),
916
+ * { fallbackKey: ValidatorKey.Pattern },
917
+ * )
918
+ *
919
+ * @example
920
+ * // Required field — disable allowEmpty so empty value triggers Required / MinLength
921
+ * HelixZodValidators.fromZod(UserSchema.shape.name, { allowEmpty: false })
922
+ */
923
+ fromZod(schema, options = {}) {
924
+ const { fallbackKey, allowEmpty = true } = options;
925
+ return (control) => {
926
+ const value = control.value;
927
+ // Replicate Validators' allowEmpty = true default:
928
+ // skip Zod entirely for empty values unless the caller opts out.
929
+ if (allowEmpty && isEmptyValue(value)) {
930
+ return null;
931
+ }
932
+ const result = schema.safeParse(value);
933
+ if (result.success)
934
+ return null;
935
+ const errors = {};
936
+ for (const issue of result.error.issues) {
937
+ const key = zodIssueToHelixKey(issue, value);
938
+ if (key !== null) {
939
+ errors[key] = issue.message;
940
+ continue;
941
+ }
942
+ // No automatic mapping — apply Option B logic.
943
+ if (fallbackKey !== undefined) {
944
+ errors[fallbackKey] = issue.message;
945
+ }
946
+ else {
947
+ // ngDevMode is set by Angular's build toolchain:
948
+ // true in development builds, false (tree-shaken) in production.
949
+ if (typeof ngDevMode !== 'undefined' && ngDevMode) {
950
+ throw new Error(`[HelixZodValidators.fromZod] Zod issue code "${issue.code}" has no ` +
951
+ `automatic ValidatorKey mapping.\n` +
952
+ `Pass { fallbackKey: ValidatorKey.xxx } as the second argument.\n` +
953
+ `Failing issue: ${JSON.stringify(issue, null, 2)}`);
954
+ }
955
+ // Production: skip the unmapped issue silently.
956
+ // The control remains invalid if other issues produce mapped errors.
957
+ }
958
+ }
959
+ return Object.keys(errors).length ? errors : null;
960
+ };
961
+ },
962
+ };
963
+
964
+ /*
965
+ * Public API Surface of @gravionlabs/helix-zod
966
+ */
967
+ // Dynamic forms — components
968
+
969
+ /**
970
+ * Generated bundle index. Do not edit.
971
+ */
972
+
973
+ export { HELIX_DYNAMIC_FIELD_WIDGETS, HELIX_ERROR_MESSAGE_RESOLVER, HelixArrayWidget, HelixCheckboxWidget, HelixDateInput, HelixDateWidget, HelixDynamicField, HelixDynamicForm, HelixFieldWidgetBase, HelixFieldWidgetResolver, HelixNumberWidget, HelixObjectWidget, HelixSelectWidget, HelixTextWidget, HelixTextareaWidget, HelixUnionWidget, HelixZodValidators, buildDefaultValue, buildHelixSchema, fieldAtPath, helixErrorKey, helixFieldMetaRegistry, helixFirstErrorMessage, helixMeta, provideHelixDynamicForms, readHelixMeta, zodToFieldDescriptors };
974
+ //# sourceMappingURL=gravionlabs-helix-zod.mjs.map