@formspec/dsl 0.1.0-alpha.2 → 0.1.0-alpha.21

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/dsl.d.ts ADDED
@@ -0,0 +1,665 @@
1
+ /**
2
+ * `@formspec/dsl` - DSL functions for defining FormSpec forms
3
+ *
4
+ * This package provides the builder functions for creating form specifications:
5
+ * - `field.*` - Field builders (text, number, boolean, enum, dynamicEnum)
6
+ * - `group()` - Visual grouping
7
+ * - `when()` + `is()` - Conditional visibility
8
+ * - `formspec()` - Top-level form definition
9
+ *
10
+ * @example
11
+ * ```typescript
12
+ * import { formspec, field, group, when, is } from "@formspec/dsl";
13
+ *
14
+ * const InvoiceForm = formspec(
15
+ * group("Customer",
16
+ * field.text("customerName", { label: "Customer Name" }),
17
+ * field.dynamicEnum("country", "fetch_countries", { label: "Country" }),
18
+ * ),
19
+ * group("Invoice Details",
20
+ * field.number("amount", { label: "Amount", min: 0 }),
21
+ * field.enum("status", ["draft", "sent", "paid"] as const),
22
+ * when(is("status", "draft"),
23
+ * field.text("internalNotes", { label: "Internal Notes" }),
24
+ * ),
25
+ * ),
26
+ * );
27
+ * ```
28
+ *
29
+ * @packageDocumentation
30
+ */
31
+
32
+ import type { AnyField } from '@formspec/core';
33
+ import type { ArrayField } from '@formspec/core';
34
+ import type { BooleanField } from '@formspec/core';
35
+ import type { Conditional } from '@formspec/core';
36
+ import type { DataSourceValueType } from '@formspec/core';
37
+ import type { DynamicEnumField } from '@formspec/core';
38
+ import type { DynamicSchemaField } from '@formspec/core';
39
+ import { EnumOption } from '@formspec/core';
40
+ import { EnumOptionValue } from '@formspec/core';
41
+ import type { EqualsPredicate } from '@formspec/core';
42
+ import type { FormElement } from '@formspec/core';
43
+ import type { FormSpec } from '@formspec/core';
44
+ import type { Group } from '@formspec/core';
45
+ import type { NumberField } from '@formspec/core';
46
+ import type { ObjectField } from '@formspec/core';
47
+ import type { Predicate } from '@formspec/core';
48
+ import type { StaticEnumField } from '@formspec/core';
49
+ import type { TextField } from '@formspec/core';
50
+
51
+ /**
52
+ * Builds a schema type from extracted fields.
53
+ *
54
+ * Maps field names to their inferred value types.
55
+ *
56
+ * @public
57
+ */
58
+ export declare type BuildSchema<Fields> = {
59
+ [F in Fields as F extends {
60
+ name: infer N extends string;
61
+ } ? N : never]: F extends AnyField ? InferFieldValue<F> : never;
62
+ };
63
+
64
+ export { EnumOption }
65
+
66
+ export { EnumOptionValue }
67
+
68
+ /**
69
+ * Extracts fields that ARE inside conditionals.
70
+ * These fields may or may not be visible and should be optional in the inferred schema.
71
+ *
72
+ * @example
73
+ * ```typescript
74
+ * // Given a form with conditional fields:
75
+ * const form = formspec(
76
+ * field.text("name"), // non-conditional (skipped)
77
+ * when(is("type", "business"),
78
+ * field.text("company"), // conditional (extracted)
79
+ * field.text("taxId"), // conditional (extracted)
80
+ * ),
81
+ * );
82
+ *
83
+ * // ExtractConditionalFields extracts: TextField<"company"> | TextField<"taxId">
84
+ * ```
85
+ *
86
+ * @public
87
+ */
88
+ export declare type ExtractConditionalFields<E> = E extends AnyField ? never : E extends Group<infer Elements> ? ExtractConditionalFieldsFromArray<Elements> : E extends Conditional<string, unknown, infer Elements> ? ExtractFieldsFromArray<Elements> : never;
89
+
90
+ /**
91
+ * Extracts conditional fields from an array of elements.
92
+ *
93
+ * @example
94
+ * ```typescript
95
+ * type Elements = readonly [
96
+ * TextField<"name">,
97
+ * Conditional<"type", "business", readonly [TextField<"company">]>
98
+ * ];
99
+ * type Fields = ExtractConditionalFieldsFromArray<Elements>;
100
+ * // TextField<"company">
101
+ * ```
102
+ *
103
+ * @public
104
+ */
105
+ export declare type ExtractConditionalFieldsFromArray<Elements> = Elements extends readonly [
106
+ infer First,
107
+ ...infer Rest
108
+ ] ? ExtractConditionalFields<First> | ExtractConditionalFieldsFromArray<Rest> : never;
109
+
110
+ /**
111
+ * Extracts all fields from a single element (recursively).
112
+ *
113
+ * - Field elements return themselves
114
+ * - Groups extract fields from all child elements
115
+ * - Conditionals extract fields from all child elements
116
+ *
117
+ * @public
118
+ */
119
+ export declare type ExtractFields<E> = E extends AnyField ? E : E extends Group<infer Elements> ? ExtractFieldsFromArray<Elements> : E extends Conditional<string, unknown, infer Elements> ? ExtractFieldsFromArray<Elements> : never;
120
+
121
+ /**
122
+ * Extracts fields from an array of elements.
123
+ *
124
+ * Recursively processes each element and unions the results.
125
+ *
126
+ * @public
127
+ */
128
+ export declare type ExtractFieldsFromArray<Elements> = Elements extends readonly [
129
+ infer First,
130
+ ...infer Rest
131
+ ] ? ExtractFields<First> | ExtractFieldsFromArray<Rest> : never;
132
+
133
+ /**
134
+ * Extracts fields that are NOT inside conditionals.
135
+ * These fields are always visible and should be required in the inferred schema.
136
+ *
137
+ * @example
138
+ * ```typescript
139
+ * // Given a form with conditional and non-conditional fields:
140
+ * const form = formspec(
141
+ * field.text("name"), // non-conditional
142
+ * field.number("age"), // non-conditional
143
+ * when(is("type", "business"),
144
+ * field.text("company"), // conditional (skipped)
145
+ * ),
146
+ * );
147
+ *
148
+ * // ExtractNonConditionalFields extracts: TextField<"name"> | NumberField<"age">
149
+ * ```
150
+ *
151
+ * @public
152
+ */
153
+ export declare type ExtractNonConditionalFields<E> = E extends AnyField ? E : E extends Group<infer Elements> ? ExtractNonConditionalFieldsFromArray<Elements> : E extends Conditional<string, unknown, infer _Elements> ? never : never;
154
+
155
+ /**
156
+ * Extracts non-conditional fields from an array of elements.
157
+ *
158
+ * @example
159
+ * ```typescript
160
+ * type Elements = readonly [TextField<"name">, NumberField<"age">];
161
+ * type Fields = ExtractNonConditionalFieldsFromArray<Elements>;
162
+ * // TextField<"name"> | NumberField<"age">
163
+ * ```
164
+ *
165
+ * @public
166
+ */
167
+ export declare type ExtractNonConditionalFieldsFromArray<Elements> = Elements extends readonly [
168
+ infer First,
169
+ ...infer Rest
170
+ ] ? ExtractNonConditionalFields<First> | ExtractNonConditionalFieldsFromArray<Rest> : never;
171
+
172
+ /**
173
+ * Field builder namespace containing functions to create each field type.
174
+ *
175
+ * @example
176
+ * ```typescript
177
+ * import { field } from "@formspec/dsl";
178
+ *
179
+ * field.text("name", { label: "Full Name" });
180
+ * field.number("age", { min: 0, max: 150 });
181
+ * field.enum("status", ["draft", "sent", "paid"]);
182
+ * field.dynamicEnum("country", "countries", { label: "Country" });
183
+ * ```
184
+ *
185
+ * @public
186
+ */
187
+ export declare const field: {
188
+ /**
189
+ * Creates a text input field.
190
+ *
191
+ * @param name - The field name (used as the schema key)
192
+ * @param config - Optional configuration for label, placeholder, etc.
193
+ * @returns A TextField descriptor
194
+ */
195
+ text: <const N extends string>(name: N, config?: Omit<TextField<N>, "_type" | "_field" | "name">) => TextField<N>;
196
+ /**
197
+ * Creates a numeric input field.
198
+ *
199
+ * @param name - The field name (used as the schema key)
200
+ * @param config - Optional configuration for label, min, max, etc.
201
+ * @returns A NumberField descriptor
202
+ */
203
+ number: <const N extends string>(name: N, config?: Omit<NumberField<N>, "_type" | "_field" | "name">) => NumberField<N>;
204
+ /**
205
+ * Creates a boolean checkbox field.
206
+ *
207
+ * @param name - The field name (used as the schema key)
208
+ * @param config - Optional configuration for label, etc.
209
+ * @returns A BooleanField descriptor
210
+ */
211
+ boolean: <const N extends string>(name: N, config?: Omit<BooleanField<N>, "_type" | "_field" | "name">) => BooleanField<N>;
212
+ /**
213
+ * Creates a field with static enum options (known at compile time).
214
+ *
215
+ * Literal types are automatically inferred - no `as const` needed:
216
+ * ```typescript
217
+ * field.enum("status", ["draft", "sent", "paid"])
218
+ * // Schema type: "draft" | "sent" | "paid"
219
+ * ```
220
+ *
221
+ * Options can be strings or objects with `id` and `label`:
222
+ * ```typescript
223
+ * field.enum("priority", [
224
+ * { id: "low", label: "Low Priority" },
225
+ * { id: "high", label: "High Priority" },
226
+ * ])
227
+ * ```
228
+ *
229
+ * **Note:** All options must be of the same type (all strings OR all objects).
230
+ * Mixing strings and objects will throw a runtime error.
231
+ *
232
+ * @param name - The field name (used as the schema key)
233
+ * @param options - Array of allowed string values or objects with `id` and `label` properties
234
+ * @param config - Optional configuration for label, etc.
235
+ * @returns A StaticEnumField descriptor
236
+ * @throws Error if options array contains mixed types (strings and objects)
237
+ */
238
+ enum: <const N extends string, const O extends readonly EnumOptionValue[]>(name: N, options: O, config?: Omit<StaticEnumField<N, O>, "_type" | "_field" | "name" | "options">) => StaticEnumField<N, O>;
239
+ /**
240
+ * Creates a field with dynamic enum options (fetched from a data source at runtime).
241
+ *
242
+ * The data source must be registered in DataSourceRegistry via module augmentation:
243
+ * ```typescript
244
+ * declare module "@formspec/core" {
245
+ * interface DataSourceRegistry {
246
+ * countries: { id: string; code: string; name: string };
247
+ * }
248
+ * }
249
+ *
250
+ * field.dynamicEnum("country", "countries", { label: "Country" })
251
+ * ```
252
+ *
253
+ * @param name - The field name (used as the schema key)
254
+ * @param source - The data source key (must be in DataSourceRegistry)
255
+ * @param config - Optional configuration for label, params, etc.
256
+ * @returns A DynamicEnumField descriptor
257
+ */
258
+ dynamicEnum: <const N extends string, const Source extends string>(name: N, source: Source, config?: Omit<DynamicEnumField<N, Source>, "_type" | "_field" | "name" | "source">) => DynamicEnumField<N, Source>;
259
+ /**
260
+ * Creates a field that loads its schema dynamically (e.g., from an extension).
261
+ *
262
+ * @param name - The field name (used as the schema key)
263
+ * @param schemaSource - Identifier for the schema source
264
+ * @param config - Optional configuration for label, etc.
265
+ * @returns A DynamicSchemaField descriptor
266
+ */
267
+ dynamicSchema: <const N extends string>(name: N, schemaSource: string, config?: Omit<DynamicSchemaField<N>, "_type" | "_field" | "name" | "schemaSource">) => DynamicSchemaField<N>;
268
+ /**
269
+ * Creates an array field containing repeating items.
270
+ *
271
+ * Use this for lists of values (e.g., multiple addresses, line items).
272
+ *
273
+ * @example
274
+ * ```typescript
275
+ * field.array("addresses",
276
+ * field.text("street", { label: "Street" }),
277
+ * field.text("city", { label: "City" }),
278
+ * field.text("zip", { label: "ZIP Code" }),
279
+ * )
280
+ * ```
281
+ *
282
+ * @param name - The field name (used as the schema key)
283
+ * @param items - The form elements that define each array item
284
+ * @returns An ArrayField descriptor
285
+ */
286
+ array: <const N extends string, const Items extends readonly FormElement[]>(name: N, ...items: Items) => ArrayField<N, Items>;
287
+ /**
288
+ * Creates an array field with additional configuration options.
289
+ *
290
+ * @example
291
+ * ```typescript
292
+ * field.arrayWithConfig("lineItems", {
293
+ * label: "Line Items",
294
+ * minItems: 1,
295
+ * maxItems: 10,
296
+ * },
297
+ * field.text("description"),
298
+ * field.number("quantity"),
299
+ * )
300
+ * ```
301
+ *
302
+ * @param name - The field name (used as the schema key)
303
+ * @param config - Configuration for label, minItems, maxItems, etc.
304
+ * @param items - The form elements that define each array item
305
+ * @returns An ArrayField descriptor
306
+ */
307
+ arrayWithConfig: <const N extends string, const Items extends readonly FormElement[]>(name: N, config: Omit<ArrayField<N, Items>, "_type" | "_field" | "name" | "items">, ...items: Items) => ArrayField<N, Items>;
308
+ /**
309
+ * Creates an object field containing nested properties.
310
+ *
311
+ * Use this for grouping related fields under a single key in the schema.
312
+ *
313
+ * @example
314
+ * ```typescript
315
+ * field.object("address",
316
+ * field.text("street", { label: "Street" }),
317
+ * field.text("city", { label: "City" }),
318
+ * field.text("zip", { label: "ZIP Code" }),
319
+ * )
320
+ * ```
321
+ *
322
+ * @param name - The field name (used as the schema key)
323
+ * @param properties - The form elements that define the object's properties
324
+ * @returns An ObjectField descriptor
325
+ */
326
+ object: <const N extends string, const Properties extends readonly FormElement[]>(name: N, ...properties: Properties) => ObjectField<N, Properties>;
327
+ /**
328
+ * Creates an object field with additional configuration options.
329
+ *
330
+ * @example
331
+ * ```typescript
332
+ * field.objectWithConfig("billingAddress", { label: "Billing Address", required: true },
333
+ * field.text("street"),
334
+ * field.text("city"),
335
+ * )
336
+ * ```
337
+ *
338
+ * @param name - The field name (used as the schema key)
339
+ * @param config - Configuration for label, required, etc.
340
+ * @param properties - The form elements that define the object's properties
341
+ * @returns An ObjectField descriptor
342
+ */
343
+ objectWithConfig: <const N extends string, const Properties extends readonly FormElement[]>(name: N, config: Omit<ObjectField<N, Properties>, "_type" | "_field" | "name" | "properties">, ...properties: Properties) => ObjectField<N, Properties>;
344
+ };
345
+
346
+ /**
347
+ * Utility type that flattens intersection types into a single object type.
348
+ *
349
+ * This improves TypeScript's display of inferred types and ensures
350
+ * structural equality checks work correctly.
351
+ *
352
+ * @example
353
+ * ```typescript
354
+ * // Without FlattenIntersection:
355
+ * type Messy = { a: string } & { b: number };
356
+ * // Displays as: { a: string } & { b: number }
357
+ *
358
+ * // With FlattenIntersection:
359
+ * type Clean = FlattenIntersection<{ a: string } & { b: number }>;
360
+ * // Displays as: { a: string; b: number }
361
+ * ```
362
+ *
363
+ * @public
364
+ */
365
+ export declare type FlattenIntersection<T> = {
366
+ [K in keyof T]: T[K];
367
+ } & {};
368
+
369
+ /**
370
+ * Creates a complete form specification.
371
+ *
372
+ * The structure IS the definition:
373
+ * - Nesting with `group()` defines visual layout
374
+ * - Nesting with `when()` defines conditional visibility
375
+ * - Field type implies control type (text field → text input)
376
+ * - Array position implies field ordering
377
+ *
378
+ * Schema is automatically inferred from all fields in the structure.
379
+ *
380
+ * @example
381
+ * ```typescript
382
+ * const InvoiceForm = formspec(
383
+ * group("Customer",
384
+ * field.text("customerName", { label: "Customer Name" }),
385
+ * field.dynamicEnum("country", "fetch_countries", { label: "Country" }),
386
+ * ),
387
+ * group("Invoice Details",
388
+ * field.number("amount", { label: "Amount", min: 0 }),
389
+ * field.enum("status", ["draft", "sent", "paid"] as const),
390
+ * when(is("status", "draft"),
391
+ * field.text("internalNotes", { label: "Internal Notes" }),
392
+ * ),
393
+ * ),
394
+ * );
395
+ * ```
396
+ *
397
+ * @param elements - The top-level form elements
398
+ * @returns A FormSpec descriptor
399
+ *
400
+ * @public
401
+ */
402
+ export declare function formspec<const Elements extends readonly FormElement[]>(...elements: Elements): FormSpec<Elements>;
403
+
404
+ /**
405
+ * Options for creating a form specification.
406
+ *
407
+ * @public
408
+ */
409
+ export declare interface FormSpecOptions {
410
+ /**
411
+ * Whether to validate the form structure.
412
+ * - `true` or `"warn"`: Validate and log warnings/errors to console
413
+ * - `"throw"`: Validate and throw an error if validation fails
414
+ * - `false`: Skip validation (default in production for performance)
415
+ *
416
+ * @defaultValue false
417
+ */
418
+ validate?: boolean | "warn" | "throw";
419
+ /**
420
+ * Optional name for the form (used in validation messages).
421
+ */
422
+ name?: string;
423
+ }
424
+
425
+ /**
426
+ * Creates a complete form specification with validation options.
427
+ *
428
+ * @example
429
+ * ```typescript
430
+ * const form = formspecWithValidation(
431
+ * { validate: true, name: "MyForm" },
432
+ * field.text("name"),
433
+ * field.enum("status", ["draft", "sent"] as const),
434
+ * when(is("status", "draft"),
435
+ * field.text("notes"),
436
+ * ),
437
+ * );
438
+ * ```
439
+ *
440
+ * @param options - Validation options
441
+ * @param elements - The top-level form elements
442
+ * @returns A FormSpec descriptor
443
+ *
444
+ * @public
445
+ */
446
+ export declare function formspecWithValidation<const Elements extends readonly FormElement[]>(options: FormSpecOptions, ...elements: Elements): FormSpec<Elements>;
447
+
448
+ /**
449
+ * Creates a visual group of form elements.
450
+ *
451
+ * Groups provide visual organization and can be rendered as fieldsets or sections.
452
+ * The nesting of groups defines the visual hierarchy of the form.
453
+ *
454
+ * @example
455
+ * ```typescript
456
+ * group("Customer Information",
457
+ * field.text("name", { label: "Name" }),
458
+ * field.text("email", { label: "Email" }),
459
+ * )
460
+ * ```
461
+ *
462
+ * @param label - The group's display label
463
+ * @param elements - The form elements contained in this group
464
+ * @returns A Group descriptor
465
+ *
466
+ * @public
467
+ */
468
+ export declare function group<const Elements extends readonly FormElement[]>(label: string, ...elements: Elements): Group<Elements>;
469
+
470
+ /**
471
+ * Infers the value type from a single field.
472
+ *
473
+ * - TextField returns string
474
+ * - NumberField returns number
475
+ * - BooleanField returns boolean
476
+ * - StaticEnumField returns union of option literals
477
+ * - DynamicEnumField returns DataSourceValueType (usually string)
478
+ * - DynamicSchemaField returns Record of string to unknown
479
+ * - ArrayField returns array of inferred item schema
480
+ * - ObjectField returns object of inferred property schema
481
+ *
482
+ * @example
483
+ * ```typescript
484
+ * // Simple fields
485
+ * type T1 = InferFieldValue<TextField<"name">>; // string
486
+ * type T2 = InferFieldValue<NumberField<"age">>; // number
487
+ *
488
+ * // Enum fields
489
+ * type T3 = InferFieldValue<StaticEnumField<"status", ["draft", "sent"]>>; // "draft" | "sent"
490
+ *
491
+ * // Nested fields
492
+ * type T4 = InferFieldValue<ArrayField<"items", [TextField<"name">]>>; // { name: string }[]
493
+ * type T5 = InferFieldValue<ObjectField<"address", [TextField<"city">]>>; // { city: string }
494
+ * ```
495
+ *
496
+ * @public
497
+ */
498
+ export declare type InferFieldValue<F> = F extends TextField<string> ? string : F extends NumberField<string> ? number : F extends BooleanField<string> ? boolean : F extends StaticEnumField<string, infer O extends readonly EnumOptionValue[]> ? O extends readonly EnumOption[] ? O[number]["id"] : O extends readonly string[] ? O[number] : never : F extends DynamicEnumField<string, infer Source> ? DataSourceValueType<Source> : F extends DynamicSchemaField<string> ? Record<string, unknown> : F extends ArrayField<string, infer Items extends readonly FormElement[]> ? InferSchema<Items>[] : F extends ObjectField<string, infer Properties extends readonly FormElement[]> ? InferSchema<Properties> : never;
499
+
500
+ /**
501
+ * Infers the schema type from a FormSpec.
502
+ *
503
+ * Convenience type that extracts elements and infers the schema.
504
+ *
505
+ * @example
506
+ * ```typescript
507
+ * const form = formspec(...);
508
+ * type Schema = InferFormSchema<typeof form>;
509
+ * ```
510
+ *
511
+ * @public
512
+ */
513
+ export declare type InferFormSchema<F extends FormSpec<readonly FormElement[]>> = F extends FormSpec<infer Elements> ? InferSchema<Elements> : never;
514
+
515
+ /**
516
+ * Infers the complete schema type from a form's elements.
517
+ *
518
+ * This is the main inference type that converts a form structure
519
+ * into its corresponding TypeScript schema type.
520
+ *
521
+ * Non-conditional fields are required, conditional fields are optional.
522
+ *
523
+ * @example
524
+ * ```typescript
525
+ * const form = formspec(
526
+ * field.text("name"),
527
+ * field.number("age"),
528
+ * field.enum("status", ["active", "inactive"] as const),
529
+ * );
530
+ *
531
+ * type Schema = InferSchema<typeof form.elements>;
532
+ * // { name: string; age: number; status: "active" | "inactive" }
533
+ *
534
+ * // Conditional fields become optional:
535
+ * const formWithConditional = formspec(
536
+ * field.enum("type", ["a", "b"] as const),
537
+ * when(is("type", "a"), field.text("aField")),
538
+ * );
539
+ * type ConditionalSchema = InferSchema<typeof formWithConditional.elements>;
540
+ * // { type: "a" | "b"; aField?: string }
541
+ * ```
542
+ *
543
+ * @public
544
+ */
545
+ export declare type InferSchema<Elements extends readonly FormElement[]> = FlattenIntersection<BuildSchema<ExtractNonConditionalFieldsFromArray<Elements>> & Partial<BuildSchema<ExtractConditionalFieldsFromArray<Elements>>>>;
546
+
547
+ /**
548
+ * Creates an equality predicate that checks if a field equals a specific value.
549
+ *
550
+ * Use this with `when()` to create readable conditional expressions:
551
+ *
552
+ * @example
553
+ * ```typescript
554
+ * // Show cardNumber field when paymentMethod is "card"
555
+ * when(is("paymentMethod", "card"),
556
+ * field.text("cardNumber", { label: "Card Number" }),
557
+ * )
558
+ * ```
559
+ *
560
+ * @typeParam K - The field name (inferred as string literal)
561
+ * @typeParam V - The value type (inferred as literal)
562
+ * @param field - The name of the field to check
563
+ * @param value - The value the field must equal
564
+ * @returns An EqualsPredicate for use with `when()`
565
+ * @public
566
+ */
567
+ export declare function is<const K extends string, const V>(field: K, value: V): EqualsPredicate<K, V>;
568
+
569
+ /**
570
+ * Logs validation issues to the console.
571
+ *
572
+ * @param result - The validation result to log
573
+ * @param formName - Optional name for the form (for better error messages)
574
+ *
575
+ * @public
576
+ */
577
+ export declare function logValidationIssues(result: ValidationResult, formName?: string): void;
578
+
579
+ /**
580
+ * Validates a form specification for common issues.
581
+ *
582
+ * Checks for:
583
+ * - Duplicate field names at the root level (warning)
584
+ * - References to non-existent fields in conditionals (error)
585
+ *
586
+ * @example
587
+ * ```typescript
588
+ * const form = formspec(
589
+ * field.text("name"),
590
+ * field.text("name"), // Duplicate!
591
+ * when("nonExistent", "value", // Reference to non-existent field!
592
+ * field.text("extra"),
593
+ * ),
594
+ * );
595
+ *
596
+ * const result = validateForm(form.elements);
597
+ * // result.valid === false
598
+ * // result.issues contains duplicate and reference errors
599
+ * ```
600
+ *
601
+ * @param elements - The form elements to validate
602
+ * @returns Validation result with any issues found
603
+ *
604
+ * @public
605
+ */
606
+ export declare function validateForm(elements: readonly FormElement[]): ValidationResult;
607
+
608
+ /**
609
+ * A validation issue found in a form specification.
610
+ *
611
+ * @public
612
+ */
613
+ export declare interface ValidationIssue {
614
+ /** Severity of the issue */
615
+ severity: ValidationSeverity;
616
+ /** Human-readable message describing the issue */
617
+ message: string;
618
+ /** Path to the element with the issue (e.g., "group.fieldName") */
619
+ path: string;
620
+ }
621
+
622
+ /**
623
+ * Result of validating a form specification.
624
+ *
625
+ * @public
626
+ */
627
+ export declare interface ValidationResult {
628
+ /** Whether the form is valid (no errors, warnings are ok) */
629
+ valid: boolean;
630
+ /** List of validation issues found */
631
+ issues: ValidationIssue[];
632
+ }
633
+
634
+ /**
635
+ * Validation issue severity levels.
636
+ *
637
+ * @public
638
+ */
639
+ export declare type ValidationSeverity = "error" | "warning";
640
+
641
+ /**
642
+ * Creates a conditional wrapper that shows elements based on a predicate.
643
+ *
644
+ * When the predicate evaluates to true, the contained elements are shown.
645
+ * Otherwise, they are hidden (but still part of the schema).
646
+ *
647
+ * @example
648
+ * ```typescript
649
+ * import { is } from "@formspec/dsl";
650
+ *
651
+ * field.enum("status", ["draft", "sent", "paid"] as const),
652
+ * when(is("status", "draft"),
653
+ * field.text("internalNotes", { label: "Internal Notes" }),
654
+ * )
655
+ * ```
656
+ *
657
+ * @param predicate - The condition to evaluate (use `is()` to create)
658
+ * @param elements - The form elements to show when condition is met
659
+ * @returns A Conditional descriptor
660
+ *
661
+ * @public
662
+ */
663
+ export declare function when<const K extends string, const V, const Elements extends readonly FormElement[]>(predicate: Predicate<K, V>, ...elements: Elements): Conditional<K, V, Elements>;
664
+
665
+ export { }
package/dist/field.d.ts CHANGED
@@ -17,6 +17,8 @@ import type { TextField, NumberField, BooleanField, StaticEnumField, EnumOptionV
17
17
  * field.enum("status", ["draft", "sent", "paid"]);
18
18
  * field.dynamicEnum("country", "countries", { label: "Country" });
19
19
  * ```
20
+ *
21
+ * @public
20
22
  */
21
23
  export declare const field: {
22
24
  /**
@@ -52,10 +54,22 @@ export declare const field: {
52
54
  * // Schema type: "draft" | "sent" | "paid"
53
55
  * ```
54
56
  *
57
+ * Options can be strings or objects with `id` and `label`:
58
+ * ```typescript
59
+ * field.enum("priority", [
60
+ * { id: "low", label: "Low Priority" },
61
+ * { id: "high", label: "High Priority" },
62
+ * ])
63
+ * ```
64
+ *
65
+ * **Note:** All options must be of the same type (all strings OR all objects).
66
+ * Mixing strings and objects will throw a runtime error.
67
+ *
55
68
  * @param name - The field name (used as the schema key)
56
- * @param options - Array of allowed string values
69
+ * @param options - Array of allowed string values or objects with `id` and `label` properties
57
70
  * @param config - Optional configuration for label, etc.
58
71
  * @returns A StaticEnumField descriptor
72
+ * @throws Error if options array contains mixed types (strings and objects)
59
73
  */
60
74
  enum: <const N extends string, const O extends readonly EnumOptionValue[]>(name: N, options: O, config?: Omit<StaticEnumField<N, O>, "_type" | "_field" | "name" | "options">) => StaticEnumField<N, O>;
61
75
  /**