@petrarca/sonnet-core 0.4.2 → 0.6.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.
@@ -1,3 +1,4 @@
1
+ import Ajv from 'ajv';
1
2
  import { z } from 'zod';
2
3
 
3
4
  /**
@@ -7,6 +8,37 @@ import { z } from 'zod';
7
8
  * for form/widget integration. They are domain-agnostic -- no
8
9
  * application-specific entity concepts live here.
9
10
  */
11
+ /**
12
+ * A declarative presentation rule (JSONForms-style) attached to a field.
13
+ *
14
+ * The rule lives on the field it controls (the target). Its `condition` points
15
+ * at another field's value via a JSON Pointer (RFC 6901) into the form DATA
16
+ * (rooted at the whole form), and validates that value against `condition.schema`.
17
+ * When the condition holds, `effect` is applied to the target:
18
+ * - "show" -- render only when the condition holds
19
+ * - "hide" -- render only when the condition does NOT hold
20
+ * - "enable" -- interactive only when the condition holds
21
+ * - "disable"-- non-interactive when the condition holds
22
+ *
23
+ * Example: show `pregnancy_status` when `gender` equals "female":
24
+ * ```json
25
+ * { "effect": "show",
26
+ * "condition": { "scope": "/gender", "schema": { "const": "female" } } }
27
+ * ```
28
+ * Unlike if/then/else, the target field stays declared at its own position, so
29
+ * layout placement is stable.
30
+ */
31
+ interface UIRuleCondition {
32
+ /** JSON Pointer (RFC 6901) into the form data, e.g. "/gender" or "/patient/gender". */
33
+ scope: string;
34
+ /** Schema the value at `scope` is validated against (e.g. { const: "female" }). */
35
+ schema: JsonSchemaProperty;
36
+ }
37
+ type UIRuleEffect = "show" | "hide" | "enable" | "disable";
38
+ interface UIRule {
39
+ effect: UIRuleEffect;
40
+ condition: UIRuleCondition;
41
+ }
10
42
  /**
11
43
  * JSON Schema property definition following Draft 2020-12 specification.
12
44
  * Supports basic types, validation constraints, conditionals, and custom extensions.
@@ -24,20 +56,34 @@ type JsonSchemaProperty = {
24
56
  maxLength?: number;
25
57
  minimum?: number;
26
58
  maximum?: number;
59
+ exclusiveMinimum?: number;
60
+ exclusiveMaximum?: number;
27
61
  items?: JsonSchemaProperty;
28
62
  minItems?: number;
29
63
  maxItems?: number;
30
64
  properties?: Record<string, JsonSchemaProperty>;
31
65
  required?: string[];
32
66
  $ref?: string;
67
+ $defs?: Record<string, JsonSchemaProperty>;
68
+ definitions?: Record<string, JsonSchemaProperty>;
33
69
  allOf?: JsonSchemaProperty[];
34
70
  anyOf?: JsonSchemaProperty[];
35
71
  oneOf?: JsonSchemaProperty[];
36
72
  not?: JsonSchemaProperty;
73
+ /**
74
+ * Discriminator for a `oneOf` union (OpenAPI convention). `propertyName` is
75
+ * the shared property whose `const` value tags the active branch. When
76
+ * absent, the discriminator is auto-detected as the property that is const
77
+ * in every branch.
78
+ */
79
+ discriminator?: {
80
+ propertyName: string;
81
+ };
37
82
  if?: JsonSchemaProperty;
38
83
  then?: JsonSchemaProperty;
39
84
  else?: JsonSchemaProperty;
40
85
  "x-ui-order"?: string[];
86
+ "x-ui-rule"?: UIRule;
41
87
  "x-ui-widget"?: string;
42
88
  "x-ui-options"?: Record<string, any>;
43
89
  "x-ui-layout"?: UILayoutConfig;
@@ -85,6 +131,21 @@ type JsonSchema = {
85
131
  "x-ui-order"?: string[];
86
132
  [key: `x-${string}`]: unknown;
87
133
  };
134
+ /**
135
+ * WidthToken -- the shared semantic sizing vocabulary used across the form
136
+ * layout system: per-field slot width (x-ui-width) and per-column width
137
+ * (UILayoutRowConfig.widths). "full" fills available space; sm/md/lg are
138
+ * fixed inline sizes. Raw CSS remains available as an advanced escape hatch.
139
+ */
140
+ type WidthToken = "sm" | "md" | "lg" | "full";
141
+ /**
142
+ * The fixed rem values for each non-full WidthToken. This is the single source
143
+ * of truth for token sizing used by:
144
+ * - the CSS grid track map in JsonSchemaFormRenderer (sm -> "8rem" track)
145
+ * - the Tailwind class map in FormFieldRenderer (sm -> w-32 = 8rem)
146
+ * Consumers that need px can multiply by 16.
147
+ */
148
+ declare const WIDTH_TOKEN_REMS: Record<WidthToken, string>;
88
149
  /**
89
150
  * UISchema - User Interface Schema configuration.
90
151
  *
@@ -98,10 +159,14 @@ interface UISchema {
98
159
  "x-ui-options"?: UISchemaOptions;
99
160
  "x-ui-globalOptions"?: UISchemaOptions;
100
161
  "x-ui-order"?: string[];
162
+ /** Declarative show/hide or enable/disable rule (see UIRule). */
163
+ "x-ui-rule"?: UIRule;
101
164
  "x-ui-title"?: string | false;
102
165
  "x-ui-description"?: string;
103
166
  "x-ui-placeholder"?: string;
104
167
  "x-ui-help"?: string;
168
+ /** Semantic field-slot width (default "full"). */
169
+ "x-ui-width"?: WidthToken;
105
170
  /** Column override -- merged over schema-level x-ui-column, UI schema wins. */
106
171
  "x-ui-column"?: JsonSchemaProperty["x-ui-column"] & {
107
172
  label?: string;
@@ -123,18 +188,70 @@ interface UISchema {
123
188
  * UILayoutRowConfig -- one horizontal row in a multi-column object form layout.
124
189
  *
125
190
  * `fields` lists the field names to place in this row, left to right.
126
- * `columns` is a raw CSS grid-template-columns string (default: equal 1fr
127
- * per field). `gap` overrides the parent layout gap for this row only.
191
+ * Column sizing (grid mode) is resolved in this precedence:
192
+ * 1. `columns` -- raw CSS grid-template-columns (advanced escape hatch).
193
+ * 2. `widths` -- per-cell semantic tokens (the designer-friendly path).
194
+ * 3. default -- equal 1fr per field.
195
+ * `gap` overrides the parent layout gap for this row only.
196
+ *
197
+ * `group` renders the row inside a titled fieldset (presentation-only grouping
198
+ * over flat schema fields -- the data shape is unchanged).
128
199
  *
129
200
  * Examples:
130
- * { fields: ["first_name", "last_name"], columns: "1fr 1fr" }
201
+ * { fields: ["first_name", "last_name"], widths: ["md", "full"] }
131
202
  * { fields: ["npi", "specialty", "email"], columns: "20% 1fr 1fr" }
132
- * { fields: ["start_date", "end_date"], columns: "200px 1fr", gap: 8 }
203
+ * { group: "Contact", fields: ["email", "phone"] }
133
204
  */
134
205
  interface UILayoutRowConfig {
135
206
  fields: string[];
136
207
  columns?: string;
208
+ /**
209
+ * Per-cell semantic widths, aligned to `fields` by index. Translated to grid
210
+ * tracks: sm/md/lg -> fixed rem tracks, full -> 1fr. Ignored when `columns`
211
+ * is set (raw CSS wins) or in "flow" mode. Missing/short entries default to
212
+ * "full".
213
+ */
214
+ widths?: WidthToken[];
137
215
  gap?: number;
216
+ /**
217
+ * Optional title. When set, the row renders inside a <fieldset> with a
218
+ * <legend>, grouping its fields visually without nesting them in the schema.
219
+ */
220
+ group?: string;
221
+ /**
222
+ * How this row arranges its fields:
223
+ * - "grid" (default): CSS grid using `columns`/`widths` (equal 1fr per field
224
+ * when neither is set).
225
+ * - "flow": flex row, fields at their natural width, left-to-right, wrapping.
226
+ * Use for compact toolbars / tight field groups. `columns`/`widths` are
227
+ * ignored.
228
+ */
229
+ arrange?: "grid" | "flow";
230
+ }
231
+ /**
232
+ * One named group of a grouped form layout. A group holds rows of fields under
233
+ * a label; the data stays flat (grouping is presentation only, like `group` on
234
+ * a single row). How groups are shown -- tabs, wizard steps, stacked sections
235
+ * -- is chosen by {@link UILayoutConfig.variant}, not by this structure.
236
+ *
237
+ * A group may carry an `x-ui-rule`-style {@link UIRule} to appear conditionally
238
+ * (e.g. show an "Address" group only when "provide address" is checked). The
239
+ * rule is evaluated against the form data exactly as a field-level rule is.
240
+ */
241
+ interface UILayoutGroupConfig {
242
+ /** Label shown on the group's trigger or heading. */
243
+ label: string;
244
+ /** Rows of fields in this group (same shape as UILayoutConfig.rows). */
245
+ rows: UILayoutRowConfig[];
246
+ /**
247
+ * Optional show/hide rule for the whole group: a hidden group disappears
248
+ * along with its fields. Narrowed to the visibility effects -- a disabled
249
+ * group has no meaning, and permitting the effect would let it be authored
250
+ * and then silently ignored.
251
+ */
252
+ rule?: UIRule & {
253
+ effect: "show" | "hide";
254
+ };
138
255
  }
139
256
  /**
140
257
  * UILayoutConfig -- layout configuration for ObjectWidget and ArrayWidget.
@@ -145,10 +262,13 @@ interface UILayoutRowConfig {
145
262
  interface UILayoutConfig {
146
263
  direction?: "vertical" | "horizontal";
147
264
  rows?: UILayoutRowConfig[];
265
+ groups?: UILayoutGroupConfig[];
266
+ variant?: "tabs" | "stepper" | "sections" | (string & {});
148
267
  columns?: string;
149
268
  gap?: number;
150
269
  labels?: boolean;
151
270
  compact?: boolean;
271
+ labelPlacement?: "top" | "start";
152
272
  }
153
273
  /**
154
274
  * ValueChange -- a single scalar or nested-object field that changed value.
@@ -218,6 +338,12 @@ interface UISchemaOptions {
218
338
  rows?: number;
219
339
  searchable?: boolean;
220
340
  clearable?: boolean;
341
+ /** Allow the user to type and submit a value not in the enum list (combobox / open-choice). */
342
+ creatable?: boolean;
343
+ /** Label placement: "top" (default, above) or "start" (inline, RTL-safe). */
344
+ labelPlacement?: "top" | "start";
345
+ /** Semantic field-slot width (default "full"). */
346
+ width?: WidthToken;
221
347
  [key: string]: any;
222
348
  }
223
349
 
@@ -277,6 +403,38 @@ declare function getRequiredFields(schema: JsonSchema): string[];
277
403
  declare function getOptionalFields(schema: JsonSchema): string[];
278
404
  declare function hasProperties(schema: JsonSchema): boolean;
279
405
 
406
+ /**
407
+ * oneOf (discriminated-union) helpers.
408
+ *
409
+ * A oneOf property is an object whose shape is one of several branches, modeled
410
+ * as `oneOf: [branchSchema, ...]`. Each branch shares a discriminator property
411
+ * -- a field with a `const` value that tags which branch is active. The
412
+ * discriminator is named by `discriminator.propertyName` (OpenAPI convention),
413
+ * or auto-detected as the property that carries a `const` in every branch.
414
+ *
415
+ * These helpers are pure and domain-agnostic.
416
+ */
417
+
418
+ /** Whether a property is a oneOf union (has a non-empty `oneOf`). */
419
+ declare function isOneOf(property: JsonSchemaProperty): boolean;
420
+ /**
421
+ * Resolve the discriminator property name for a oneOf union.
422
+ *
423
+ * Uses `discriminator.propertyName` when set; otherwise auto-detects the single
424
+ * property that carries a `const` in every branch. Returns undefined when no
425
+ * consistent discriminator can be determined.
426
+ */
427
+ declare function getDiscriminator(property: JsonSchemaProperty): string | undefined;
428
+ /** The discriminator const value for a branch, or undefined. */
429
+ declare function getBranchValue(branch: JsonSchemaProperty, discriminator: string): string | number | boolean | undefined;
430
+ /** Human label for a branch: its `title`, else the discriminator value. */
431
+ declare function getBranchLabel(branch: JsonSchemaProperty, discriminator: string | undefined, index: number): string;
432
+ /**
433
+ * Find the branch that matches the current form data by comparing the
434
+ * discriminator value. Returns the branch index, or -1 when none match.
435
+ */
436
+ declare function findActiveBranch(property: JsonSchemaProperty, data: Record<string, unknown> | undefined, discriminator: string | undefined): number;
437
+
280
438
  /**
281
439
  * Schema Resolver - Resolves conditional schemas (if-then-else, allOf, etc.)
282
440
  *
@@ -293,6 +451,7 @@ declare function hasProperties(schema: JsonSchema): boolean;
293
451
  * @module services/schemaResolver
294
452
  */
295
453
 
454
+ declare const ajv: Ajv;
296
455
  /**
297
456
  * Resolve conditional schema based on form data
298
457
  *
@@ -363,6 +522,33 @@ declare function matchesConst(value: any, constValue: any): boolean;
363
522
  */
364
523
  declare function hasConditionals(schema: JsonSchema): boolean;
365
524
 
525
+ /**
526
+ * Declarative UI rules (x-ui-rule): show/hide or enable/disable a field based
527
+ * on another field's value.
528
+ *
529
+ * The rule lives on the target field. Its condition references a value elsewhere
530
+ * in the form data via a JSON Pointer (RFC 6901) rooted at the whole form, and
531
+ * validates that value against a schema. Evaluation yields presentation flags
532
+ * (visible / enabled) the renderer applies -- the field itself stays declared at
533
+ * its own position, so layout is stable (unlike if/then/else).
534
+ */
535
+
536
+ /** The presentation state a rule resolves to for its target field. */
537
+ interface RuleState {
538
+ visible: boolean;
539
+ enabled: boolean;
540
+ }
541
+ /**
542
+ * Resolve a JSON Pointer against a data object. Returns the value at the pointer,
543
+ * or undefined when any segment is missing. "" (empty pointer) returns the root.
544
+ */
545
+ declare function getAtPointer(data: unknown, pointer: string): unknown;
546
+ /**
547
+ * Resolve a field's presentation state from its `x-ui-rule` (if any) against the
548
+ * current form data. Fields without a rule are visible and enabled.
549
+ */
550
+ declare function evaluateUiRule(rule: UIRule | undefined, formData: unknown): RuleState;
551
+
366
552
  /**
367
553
  * Schema utilities
368
554
  *
@@ -469,42 +655,67 @@ declare function generateJsonSchemaFromVariables(variables: string[], existingSc
469
655
  declare function toJsonSchema(schema: z.ZodType): JsonSchema;
470
656
 
471
657
  /**
472
- * $ref Resolution -- resolves local JSON Schema references.
658
+ * $ref Resolution -- resolves JSON Schema references.
473
659
  *
474
- * Walks the schema tree and replaces { "$ref": "#/$defs/name" } nodes
475
- * with the corresponding sub-schema from the document's $defs (Draft
476
- * 2020-12) or definitions (Draft 7) block.
660
+ * Walks the schema tree and replaces `{ "$ref": ... }` nodes with the
661
+ * corresponding sub-schema. By default it resolves local, in-document refs:
662
+ * `#/$defs/name` (Draft 2020-12) or `#/definitions/name` (Draft 7).
477
663
  *
478
- * Properties on the referencing node are merged on top of the resolved
479
- * target, so context-specific overrides (e.g. a custom title) work
480
- * without duplicating the definition.
664
+ * A caller may inject a `RefResolver` to resolve ANY ref string (e.g. a schema
665
+ * registry id like `sdf_...`, or a canonical URI). The injected resolver is
666
+ * tried first; the in-document `$defs`/`definitions` lookup is the fallback, so
667
+ * both ref styles work together. The resolver is synchronous: consumers that
668
+ * fetch schemas asynchronously (e.g. from a registry over HTTP) pre-load them
669
+ * into a map and pass a `(ref) => map[ref]` lookup. This keeps the toolkit pure
670
+ * (no HTTP, no async).
481
671
  *
482
- * Only local refs (starting with "#/") are supported. External URIs
483
- * are left as-is -- a future pluggable SchemaResolver interface will
484
- * handle those.
672
+ * Properties on the referencing node are merged on top of the resolved target,
673
+ * so context-specific overrides (e.g. a custom title) work without duplicating
674
+ * the definition. Refs that no resolver can satisfy are left as-is.
485
675
  */
486
676
 
677
+ /** Resolve a `$ref` string to its target sub-schema, or undefined if unknown. */
678
+ type RefResolver = (ref: string) => JsonSchemaProperty | undefined;
487
679
  /**
488
- * Resolve all local $ref pointers in a JSON Schema document.
680
+ * Resolve `$ref` pointers in a JSON Schema document.
489
681
  *
490
- * Returns the schema unchanged if it contains no $defs/definitions
491
- * or no $ref nodes. The original schema object is never mutated.
682
+ * By default resolves in-document `#/$defs` / `#/definitions` refs. Pass a
683
+ * `resolve` to also resolve registry/URI refs; it is tried first and the
684
+ * in-document lookup is the fallback. The original schema is never mutated;
685
+ * a document with no refs to resolve is returned unchanged.
492
686
  */
493
- declare function resolveRefs(schema: JsonSchema): JsonSchema;
687
+ declare function resolveRefs(schema: JsonSchema, resolve?: RefResolver): JsonSchema;
494
688
 
495
689
  /**
496
690
  * Recursive form data validation against a JSON Schema.
497
691
  *
498
692
  * Validates required fields at every nesting level: top-level properties,
499
693
  * nested objects (type: "object"), and array items (type: "array" with
500
- * items.type: "object"). Returns a flat list of human-readable error
501
- * strings and a boolean summary.
694
+ * items.type: "object"). Returns human-readable error strings, the same
695
+ * errors keyed by JSON Pointer, and a boolean summary.
696
+ *
697
+ * The pointer form lets callers attribute an error to a specific field --
698
+ * needed to surface errors that are not currently on screen (e.g. a badge on
699
+ * a form tab whose fields are hidden). `errors` remains the flat message list.
502
700
  *
503
701
  * Pure function -- no React, no side effects.
504
702
  */
505
703
 
704
+ /** A validation error attributed to the field that produced it. */
705
+ interface FieldError {
706
+ /**
707
+ * JSON Pointer (RFC 6901) to the offending field, e.g. `/email`,
708
+ * `/contact/email`, or `/tags/2/label` for the third array item.
709
+ */
710
+ pointer: string;
711
+ /** Human-readable message, e.g. "Contact > Email is required". */
712
+ message: string;
713
+ }
506
714
  interface ValidationResult {
715
+ /** Flat, human-readable messages (in field declaration order). */
507
716
  errors: string[];
717
+ /** The same errors, each attributed to its field via a JSON Pointer. */
718
+ fieldErrors: FieldError[];
508
719
  isValid: boolean;
509
720
  }
510
721
  /**
@@ -530,4 +741,4 @@ declare function validateFormData(schema: JsonSchema, data: Record<string, unkno
530
741
  */
531
742
  declare function diffFormData(original: Record<string, unknown>, updated: Record<string, unknown>): FormDiff;
532
743
 
533
- export { type ArrayItemChange, type FieldDiff, type FormDiff, type JsonSchema, type JsonSchemaProperty, type UILayoutConfig, type UILayoutRowConfig, type UISchema, type UISchemaOptions, type ValidationResult, type ValueChange, applySchemaDefaults, diffFormData, extractEnumValues, extractSchemaDefaults, generateJsonSchemaFromVariables, getFieldConstraints, getFieldDefault, getFieldHelpText, getFieldPlaceholder, getFieldTitle, getFieldType, getFieldWidget, getOptionalFields, getRequiredFields, getSchemaProperties, hasConditionals, hasProperties, isFieldOptional, isFieldRequired, isNullableBoolean, matchesConst, resolveRefs, resolveSchema, toJsonSchema, validateFormData, validateSchema };
744
+ export { type ArrayItemChange, type FieldDiff, type FieldError, type FormDiff, type JsonSchema, type JsonSchemaProperty, type RefResolver, type RuleState, type UILayoutConfig, type UILayoutGroupConfig, type UILayoutRowConfig, type UIRule, type UIRuleCondition, type UIRuleEffect, type UISchema, type UISchemaOptions, type ValidationResult, type ValueChange, WIDTH_TOKEN_REMS, type WidthToken, ajv, applySchemaDefaults, diffFormData, evaluateUiRule, extractEnumValues, extractSchemaDefaults, findActiveBranch, generateJsonSchemaFromVariables, getAtPointer, getBranchLabel, getBranchValue, getDiscriminator, getFieldConstraints, getFieldDefault, getFieldHelpText, getFieldPlaceholder, getFieldTitle, getFieldType, getFieldWidget, getOptionalFields, getRequiredFields, getSchemaProperties, hasConditionals, hasProperties, isFieldOptional, isFieldRequired, isNullableBoolean, isOneOf, matchesConst, resolveRefs, resolveSchema, toJsonSchema, validateFormData, validateSchema };