@frt-platform/report-core 1.1.0 → 1.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,14 +1,26 @@
1
- # @frt/report-core
1
+ # `@frt-platform/report-core`
2
2
 
3
- Core engine for building, validating, and migrating **report templates**.
3
+ Core engine for building, validating, and migrating **report templates** – plus validating **report responses** and exporting **JSON Schema**.
4
4
 
5
- This package is framework-agnostic, React-free, and storage-agnostic. It gives you:
5
+ This package is:
6
6
 
7
- - A **typed schema** for report templates (templates → sections → fields)
8
- - **Zod-based validation** and parsing from unknown / JSON input
9
- - **Legacy schema migration** (flat `fields` → `sections`, old type names, etc.)
10
- - **Normalization** helpers to ensure safe IDs & consistent structure
11
- - **Field defaults** and **ID utilities** you can use to build your own UI
7
+ * **Framework-agnostic** (no React)
8
+ * **Storage-agnostic** (no DB assumptions)
9
+ * **Runtime-safe** (Zod-based validation)
10
+
11
+ It gives you:
12
+
13
+ * A **typed schema** for report templates (templates → sections → fields)
14
+ * **Zod-based validation** and parsing from unknown / JSON input
15
+ * **Legacy schema migration** (flat `fields` → `sections`, old type names, etc.)
16
+ * **Normalization** helpers to ensure safe IDs & consistent structure
17
+ * **Field defaults** and **ID utilities** you can use to build your own UI
18
+ * **Response validation** based on templates (`validateReportResponse`)
19
+ * **Conditional logic** for fields (`visibleIf` / `requiredIf`)
20
+ * **Template diffing** (`diffTemplates`) to compare template versions (including ordering)
21
+ * **Type-level response inference** (`InferResponse`) for fully typed responses
22
+ * **Field registry** (`FieldRegistry`) for custom / extensible field types
23
+ * **JSON Schema export** (`exportJSONSchema`) for integration with OpenAPI / external validators
12
24
 
13
25
  It’s the core that powers a flexible incident/report builder, but it’s generic enough to be reused in any app.
14
26
 
@@ -18,14 +30,14 @@ It’s the core that powers a flexible incident/report builder, but it’s gener
18
30
 
19
31
  ```bash
20
32
  # npm
21
- npm install @frt/report-core zod
33
+ npm install @frt-platform/report-core zod
22
34
 
23
35
  # yarn
24
- yarn add @frt/report-core zod
36
+ yarn add @frt-platform/report-core zod
25
37
 
26
38
  # pnpm
27
- pnpm add @frt/report-core zod
28
- ````
39
+ pnpm add @frt-platform/report-core zod
40
+ ```
29
41
 
30
42
  `zod` is a peer dependency and is used for schema validation & parsing.
31
43
 
@@ -38,11 +50,12 @@ The core model looks like this:
38
50
  * A **template** contains multiple **sections**
39
51
  * A **section** contains multiple **fields**
40
52
  * A **field** has a `type` (short text, long text, number, select, etc.) and optional constraints
53
+ * Fields can have **conditional logic** (`visibleIf`, `requiredIf`)
41
54
 
42
55
  Supported field types:
43
56
 
44
57
  ```ts
45
- import { REPORT_TEMPLATE_FIELD_TYPES } from "@frt/report-core";
58
+ import { REPORT_TEMPLATE_FIELD_TYPES } from "@frt-platform/report-core";
46
59
 
47
60
  console.log(REPORT_TEMPLATE_FIELD_TYPES);
48
61
  // [
@@ -53,9 +66,12 @@ console.log(REPORT_TEMPLATE_FIELD_TYPES);
53
66
  // "checkbox",
54
67
  // "singleSelect",
55
68
  // "multiSelect",
69
+ // "repeatGroup", // reserved for repeating groups / fieldsets
56
70
  // ]
57
71
  ```
58
72
 
73
+ > `repeatGroup` is reserved for repeating fieldsets (e.g. multiple injured people); the core schema knows about it and provides defaults, and the full runtime semantics can be layered on top in your app/React package.
74
+
59
75
  ---
60
76
 
61
77
  ## Quick Start
@@ -65,7 +81,7 @@ console.log(REPORT_TEMPLATE_FIELD_TYPES);
65
81
  You can define templates statically or load them from DB / API:
66
82
 
67
83
  ```ts
68
- import type { ReportTemplateSchema } from "@frt/report-core";
84
+ import type { ReportTemplateSchema } from "@frt-platform/report-core";
69
85
 
70
86
  const templateJson: ReportTemplateSchema = {
71
87
  version: 1,
@@ -81,7 +97,7 @@ const templateJson: ReportTemplateSchema = {
81
97
  type: "longText",
82
98
  label: "What happened?",
83
99
  required: true,
84
- minLength: 10
100
+ minLength: 10,
85
101
  },
86
102
  {
87
103
  id: "status",
@@ -89,30 +105,30 @@ const templateJson: ReportTemplateSchema = {
89
105
  label: "Incident status",
90
106
  required: true,
91
107
  options: ["Pending review", "Resolved", "Escalated"],
92
- defaultValue: "Resolved"
93
- }
94
- ]
95
- }
96
- ]
108
+ defaultValue: "Resolved",
109
+ },
110
+ ],
111
+ },
112
+ ],
97
113
  };
98
114
  ```
99
115
 
100
- ### 2. Validate and normalize it
116
+ ### 2. Validate and normalize the template
101
117
 
102
118
  Use the helpers to safely parse and normalize unknown input:
103
119
 
104
120
  ```ts
105
121
  import {
106
122
  parseReportTemplateSchema,
107
- normalizeReportTemplateSchema
108
- } from "@frt/report-core";
123
+ normalizeReportTemplateSchema,
124
+ } from "@frt-platform/report-core";
109
125
 
110
126
  const raw = /* from DB, file, API, etc. */ templateJson;
111
127
 
112
128
  // 1) parse + validate (throws on invalid input)
113
129
  const parsed = parseReportTemplateSchema(raw);
114
130
 
115
- // 2) optional extra normalization step (IDs, trimming, etc.)
131
+ // 2) optional extra normalization step (IDs, trimming, de-duplication, etc.)
116
132
  const normalized = normalizeReportTemplateSchema(parsed);
117
133
 
118
134
  console.log(normalized.sections[0].fields[0].id);
@@ -122,7 +138,7 @@ console.log(normalized.sections[0].fields[0].id);
122
138
  ### 3. Serialize for storage
123
139
 
124
140
  ```ts
125
- import { serializeReportTemplateSchema } from "@frt/report-core";
141
+ import { serializeReportTemplateSchema } from "@frt-platform/report-core";
126
142
 
127
143
  const jsonString = serializeReportTemplateSchema(normalized);
128
144
  // ready to store in DB, file, etc.
@@ -135,7 +151,7 @@ const jsonString = serializeReportTemplateSchema(normalized);
135
151
  If you have a JSON string (e.g. from a form textarea):
136
152
 
137
153
  ```ts
138
- import { parseReportTemplateSchemaFromString } from "@frt/report-core";
154
+ import { parseReportTemplateSchemaFromString } from "@frt-platform/report-core";
139
155
 
140
156
  try {
141
157
  const template = parseReportTemplateSchemaFromString(userInputString);
@@ -147,6 +163,152 @@ try {
147
163
 
148
164
  ---
149
165
 
166
+ ## Validating report responses
167
+
168
+ Once you have a template, you can validate **user-submitted responses** against it.
169
+
170
+ ### Runtime validation
171
+
172
+ ```ts
173
+ import { validateReportResponse } from "@frt-platform/report-core";
174
+
175
+ const template = /* a valid ReportTemplateSchema */ normalized;
176
+
177
+ const responseData = {
178
+ summary: "Student slipped in the hallway, no serious injuries.",
179
+ status: "Resolved",
180
+ };
181
+
182
+ // Validate (throws ZodError on failure)
183
+ const safeResponse = validateReportResponse(template, responseData);
184
+ ```
185
+
186
+ `validateReportResponse` respects:
187
+
188
+ * `required` flags
189
+ * `minLength` / `maxLength` (short/long text)
190
+ * `minValue` / `maxValue` (numbers)
191
+ * `minSelections` / `maxSelections` (multiSelect)
192
+ * `options` (singleSelect + multiSelect)
193
+ * `checkbox` semantics (required checkbox must be `true`)
194
+ * **Conditional logic**:
195
+
196
+ * `visibleIf`: if false, the field is treated as invisible and removed from the parsed output
197
+ * `requiredIf`: if true, the field is treated as required for this specific response
198
+
199
+ You can still build a raw schema yourself using the conditional builder:
200
+
201
+ ```ts
202
+ import { buildResponseSchemaWithConditions } from "@frt-platform/report-core";
203
+
204
+ const responseSchema = buildResponseSchemaWithConditions(template, responseData);
205
+ const safeResponse = responseSchema.parse(responseData);
206
+ ```
207
+
208
+ ---
209
+
210
+ ## Conditional logic (`visibleIf` / `requiredIf`)
211
+
212
+ Fields can declare conditions that refer to other field values.
213
+
214
+ Example:
215
+
216
+ ```json
217
+ {
218
+ "id": "injury_details",
219
+ "type": "longText",
220
+ "label": "Describe the injury",
221
+ "visibleIf": { "equals": { "injured": true } },
222
+ "requiredIf": { "equals": { "severity": "High" } }
223
+ }
224
+ ```
225
+
226
+ Supported condition shapes:
227
+
228
+ * `{"equals": { "fieldId": value } }`
229
+ * `{"any": [cond1, cond2, ...] }`
230
+ * `{"all": [cond1, cond2, ...] }`
231
+ * `{"not": cond }`
232
+
233
+ At runtime:
234
+
235
+ * If `visibleIf` is **false** → field is treated as **invisible**, becomes optional, and is stripped from the validated response.
236
+ * If `requiredIf` is **true** → field is treated as **required** for that specific response.
237
+
238
+ ---
239
+
240
+ ## Type-level response inference (`InferResponse`)
241
+
242
+ If you define templates statically (`as const`), you can get a fully-typed response type from the template:
243
+
244
+ ```ts
245
+ import type { InferResponse } from "@frt-platform/report-core";
246
+
247
+ const incidentTemplate = {
248
+ version: 1,
249
+ sections: [
250
+ {
251
+ id: "section-overview",
252
+ fields: [
253
+ {
254
+ id: "summary",
255
+ type: "longText",
256
+ label: "What happened?",
257
+ required: true,
258
+ minLength: 10,
259
+ },
260
+ {
261
+ id: "status",
262
+ type: "singleSelect",
263
+ label: "Status",
264
+ required: true,
265
+ options: ["Pending", "Resolved", "Escalated"] as const,
266
+ },
267
+ {
268
+ id: "follow_up_date",
269
+ type: "date",
270
+ label: "Follow-up date",
271
+ },
272
+ {
273
+ id: "tags",
274
+ type: "multiSelect",
275
+ label: "Tags",
276
+ options: ["Safety", "Medical", "Security"] as const,
277
+ },
278
+ {
279
+ id: "confirmed",
280
+ type: "checkbox",
281
+ label: "I confirm this report is accurate",
282
+ required: true,
283
+ },
284
+ ],
285
+ },
286
+ ],
287
+ } as const;
288
+
289
+ type IncidentResponse = InferResponse<typeof incidentTemplate>;
290
+ ```
291
+
292
+ `IncidentResponse` will be inferred as:
293
+
294
+ ```ts
295
+ type IncidentResponse = {
296
+ summary: string; // required
297
+ status: "Pending" | "Resolved" | "Escalated"; // required
298
+ follow_up_date?: string; // optional
299
+ tags?: ("Safety" | "Medical" | "Security")[]; // optional
300
+ confirmed: boolean; // required checkbox
301
+ };
302
+ ```
303
+
304
+ This is extremely useful for:
305
+
306
+ * API handlers (`(body: IncidentResponse) => { ... }`)
307
+ * DB layers that store `data` blobs
308
+ * Frontend forms with type-safe state
309
+
310
+ ---
311
+
150
312
  ## Using field defaults
151
313
 
152
314
  The package includes core defaults for each field type (no UI, no icons):
@@ -155,8 +317,8 @@ The package includes core defaults for each field type (no UI, no icons):
155
317
  import {
156
318
  CORE_FIELD_DEFAULTS,
157
319
  DEFAULT_FIELD_LABEL,
158
- type ReportTemplateFieldType
159
- } from "@frt/report-core";
320
+ type ReportTemplateFieldType,
321
+ } from "@frt-platform/report-core";
160
322
 
161
323
  function createDefaultField(type: ReportTemplateFieldType) {
162
324
  const defaults = CORE_FIELD_DEFAULTS[type] ?? {};
@@ -164,23 +326,70 @@ function createDefaultField(type: ReportTemplateFieldType) {
164
326
  id: "your-id-here",
165
327
  type,
166
328
  label: (defaults.label as string) ?? DEFAULT_FIELD_LABEL,
167
- ...defaults
329
+ ...defaults,
168
330
  };
169
331
  }
170
332
 
171
333
  const field = createDefaultField("shortText");
172
334
  ```
173
335
 
336
+ `CORE_FIELD_DEFAULTS` includes basic defaults for:
337
+
338
+ * `shortText`, `longText`, `number`, `date`, `checkbox`, `singleSelect`, `multiSelect`
339
+ * `repeatGroup` (with an empty `fields` array; your builder populates nested fields)
340
+
174
341
  This is especially useful in a form builder UI when the user adds a new field of a given type.
175
342
 
176
343
  ---
177
344
 
345
+ ## Field Registry (`FieldRegistry`)
346
+
347
+ You can register **custom field types** or override core types at runtime:
348
+
349
+ ```ts
350
+ import { FieldRegistry } from "@frt-platform/report-core";
351
+ import { z } from "zod";
352
+
353
+ // Simple example: a "richText" field that behaves like a string
354
+ FieldRegistry.register("richText", {
355
+ defaults: {
356
+ label: "Details",
357
+ placeholder: "Write here...",
358
+ },
359
+ buildResponseSchema: (field) => {
360
+ let schema = z.string();
361
+
362
+ if (typeof field.minLength === "number") {
363
+ schema = schema.min(field.minLength);
364
+ }
365
+ if (typeof field.maxLength === "number") {
366
+ schema = schema.max(field.maxLength);
367
+ }
368
+
369
+ return field.required ? schema : schema.optional();
370
+ },
371
+ });
372
+ ```
373
+
374
+ At validation time, `validateReportResponse` and `buildBaseFieldSchema` will:
375
+
376
+ 1. Check the registry for a custom handler.
377
+ 2. Use the built-in fallback if none is registered.
378
+
379
+ Use this for:
380
+
381
+ * App-specific field types (e.g. `fileUpload`, `signature`, `mapLocation`)
382
+ * Tenant-specific extensions in a multi-tenant platform
383
+ * Gradually introducing new field behaviors without changing the core package
384
+
385
+ ---
386
+
178
387
  ## Generating unique IDs
179
388
 
180
389
  The core package exposes a generic `createUniqueId` helper:
181
390
 
182
391
  ```ts
183
- import { createUniqueId } from "@frt/report-core";
392
+ import { createUniqueId } from "@frt-platform/report-core";
184
393
 
185
394
  const existingSectionIds = ["section-1", "section-2"];
186
395
  const newId = createUniqueId("section", existingSectionIds);
@@ -203,16 +412,16 @@ you can pass them through the migration pipeline:
203
412
  ```ts
204
413
  import {
205
414
  migrateLegacySchema,
206
- parseReportTemplateSchema
207
- } from "@frt/report-core";
415
+ parseReportTemplateSchema,
416
+ } from "@frt-platform/report-core";
208
417
 
209
418
  const legacy = {
210
419
  title: "Old template",
211
420
  description: "Using flat fields",
212
421
  fields: [
213
422
  { id: "summary", type: "textarea", label: "Summary" },
214
- { id: "status", type: "dropdown", label: "Status", options: ["Open", "Closed"] }
215
- ]
423
+ { id: "status", type: "dropdown", label: "Status", options: ["Open", "Closed"] },
424
+ ],
216
425
  };
217
426
 
218
427
  const migrated = migrateLegacySchema(legacy);
@@ -232,6 +441,88 @@ const parsed = ReportTemplateSchemaValidator.parse(migrated);
232
441
 
233
442
  ---
234
443
 
444
+ ## Diffing templates
445
+
446
+ When you version templates (e.g. editing in a UI), you might want to see what changed between versions. The core exposes a diff utility:
447
+
448
+ ```ts
449
+ import { diffTemplates } from "@frt-platform/report-core";
450
+
451
+ const beforeTemplate = /* old template */;
452
+ const afterTemplate = /* updated template */;
453
+
454
+ const diff = diffTemplates(beforeTemplate, afterTemplate);
455
+
456
+ console.log(diff.addedSections);
457
+ console.log(diff.removedSections);
458
+ console.log(diff.reorderedSections);
459
+ console.log(diff.modifiedSections);
460
+
461
+ console.log(diff.addedFields);
462
+ console.log(diff.removedFields);
463
+ console.log(diff.reorderedFields);
464
+ console.log(diff.modifiedFields);
465
+ ```
466
+
467
+ `diff.modifiedFields` contains entries like:
468
+
469
+ ```ts
470
+ {
471
+ sectionId: "section-overview",
472
+ fieldId: "summary",
473
+ before: { label: "What happened?", required: false, ... },
474
+ after: { label: "What happened exactly?", required: true, ... },
475
+ changes: [
476
+ { key: "label", before: "What happened?", after: "What happened exactly?" },
477
+ { key: "required", before: false, after: true },
478
+ ],
479
+ }
480
+ ```
481
+
482
+ Use this for:
483
+
484
+ * “Changes” tabs in your UI
485
+ * Migration & compatibility warnings
486
+ * Audit trails
487
+ * Showing ordered changes (section/field reorder is explicitly tracked)
488
+
489
+ ---
490
+
491
+ ## JSON Schema export
492
+
493
+ You can export a template as a **JSON Schema** object for use with OpenAPI, Swagger, Postman, or other runtimes that don’t speak TypeScript/Zod.
494
+
495
+ ```ts
496
+ import { exportJSONSchema } from "@frt-platform/report-core";
497
+
498
+ const jsonSchema = exportJSONSchema(template);
499
+ ```
500
+
501
+ Shape (simplified):
502
+
503
+ ```json
504
+ {
505
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
506
+ "type": "object",
507
+ "properties": {
508
+ "summary": { "type": "string", "minLength": 10 },
509
+ "status": { "type": "string", "enum": ["Pending", "Resolved", "Escalated"] }
510
+ },
511
+ "required": ["summary", "status"],
512
+ "additionalProperties": false,
513
+ "x-frt-templateTitle": "Incident follow-up",
514
+ "x-frt-version": 1
515
+ }
516
+ ```
517
+
518
+ Key points:
519
+
520
+ * String/number/boolean/array mapping matches the runtime validation semantics.
521
+ * `required` only includes **unconditionally required** fields (no `visibleIf` / `requiredIf`), but we expose conditional logic as vendor extensions.
522
+ * Extra metadata is available under `x-frt-*` keys, e.g. `x-frt-placeholder`, `x-frt-dataClassification`, `x-frt-visibleIf`, `x-frt-requiredIf`.
523
+
524
+ ---
525
+
235
526
  ## API Overview
236
527
 
237
528
  ### Types
@@ -241,14 +532,16 @@ import type {
241
532
  ReportTemplateSchema,
242
533
  ReportTemplateSection,
243
534
  ReportTemplateField,
244
- ReportTemplateFieldType
245
- } from "@frt/report-core";
535
+ ReportTemplateFieldType,
536
+ InferResponse,
537
+ } from "@frt-platform/report-core";
246
538
  ```
247
539
 
248
540
  * `ReportTemplateFieldType` – union of all field type strings
249
541
  * `ReportTemplateField` – one question/field in a section
250
542
  * `ReportTemplateSection` – a logical grouping of fields
251
543
  * `ReportTemplateSchema` – full template
544
+ * `InferResponse<TTemplate>` – builds a typed response object from a static template
252
545
 
253
546
  ### Constants
254
547
 
@@ -257,8 +550,8 @@ import {
257
550
  REPORT_TEMPLATE_VERSION,
258
551
  REPORT_TEMPLATE_FIELD_TYPES,
259
552
  DEFAULT_FIELD_LABEL,
260
- CORE_FIELD_DEFAULTS
261
- } from "@frt/report-core";
553
+ CORE_FIELD_DEFAULTS,
554
+ } from "@frt-platform/report-core";
262
555
  ```
263
556
 
264
557
  * `REPORT_TEMPLATE_VERSION: number`
@@ -276,14 +569,14 @@ import {
276
569
  parseReportTemplateSchema,
277
570
  parseReportTemplateSchemaFromString,
278
571
  normalizeReportTemplateSchema,
279
- serializeReportTemplateSchema
280
- } from "@frt/report-core";
572
+ serializeReportTemplateSchema,
573
+ } from "@frt-platform/report-core";
281
574
  ```
282
575
 
283
576
  Use the Zod schemas directly if you want:
284
577
 
285
578
  ```ts
286
- import { ReportTemplateSchemaValidator } from "@frt/report-core";
579
+ import { ReportTemplateSchemaValidator } from "@frt-platform/report-core";
287
580
 
288
581
  const result = ReportTemplateSchemaValidator.safeParse(raw);
289
582
  if (!result.success) {
@@ -294,15 +587,37 @@ if (!result.success) {
294
587
  ### Migration
295
588
 
296
589
  ```ts
297
- import { migrateLegacySchema } from "@frt/report-core";
590
+ import { migrateLegacySchema } from "@frt-platform/report-core";
298
591
  ```
299
592
 
300
593
  Takes `unknown` legacy input and returns a shape better suited for current schema validation.
301
594
 
302
- ### Utilities
595
+ ### Response validation
303
596
 
304
597
  ```ts
305
- import { createUniqueId } from "@frt/report-core";
598
+ import {
599
+ validateReportResponse,
600
+ buildResponseSchemaWithConditions,
601
+ type InferResponse,
602
+ } from "@frt-platform/report-core";
603
+ ```
604
+
605
+ ### Template diffing
606
+
607
+ ```ts
608
+ import { diffTemplates } from "@frt-platform/report-core";
609
+ ```
610
+
611
+ ### JSON Schema export
612
+
613
+ ```ts
614
+ import { exportJSONSchema } from "@frt-platform/report-core";
615
+ ```
616
+
617
+ ### Field registry
618
+
619
+ ```ts
620
+ import { FieldRegistry } from "@frt-platform/report-core";
306
621
  ```
307
622
 
308
623
  ---
@@ -321,46 +636,60 @@ This package is intentionally minimal. You can layer it into:
321
636
  * Validating templates on save
322
637
  * Migrating old template shapes
323
638
  * Ensuring consistent data before persisting to a DB
639
+ * Validating report responses before saving them
640
+ * Serving JSON Schema to other services
324
641
 
325
642
  Typical pattern in a full stack app:
326
643
 
327
- 1. **Frontend**:
644
+ 1. **Frontend**
328
645
 
329
646
  * Build a visual editor for templates
330
647
  * Send template JSON to server
331
648
 
332
- 2. **Backend**:
649
+ 2. **Backend**
333
650
 
334
- * `parseReportTemplateSchema` to validate
651
+ * `parseReportTemplateSchema` to validate templates
335
652
  * Store `ReportTemplateSchema` JSON in your DB (e.g. Mongo via Prisma)
653
+ * On response submission:
336
654
 
337
- 3. **Runtime**:
655
+ * Load template from DB
656
+ * `validateReportResponse(template, data)` before insert
657
+
658
+ 3. **Runtime**
338
659
 
339
660
  * Load templates from DB
340
- * Use them to render dynamic forms and validate responses (you can build a separate response schema per field, if you want).
661
+ * Use them to render dynamic forms
662
+ * Use `validateReportResponse` / `buildResponseSchemaWithConditions` to validate and normalize responses
663
+ * Use `exportJSONSchema` for interoperability with other stacks
341
664
 
342
665
  ---
343
666
 
344
667
  ## Roadmap / Extensions (ideas)
345
668
 
346
- Things you might add on top (in a separate package, e.g. `@frt/report-react`):
669
+ Things you might add on top (in a separate package, e.g. `@frt-platform/report-react`):
347
670
 
348
671
  * React components for:
349
672
 
350
673
  * Template builder UI
351
674
  * Field editors/renderers (short text, selects, etc.)
352
- * Response schemas built from templates
675
+ * Auto-generated `<ReportForm />` that consumes a template
353
676
  * More advanced field types:
354
677
 
355
678
  * File uploads
356
679
  * Signature pads
357
680
  * Matrix / grid fields
681
+ * Fully-featured `repeatGroup` UI & runtime
682
+ * Richer conditional logic:
683
+
684
+ * `visibleIf`
685
+ * `requiredIf`
686
+ * multi-step branching
358
687
  * Multi-language labels & descriptions
359
688
 
360
- The core stays simple: schema + validation + migration.
689
+ The core stays simple: **schema + validation + conditional logic + migration + diffing + registry + JSON Schema export**.
361
690
 
362
691
  ---
363
692
 
364
693
  ## License
365
694
 
366
- MIT – use it in your projects, commercial or not. Contributions welcome.
695
+ MIT – use it in your projects, commercial or not. Contributions welcome.