@marktiderman/genesis-forms-studio 0.0.0-stage → 0.2.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.
package/dist/index.js ADDED
@@ -0,0 +1,1011 @@
1
+ "use client";
2
+ import { SAVE_STATE_LABEL, readinessChip, prefilledUrl, embedSnippet, STATUS_FILTER_LABEL, lowThisWeek, NO_FILTERS, lowScaleAtMost, dayLabel } from './chunk-HAEZ7G3G.js';
3
+ export { NO_FILTERS, RESPONSE_STATUS_LABEL, SAVE_STATE_LABEL, STATUS_FILTER_LABEL, answerCell, answerColumns, answersByKey, carryAnswers, comparisonFilter, dayLabel, dropOffByQuestion, embedSnippet, filterResponses, followUpOf, formAvailability, formCounts, formNavItems, formStatusChip, hasLowRating, hasLowRatingExcept, headlineCards, headlineNumbers, highestRating, isFiltered, isFull, lowRatingAtMost, lowRatingLabel, lowScaleAtMost, lowThisWeek, lowestRating, matchesStatus, needsFollowUp, parseInstant, plainLabel, prefilledUrl, presetRange, readinessChip, readinessOf, summarizeResponses, takesResponses, toCsv, toResponseFilter, valuesByKey } from './chunk-HAEZ7G3G.js';
4
+ import { StatusBadge, cn, Badge, Breadcrumb, BreadcrumbList, BreadcrumbItem, BreadcrumbLink, BreadcrumbSeparator, BreadcrumbPage, Button, Separator, ToggleGroup, ToggleGroupItem, Skeleton, Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, Tabs, TabsList, TabsTrigger, TabsContent, Select, SelectTrigger, SelectValue, SelectContent, SelectItem, Label, Textarea, Input, EmptyState, toast } from '@marktiderman/genesis-ui';
5
+ import { Settings, Inbox, GitBranch, ListChecks, Check, Loader2, TriangleAlert, Undo2, Redo2, Eye, Share2, Rocket, CircleAlert, CircleCheck, Star, ExternalLink, Printer, Copy, CalendarDays, CircleDashed, X, CircleDot, SquareCheck, Gauge, Type, ChartNoAxesColumnDecreasing, ImageOff, Image } from 'lucide-react';
6
+ import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
7
+ import { useId, useState, useEffect } from 'react';
8
+
9
+ function Chip({
10
+ tone,
11
+ icon,
12
+ children,
13
+ className,
14
+ testID
15
+ }) {
16
+ const inner = /* @__PURE__ */ jsxs(Fragment, { children: [
17
+ icon ? /* @__PURE__ */ jsx("span", { className: "mr-1 inline-flex [&_svg]:size-3", "aria-hidden": true, children: icon }) : null,
18
+ children
19
+ ] });
20
+ if (tone === "danger") {
21
+ return /* @__PURE__ */ jsx(StatusBadge, { status: "offTrack", className: cn("font-normal", className), "data-testid": testID, children: inner });
22
+ }
23
+ return /* @__PURE__ */ jsx(Badge, { variant: tone, className: cn("font-normal", className), "data-testid": testID, children: inner });
24
+ }
25
+ var SAVE_TONE = {
26
+ saved: "muted",
27
+ saving: "muted",
28
+ unsaved: "muted",
29
+ failed: "warning",
30
+ stopped: "danger",
31
+ conflict: "danger"
32
+ };
33
+ function ResourceHeader({
34
+ title,
35
+ untitled = "Untitled",
36
+ parent,
37
+ status,
38
+ saveState,
39
+ onUndo,
40
+ onRedo,
41
+ canUndo = true,
42
+ canRedo = true,
43
+ onPreview,
44
+ onShare,
45
+ publish,
46
+ actions,
47
+ className
48
+ }) {
49
+ const history = onUndo || onRedo;
50
+ const name = title.trim() || untitled;
51
+ return /* @__PURE__ */ jsxs(
52
+ "header",
53
+ {
54
+ "data-slot": "resource-header",
55
+ className: cn("flex flex-col gap-3 md:flex-row md:items-start md:justify-between", className),
56
+ children: [
57
+ /* @__PURE__ */ jsxs("div", { className: "min-w-0 flex-1 space-y-1", children: [
58
+ parent ? /* @__PURE__ */ jsx(Breadcrumb, { children: /* @__PURE__ */ jsxs(BreadcrumbList, { className: "text-xs", children: [
59
+ /* @__PURE__ */ jsx(BreadcrumbItem, { children: /* @__PURE__ */ jsx(BreadcrumbLink, { asChild: true, children: parent }) }),
60
+ /* @__PURE__ */ jsx(BreadcrumbSeparator, {}),
61
+ /* @__PURE__ */ jsx(BreadcrumbItem, { className: "min-w-0", children: /* @__PURE__ */ jsx(BreadcrumbPage, { className: "truncate", children: name }) })
62
+ ] }) }) : null,
63
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center gap-x-3 gap-y-1", children: [
64
+ /* @__PURE__ */ jsx("h1", { className: "min-w-0 break-words text-xl font-semibold tracking-tight sm:text-2xl", children: name }),
65
+ status ? /* @__PURE__ */ jsx(Chip, { tone: status.tone, testID: "resource-status", children: status.label }) : null,
66
+ saveState ? /* @__PURE__ */ jsx("span", { "aria-live": "polite", children: /* @__PURE__ */ jsx(
67
+ Chip,
68
+ {
69
+ tone: SAVE_TONE[saveState],
70
+ testID: "resource-save-state",
71
+ icon: saveState === "saved" ? /* @__PURE__ */ jsx(Check, {}) : saveState === "saving" ? /* @__PURE__ */ jsx(Loader2, { className: "animate-spin" }) : saveState === "unsaved" ? null : /* @__PURE__ */ jsx(TriangleAlert, {}),
72
+ children: SAVE_STATE_LABEL[saveState]
73
+ }
74
+ ) }) : null
75
+ ] })
76
+ ] }),
77
+ /* @__PURE__ */ jsxs("div", { className: "flex shrink-0 flex-wrap items-center gap-2", children: [
78
+ history ? /* @__PURE__ */ jsxs(Fragment, { children: [
79
+ /* @__PURE__ */ jsx(
80
+ Button,
81
+ {
82
+ variant: "ghost",
83
+ size: "icon",
84
+ "aria-label": "Undo",
85
+ title: "Undo",
86
+ disabled: !onUndo || !canUndo,
87
+ onClick: onUndo,
88
+ "data-testid": "resource-undo",
89
+ children: /* @__PURE__ */ jsx(Undo2, { className: "size-4" })
90
+ }
91
+ ),
92
+ /* @__PURE__ */ jsx(
93
+ Button,
94
+ {
95
+ variant: "ghost",
96
+ size: "icon",
97
+ "aria-label": "Redo",
98
+ title: "Redo",
99
+ disabled: !onRedo || !canRedo,
100
+ onClick: onRedo,
101
+ "data-testid": "resource-redo",
102
+ children: /* @__PURE__ */ jsx(Redo2, { className: "size-4" })
103
+ }
104
+ ),
105
+ /* @__PURE__ */ jsx(Separator, { orientation: "vertical", className: "mx-1 h-6" })
106
+ ] }) : null,
107
+ actions,
108
+ onPreview ? /* @__PURE__ */ jsx(Button, { variant: "ghost", startIcon: /* @__PURE__ */ jsx(Eye, { className: "size-4" }), onClick: onPreview, "data-testid": "resource-preview", children: "Preview" }) : null,
109
+ onShare ? /* @__PURE__ */ jsx(Button, { variant: "outline", startIcon: /* @__PURE__ */ jsx(Share2, { className: "size-4" }), onClick: onShare, "data-testid": "resource-share", children: "Share" }) : null,
110
+ publish ? /* @__PURE__ */ jsx(
111
+ Button,
112
+ {
113
+ startIcon: /* @__PURE__ */ jsx(Rocket, { className: "size-4" }),
114
+ loading: publish.loading,
115
+ disabled: publish.disabled,
116
+ onClick: publish.onClick,
117
+ "data-testid": "resource-publish",
118
+ children: publish.label ?? "Publish"
119
+ }
120
+ ) : null
121
+ ] })
122
+ ]
123
+ }
124
+ );
125
+ }
126
+ function ReadinessChip({
127
+ readiness,
128
+ onClick,
129
+ className
130
+ }) {
131
+ const { label, tone } = readinessChip(readiness);
132
+ const icon = readiness.state === "ready" ? /* @__PURE__ */ jsx(Check, {}) : readiness.state === "problems" ? /* @__PURE__ */ jsx(CircleAlert, {}) : readiness.state === "warnings" ? /* @__PURE__ */ jsx(TriangleAlert, {}) : null;
133
+ const chip = /* @__PURE__ */ jsx(Chip, { tone, icon, testID: "readiness", className, children: label });
134
+ if (!onClick) return chip;
135
+ return /* @__PURE__ */ jsx(
136
+ "button",
137
+ {
138
+ type: "button",
139
+ onClick,
140
+ className: "rounded-full focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
141
+ children: chip
142
+ }
143
+ );
144
+ }
145
+ function ResponsesToolbar({
146
+ view,
147
+ onViewChange,
148
+ filters,
149
+ actions,
150
+ className
151
+ }) {
152
+ return /* @__PURE__ */ jsxs(
153
+ "div",
154
+ {
155
+ "data-slot": "responses-toolbar",
156
+ className: cn("flex flex-col gap-2 lg:flex-row lg:items-start lg:justify-between", className),
157
+ children: [
158
+ /* @__PURE__ */ jsxs("div", { className: "flex min-w-0 flex-wrap items-center gap-2", children: [
159
+ /* @__PURE__ */ jsxs(
160
+ ToggleGroup,
161
+ {
162
+ type: "single",
163
+ variant: "outline",
164
+ size: "sm",
165
+ className: "h-9",
166
+ value: view,
167
+ onValueChange: (v) => v && onViewChange(v),
168
+ "aria-label": "Show responses as",
169
+ children: [
170
+ /* @__PURE__ */ jsx(ToggleGroupItem, { value: "summary", className: "h-9 px-3", "data-testid": "responses-view-summary", children: "Summary" }),
171
+ /* @__PURE__ */ jsx(ToggleGroupItem, { value: "each", className: "h-9 px-3", "data-testid": "responses-view-each", children: "Each response" })
172
+ ]
173
+ }
174
+ ),
175
+ filters
176
+ ] }),
177
+ actions ? /* @__PURE__ */ jsx("div", { className: "flex shrink-0 flex-wrap items-center gap-2", children: actions }) : null
178
+ ]
179
+ }
180
+ );
181
+ }
182
+ var TONE = {
183
+ positive: "text-emerald-700 dark:text-emerald-300",
184
+ negative: "text-red-700 dark:text-red-300",
185
+ warning: "text-amber-800 dark:text-amber-200",
186
+ muted: "text-muted-foreground"
187
+ };
188
+ function HeadlineNumbers({
189
+ numbers,
190
+ isLoading = false,
191
+ className
192
+ }) {
193
+ if (isLoading) {
194
+ return /* @__PURE__ */ jsx("div", { className: cn("grid grid-cols-2 gap-3 lg:grid-cols-4", className), children: [0, 1, 2, 3].map((i) => /* @__PURE__ */ jsx(Skeleton, { className: "h-28 w-full rounded-lg" }, i)) });
195
+ }
196
+ if (!numbers.length) return null;
197
+ return /* @__PURE__ */ jsx(
198
+ "dl",
199
+ {
200
+ "data-slot": "headline-numbers",
201
+ className: cn(
202
+ "grid grid-cols-2 gap-3",
203
+ numbers.length >= 4 ? "lg:grid-cols-4" : numbers.length === 3 ? "lg:grid-cols-3" : "",
204
+ className
205
+ ),
206
+ children: numbers.map((n) => /* @__PURE__ */ jsxs(
207
+ "div",
208
+ {
209
+ className: "flex min-w-0 flex-col gap-1 rounded-lg border border-border bg-card p-4 text-card-foreground",
210
+ "data-testid": `headline-${n.key}`,
211
+ children: [
212
+ /* @__PURE__ */ jsx("dt", { className: "truncate text-sm text-muted-foreground", children: n.label }),
213
+ /* @__PURE__ */ jsx("dd", { className: "text-2xl font-semibold tabular-nums tracking-tight sm:text-3xl", children: n.value }),
214
+ n.detail ? /* @__PURE__ */ jsx("dd", { className: cn("text-xs", TONE[n.tone ?? "muted"]), children: n.detail }) : null
215
+ ]
216
+ },
217
+ n.key
218
+ ))
219
+ }
220
+ );
221
+ }
222
+ function SummaryCard({
223
+ icon,
224
+ title,
225
+ meta,
226
+ children,
227
+ className,
228
+ testID
229
+ }) {
230
+ return /* @__PURE__ */ jsxs(
231
+ "section",
232
+ {
233
+ "aria-label": title,
234
+ "data-testid": testID,
235
+ "data-slot": "summary-card",
236
+ className: cn("flex flex-col gap-4 rounded-lg border border-border bg-card p-5 text-card-foreground", className),
237
+ children: [
238
+ /* @__PURE__ */ jsxs("header", { className: "flex flex-wrap items-baseline justify-between gap-x-4 gap-y-1", children: [
239
+ /* @__PURE__ */ jsxs("h3", { className: "flex min-w-0 items-center gap-2 font-semibold", children: [
240
+ icon ? /* @__PURE__ */ jsx("span", { className: "shrink-0 text-muted-foreground [&_svg]:size-4", "aria-hidden": true, children: icon }) : null,
241
+ /* @__PURE__ */ jsx("span", { className: "min-w-0 break-words", children: title })
242
+ ] }),
243
+ meta ? /* @__PURE__ */ jsx("span", { className: "text-xs text-muted-foreground", children: meta }) : null
244
+ ] }),
245
+ children
246
+ ]
247
+ }
248
+ );
249
+ }
250
+ function FollowUpList({
251
+ entries,
252
+ actions,
253
+ title = "Needs a follow-up",
254
+ isLoading = false,
255
+ empty = "Nothing to follow up right now.",
256
+ className
257
+ }) {
258
+ return /* @__PURE__ */ jsxs(
259
+ "section",
260
+ {
261
+ "aria-label": title,
262
+ "data-testid": "follow-up-list",
263
+ className: cn("flex flex-col gap-3 rounded-lg border border-border bg-card p-4 text-card-foreground", className),
264
+ children: [
265
+ /* @__PURE__ */ jsxs("header", { className: "flex items-center justify-between gap-2", children: [
266
+ /* @__PURE__ */ jsxs("h3", { className: "flex items-center gap-2 font-semibold", children: [
267
+ /* @__PURE__ */ jsx(TriangleAlert, { className: "size-4 text-amber-800 dark:text-amber-200", "aria-hidden": true }),
268
+ title
269
+ ] }),
270
+ !isLoading ? /* @__PURE__ */ jsx("span", { className: "rounded-md bg-muted px-2 py-0.5 text-xs tabular-nums text-muted-foreground", children: entries.length }) : null
271
+ ] }),
272
+ isLoading ? /* @__PURE__ */ jsxs(Fragment, { children: [
273
+ /* @__PURE__ */ jsx(Skeleton, { className: "h-24 w-full rounded-md" }),
274
+ /* @__PURE__ */ jsx(Skeleton, { className: "h-24 w-full rounded-md" })
275
+ ] }) : entries.length === 0 ? /* @__PURE__ */ jsxs("p", { className: "flex items-center gap-2 text-sm text-muted-foreground", children: [
276
+ /* @__PURE__ */ jsx(CircleCheck, { className: "size-4 shrink-0", "aria-hidden": true }),
277
+ empty
278
+ ] }) : /* @__PURE__ */ jsx("ul", { className: "flex flex-col gap-3", children: entries.map((entry) => {
279
+ const buttons = actions?.(entry);
280
+ return /* @__PURE__ */ jsxs(
281
+ "li",
282
+ {
283
+ className: "flex flex-col gap-1.5 rounded-md border border-border p-3",
284
+ "data-testid": `follow-up-${entry.id}`,
285
+ children: [
286
+ /* @__PURE__ */ jsxs("div", { className: "flex items-start justify-between gap-2", children: [
287
+ /* @__PURE__ */ jsxs("p", { className: "flex min-w-0 flex-wrap items-center gap-2", children: [
288
+ /* @__PURE__ */ jsx("span", { className: "break-words font-medium", children: entry.name }),
289
+ entry.tag ? /* @__PURE__ */ jsx(Chip, { tone: entry.tag.tone ?? "muted", children: entry.tag.label }) : null
290
+ ] }),
291
+ entry.rating ? /* @__PURE__ */ jsxs(
292
+ "span",
293
+ {
294
+ className: "flex shrink-0 items-center gap-0.5 text-sm font-medium tabular-nums text-amber-800 dark:text-amber-200",
295
+ role: "img",
296
+ "aria-label": `Rated ${entry.rating.value} out of ${entry.rating.max}`,
297
+ children: [
298
+ entry.rating.value,
299
+ /* @__PURE__ */ jsx(Star, { className: "size-3.5 fill-current", "aria-hidden": true })
300
+ ]
301
+ }
302
+ ) : null
303
+ ] }),
304
+ entry.meta ? /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: entry.meta }) : null,
305
+ /* @__PURE__ */ jsx("p", { className: cn("line-clamp-3 break-words text-sm", !entry.comment && "text-muted-foreground"), children: entry.comment || "No comment" }),
306
+ buttons ? /* @__PURE__ */ jsx("div", { className: "mt-1 flex flex-wrap gap-2", children: buttons }) : null
307
+ ]
308
+ },
309
+ entry.id
310
+ );
311
+ }) })
312
+ ]
313
+ }
314
+ );
315
+ }
316
+ var ANY = "__any";
317
+ async function copy(text, what) {
318
+ try {
319
+ await navigator.clipboard.writeText(text);
320
+ toast.success(`${what} copied.`);
321
+ } catch {
322
+ toast.error("Couldn't copy. Select it and copy it instead.");
323
+ }
324
+ }
325
+ function CopyField({
326
+ id,
327
+ label,
328
+ value,
329
+ what
330
+ }) {
331
+ return /* @__PURE__ */ jsxs("div", { className: "space-y-1.5", children: [
332
+ /* @__PURE__ */ jsx(Label, { htmlFor: id, children: label }),
333
+ /* @__PURE__ */ jsxs("div", { className: "flex gap-2", children: [
334
+ /* @__PURE__ */ jsx(Input, { id, readOnly: true, value, onFocus: (e) => e.target.select() }),
335
+ /* @__PURE__ */ jsx(
336
+ Button,
337
+ {
338
+ variant: "outline",
339
+ startIcon: /* @__PURE__ */ jsx(Copy, { className: "size-4" }),
340
+ onClick: () => void copy(value, what),
341
+ children: "Copy"
342
+ }
343
+ )
344
+ ] })
345
+ ] });
346
+ }
347
+ function ShareDialog({
348
+ open,
349
+ onOpenChange,
350
+ noun = "form",
351
+ title = `Share your ${noun}`,
352
+ description = "Anyone with the link can answer.",
353
+ url,
354
+ posterUrl,
355
+ renderQr,
356
+ name,
357
+ prefill = [],
358
+ prefillNote,
359
+ defaultTab = "link"
360
+ }) {
361
+ const id = useId();
362
+ const [values, setValues] = useState({});
363
+ const chosen = Object.fromEntries(Object.entries(values).filter(([, v]) => v && v !== ANY));
364
+ const prefilled = url && Object.keys(chosen).length ? prefilledUrl(url, chosen) : null;
365
+ const poster = url ? posterUrl?.(url, {}) : null;
366
+ const prefilledPoster = prefilled && url ? posterUrl?.(url, chosen) : null;
367
+ const usable = prefill.filter((p) => p.options.length > 0);
368
+ return /* @__PURE__ */ jsx(Dialog, { open, onOpenChange, children: /* @__PURE__ */ jsxs(DialogContent, { className: "max-h-[90dvh] overflow-y-auto sm:max-w-lg", children: [
369
+ /* @__PURE__ */ jsxs(DialogHeader, { children: [
370
+ /* @__PURE__ */ jsx(DialogTitle, { children: title }),
371
+ /* @__PURE__ */ jsx(DialogDescription, { children: description })
372
+ ] }),
373
+ !url ? /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", children: "Loading the link\u2026" }) : /* @__PURE__ */ jsxs(Tabs, { defaultValue: defaultTab === "qr" && !renderQr ? "link" : defaultTab, children: [
374
+ /* @__PURE__ */ jsxs(TabsList, { children: [
375
+ /* @__PURE__ */ jsx(TabsTrigger, { value: "link", children: "Link" }),
376
+ renderQr && /* @__PURE__ */ jsx(TabsTrigger, { value: "qr", children: "QR code" }),
377
+ /* @__PURE__ */ jsx(TabsTrigger, { value: "embed", children: "Embed" })
378
+ ] }),
379
+ /* @__PURE__ */ jsxs(TabsContent, { value: "link", className: "space-y-5 pt-2", children: [
380
+ /* @__PURE__ */ jsx(CopyField, { id: `${id}-link`, label: "Link", value: url, what: "Link" }),
381
+ /* @__PURE__ */ jsx(
382
+ Button,
383
+ {
384
+ asChild: true,
385
+ variant: "ghost",
386
+ size: "sm",
387
+ startIcon: /* @__PURE__ */ jsx(ExternalLink, { className: "size-4" }),
388
+ children: /* @__PURE__ */ jsxs("a", { href: url, target: "_blank", rel: "noreferrer", children: [
389
+ "Open the ",
390
+ noun
391
+ ] })
392
+ }
393
+ ),
394
+ usable.length > 0 && /* @__PURE__ */ jsxs("div", { className: "space-y-3 border-t border-border pt-4", children: [
395
+ /* @__PURE__ */ jsxs("div", { children: [
396
+ /* @__PURE__ */ jsx("p", { className: "text-sm font-medium", children: "Prefilled link" }),
397
+ prefillNote && /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", children: prefillNote })
398
+ ] }),
399
+ /* @__PURE__ */ jsx("div", { className: "flex flex-wrap gap-2", children: usable.map((p) => /* @__PURE__ */ jsxs(
400
+ Select,
401
+ {
402
+ value: values[p.key] ?? ANY,
403
+ onValueChange: (v) => setValues((old) => ({ ...old, [p.key]: v })),
404
+ children: [
405
+ /* @__PURE__ */ jsx(SelectTrigger, { className: "min-w-40 flex-1", "aria-label": p.label, children: /* @__PURE__ */ jsx(SelectValue, {}) }),
406
+ /* @__PURE__ */ jsxs(SelectContent, { children: [
407
+ /* @__PURE__ */ jsx(SelectItem, { value: ANY, children: p.anyLabel }),
408
+ p.options.map((o) => /* @__PURE__ */ jsx(SelectItem, { value: o.value, children: o.label }, o.value))
409
+ ] })
410
+ ]
411
+ },
412
+ p.key
413
+ )) }),
414
+ prefilled && /* @__PURE__ */ jsxs(Fragment, { children: [
415
+ /* @__PURE__ */ jsx(
416
+ CopyField,
417
+ {
418
+ id: `${id}-prefilled`,
419
+ label: "Prefilled link",
420
+ value: prefilled,
421
+ what: "Prefilled link"
422
+ }
423
+ ),
424
+ prefilledPoster && /* @__PURE__ */ jsx(
425
+ Button,
426
+ {
427
+ asChild: true,
428
+ variant: "outline",
429
+ startIcon: /* @__PURE__ */ jsx(Printer, { className: "size-4" }),
430
+ children: /* @__PURE__ */ jsx("a", { href: prefilledPoster, target: "_blank", rel: "noreferrer", children: "Print a poster for this link" })
431
+ }
432
+ )
433
+ ] })
434
+ ] })
435
+ ] }),
436
+ renderQr && /* @__PURE__ */ jsxs(TabsContent, { value: "qr", className: "space-y-4 pt-2", children: [
437
+ /* @__PURE__ */ jsx("div", { className: "mx-auto w-full max-w-56", children: renderQr(url) }),
438
+ /* @__PURE__ */ jsxs("p", { className: "text-center text-sm text-muted-foreground", children: [
439
+ "Scan with a phone's camera to open the ",
440
+ noun,
441
+ "."
442
+ ] }),
443
+ poster && /* @__PURE__ */ jsx("div", { className: "flex justify-center", children: /* @__PURE__ */ jsx(Button, { asChild: true, variant: "outline", startIcon: /* @__PURE__ */ jsx(Printer, { className: "size-4" }), children: /* @__PURE__ */ jsx("a", { href: poster, target: "_blank", rel: "noreferrer", children: "Print a QR poster" }) }) })
444
+ ] }),
445
+ /* @__PURE__ */ jsxs(TabsContent, { value: "embed", className: "space-y-3 pt-2", children: [
446
+ /* @__PURE__ */ jsxs("div", { className: "space-y-1.5", children: [
447
+ /* @__PURE__ */ jsx(Label, { htmlFor: `${id}-embed`, children: "Embed on your website" }),
448
+ /* @__PURE__ */ jsx(
449
+ Textarea,
450
+ {
451
+ id: `${id}-embed`,
452
+ readOnly: true,
453
+ rows: 4,
454
+ className: "font-mono text-xs",
455
+ value: embedSnippet(url, name),
456
+ onFocus: (e) => e.target.select()
457
+ }
458
+ )
459
+ ] }),
460
+ /* @__PURE__ */ jsx(
461
+ Button,
462
+ {
463
+ variant: "outline",
464
+ startIcon: /* @__PURE__ */ jsx(Copy, { className: "size-4" }),
465
+ onClick: () => void copy(embedSnippet(url, name), "Embed code"),
466
+ children: "Copy embed code"
467
+ }
468
+ )
469
+ ] })
470
+ ] })
471
+ ] }) });
472
+ }
473
+ function SettingsSection({
474
+ id,
475
+ icon,
476
+ title,
477
+ description,
478
+ children,
479
+ className
480
+ }) {
481
+ const headingId = id ? `${id}-title` : void 0;
482
+ return /* @__PURE__ */ jsxs(
483
+ "section",
484
+ {
485
+ id,
486
+ "aria-labelledby": headingId,
487
+ "data-slot": "settings-section",
488
+ className: cn("scroll-mt-6 rounded-lg border border-border bg-card text-card-foreground", className),
489
+ children: [
490
+ /* @__PURE__ */ jsxs("header", { className: "flex gap-3 border-b border-border px-5 py-4", children: [
491
+ icon ? /* @__PURE__ */ jsx("span", { className: "mt-0.5 text-primary [&_svg]:size-5", "aria-hidden": true, children: icon }) : null,
492
+ /* @__PURE__ */ jsxs("div", { className: "min-w-0", children: [
493
+ /* @__PURE__ */ jsx("h2", { id: headingId, className: "font-semibold", children: title }),
494
+ description ? /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", children: description }) : null
495
+ ] })
496
+ ] }),
497
+ /* @__PURE__ */ jsx(
498
+ "div",
499
+ {
500
+ className: cn(
501
+ "divide-y divide-border",
502
+ // Rows: genesis-ui SettingsRow and ToggleRow, padded to the card.
503
+ "[&>*]:px-5 [&>[data-slot=settings-row]]:py-3.5",
504
+ "[&>[data-slot=settings-row]:not(:has([role=switch]))]:max-sm:flex-col",
505
+ "[&>[data-slot=settings-row]:not(:has([role=switch]))]:max-sm:items-stretch",
506
+ "[&>[data-slot=settings-row]>div:last-child]:max-sm:flex-wrap"
507
+ ),
508
+ children
509
+ }
510
+ )
511
+ ]
512
+ }
513
+ );
514
+ }
515
+ function ResourceSettingsPage({
516
+ sections,
517
+ navLabel = "On this page",
518
+ className
519
+ }) {
520
+ const [active, setActive] = useState(sections[0]?.id);
521
+ const ids = sections.map((s) => s.id).join(" ");
522
+ useEffect(() => {
523
+ if (typeof window === "undefined") return;
524
+ let frame = 0;
525
+ const update = () => {
526
+ frame = 0;
527
+ const line = window.innerHeight / 3;
528
+ let current = sections[0]?.id;
529
+ for (const s of sections) {
530
+ const el = document.getElementById(s.id);
531
+ if (el && el.getBoundingClientRect().top <= line) current = s.id;
532
+ }
533
+ const bottom = window.innerHeight + window.scrollY >= document.documentElement.scrollHeight - 2;
534
+ const last = [...sections].reverse().find((s) => document.getElementById(s.id));
535
+ if (bottom && last && window.scrollY > 0) current = last.id;
536
+ setActive(current);
537
+ };
538
+ const onScroll = () => {
539
+ if (!frame) frame = window.requestAnimationFrame(update);
540
+ };
541
+ update();
542
+ window.addEventListener("scroll", onScroll, { passive: true, capture: true });
543
+ return () => {
544
+ window.removeEventListener("scroll", onScroll, { capture: true });
545
+ if (frame) window.cancelAnimationFrame(frame);
546
+ };
547
+ }, [ids]);
548
+ return /* @__PURE__ */ jsxs(
549
+ "div",
550
+ {
551
+ "data-slot": "resource-settings-page",
552
+ className: cn("grid grid-cols-[minmax(0,1fr)] gap-6 md:grid-cols-[12rem_minmax(0,1fr)] lg:gap-8", className),
553
+ children: [
554
+ /* @__PURE__ */ jsxs("nav", { "aria-label": navLabel, className: "min-w-0 md:sticky md:top-4 md:self-start", children: [
555
+ /* @__PURE__ */ jsx("p", { className: "mb-2 hidden text-xs font-medium uppercase tracking-wide text-muted-foreground md:block", children: navLabel }),
556
+ /* @__PURE__ */ jsx("ul", { className: "-mx-1 flex gap-1 overflow-x-auto px-1 md:mx-0 md:flex-col md:overflow-visible md:px-0", children: sections.map((s) => /* @__PURE__ */ jsx("li", { className: "shrink-0", children: /* @__PURE__ */ jsxs(
557
+ "a",
558
+ {
559
+ href: `#${s.id}`,
560
+ "aria-current": active === s.id ? "location" : void 0,
561
+ onClick: (e) => {
562
+ setActive(s.id);
563
+ if (s.content === void 0 && s.onSelect) {
564
+ e.preventDefault();
565
+ s.onSelect();
566
+ }
567
+ },
568
+ className: cn(
569
+ "flex items-center gap-2 whitespace-nowrap rounded-md px-3 py-1.5 text-sm text-muted-foreground hover:bg-accent hover:text-accent-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring [&_svg]:size-4",
570
+ active === s.id && "bg-muted font-medium text-foreground"
571
+ ),
572
+ children: [
573
+ s.icon ? /* @__PURE__ */ jsx("span", { "aria-hidden": true, children: s.icon }) : null,
574
+ s.title
575
+ ]
576
+ }
577
+ ) }, s.id)) })
578
+ ] }),
579
+ /* @__PURE__ */ jsx("div", { className: "flex min-w-0 flex-col gap-5", children: sections.filter((s) => s.content !== void 0).map((s) => /* @__PURE__ */ jsx(SettingsSection, { id: s.id, icon: s.icon, title: s.title, description: s.description, children: s.content }, s.id)) })
580
+ ]
581
+ }
582
+ );
583
+ }
584
+ var FORM_NAV_ICONS = {
585
+ questions: ListChecks,
586
+ logic: GitBranch,
587
+ responses: Inbox,
588
+ settings: Settings
589
+ };
590
+ var WHEN = [
591
+ { value: "all", label: "All time" },
592
+ { value: "today", label: "Today" },
593
+ { value: "week", label: "This week" },
594
+ { value: "7d", label: "Last 7 days" },
595
+ { value: "30d", label: "Last 30 days" },
596
+ { value: "month", label: "This month" },
597
+ { value: "custom", label: "Pick dates" }
598
+ ];
599
+ var ANY2 = "__any";
600
+ var TRIGGER = "h-9 w-auto min-w-0 gap-2 [&>span]:flex [&>span]:items-center [&>span]:gap-2";
601
+ function ResponsesFilters({
602
+ value,
603
+ onChange,
604
+ choices = [],
605
+ ratingMax = null,
606
+ lowLabel = null,
607
+ statuses = false,
608
+ filtered
609
+ }) {
610
+ const ratings = ratingMax ? Array.from({ length: Math.min(ratingMax, 10) - 1 }, (_, i) => i + 1) : [];
611
+ return /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center gap-2", "data-testid": "responses-filters", children: [
612
+ /* @__PURE__ */ jsxs(Select, { value: value.when, onValueChange: (when) => onChange({ ...value, when }), children: [
613
+ /* @__PURE__ */ jsxs(SelectTrigger, { className: TRIGGER, "aria-label": "When", children: [
614
+ /* @__PURE__ */ jsx(CalendarDays, { className: "size-4 text-muted-foreground", "aria-hidden": true }),
615
+ /* @__PURE__ */ jsx(SelectValue, {})
616
+ ] }),
617
+ /* @__PURE__ */ jsx(SelectContent, { children: WHEN.map((w) => /* @__PURE__ */ jsx(SelectItem, { value: w.value, children: w.label }, w.value)) })
618
+ ] }),
619
+ value.when === "custom" ? /* @__PURE__ */ jsxs("span", { className: "flex items-center gap-1", children: [
620
+ /* @__PURE__ */ jsx(
621
+ Input,
622
+ {
623
+ type: "date",
624
+ className: "h-9 w-auto",
625
+ "aria-label": "From",
626
+ value: value.from,
627
+ max: value.to || void 0,
628
+ onChange: (e) => onChange({ ...value, from: e.target.value })
629
+ }
630
+ ),
631
+ /* @__PURE__ */ jsx("span", { className: "text-sm text-muted-foreground", children: "to" }),
632
+ /* @__PURE__ */ jsx(
633
+ Input,
634
+ {
635
+ type: "date",
636
+ className: "h-9 w-auto",
637
+ "aria-label": "To",
638
+ value: value.to,
639
+ min: value.from || void 0,
640
+ onChange: (e) => onChange({ ...value, to: e.target.value })
641
+ }
642
+ )
643
+ ] }) : null,
644
+ statuses ? /* @__PURE__ */ jsxs(
645
+ Select,
646
+ {
647
+ value: value.status ?? "counted",
648
+ onValueChange: (status) => onChange({ ...value, status }),
649
+ children: [
650
+ /* @__PURE__ */ jsxs(SelectTrigger, { className: TRIGGER, "aria-label": "Status", "data-testid": "responses-status", children: [
651
+ /* @__PURE__ */ jsx(CircleDashed, { className: "size-4 text-muted-foreground", "aria-hidden": true }),
652
+ /* @__PURE__ */ jsx(SelectValue, {})
653
+ ] }),
654
+ /* @__PURE__ */ jsx(SelectContent, { children: Object.keys(STATUS_FILTER_LABEL).map((s) => /* @__PURE__ */ jsx(SelectItem, { value: s, children: STATUS_FILTER_LABEL[s] }, s)) })
655
+ ]
656
+ }
657
+ ) : null,
658
+ choices.filter((c) => c.options.length > 0).map((c) => /* @__PURE__ */ jsxs(
659
+ Select,
660
+ {
661
+ value: value.match[c.key] || ANY2,
662
+ onValueChange: (v) => onChange({ ...value, match: { ...value.match, [c.key]: v === ANY2 ? "" : v } }),
663
+ children: [
664
+ /* @__PURE__ */ jsxs(SelectTrigger, { className: `${TRIGGER} max-w-56`, "aria-label": c.label, children: [
665
+ c.icon ? /* @__PURE__ */ jsx("span", { className: "text-muted-foreground [&_svg]:size-4", "aria-hidden": true, children: c.icon }) : null,
666
+ /* @__PURE__ */ jsx(SelectValue, {})
667
+ ] }),
668
+ /* @__PURE__ */ jsxs(SelectContent, { children: [
669
+ /* @__PURE__ */ jsx(SelectItem, { value: ANY2, children: c.allLabel }),
670
+ c.options.map((o) => /* @__PURE__ */ jsx(SelectItem, { value: o, children: o }, o))
671
+ ] })
672
+ ]
673
+ },
674
+ c.key
675
+ )),
676
+ ratings.length > 0 ? /* @__PURE__ */ jsxs(
677
+ Select,
678
+ {
679
+ value: value.ratingAtMost == null ? ANY2 : String(value.ratingAtMost),
680
+ onValueChange: (v) => onChange({ ...value, ratingAtMost: v === ANY2 ? null : Number(v) }),
681
+ children: [
682
+ /* @__PURE__ */ jsx(SelectTrigger, { className: TRIGGER, "aria-label": "Rating", children: /* @__PURE__ */ jsx(SelectValue, {}) }),
683
+ /* @__PURE__ */ jsxs(SelectContent, { children: [
684
+ /* @__PURE__ */ jsx(SelectItem, { value: ANY2, children: "Any rating" }),
685
+ ratings.map((r) => /* @__PURE__ */ jsx(SelectItem, { value: String(r), children: r === 1 ? "Rated 1" : `Rated ${r} or below` }, r))
686
+ ] })
687
+ ]
688
+ }
689
+ ) : null,
690
+ lowLabel ? value.low ? /* @__PURE__ */ jsxs(
691
+ "span",
692
+ {
693
+ className: "inline-flex h-9 items-center gap-1.5 rounded-md bg-warning/15 pl-2.5 pr-1 text-sm text-amber-800 dark:bg-warning/20 dark:text-amber-200",
694
+ "data-testid": "low-ratings-chip",
695
+ children: [
696
+ /* @__PURE__ */ jsx(TriangleAlert, { className: "size-4", "aria-hidden": true }),
697
+ `Low ratings (${lowLabel})`,
698
+ /* @__PURE__ */ jsx(
699
+ "button",
700
+ {
701
+ type: "button",
702
+ "aria-label": "Show every rating",
703
+ className: "rounded p-1 hover:bg-warning/20 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
704
+ onClick: () => onChange({ ...value, low: false }),
705
+ children: /* @__PURE__ */ jsx(X, { className: "size-3.5", "aria-hidden": true })
706
+ }
707
+ )
708
+ ]
709
+ }
710
+ ) : /* @__PURE__ */ jsx(
711
+ Button,
712
+ {
713
+ variant: "outline",
714
+ size: "sm",
715
+ className: "h-9",
716
+ startIcon: /* @__PURE__ */ jsx(TriangleAlert, { className: "size-4" }),
717
+ onClick: () => onChange(lowThisWeek(value)),
718
+ "data-testid": "low-ratings-preset",
719
+ children: "Low ratings this week"
720
+ }
721
+ ) : null,
722
+ filtered ? /* @__PURE__ */ jsx(Button, { variant: "ghost", size: "sm", className: "h-9", onClick: () => onChange(NO_FILTERS), children: "Clear filters" }) : null
723
+ ] });
724
+ }
725
+ var FIRST_TEXT = 5;
726
+ var MORE_TEXT = 10;
727
+ var plural = (n, one, many = `${one}s`) => `${n} ${n === 1 ? one : many}`;
728
+ var ICONS = {
729
+ rating: Star,
730
+ nps: Gauge,
731
+ consent: SquareCheck,
732
+ "yes-no": CircleCheck,
733
+ "single-choice": CircleDot,
734
+ dropdown: CircleDot,
735
+ "multi-choice": ListChecks
736
+ };
737
+ function Bar({ share, low = false }) {
738
+ return /* @__PURE__ */ jsx("span", { className: "h-2 overflow-hidden rounded-full bg-muted", "aria-hidden": true, children: /* @__PURE__ */ jsx(
739
+ "span",
740
+ {
741
+ className: cn("block h-full rounded-full", low ? "bg-warning" : "bg-primary"),
742
+ style: { width: `${Math.max(0, Math.min(100, share))}%` }
743
+ }
744
+ ) });
745
+ }
746
+ function ScaleBody({ s }) {
747
+ const most = Math.max(1, ...s.buckets.map((b) => b.count));
748
+ const atMost = lowScaleAtMost(s.type, s.max);
749
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
750
+ s.nps ? /* @__PURE__ */ jsxs("p", { className: "text-sm", children: [
751
+ /* @__PURE__ */ jsx("span", { className: "text-2xl font-semibold tabular-nums", children: s.nps.score }),
752
+ " ",
753
+ /* @__PURE__ */ jsx("span", { className: "text-muted-foreground", children: "NPS" }),
754
+ /* @__PURE__ */ jsxs("span", { className: "block text-xs text-muted-foreground", children: [
755
+ plural(s.nps.promoters, "promoter"),
756
+ " (9\u201310), ",
757
+ plural(s.nps.passives, "passive"),
758
+ " (7\u20138),",
759
+ " ",
760
+ plural(s.nps.detractors, "detractor"),
761
+ " (0\u20136)"
762
+ ] })
763
+ ] }) : null,
764
+ s.rescaled || s.excluded ? /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", "data-testid": `scale-note-${s.key}`, children: [
765
+ s.rescaled ? `${plural(s.rescaled, "answer")} from an earlier version's scale moved onto ${s.min}\u2013${s.max}.` : "",
766
+ s.excluded ? `${plural(s.excluded, "answer")} outside the scale left out.` : ""
767
+ ].filter(Boolean).join(" ") }) : null,
768
+ /* @__PURE__ */ jsx("ul", { className: "space-y-2", "aria-label": "Answers by score", children: [...s.buckets].reverse().map((b) => /* @__PURE__ */ jsxs("li", { className: "grid grid-cols-[2.25rem_1fr_2rem] items-center gap-3 text-sm", children: [
769
+ /* @__PURE__ */ jsxs("span", { className: "flex items-center gap-1 tabular-nums", children: [
770
+ b.value,
771
+ s.type === "rating" ? /* @__PURE__ */ jsx(Star, { className: "size-3 fill-current", "aria-label": "stars" }) : null
772
+ ] }),
773
+ /* @__PURE__ */ jsx(Bar, { share: b.count / most * 100, low: b.value <= atMost }),
774
+ /* @__PURE__ */ jsx("span", { className: "text-right tabular-nums text-muted-foreground", children: b.count })
775
+ ] }, b.value)) })
776
+ ] });
777
+ }
778
+ function ChoiceBody({ s }) {
779
+ if (s.fieldType === "consent" || s.fieldType === "yes-no") {
780
+ const yes = s.options.find((o) => o.value === "true");
781
+ const no = s.options.find((o) => o.value === "false");
782
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
783
+ /* @__PURE__ */ jsx("span", { className: "flex h-2.5 overflow-hidden rounded-full bg-muted", "aria-hidden": true, children: /* @__PURE__ */ jsx("span", { className: "h-full bg-primary", style: { width: `${yes?.percent ?? 0}%` } }) }),
784
+ /* @__PURE__ */ jsxs("p", { className: "flex flex-wrap gap-x-4 gap-y-1 text-sm", children: [
785
+ yes ? /* @__PURE__ */ jsxs("span", { className: "text-emerald-700 dark:text-emerald-300", children: [
786
+ yes.label,
787
+ " \xB7 ",
788
+ yes.count,
789
+ " (",
790
+ Math.round(yes.percent),
791
+ "%)"
792
+ ] }) : null,
793
+ no ? /* @__PURE__ */ jsxs("span", { className: "text-muted-foreground", children: [
794
+ no.label,
795
+ " \xB7 ",
796
+ no.count
797
+ ] }) : null
798
+ ] })
799
+ ] });
800
+ }
801
+ const most = Math.max(1, ...s.options.map((o) => o.count));
802
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
803
+ /* @__PURE__ */ jsx("ul", { className: "space-y-2", children: s.options.map((o) => /* @__PURE__ */ jsxs("li", { className: "grid grid-cols-[minmax(2rem,12rem)_1fr_auto] items-center gap-3 text-sm", children: [
804
+ /* @__PURE__ */ jsx("span", { className: "truncate", title: o.label, children: o.label }),
805
+ /* @__PURE__ */ jsx(Bar, { share: o.count / most * 100 }),
806
+ /* @__PURE__ */ jsxs("span", { className: "whitespace-nowrap text-right tabular-nums text-muted-foreground", children: [
807
+ o.count,
808
+ " \xB7 ",
809
+ Math.round(o.percent),
810
+ "%"
811
+ ] })
812
+ ] }, o.value)) }),
813
+ s.multiple ? /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: "People could pick more than one." }) : null
814
+ ] });
815
+ }
816
+ function TextBody({
817
+ s,
818
+ today,
819
+ answerMeta
820
+ }) {
821
+ const [shown, setShown] = useState(FIRST_TEXT);
822
+ const left = s.answers.length - shown;
823
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
824
+ /* @__PURE__ */ jsx("ul", { className: "space-y-3", children: s.answers.slice(0, shown).map((a) => {
825
+ const meta = answerMeta?.(a.id);
826
+ return /* @__PURE__ */ jsxs("li", { className: "border-l-2 border-border pl-3", children: [
827
+ /* @__PURE__ */ jsxs("p", { className: "whitespace-pre-wrap break-words text-sm", children: [
828
+ "\u201C",
829
+ a.text,
830
+ "\u201D"
831
+ ] }),
832
+ /* @__PURE__ */ jsxs("p", { className: "text-xs text-muted-foreground", children: [
833
+ dayLabel(a.date, today),
834
+ meta ? /* @__PURE__ */ jsxs(Fragment, { children: [
835
+ " \xB7 ",
836
+ meta
837
+ ] }) : null
838
+ ] })
839
+ ] }, a.id);
840
+ }) }),
841
+ left > 0 ? /* @__PURE__ */ jsxs(Button, { variant: "ghost", size: "sm", className: "self-start", onClick: () => setShown((n) => n + MORE_TEXT), children: [
842
+ "Show more (",
843
+ left,
844
+ " more)"
845
+ ] }) : null
846
+ ] });
847
+ }
848
+ function QuestionSummaryCard({
849
+ summary: s,
850
+ today,
851
+ answerMeta
852
+ }) {
853
+ const Icon = ICONS[s.fieldType] ?? Type;
854
+ const average = s.kind === "scale" && s.average !== null ? ` \xB7 average ${s.average.toFixed(1)}` : "";
855
+ return /* @__PURE__ */ jsx(
856
+ SummaryCard,
857
+ {
858
+ icon: /* @__PURE__ */ jsx(Icon, {}),
859
+ title: s.label || "Untitled question",
860
+ meta: s.count ? `${plural(s.count, "answer")}${average}` : "No answers yet",
861
+ testID: `summary-${s.key}`,
862
+ children: s.count > 0 ? s.kind === "scale" ? /* @__PURE__ */ jsx(ScaleBody, { s }) : s.kind === "choice" ? /* @__PURE__ */ jsx(ChoiceBody, { s }) : /* @__PURE__ */ jsx(TextBody, { s, today, answerMeta }) : null
863
+ }
864
+ );
865
+ }
866
+ function QuestionSummaryCards({
867
+ summaries,
868
+ responses,
869
+ isLoading = false,
870
+ emptyTitle = "No responses yet",
871
+ emptyDescription,
872
+ onClearFilters,
873
+ today,
874
+ answerMeta,
875
+ className
876
+ }) {
877
+ if (isLoading) {
878
+ return /* @__PURE__ */ jsxs("div", { className: cn("space-y-3", className), children: [
879
+ /* @__PURE__ */ jsx(Skeleton, { className: "h-40 w-full rounded-lg" }),
880
+ /* @__PURE__ */ jsx(Skeleton, { className: "h-32 w-full rounded-lg" })
881
+ ] });
882
+ }
883
+ if (!responses) {
884
+ return /* @__PURE__ */ jsx(
885
+ EmptyState,
886
+ {
887
+ icon: Inbox,
888
+ title: emptyTitle,
889
+ ...emptyDescription ? { description: emptyDescription } : {},
890
+ hasFilters: !!onClearFilters,
891
+ ...onClearFilters ? { onClearFilters } : {}
892
+ }
893
+ );
894
+ }
895
+ return /* @__PURE__ */ jsx("div", { className: cn("flex min-w-0 flex-col gap-3", className), children: summaries.map((s) => /* @__PURE__ */ jsx(QuestionSummaryCard, { summary: s, today, answerMeta }, s.key)) });
896
+ }
897
+ var plural2 = (n, one, many = `${one}s`) => `${n} ${n === 1 ? one : many}`;
898
+ function DropOffByQuestion({
899
+ dropOffs,
900
+ isLoading = false,
901
+ title = "Drop-off by question",
902
+ className
903
+ }) {
904
+ if (isLoading) return /* @__PURE__ */ jsx(Skeleton, { className: cn("h-40 w-full rounded-lg", className) });
905
+ const started = dropOffs[0]?.views ?? 0;
906
+ const worst = Math.max(0, ...dropOffs.map((d) => d.stopped));
907
+ return /* @__PURE__ */ jsx(
908
+ SummaryCard,
909
+ {
910
+ icon: /* @__PURE__ */ jsx(ChartNoAxesColumnDecreasing, {}),
911
+ title,
912
+ meta: started ? `${plural2(started, "person", "people")} started` : "Nobody has started yet",
913
+ className,
914
+ testID: "drop-off",
915
+ children: started ? /* @__PURE__ */ jsx("ol", { className: "space-y-3", children: dropOffs.map((d, i) => /* @__PURE__ */ jsxs("li", { className: "space-y-1.5 text-sm", "data-testid": `drop-off-${d.key}`, children: [
916
+ /* @__PURE__ */ jsxs("p", { className: "flex items-baseline justify-between gap-3", children: [
917
+ /* @__PURE__ */ jsxs("span", { className: "min-w-0 break-words", children: [
918
+ /* @__PURE__ */ jsxs("span", { className: "text-muted-foreground tabular-nums", children: [
919
+ i + 1,
920
+ ". "
921
+ ] }),
922
+ d.label || "Untitled question"
923
+ ] }),
924
+ /* @__PURE__ */ jsx("span", { className: "shrink-0 whitespace-nowrap tabular-nums text-muted-foreground", children: d.dropOff === null ? "\u2013" : `${d.dropOff}% drop-off` })
925
+ ] }),
926
+ /* @__PURE__ */ jsx("span", { className: "block h-2 overflow-hidden rounded-full bg-muted", "aria-hidden": true, children: /* @__PURE__ */ jsx(
927
+ "span",
928
+ {
929
+ className: cn(
930
+ "block h-full rounded-full",
931
+ d.stopped && d.stopped === worst ? "bg-warning" : "bg-primary"
932
+ ),
933
+ style: { width: `${d.dropOff ?? 0}%` }
934
+ }
935
+ ) }),
936
+ /* @__PURE__ */ jsxs("p", { className: "text-xs text-muted-foreground", children: [
937
+ plural2(d.views, "view"),
938
+ " \xB7 ",
939
+ d.stopped,
940
+ " stopped here"
941
+ ] })
942
+ ] }, d.key)) }) : null
943
+ }
944
+ );
945
+ }
946
+ var UNDRAWABLE = /* @__PURE__ */ new Set(["image/heic", "image/heif"]);
947
+ function Photo({ photo, resolveUrl }) {
948
+ const [resolved, setResolved] = useState({ state: "loading" });
949
+ const [broken, setBroken] = useState(false);
950
+ useEffect(() => {
951
+ let live = true;
952
+ setResolved({ state: "loading" });
953
+ setBroken(false);
954
+ resolveUrl(photo).then((url) => live && setResolved(url ? { state: "ready", url } : { state: "missing" })).catch(() => live && setResolved({ state: "missing" }));
955
+ return () => {
956
+ live = false;
957
+ };
958
+ }, [photo.id]);
959
+ const tile = "relative size-28 overflow-hidden rounded-md border border-border bg-muted";
960
+ if (resolved.state === "loading") return /* @__PURE__ */ jsx(Skeleton, { className: "size-28 rounded-md", "aria-label": `Loading ${photo.name}` });
961
+ if (resolved.state === "missing") {
962
+ return /* @__PURE__ */ jsxs("div", { className: cn(tile, "flex flex-col items-center justify-center gap-1 p-2 text-center text-xs text-muted-foreground"), children: [
963
+ /* @__PURE__ */ jsx(ImageOff, { className: "size-5", "aria-hidden": "true" }),
964
+ /* @__PURE__ */ jsx("span", { children: "Photo unavailable" })
965
+ ] });
966
+ }
967
+ if (broken || UNDRAWABLE.has(photo.type.toLowerCase())) {
968
+ return /* @__PURE__ */ jsxs("div", { className: cn(tile, "flex flex-col items-center justify-center gap-1 p-2 text-center text-xs"), children: [
969
+ /* @__PURE__ */ jsx(Image, { className: "size-5 text-muted-foreground", "aria-hidden": "true" }),
970
+ /* @__PURE__ */ jsx("span", { className: "line-clamp-2 break-all text-muted-foreground", children: photo.name }),
971
+ /* @__PURE__ */ jsxs(
972
+ "a",
973
+ {
974
+ href: resolved.url,
975
+ target: "_blank",
976
+ rel: "noreferrer",
977
+ className: "inline-flex items-center gap-1 font-medium text-primary underline-offset-4 hover:underline",
978
+ children: [
979
+ "Open ",
980
+ /* @__PURE__ */ jsx(ExternalLink, { className: "size-3", "aria-hidden": "true" }),
981
+ /* @__PURE__ */ jsx("span", { className: "sr-only", children: photo.name })
982
+ ]
983
+ }
984
+ )
985
+ ] });
986
+ }
987
+ return /* @__PURE__ */ jsx(
988
+ "a",
989
+ {
990
+ href: resolved.url,
991
+ target: "_blank",
992
+ rel: "noreferrer",
993
+ className: cn(tile, "block outline-none ring-offset-background focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"),
994
+ title: photo.name,
995
+ children: /* @__PURE__ */ jsx("img", { src: resolved.url, alt: photo.name, loading: "lazy", className: "size-full object-cover", onError: () => setBroken(true) })
996
+ }
997
+ );
998
+ }
999
+ function PhotoAnswer({
1000
+ photos,
1001
+ resolveUrl,
1002
+ className,
1003
+ testID
1004
+ }) {
1005
+ if (!photos.length) return null;
1006
+ return /* @__PURE__ */ jsx("ul", { className: cn("flex flex-wrap gap-3", className), "aria-label": "Photos", "data-testid": testID, children: photos.map((photo) => /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsx(Photo, { photo, resolveUrl }) }, photo.id)) });
1007
+ }
1008
+
1009
+ export { Chip, DropOffByQuestion, FORM_NAV_ICONS, FollowUpList, HeadlineNumbers, PhotoAnswer, QuestionSummaryCard, QuestionSummaryCards, ReadinessChip, ResourceHeader, ResourceSettingsPage, ResponsesFilters, ResponsesToolbar, SettingsSection, ShareDialog, SummaryCard };
1010
+ //# sourceMappingURL=index.js.map
1011
+ //# sourceMappingURL=index.js.map