+
+
+
+ {uploading && {labels.uploading}}
+ {!uploading && value && ✓}
+ {error && {error}}
+
+
+ );
+}
diff --git a/apps/web/src/i18n/dictionaries.ts b/apps/web/src/i18n/dictionaries.ts
index f59ed85..c4046ae 100644
--- a/apps/web/src/i18n/dictionaries.ts
+++ b/apps/web/src/i18n/dictionaries.ts
@@ -48,6 +48,7 @@ const en = {
submitFailed: "Submission failed.",
captchaRequired: "Please complete the captcha.",
fileUploading: "Uploading…", fileRemove: "Remove", uploadFailed: "Upload failed.",
+ signatureClear: "Clear",
},
status: {
yourSubmission: "Your submission", activity: "Activity", yourAnswers: "Your answers",
@@ -139,7 +140,7 @@ const en = {
short_text: "Short text", long_text: "Long text", email: "Email", number: "Number", phone: "Phone", url: "URL",
single_choice: "Single choice", dropdown: "Dropdown", multi_choice: "Multiple choice", yes_no: "Yes / No",
rating_stars: "Star rating", nps: "NPS (0–10)", slider: "Slider", emoji_scale: "Emoji scale", matrix: "Matrix",
- date: "Date", file_upload: "File upload", image_upload: "Image upload",
+ date: "Date", file_upload: "File upload", image_upload: "Image upload", signature: "Signature",
consent: "Consent", heading: "Heading", paragraph: "Paragraph", divider: "Divider",
},
},
@@ -203,6 +204,7 @@ const de: Dictionary = {
submitFailed: "Senden fehlgeschlagen.",
captchaRequired: "Bitte das Captcha lösen.",
fileUploading: "Wird hochgeladen…", fileRemove: "Entfernen", uploadFailed: "Upload fehlgeschlagen.",
+ signatureClear: "Löschen",
},
status: {
yourSubmission: "Deine Einreichung", activity: "Aktivität", yourAnswers: "Deine Antworten",
@@ -294,7 +296,7 @@ const de: Dictionary = {
short_text: "Kurztext", long_text: "Langtext", email: "E-Mail", number: "Zahl", phone: "Telefon", url: "URL",
single_choice: "Einfachauswahl", dropdown: "Dropdown", multi_choice: "Mehrfachauswahl", yes_no: "Ja / Nein",
rating_stars: "Sterne-Bewertung", nps: "NPS (0–10)", slider: "Schieberegler", emoji_scale: "Emoji-Skala", matrix: "Matrix",
- date: "Datum", file_upload: "Datei-Upload", image_upload: "Bild-Upload",
+ date: "Datum", file_upload: "Datei-Upload", image_upload: "Bild-Upload", signature: "Unterschrift",
consent: "Zustimmung", heading: "Überschrift", paragraph: "Absatz", divider: "Trenner",
},
},
diff --git a/apps/web/src/lib/builder-fields.ts b/apps/web/src/lib/builder-fields.ts
index f21f78f..06f2292 100644
--- a/apps/web/src/lib/builder-fields.ts
+++ b/apps/web/src/lib/builder-fields.ts
@@ -20,6 +20,7 @@ export const BUILDER_FIELDS: { type: FieldType; label: string }[] = [
{ type: "date", label: "Date" },
{ type: "file_upload", label: "File upload" },
{ type: "image_upload", label: "Image upload" },
+ { type: "signature", label: "Signature" },
{ type: "consent", label: "Consent" },
{ type: "heading", label: "Heading" },
{ type: "paragraph", label: "Paragraph" },
diff --git a/packages/shared/src/form-spec.test.ts b/packages/shared/src/form-spec.test.ts
index bb3e3a0..8a7080d 100644
--- a/packages/shared/src/form-spec.test.ts
+++ b/packages/shared/src/form-spec.test.ts
@@ -197,6 +197,13 @@ describe("buildAnswerSchema", () => {
expect(s.safeParse({}).success).toBe(true);
expect(s.safeParse({ m: { r1: "y" } }).success).toBe(true);
});
+
+ it("treats a signature as a file-reference answer", () => {
+ const s = buildAnswerSchema(specWith({ id: "sig", type: "signature", validation: { required: true } }));
+ const ok = { sig: { key: "uploads/f/abc", name: "signature.png", size: 120, mime: "image/png" } };
+ expect(s.safeParse(ok).success).toBe(true);
+ expect(s.safeParse({ sig: "not-a-file" }).success).toBe(false);
+ });
});
describe("formatAnswerValue (rating family)", () => {
diff --git a/packages/shared/src/form-spec.ts b/packages/shared/src/form-spec.ts
index f403667..853e9e3 100644
--- a/packages/shared/src/form-spec.ts
+++ b/packages/shared/src/form-spec.ts
@@ -136,8 +136,12 @@ export const formSpecSchema = z.object({
});
export type FormSpec = z.infer