@marktiderman/genesis-forms 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,4 +1,4 @@
1
- import { J as Json, F as FormDefinition, a as FieldBlock } from './types-BBKBQ8sp.cjs';
1
+ import { J as Json, F as FormDefinition, a as FieldBlock } from './types-oAckGR_Z.cjs';
2
2
 
3
3
  /**
4
4
  * The contract every destination keeps. A destination sends one checked
@@ -1,4 +1,4 @@
1
- import { J as Json, F as FormDefinition, a as FieldBlock } from './types-BBKBQ8sp.js';
1
+ import { J as Json, F as FormDefinition, a as FieldBlock } from './types-oAckGR_Z.js';
2
2
 
3
3
  /**
4
4
  * The contract every destination keeps. A destination sends one checked
@@ -326,6 +326,79 @@ var structuredFieldTypes = [
326
326
  }
327
327
  ];
328
328
 
329
+ // src/photo.ts
330
+ var PHOTO_TYPE = "photo";
331
+ var PHOTO_FORMATS = {
332
+ jpeg: ["image/jpeg"],
333
+ png: ["image/png"],
334
+ heic: ["image/heic", "image/heif"],
335
+ webp: ["image/webp"]
336
+ };
337
+ var FORMAT_NAMES = { jpeg: "JPEG", png: "PNG", heic: "HEIC", webp: "WebP" };
338
+ var ALL_FORMATS = Object.keys(PHOTO_FORMATS);
339
+ var DEFAULT_PHOTO_FILES = 3;
340
+ var MAX_PHOTO_FILES = 5;
341
+ var DEFAULT_PHOTO_SIZE_MB = 10;
342
+ var MAX_PHOTO_SIZE_MB = 25;
343
+ var MAX_PHOTO_ID_LENGTH = 200;
344
+ var MAX_PHOTO_NAME_LENGTH = 255;
345
+ var wholeIn = (v, min, max) => typeof v === "number" && Number.isInteger(v) && v >= min && v <= max;
346
+ function photoLimits(field) {
347
+ const maxFiles = wholeIn(field.props?.maxFiles, 1, MAX_PHOTO_FILES) ? field.props.maxFiles : DEFAULT_PHOTO_FILES;
348
+ const maxSizeMb = wholeIn(field.props?.maxSizeMb, 1, MAX_PHOTO_SIZE_MB) ? field.props.maxSizeMb : DEFAULT_PHOTO_SIZE_MB;
349
+ const raw = field.props?.formats;
350
+ const picked = Array.isArray(raw) ? ALL_FORMATS.filter((f) => raw.includes(f)) : [];
351
+ const formats = picked.length ? picked : [...ALL_FORMATS];
352
+ return {
353
+ maxFiles,
354
+ maxSizeMb,
355
+ maxBytes: maxSizeMb * 1024 * 1024,
356
+ formats,
357
+ mimeTypes: formats.flatMap((f) => PHOTO_FORMATS[f])
358
+ };
359
+ }
360
+ function isPhotoRef(v) {
361
+ if (!v || typeof v !== "object" || Array.isArray(v)) return false;
362
+ const r = v;
363
+ return typeof r.id === "string" && r.id.length > 0 && r.id.length <= MAX_PHOTO_ID_LENGTH && typeof r.name === "string" && r.name.length <= MAX_PHOTO_NAME_LENGTH && typeof r.type === "string" && typeof r.size === "number" && Number.isInteger(r.size) && r.size > 0;
364
+ }
365
+ function photoRefs(value) {
366
+ if (!Array.isArray(value)) return [];
367
+ const refs = [];
368
+ for (const r of value) if (isPhotoRef(r)) refs.push({ id: r.id, name: r.name, type: r.type, size: r.size });
369
+ return refs;
370
+ }
371
+ var plural2 = (n, one, many = `${one}s`) => `${n} ${n === 1 ? one : many}`;
372
+ function photoFormatList(formats) {
373
+ const names = formats.map((f) => FORMAT_NAMES[f]);
374
+ return names.length > 1 ? `${names.slice(0, -1).join(", ")} or ${names[names.length - 1]}` : names[0] ?? "";
375
+ }
376
+ var photoTypeMessage = (field) => `Use a ${photoFormatList(photoLimits(field).formats)} photo.`;
377
+ var photoSizeMessage = (field) => `Each photo must be ${photoLimits(field).maxSizeMb} MB or smaller.`;
378
+ var photoCountMessage = (field) => `Add at most ${plural2(photoLimits(field).maxFiles, "photo")}.`;
379
+ var photoFieldType = {
380
+ type: PHOTO_TYPE,
381
+ label: "Photo",
382
+ category: "media",
383
+ value: { json: true },
384
+ operators: ["answered", "empty"],
385
+ a11y: { semantics: "group" },
386
+ isEmpty: (value) => value === void 0 || value === null || Array.isArray(value) && value.length === 0,
387
+ validate: (value, field) => {
388
+ if (!Array.isArray(value) || !value.every(isPhotoRef)) return "Add a photo.";
389
+ const limits = photoLimits(field);
390
+ if (value.length > limits.maxFiles) return photoCountMessage(field);
391
+ const refs = value;
392
+ if (new Set(refs.map((r) => r.id)).size !== refs.length) return "Each photo can be added once.";
393
+ if (refs.some((r) => !limits.mimeTypes.includes(r.type.toLowerCase()))) return photoTypeMessage(field);
394
+ if (refs.some((r) => r.size > limits.maxBytes)) return photoSizeMessage(field);
395
+ return null;
396
+ },
397
+ normalize: (value) => Array.isArray(value) && value.every(isPhotoRef) ? photoRefs(value) : value,
398
+ summarize: (value) => photoRefs(value).map((r) => r.name || "Photo").join(", "),
399
+ samples: () => [[{ id: "sample-photo", name: "photo.jpg", type: "image/jpeg", size: 1024 }]]
400
+ };
401
+
329
402
  // src/fields.ts
330
403
  var REGISTRY = /* @__PURE__ */ Symbol.for("@marktiderman/genesis-forms/field-types");
331
404
  var globalScope = globalThis;
@@ -361,12 +434,78 @@ function summarizeValue(value, field) {
361
434
  if (Array.isArray(value)) return value.join(", ");
362
435
  return String(value);
363
436
  }
437
+ var OPTION_LIMITS = {
438
+ /** Characters in a description. */
439
+ description: 500,
440
+ /** Characters in an image URL. */
441
+ image: 2048,
442
+ /** Attributes on one option. */
443
+ attributes: 20,
444
+ /** Characters in an attribute's name. */
445
+ attributeName: 64,
446
+ /** Characters in one attribute value. */
447
+ attributeValue: 200,
448
+ /** Values in one attribute's list. */
449
+ attributeItems: 50
450
+ };
451
+ function isHttpsUrl(url) {
452
+ return typeof url === "string" && url.length <= OPTION_LIMITS.image && /^https:\/\/[^\s/?#@\\]+(?:[/?#][^\s]*)?$/i.test(url) && // No control characters anywhere (a line break would end a CSV row or an HTML attribute).
453
+ ![...url].some((c) => c.charCodeAt(0) < 32 || c.charCodeAt(0) === 127);
454
+ }
455
+ function attributeName(name) {
456
+ return name.trim() !== "" && name.length <= OPTION_LIMITS.attributeName && !(name in Object.prototype);
457
+ }
458
+ function readAttributes(raw) {
459
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return void 0;
460
+ const out = {};
461
+ let count = 0;
462
+ for (const name of Object.keys(raw)) {
463
+ if (count >= OPTION_LIMITS.attributes) break;
464
+ if (!attributeName(name)) continue;
465
+ const v = raw[name];
466
+ const fits = (x) => typeof x === "string" && x.length <= OPTION_LIMITS.attributeValue;
467
+ if (fits(v)) out[name] = v;
468
+ else if (Array.isArray(v) && v.length <= OPTION_LIMITS.attributeItems && v.every(fits)) out[name] = [...v];
469
+ else continue;
470
+ count++;
471
+ }
472
+ return count ? out : void 0;
473
+ }
474
+ function readOption(raw) {
475
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
476
+ const o = raw;
477
+ if (typeof o.value !== "string") return null;
478
+ const out = { value: o.value };
479
+ if (typeof o.label === "string") out.label = o.label;
480
+ if (typeof o.description === "string" && o.description.trim() && o.description.length <= OPTION_LIMITS.description) {
481
+ out.description = o.description;
482
+ }
483
+ if (isHttpsUrl(o.image)) out.image = o.image;
484
+ const attributes = readAttributes(o.attributes);
485
+ if (attributes) out.attributes = attributes;
486
+ return out;
487
+ }
488
+ var OTHER_MAX_LENGTH = 200;
489
+ function otherOption(field) {
490
+ const other = field.props?.other;
491
+ if (other === true) return { label: "Other" };
492
+ if (other && typeof other === "object" && !Array.isArray(other)) {
493
+ const label = typeof other.label === "string" && other.label.trim() ? other.label.trim() : "Other";
494
+ return { label };
495
+ }
496
+ return null;
497
+ }
498
+ function isOtherValue(field, value) {
499
+ return otherOption(field) !== null && typeof value === "string" && !choiceOptions(field).some((o) => o.value === value);
500
+ }
501
+ var typedOther = (field, value) => isOtherValue(field, value) && value.trim() !== "" && value.length <= OTHER_MAX_LENGTH;
364
502
  function choiceOptions(field) {
365
503
  const raw = field.props?.options;
366
504
  if (!Array.isArray(raw)) return [];
367
- return raw.flatMap(
368
- (o) => o && typeof o === "object" && !Array.isArray(o) && typeof o.value === "string" ? [{ value: o.value, label: typeof o.label === "string" ? o.label : void 0 }] : []
369
- );
505
+ return raw.flatMap((o) => {
506
+ const option = readOption(o);
507
+ return option ? [option] : [];
508
+ });
370
509
  }
371
510
  function optionLabel(field, value) {
372
511
  const option = choiceOptions(field).find((o) => o.value === value);
@@ -486,7 +625,7 @@ var builtins = [
486
625
  value: { json: true },
487
626
  operators: ["eq", "neq", "in", "answered", "empty"],
488
627
  a11y: { semantics: "group" },
489
- validate: (value, field) => typeof value === "string" && choiceOptions(field).some((o) => o.value === value) ? null : "Choose one of the options.",
628
+ validate: (value, field) => typeof value === "string" && (choiceOptions(field).some((o) => o.value === value) || typedOther(field, value)) ? null : "Choose one of the options.",
490
629
  summarize: (value, field) => optionLabel(field, String(value))
491
630
  },
492
631
  {
@@ -498,7 +637,8 @@ var builtins = [
498
637
  a11y: { semantics: "group" },
499
638
  validate: (value, field) => {
500
639
  const allowed = new Set(choiceOptions(field).map((o) => o.value));
501
- return Array.isArray(value) && value.length <= allowed.size && new Set(value).size === value.length && value.every((v) => typeof v === "string" && allowed.has(v)) ? null : "Choose from the options.";
640
+ const others = Array.isArray(value) ? value.filter((v) => typeof v === "string" && !allowed.has(v)) : [];
641
+ return Array.isArray(value) && new Set(value).size === value.length && value.every((v) => typeof v === "string") && (others.length === 0 || others.length === 1 && typedOther(field, others[0])) ? null : "Choose from the options.";
502
642
  },
503
643
  summarize: (value, field) => Array.isArray(value) ? value.map((v) => optionLabel(field, String(v))).join(", ") : String(value)
504
644
  },
@@ -519,7 +659,7 @@ var builtins = [
519
659
  value: { json: true },
520
660
  operators: ["eq", "neq", "in", "answered", "empty"],
521
661
  a11y: { semantics: "control" },
522
- validate: (value, field) => typeof value === "string" && choiceOptions(field).some((o) => o.value === value) ? null : "Choose one of the options.",
662
+ validate: (value, field) => typeof value === "string" && (choiceOptions(field).some((o) => o.value === value) || typedOther(field, value)) ? null : "Choose one of the options.",
523
663
  summarize: (value, field) => optionLabel(field, String(value))
524
664
  },
525
665
  {
@@ -626,7 +766,7 @@ function dateLimits(field) {
626
766
  if (typeof max === "string" && isIsoDate(max)) out.max = max;
627
767
  return out;
628
768
  }
629
- for (const manifest of [...builtins, ...structuredFieldTypes, calculatedFieldType]) registerFieldType(manifest);
769
+ for (const manifest of [...builtins, ...structuredFieldTypes, calculatedFieldType, photoFieldType]) registerFieldType(manifest);
630
770
 
631
771
  // src/logic.ts
632
772
  function own(record, key) {