@marktiderman/genesis-forms 0.3.1 → 0.4.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.
@@ -112,6 +112,39 @@ async function sha256Hex(text) {
112
112
  return Array.from(new Uint8Array(digest), (b) => b.toString(16).padStart(2, "0")).join("");
113
113
  }
114
114
 
115
+ // src/calculated.ts
116
+ var CALCULATED_TYPE = "calculated";
117
+ var MAX_CALCULATED_DECIMALS = 10;
118
+ function calculatedProps(field) {
119
+ const p = field.props ?? {};
120
+ const out = { formula: typeof p.formula === "string" ? p.formula : "" };
121
+ if (p.format === "number" || p.format === "percent" || p.format === "text") out.format = p.format;
122
+ if (typeof p.decimals === "number" && Number.isInteger(p.decimals) && p.decimals >= 0 && p.decimals <= MAX_CALCULATED_DECIMALS) {
123
+ out.decimals = p.decimals;
124
+ }
125
+ return out;
126
+ }
127
+ function formatCalculated(value, field) {
128
+ if (value === void 0 || value === null) return "";
129
+ const { format, decimals } = calculatedProps(field);
130
+ const fixed = (n) => decimals === void 0 ? String(Number(n.toFixed(6))) : n.toFixed(decimals);
131
+ if (typeof value === "number") return format === "percent" ? `${fixed(value * 100)}%` : fixed(value);
132
+ if (typeof value === "boolean") return value ? "Yes" : "No";
133
+ return typeof value === "string" ? value : "";
134
+ }
135
+ var numberOps = ["eq", "neq", "lt", "lte", "gt", "gte", "in", "answered", "empty"];
136
+ var calculatedFieldType = {
137
+ type: CALCULATED_TYPE,
138
+ label: "Calculated",
139
+ category: "advanced",
140
+ value: { json: true },
141
+ operators: numberOps,
142
+ a11y: { semantics: "control" },
143
+ // The engine works the value out; any number, text or true / false it made is valid.
144
+ validate: (value) => typeof value === "number" || typeof value === "string" || typeof value === "boolean" ? null : "This value is worked out from other answers.",
145
+ summarize: (value, field) => formatCalculated(value, field)
146
+ };
147
+
115
148
  // src/structured.ts
116
149
  var isObject = (v) => !!v && typeof v === "object" && !Array.isArray(v);
117
150
  function intProp(field, name, fallback, floor = 1) {
@@ -343,7 +376,7 @@ function intRange(field, min, maxDefault) {
343
376
  const max = typeof field.props?.max === "number" ? field.props.max : maxDefault;
344
377
  return { min, max };
345
378
  }
346
- var numberOps = ["eq", "neq", "lt", "lte", "gt", "gte", "in", "answered", "empty"];
379
+ var numberOps2 = ["eq", "neq", "lt", "lte", "gt", "gte", "in", "answered", "empty"];
347
380
  var textOps = ["eq", "neq", "contains", "answered", "empty"];
348
381
  function maxLength(field, fallback) {
349
382
  return typeof field.props?.maxLength === "number" ? field.props.maxLength : fallback;
@@ -421,23 +454,30 @@ var builtins = [
421
454
  label: "Rating",
422
455
  category: "scale",
423
456
  value: { json: true },
424
- operators: numberOps,
457
+ operators: numberOps2,
425
458
  a11y: { semantics: "group" },
426
459
  validate: (value, field) => {
427
460
  const { min, max } = intRange(field, 1, 5);
428
461
  return Number.isInteger(value) && value >= min && value <= max ? null : `Choose a rating from ${min} to ${max}.`;
429
462
  },
430
- summarize: (value, field) => `${value}/${intRange(field, 1, 5).max}`
463
+ summarize: (value, field) => {
464
+ const { max } = intRange(field, 1, 5);
465
+ const end = scaleEndLabel(field, value, 1, max);
466
+ return end ? `${value}/${max} (${end})` : `${value}/${max}`;
467
+ }
431
468
  },
432
469
  {
433
470
  type: "nps",
434
471
  label: "Net Promoter Score",
435
472
  category: "scale",
436
473
  value: { json: true },
437
- operators: numberOps,
474
+ operators: numberOps2,
438
475
  a11y: { semantics: "group" },
439
476
  validate: (value) => Number.isInteger(value) && value >= 0 && value <= 10 ? null : "Choose a score from 0 to 10.",
440
- summarize: (value) => `${value}/10`
477
+ summarize: (value, field) => {
478
+ const end = scaleEndLabel(field, value, 0, 10);
479
+ return end ? `${value}/10 (${end})` : `${value}/10`;
480
+ }
441
481
  },
442
482
  {
443
483
  type: "single-choice",
@@ -471,9 +511,122 @@ var builtins = [
471
511
  a11y: { semantics: "group" },
472
512
  validate: (value) => typeof value === "boolean" ? null : "Choose yes or no.",
473
513
  summarize: (value) => value ? "Yes" : "No"
514
+ },
515
+ {
516
+ type: "dropdown",
517
+ label: "Dropdown",
518
+ category: "choice",
519
+ value: { json: true },
520
+ operators: ["eq", "neq", "in", "answered", "empty"],
521
+ a11y: { semantics: "control" },
522
+ validate: (value, field) => typeof value === "string" && choiceOptions(field).some((o) => o.value === value) ? null : "Choose one of the options.",
523
+ summarize: (value, field) => optionLabel(field, String(value))
524
+ },
525
+ {
526
+ type: "number",
527
+ label: "Number",
528
+ category: "text",
529
+ value: { json: true },
530
+ operators: numberOps2,
531
+ a11y: { semantics: "control", inputMode: "decimal" },
532
+ validate: (value, field) => {
533
+ if (typeof value !== "number" || !Number.isFinite(value)) return "Enter a number.";
534
+ const { min, max, step } = numberLimits(field);
535
+ if (min !== void 0 && max !== void 0 && (value < min || value > max)) {
536
+ return `Enter a number from ${min} to ${max}.`;
537
+ }
538
+ if (min !== void 0 && value < min) return `Enter ${min} or more.`;
539
+ if (max !== void 0 && value > max) return `Enter ${max} or less.`;
540
+ if (step !== void 0 && !onStep(value, min ?? 0, step)) {
541
+ return step === 1 && Number.isInteger(min ?? 0) ? "Enter a whole number." : `Enter a number in steps of ${step}.`;
542
+ }
543
+ return null;
544
+ },
545
+ summarize: (value) => String(value),
546
+ samples: (field) => {
547
+ const { min, max } = numberLimits(field);
548
+ return [min, max, 0, 1].filter((v) => v !== void 0);
549
+ }
550
+ },
551
+ {
552
+ type: "date",
553
+ label: "Date",
554
+ category: "date",
555
+ value: { json: true },
556
+ operators: ["eq", "neq", "answered", "empty"],
557
+ a11y: { semantics: "control" },
558
+ validate: (value, field) => {
559
+ if (typeof value !== "string" || !isIsoDate(value.trim())) return "Enter a date like 2026-10-22.";
560
+ const date = value.trim();
561
+ const { min, max } = dateLimits(field);
562
+ if (min && max && (date < min || date > max)) return `Choose a date from ${min} to ${max}.`;
563
+ if (min && date < min) return `Choose ${min} or later.`;
564
+ if (max && date > max) return `Choose ${max} or earlier.`;
565
+ return null;
566
+ },
567
+ normalize: (value) => typeof value === "string" ? value.trim() : value,
568
+ samples: (field) => {
569
+ const { min, max } = dateLimits(field);
570
+ return [min ?? max ?? "2000-01-01"];
571
+ }
572
+ },
573
+ {
574
+ type: "consent",
575
+ label: "Consent checkbox",
576
+ category: "choice",
577
+ value: { json: true },
578
+ operators: ["answered", "empty"],
579
+ a11y: { semantics: "group" },
580
+ // Unchecked is unanswered, so a required consent must be checked.
581
+ isEmpty: (value) => value === void 0 || value === null || value === false,
582
+ validate: (value) => value === true ? null : "Check the box to agree.",
583
+ summarize: () => "Agreed",
584
+ samples: () => [true]
474
585
  }
475
586
  ];
476
- for (const manifest of [...builtins, ...structuredFieldTypes]) registerFieldType(manifest);
587
+ function scaleLabels(field) {
588
+ const out = {};
589
+ const low = field.props?.lowLabel;
590
+ const high = field.props?.highLabel;
591
+ if (typeof low === "string" && low.trim()) out.low = low.trim();
592
+ if (typeof high === "string" && high.trim()) out.high = high.trim();
593
+ return out;
594
+ }
595
+ function scaleEndLabel(field, value, min, max) {
596
+ const { low, high } = scaleLabels(field);
597
+ return value === min ? low : value === max ? high : void 0;
598
+ }
599
+ var finite = (v) => typeof v === "number" && Number.isFinite(v) ? v : void 0;
600
+ function numberLimits(field) {
601
+ const out = {};
602
+ const min = finite(field.props?.min);
603
+ const max = finite(field.props?.max);
604
+ const step = finite(field.props?.step);
605
+ if (min !== void 0) out.min = min;
606
+ if (max !== void 0) out.max = max;
607
+ if (step !== void 0 && step > 0) out.step = step;
608
+ return out;
609
+ }
610
+ function onStep(value, base, step) {
611
+ const n = (value - base) / step;
612
+ return Math.abs(n - Math.round(n)) < 1e-9 * Math.max(1, Math.abs(n));
613
+ }
614
+ function isIsoDate(value) {
615
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(value)) return false;
616
+ const [y, m, d] = value.split("-").map(Number);
617
+ if (y < 1 || m < 1 || m > 12 || d < 1) return false;
618
+ const days = [31, y % 4 === 0 && (y % 100 !== 0 || y % 400 === 0) ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
619
+ return d <= days[m - 1];
620
+ }
621
+ function dateLimits(field) {
622
+ const out = {};
623
+ const min = field.props?.min;
624
+ const max = field.props?.max;
625
+ if (typeof min === "string" && isIsoDate(min)) out.min = min;
626
+ if (typeof max === "string" && isIsoDate(max)) out.max = max;
627
+ return out;
628
+ }
629
+ for (const manifest of [...builtins, ...structuredFieldTypes, calculatedFieldType]) registerFieldType(manifest);
477
630
 
478
631
  // src/logic.ts
479
632
  function own(record, key) {