@fayz-ai/plugin-forms 0.15.14 → 0.15.16

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.
@@ -0,0 +1,824 @@
1
+ import { yesNoAnswer, useMediaUpload, stencilOf, useMediaUrl, isMedia, SignatureField, RecommendationField, GalleryField, StencilPhotoField, YesNoTextField, YesNoField, RecommendationView, GalleryView, MediaImage, aspectRatioOf, answerAlerts, AnswerAlertNote } from './chunk-YOFFKQD4.js';
2
+ import React, { useCallback } from 'react';
3
+ import { Checkbox, Badge, Select, SelectTrigger, SelectValue, SelectContent, SelectItem, Input, Textarea, Button } from '@fayz-ai/ui';
4
+ import { describeError, useTranslation } from '@fayz-ai/core';
5
+ import { createPortal } from 'react-dom';
6
+ import { X, SwitchCamera, RotateCcw, Check, ImageUp, Camera, SkipForward } from 'lucide-react';
7
+ import { jsxs, Fragment, jsx } from 'react/jsx-runtime';
8
+
9
+ // src/lib/html-text.ts
10
+ function looksLikeHtml(value) {
11
+ return /<\/?[a-z][^>]*>/i.test(value);
12
+ }
13
+ var BLOCKS = "p, div, li, tr, h1, h2, h3, h4, h5, h6, blockquote, section, article";
14
+ var NBSP = / /g;
15
+ function htmlToPlainText(value) {
16
+ if (!value) return "";
17
+ if (typeof document === "undefined") {
18
+ return value.replace(/<br\s*\/?>/gi, "\n").replace(/<\/(p|div|li|tr|h[1-6]|blockquote)>/gi, "\n").replace(/<[^>]*>/g, "").replace(/&nbsp;/g, " ").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"').replace(/&#39;/g, "'").replace(/&amp;/g, "&").replace(/\n{3,}/g, "\n\n").trim();
19
+ }
20
+ const el = document.createElement("div");
21
+ el.innerHTML = value;
22
+ for (const br of Array.from(el.querySelectorAll("br"))) {
23
+ br.replaceWith("\n");
24
+ }
25
+ for (const block of Array.from(el.querySelectorAll(BLOCKS))) {
26
+ block.append("\n");
27
+ }
28
+ return (el.textContent ?? "").replace(NBSP, " ").replace(/[ \t]+\n/g, "\n").replace(/\n{3,}/g, "\n\n").trim();
29
+ }
30
+ var DROP_WITH_CONTENT = /* @__PURE__ */ new Set([
31
+ "script",
32
+ "style",
33
+ "iframe",
34
+ "object",
35
+ "embed",
36
+ "svg",
37
+ "math",
38
+ "link",
39
+ "meta",
40
+ "form",
41
+ "input",
42
+ "button",
43
+ "textarea",
44
+ "select",
45
+ "template",
46
+ "noscript"
47
+ ]);
48
+ var ALLOWED_TAGS = /* @__PURE__ */ new Set([
49
+ "p",
50
+ "br",
51
+ "hr",
52
+ "div",
53
+ "span",
54
+ "b",
55
+ "strong",
56
+ "i",
57
+ "em",
58
+ "u",
59
+ "s",
60
+ "sub",
61
+ "sup",
62
+ "mark",
63
+ "small",
64
+ "ul",
65
+ "ol",
66
+ "li",
67
+ "blockquote",
68
+ "pre",
69
+ "code",
70
+ "h1",
71
+ "h2",
72
+ "h3",
73
+ "h4",
74
+ "h5",
75
+ "h6",
76
+ "table",
77
+ "thead",
78
+ "tbody",
79
+ "tfoot",
80
+ "tr",
81
+ "td",
82
+ "th",
83
+ "caption",
84
+ "a"
85
+ ]);
86
+ var ALLOWED_ATTRS = {
87
+ a: /* @__PURE__ */ new Set(["href", "title"]),
88
+ td: /* @__PURE__ */ new Set(["colspan", "rowspan"]),
89
+ th: /* @__PURE__ */ new Set(["colspan", "rowspan"])
90
+ };
91
+ var SAFE_HREF = /^(https?:|mailto:|tel:)/i;
92
+ function cleanElement(el) {
93
+ for (const child of Array.from(el.children)) {
94
+ cleanElement(child);
95
+ }
96
+ const tag = el.tagName.toLowerCase();
97
+ if (DROP_WITH_CONTENT.has(tag)) {
98
+ el.remove();
99
+ return;
100
+ }
101
+ for (const attr of Array.from(el.attributes)) {
102
+ const name = attr.name.toLowerCase();
103
+ const allowed = ALLOWED_ATTRS[tag]?.has(name) ?? false;
104
+ if (!allowed) {
105
+ el.removeAttribute(attr.name);
106
+ continue;
107
+ }
108
+ if (name === "href" && !SAFE_HREF.test(attr.value.trim())) {
109
+ el.removeAttribute(attr.name);
110
+ }
111
+ }
112
+ if (!ALLOWED_TAGS.has(tag)) {
113
+ el.replaceWith(...Array.from(el.childNodes));
114
+ return;
115
+ }
116
+ if (tag === "a") {
117
+ if (!el.getAttribute("href")) {
118
+ el.replaceWith(...Array.from(el.childNodes));
119
+ return;
120
+ }
121
+ el.setAttribute("target", "_blank");
122
+ el.setAttribute("rel", "noopener noreferrer nofollow");
123
+ }
124
+ }
125
+ function sanitizeHtml(value) {
126
+ if (!value) return "";
127
+ if (typeof DOMParser === "undefined") return "";
128
+ const doc = new DOMParser().parseFromString(value, "text/html");
129
+ for (const child of Array.from(doc.body.children)) {
130
+ cleanElement(child);
131
+ }
132
+ return doc.body.innerHTML;
133
+ }
134
+ function fieldRichValue(value) {
135
+ if (typeof value !== "string" || !value) return { text: "" };
136
+ if (!looksLikeHtml(value)) return { text: value };
137
+ const html = sanitizeHtml(value);
138
+ return html ? { html } : { text: htmlToPlainText(value) };
139
+ }
140
+
141
+ // src/lib/plain-text.ts
142
+ var ENTITIES = {
143
+ amp: "&",
144
+ lt: "<",
145
+ gt: ">",
146
+ quot: '"',
147
+ apos: "'",
148
+ nbsp: " ",
149
+ // Os acentos do português, que é o que 23 moldes do Feegow trouxeram. Escrito
150
+ // à mão e não resolvido pelo DOM porque esta função também roda em teste e
151
+ // fora do navegador — e um `&ccedil;` visível na anamnese é o defeito que
152
+ // ela veio consertar.
153
+ ...Object.fromEntries(
154
+ "agrave \xE0 aacute \xE1 acirc \xE2 atilde \xE3 auml \xE4 ccedil \xE7 egrave \xE8 eacute \xE9 ecirc \xEA euml \xEB igrave \xEC iacute \xED icirc \xEE iuml \xEF ntilde \xF1 ograve \xF2 oacute \xF3 ocirc \xF4 otilde \xF5 ouml \xF6 ugrave \xF9 uacute \xFA ucirc \xFB uuml \xFC".split(" ").reduce((acc, part, i, all) => {
155
+ if (i % 2 === 0) acc.push([part, all[i + 1]]);
156
+ return acc;
157
+ }, []).flatMap(([name, char]) => [
158
+ [name, char],
159
+ [name.charAt(0).toUpperCase() + name.slice(1), char.toUpperCase()]
160
+ ])
161
+ )
162
+ };
163
+ function decodeEntities(value) {
164
+ return value.replace(/&(#x?[0-9a-f]+|[a-z]+);/gi, (whole, body) => {
165
+ if (body[0] === "#") {
166
+ const code = body[1] === "x" || body[1] === "X" ? parseInt(body.slice(2), 16) : parseInt(body.slice(1), 10);
167
+ return Number.isFinite(code) ? String.fromCodePoint(code) : whole;
168
+ }
169
+ const named = ENTITIES[body] ?? ENTITIES[body.toLowerCase()];
170
+ if (named) return named;
171
+ if (typeof document === "undefined") return whole;
172
+ const el = document.createElement("textarea");
173
+ el.innerHTML = whole;
174
+ return el.value;
175
+ });
176
+ }
177
+ function htmlToPlainText2(value) {
178
+ if (!value || !/[<&]/.test(value)) return value;
179
+ const text = decodeEntities(
180
+ value.replace(/<\s*br\s*\/?\s*>/gi, "\n").replace(/<\s*\/\s*(p|div|li|h[1-6])\s*>/gi, "\n").replace(/<[^>]*>/g, "")
181
+ );
182
+ return text.split("\n").map((line) => line.replace(/\s+/g, " ").trim()).filter(Boolean).join("\n") || void 0;
183
+ }
184
+ var ratioOf = (a) => a === "1:1" ? 1 : a === "4:3" ? 4 / 3 : 3 / 4;
185
+ function StencilCamera({
186
+ title,
187
+ overlayUrl,
188
+ opacity,
189
+ aspect,
190
+ camera,
191
+ burnOverlay,
192
+ onCapture,
193
+ onSkip,
194
+ onClose
195
+ }) {
196
+ const videoRef = React.useRef(null);
197
+ const streamRef = React.useRef(null);
198
+ const fileRef = React.useRef(null);
199
+ const [facing, setFacing] = React.useState(camera);
200
+ const [preview, setPreview] = React.useState(null);
201
+ const [starting, setStarting] = React.useState(true);
202
+ const [noCamera, setNoCamera] = React.useState(false);
203
+ const [retake, setRetake] = React.useState(0);
204
+ const ratio = ratioOf(aspect);
205
+ const stop = () => {
206
+ streamRef.current?.getTracks().forEach((t) => t.stop());
207
+ streamRef.current = null;
208
+ };
209
+ React.useEffect(() => {
210
+ if (preview) return;
211
+ let alive = true;
212
+ (async () => {
213
+ setStarting(true);
214
+ try {
215
+ if (!navigator.mediaDevices?.getUserMedia) throw new Error("unsupported");
216
+ stop();
217
+ const stream = await navigator.mediaDevices.getUserMedia({
218
+ video: { facingMode: { ideal: facing }, width: { ideal: 1920 }, height: { ideal: 1920 } },
219
+ audio: false
220
+ });
221
+ if (!alive) {
222
+ stream.getTracks().forEach((t) => t.stop());
223
+ return;
224
+ }
225
+ streamRef.current = stream;
226
+ if (videoRef.current) {
227
+ videoRef.current.srcObject = stream;
228
+ await videoRef.current.play().catch(() => {
229
+ });
230
+ }
231
+ setNoCamera(false);
232
+ } catch {
233
+ if (alive) setNoCamera(true);
234
+ } finally {
235
+ if (alive) setStarting(false);
236
+ }
237
+ })();
238
+ return () => {
239
+ alive = false;
240
+ };
241
+ }, [facing, retake]);
242
+ React.useEffect(() => () => stop(), []);
243
+ React.useEffect(() => {
244
+ const onKey = (e) => {
245
+ if (e.key !== "Escape") return;
246
+ e.stopPropagation();
247
+ e.preventDefault();
248
+ onClose();
249
+ };
250
+ window.addEventListener("keydown", onKey, true);
251
+ return () => window.removeEventListener("keydown", onKey, true);
252
+ }, [onClose]);
253
+ async function drawOverlay(ctx, w, h) {
254
+ if (!burnOverlay || !overlayUrl) return;
255
+ await new Promise((resolve) => {
256
+ const timer = setTimeout(resolve, 2500);
257
+ const img = new Image();
258
+ img.crossOrigin = "anonymous";
259
+ img.onload = () => {
260
+ try {
261
+ const s = Math.min(w / img.width, h / img.height);
262
+ ctx.globalAlpha = opacity;
263
+ ctx.drawImage(img, (w - img.width * s) / 2, (h - img.height * s) / 2, img.width * s, img.height * s);
264
+ ctx.globalAlpha = 1;
265
+ } catch {
266
+ }
267
+ clearTimeout(timer);
268
+ resolve();
269
+ };
270
+ img.onerror = () => {
271
+ clearTimeout(timer);
272
+ resolve();
273
+ };
274
+ img.src = overlayUrl;
275
+ });
276
+ }
277
+ async function capture() {
278
+ const video = videoRef.current;
279
+ if (!video || !video.videoWidth) return;
280
+ const vw = video.videoWidth, vh = video.videoHeight;
281
+ let cw = vw, ch = vh;
282
+ if (vw / vh > ratio) cw = vh * ratio;
283
+ else ch = vw / ratio;
284
+ const canvas = document.createElement("canvas");
285
+ canvas.width = Math.round(cw);
286
+ canvas.height = Math.round(ch);
287
+ const ctx = canvas.getContext("2d");
288
+ if (facing === "user") {
289
+ ctx.translate(canvas.width, 0);
290
+ ctx.scale(-1, 1);
291
+ }
292
+ ctx.drawImage(video, (vw - cw) / 2, (vh - ch) / 2, cw, ch, 0, 0, canvas.width, canvas.height);
293
+ ctx.setTransform(1, 0, 0, 1, 0, 0);
294
+ await drawOverlay(ctx, canvas.width, canvas.height);
295
+ const blob = await new Promise((r) => {
296
+ try {
297
+ canvas.toBlob(r, "image/jpeg", 0.9);
298
+ } catch {
299
+ r(null);
300
+ }
301
+ });
302
+ if (!blob) return;
303
+ stop();
304
+ setPreview({ blob, url: URL.createObjectURL(blob) });
305
+ }
306
+ function pickFile(file) {
307
+ if (!file) return;
308
+ setPreview({ blob: file, url: URL.createObjectURL(file) });
309
+ }
310
+ return createPortal(
311
+ /* @__PURE__ */ jsxs("div", { className: "fixed inset-0 z-[2147483647] flex select-none flex-col bg-black text-white", role: "dialog", "aria-label": title, children: [
312
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between p-3", children: [
313
+ /* @__PURE__ */ jsx(
314
+ Button,
315
+ {
316
+ variant: "bare",
317
+ size: "bare",
318
+ type: "button",
319
+ onClick: onClose,
320
+ "aria-label": "Fechar",
321
+ className: "font-normal flex h-10 w-10 items-center justify-center rounded-full bg-white/10 hover:bg-white/20",
322
+ children: /* @__PURE__ */ jsx(X, { className: "h-5 w-5" })
323
+ }
324
+ ),
325
+ /* @__PURE__ */ jsx("div", { className: "truncate px-2 text-sm font-medium", children: title }),
326
+ !preview && !noCamera ? /* @__PURE__ */ jsx(
327
+ Button,
328
+ {
329
+ variant: "bare",
330
+ size: "bare",
331
+ type: "button",
332
+ onClick: () => setFacing((f) => f === "user" ? "environment" : "user"),
333
+ "aria-label": "Trocar c\xE2mera",
334
+ className: "font-normal flex h-10 w-10 items-center justify-center rounded-full bg-white/10 hover:bg-white/20",
335
+ children: /* @__PURE__ */ jsx(SwitchCamera, { className: "h-5 w-5" })
336
+ }
337
+ ) : /* @__PURE__ */ jsx("div", { className: "w-10" })
338
+ ] }),
339
+ /* @__PURE__ */ jsx("div", { className: "flex flex-1 items-center justify-center overflow-hidden", children: /* @__PURE__ */ jsx("div", { className: "relative h-full max-h-full max-w-full bg-black", style: { aspectRatio: String(ratio) }, children: preview ? /* @__PURE__ */ jsx("img", { src: preview.url, alt: "", className: "h-full w-full object-contain" }) : noCamera ? /* @__PURE__ */ jsxs("div", { className: "flex h-full w-full flex-col items-center justify-center gap-3 px-6 text-center text-sm text-white/80", children: [
340
+ overlayUrl && /* @__PURE__ */ jsx("img", { src: overlayUrl, alt: "", className: "max-h-[50%] opacity-60" }),
341
+ /* @__PURE__ */ jsx("p", { children: "N\xE3o foi poss\xEDvel abrir a c\xE2mera neste aparelho. Escolha uma foto do arquivo." })
342
+ ] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
343
+ /* @__PURE__ */ jsx(
344
+ "video",
345
+ {
346
+ ref: videoRef,
347
+ playsInline: true,
348
+ muted: true,
349
+ className: "h-full w-full object-cover",
350
+ style: { transform: facing === "user" ? "scaleX(-1)" : void 0 }
351
+ }
352
+ ),
353
+ overlayUrl && /* @__PURE__ */ jsx(
354
+ "img",
355
+ {
356
+ src: overlayUrl,
357
+ alt: "",
358
+ className: "pointer-events-none absolute inset-0 h-full w-full object-contain",
359
+ style: { opacity }
360
+ }
361
+ ),
362
+ starting && /* @__PURE__ */ jsx("div", { className: "absolute inset-0 flex items-center justify-center text-sm text-white/80", children: "Iniciando c\xE2mera\u2026" })
363
+ ] }) }) }),
364
+ /* @__PURE__ */ jsx("input", { ref: fileRef, type: "file", accept: "image/*", className: "hidden", onChange: (e) => pickFile(e.target.files?.[0]) }),
365
+ /* @__PURE__ */ jsxs("div", { className: "relative flex items-center justify-center gap-6 p-4 pb-[calc(2rem+env(safe-area-inset-bottom))]", children: [
366
+ preview ? /* @__PURE__ */ jsxs(Fragment, { children: [
367
+ /* @__PURE__ */ jsxs(
368
+ Button,
369
+ {
370
+ type: "button",
371
+ variant: "secondary",
372
+ size: "lg",
373
+ className: "gap-2",
374
+ onClick: () => {
375
+ URL.revokeObjectURL(preview.url);
376
+ setPreview(null);
377
+ setRetake((n) => n + 1);
378
+ },
379
+ children: [
380
+ /* @__PURE__ */ jsx(RotateCcw, { className: "h-4 w-4" }),
381
+ " Tirar novamente"
382
+ ]
383
+ }
384
+ ),
385
+ /* @__PURE__ */ jsxs(Button, { type: "button", size: "lg", className: "gap-2", onClick: () => onCapture(preview.blob), children: [
386
+ /* @__PURE__ */ jsx(Check, { className: "h-4 w-4" }),
387
+ " Usar foto"
388
+ ] })
389
+ ] }) : noCamera ? /* @__PURE__ */ jsxs(Button, { type: "button", size: "lg", className: "gap-2", onClick: () => fileRef.current?.click(), children: [
390
+ /* @__PURE__ */ jsx(ImageUp, { className: "h-4 w-4" }),
391
+ " Escolher foto"
392
+ ] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
393
+ /* @__PURE__ */ jsx(
394
+ Button,
395
+ {
396
+ variant: "bare",
397
+ size: "bare",
398
+ type: "button",
399
+ disabled: starting,
400
+ onClick: capture,
401
+ "aria-label": "Capturar",
402
+ className: "font-normal flex h-16 w-16 items-center justify-center rounded-full border-4 border-white/40 bg-white transition-transform active:scale-95 disabled:opacity-60",
403
+ children: /* @__PURE__ */ jsx(Camera, { className: "h-6 w-6 text-black" })
404
+ }
405
+ ),
406
+ /* @__PURE__ */ jsx(
407
+ Button,
408
+ {
409
+ variant: "bare",
410
+ size: "bare",
411
+ type: "button",
412
+ onClick: () => fileRef.current?.click(),
413
+ "aria-label": "Escolher do arquivo",
414
+ className: "font-normal absolute left-6 flex h-12 w-12 items-center justify-center rounded-full bg-white/10 hover:bg-white/20",
415
+ children: /* @__PURE__ */ jsx(ImageUp, { className: "h-5 w-5" })
416
+ }
417
+ )
418
+ ] }),
419
+ !preview && onSkip && /* @__PURE__ */ jsx(
420
+ Button,
421
+ {
422
+ variant: "bare",
423
+ size: "bare",
424
+ type: "button",
425
+ onClick: onSkip,
426
+ "aria-label": "Pular esta foto",
427
+ className: "font-normal absolute right-6 flex h-12 w-12 items-center justify-center rounded-full bg-white/10 hover:bg-white/20",
428
+ children: /* @__PURE__ */ jsx(SkipForward, { className: "h-5 w-5" })
429
+ }
430
+ )
431
+ ] })
432
+ ] }),
433
+ document.body
434
+ );
435
+ }
436
+ var RICH_TEXT_CLASS = [
437
+ "space-y-2",
438
+ "[&_p]:leading-relaxed",
439
+ "[&_b]:font-semibold [&_strong]:font-semibold",
440
+ "[&_i]:italic [&_em]:italic",
441
+ "[&_u]:underline",
442
+ "[&_ul]:list-disc [&_ul]:pl-5 [&_ol]:list-decimal [&_ol]:pl-5 [&_li]:leading-relaxed",
443
+ "[&_blockquote]:border-l-2 [&_blockquote]:border-border [&_blockquote]:pl-3 [&_blockquote]:text-muted-foreground",
444
+ "[&_a]:text-primary [&_a]:underline",
445
+ "[&_h1]:font-semibold [&_h2]:font-semibold [&_h3]:font-semibold",
446
+ "[&_table]:w-full [&_td]:border [&_td]:border-border [&_td]:px-2 [&_td]:py-1",
447
+ "[&_th]:border [&_th]:border-border [&_th]:px-2 [&_th]:py-1 [&_th]:font-medium"
448
+ ].join(" ");
449
+ function isFieldEmpty(field, value) {
450
+ if (field.type === "title") return false;
451
+ if (value == null || value === "" || value === false) return true;
452
+ if (Array.isArray(value)) return value.length === 0;
453
+ if (field.type === "yes_no" || field.type === "yes_no_text") return yesNoAnswer(value) === "";
454
+ return false;
455
+ }
456
+ function ReadField({ label, children }) {
457
+ return /* @__PURE__ */ jsxs("div", { className: "space-y-0.5", children: [
458
+ label && /* @__PURE__ */ jsx("p", { className: "text-xs font-medium text-muted-foreground", children: label }),
459
+ /* @__PURE__ */ jsx("div", { className: "text-sm text-foreground", children })
460
+ ] });
461
+ }
462
+ function EmptyValue() {
463
+ return /* @__PURE__ */ jsx("span", { className: "text-muted-foreground/50", children: "\u2014" });
464
+ }
465
+ function optionLabel(field, value) {
466
+ if (value == null || value === "") return null;
467
+ const match = (field.options ?? []).find((o) => o.value === value);
468
+ return match?.label ?? String(value);
469
+ }
470
+ function ReadValue({ field, value }) {
471
+ switch (field.type) {
472
+ case "memo":
473
+ case "richtext": {
474
+ const rich = fieldRichValue(value);
475
+ return /* @__PURE__ */ jsx(ReadField, { label: field.label, children: "html" in rich ? /* @__PURE__ */ jsx("div", { className: RICH_TEXT_CLASS, dangerouslySetInnerHTML: { __html: rich.html } }) : rich.text ? /* @__PURE__ */ jsx("p", { className: "whitespace-pre-wrap", children: rich.text }) : /* @__PURE__ */ jsx(EmptyValue, {}) });
476
+ }
477
+ case "date": {
478
+ const raw = typeof value === "string" ? value : "";
479
+ const date = raw ? new Date(raw) : null;
480
+ const text = date && !Number.isNaN(date.getTime()) ? raw.slice(0, 10).split("-").reverse().join("/") : raw;
481
+ return /* @__PURE__ */ jsx(ReadField, { label: field.label, children: text || /* @__PURE__ */ jsx(EmptyValue, {}) });
482
+ }
483
+ case "select":
484
+ case "radio": {
485
+ const label = optionLabel(field, value);
486
+ return /* @__PURE__ */ jsx(ReadField, { label: field.label, children: label ?? /* @__PURE__ */ jsx(EmptyValue, {}) });
487
+ }
488
+ case "tags": {
489
+ const selected = Array.isArray(value) ? value : [];
490
+ return /* @__PURE__ */ jsx(ReadField, { label: field.label, children: selected.length > 0 ? /* @__PURE__ */ jsx("div", { className: "flex flex-wrap gap-1.5", children: selected.map((v) => /* @__PURE__ */ jsx(Badge, { variant: "secondary", className: "text-xs", children: optionLabel(field, v) ?? v }, v)) }) : /* @__PURE__ */ jsx(EmptyValue, {}) });
491
+ }
492
+ case "checkbox":
493
+ return /* @__PURE__ */ jsxs(ReadField, { children: [
494
+ /* @__PURE__ */ jsx("span", { className: "font-medium", children: field.label }),
495
+ /* @__PURE__ */ jsx("span", { className: "text-muted-foreground", children: " \xB7 " }),
496
+ /* @__PURE__ */ jsx("span", { children: value ? "Sim" : "N\xE3o" })
497
+ ] });
498
+ case "yes_no":
499
+ case "yes_no_text": {
500
+ const answer = yesNoAnswer(value);
501
+ const text = value && typeof value === "object" ? value.text?.trim() : "";
502
+ const alert = answerAlerts(field, value);
503
+ return /* @__PURE__ */ jsxs(ReadField, { label: field.label, children: [
504
+ answer ? /* @__PURE__ */ jsx("span", { className: "font-medium", children: answer === "sim" ? "Sim" : "N\xE3o" }) : /* @__PURE__ */ jsx(EmptyValue, {}),
505
+ text ? /* @__PURE__ */ jsxs("span", { className: "text-muted-foreground", children: [
506
+ " \u2014 ",
507
+ text
508
+ ] }) : null,
509
+ alert && /* @__PURE__ */ jsx("div", { children: /* @__PURE__ */ jsx(AnswerAlertNote, { alert }) })
510
+ ] });
511
+ }
512
+ case "stencil_photo":
513
+ case "signature":
514
+ return /* @__PURE__ */ jsx(ReadField, { label: field.label, children: isMedia(value) ? /* @__PURE__ */ jsx(
515
+ MediaImage,
516
+ {
517
+ value,
518
+ alt: field.label,
519
+ className: field.type === "signature" ? "max-h-32 rounded-md border bg-white" : "w-full max-w-xs rounded-lg border object-cover",
520
+ style: field.type === "signature" ? void 0 : { aspectRatio: String(aspectRatioOf(stencilOf(field))) }
521
+ }
522
+ ) : /* @__PURE__ */ jsx(EmptyValue, {}) });
523
+ case "gallery": {
524
+ const items = Array.isArray(value) ? value.filter(isMedia) : [];
525
+ return /* @__PURE__ */ jsx(ReadField, { label: field.label, children: items.length ? /* @__PURE__ */ jsx(GalleryView, { items }) : /* @__PURE__ */ jsx(EmptyValue, {}) });
526
+ }
527
+ case "recommendation":
528
+ return /* @__PURE__ */ jsx(ReadField, { label: field.label, children: /* @__PURE__ */ jsx(RecommendationView, { items: Array.isArray(value) ? value : [] }) });
529
+ default: {
530
+ const text = value == null || value === "" ? null : String(value);
531
+ return /* @__PURE__ */ jsx(ReadField, { label: field.label, children: text ?? /* @__PURE__ */ jsx(EmptyValue, {}) });
532
+ }
533
+ }
534
+ }
535
+ function FieldLabel({ field }) {
536
+ return /* @__PURE__ */ jsxs("label", { className: "text-xs font-medium block", children: [
537
+ field.label,
538
+ field.required && /* @__PURE__ */ jsx("span", { className: "text-destructive ml-0.5", children: "*" })
539
+ ] });
540
+ }
541
+ function FieldRenderer({
542
+ field,
543
+ value,
544
+ onChange,
545
+ readOnly,
546
+ onOpenCamera,
547
+ uploading,
548
+ shortcuts
549
+ }) {
550
+ useTranslation();
551
+ if (readOnly && field.type !== "title" && field.type !== "image" && field.type !== "budget") {
552
+ return /* @__PURE__ */ jsx(ReadValue, { field, value });
553
+ }
554
+ switch (field.type) {
555
+ case "title":
556
+ return /* @__PURE__ */ jsx("h3", { className: "text-base font-semibold pt-2 pb-1 border-b", children: field.label });
557
+ case "text":
558
+ return /* @__PURE__ */ jsxs("div", { className: "space-y-1.5", children: [
559
+ /* @__PURE__ */ jsxs("label", { className: "text-xs font-medium block", children: [
560
+ field.label,
561
+ field.required && /* @__PURE__ */ jsx("span", { className: "text-destructive ml-0.5", children: "*" })
562
+ ] }),
563
+ /* @__PURE__ */ jsx(
564
+ Input,
565
+ {
566
+ value: value ?? "",
567
+ onChange: (e) => onChange(e.target.value),
568
+ placeholder: htmlToPlainText2(field.placeholder),
569
+ className: "h-9 text-sm",
570
+ readOnly
571
+ }
572
+ )
573
+ ] });
574
+ case "memo":
575
+ case "richtext":
576
+ return /* @__PURE__ */ jsxs("div", { className: "space-y-1.5", children: [
577
+ /* @__PURE__ */ jsxs("label", { className: "text-xs font-medium block", children: [
578
+ field.label,
579
+ field.required && /* @__PURE__ */ jsx("span", { className: "text-destructive ml-0.5", children: "*" })
580
+ ] }),
581
+ /* @__PURE__ */ jsx(
582
+ Textarea,
583
+ {
584
+ value: value ?? "",
585
+ onChange: (e) => onChange(e.target.value),
586
+ placeholder: htmlToPlainText2(field.placeholder),
587
+ className: "min-h-[100px] bg-background shadow-none",
588
+ readOnly
589
+ }
590
+ )
591
+ ] });
592
+ case "date":
593
+ return /* @__PURE__ */ jsxs("div", { className: "space-y-1.5", children: [
594
+ /* @__PURE__ */ jsxs("label", { className: "text-xs font-medium block", children: [
595
+ field.label,
596
+ field.required && /* @__PURE__ */ jsx("span", { className: "text-destructive ml-0.5", children: "*" })
597
+ ] }),
598
+ /* @__PURE__ */ jsx(
599
+ Input,
600
+ {
601
+ type: "date",
602
+ value: value ?? "",
603
+ onChange: (e) => onChange(e.target.value),
604
+ className: "h-9 text-sm",
605
+ readOnly
606
+ }
607
+ )
608
+ ] });
609
+ case "select":
610
+ return /* @__PURE__ */ jsxs("div", { className: "space-y-1.5", children: [
611
+ /* @__PURE__ */ jsxs("label", { className: "text-xs font-medium block", children: [
612
+ field.label,
613
+ field.required && /* @__PURE__ */ jsx("span", { className: "text-destructive ml-0.5", children: "*" })
614
+ ] }),
615
+ readOnly ? /* @__PURE__ */ jsx("p", { className: "text-sm py-1", children: value ?? "-" }) : /* @__PURE__ */ jsxs(Select, { value: value ?? "", onValueChange: onChange, children: [
616
+ /* @__PURE__ */ jsx(SelectTrigger, { className: "h-9 text-sm", children: /* @__PURE__ */ jsx(SelectValue, { placeholder: htmlToPlainText2(field.placeholder) ?? "..." }) }),
617
+ /* @__PURE__ */ jsx(SelectContent, { children: (field.options ?? []).map((opt) => /* @__PURE__ */ jsx(SelectItem, { value: opt.value, children: opt.label }, opt.value)) })
618
+ ] })
619
+ ] });
620
+ case "radio":
621
+ return /* @__PURE__ */ jsxs("div", { className: "space-y-1.5", children: [
622
+ /* @__PURE__ */ jsxs("label", { className: "text-xs font-medium block", children: [
623
+ field.label,
624
+ field.required && /* @__PURE__ */ jsx("span", { className: "text-destructive ml-0.5", children: "*" })
625
+ ] }),
626
+ /* @__PURE__ */ jsx("div", { className: "space-y-1", children: (field.options ?? []).map((opt) => /* @__PURE__ */ jsxs("label", { className: "flex items-center gap-2 cursor-pointer", children: [
627
+ /* @__PURE__ */ jsx(
628
+ "input",
629
+ {
630
+ type: "radio",
631
+ name: field.id,
632
+ value: opt.value,
633
+ checked: value === opt.value,
634
+ onChange: () => onChange(opt.value),
635
+ disabled: readOnly,
636
+ className: "text-primary"
637
+ }
638
+ ),
639
+ /* @__PURE__ */ jsx("span", { className: "text-sm", children: opt.label })
640
+ ] }, opt.value)) })
641
+ ] });
642
+ case "tags": {
643
+ const selectedTags = Array.isArray(value) ? value : [];
644
+ return /* @__PURE__ */ jsxs("div", { className: "space-y-1.5", children: [
645
+ /* @__PURE__ */ jsxs("label", { className: "text-xs font-medium block", children: [
646
+ field.label,
647
+ field.required && /* @__PURE__ */ jsx("span", { className: "text-destructive ml-0.5", children: "*" })
648
+ ] }),
649
+ /* @__PURE__ */ jsx("div", { className: "flex flex-wrap gap-1.5", children: (field.options ?? []).map((opt) => {
650
+ const active = selectedTags.includes(opt.value);
651
+ return /* @__PURE__ */ jsx(
652
+ Badge,
653
+ {
654
+ variant: active ? "default" : "outline",
655
+ className: `cursor-pointer text-xs ${readOnly ? "pointer-events-none" : ""}`,
656
+ onClick: () => {
657
+ if (readOnly) return;
658
+ onChange(
659
+ active ? selectedTags.filter((t2) => t2 !== opt.value) : [...selectedTags, opt.value]
660
+ );
661
+ },
662
+ children: opt.label
663
+ },
664
+ opt.value
665
+ );
666
+ }) })
667
+ ] });
668
+ }
669
+ case "checkbox":
670
+ return /* @__PURE__ */ jsxs("label", { className: "flex items-center gap-2 cursor-pointer py-1", children: [
671
+ /* @__PURE__ */ jsx(Checkbox, { checked: !!value, onChange, disabled: readOnly }),
672
+ /* @__PURE__ */ jsxs("span", { className: "text-sm font-medium", children: [
673
+ field.label,
674
+ field.required && /* @__PURE__ */ jsx("span", { className: "text-destructive ml-0.5", children: "*" })
675
+ ] })
676
+ ] });
677
+ case "yes_no":
678
+ return /* @__PURE__ */ jsxs("div", { className: "space-y-1.5", children: [
679
+ /* @__PURE__ */ jsx(FieldLabel, { field }),
680
+ /* @__PURE__ */ jsx(YesNoField, { field, value, onChange })
681
+ ] });
682
+ case "yes_no_text":
683
+ return /* @__PURE__ */ jsxs("div", { className: "space-y-1.5", children: [
684
+ /* @__PURE__ */ jsx(FieldLabel, { field }),
685
+ /* @__PURE__ */ jsx(YesNoTextField, { field, value, onChange })
686
+ ] });
687
+ case "stencil_photo":
688
+ return /* @__PURE__ */ jsxs("div", { className: "space-y-1.5", children: [
689
+ /* @__PURE__ */ jsx(FieldLabel, { field }),
690
+ /* @__PURE__ */ jsx(
691
+ StencilPhotoField,
692
+ {
693
+ field,
694
+ value,
695
+ onChange,
696
+ uploading,
697
+ onOpenCamera: () => onOpenCamera?.()
698
+ }
699
+ )
700
+ ] });
701
+ case "gallery":
702
+ return /* @__PURE__ */ jsxs("div", { className: "space-y-1.5", children: [
703
+ /* @__PURE__ */ jsx(FieldLabel, { field }),
704
+ /* @__PURE__ */ jsx(GalleryField, { field, value, onChange })
705
+ ] });
706
+ case "recommendation":
707
+ return /* @__PURE__ */ jsxs("div", { className: "space-y-1.5", children: [
708
+ /* @__PURE__ */ jsx(FieldLabel, { field }),
709
+ /* @__PURE__ */ jsx(RecommendationField, { value, onChange, shortcuts })
710
+ ] });
711
+ case "signature":
712
+ return /* @__PURE__ */ jsxs("div", { className: "space-y-1.5", children: [
713
+ /* @__PURE__ */ jsx(FieldLabel, { field }),
714
+ /* @__PURE__ */ jsx(SignatureField, { field, value, onChange })
715
+ ] });
716
+ case "image":
717
+ return /* @__PURE__ */ jsxs("div", { className: "space-y-1.5", children: [
718
+ /* @__PURE__ */ jsxs("label", { className: "text-xs font-medium block", children: [
719
+ field.label,
720
+ field.required && /* @__PURE__ */ jsx("span", { className: "text-destructive ml-0.5", children: "*" })
721
+ ] }),
722
+ /* @__PURE__ */ jsx("div", { className: "flex items-center justify-center rounded-lg border-2 border-dashed py-8 text-sm text-muted-foreground", children: "Image upload \u2014 v2" })
723
+ ] });
724
+ case "budget":
725
+ return /* @__PURE__ */ jsxs("div", { className: "space-y-1.5", children: [
726
+ /* @__PURE__ */ jsxs("label", { className: "text-xs font-medium block", children: [
727
+ field.label,
728
+ field.required && /* @__PURE__ */ jsx("span", { className: "text-destructive ml-0.5", children: "*" })
729
+ ] }),
730
+ /* @__PURE__ */ jsx("div", { className: "flex items-center justify-center rounded-lg border-2 border-dashed py-8 text-sm text-muted-foreground", children: "Budget / line items \u2014 v2" })
731
+ ] });
732
+ default:
733
+ return null;
734
+ }
735
+ }
736
+ function FormRenderer({ schema, data, onChange, readOnly, missing, recommendationShortcuts }) {
737
+ const sortedFields = [...schema.fields].sort((a, b) => a.row - b.row || a.col - b.col);
738
+ const upload = useMediaUpload();
739
+ const [camera, setCamera] = React.useState(null);
740
+ const [uploading, setUploading] = React.useState(null);
741
+ const [cameraError, setCameraError] = React.useState(null);
742
+ const dataRef = React.useRef(data);
743
+ dataRef.current = data;
744
+ const handleFieldChange = useCallback(
745
+ (fieldId, value) => {
746
+ onChange({ ...dataRef.current, [fieldId]: value });
747
+ },
748
+ [onChange]
749
+ );
750
+ const nextStencil = (afterId) => {
751
+ const i = sortedFields.findIndex((f) => f.id === afterId);
752
+ return sortedFields.slice(i + 1).find((f) => f.type === "stencil_photo" && !isMedia(dataRef.current[f.id])) ?? null;
753
+ };
754
+ async function onCapture(photo) {
755
+ const field = camera;
756
+ if (!field || !upload) return;
757
+ setCamera(nextStencil(field.id));
758
+ setUploading(field.id);
759
+ setCameraError(null);
760
+ try {
761
+ const { path } = await upload(photo, `uploads/${field.id}`, "foto.jpg");
762
+ handleFieldChange(field.id, { path, type: "photo" });
763
+ } catch (e) {
764
+ setCameraError(`${field.label}: ${describeError(e, "n\xE3o foi poss\xEDvel enviar a foto")}`);
765
+ } finally {
766
+ setUploading(null);
767
+ }
768
+ }
769
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
770
+ cameraError && /* @__PURE__ */ jsx("p", { className: "mb-3 rounded-lg bg-destructive/10 px-3 py-2 text-xs text-destructive", children: cameraError }),
771
+ /* @__PURE__ */ jsx(
772
+ "div",
773
+ {
774
+ className: "grid gap-x-4 gap-y-3",
775
+ style: { gridTemplateColumns: "repeat(12, 1fr)" },
776
+ children: sortedFields.map((field) => /* @__PURE__ */ jsx(
777
+ "div",
778
+ {
779
+ id: `ff-field-${field.id}`,
780
+ style: { gridColumn: `span ${field.colSpan}` },
781
+ className: missing?.has(field.id) ? "rounded-lg ring-2 ring-destructive/60 ring-offset-4 ring-offset-background" : void 0,
782
+ children: /* @__PURE__ */ jsx(
783
+ FieldRenderer,
784
+ {
785
+ field,
786
+ value: data[field.id],
787
+ onChange: (v) => handleFieldChange(field.id, v),
788
+ readOnly,
789
+ uploading: uploading === field.id,
790
+ onOpenCamera: () => setCamera(field),
791
+ shortcuts: recommendationShortcuts
792
+ }
793
+ )
794
+ },
795
+ field.id
796
+ ))
797
+ }
798
+ ),
799
+ camera && /* @__PURE__ */ jsx(CameraHost, { field: camera, onCapture, onSkip: () => setCamera(nextStencil(camera.id)), onClose: () => setCamera(null) })
800
+ ] });
801
+ }
802
+ function CameraHost({ field, onCapture, onSkip, onClose }) {
803
+ const cfg = stencilOf(field);
804
+ const overlay = useMediaUrl(cfg.overlayPath);
805
+ return /* @__PURE__ */ jsx(
806
+ StencilCamera,
807
+ {
808
+ title: field.label,
809
+ overlayUrl: overlay,
810
+ opacity: cfg.opacity ?? 0.6,
811
+ aspect: cfg.aspect ?? "3:4",
812
+ camera: cfg.camera ?? "environment",
813
+ burnOverlay: !!cfg.burnOverlay,
814
+ onCapture,
815
+ onSkip,
816
+ onClose
817
+ },
818
+ field.id
819
+ );
820
+ }
821
+
822
+ export { FormRenderer, fieldRichValue, htmlToPlainText2 as htmlToPlainText, isFieldEmpty, sanitizeHtml };
823
+ //# sourceMappingURL=chunk-RB7UYZLA.js.map
824
+ //# sourceMappingURL=chunk-RB7UYZLA.js.map