@ydtb/tk-scope-capture 0.22.0 → 0.23.1

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.
Files changed (69) hide show
  1. package/dist/src/client/components/CaptureFormSurface.d.ts +58 -0
  2. package/dist/src/client/components/CaptureFormSurface.d.ts.map +1 -0
  3. package/dist/src/client/components/CaptureFormSurface.js +301 -0
  4. package/dist/src/client/components/CaptureFormSurface.js.map +1 -0
  5. package/dist/src/client/components/CaptureSidebar.d.ts +43 -0
  6. package/dist/src/client/components/CaptureSidebar.d.ts.map +1 -0
  7. package/dist/src/client/components/CaptureSidebar.js +38 -0
  8. package/dist/src/client/components/CaptureSidebar.js.map +1 -0
  9. package/dist/src/client/pages/CaptureBuilderPage.d.ts.map +1 -1
  10. package/dist/src/client/pages/CaptureBuilderPage.js +1855 -129
  11. package/dist/src/client/pages/CaptureBuilderPage.js.map +1 -1
  12. package/dist/src/client/pages/CaptureSubmissionDetailPage.d.ts.map +1 -1
  13. package/dist/src/client/pages/CaptureSubmissionDetailPage.js +61 -10
  14. package/dist/src/client/pages/CaptureSubmissionDetailPage.js.map +1 -1
  15. package/dist/src/client/pages/CaptureSubmissionsPage.d.ts.map +1 -1
  16. package/dist/src/client/pages/CaptureSubmissionsPage.js +14 -2
  17. package/dist/src/client/pages/CaptureSubmissionsPage.js.map +1 -1
  18. package/dist/src/client/pages/PublicFormPage.d.ts.map +1 -1
  19. package/dist/src/client/pages/PublicFormPage.js +151 -27
  20. package/dist/src/client/pages/PublicFormPage.js.map +1 -1
  21. package/dist/src/client/pages/PublicQuizPage.d.ts.map +1 -1
  22. package/dist/src/client/pages/PublicQuizPage.js +23 -11
  23. package/dist/src/client/pages/PublicQuizPage.js.map +1 -1
  24. package/dist/src/client/pages/QuizBuilderPage.d.ts.map +1 -1
  25. package/dist/src/client/pages/QuizBuilderPage.js +6 -2
  26. package/dist/src/client/pages/QuizBuilderPage.js.map +1 -1
  27. package/dist/src/client.d.ts +849 -8
  28. package/dist/src/client.d.ts.map +1 -1
  29. package/dist/src/client.js +2 -0
  30. package/dist/src/client.js.map +1 -1
  31. package/dist/src/index.d.ts +2 -0
  32. package/dist/src/index.d.ts.map +1 -1
  33. package/dist/src/index.js +1 -0
  34. package/dist/src/index.js.map +1 -1
  35. package/dist/src/server/api/action-adapters.d.ts +2 -0
  36. package/dist/src/server/api/action-adapters.d.ts.map +1 -1
  37. package/dist/src/server/api/action-adapters.js +1 -0
  38. package/dist/src/server/api/action-adapters.js.map +1 -1
  39. package/dist/src/server/api/router.d.ts +875 -9
  40. package/dist/src/server/api/router.d.ts.map +1 -1
  41. package/dist/src/server/api/router.js +544 -22
  42. package/dist/src/server/api/router.js.map +1 -1
  43. package/dist/src/server.d.ts.map +1 -1
  44. package/dist/src/server.js +51 -1
  45. package/dist/src/server.js.map +1 -1
  46. package/dist/src/shared/conditions.d.ts +2 -2
  47. package/dist/src/shared/conditions.d.ts.map +1 -1
  48. package/dist/src/shared/conditions.js +47 -0
  49. package/dist/src/shared/conditions.js.map +1 -1
  50. package/dist/src/shared/db/schema.d.ts +414 -0
  51. package/dist/src/shared/db/schema.d.ts.map +1 -1
  52. package/dist/src/shared/db/schema.js +40 -0
  53. package/dist/src/shared/db/schema.js.map +1 -1
  54. package/dist/src/shared/document.d.ts +120 -0
  55. package/dist/src/shared/document.d.ts.map +1 -0
  56. package/dist/src/shared/document.js +111 -0
  57. package/dist/src/shared/document.js.map +1 -0
  58. package/dist/src/shared/field-types.d.ts +2 -0
  59. package/dist/src/shared/field-types.d.ts.map +1 -1
  60. package/dist/src/shared/field-types.js +43 -10
  61. package/dist/src/shared/field-types.js.map +1 -1
  62. package/dist/src/shared/types.d.ts +89 -3
  63. package/dist/src/shared/types.d.ts.map +1 -1
  64. package/dist/src/shared/types.js.map +1 -1
  65. package/dist/src/shared/validation.d.ts +6 -0
  66. package/dist/src/shared/validation.d.ts.map +1 -0
  67. package/dist/src/shared/validation.js +87 -0
  68. package/dist/src/shared/validation.js.map +1 -0
  69. package/package.json +9 -6
@@ -1,80 +1,311 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import { useCallback, useEffect, useMemo, useState } from "react";
3
- import { useNavigate, useParams } from "@tanstack/react-router";
3
+ import { useLocation, useNavigate, useParams, useSearch } from "@tanstack/react-router";
4
4
  import { formatRelativeTime } from "@ydtb/tk-scope-lib/date";
5
- import { AlignLeft, CheckCircle2, Eye, FileText, GitBranch, Info, GripVertical, Inbox, LayoutPanelTop, PanelRight, ListTree, Mail, Palette, Phone, Plus, Save, Search, Send, Sparkles, Star, Type, } from "lucide-react";
5
+ import { AlertCircle, ArrowLeft, ArrowRight, CheckCheck, CheckCircle2, ChevronLeft, ChevronRight, Columns3, Copy, Eye, Filter, ExternalLink, FileText, GitBranch, ImageIcon, Info, Inbox, Mail, Monitor, PanelRight, ListTree, Plus, Save, Search, Settings2, Send, Smartphone, Sparkles, Trash2, UserRound, } from "lucide-react";
6
+ import { StorageAssetPicker, useAssetPickerHooks } from "@ydtb/tk-scope-asset-picker";
7
+ import { WorkbenchColumnPanel, WorkbenchFilterPanel, WorkbenchSaveResetControls, WorkbenchTable, adaptWorkbenchColumns, fieldsToFilterFields, isWorkbenchStateDirty, persistWorkbenchColumns, useWorkbenchOffsetPagination, } from "@ydtb/tk-scope-data-workbench";
6
8
  import { useMutation, useQuery, useQueryClient } from "@ydtb/tk-scope-query-client";
7
- import { HeaderPortal, SidebarPortal, useContributions, useScopeLink } from "@ydtb/tk-scope/client";
9
+ import { HeaderPortal, useContributions, useScopeLink } from "@ydtb/tk-scope/client";
8
10
  import { ToolPageHeader } from "@ydtb/tk-scope-shell";
11
+ import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, } from "@ydtb/tk-scope-ui/components/alert-dialog";
12
+ import { BulkActionBar, BulkActionButton } from "@ydtb/tk-scope-ui/components/bulk-action-bar";
9
13
  import { Button } from "@ydtb/tk-scope-ui/components/button";
14
+ import { InlineCellEditor } from "@ydtb/tk-scope-ui/components/data-table";
15
+ import { FilterPanel } from "@ydtb/tk-scope-ui/components/filter-panel";
16
+ import { AvatarPreview, COLORS, ICON_LIST, getColorStyle } from "@ydtb/tk-scope-ui/components/icon-picker";
17
+ import { InlineEditableText } from "@ydtb/tk-scope-ui/components/inline-edit";
10
18
  import { Input } from "@ydtb/tk-scope-ui/components/input";
11
19
  import { Label } from "@ydtb/tk-scope-ui/components/label";
12
20
  import { TabbedPanel } from "@ydtb/tk-scope-ui/components/tabbed-panel";
21
+ import { Skeleton } from "@ydtb/tk-scope-ui/components/skeleton";
13
22
  import { Switch } from "@ydtb/tk-scope-ui/components/switch";
14
23
  import { Textarea } from "@ydtb/tk-scope-ui/components/textarea";
15
24
  import { toast } from "@ydtb/tk-scope-ui/components/sonner";
16
25
  import { cn } from "@ydtb/tk-scope-ui/lib/utils";
17
26
  import { captureApi } from "../../client.js";
27
+ import { createCapturePageHeaderItem, createCapturePageProgressItem, deriveCaptureDocumentFromFields, extractCaptureAnswerFields, extractCaptureAnswerItems, resolveCaptureDocument, } from "../../shared/document.js";
18
28
  import { BASE_CAPTURE_FIELD_TYPES, getCaptureFieldType } from "../../shared/field-types.js";
19
- import { CAPTURE_FORM_TEMPLATES } from "../../shared/templates.js";
29
+ import { CAPTURE_RATING_COLORS, CaptureFormSurface, getFieldLayout, getFieldPlaceholder, getImageClassName, getRatingAlign, getRatingColor, getRatingIcon } from "../components/CaptureFormSurface.js";
30
+ import { CaptureSidebar } from "../components/CaptureSidebar.js";
20
31
  const CAPTURE_BUILDER_DRAG_TYPE = "application/vnd.ydtb.capture.palette-item+json";
21
- const FIELD_ICON_MAP = { AlignLeft, ListTree, Mail, Phone, Star, Type };
32
+ const CAPTURE_BUILDER_CONTENT_DRAG_TYPE = "application/vnd.ydtb.capture.content-kind";
22
33
  const FORM_PANEL_TABS = [
23
34
  { id: "outline", icon: ListTree, label: "Outline" },
24
35
  { id: "inspector", icon: CheckCircle2, label: "Inspector" },
25
- { id: "submissions", icon: Inbox, label: "Submissions" },
26
- { id: "actions", icon: GitBranch, label: "Actions" },
27
- { id: "publish", icon: Send, label: "Publish" },
28
- { id: "theme", icon: Palette, label: "Theme", position: "bottom" },
36
+ { id: "conditions", icon: GitBranch, label: "Conditional Visibility" },
37
+ { id: "validation", icon: CheckCheck, label: "Validation" },
38
+ ];
39
+ const CAPTURE_LEAD_ACTIONS = [
40
+ { id: "contacts", label: "Contact update", description: "Create or update a Contact from answers.", icon: UserRound },
41
+ { id: "email", label: "Email send", description: "Send operator or respondent notifications.", icon: Mail },
29
42
  ];
30
- const CAPTURE_LIST_PANEL_TABS = [
43
+ const CAPTURE_LIST_BASE_PANEL_TABS = [
31
44
  { id: "overview", icon: Info, label: "Overview" },
32
45
  { id: "submissions", icon: Inbox, label: "Submissions" },
33
46
  { id: "publish", icon: Send, label: "Publish" },
34
- { id: "actions", icon: GitBranch, label: "Actions" },
47
+ { id: "actions", icon: GitBranch, label: "Lead actions" },
35
48
  ];
49
+ function buildCaptureListPanelTabs(config) {
50
+ return [
51
+ ...CAPTURE_LIST_BASE_PANEL_TABS,
52
+ ...CAPTURE_LEAD_ACTIONS.filter((action) => Boolean(config[action.id]?.enabled)).map((action) => ({
53
+ id: action.id,
54
+ icon: action.icon,
55
+ label: action.label,
56
+ })),
57
+ ];
58
+ }
59
+ const HONEYPOT_FIELD_KEYS = ["email", "name", "phone", "company", "website"];
60
+ function createDefaultFieldKey(tile, items) {
61
+ const suffix = Date.now().toString(36);
62
+ if (tile.defaultField.type !== "honeypot")
63
+ return `${tile.id}_${suffix}`;
64
+ const existingKeys = new Set(items.filter((item) => item.kind === "input").map((item) => item.key));
65
+ const availableKey = HONEYPOT_FIELD_KEYS.find((key) => !existingKeys.has(key));
66
+ return availableKey ?? `website_${suffix}`;
67
+ }
68
+ const CAPTURE_CONDITION_OPERATORS = {
69
+ text: ["is", "is_not", "contains", "does_not_contain", "starts_with", "ends_with", "regex", "is_empty", "is_not_empty"],
70
+ email: ["is", "is_not", "contains", "does_not_contain", "regex", "is_empty", "is_not_empty"],
71
+ phone: ["is", "is_not", "contains", "does_not_contain", "is_empty", "is_not_empty"],
72
+ textarea: ["contains", "does_not_contain", "regex", "is_empty", "is_not_empty"],
73
+ select: ["is", "is_not", "is_empty", "is_not_empty"],
74
+ rating: ["is", "is_not", "greater_than", "less_than", "greater_than_or_equal", "less_than_or_equal", "is_empty", "is_not_empty"],
75
+ };
76
+ function isConditionGroup(value) {
77
+ return Boolean(value && "conditions" in value);
78
+ }
79
+ function toConditionGroup(value) {
80
+ if (!value)
81
+ return undefined;
82
+ if (isConditionGroup(value))
83
+ return value;
84
+ const operator = value.operator === "notEquals"
85
+ ? "is_not"
86
+ : value.operator === "isEmpty"
87
+ ? "is_empty"
88
+ : value.operator === "isNotEmpty"
89
+ ? "is_not_empty"
90
+ : "is";
91
+ return {
92
+ id: crypto.randomUUID().slice(0, 12),
93
+ logic: "and",
94
+ conditions: [{
95
+ id: crypto.randomUUID().slice(0, 12),
96
+ fieldKey: value.fieldKey,
97
+ operator,
98
+ value: value.value == null ? undefined : String(value.value),
99
+ }],
100
+ };
101
+ }
102
+ function createDefaultValidationRule(field) {
103
+ return {
104
+ id: crypto.randomUUID().slice(0, 12),
105
+ message: `${field.label} is invalid`,
106
+ expression: {
107
+ id: crypto.randomUUID().slice(0, 12),
108
+ logic: "and",
109
+ conditions: [],
110
+ },
111
+ };
112
+ }
36
113
  export function CaptureBuilderPage() {
37
114
  const { id } = useParams({ strict: false });
38
115
  const navigate = useNavigate();
116
+ const location = useLocation();
117
+ const search = useSearch({ strict: false });
39
118
  const sl = useScopeLink();
40
119
  const queryClient = useQueryClient();
120
+ const urlSubmissionViewId = typeof search.view === "string" && search.view.trim().length > 0 ? search.view : undefined;
41
121
  const [newFormName] = useState("Lead capture form");
42
122
  const [landingSearch, setLandingSearch] = useState("");
43
123
  const [selectedLandingDefinition, setSelectedLandingDefinition] = useState(null);
44
124
  const [activeLandingDetailTab, setActiveLandingDetailTab] = useState("overview");
125
+ const [activeWorkbenchViewId, setActiveWorkbenchViewId] = useState("all");
126
+ const [activeWorkbenchPanel, setActiveWorkbenchPanel] = useState(null);
127
+ const [captureItemsFilters, setCaptureItemsFilters] = useState(undefined);
128
+ const [captureItemsColumns, setCaptureItemsColumns] = useState(() => adaptWorkbenchColumns({
129
+ fields: CAPTURE_ITEMS_FIELDS,
130
+ persisted: DEFAULT_CAPTURE_ITEMS_COLUMNS,
131
+ }).configs);
132
+ const [savedCaptureItemsColumns, setSavedCaptureItemsColumns] = useState(captureItemsColumns);
133
+ const [captureItemsSort, setCaptureItemsSort] = useState({ field: "updatedAt", direction: "desc" });
134
+ const [savedCaptureItemsSort, setSavedCaptureItemsSort] = useState(captureItemsSort);
135
+ const [savedCaptureItemsFilters, setSavedCaptureItemsFilters] = useState(undefined);
136
+ const [activeSubmissionViewId, setActiveSubmissionViewId] = useState("all");
137
+ const [activeSubmissionPanel, setActiveSubmissionPanel] = useState(null);
138
+ const [submissionViewName, setSubmissionViewName] = useState("New submission view");
139
+ const [submissionViewNameError, setSubmissionViewNameError] = useState(null);
140
+ const [submissionFilters, setSubmissionFilters] = useState(undefined);
141
+ const [submissionColumns, setSubmissionColumns] = useState(() => adaptWorkbenchColumns({
142
+ fields: CAPTURE_SUBMISSION_SYSTEM_FIELDS,
143
+ persisted: DEFAULT_CAPTURE_SUBMISSION_COLUMNS,
144
+ }).configs);
145
+ const [savedSubmissionColumns, setSavedSubmissionColumns] = useState(submissionColumns);
146
+ const [submissionSort, setSubmissionSort] = useState({ field: "submittedAt", direction: "desc" });
147
+ const [savedSubmissionSort, setSavedSubmissionSort] = useState(submissionSort);
148
+ const [savedSubmissionFilters, setSavedSubmissionFilters] = useState(undefined);
45
149
  const [formName, setFormName] = useState("Untitled form");
46
- const [fields, setFields] = useState([]);
150
+ const [formDocument, setFormDocument] = useState(() => deriveCaptureDocumentFromFields([]));
47
151
  const [actionConfig, setActionConfig] = useState({ contacts: { enabled: false, customFieldMappings: {} } });
48
152
  const [selectedId, setSelectedId] = useState(null);
49
- const [isDragOver, setIsDragOver] = useState(false);
153
+ const [selectedItemIds, setSelectedItemIds] = useState(() => new Set());
154
+ const [currentPageId, setCurrentPageId] = useState("page-1");
50
155
  const [detailsOpen, setDetailsOpen] = useState(true);
51
156
  const [activePanel, setActivePanel] = useState("outline");
157
+ const [previewDevice, setPreviewDevice] = useState("desktop");
158
+ const [savedFieldIds, setSavedFieldIds] = useState(() => new Set());
52
159
  const captureContributions = useContributions("capture");
53
160
  const fieldTypes = useMemo(() => [...BASE_CAPTURE_FIELD_TYPES, ...captureContributions.flatMap((contribution) => contribution.fieldTypes ?? [])], [captureContributions]);
54
161
  const definitionsQuery = useQuery(captureApi.definitions.list.queryOptions({ input: {} }));
162
+ const itemViewsQuery = useQuery(captureApi.definitions.views.list.queryOptions({ input: {} }));
163
+ const submissionViewsQuery = useQuery({
164
+ ...captureApi.submissions.views.list.queryOptions({ input: { definitionId: id ?? "" } }),
165
+ enabled: Boolean(id),
166
+ });
55
167
  const definitionQuery = useQuery({
56
168
  ...captureApi.definitions.get.queryOptions({ input: { id: id ?? "" } }),
57
169
  enabled: Boolean(id),
58
170
  });
59
171
  const submissionsQuery = useQuery(captureApi.submissions.list.queryOptions({ input: {} }));
60
- const workflowsQuery = useQuery({
61
- ...captureApi.definitions.listWorkflows.queryOptions({ input: {} }),
62
- enabled: Boolean(id),
63
- });
172
+ const workflowsQuery = useQuery(captureApi.definitions.listWorkflows.queryOptions({ input: {} }));
173
+ const contactCustomFieldsQuery = useQuery(captureApi.definitions.listContactCustomFields.queryOptions({ input: {} }));
64
174
  useEffect(() => {
65
175
  const definition = definitionQuery.data;
66
176
  if (!definition)
67
177
  return;
178
+ const nextDocument = resolveCaptureDocument(definition.fields, definition.document);
68
179
  setFormName(definition.name);
69
- setFields(definition.fields);
180
+ setFormDocument(nextDocument);
181
+ const firstPageId = nextDocument.pages[0]?.id ?? "page-1";
70
182
  setActionConfig(definition.actionConfig ?? { contacts: { enabled: false, customFieldMappings: {} } });
71
- setSelectedId(definition.fields[0]?.id ?? null);
183
+ setSavedFieldIds(new Set(extractCaptureAnswerItems(nextDocument).map((field) => field.id)));
184
+ setCurrentPageId(firstPageId);
185
+ const firstItemId = nextDocument.items.find((item) => item.pageId === firstPageId)?.id ?? null;
186
+ setSelectedId(firstItemId);
187
+ setSelectedItemIds(firstItemId ? new Set([firstItemId]) : new Set());
72
188
  }, [definitionQuery.data]);
73
- const selectedField = useMemo(() => fields.find((field) => field.id === selectedId) ?? null, [fields, selectedId]);
189
+ useEffect(() => {
190
+ if (!selectedLandingDefinition)
191
+ return;
192
+ const freshDefinition = definitionsQuery.data?.find((definition) => definition.id === selectedLandingDefinition.id);
193
+ if (freshDefinition && freshDefinition !== selectedLandingDefinition)
194
+ setSelectedLandingDefinition(freshDefinition);
195
+ }, [definitionsQuery.data, selectedLandingDefinition]);
196
+ const fields = useMemo(() => extractCaptureAnswerFields(formDocument), [formDocument]);
197
+ const submissionFields = useMemo(() => buildSubmissionWorkbenchFields(fields), [fields]);
198
+ const sortedPages = useMemo(() => [...formDocument.pages].sort((a, b) => a.order - b.order), [formDocument.pages]);
199
+ const currentPage = useMemo(() => sortedPages.find((page) => page.id === currentPageId) ?? sortedPages[0] ?? null, [currentPageId, sortedPages]);
200
+ const currentPageIndex = Math.max(0, sortedPages.findIndex((page) => page.id === currentPage?.id));
201
+ const currentPageItems = useMemo(() => formDocument.items
202
+ .filter((item) => item.pageId === currentPage?.id)
203
+ .sort((a, b) => a.order - b.order), [currentPage?.id, formDocument.items]);
204
+ const selectedPageId = selectedId?.startsWith("page:") ? selectedId.slice("page:".length) : null;
205
+ const selectedPage = selectedPageId ? sortedPages.find((page) => page.id === selectedPageId) ?? null : null;
206
+ const selectedItem = useMemo(() => formDocument.items.find((item) => item.id === selectedId) ?? null, [formDocument.items, selectedId]);
207
+ const selectedItems = useMemo(() => formDocument.items.filter((item) => selectedItemIds.has(item.id)), [formDocument.items, selectedItemIds]);
208
+ const selectedInputItems = useMemo(() => selectedItems.filter((item) => item.kind === "input"), [selectedItems]);
209
+ const hasMultiSelection = selectedItems.length > 1;
210
+ const selectedField = !hasMultiSelection && selectedItem?.kind === "input" ? selectedItem : null;
211
+ const selectedTextBlock = !hasMultiSelection && selectedItem?.kind === "content.text" ? selectedItem : null;
212
+ const selectedIconBlock = !hasMultiSelection && selectedItem?.kind === "content.icon" ? selectedItem : null;
213
+ const selectedImageBlock = !hasMultiSelection && selectedItem?.kind === "content.image" ? selectedItem : null;
214
+ const selectedDivider = !hasMultiSelection && selectedItem?.kind === "content.divider" ? selectedItem : null;
215
+ const selectedPageHeader = !hasMultiSelection && selectedItem?.kind === "content.page-header" ? selectedItem : null;
216
+ const selectedPageProgress = !hasMultiSelection && selectedItem?.kind === "content.page-progress" ? selectedItem : null;
217
+ const selectedVisibilityTarget = selectedPage ?? selectedField ?? selectedTextBlock ?? selectedIconBlock ?? selectedImageBlock ?? selectedDivider ?? selectedPageHeader ?? selectedPageProgress;
218
+ const selectedIsStaticallyHidden = Boolean(selectedVisibilityTarget?.hidden);
219
+ const showValidationPanel = Boolean(selectedField?.required);
220
+ const showConditionsPanel = Boolean(selectedVisibilityTarget && !selectedIsStaticallyHidden);
221
+ const formPanelTabs = useMemo(() => FORM_PANEL_TABS.filter((tab) => {
222
+ if (tab.id === "validation")
223
+ return showValidationPanel;
224
+ if (tab.id === "conditions")
225
+ return showConditionsPanel;
226
+ return true;
227
+ }), [showConditionsPanel, showValidationPanel]);
228
+ useEffect(() => {
229
+ if (((!showValidationPanel && activePanel === "validation") || (!showConditionsPanel && activePanel === "conditions")))
230
+ setActivePanel("inspector");
231
+ }, [activePanel, showConditionsPanel, showValidationPanel]);
232
+ useEffect(() => {
233
+ const availableKeys = new Set(submissionFields.map((field) => field.key));
234
+ setSubmissionColumns((current) => adaptWorkbenchColumns({
235
+ fields: submissionFields,
236
+ persisted: persistWorkbenchColumns(current).filter((column) => availableKeys.has(column.fieldKey)),
237
+ }).configs);
238
+ setSavedSubmissionColumns((current) => adaptWorkbenchColumns({
239
+ fields: submissionFields,
240
+ persisted: persistWorkbenchColumns(current).filter((column) => availableKeys.has(column.fieldKey)),
241
+ }).configs);
242
+ }, [submissionFields]);
243
+ const selectedFieldKeyDuplicate = selectedField
244
+ ? fields.some((field) => field.id !== selectedField.id && field.key.trim() === selectedField.key.trim())
245
+ : false;
246
+ const selectedItemDeleteLabel = selectedField
247
+ ? "Delete field"
248
+ : selectedTextBlock
249
+ ? "Delete text block"
250
+ : selectedIconBlock
251
+ ? "Delete icon"
252
+ : selectedImageBlock
253
+ ? "Delete image"
254
+ : selectedDivider
255
+ ? "Delete divider"
256
+ : selectedPageHeader
257
+ ? "Delete page header"
258
+ : selectedPageProgress
259
+ ? "Delete page progress"
260
+ : null;
261
+ const selectedContextLabel = selectedPage
262
+ ? (selectedPage.title || "Page")
263
+ : selectedField
264
+ ? selectedField.label
265
+ : selectedTextBlock
266
+ ? (selectedTextBlock.title || "Text")
267
+ : selectedIconBlock
268
+ ? "Icon"
269
+ : selectedImageBlock
270
+ ? "Image"
271
+ : selectedDivider
272
+ ? "Divider"
273
+ : selectedPageHeader
274
+ ? "Page header"
275
+ : selectedPageProgress
276
+ ? "Page progress"
277
+ : null;
278
+ const detailsPanelTitle = activePanel === "conditions" && selectedContextLabel
279
+ ? `Conditional Visibility - ${selectedContextLabel}`
280
+ : activePanel === "validation" && selectedContextLabel
281
+ ? `Validation - ${selectedContextLabel}`
282
+ : formPanelTabs.find((tab) => tab.id === activePanel)?.label;
74
283
  const invalidateCapture = useCallback(() => {
75
284
  queryClient.invalidateQueries({ queryKey: captureApi.definitions.key() });
76
285
  queryClient.invalidateQueries({ queryKey: captureApi.submissions.key() });
77
286
  }, [queryClient]);
287
+ const invalidateItemViews = useCallback(() => {
288
+ queryClient.invalidateQueries({ queryKey: captureApi.definitions.views.list.key() });
289
+ }, [queryClient]);
290
+ const createItemViewMutation = useMutation(captureApi.definitions.views.create.mutationOptions({ onSuccess: invalidateItemViews }));
291
+ const updateItemViewMutation = useMutation(captureApi.definitions.views.update.mutationOptions({ onSuccess: invalidateItemViews }));
292
+ const invalidateSubmissionViews = useCallback(() => {
293
+ queryClient.invalidateQueries({ queryKey: captureApi.submissions.views.list.key() });
294
+ }, [queryClient]);
295
+ const createSubmissionViewMutation = useMutation(captureApi.submissions.views.create.mutationOptions({ onSuccess: invalidateSubmissionViews }));
296
+ const updateSubmissionViewMutation = useMutation(captureApi.submissions.views.update.mutationOptions({ onSuccess: invalidateSubmissionViews }));
297
+ const updateTagsMutation = useMutation(captureApi.definitions.updateTags.mutationOptions({
298
+ onSuccess: invalidateCapture,
299
+ onError: () => toast.error("Failed to update tags"),
300
+ }));
301
+ const updateActionConfigMutation = useMutation(captureApi.definitions.updateActionConfig.mutationOptions({
302
+ onSuccess: (definition) => {
303
+ invalidateCapture();
304
+ setSelectedLandingDefinition((current) => current?.id === definition.id ? { ...current, ...definition } : current);
305
+ toast.success("Capture actions saved");
306
+ },
307
+ onError: () => toast.error("Failed to update capture actions"),
308
+ }));
78
309
  const createMutation = useMutation(captureApi.definitions.create.mutationOptions({
79
310
  onSuccess: (definition) => {
80
311
  invalidateCapture();
@@ -97,73 +328,811 @@ export function CaptureBuilderPage() {
97
328
  },
98
329
  onError: () => toast.error("Failed to publish form"),
99
330
  }));
100
- const applyFormTemplate = useCallback((templateId) => {
101
- const template = CAPTURE_FORM_TEMPLATES.find((item) => item.id === templateId);
102
- if (!template)
103
- return;
104
- setFormName(template.title);
105
- setFields(template.fields.map((field) => ({ ...field, metadata: field.metadata ? { ...field.metadata } : undefined, options: field.options ? [...field.options] : undefined })));
106
- setActionConfig(template.actionConfig ?? { contacts: { enabled: false, customFieldMappings: {} } });
107
- setSelectedId(template.fields[0]?.id ?? null);
108
- toast.success(`Applied ${template.label}`);
331
+ const setSingleSelectedItem = useCallback((itemId) => {
332
+ setSelectedId(itemId);
333
+ setSelectedItemIds(itemId ? new Set([itemId]) : new Set());
109
334
  }, []);
110
335
  const insertField = useCallback((tile) => {
111
336
  const fieldId = `field-${Date.now().toString(36)}`;
112
- const newField = {
113
- id: fieldId,
114
- key: `${tile.id}_${Date.now().toString(36)}`,
115
- label: tile.defaultField.label,
116
- type: tile.defaultField.type,
117
- required: tile.defaultField.required ?? false,
118
- ...(tile.defaultField.options ? { options: tile.defaultField.options } : {}),
119
- ...(tile.defaultField.metadata ? { metadata: tile.defaultField.metadata } : {}),
120
- };
121
- setFields((current) => [...current, newField]);
122
- setSelectedId(fieldId);
123
- }, []);
337
+ const targetPageId = currentPage?.id ?? currentPageId;
338
+ setFormDocument((current) => {
339
+ const pageId = current.pages.some((page) => page.id === targetPageId) ? targetPageId : (current.pages[0]?.id ?? "page-1");
340
+ const newField = {
341
+ id: fieldId,
342
+ key: createDefaultFieldKey(tile, current.items),
343
+ label: tile.defaultField.label,
344
+ type: tile.defaultField.type,
345
+ required: tile.defaultField.required ?? false,
346
+ ...(tile.defaultField.options ? { options: tile.defaultField.options } : {}),
347
+ ...(tile.defaultField.metadata ? { metadata: tile.defaultField.metadata } : {}),
348
+ kind: "input",
349
+ pageId,
350
+ order: current.items.filter((item) => item.pageId === pageId).length,
351
+ };
352
+ return { ...current, items: [...current.items, newField] };
353
+ });
354
+ setSingleSelectedItem(fieldId);
355
+ }, [currentPage?.id, currentPageId, setSingleSelectedItem]);
356
+ const insertTextBlock = useCallback(() => {
357
+ const blockId = `text-${Date.now().toString(36)}`;
358
+ const targetPageId = currentPage?.id ?? currentPageId;
359
+ setFormDocument((current) => {
360
+ const pageId = current.pages.some((page) => page.id === targetPageId) ? targetPageId : (current.pages[0]?.id ?? "page-1");
361
+ const textBlock = {
362
+ id: blockId,
363
+ kind: "content.text",
364
+ pageId,
365
+ order: current.items.filter((item) => item.pageId === pageId).length,
366
+ title: "Section intro",
367
+ body: "Add helpful context before respondents answer the next question.",
368
+ };
369
+ return { ...current, items: [...current.items, textBlock] };
370
+ });
371
+ setSingleSelectedItem(blockId);
372
+ setActivePanel("inspector");
373
+ setDetailsOpen(true);
374
+ }, [currentPage?.id, currentPageId, setSingleSelectedItem]);
375
+ const insertImageBlock = useCallback(() => {
376
+ const imageId = `image-${Date.now().toString(36)}`;
377
+ const targetPageId = currentPage?.id ?? currentPageId;
378
+ setFormDocument((current) => {
379
+ const pageId = current.pages.some((page) => page.id === targetPageId) ? targetPageId : (current.pages[0]?.id ?? "page-1");
380
+ const imageBlock = {
381
+ id: imageId,
382
+ kind: "content.image",
383
+ pageId,
384
+ order: current.items.filter((item) => item.pageId === pageId).length,
385
+ imageUrl: "",
386
+ shape: "square",
387
+ align: "center",
388
+ size: "medium",
389
+ fit: "cover",
390
+ source: { type: "upload" },
391
+ };
392
+ return { ...current, items: [...current.items, imageBlock] };
393
+ });
394
+ setSingleSelectedItem(imageId);
395
+ setActivePanel("inspector");
396
+ setDetailsOpen(true);
397
+ }, [currentPage?.id, currentPageId, setSingleSelectedItem]);
398
+ const insertIconBlock = useCallback(() => {
399
+ const iconId = `icon-${Date.now().toString(36)}`;
400
+ const targetPageId = currentPage?.id ?? currentPageId;
401
+ setFormDocument((current) => {
402
+ const pageId = current.pages.some((page) => page.id === targetPageId) ? targetPageId : (current.pages[0]?.id ?? "page-1");
403
+ const iconBlock = {
404
+ id: iconId,
405
+ kind: "content.icon",
406
+ pageId,
407
+ order: current.items.filter((item) => item.pageId === pageId).length,
408
+ icon: "sparkles",
409
+ iconColor: "indigo",
410
+ align: "center",
411
+ };
412
+ return { ...current, items: [...current.items, iconBlock] };
413
+ });
414
+ setSingleSelectedItem(iconId);
415
+ setActivePanel("inspector");
416
+ setDetailsOpen(true);
417
+ }, [currentPage?.id, currentPageId, setSingleSelectedItem]);
418
+ const insertDivider = useCallback(() => {
419
+ const dividerId = `divider-${Date.now().toString(36)}`;
420
+ const targetPageId = currentPage?.id ?? currentPageId;
421
+ setFormDocument((current) => {
422
+ const pageId = current.pages.some((page) => page.id === targetPageId) ? targetPageId : (current.pages[0]?.id ?? "page-1");
423
+ const divider = {
424
+ id: dividerId,
425
+ kind: "content.divider",
426
+ pageId,
427
+ order: current.items.filter((item) => item.pageId === pageId).length,
428
+ };
429
+ return { ...current, items: [...current.items, divider] };
430
+ });
431
+ setSingleSelectedItem(dividerId);
432
+ setActivePanel("inspector");
433
+ setDetailsOpen(true);
434
+ }, [currentPage?.id, currentPageId, setSingleSelectedItem]);
435
+ const insertPageHeader = useCallback(() => {
436
+ const headerId = `header-${Date.now().toString(36)}`;
437
+ const targetPageId = currentPage?.id ?? currentPageId;
438
+ setFormDocument((current) => {
439
+ const pageId = current.pages.some((page) => page.id === targetPageId) ? targetPageId : (current.pages[0]?.id ?? "page-1");
440
+ const page = current.pages.find((candidate) => candidate.id === pageId);
441
+ const header = {
442
+ ...createCapturePageHeaderItem(pageId, current.items.filter((item) => item.pageId === pageId).length, {
443
+ preHeading: page?.preHeading,
444
+ title: page?.title,
445
+ description: page?.description,
446
+ }),
447
+ id: headerId,
448
+ };
449
+ return { ...current, items: [...current.items, header] };
450
+ });
451
+ setSingleSelectedItem(headerId);
452
+ setActivePanel("inspector");
453
+ setDetailsOpen(true);
454
+ }, [currentPage?.id, currentPageId, setSingleSelectedItem]);
455
+ const insertPageProgress = useCallback(() => {
456
+ const progressId = `progress-${Date.now().toString(36)}`;
457
+ const targetPageId = currentPage?.id ?? currentPageId;
458
+ setFormDocument((current) => {
459
+ const pageId = current.pages.some((page) => page.id === targetPageId) ? targetPageId : (current.pages[0]?.id ?? "page-1");
460
+ const progress = {
461
+ ...createCapturePageProgressItem(pageId, current.items.filter((item) => item.pageId === pageId).length),
462
+ id: progressId,
463
+ };
464
+ return { ...current, items: [...current.items, progress] };
465
+ });
466
+ setSingleSelectedItem(progressId);
467
+ setActivePanel("inspector");
468
+ setDetailsOpen(true);
469
+ }, [currentPage?.id, currentPageId, setSingleSelectedItem]);
470
+ const handleCanvasItemSelect = useCallback((item, event) => {
471
+ const additive = Boolean(event?.metaKey || event?.ctrlKey || event?.shiftKey);
472
+ if (!additive) {
473
+ setSingleSelectedItem(item.id);
474
+ setActivePanel("inspector");
475
+ setDetailsOpen(true);
476
+ return;
477
+ }
478
+ setSelectedItemIds((current) => {
479
+ const next = new Set(current);
480
+ if (next.has(item.id) && next.size > 1)
481
+ next.delete(item.id);
482
+ else
483
+ next.add(item.id);
484
+ const nextIds = formDocument.items.filter((candidate) => next.has(candidate.id)).map((candidate) => candidate.id);
485
+ setSelectedId(nextIds[0] ?? null);
486
+ if (nextIds.length > 1) {
487
+ setActivePanel("outline");
488
+ setDetailsOpen(true);
489
+ }
490
+ return new Set(nextIds);
491
+ });
492
+ }, [formDocument.items, setSingleSelectedItem]);
124
493
  const updateSelectedField = useCallback((patch) => {
125
494
  if (!selectedId)
126
495
  return;
127
- setFields((current) => current.map((field) => (field.id === selectedId ? { ...field, ...patch } : field)));
496
+ setFormDocument((current) => ({
497
+ ...current,
498
+ items: current.items.map((item) => (item.kind === "input" && item.id === selectedId ? { ...item, ...patch } : item)),
499
+ }));
500
+ }, [selectedId]);
501
+ const updateSelectedTextBlock = useCallback((patch) => {
502
+ if (!selectedId)
503
+ return;
504
+ setFormDocument((current) => ({
505
+ ...current,
506
+ items: current.items.map((item) => (item.kind === "content.text" && item.id === selectedId ? { ...item, ...patch } : item)),
507
+ }));
508
+ }, [selectedId]);
509
+ const updateSelectedIconBlock = useCallback((patch) => {
510
+ if (!selectedId)
511
+ return;
512
+ setFormDocument((current) => ({
513
+ ...current,
514
+ items: current.items.map((item) => (item.kind === "content.icon" && item.id === selectedId ? { ...item, ...patch } : item)),
515
+ }));
516
+ }, [selectedId]);
517
+ const updateSelectedImageBlock = useCallback((patch) => {
518
+ if (!selectedId)
519
+ return;
520
+ setFormDocument((current) => ({
521
+ ...current,
522
+ items: current.items.map((item) => (item.kind === "content.image" && item.id === selectedId ? { ...item, ...patch } : item)),
523
+ }));
524
+ }, [selectedId]);
525
+ const updateSelectedDivider = useCallback((patch) => {
526
+ if (!selectedId)
527
+ return;
528
+ setFormDocument((current) => ({
529
+ ...current,
530
+ items: current.items.map((item) => (item.kind === "content.divider" && item.id === selectedId ? { ...item, ...patch } : item)),
531
+ }));
532
+ }, [selectedId]);
533
+ const updateSelectedPageHeader = useCallback((patch) => {
534
+ if (!selectedId)
535
+ return;
536
+ setFormDocument((current) => ({
537
+ ...current,
538
+ items: current.items.map((item) => (item.kind === "content.page-header" && item.id === selectedId ? { ...item, ...patch } : item)),
539
+ }));
540
+ }, [selectedId]);
541
+ const updateSelectedPageProgress = useCallback((patch) => {
542
+ if (!selectedId)
543
+ return;
544
+ setFormDocument((current) => ({
545
+ ...current,
546
+ items: current.items.map((item) => (item.kind === "content.page-progress" && item.id === selectedId ? { ...item, ...patch } : item)),
547
+ }));
128
548
  }, [selectedId]);
549
+ const deleteSelectedItem = useCallback(() => {
550
+ if (!selectedItem)
551
+ return;
552
+ const pageId = selectedItem.pageId;
553
+ setFormDocument((current) => {
554
+ const remainingItems = current.items.filter((item) => item.id !== selectedItem.id);
555
+ const reorderedPageItems = remainingItems
556
+ .filter((item) => item.pageId === pageId)
557
+ .sort((a, b) => a.order - b.order)
558
+ .map((item, order) => ({ ...item, order }));
559
+ const reorderedIds = new Set(reorderedPageItems.map((item) => item.id));
560
+ return {
561
+ ...current,
562
+ items: remainingItems.map((item) => reorderedIds.has(item.id)
563
+ ? reorderedPageItems.find((pageItem) => pageItem.id === item.id) ?? item
564
+ : item),
565
+ };
566
+ });
567
+ const nextItem = formDocument.items
568
+ .filter((item) => item.pageId === pageId && item.id !== selectedItem.id)
569
+ .sort((a, b) => a.order - b.order)[0];
570
+ setSingleSelectedItem(nextItem?.id ?? `page:${pageId}`);
571
+ setActivePanel("inspector");
572
+ }, [formDocument.items, selectedItem, setSingleSelectedItem]);
573
+ const updateSelectedInputItems = useCallback((update) => {
574
+ if (selectedInputItems.length === 0)
575
+ return;
576
+ const inputIds = new Set(selectedInputItems.map((item) => item.id));
577
+ setFormDocument((current) => ({
578
+ ...current,
579
+ items: current.items.map((item) => item.kind === "input" && inputIds.has(item.id) ? update(item) : item),
580
+ }));
581
+ }, [selectedInputItems]);
582
+ const updateSelectedInputLayout = useCallback((patch) => {
583
+ updateSelectedInputItems((field) => ({
584
+ ...field,
585
+ metadata: {
586
+ ...(field.metadata ?? {}),
587
+ layout: {
588
+ ...getFieldLayout(field),
589
+ ...patch,
590
+ },
591
+ },
592
+ }));
593
+ }, [updateSelectedInputItems]);
594
+ const updateSelectedInputRequired = useCallback((required) => {
595
+ updateSelectedInputItems((field) => ({ ...field, required }));
596
+ }, [updateSelectedInputItems]);
597
+ const deleteSelectedItems = useCallback(() => {
598
+ if (selectedItems.length === 0)
599
+ return;
600
+ const idsToDelete = new Set(selectedItems.map((item) => item.id));
601
+ const touchedPageIds = new Set(selectedItems.map((item) => item.pageId));
602
+ const fallbackPageId = selectedItems[0]?.pageId ?? currentPage?.id ?? currentPageId;
603
+ setFormDocument((current) => {
604
+ const remainingItems = current.items.filter((item) => !idsToDelete.has(item.id));
605
+ const reorderedById = new Map();
606
+ for (const pageId of touchedPageIds) {
607
+ remainingItems
608
+ .filter((item) => item.pageId === pageId)
609
+ .sort((a, b) => a.order - b.order)
610
+ .forEach((item, order) => reorderedById.set(item.id, { ...item, order }));
611
+ }
612
+ return {
613
+ ...current,
614
+ items: remainingItems.map((item) => reorderedById.get(item.id) ?? item),
615
+ };
616
+ });
617
+ const nextItem = formDocument.items
618
+ .filter((item) => !idsToDelete.has(item.id) && touchedPageIds.has(item.pageId))
619
+ .sort((a, b) => a.order - b.order)[0];
620
+ setSingleSelectedItem(nextItem?.id ?? `page:${fallbackPageId}`);
621
+ setActivePanel("inspector");
622
+ }, [currentPage?.id, currentPageId, formDocument.items, selectedItems, setSingleSelectedItem]);
623
+ useEffect(() => {
624
+ if (selectedItems.length === 0)
625
+ return;
626
+ const handleDeleteKey = (event) => {
627
+ if (event.key !== "Delete" || event.defaultPrevented || event.metaKey || event.ctrlKey || event.altKey)
628
+ return;
629
+ const target = event.target instanceof HTMLElement ? event.target : null;
630
+ const isEditableTarget = target
631
+ ? target.isContentEditable
632
+ || ["INPUT", "TEXTAREA", "SELECT"].includes(target.tagName)
633
+ || target.getAttribute("role") === "textbox"
634
+ : false;
635
+ if (isEditableTarget)
636
+ return;
637
+ event.preventDefault();
638
+ if (selectedItems.length > 1)
639
+ deleteSelectedItems();
640
+ else
641
+ deleteSelectedItem();
642
+ };
643
+ window.addEventListener("keydown", handleDeleteKey);
644
+ return () => window.removeEventListener("keydown", handleDeleteKey);
645
+ }, [deleteSelectedItem, deleteSelectedItems, selectedItems]);
646
+ const addPage = useCallback(() => {
647
+ const pageId = `page-${Date.now().toString(36)}`;
648
+ setFormDocument((current) => {
649
+ const pageNumber = current.pages.length + 1;
650
+ const page = {
651
+ id: pageId,
652
+ title: `Page ${pageNumber}`,
653
+ preHeading: "Capture Form",
654
+ headerContentInitialized: true,
655
+ order: current.pages.length,
656
+ };
657
+ return {
658
+ ...current,
659
+ pages: [...current.pages, page],
660
+ items: [
661
+ ...current.items,
662
+ createCapturePageHeaderItem(pageId, 0, {
663
+ preHeading: page.preHeading,
664
+ title: page.title,
665
+ description: page.description,
666
+ }),
667
+ createCapturePageProgressItem(pageId, 1),
668
+ ],
669
+ };
670
+ });
671
+ setCurrentPageId(pageId);
672
+ setSingleSelectedItem(null);
673
+ setActivePanel("inspector");
674
+ setDetailsOpen(true);
675
+ }, [setSingleSelectedItem]);
676
+ const selectBuilderPage = useCallback((pageId) => {
677
+ setCurrentPageId(pageId);
678
+ const firstItem = formDocument.items
679
+ .filter((item) => item.pageId === pageId)
680
+ .sort((a, b) => a.order - b.order)[0];
681
+ setSingleSelectedItem(firstItem?.id ?? null);
682
+ }, [formDocument.items, setSingleSelectedItem]);
683
+ const selectPageForInspection = useCallback((pageId) => {
684
+ setCurrentPageId(pageId);
685
+ setSingleSelectedItem(`page:${pageId}`);
686
+ setActivePanel("inspector");
687
+ setDetailsOpen(true);
688
+ }, [setSingleSelectedItem]);
689
+ const goToPreviousPage = useCallback(() => {
690
+ const previousPage = sortedPages[currentPageIndex - 1];
691
+ if (previousPage)
692
+ selectBuilderPage(previousPage.id);
693
+ }, [currentPageIndex, selectBuilderPage, sortedPages]);
694
+ const goToNextPage = useCallback(() => {
695
+ const nextPage = sortedPages[currentPageIndex + 1];
696
+ if (nextPage)
697
+ selectBuilderPage(nextPage.id);
698
+ }, [currentPageIndex, selectBuilderPage, sortedPages]);
699
+ const updateCurrentPage = useCallback((patch) => {
700
+ if (!currentPage)
701
+ return;
702
+ setFormDocument((current) => ({
703
+ ...current,
704
+ pages: current.pages.map((page) => (page.id === currentPage.id ? { ...page, ...patch } : page)),
705
+ }));
706
+ }, [currentPage]);
707
+ const deleteCurrentPage = useCallback(() => {
708
+ if (!currentPage || formDocument.pages.length <= 1)
709
+ return;
710
+ const sorted = [...formDocument.pages].sort((a, b) => a.order - b.order);
711
+ const currentIndex = sorted.findIndex((page) => page.id === currentPage.id);
712
+ const fallbackPage = sorted[currentIndex + 1] ?? sorted[currentIndex - 1] ?? sorted.find((page) => page.id !== currentPage.id);
713
+ setFormDocument((current) => {
714
+ const remainingPages = current.pages
715
+ .filter((page) => page.id !== currentPage.id)
716
+ .sort((a, b) => a.order - b.order)
717
+ .map((page, index) => ({ ...page, order: index }));
718
+ return {
719
+ ...current,
720
+ pages: remainingPages,
721
+ items: current.items.filter((item) => item.pageId !== currentPage.id),
722
+ };
723
+ });
724
+ if (fallbackPage) {
725
+ setCurrentPageId(fallbackPage.id);
726
+ setSingleSelectedItem(`page:${fallbackPage.id}`);
727
+ }
728
+ else {
729
+ setSingleSelectedItem(null);
730
+ }
731
+ setActivePanel("inspector");
732
+ setDetailsOpen(true);
733
+ }, [currentPage, formDocument.pages, setSingleSelectedItem]);
734
+ const reorderItems = useCallback((draggedItemId, targetItemId, placement) => {
735
+ if (draggedItemId === targetItemId)
736
+ return;
737
+ setFormDocument((current) => {
738
+ const draggedItem = current.items.find((item) => item.id === draggedItemId);
739
+ const targetItem = current.items.find((item) => item.id === targetItemId);
740
+ if (!draggedItem || !targetItem || draggedItem.pageId !== targetItem.pageId)
741
+ return current;
742
+ const pageItems = current.items
743
+ .filter((item) => item.pageId === targetItem.pageId)
744
+ .sort((a, b) => a.order - b.order)
745
+ .filter((item) => item.id !== draggedItemId);
746
+ const targetIndex = pageItems.findIndex((item) => item.id === targetItemId);
747
+ if (targetIndex === -1)
748
+ return current;
749
+ const insertIndex = placement === "before" ? targetIndex : targetIndex + 1;
750
+ const reorderedPageItems = [
751
+ ...pageItems.slice(0, insertIndex),
752
+ draggedItem,
753
+ ...pageItems.slice(insertIndex),
754
+ ].map((item, order) => ({ ...item, order }));
755
+ const reorderedIds = new Set(reorderedPageItems.map((item) => item.id));
756
+ return {
757
+ ...current,
758
+ items: current.items.map((item) => reorderedIds.has(item.id)
759
+ ? reorderedPageItems.find((pageItem) => pageItem.id === item.id) ?? item
760
+ : item),
761
+ };
762
+ });
763
+ }, []);
129
764
  const handleDragStart = useCallback((event, tile) => {
130
765
  event.dataTransfer.effectAllowed = "copy";
131
766
  event.dataTransfer.setData(CAPTURE_BUILDER_DRAG_TYPE, JSON.stringify(tile));
132
767
  event.dataTransfer.setData("text/plain", tile.label);
133
768
  }, []);
769
+ const handleContentDragStart = useCallback((event, kind) => {
770
+ event.dataTransfer.effectAllowed = "copy";
771
+ event.dataTransfer.setData(CAPTURE_BUILDER_CONTENT_DRAG_TYPE, kind);
772
+ event.dataTransfer.setData("text/plain", kind === "text" ? "Text block" : kind === "icon" ? "Icon" : kind === "image" ? "Image" : kind === "divider" ? "Divider" : kind === "page-header" ? "Page header" : "Page progress");
773
+ }, []);
774
+ const insertPaletteItemAt = useCallback((event, target) => {
775
+ const contentKind = event.dataTransfer.getData(CAPTURE_BUILDER_CONTENT_DRAG_TYPE);
776
+ const raw = event.dataTransfer.getData(CAPTURE_BUILDER_DRAG_TYPE);
777
+ if (!contentKind && !raw)
778
+ return;
779
+ const newId = `${contentKind || "field"}-${Date.now().toString(36)}`;
780
+ setFormDocument((current) => {
781
+ const targetItem = current.items.find((item) => item.id === target.itemId);
782
+ const pageId = targetItem?.pageId ?? currentPage?.id ?? currentPageId;
783
+ if (!current.pages.some((page) => page.id === pageId))
784
+ return current;
785
+ const order = 0;
786
+ const page = current.pages.find((candidate) => candidate.id === pageId);
787
+ let newItem = null;
788
+ if (contentKind === "text") {
789
+ newItem = { id: newId, kind: "content.text", pageId, order, title: "Section intro", body: "Add helpful context before respondents answer the next question." };
790
+ }
791
+ else if (contentKind === "icon") {
792
+ newItem = { id: newId, kind: "content.icon", pageId, order, icon: "sparkles", iconColor: "indigo", align: "center" };
793
+ }
794
+ else if (contentKind === "image") {
795
+ newItem = { id: newId, kind: "content.image", pageId, order, imageUrl: "", shape: "square", align: "center", size: "medium", fit: "cover", source: { type: "upload" } };
796
+ }
797
+ else if (contentKind === "divider") {
798
+ newItem = { id: newId, kind: "content.divider", pageId, order };
799
+ }
800
+ else if (contentKind === "page-header") {
801
+ newItem = { ...createCapturePageHeaderItem(pageId, order, { preHeading: page?.preHeading, title: page?.title, description: page?.description }), id: newId };
802
+ }
803
+ else if (contentKind === "page-progress") {
804
+ newItem = { ...createCapturePageProgressItem(pageId, order), id: newId };
805
+ }
806
+ else if (raw) {
807
+ const tile = JSON.parse(raw);
808
+ newItem = {
809
+ id: newId,
810
+ key: createDefaultFieldKey(tile, current.items),
811
+ label: tile.defaultField.label,
812
+ type: tile.defaultField.type,
813
+ required: tile.defaultField.required ?? false,
814
+ ...(tile.defaultField.options ? { options: tile.defaultField.options } : {}),
815
+ ...(tile.defaultField.metadata ? { metadata: tile.defaultField.metadata } : {}),
816
+ kind: "input",
817
+ pageId,
818
+ order,
819
+ };
820
+ }
821
+ if (!newItem)
822
+ return current;
823
+ const pageItems = current.items.filter((item) => item.pageId === pageId).sort((a, b) => a.order - b.order);
824
+ const targetIndex = Math.max(0, pageItems.findIndex((item) => item.id === target.itemId));
825
+ const insertIndex = target.placement === "before" ? targetIndex : targetIndex + 1;
826
+ const reorderedPageItems = [
827
+ ...pageItems.slice(0, insertIndex),
828
+ newItem,
829
+ ...pageItems.slice(insertIndex),
830
+ ].map((item, nextOrder) => ({ ...item, order: nextOrder }));
831
+ const reorderedIds = new Set(reorderedPageItems.map((item) => item.id));
832
+ return {
833
+ ...current,
834
+ items: [
835
+ ...current.items.filter((item) => item.pageId !== pageId),
836
+ ...reorderedPageItems,
837
+ ].map((item) => reorderedIds.has(item.id) ? reorderedPageItems.find((pageItem) => pageItem.id === item.id) ?? item : item),
838
+ };
839
+ });
840
+ setSingleSelectedItem(newId);
841
+ setActivePanel("inspector");
842
+ setDetailsOpen(true);
843
+ }, [currentPage?.id, currentPageId, setSingleSelectedItem]);
134
844
  const handleDragOver = useCallback((event) => {
135
- if (event.dataTransfer.types.includes(CAPTURE_BUILDER_DRAG_TYPE)) {
845
+ if (event.dataTransfer.types.includes(CAPTURE_BUILDER_DRAG_TYPE) || event.dataTransfer.types.includes(CAPTURE_BUILDER_CONTENT_DRAG_TYPE)) {
136
846
  event.preventDefault();
137
847
  event.dataTransfer.dropEffect = "copy";
138
- setIsDragOver(true);
139
848
  }
140
849
  }, []);
141
850
  const handleDrop = useCallback((event) => {
851
+ const contentKind = event.dataTransfer.getData(CAPTURE_BUILDER_CONTENT_DRAG_TYPE);
852
+ if (contentKind === "text" || contentKind === "icon" || contentKind === "image" || contentKind === "divider" || contentKind === "page-header" || contentKind === "page-progress") {
853
+ event.preventDefault();
854
+ if (contentKind === "text")
855
+ insertTextBlock();
856
+ else if (contentKind === "icon")
857
+ insertIconBlock();
858
+ else if (contentKind === "image")
859
+ insertImageBlock();
860
+ else if (contentKind === "divider")
861
+ insertDivider();
862
+ else if (contentKind === "page-header")
863
+ insertPageHeader();
864
+ else
865
+ insertPageProgress();
866
+ return;
867
+ }
142
868
  const raw = event.dataTransfer.getData(CAPTURE_BUILDER_DRAG_TYPE);
143
869
  if (!raw)
144
870
  return;
145
871
  event.preventDefault();
146
- setIsDragOver(false);
147
872
  const parsed = JSON.parse(raw);
148
873
  insertField(parsed);
149
- }, [insertField]);
874
+ }, [insertDivider, insertField, insertIconBlock, insertImageBlock, insertPageHeader, insertPageProgress, insertTextBlock]);
875
+ const validateUniqueFieldKeys = useCallback(() => {
876
+ const seen = new Set();
877
+ for (const field of fields) {
878
+ const key = field.key.trim();
879
+ if (!key) {
880
+ toast.error("Field keys cannot be blank");
881
+ return false;
882
+ }
883
+ if (seen.has(key)) {
884
+ toast.error(`Field key “${key}” is already used in this form`);
885
+ return false;
886
+ }
887
+ seen.add(key);
888
+ }
889
+ return true;
890
+ }, [fields]);
891
+ const markFieldsSaved = useCallback(() => {
892
+ setSavedFieldIds(new Set(extractCaptureAnswerItems(formDocument).map((field) => field.id)));
893
+ }, [formDocument]);
150
894
  const saveForm = useCallback(async () => {
151
- if (!id)
895
+ if (!id || !validateUniqueFieldKeys())
152
896
  return;
153
- await updateMutation.mutateAsync({ id, name: formName, fields, actionConfig });
154
- }, [actionConfig, fields, formName, id, updateMutation]);
897
+ await updateMutation.mutateAsync({ id, name: formName, fields, document: formDocument, actionConfig });
898
+ markFieldsSaved();
899
+ }, [actionConfig, fields, formDocument, formName, id, markFieldsSaved, updateMutation, validateUniqueFieldKeys]);
155
900
  const publishForm = useCallback(async () => {
156
- if (!id)
901
+ if (!id || !validateUniqueFieldKeys())
157
902
  return;
158
- await updateMutation.mutateAsync({ id, name: formName, fields, actionConfig });
903
+ await updateMutation.mutateAsync({ id, name: formName, fields, document: formDocument, actionConfig });
904
+ markFieldsSaved();
159
905
  await publishMutation.mutateAsync({ id });
160
- }, [actionConfig, fields, formName, id, publishMutation, updateMutation]);
906
+ }, [actionConfig, fields, formDocument, formName, id, markFieldsSaved, publishMutation, updateMutation, validateUniqueFieldKeys]);
161
907
  const definitionSubmissions = useMemo(() => {
162
908
  const rows = (submissionsQuery.data ?? []);
163
909
  return id ? rows.filter((submission) => submission.definitionId === id) : rows;
164
910
  }, [id, submissionsQuery.data]);
165
911
  const publicationSlug = definitionQuery.data?.publicationSlug ?? undefined;
912
+ const formStatus = definitionQuery.data?.status ?? definitionsQuery.data?.find((definition) => definition.id === id)?.status ?? "draft";
166
913
  const publicPath = publicationSlug ? `/forms/${publicationSlug}` : null;
914
+ const publicUrl = publicPath ? buildPublicUrl(publicPath) : null;
915
+ const builderSubView = location.pathname.endsWith("/submissions")
916
+ ? "submissions"
917
+ : location.pathname.endsWith("/share")
918
+ ? "share"
919
+ : "build";
920
+ const isCreating = createMutation.isPending;
921
+ const itemViews = useMemo(() => (itemViewsQuery.data ?? []), [itemViewsQuery.data]);
922
+ const isCaptureItemsViewDirty = isWorkbenchStateDirty({
923
+ current: { columns: persistWorkbenchColumns(captureItemsColumns), filters: captureItemsFilters, sort: captureItemsSort },
924
+ saved: { columns: persistWorkbenchColumns(savedCaptureItemsColumns), filters: savedCaptureItemsFilters, sort: savedCaptureItemsSort },
925
+ });
926
+ const saveCaptureItemsView = useCallback(() => {
927
+ const state = {
928
+ filters: captureItemsFilters ?? null,
929
+ sorts: [captureItemsSort],
930
+ columns: persistWorkbenchColumns(captureItemsColumns),
931
+ };
932
+ const existing = itemViews.find((view) => view.id === activeWorkbenchViewId);
933
+ if (existing) {
934
+ updateItemViewMutation.mutate({ id: existing.id, ...state }, {
935
+ onSuccess: () => {
936
+ setSavedCaptureItemsColumns(captureItemsColumns);
937
+ setSavedCaptureItemsFilters(captureItemsFilters);
938
+ setSavedCaptureItemsSort(captureItemsSort);
939
+ toast.success("View saved");
940
+ },
941
+ onError: () => toast.error("Failed to save view"),
942
+ });
943
+ return;
944
+ }
945
+ const name = window.prompt("Name this view", "New Capture view")?.trim();
946
+ if (!name)
947
+ return;
948
+ createItemViewMutation.mutate({ name, ...state }, {
949
+ onSuccess: (view) => {
950
+ if (!view)
951
+ return;
952
+ setActiveWorkbenchViewId(view.id);
953
+ setSavedCaptureItemsColumns(captureItemsColumns);
954
+ setSavedCaptureItemsFilters(captureItemsFilters);
955
+ setSavedCaptureItemsSort(captureItemsSort);
956
+ toast.success("View created");
957
+ },
958
+ onError: () => toast.error("Failed to create view"),
959
+ });
960
+ }, [activeWorkbenchViewId, captureItemsColumns, captureItemsFilters, captureItemsSort, createItemViewMutation, itemViews, updateItemViewMutation]);
961
+ const resetCaptureItemsView = useCallback(() => {
962
+ setCaptureItemsColumns(savedCaptureItemsColumns);
963
+ setCaptureItemsFilters(savedCaptureItemsFilters);
964
+ setCaptureItemsSort(savedCaptureItemsSort);
965
+ }, [savedCaptureItemsColumns, savedCaptureItemsFilters, savedCaptureItemsSort]);
966
+ const workbenchViews = useMemo(() => [
967
+ ...BUILT_IN_CAPTURE_ITEMS_WORKBENCH_VIEWS,
968
+ ...itemViews.map((view) => ({ id: view.id, name: view.name, icon: _jsx(ListTree, { className: "h-4 w-4" }) })),
969
+ ], [itemViews]);
970
+ const applyCaptureItemsView = useCallback((viewId) => {
971
+ setActiveWorkbenchViewId(viewId);
972
+ const view = itemViews.find((candidate) => candidate.id === viewId);
973
+ if (!view) {
974
+ setCaptureItemsFilters(undefined);
975
+ setSavedCaptureItemsFilters(undefined);
976
+ const defaultSort = { field: "updatedAt", direction: "desc" };
977
+ setCaptureItemsSort(defaultSort);
978
+ setSavedCaptureItemsSort(defaultSort);
979
+ const defaultColumns = adaptWorkbenchColumns({ fields: CAPTURE_ITEMS_FIELDS, persisted: DEFAULT_CAPTURE_ITEMS_COLUMNS }).configs;
980
+ setCaptureItemsColumns(defaultColumns);
981
+ setSavedCaptureItemsColumns(defaultColumns);
982
+ return;
983
+ }
984
+ setCaptureItemsFilters(view.filters ?? undefined);
985
+ setSavedCaptureItemsFilters(view.filters ?? undefined);
986
+ const nextSort = view.sorts?.[0] ?? { field: "updatedAt", direction: "desc" };
987
+ setCaptureItemsSort(nextSort);
988
+ setSavedCaptureItemsSort(nextSort);
989
+ const nextColumns = adaptWorkbenchColumns({
990
+ fields: CAPTURE_ITEMS_FIELDS,
991
+ persisted: view.columns ?? DEFAULT_CAPTURE_ITEMS_COLUMNS,
992
+ }).configs;
993
+ setCaptureItemsColumns(nextColumns);
994
+ setSavedCaptureItemsColumns(nextColumns);
995
+ }, [itemViews]);
996
+ const submissionViews = useMemo(() => (submissionViewsQuery.data ?? []), [submissionViewsQuery.data]);
997
+ const activeSubmissionSavedView = useMemo(() => submissionViews.find((view) => view.id === activeSubmissionViewId) ?? null, [activeSubmissionViewId, submissionViews]);
998
+ useEffect(() => {
999
+ const builtInView = BUILT_IN_CAPTURE_SUBMISSION_WORKBENCH_VIEWS.find((view) => view.id === activeSubmissionViewId);
1000
+ setSubmissionViewName(activeSubmissionSavedView?.name ?? (builtInView ? `${builtInView.name} copy` : "New submission view"));
1001
+ setSubmissionViewNameError(null);
1002
+ }, [activeSubmissionSavedView, activeSubmissionViewId]);
1003
+ const isSubmissionViewDirty = isWorkbenchStateDirty({
1004
+ current: { columns: persistWorkbenchColumns(submissionColumns), filters: submissionFilters, sort: submissionSort },
1005
+ saved: { columns: persistWorkbenchColumns(savedSubmissionColumns), filters: savedSubmissionFilters, sort: savedSubmissionSort },
1006
+ });
1007
+ const writeSubmissionViewToUrl = useCallback((viewId, options) => {
1008
+ void navigate({
1009
+ to: ".",
1010
+ search: (previous) => ({ ...previous, view: viewId }),
1011
+ replace: options?.replace,
1012
+ });
1013
+ }, [navigate]);
1014
+ const getSubmissionViewState = useCallback(() => ({
1015
+ filters: submissionFilters ?? null,
1016
+ sorts: [submissionSort],
1017
+ columns: persistWorkbenchColumns(submissionColumns),
1018
+ }), [submissionColumns, submissionFilters, submissionSort]);
1019
+ const markSubmissionViewSaved = useCallback(() => {
1020
+ setSavedSubmissionColumns(submissionColumns);
1021
+ setSavedSubmissionFilters(submissionFilters);
1022
+ setSavedSubmissionSort(submissionSort);
1023
+ }, [submissionColumns, submissionFilters, submissionSort]);
1024
+ const saveSubmissionView = useCallback(() => {
1025
+ if (!id)
1026
+ return;
1027
+ if (!activeSubmissionSavedView) {
1028
+ setActiveSubmissionPanel("details");
1029
+ setSubmissionViewNameError(null);
1030
+ return;
1031
+ }
1032
+ updateSubmissionViewMutation.mutate({ id: activeSubmissionSavedView.id, ...getSubmissionViewState() }, {
1033
+ onSuccess: () => {
1034
+ markSubmissionViewSaved();
1035
+ toast.success("Submission view saved");
1036
+ },
1037
+ onError: () => toast.error("Failed to save submission view"),
1038
+ });
1039
+ }, [activeSubmissionSavedView, getSubmissionViewState, id, markSubmissionViewSaved, updateSubmissionViewMutation]);
1040
+ const saveSubmissionViewDetails = useCallback(() => {
1041
+ if (!id)
1042
+ return;
1043
+ const name = submissionViewName.trim();
1044
+ if (!name) {
1045
+ setSubmissionViewNameError("Name is required");
1046
+ return;
1047
+ }
1048
+ setSubmissionViewNameError(null);
1049
+ const state = getSubmissionViewState();
1050
+ if (activeSubmissionSavedView) {
1051
+ updateSubmissionViewMutation.mutate({ id: activeSubmissionSavedView.id, name, ...state }, {
1052
+ onSuccess: () => {
1053
+ markSubmissionViewSaved();
1054
+ toast.success("Submission view saved");
1055
+ },
1056
+ onError: () => toast.error("Failed to save submission view"),
1057
+ });
1058
+ return;
1059
+ }
1060
+ createSubmissionViewMutation.mutate({ definitionId: id, name, ...state }, {
1061
+ onSuccess: (view) => {
1062
+ if (!view)
1063
+ return;
1064
+ setActiveSubmissionViewId(view.id);
1065
+ setSubmissionViewName(view.name);
1066
+ writeSubmissionViewToUrl(view.id);
1067
+ markSubmissionViewSaved();
1068
+ toast.success("Submission view created");
1069
+ },
1070
+ onError: () => toast.error("Failed to create submission view"),
1071
+ });
1072
+ }, [activeSubmissionSavedView, createSubmissionViewMutation, getSubmissionViewState, id, markSubmissionViewSaved, submissionViewName, updateSubmissionViewMutation, writeSubmissionViewToUrl]);
1073
+ const resetSubmissionView = useCallback(() => {
1074
+ setSubmissionColumns(savedSubmissionColumns);
1075
+ setSubmissionFilters(savedSubmissionFilters);
1076
+ setSubmissionSort(savedSubmissionSort);
1077
+ }, [savedSubmissionColumns, savedSubmissionFilters, savedSubmissionSort]);
1078
+ const submissionWorkbenchViews = useMemo(() => [
1079
+ ...BUILT_IN_CAPTURE_SUBMISSION_WORKBENCH_VIEWS,
1080
+ ...submissionViews.map((view) => ({ id: view.id, name: view.name, icon: _jsx(Inbox, { className: "h-4 w-4" }) })),
1081
+ ], [submissionViews]);
1082
+ const applySubmissionViewState = useCallback((viewId) => {
1083
+ setActiveSubmissionViewId(viewId);
1084
+ const view = submissionViews.find((candidate) => candidate.id === viewId);
1085
+ if (!view) {
1086
+ const builtInFilters = viewId === "with-actions" ? createSubmissionActionStatusFilter() : undefined;
1087
+ setSubmissionFilters(builtInFilters);
1088
+ setSavedSubmissionFilters(builtInFilters);
1089
+ const defaultSort = { field: "submittedAt", direction: "desc" };
1090
+ setSubmissionSort(defaultSort);
1091
+ setSavedSubmissionSort(defaultSort);
1092
+ const defaultColumns = adaptWorkbenchColumns({ fields: submissionFields, persisted: DEFAULT_CAPTURE_SUBMISSION_COLUMNS }).configs;
1093
+ setSubmissionColumns(defaultColumns);
1094
+ setSavedSubmissionColumns(defaultColumns);
1095
+ return;
1096
+ }
1097
+ setSubmissionFilters(view.filters ?? undefined);
1098
+ setSavedSubmissionFilters(view.filters ?? undefined);
1099
+ const nextSort = view.sorts?.[0] ?? { field: "submittedAt", direction: "desc" };
1100
+ setSubmissionSort(nextSort);
1101
+ setSavedSubmissionSort(nextSort);
1102
+ const nextColumns = adaptWorkbenchColumns({ fields: submissionFields, persisted: view.columns ?? DEFAULT_CAPTURE_SUBMISSION_COLUMNS }).configs;
1103
+ setSubmissionColumns(nextColumns);
1104
+ setSavedSubmissionColumns(nextColumns);
1105
+ }, [submissionFields, submissionViews]);
1106
+ const applySubmissionView = useCallback((viewId) => {
1107
+ applySubmissionViewState(viewId);
1108
+ writeSubmissionViewToUrl(viewId);
1109
+ }, [applySubmissionViewState, writeSubmissionViewToUrl]);
1110
+ useEffect(() => {
1111
+ if (builderSubView !== "submissions")
1112
+ return;
1113
+ if (!urlSubmissionViewId) {
1114
+ if (activeSubmissionViewId !== "all")
1115
+ applySubmissionViewState("all");
1116
+ writeSubmissionViewToUrl("all", { replace: true });
1117
+ return;
1118
+ }
1119
+ const nextViewId = urlSubmissionViewId;
1120
+ if (nextViewId === activeSubmissionViewId)
1121
+ return;
1122
+ const knownBuiltIn = BUILT_IN_CAPTURE_SUBMISSION_WORKBENCH_VIEWS.some((view) => view.id === nextViewId);
1123
+ const knownSaved = submissionViews.some((view) => view.id === nextViewId);
1124
+ if (!knownBuiltIn && !knownSaved) {
1125
+ if (!urlSubmissionViewId || submissionViewsQuery.isLoading)
1126
+ return;
1127
+ writeSubmissionViewToUrl("all", { replace: true });
1128
+ return;
1129
+ }
1130
+ if (nextViewId !== activeSubmissionViewId)
1131
+ applySubmissionViewState(nextViewId);
1132
+ }, [activeSubmissionViewId, applySubmissionViewState, builderSubView, submissionViews, submissionViewsQuery.isLoading, urlSubmissionViewId, writeSubmissionViewToUrl]);
1133
+ const isSaving = updateMutation.isPending;
1134
+ const isPublishing = publishMutation.isPending;
1135
+ const isBuilderActionPending = isSaving || isPublishing;
167
1136
  const openDefinition = useCallback((definitionId) => {
168
1137
  const definition = definitionsQuery.data?.find((item) => item.id === definitionId);
169
1138
  if (definition?.surface === "quiz") {
@@ -172,77 +1141,647 @@ export function CaptureBuilderPage() {
172
1141
  }
173
1142
  void navigate({ to: sl("/capture/forms/$id/builder"), params: { id: definitionId } });
174
1143
  }, [definitionsQuery.data, navigate, sl]);
175
- return (_jsxs(_Fragment, { children: [_jsx(HeaderPortal, { children: _jsx(ToolPageHeader, { title: id ? "Forms builder" : "Capture Items", description: id ? "Create a Capture-backed Form, publish it, and review submitted responses." : "Manage forms, quizzes, and other Capture-backed intake surfaces.", actions: id ? (_jsxs(_Fragment, { children: [publicPath && (_jsx(Button, { variant: "outline", size: "sm", type: "button", asChild: true, children: _jsxs("a", { href: publicPath, target: "_blank", rel: "noreferrer", children: [_jsx(Eye, { className: "h-4 w-4" }), "Public form"] }) })), _jsx(Button, { variant: "outline", size: "icon", type: "button", className: cn("h-9 w-9", detailsOpen && "bg-muted"), onClick: () => setDetailsOpen((open) => !open), "aria-label": detailsOpen ? "Hide details panel" : "Show details panel", "aria-pressed": detailsOpen, title: detailsOpen ? "Hide details panel" : "Show details panel", children: _jsx(PanelRight, { className: "h-4 w-4" }) }), _jsxs(Button, { variant: "outline", size: "sm", type: "button", onClick: () => void saveForm(), children: [_jsx(Save, { className: "h-4 w-4" }), "Save"] }), _jsxs(Button, { size: "sm", type: "button", onClick: () => void publishForm(), children: [_jsx(Send, { className: "h-4 w-4" }), "Publish"] })] })) : (_jsxs(_Fragment, { children: [_jsxs("div", { className: "relative w-64", children: [_jsx(Search, { className: "absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" }), _jsx(Input, { type: "search", placeholder: "Search capture items...", value: landingSearch, onChange: (event) => setLandingSearch(event.target.value), className: "h-9 pl-9" })] }), selectedLandingDefinition && (_jsx(Button, { variant: "outline", size: "icon", type: "button", className: "h-9 w-9 bg-muted", onClick: () => setSelectedLandingDefinition(null), "aria-label": "Hide details panel", "aria-pressed": "true", title: "Hide details panel", children: _jsx(PanelRight, { className: "h-4 w-4" }) })), _jsxs(Button, { size: "sm", type: "button", onClick: () => createMutation.mutate({ name: newFormName }), children: [_jsx(Plus, { className: "h-4 w-4" }), "New Form"] })] })) }) }), _jsx(CaptureBuilderSidebar, { definitions: definitionsQuery.data ?? [], currentId: id, fields: fields, fieldTypes: fieldTypes, publicationSlug: publicationSlug, onOpen: openDefinition, onCreate: () => createMutation.mutate({ name: newFormName }), onApplyTemplate: applyFormTemplate, onInsertField: insertField, onDragStart: handleDragStart, onSave: () => void saveForm(), onPublish: () => void publishForm() }), _jsx("main", { className: "flex h-full min-h-0 flex-1 flex-col gap-4 p-4 sm:p-6", "data-testid": "capture-builder-shell", children: !id ? (_jsx(StartPanel, { definitions: definitionsQuery.data ?? [], isLoading: definitionsQuery.isLoading, search: landingSearch, selectedDefinition: selectedLandingDefinition, activeDetailTab: activeLandingDetailTab, onSelectDefinition: (definition) => {
1144
+ const builderSaveAction = (_jsxs(_Fragment, { children: [_jsxs(Button, { variant: "outline", size: "sm", type: "button", onClick: () => void saveForm(), disabled: isBuilderActionPending, children: [_jsx(Save, { className: "h-4 w-4" }), isSaving ? "Saving…" : "Save"] }), _jsxs(Button, { size: "sm", type: "button", onClick: () => void publishForm(), disabled: isBuilderActionPending, children: [_jsx(Send, { className: "h-4 w-4" }), isPublishing ? "Publishing…" : "Publish"] })] }));
1145
+ const copyToClipboard = useCallback(async (value, label) => {
1146
+ if (typeof navigator === "undefined" || !navigator.clipboard?.writeText) {
1147
+ toast.error("Clipboard is not available");
1148
+ return;
1149
+ }
1150
+ try {
1151
+ await navigator.clipboard.writeText(value);
1152
+ toast.success(`${label} copied`);
1153
+ }
1154
+ catch {
1155
+ toast.error(`Could not copy ${label.toLowerCase()}`);
1156
+ }
1157
+ }, []);
1158
+ const publicFormAction = publicPath ? (_jsx(Button, { variant: "outline", size: "icon", type: "button", className: "h-9 w-9", asChild: true, children: _jsx("a", { href: publicPath, target: "_blank", rel: "noreferrer", "aria-label": "Preview public form", title: "Preview public form", children: _jsx(Eye, { className: "h-4 w-4" }) }) })) : (_jsx(Button, { variant: "outline", size: "icon", type: "button", className: "h-9 w-9", disabled: true, "aria-label": "Preview public form", title: "Publish this form to enable public preview", children: _jsx(Eye, { className: "h-4 w-4" }) }));
1159
+ const builderHeaderLabel = builderSubView === "submissions" ? "Submissions" : "Share / Embed";
1160
+ const builderHeaderDescription = builderSubView === "build"
1161
+ ? "Build the public form canvas. Add fields from the sidebar and inspect details in the right panel."
1162
+ : builderSubView === "submissions"
1163
+ ? `Review public responses for ${formName || "this form"}. Detail rows open the existing Capture submission route.`
1164
+ : "Manage public links and embed modes for this form. Publish from the header to enable public access.";
1165
+ const goBackToCaptureItems = useCallback(() => {
1166
+ void navigate({ to: sl("/capture/builder") });
1167
+ }, [navigate, sl]);
1168
+ const builderHeaderTitle = (_jsxs("div", { className: "flex min-w-0 items-center gap-2", children: [_jsx(Button, { type: "button", variant: "ghost", size: "icon", className: "-ml-1 h-8 w-8 shrink-0 rounded-full text-muted-foreground hover:bg-muted hover:text-foreground", onClick: goBackToCaptureItems, "aria-label": "Back to Capture Items", title: "Back to Capture Items", children: _jsx(ArrowLeft, { className: "h-4 w-4" }) }), _jsxs("div", { className: "min-w-0", children: [_jsxs("div", { className: "flex min-w-0 flex-wrap items-center gap-2", children: [builderSubView === "build" ? (_jsx(InlineEditableText, { value: formName, onCommit: setFormName, ariaLabel: "Form name", variant: "headerTitle", placeholder: "Untitled form", required: true })) : (_jsx("h1", { className: "text-lg font-bold text-foreground", children: builderHeaderLabel })), _jsx(CaptureStatusBadge, { status: formStatus })] }), _jsx("p", { className: "mt-0.5 truncate text-sm text-muted-foreground", children: builderHeaderDescription })] })] }));
1169
+ const previewDeviceControl = (_jsxs("div", { className: "flex rounded-lg border border-border bg-muted/40 p-0.5", "aria-label": "Preview device width", children: [_jsx(Button, { type: "button", variant: previewDevice === "desktop" ? "secondary" : "ghost", size: "icon", className: "h-8 w-9", onClick: () => setPreviewDevice("desktop"), "aria-label": "Desktop preview", title: "Desktop preview", children: _jsx(Monitor, { className: "h-3.5 w-3.5" }) }), _jsx(Button, { type: "button", variant: previewDevice === "mobile" ? "secondary" : "ghost", size: "icon", className: "h-8 w-9", onClick: () => setPreviewDevice("mobile"), "aria-label": "Mobile preview", title: "Mobile preview", children: _jsx(Smartphone, { className: "h-3.5 w-3.5" }) })] }));
1170
+ const builderHeaderActions = builderSubView === "build" ? (_jsxs(_Fragment, { children: [_jsxs("span", { className: "rounded-full bg-muted px-2.5 py-1 text-xs font-medium text-muted-foreground", children: [currentPageItems.length, " item", currentPageItems.length === 1 ? "" : "s", " on this page"] }), previewDeviceControl, publicFormAction, _jsx(Button, { variant: "outline", size: "icon", type: "button", className: cn("h-9 w-9", detailsOpen && "bg-muted"), onClick: () => setDetailsOpen((open) => !open), "aria-label": detailsOpen ? "Hide details panel" : "Show details panel", "aria-pressed": detailsOpen, title: detailsOpen ? "Hide details panel" : "Show details panel", children: _jsx(PanelRight, { className: "h-4 w-4" }) }), builderSaveAction] })) : builderSubView === "submissions" ? (_jsxs(_Fragment, { children: [!submissionsQuery.isLoading && !submissionsQuery.isError && (_jsxs("span", { className: "rounded-full bg-muted px-2.5 py-1 text-xs font-medium text-muted-foreground", children: [definitionSubmissions.length, " response", definitionSubmissions.length === 1 ? "" : "s"] })), _jsx(WorkbenchSaveResetControls, { isDirty: isSubmissionViewDirty, isSaving: createSubmissionViewMutation.isPending || updateSubmissionViewMutation.isPending, onSave: saveSubmissionView, onReset: resetSubmissionView }), _jsx(Button, { type: "button", variant: "outline", size: "icon-sm", title: "Filters", "aria-label": "Filters", onClick: () => setActiveSubmissionPanel("filters"), children: _jsx(Filter, { className: "h-4 w-4" }) }), publicFormAction] })) : (_jsxs(_Fragment, { children: [publicUrl && (_jsxs(Button, { variant: "outline", size: "sm", type: "button", onClick: () => void copyToClipboard(publicUrl, "Public link"), children: [_jsx(Copy, { className: "h-4 w-4" }), "Copy link"] })), publicFormAction, builderSaveAction] }));
1171
+ return (_jsxs(_Fragment, { children: [_jsx(HeaderPortal, { children: _jsx(ToolPageHeader, { title: id ? builderHeaderTitle : "Capture Items", description: id ? undefined : "Manage forms, quizzes, and other Capture-backed intake surfaces.", actions: id ? builderHeaderActions : (_jsxs(_Fragment, { children: [_jsxs("div", { className: "relative w-64", children: [_jsx(Search, { className: "absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" }), _jsx(Input, { type: "search", placeholder: "Search capture items...", value: landingSearch, onChange: (event) => setLandingSearch(event.target.value), className: "h-9 pl-9" })] }), _jsx(WorkbenchSaveResetControls, { isDirty: isCaptureItemsViewDirty, onSave: saveCaptureItemsView, onReset: resetCaptureItemsView }), _jsx(Button, { type: "button", variant: "outline", size: "icon-sm", title: "Filters", "aria-label": "Filters", onClick: () => setActiveWorkbenchPanel("filters"), children: _jsx(Filter, { className: "h-4 w-4" }) }), _jsx(Button, { type: "button", variant: "outline", size: "icon-sm", title: "Columns", "aria-label": "Columns", onClick: () => setActiveWorkbenchPanel("columns"), children: _jsx(Columns3, { className: "h-4 w-4" }) }), _jsx(Button, { type: "button", variant: "outline", size: "icon-sm", title: "Save as named view", "aria-label": "Save as named view", onClick: saveCaptureItemsView, children: _jsx(Settings2, { className: "h-4 w-4" }) }), selectedLandingDefinition && (_jsx(Button, { variant: "outline", size: "icon", type: "button", className: "h-9 w-9 bg-muted", onClick: () => setSelectedLandingDefinition(null), "aria-label": "Hide details panel", "aria-pressed": "true", title: "Hide details panel", children: _jsx(PanelRight, { className: "h-4 w-4" }) })), _jsxs(Button, { size: "sm", type: "button", onClick: () => createMutation.mutate({ name: newFormName }), disabled: isCreating, children: [_jsx(Plus, { className: "h-4 w-4" }), isCreating ? "Creating…" : "New Form"] })] })) }) }), _jsx(CaptureSidebar, { currentId: id, fieldTypes: fieldTypes, builderSubView: builderSubView, workbenchViews: workbenchViews, activeWorkbenchViewId: activeWorkbenchViewId, onWorkbenchViewSelect: applyCaptureItemsView, submissionWorkbenchViews: submissionWorkbenchViews, activeSubmissionViewId: activeSubmissionViewId, onSubmissionViewSelect: applySubmissionView, onInsertField: insertField, onInsertTextBlock: insertTextBlock, onInsertIconBlock: insertIconBlock, onInsertImageBlock: insertImageBlock, onInsertDivider: insertDivider, onInsertPageHeader: insertPageHeader, onInsertPageProgress: insertPageProgress, onDragStart: handleDragStart, onContentDragStart: handleContentDragStart }), _jsx("main", { className: cn("flex h-full min-h-0 flex-1 flex-col gap-4 p-4 sm:p-6", builderSubView === "build" && id && "bg-muted/40"), "data-testid": "capture-builder-shell", children: !id ? (_jsx(StartPanel, { definitions: definitionsQuery.data ?? [], isLoading: definitionsQuery.isLoading, isError: definitionsQuery.isError, onRetryDefinitions: () => void definitionsQuery.refetch(), search: landingSearch, selectedDefinition: selectedLandingDefinition, activeDetailTab: activeLandingDetailTab, activeWorkbenchViewId: activeWorkbenchViewId, activeWorkbenchPanel: activeWorkbenchPanel, onActiveWorkbenchPanelChange: setActiveWorkbenchPanel, filters: captureItemsFilters, onFiltersChange: setCaptureItemsFilters, columns: captureItemsColumns, onColumnsChange: setCaptureItemsColumns, sort: captureItemsSort, onSortChange: setCaptureItemsSort, onSelectDefinition: (definition) => {
176
1172
  setSelectedLandingDefinition(definition);
177
1173
  setActiveLandingDetailTab("overview");
178
- }, onCloseDetails: () => setSelectedLandingDefinition(null), onDetailTabChange: setActiveLandingDetailTab, onCreate: () => createMutation.mutate({ name: newFormName }), onOpen: openDefinition })) : (_jsxs(_Fragment, { children: [_jsx("section", { className: "rounded-2xl border border-border bg-card p-4 shadow-sm", children: _jsxs("div", { className: "max-w-xl", children: [_jsx(Label, { htmlFor: "capture-form-name", children: "Form name" }), _jsx(Input, { id: "capture-form-name", className: "mt-1", value: formName, onChange: (event) => setFormName(event.target.value) })] }) }), _jsxs("div", { className: "flex min-h-0 flex-1 gap-4", children: [_jsxs("section", { className: "flex min-h-0 min-w-0 flex-1 flex-col rounded-2xl border border-border bg-card shadow-sm", children: [_jsxs("div", { className: "flex items-center justify-between border-b border-border px-4 py-3", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(LayoutPanelTop, { className: "h-4 w-4 text-primary" }), _jsxs("div", { children: [_jsx("h3", { className: "text-sm font-semibold text-foreground", children: "Vertical form canvas" }), _jsx("p", { className: "text-xs text-muted-foreground", children: "Fields render in respondent order. Add fields from the sidebar; tune details in the right panel." })] })] }), _jsxs("span", { className: "rounded-full bg-muted px-2.5 py-1 text-xs font-medium text-muted-foreground", children: [fields.length, " field", fields.length === 1 ? "" : "s"] })] }), _jsx("div", { className: cn("m-4 flex min-h-[560px] flex-1 flex-col gap-3 rounded-2xl border border-dashed border-border bg-muted/25 p-4 transition", isDragOver && "border-primary bg-primary/5 ring-2 ring-primary/20"), onDragEnter: handleDragOver, onDragOver: handleDragOver, onDragLeave: () => setIsDragOver(false), onDrop: handleDrop, "data-testid": "capture-builder-canvas", children: fields.length === 0 ? (_jsxs("div", { className: "flex flex-1 flex-col items-center justify-center rounded-xl border border-dashed border-border bg-background/80 p-8 text-center", children: [_jsx(Plus, { className: "mb-3 h-8 w-8 text-muted-foreground" }), _jsx("h4", { className: "text-sm font-semibold text-foreground", children: "Add the first form field" }), _jsx("p", { className: "mt-1 max-w-sm text-sm text-muted-foreground", children: "Use the sidebar palette to add Capture-owned or contributed fields to the public form body." })] })) : (fields.map((field, index) => (_jsxs("button", { type: "button", onClick: () => { setSelectedId(field.id); setActivePanel("inspector"); setDetailsOpen(true); }, className: cn("flex items-start gap-3 rounded-xl border bg-background p-4 text-left shadow-sm transition", selectedId === field.id
179
- ? "border-primary ring-2 ring-primary/20"
180
- : "border-border hover:border-primary/50"), children: [_jsx("span", { className: "flex h-7 w-7 shrink-0 items-center justify-center rounded-full bg-muted text-xs font-semibold text-muted-foreground", children: index + 1 }), _jsxs("span", { className: "min-w-0 flex-1", children: [_jsxs("span", { className: "flex items-center gap-2 text-sm font-semibold text-foreground", children: [field.label, _jsx("span", { className: "rounded-full bg-primary/10 px-2 py-0.5 text-[11px] font-medium text-primary", children: getCaptureFieldType(field.type, fieldTypes).label }), field.required && _jsx("span", { className: "text-xs text-destructive", children: "Required" }), field.visibleWhen && _jsx("span", { className: "text-xs text-muted-foreground", children: "Conditional" })] }), _jsxs("span", { className: "mt-1 block text-sm text-muted-foreground", children: ["Key: ", field.key] })] })] }, field.id)))) })] }), _jsx(TabbedPanel, { open: detailsOpen, onClose: () => setDetailsOpen(false), tabs: FORM_PANEL_TABS, activeTab: activePanel, onTabChange: setActivePanel, width: "380px", inline: true, flush: true, className: "self-stretch rounded-2xl border shadow-sm", header: _jsxs("div", { className: "border-b border-border px-4 py-3", children: [_jsx("h2", { className: "text-sm font-semibold text-foreground", children: FORM_PANEL_TABS.find((tab) => tab.id === activePanel)?.label }), _jsx("p", { className: "mt-1 text-xs text-muted-foreground", children: "Builder context and selected-field controls." })] }), children: _jsx(FormDetailsPanel, { activePanel: activePanel, fields: fields, fieldTypes: fieldTypes, selectedField: selectedField, selectedId: selectedId, setSelectedId: (fieldId) => { setSelectedId(fieldId); setActivePanel("inspector"); }, updateSelectedField: updateSelectedField, submissions: definitionSubmissions, actionConfig: actionConfig, workflows: workflowsQuery.data ?? [], setActionConfig: setActionConfig }) })] })] })) })] }));
1174
+ }, onCloseDetails: () => setSelectedLandingDefinition(null), onDetailTabChange: setActiveLandingDetailTab, onCreate: () => createMutation.mutate({ name: newFormName }), isCreating: isCreating, onOpen: openDefinition, onTagsChange: (definitionId, tags) => updateTagsMutation.mutate({ id: definitionId, tags }), workflows: workflowsQuery.data ?? [], workflowsLoading: workflowsQuery.isLoading, workflowsError: workflowsQuery.isError, onRetryWorkflows: () => void workflowsQuery.refetch(), contactCustomFields: (contactCustomFieldsQuery.data ?? []), contactCustomFieldsLoading: contactCustomFieldsQuery.isLoading, contactCustomFieldsError: contactCustomFieldsQuery.isError, onRetryContactCustomFields: () => void contactCustomFieldsQuery.refetch(), onActionConfigChange: (definitionId, actionConfig) => updateActionConfigMutation.mutate({ id: definitionId, actionConfig }), isActionConfigSaving: updateActionConfigMutation.isPending })) : definitionQuery.isLoading ? (_jsx(BuilderWorkspaceSkeleton, {})) : definitionQuery.isError || !definitionQuery.data ? (_jsx(QueryErrorState, { title: "Could not load this form", description: "The form may have moved, been deleted, or failed to load. Try again or return to Capture Items.", onRetry: () => void definitionQuery.refetch() })) : (_jsx(_Fragment, { children: builderSubView === "build" ? (_jsxs("div", { className: "flex min-h-0 flex-1 gap-4", children: [_jsx("section", { className: "min-h-0 min-w-0 flex-1 overflow-auto p-4", children: _jsxs("div", { className: "flex min-h-[560px] items-center justify-center gap-3", children: [_jsx(Button, { type: "button", variant: "outline", size: "icon", className: "h-9 w-9 shrink-0 rounded-full bg-background/90 shadow-sm", onClick: goToPreviousPage, disabled: currentPageIndex === 0, "aria-label": "Previous page", title: "Previous page", children: _jsx(ChevronLeft, { className: "h-4 w-4" }) }), _jsx("div", { className: cn("flex min-h-[560px] flex-1 flex-col rounded-[2rem] border border-border bg-card p-4 shadow-sm transition", previewDevice === "mobile" ? "max-w-sm" : "max-w-3xl"), onDragEnter: currentPageItems.length === 0 ? handleDragOver : undefined, onDragOver: currentPageItems.length === 0 ? handleDragOver : undefined, onDrop: currentPageItems.length === 0 ? handleDrop : undefined, "data-testid": "capture-builder-canvas", children: _jsx(CaptureFormSurface, { title: formName || "Untitled form", pages: sortedPages, items: formDocument.items, currentPageId: currentPage?.id ?? currentPageId, fieldTypes: fieldTypes, mode: "builder", selectedItemId: selectedId, selectedItemIds: selectedItemIds, className: "flex min-h-full flex-1 flex-col gap-4 space-y-0", onSelectItem: handleCanvasItemSelect, onReorderItem: reorderItems, paletteDragTypes: [CAPTURE_BUILDER_DRAG_TYPE, CAPTURE_BUILDER_CONTENT_DRAG_TYPE], onPaletteDrop: insertPaletteItemAt, emptyContent: (_jsxs("div", { className: "flex flex-1 flex-col items-center justify-center rounded-xl border border-dashed border-border bg-muted/25 p-8 text-center", children: [_jsx(Plus, { className: "mb-3 h-8 w-8 text-muted-foreground" }), _jsx("h4", { className: "text-sm font-semibold text-foreground", children: "Add the first item to this page" }), _jsxs("p", { className: "mt-1 max-w-sm text-sm text-muted-foreground", children: ["Use the sidebar palette to add fields or explainer text to ", currentPage?.title ?? "the selected page", "."] })] })), footer: sortedPages.length > 1 ? (_jsxs("div", { className: "flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between", children: [_jsxs(Button, { type: "button", variant: "outline", className: "h-11 rounded-xl sm:w-auto", disabled: currentPageIndex === 0, onClick: goToPreviousPage, children: [_jsx(ArrowLeft, { className: "h-4 w-4" }), "Back"] }), _jsxs(Button, { type: "button", className: "h-11 rounded-xl sm:w-auto", disabled: currentPageIndex >= sortedPages.length - 1, onClick: goToNextPage, children: ["Next", _jsx(ArrowRight, { className: "h-4 w-4" })] })] })) : null }) }), _jsx(Button, { type: "button", variant: "outline", size: "icon", className: "h-9 w-9 shrink-0 rounded-full bg-background/90 shadow-sm", onClick: currentPageIndex >= sortedPages.length - 1 ? addPage : goToNextPage, "aria-label": currentPageIndex >= sortedPages.length - 1 ? "Add page" : "Next page", title: currentPageIndex >= sortedPages.length - 1 ? "Add page" : "Next page", children: currentPageIndex >= sortedPages.length - 1 ? _jsx(Plus, { className: "h-4 w-4" }) : _jsx(ChevronRight, { className: "h-4 w-4" }) })] }) }), _jsx(TabbedPanel, { open: detailsOpen, onClose: () => setDetailsOpen(false), tabs: formPanelTabs, activeTab: activePanel, onTabChange: setActivePanel, width: "380px", inline: true, flush: true, className: "self-stretch rounded-2xl border shadow-sm", header: _jsxs("div", { className: "border-b border-border px-4 py-3", children: [_jsx("h2", { className: "text-sm font-semibold text-foreground", children: detailsPanelTitle }), _jsx("p", { className: "mt-1 text-xs text-muted-foreground", children: activePanel === "validation"
1175
+ ? "When the validation expression matches, respondents are blocked and see the failure message."
1176
+ : "Builder context and selected-field controls." })] }), footer: selectedItemDeleteLabel ? (_jsx("div", { className: "border-t border-border p-3", children: _jsx(DeleteSelectedItemButton, { onDelete: deleteSelectedItem, label: selectedItemDeleteLabel }) })) : null, children: _jsx(FormDetailsPanel, { activePanel: activePanel, pages: sortedPages, items: formDocument.items, currentPage: currentPage, selectedPage: selectedPage, fieldTypes: fieldTypes, selectedField: selectedField, selectedTextBlock: selectedTextBlock, selectedIconBlock: selectedIconBlock, selectedImageBlock: selectedImageBlock, selectedDivider: selectedDivider, selectedPageHeader: selectedPageHeader, selectedPageProgress: selectedPageProgress, selectedId: selectedId, selectedItemIds: selectedItemIds, savedFieldIds: savedFieldIds, selectedFieldKeyDuplicate: selectedFieldKeyDuplicate, onSelectItem: handleCanvasItemSelect, onOpenInspector: (itemId) => { setSingleSelectedItem(itemId); setActivePanel("inspector"); setDetailsOpen(true); }, onSelectPage: selectPageForInspection, updateSelectedField: updateSelectedField, updateSelectedTextBlock: updateSelectedTextBlock, updateSelectedIconBlock: updateSelectedIconBlock, updateSelectedImageBlock: updateSelectedImageBlock, updateSelectedDivider: updateSelectedDivider, updateSelectedPageHeader: updateSelectedPageHeader, updateSelectedPageProgress: updateSelectedPageProgress, updateCurrentPage: updateCurrentPage, deleteCurrentPage: deleteCurrentPage }) })] })) : builderSubView === "submissions" ? (_jsx(BuilderSubmissionsView, { submissions: definitionSubmissions, fields: submissionFields, columns: submissionColumns, filters: submissionFilters, sort: submissionSort, onFiltersChange: setSubmissionFilters, onSortChange: setSubmissionSort, onOpenColumns: () => setActiveSubmissionPanel("columns"), isLoading: submissionsQuery.isLoading, isError: submissionsQuery.isError, onRetry: () => void submissionsQuery.refetch() })) : (_jsx(BuilderShareView, { publicationSlug: publicationSlug, onCopy: copyToClipboard })) })) }), _jsx(TabbedPanel, { open: builderSubView === "submissions" && activeSubmissionPanel != null, onClose: () => setActiveSubmissionPanel(null), tabs: [
1177
+ { id: "details", icon: Settings2, label: "View details" },
1178
+ { id: "filters", icon: Filter, label: "Filters" },
1179
+ { id: "columns", icon: Columns3, label: "Columns" },
1180
+ ], activeTab: activeSubmissionPanel ?? "filters", onTabChange: (tabId) => setActiveSubmissionPanel(tabId), width: "420px", header: (_jsxs("div", { className: "border-b border-border px-4 py-3", children: [_jsx("h2", { className: "text-sm font-semibold text-foreground", children: "Submission view" }), _jsx("p", { className: "mt-1 text-xs text-muted-foreground", children: "Name, save, and adjust this form\u2019s submission view." })] })), footer: activeSubmissionPanel === "details" ? (_jsxs("div", { className: "flex items-center justify-end gap-2 border-t border-border px-4 py-3", children: [_jsx(Button, { type: "button", variant: "outline", onClick: () => setActiveSubmissionPanel(null), disabled: createSubmissionViewMutation.isPending || updateSubmissionViewMutation.isPending, children: "Cancel" }), _jsxs(Button, { type: "button", onClick: saveSubmissionViewDetails, disabled: createSubmissionViewMutation.isPending || updateSubmissionViewMutation.isPending, children: [_jsx(Save, { className: "h-4 w-4" }), createSubmissionViewMutation.isPending || updateSubmissionViewMutation.isPending ? "Saving…" : activeSubmissionSavedView ? "Save view" : "Create view"] })] })) : undefined, children: activeSubmissionPanel === "details" ? (_jsx(SubmissionViewDetailsPanel, { name: submissionViewName, error: submissionViewNameError, isExisting: Boolean(activeSubmissionSavedView), isDirty: isSubmissionViewDirty, onNameChange: (name) => { setSubmissionViewName(name); setSubmissionViewNameError(null); } })) : activeSubmissionPanel === "columns" ? (_jsx(WorkbenchColumnPanel, { columns: submissionColumns, onColumnsChange: setSubmissionColumns })) : (_jsx(WorkbenchFilterPanel, { filters: submissionFilters, onFiltersChange: setSubmissionFilters, fields: fieldsToFilterFields(submissionFields), operatorsByFieldType: CAPTURE_SUBMISSION_FILTER_OPERATORS, resultCount: filterSubmissions(definitionSubmissions, submissionFilters).length })) }), builderSubView === "build" && selectedItems.length > 1 && (_jsx(CaptureBuilderBulkActionBar, { selectedCount: selectedItems.length, selectedFields: selectedInputItems, onDeselectAll: () => setSingleSelectedItem(null), onSetLabelVisible: (labelVisible) => updateSelectedInputLayout({ labelVisible }), onSetWidth: (width) => updateSelectedInputLayout({ width }), onSetWrapAfter: (wrapAfter) => updateSelectedInputLayout({ wrapAfter }), onSetRequired: updateSelectedInputRequired, onDelete: deleteSelectedItems }))] }));
181
1181
  }
182
- function CaptureBuilderSidebar({ definitions, currentId, fields, fieldTypes, publicationSlug, onOpen, onCreate, onApplyTemplate, onInsertField, onDragStart, onSave, onPublish, }) {
183
- const navigate = useNavigate();
184
- const sl = useScopeLink();
185
- const currentDefinition = definitions.find((definition) => definition.id === currentId);
186
- const itemClass = (active) => `w-full flex items-center gap-2.5 px-3 py-1.5 rounded-lg text-sm transition-colors ${active
187
- ? "bg-muted text-foreground font-medium"
188
- : "text-muted-foreground hover:bg-muted/50 hover:text-foreground"}`;
189
- return (_jsx(SidebarPortal, { children: _jsxs("div", { className: "flex h-full flex-col", children: [_jsxs("div", { className: "flex-1 overflow-y-auto px-3 py-2", children: [_jsxs("div", { children: [_jsx("div", { className: "px-3 mb-1", children: _jsx("span", { className: "text-[11px] font-semibold uppercase tracking-wider text-primary", children: "Navigate" }) }), _jsxs("div", { className: "space-y-0.5", children: [_jsxs("button", { type: "button", className: itemClass(true), onClick: () => currentId ? onOpen(currentId) : undefined, children: [_jsx(FileText, { className: "h-4 w-4 shrink-0" }), _jsx("span", { className: "flex-1 text-left", children: "Forms" })] }), _jsxs("button", { type: "button", className: itemClass(false), onClick: () => void navigate({ to: sl("/capture/quizzes/builder") }), children: [_jsx(Sparkles, { className: "h-4 w-4 shrink-0" }), _jsx("span", { className: "flex-1 text-left", children: "Quizzes" })] }), _jsxs("button", { type: "button", className: itemClass(false), onClick: () => void navigate({ to: sl("/capture/submissions") }), children: [_jsx(Inbox, { className: "h-4 w-4 shrink-0" }), _jsx("span", { className: "flex-1 text-left", children: "Submissions" })] })] })] }), _jsx("div", { className: "my-3 border-t border-border" }), currentId && (_jsxs("div", { children: [_jsxs("div", { className: "flex items-center justify-between px-3 mb-1", children: [_jsx("span", { className: "text-[11px] font-semibold uppercase tracking-wider text-primary", children: "Current Form" }), _jsx("span", { className: "text-[11px] font-medium text-muted-foreground", children: fields.length })] }), _jsxs("div", { className: "rounded-lg bg-muted/40 px-3 py-2 text-xs text-muted-foreground", children: [_jsx("p", { className: "font-medium text-foreground", children: currentDefinition?.name ?? "Untitled form" }), _jsxs("p", { className: "mt-1", children: [currentDefinition?.status ?? "draft", publicationSlug ? ` · /forms/${publicationSlug}` : ""] })] })] })), definitions.length > 0 && (_jsxs("div", { className: "mt-4", children: [_jsxs("div", { className: "flex items-center justify-between px-3 mb-1", children: [_jsx("span", { className: "text-[11px] font-semibold uppercase tracking-wider text-primary", children: "Capture Items" }), _jsx("span", { className: "text-[11px] font-medium text-muted-foreground", children: definitions.length })] }), _jsx("div", { className: "space-y-0.5", children: definitions.map((definition) => (_jsxs("button", { type: "button", className: itemClass(definition.id === currentId), onClick: () => onOpen(definition.id), children: [definition.surface === "quiz" ? _jsx(Sparkles, { className: "h-3.5 w-3.5 shrink-0" }) : _jsx(FileText, { className: "h-3.5 w-3.5 shrink-0" }), _jsxs("span", { className: "min-w-0 flex-1 text-left", children: [_jsx("span", { className: "block truncate", children: definition.name }), _jsx("span", { className: "block text-[11px] capitalize text-muted-foreground", children: definition.surface })] })] }, definition.id))) })] })), currentId && (_jsxs(_Fragment, { children: [_jsxs("div", { className: "mt-4", children: [_jsx("div", { className: "px-3 mb-1", children: _jsx("span", { className: "text-[11px] font-semibold uppercase tracking-wider text-primary", children: "Templates" }) }), _jsx("div", { className: "space-y-1", children: CAPTURE_FORM_TEMPLATES.map((template) => (_jsxs("button", { type: "button", onClick: () => onApplyTemplate(template.id), className: "w-full rounded-lg px-3 py-2 text-left text-sm text-muted-foreground transition-colors hover:bg-muted/50 hover:text-foreground", children: [_jsx("span", { className: "block font-medium", children: template.label }), _jsx("span", { className: "mt-0.5 block text-xs text-muted-foreground", children: template.description })] }, template.id))) })] }), _jsxs("div", { className: "mt-4", children: [_jsx("div", { className: "px-3 mb-1", children: _jsx("span", { className: "text-[11px] font-semibold uppercase tracking-wider text-primary", children: "Palette" }) }), _jsx("div", { className: "space-y-1", children: fieldTypes.map((tile) => {
190
- const Icon = FIELD_ICON_MAP[tile.icon] ?? Type;
191
- return (_jsxs("button", { type: "button", draggable: true, onClick: () => onInsertField(tile), onDragStart: (event) => onDragStart(event, tile), className: "group flex w-full items-start gap-2 rounded-lg px-3 py-2 text-left text-muted-foreground transition-colors hover:bg-muted/50 hover:text-foreground", children: [_jsx(Icon, { className: "mt-0.5 h-3.5 w-3.5 shrink-0" }), _jsxs("span", { className: "min-w-0 flex-1", children: [_jsx("span", { className: "block text-sm font-medium", children: tile.label }), _jsx("span", { className: "block text-xs text-muted-foreground", children: tile.description })] }), _jsx(GripVertical, { className: "mt-0.5 h-3.5 w-3.5 opacity-40 transition group-hover:opacity-80" })] }, tile.id));
192
- }) })] })] }))] }), _jsxs("div", { className: "space-y-1.5 border-t border-border px-3 py-3", children: [currentId && (_jsxs(_Fragment, { children: [_jsxs(Button, { type: "button", variant: "outline", size: "sm", className: "w-full justify-center", onClick: onSave, children: [_jsx(Save, { className: "h-4 w-4" }), "Save"] }), _jsxs(Button, { type: "button", size: "sm", className: "w-full justify-center", onClick: onPublish, children: [_jsx(Send, { className: "h-4 w-4" }), "Publish"] })] })), _jsxs(Button, { type: "button", variant: currentId ? "ghost" : "default", size: "sm", className: "w-full justify-center", onClick: onCreate, children: [_jsx(Plus, { className: "h-4 w-4" }), "New form"] })] })] }) }));
193
- }
194
- function FormDetailsPanel({ activePanel, fields, fieldTypes, selectedField, selectedId, setSelectedId, updateSelectedField, submissions, actionConfig, workflows, setActionConfig, }) {
1182
+ function CaptureBuilderBulkActionBar({ selectedCount, selectedFields, onDeselectAll, onSetLabelVisible, onSetWidth, onSetWrapAfter, onSetRequired, onDelete, }) {
1183
+ const fieldLayouts = selectedFields.map(getFieldLayout);
1184
+ const allLabelsVisible = selectedFields.length > 0 && fieldLayouts.every((layout) => layout.labelVisible);
1185
+ const allWrapAfter = selectedFields.length > 0 && fieldLayouts.every((layout) => layout.wrapAfter);
1186
+ const allRequired = selectedFields.length > 0 && selectedFields.every((field) => field.required);
1187
+ return (_jsxs(BulkActionBar, { selectedCount: selectedCount, onDeselectAll: onDeselectAll, children: [selectedFields.length > 0 && (_jsxs(_Fragment, { children: [_jsx(BulkActionButton, { onClick: () => onSetLabelVisible(!allLabelsVisible), children: allLabelsVisible ? "Hide labels" : "Show labels" }), _jsx("div", { className: "flex items-center gap-1 rounded-lg bg-zinc-950/40 p-1 dark:bg-zinc-900/50", "aria-label": "Set field width", children: [
1188
+ ["1/3", "⅓"],
1189
+ ["1/2", "½"],
1190
+ ["full", "Full"],
1191
+ ].map(([width, label]) => (_jsx(BulkActionButton, { className: "px-2 py-1", onClick: () => onSetWidth(width), children: label }, width))) }), _jsx(BulkActionButton, { onClick: () => onSetWrapAfter(!allWrapAfter), children: allWrapAfter ? "Unwrap" : "Wrap" }), _jsx(BulkActionButton, { onClick: () => onSetRequired(!allRequired), children: allRequired ? "Optional" : "Required" })] })), _jsx(BulkActionButton, { variant: "destructive", onClick: onDelete, "aria-label": "Delete selected", children: _jsx(Trash2, { className: "h-4 w-4" }) })] }));
1192
+ }
1193
+ function FormDetailsPanel({ activePanel, pages, items, selectedPage, fieldTypes, selectedField, selectedTextBlock, selectedIconBlock, selectedImageBlock, selectedDivider, selectedPageHeader, selectedPageProgress, selectedId, selectedItemIds, savedFieldIds, selectedFieldKeyDuplicate, onSelectItem, onOpenInspector, onSelectPage, updateSelectedField, updateSelectedTextBlock, updateSelectedIconBlock, updateSelectedImageBlock, updateSelectedDivider, updateSelectedPageHeader, updateSelectedPageProgress, updateCurrentPage, deleteCurrentPage, }) {
1194
+ const [confirmDeletePageOpen, setConfirmDeletePageOpen] = useState(false);
1195
+ const selectedVisibilityTarget = selectedPage ?? selectedField ?? selectedTextBlock ?? selectedIconBlock ?? selectedImageBlock ?? selectedDivider ?? selectedPageHeader ?? selectedPageProgress;
1196
+ const updateStaticVisibility = (hidden) => {
1197
+ const patch = { hidden: hidden || undefined, ...(hidden ? { visibleWhen: undefined } : {}) };
1198
+ if (selectedPage)
1199
+ updateCurrentPage(patch);
1200
+ else if (selectedField)
1201
+ updateSelectedField(patch);
1202
+ else if (selectedTextBlock)
1203
+ updateSelectedTextBlock(patch);
1204
+ else if (selectedIconBlock)
1205
+ updateSelectedIconBlock(patch);
1206
+ else if (selectedImageBlock)
1207
+ updateSelectedImageBlock(patch);
1208
+ else if (selectedDivider)
1209
+ updateSelectedDivider(patch);
1210
+ else if (selectedPageHeader)
1211
+ updateSelectedPageHeader(patch);
1212
+ else if (selectedPageProgress)
1213
+ updateSelectedPageProgress(patch);
1214
+ };
1215
+ const staticVisibilityControl = selectedVisibilityTarget ? (_jsx(StaticVisibilityControl, { hidden: Boolean(selectedVisibilityTarget.hidden), onHiddenChange: updateStaticVisibility })) : null;
195
1216
  if (activePanel === "outline") {
196
- return (_jsx("div", { className: "p-3", children: fields.length === 0 ? _jsx(EmptyPanelText, { children: "No form fields yet." }) : (_jsx("div", { className: "space-y-1", children: fields.map((field, index) => (_jsxs("button", { type: "button", onClick: () => setSelectedId(field.id), className: cn("flex w-full items-center gap-2 rounded-lg px-2 py-2 text-left text-sm transition", selectedId === field.id
197
- ? "bg-muted text-foreground"
198
- : "text-muted-foreground hover:bg-muted/60 hover:text-foreground"), children: [_jsx("span", { className: "text-xs font-semibold tabular-nums", children: index + 1 }), _jsx("span", { className: "min-w-0 flex-1 truncate", children: field.label }), _jsx("span", { className: "rounded-full bg-muted px-1.5 py-0.5 text-[10px] text-muted-foreground", children: getCaptureFieldType(field.type, fieldTypes).label })] }, field.id))) })) }));
1217
+ return (_jsx("div", { className: "p-3", children: pages.length === 0 ? _jsx(EmptyPanelText, { children: "No pages yet." }) : (_jsxs("div", { className: "space-y-3", children: [_jsxs("div", { className: "flex items-center justify-between px-1", children: [_jsx("span", { className: "text-[11px] font-semibold uppercase tracking-wide text-muted-foreground", children: "Pages" }), _jsxs("span", { className: "text-[11px] text-muted-foreground", children: [items.length, " total item", items.length === 1 ? "" : "s"] })] }), _jsx("div", { className: "space-y-3", children: pages.map((page, pageIndex) => {
1218
+ const pageItems = items
1219
+ .filter((item) => item.pageId === page.id)
1220
+ .sort((a, b) => a.order - b.order);
1221
+ const pageSelected = selectedId === `page:${page.id}`;
1222
+ return (_jsxs("section", { children: [_jsxs("button", { type: "button", onClick: () => onSelectPage(page.id), className: cn("group flex w-full items-center gap-2 rounded-lg px-2 py-2 text-left transition", pageSelected
1223
+ ? "bg-primary/10 text-foreground"
1224
+ : "text-muted-foreground hover:bg-muted/50 hover:text-foreground"), children: [_jsx("span", { className: "min-w-0 flex-1 truncate text-sm font-medium", children: page.title || `Page ${pageIndex + 1}` }), _jsxs("span", { className: "text-[11px] text-muted-foreground", children: [pageItems.length, " item", pageItems.length === 1 ? "" : "s"] })] }), pageItems.length > 0 ? (_jsx("div", { className: "mt-1 space-y-0.5 pl-3", children: pageItems.map((item) => (_jsxs("button", { type: "button", onClick: (event) => onSelectItem(item, event), onDoubleClick: () => onOpenInspector(item.id), className: cn("group/item flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-left text-xs transition", selectedItemIds.has(item.id)
1225
+ ? "bg-muted text-foreground"
1226
+ : "text-muted-foreground hover:bg-muted/60 hover:text-foreground"), children: [_jsx("span", { className: "h-1.5 w-1.5 shrink-0 rounded-full bg-muted-foreground/40 group-hover/item:bg-muted-foreground" }), _jsx("span", { className: "min-w-0 flex-1 truncate", children: item.kind === "content.text" ? (item.title || "Explainer text") : item.kind === "content.icon" ? "Icon" : item.kind === "content.image" ? "Image" : item.kind === "content.divider" ? "Divider" : item.kind === "content.page-header" ? "Page header" : item.kind === "content.page-progress" ? "Page progress" : item.kind === "input" ? item.label : "Content" }), _jsx("span", { className: "text-[10px] text-muted-foreground group-hover/item:text-foreground", children: item.kind === "content.text" ? "Text" : item.kind === "content.icon" ? "Icon" : item.kind === "content.image" ? "Image" : item.kind === "content.divider" ? "Divider" : item.kind === "content.page-header" ? "Header" : item.kind === "content.page-progress" ? "Progress" : item.kind === "input" ? getCaptureFieldType(item.type, fieldTypes).label : "Content" })] }, item.id))) })) : (_jsx("p", { className: "px-2 py-1 pl-5 text-xs text-muted-foreground", children: "No items yet." }))] }, page.id));
1227
+ }) })] })) }));
199
1228
  }
200
1229
  if (activePanel === "inspector") {
201
- return (_jsx("div", { className: "p-3", children: selectedField ? (_jsxs("div", { className: "space-y-3", children: [_jsxs("div", { className: "space-y-1", children: [_jsx(Label, { htmlFor: "field-label", children: "Label" }), _jsx(Input, { id: "field-label", value: selectedField.label, onChange: (event) => updateSelectedField({ label: event.target.value }) })] }), _jsxs("div", { className: "space-y-1", children: [_jsx(Label, { htmlFor: "field-key", children: "Key" }), _jsx(Input, { id: "field-key", value: selectedField.key, onChange: (event) => updateSelectedField({ key: event.target.value }) })] }), _jsxs("div", { className: "flex items-center justify-between gap-3 rounded-lg border border-border p-2", children: [_jsx("span", { className: "text-sm text-muted-foreground", children: "Required respondent answer" }), _jsx(Switch, { checked: selectedField.required, onCheckedChange: (checked) => updateSelectedField({ required: checked }) })] }), _jsxs("div", { className: "space-y-2 rounded-lg border border-border p-2", children: [_jsxs("div", { className: "flex items-center justify-between gap-3", children: [_jsx("span", { className: "text-sm text-muted-foreground", children: "Show condition" }), _jsx(Switch, { checked: Boolean(selectedField.visibleWhen), onCheckedChange: (checked) => {
202
- const source = fields.find((field) => field.id !== selectedField.id);
203
- updateSelectedField({
204
- visibleWhen: checked && source
205
- ? { fieldId: source.id, fieldKey: source.key, operator: "equals", value: "" }
206
- : undefined,
207
- });
208
- }, disabled: fields.every((field) => field.id === selectedField.id) })] }), selectedField.visibleWhen && (_jsxs("div", { className: "space-y-2", children: [_jsx("select", { className: "h-9 w-full rounded-md border border-input bg-background px-3 py-1 text-sm shadow-xs outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50", value: selectedField.visibleWhen.fieldId, onChange: (event) => {
209
- const source = fields.find((field) => field.id === event.target.value);
210
- if (!source)
211
- return;
212
- updateSelectedField({ visibleWhen: { ...selectedField.visibleWhen, fieldId: source.id, fieldKey: source.key } });
213
- }, children: fields.filter((field) => field.id !== selectedField.id).map((field) => (_jsxs("option", { value: field.id, children: [field.label, " (", field.key, ")"] }, field.id))) }), _jsx(Input, { placeholder: "Show when answer equals\u2026", value: String(selectedField.visibleWhen.value ?? ""), onChange: (event) => updateSelectedField({ visibleWhen: { ...selectedField.visibleWhen, value: event.target.value } }) }), _jsx("p", { className: "text-xs text-muted-foreground", children: "Uses stable field ID plus key so future renames keep a fallback." })] }))] }), selectedField.type === "select" && (_jsxs("div", { className: "space-y-1", children: [_jsx(Label, { htmlFor: "field-options", children: "Options, one per line" }), _jsx(Textarea, { id: "field-options", value: (selectedField.options ?? []).join("\n"), onChange: (event) => updateSelectedField({
1230
+ return (_jsx("div", { className: "h-full p-3", children: selectedPage ? (_jsxs("div", { className: "flex h-full flex-col gap-3", children: [_jsxs("div", { children: [_jsx("h3", { className: "text-sm font-semibold text-foreground", children: "Page details" }), _jsx("p", { className: "mt-1 text-xs text-muted-foreground", children: "This label identifies the page in the builder, progress indicators, and conditional visibility groups. Header copy lives on Page header content blocks." })] }), staticVisibilityControl, _jsxs("div", { className: "space-y-1", children: [_jsx(Label, { htmlFor: "page-title", children: "Page label" }), _jsx(Input, { id: "page-title", value: selectedPage.title, placeholder: "Page label", onChange: (event) => updateCurrentPage({ title: event.target.value || "Untitled page" }) })] }), _jsx("div", { className: "mt-auto rounded-lg border border-destructive/20 bg-destructive/5 p-3", children: _jsxs("div", { className: "flex items-center justify-between gap-3", children: [_jsxs("div", { children: [_jsx("p", { className: "text-sm font-medium text-foreground", children: "Delete page" }), _jsx("p", { className: "mt-1 text-xs text-muted-foreground", children: "Removes this page and all items on it." })] }), _jsxs(Button, { type: "button", variant: "outline", size: "sm", className: "shrink-0 border-destructive/30 text-destructive hover:bg-destructive/10", disabled: pages.length <= 1, onClick: () => setConfirmDeletePageOpen(true), children: [_jsx(Trash2, { className: "mr-1.5 h-3.5 w-3.5" }), "Delete"] })] }) }), _jsx(AlertDialog, { open: confirmDeletePageOpen, onOpenChange: setConfirmDeletePageOpen, children: _jsxs(AlertDialogContent, { children: [_jsxs(AlertDialogHeader, { children: [_jsxs(AlertDialogTitle, { children: ["Delete \u201C", selectedPage.title || "Untitled page", "\u201D?"] }), _jsxs(AlertDialogDescription, { children: ["This will permanently remove the page and ", items.filter((item) => item.pageId === selectedPage.id).length, " item", items.filter((item) => item.pageId === selectedPage.id).length === 1 ? "" : "s", " on it. This cannot be undone after you save the form."] })] }), _jsxs(AlertDialogFooter, { children: [_jsx(AlertDialogCancel, { children: "Cancel" }), _jsx(AlertDialogAction, { className: "bg-destructive text-destructive-foreground hover:bg-destructive/90", onClick: () => {
1231
+ deleteCurrentPage();
1232
+ setConfirmDeletePageOpen(false);
1233
+ }, children: "Delete page" })] })] }) })] })) : selectedTextBlock ? (_jsxs("div", { className: "space-y-3", children: [_jsxs("div", { className: "rounded-lg border border-primary/20 bg-primary/5 p-3", children: [_jsxs("p", { className: "flex items-center gap-2 text-xs font-semibold uppercase tracking-wide text-primary", children: [_jsx(FileText, { className: "h-3.5 w-3.5" }), " Explainer text"] }), _jsx("p", { className: "mt-1 text-xs text-muted-foreground", children: "Content blocks render in the form body but are ignored by answer validation and actions." })] }), staticVisibilityControl, _jsxs("div", { className: "space-y-1", children: [_jsx(Label, { htmlFor: "text-block-title", children: "Title" }), _jsx(Input, { id: "text-block-title", value: selectedTextBlock.title ?? "", placeholder: "Optional heading", onChange: (event) => updateSelectedTextBlock({ title: event.target.value }) })] }), _jsxs("div", { className: "space-y-1", children: [_jsx(Label, { htmlFor: "text-block-body", children: "Body" }), _jsx(Textarea, { id: "text-block-body", value: selectedTextBlock.body, placeholder: "Add explanatory copy for respondents.", rows: 6, onChange: (event) => updateSelectedTextBlock({ body: event.target.value }) })] })] })) : selectedField ? (_jsxs("div", { className: "space-y-5", children: [_jsxs("div", { className: "space-y-1", children: [_jsxs("div", { className: "flex items-center justify-between gap-3", children: [_jsx(Label, { htmlFor: "field-label", children: "Label" }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx("span", { className: "text-sm font-medium text-foreground", children: "Show" }), _jsx(Switch, { checked: getFieldLayout(selectedField).labelVisible, onCheckedChange: (checked) => updateSelectedField({
1234
+ metadata: {
1235
+ ...(selectedField.metadata ?? {}),
1236
+ layout: {
1237
+ ...getFieldLayout(selectedField),
1238
+ labelVisible: checked,
1239
+ },
1240
+ },
1241
+ }) })] })] }), _jsx(Input, { id: "field-label", value: selectedField.label, onChange: (event) => updateSelectedField({ label: event.target.value }) })] }), _jsx(FieldLayoutControls, { field: selectedField, onChange: (patch) => updateSelectedField({
1242
+ metadata: {
1243
+ ...(selectedField.metadata ?? {}),
1244
+ layout: {
1245
+ ...getFieldLayout(selectedField),
1246
+ ...patch,
1247
+ },
1248
+ },
1249
+ }) }), supportsPlaceholder(selectedField) && (_jsxs("div", { className: "space-y-1", children: [_jsx(Label, { htmlFor: "field-placeholder", children: "Placeholder" }), _jsx(Input, { id: "field-placeholder", value: getFieldPlaceholder(selectedField), placeholder: "Sample answer shown before typing", onChange: (event) => {
1250
+ const value = event.target.value;
1251
+ updateSelectedField({
1252
+ metadata: {
1253
+ ...(selectedField.metadata ?? {}),
1254
+ ...(value.trim() ? { placeholder: value } : { placeholder: undefined }),
1255
+ },
1256
+ });
1257
+ } }, selectedField.id), _jsx("p", { className: "text-xs text-muted-foreground", children: "Shown in the respondent preview and public form inputs." })] })), selectedField.type === "rating" && (_jsx(RatingFieldControls, { field: selectedField, onChange: (patch) => updateSelectedField({
1258
+ metadata: {
1259
+ ...(selectedField.metadata ?? {}),
1260
+ ...patch,
1261
+ },
1262
+ }) })), _jsxs("div", { className: "grid gap-3 sm:grid-cols-2", children: [staticVisibilityControl, _jsxs("div", { className: "flex items-center justify-between gap-3", children: [_jsx("span", { className: "text-sm font-medium text-foreground", children: "Required" }), _jsx(Switch, { checked: selectedField.required, onCheckedChange: (checked) => updateSelectedField({ required: checked }) })] })] }), selectedField.type === "select" && (_jsxs("div", { className: "space-y-1", children: [_jsx(Label, { htmlFor: "field-options", children: "Options, one per line" }), _jsx(Textarea, { id: "field-options", value: (selectedField.options ?? []).join("\n"), onChange: (event) => updateSelectedField({
214
1263
  options: event.target.value.split("\n").map((option) => option.trim()).filter(Boolean),
215
- }) })] }))] })) : _jsx(EmptyPanelText, { children: "Select a field to edit its basics." }) }));
1264
+ }) })] })), _jsx(FieldKeyControl, { field: selectedField, locked: savedFieldIds.has(selectedField.id), duplicate: selectedFieldKeyDuplicate, onChange: (key) => updateSelectedField({ key }) })] })) : selectedIconBlock ? (_jsxs("div", { className: "space-y-4", children: [staticVisibilityControl, _jsx(IconBlockControls, { item: selectedIconBlock, onChange: updateSelectedIconBlock })] })) : selectedImageBlock ? (_jsxs("div", { className: "space-y-4", children: [staticVisibilityControl, _jsx(ImageBlockControls, { item: selectedImageBlock, onChange: updateSelectedImageBlock })] })) : selectedDivider ? (_jsxs("div", { className: "space-y-3", children: [staticVisibilityControl, _jsxs("div", { className: "space-y-2", children: [_jsx("h3", { className: "text-sm font-semibold text-foreground", children: "Divider" }), _jsx("p", { className: "text-sm text-muted-foreground", children: "A visual divider between form items. Drag it on the canvas to reorder it." })] })] })) : selectedPageHeader ? (_jsxs("div", { className: "space-y-3", children: [staticVisibilityControl, _jsxs("div", { className: "space-y-2", children: [_jsx("h3", { className: "text-sm font-semibold text-foreground", children: "Page header" }), _jsx("p", { className: "text-sm text-muted-foreground", children: "Shows heading copy in the form body. Each header block owns its own text, so multiple headers on one page can differ." })] }), _jsxs("div", { className: "space-y-1", children: [_jsx(Label, { htmlFor: "page-header-pre-heading", children: "Pre-heading" }), _jsx(Input, { id: "page-header-pre-heading", value: selectedPageHeader.preHeading ?? "", placeholder: "Capture Form", onChange: (event) => updateSelectedPageHeader({ preHeading: event.target.value || undefined }) })] }), _jsxs("div", { className: "space-y-1", children: [_jsx(Label, { htmlFor: "page-header-title", children: "Title" }), _jsx(Input, { id: "page-header-title", value: selectedPageHeader.title, placeholder: "Header title", onChange: (event) => updateSelectedPageHeader({ title: event.target.value || "Untitled page" }) })] }), _jsxs("div", { className: "space-y-1", children: [_jsx(Label, { htmlFor: "page-header-description", children: "Description" }), _jsx(Textarea, { id: "page-header-description", value: selectedPageHeader.description ?? "", placeholder: "Optional page intro shown in the form.", rows: 4, onChange: (event) => updateSelectedPageHeader({ description: event.target.value || undefined }) })] })] })) : selectedPageProgress ? (_jsxs("div", { className: "space-y-3", children: [staticVisibilityControl, _jsxs("div", { className: "space-y-2", children: [_jsx("h3", { className: "text-sm font-semibold text-foreground", children: "Page progress" }), _jsx("p", { className: "text-sm text-muted-foreground", children: "Shows the respondent which page they are on. Drag it on the canvas to move it, or delete it if this page should not show progress." })] })] })) : _jsx(EmptyPanelText, { children: "Select a page, field, or content block to edit its basics." }) }));
216
1265
  }
217
- if (activePanel === "submissions") {
218
- return _jsx("div", { className: "p-3", children: _jsx(SubmissionPreview, { submissions: submissions }) });
1266
+ if (activePanel === "conditions") {
1267
+ const selectedConditionItem = selectedPage ?? selectedField ?? selectedTextBlock ?? selectedIconBlock ?? selectedImageBlock ?? selectedDivider ?? selectedPageHeader ?? selectedPageProgress;
1268
+ const selectedPageOrder = selectedPage
1269
+ ? selectedPage.order
1270
+ : selectedConditionItem && "pageId" in selectedConditionItem
1271
+ ? pages.find((page) => page.id === selectedConditionItem.pageId)?.order ?? 0
1272
+ : 0;
1273
+ const conditionFields = items
1274
+ .filter((item) => item.kind === "input")
1275
+ .filter((item) => item.id !== selectedConditionItem?.id)
1276
+ .filter((item) => (pages.find((page) => page.id === item.pageId)?.order ?? 0) <= selectedPageOrder)
1277
+ .sort((a, b) => {
1278
+ const pageDiff = (pages.find((page) => page.id === a.pageId)?.order ?? 0) - (pages.find((page) => page.id === b.pageId)?.order ?? 0);
1279
+ return pageDiff || a.order - b.order;
1280
+ })
1281
+ .map((field) => {
1282
+ const contribution = fieldTypes.find((type) => type.id === field.type) ?? getCaptureFieldType(field.type);
1283
+ const renderer = contribution?.renderer ?? "text";
1284
+ const page = pages.find((candidate) => candidate.id === field.pageId);
1285
+ return {
1286
+ key: field.key,
1287
+ label: field.label,
1288
+ type: renderer === "rating" ? "rating" : renderer === "select" ? "select" : renderer,
1289
+ group: page?.title ?? "Prior page",
1290
+ options: field.options?.map((option) => ({ value: option, label: option })),
1291
+ };
1292
+ });
1293
+ const filters = toConditionGroup(selectedConditionItem?.visibleWhen);
1294
+ const updateSelectedCondition = (visibleWhen) => {
1295
+ if (selectedPage)
1296
+ updateCurrentPage({ visibleWhen });
1297
+ else if (selectedField)
1298
+ updateSelectedField({ visibleWhen });
1299
+ else if (selectedTextBlock)
1300
+ updateSelectedTextBlock({ visibleWhen });
1301
+ else if (selectedIconBlock)
1302
+ updateSelectedIconBlock({ visibleWhen });
1303
+ else if (selectedImageBlock)
1304
+ updateSelectedImageBlock({ visibleWhen });
1305
+ else if (selectedDivider)
1306
+ updateSelectedDivider({ visibleWhen });
1307
+ else if (selectedPageHeader)
1308
+ updateSelectedPageHeader({ visibleWhen });
1309
+ else if (selectedPageProgress)
1310
+ updateSelectedPageProgress({ visibleWhen });
1311
+ };
1312
+ return (_jsx("div", { className: "space-y-4 p-3", children: selectedConditionItem ? (_jsx("div", { className: "space-y-3", children: conditionFields.length === 0 ? (_jsx("p", { className: "rounded-lg border border-dashed border-border bg-muted/20 p-4 text-sm text-muted-foreground", children: "Add a field on this page or an earlier page before creating a show condition." })) : (_jsx("div", { className: "[&>div>div:first-child]:hidden", children: _jsx(FilterPanel, { filters: filters, onFiltersChange: (nextFilters) => updateSelectedCondition(nextFilters ?? undefined), fields: conditionFields, operatorsByFieldType: CAPTURE_CONDITION_OPERATORS, constrainWorkspaceHeight: false, labels: {
1313
+ title: "Conditional Visibility",
1314
+ helpText: "Use answers from this page or earlier pages to decide whether this item appears.",
1315
+ emptyText: "Use answers from this page or earlier pages to decide whether this item appears.",
1316
+ addFilter: "Add condition",
1317
+ addGroup: "Add group",
1318
+ addNestedFilter: "add condition",
1319
+ clearAllTitle: "Clear all conditions",
1320
+ } }) })) })) : _jsx(EmptyPanelText, { children: "Select a page, field, or content block to configure its show condition." }) }));
219
1321
  }
220
- if (activePanel === "actions") {
221
- return _jsx("div", { className: "p-3", children: _jsx(ActionsPanel, { fields: fields, config: actionConfig, workflows: workflows, onChange: setActionConfig }) });
1322
+ if (activePanel === "validation") {
1323
+ const selectedPageOrder = selectedField
1324
+ ? pages.find((page) => page.id === selectedField.pageId)?.order ?? 0
1325
+ : 0;
1326
+ const validationFields = items
1327
+ .filter((item) => item.kind === "input")
1328
+ .filter((item) => (pages.find((page) => page.id === item.pageId)?.order ?? 0) <= selectedPageOrder)
1329
+ .sort((a, b) => {
1330
+ const pageDiff = (pages.find((page) => page.id === a.pageId)?.order ?? 0) - (pages.find((page) => page.id === b.pageId)?.order ?? 0);
1331
+ return pageDiff || a.order - b.order;
1332
+ })
1333
+ .map((field) => {
1334
+ const contribution = fieldTypes.find((type) => type.id === field.type) ?? getCaptureFieldType(field.type);
1335
+ const renderer = contribution?.renderer ?? "text";
1336
+ const page = pages.find((candidate) => candidate.id === field.pageId);
1337
+ return {
1338
+ key: field.key,
1339
+ label: field.id === selectedField?.id ? `${field.label} (this field)` : field.label,
1340
+ type: renderer === "rating" ? "rating" : renderer === "select" ? "select" : renderer,
1341
+ group: page?.title ?? "Prior page",
1342
+ options: field.options?.map((option) => ({ value: option, label: option })),
1343
+ };
1344
+ });
1345
+ const rule = selectedField?.validationRules?.[0];
1346
+ const filters = rule?.expression;
1347
+ const updateValidation = (nextFilters, message = rule?.message) => {
1348
+ if (!selectedField)
1349
+ return;
1350
+ if (!nextFilters) {
1351
+ updateSelectedField({ validationRules: [] });
1352
+ return;
1353
+ }
1354
+ updateSelectedField({
1355
+ validationRules: [{
1356
+ id: rule?.id ?? crypto.randomUUID().slice(0, 12),
1357
+ message: message || `${selectedField.label} is invalid`,
1358
+ expression: nextFilters,
1359
+ }],
1360
+ });
1361
+ };
1362
+ return (_jsx("div", { className: "space-y-4 p-3", children: selectedField ? (_jsxs("div", { className: "space-y-4", children: [_jsxs("div", { className: "space-y-1", children: [_jsx(Label, { htmlFor: `validation-message-${selectedField.id}`, children: "Failure message" }), _jsx(Input, { id: `validation-message-${selectedField.id}`, value: rule?.message ?? `${selectedField.label} is invalid`, onChange: (event) => {
1363
+ const nextMessage = event.target.value || `${selectedField.label} is invalid`;
1364
+ updateValidation((filters ?? createDefaultValidationRule(selectedField).expression), nextMessage);
1365
+ } })] }), _jsx("div", { className: "[&>div>div:first-child]:hidden", children: _jsx(FilterPanel, { filters: filters, onFiltersChange: (nextFilters) => updateValidation(nextFilters), fields: validationFields, operatorsByFieldType: CAPTURE_CONDITION_OPERATORS, constrainWorkspaceHeight: false, labels: {
1366
+ title: "Validation",
1367
+ helpText: "Add conditions that should fail this field when they match.",
1368
+ emptyText: "Add conditions that should fail this field when they match.",
1369
+ addFilter: "Add condition",
1370
+ addGroup: "Add group",
1371
+ addNestedFilter: "add condition",
1372
+ clearAllTitle: "Clear validation",
1373
+ } }) })] })) : _jsx(EmptyPanelText, { children: "Select a field to configure validation rules." }) }));
222
1374
  }
223
- return (_jsx("div", { className: "p-3", children: _jsxs("div", { className: "rounded-2xl border border-dashed border-border bg-muted/25 p-8 text-center", children: [_jsx(Send, { className: "mx-auto mb-3 h-8 w-8 text-muted-foreground" }), _jsxs("h3", { className: "text-sm font-semibold text-foreground", children: [activePanel === "publish" ? "Publish" : "Theme", " placeholder"] }), _jsx("p", { className: "mt-1 text-sm text-muted-foreground", children: "Reserved for the next Capture builder refinement pass." })] }) }));
1375
+ return null;
1376
+ }
1377
+ function SubmissionViewDetailsPanel({ name, error, isExisting, isDirty, onNameChange, }) {
1378
+ return (_jsxs("div", { className: "space-y-5 p-4", children: [_jsxs("div", { className: "space-y-2", children: [_jsx(Label, { htmlFor: "capture-submission-view-name", children: "View name" }), _jsx(Input, { id: "capture-submission-view-name", value: name, onChange: (event) => onNameChange(event.target.value), placeholder: "New submission view", "aria-invalid": Boolean(error) }), error ? (_jsx("p", { className: "text-xs font-medium text-destructive", children: error })) : (_jsx("p", { className: "text-xs text-muted-foreground", children: isExisting ? "Rename this saved view or save current filter, sort, and column changes." : "Create a named saved view from the current filter, sort, and column setup." }))] }), _jsxs("div", { className: "rounded-lg border border-border bg-muted/30 p-3 text-xs text-muted-foreground", children: [_jsx("p", { className: "font-medium text-foreground", children: "Current view state" }), _jsx("p", { className: "mt-1", children: isDirty ? "This view has unsaved workbench changes." : "Current workbench settings match the saved view." })] })] }));
1379
+ }
1380
+ function BuilderSubmissionsView({ submissions, fields, columns, filters, sort, onFiltersChange: _onFiltersChange, onSortChange, onOpenColumns, isLoading, isError, onRetry, }) {
1381
+ const navigate = useNavigate();
1382
+ const sl = useScopeLink();
1383
+ const filteredSubmissions = useMemo(() => filterSubmissions(submissions, filters).sort((a, b) => compareSubmissions(a, b, sort)), [filters, sort, submissions]);
1384
+ const pagination = useWorkbenchOffsetPagination({
1385
+ pageSize: 25,
1386
+ total: filteredSubmissions.length,
1387
+ resetKeys: [filters, sort, columns],
1388
+ });
1389
+ const pageSubmissions = filteredSubmissions.slice(pagination.offset, pagination.offset + pagination.pageSize);
1390
+ const tableColumns = useMemo(() => [
1391
+ ...columns,
1392
+ { fieldKey: "__spacer", label: "", visible: true, position: Number.MAX_SAFE_INTEGER - 1, unavailable: true },
1393
+ { fieldKey: "__manage", label: "", visible: true, position: Number.MAX_SAFE_INTEGER, unavailable: true },
1394
+ ], [columns]);
1395
+ const renderCell = (submission, column) => renderSubmissionCell(submission, column);
1396
+ const renderHeader = (column) => column.fieldKey === "__spacer" ? null : column.fieldKey === "__manage" ? (_jsx("button", { type: "button", onClick: (event) => { event.stopPropagation(); onOpenColumns(); }, className: "flex h-7 w-7 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-muted hover:text-foreground", "aria-label": "Add columns", title: "Add columns", children: _jsx(Plus, { className: "h-4 w-4" }) })) : column.label;
1397
+ return (_jsx("section", { className: "min-h-0 flex-1 overflow-auto", children: isError ? (_jsx(QueryErrorState, { title: "Could not load form submissions", description: "Responses for this form could not be loaded. Try again to refresh the submissions list.", onRetry: onRetry })) : isLoading ? (_jsx(BuilderSubmissionsSkeleton, {})) : submissions.length === 0 ? (_jsx(BuilderSubmissionsEmptyState, {})) : (_jsx(WorkbenchTable, { data: pageSubmissions, columns: tableColumns, getRowId: (submission) => submission.id, renderCell: renderCell, renderHeader: renderHeader, isColumnSortable: (column) => !column.fieldKey.startsWith("__") && fields.some((field) => field.key === column.fieldKey && field.sortable !== false), getColumnClassName: (column) => column.fieldKey === "submittedAt" ? "w-[220px]" : column.fieldKey === "__spacer" ? "w-full px-0" : column.fieldKey === "__manage" ? "w-10 px-3" : "w-[180px]", onRowClick: (submission) => void navigate({ to: sl("/capture/submissions/$id"), params: { id: submission.id } }), sorts: [sort], onSortChange: (nextSorts) => {
1398
+ const next = nextSorts[0];
1399
+ if (next && !next.field.startsWith("__"))
1400
+ onSortChange({ field: next.field, direction: next.direction });
1401
+ }, total: filteredSubmissions.length, page: pagination.page, pageSize: pagination.pageSize, onPageChange: pagination.setPage, className: "h-full" })) }));
224
1402
  }
225
- function StartPanel({ definitions, isLoading, search, selectedDefinition, activeDetailTab, onSelectDefinition, onCloseDetails, onDetailTabChange, onCreate, onOpen, }) {
1403
+ function BuilderSubmissionsSkeleton() {
1404
+ return (_jsxs("div", { className: "rounded-2xl border border-border bg-card shadow-sm", children: [_jsx("div", { className: "border-b border-border px-4 py-3", children: _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Skeleton, { className: "h-4 w-4 rounded" }), _jsx(Skeleton, { className: "h-4 w-32" })] }) }), _jsx("div", { className: "divide-y divide-border", children: Array.from({ length: 4 }).map((_, index) => (_jsxs("div", { className: "p-4", children: [_jsxs("div", { className: "flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between", children: [_jsx(Skeleton, { className: "h-4 w-48" }), _jsx(Skeleton, { className: "h-3 w-36" })] }), _jsx("div", { className: "mt-3 grid gap-2 sm:grid-cols-2 lg:grid-cols-3", children: Array.from({ length: 3 }).map((__, cell) => (_jsx(Skeleton, { className: "h-12 rounded-lg" }, cell))) })] }, index))) })] }));
1405
+ }
1406
+ function BuilderSubmissionsEmptyState() {
1407
+ return (_jsxs("div", { className: "flex min-h-[320px] flex-col items-center justify-center rounded-2xl border border-dashed border-border bg-muted/25 p-8 text-center", children: [_jsx(Inbox, { className: "mb-3 h-10 w-10 text-muted-foreground/60" }), _jsx("h3", { className: "text-sm font-semibold text-foreground", children: "No submissions yet" }), _jsx("p", { className: "mt-1 max-w-md text-sm text-muted-foreground", children: "Once this form is published and respondents submit answers, they will appear here for review." })] }));
1408
+ }
1409
+ function BuilderShareView({ publicationSlug, onCopy, }) {
1410
+ const publicPath = publicationSlug ? `/forms/${publicationSlug}` : null;
1411
+ const embedPath = publicationSlug ? `/forms/embed/${publicationSlug}` : null;
1412
+ const publicUrl = publicPath ? buildPublicUrl(publicPath) : null;
1413
+ const embedUrl = embedPath ? buildPublicUrl(embedPath) : null;
1414
+ const iframeSnippet = embedUrl ? `<iframe src="${embedUrl}" title="Capture form" style="width:100%;min-height:640px;border:0;"></iframe>` : null;
1415
+ if (!publicationSlug) {
1416
+ return (_jsx("section", { className: "min-h-0 flex-1 overflow-auto", children: _jsxs("div", { className: "flex min-h-[360px] flex-col items-center justify-center rounded-2xl border border-dashed border-border bg-muted/25 p-8 text-center", children: [_jsx(AlertCircle, { className: "mb-3 h-10 w-10 text-muted-foreground/60" }), _jsx("h3", { className: "text-sm font-semibold text-foreground", children: "Publish to enable sharing" }), _jsx("p", { className: "mt-1 max-w-md text-sm text-muted-foreground", children: "Public links and embed snippets are created after this form is published. Use Publish in the page header when the form is ready." })] }) }));
1417
+ }
1418
+ return (_jsx("section", { className: "min-h-0 flex-1 overflow-auto", children: _jsxs("div", { className: "grid gap-4 lg:grid-cols-2", children: [_jsxs("div", { className: "rounded-xl border border-border bg-card p-4 shadow-sm", children: [_jsxs("div", { className: "flex items-start justify-between gap-3", children: [_jsxs("div", { children: [_jsx("h3", { className: "text-sm font-semibold text-foreground", children: "Direct public link" }), _jsx("p", { className: "mt-1 text-xs text-muted-foreground", children: "Share this respondent-safe public form route." })] }), _jsxs("div", { className: "flex shrink-0 items-center gap-2", children: [publicUrl && (_jsxs(Button, { variant: "outline", size: "sm", type: "button", onClick: () => void onCopy(publicUrl, "Public link"), children: [_jsx(Copy, { className: "h-4 w-4" }), "Copy"] })), publicPath && (_jsx(Button, { variant: "outline", size: "icon", type: "button", asChild: true, "aria-label": "Open public form", children: _jsx("a", { href: publicPath, target: "_blank", rel: "noreferrer", children: _jsx(ExternalLink, { className: "h-4 w-4" }) }) }))] })] }), publicUrl && (_jsx("p", { className: "mt-3 break-all rounded-lg bg-muted/50 px-3 py-2 font-mono text-xs text-muted-foreground", children: publicUrl }))] }), _jsxs("div", { className: "rounded-xl border border-border bg-card p-4 shadow-sm", children: [_jsxs("div", { className: "flex items-start justify-between gap-3", children: [_jsxs("div", { children: [_jsx("h3", { className: "text-sm font-semibold text-foreground", children: "Iframe embed" }), _jsx("p", { className: "mt-1 text-xs text-muted-foreground", children: "Embed the public form in an external page with the current public embed route." })] }), iframeSnippet && (_jsxs(Button, { variant: "outline", size: "sm", type: "button", onClick: () => void onCopy(iframeSnippet, "Embed snippet"), children: [_jsx(Copy, { className: "h-4 w-4" }), "Copy"] }))] }), iframeSnippet && (_jsx("pre", { className: "mt-3 overflow-auto rounded-lg bg-muted/50 p-3 text-xs text-muted-foreground", children: _jsx("code", { children: iframeSnippet }) }))] }), _jsxs("div", { className: "rounded-xl border border-dashed border-border bg-muted/25 p-4", children: [_jsx("h3", { className: "text-sm font-semibold text-foreground", children: "Tag / script snippet" }), _jsx("p", { className: "mt-1 text-sm text-muted-foreground", children: "Not available yet. Capture currently exposes direct public and iframe routes only; tag-based embeds need a separate product protocol." })] }), _jsxs("div", { className: "rounded-xl border border-dashed border-border bg-muted/25 p-4", children: [_jsx("h3", { className: "text-sm font-semibold text-foreground", children: "Embed constraints" }), _jsx("p", { className: "mt-1 text-sm text-muted-foreground", children: "Allowed-origin controls and advanced completion behavior are not configured in this slice. Treat the iframe snippet as the current public embed mode." })] })] }) }));
1419
+ }
1420
+ function buildPublicUrl(path) {
1421
+ if (typeof window === "undefined")
1422
+ return path;
1423
+ return new URL(path, window.location.origin).toString();
1424
+ }
1425
+ const BUILT_IN_CAPTURE_ITEMS_WORKBENCH_VIEWS = [
1426
+ { id: "all", name: "All items", icon: _jsx(Inbox, { className: "h-4 w-4" }) },
1427
+ { id: "forms", name: "Forms", icon: _jsx(FileText, { className: "h-4 w-4" }) },
1428
+ { id: "quizzes", name: "Quizzes", icon: _jsx(Sparkles, { className: "h-4 w-4" }) },
1429
+ { id: "published", name: "Published", icon: _jsx(CheckCircle2, { className: "h-4 w-4" }) },
1430
+ { id: "drafts", name: "Drafts", icon: _jsx(FileText, { className: "h-4 w-4" }) },
1431
+ ];
1432
+ const CAPTURE_SUBMISSION_SYSTEM_FIELDS = [
1433
+ { key: "submittedAt", label: "Submitted", type: "date", group: "Submission", sortable: true, filterable: true, system: true },
1434
+ { key: "definitionName", label: "Form", type: "text", group: "Submission", sortable: true, filterable: true, system: true },
1435
+ { key: "actionStatus", label: "Action Status", type: "dropdown", group: "Submission", sortable: true, filterable: true, system: true, options: [{ value: "pending", label: "Pending" }, { value: "running", label: "Running" }, { value: "success", label: "Success" }, { value: "failed", label: "Failed" }] },
1436
+ { key: "contactId", label: "Contact", type: "text", group: "Submission", sortable: true, filterable: true, system: true },
1437
+ ];
1438
+ const DEFAULT_CAPTURE_SUBMISSION_COLUMNS = persistWorkbenchColumns([
1439
+ { fieldKey: "submittedAt", visible: true, position: 0 },
1440
+ { fieldKey: "actionStatus", visible: true, position: 1 },
1441
+ ]);
1442
+ const BUILT_IN_CAPTURE_SUBMISSION_WORKBENCH_VIEWS = [
1443
+ { id: "all", name: "All submissions", icon: _jsx(Inbox, { className: "h-4 w-4" }) },
1444
+ { id: "with-actions", name: "With actions", icon: _jsx(GitBranch, { className: "h-4 w-4" }) },
1445
+ ];
1446
+ const CAPTURE_ITEMS_FIELDS = [
1447
+ { key: "name", label: "Name", type: "text", group: "Capture Item", sortable: true, filterable: true, system: true },
1448
+ { key: "surface", label: "Type", type: "dropdown", group: "Capture Item", sortable: true, filterable: true, system: true, options: [{ value: "form", label: "Form" }, { value: "quiz", label: "Quiz" }] },
1449
+ { key: "status", label: "Status", type: "dropdown", group: "Capture Item", sortable: true, filterable: true, system: true, options: [{ value: "draft", label: "Draft" }, { value: "published", label: "Published" }] },
1450
+ { key: "tags", label: "Tags", type: "labels", group: "Capture Item", sortable: true, filterable: true, system: true },
1451
+ { key: "fieldCount", label: "Fields", type: "number", group: "Metrics", sortable: true, filterable: true, system: true },
1452
+ { key: "submissionCount", label: "Submissions", type: "number", group: "Metrics", sortable: true, filterable: true, system: true },
1453
+ { key: "updatedAt", label: "Last Modified", type: "date", group: "Timestamps", sortable: true, filterable: true, system: true },
1454
+ ];
1455
+ const DEFAULT_CAPTURE_ITEMS_COLUMNS = persistWorkbenchColumns([
1456
+ { fieldKey: "name", visible: true, position: 0 },
1457
+ { fieldKey: "surface", visible: true, position: 1 },
1458
+ { fieldKey: "status", visible: true, position: 2 },
1459
+ { fieldKey: "tags", visible: true, position: 3 },
1460
+ { fieldKey: "fieldCount", visible: true, position: 4 },
1461
+ { fieldKey: "submissionCount", visible: true, position: 5 },
1462
+ { fieldKey: "updatedAt", visible: true, position: 6 },
1463
+ ]);
1464
+ const CAPTURE_ITEMS_FILTER_OPERATORS = {
1465
+ text: ["contains", "does_not_contain", "is", "is_not", "starts_with", "ends_with", "is_empty", "is_not_empty"],
1466
+ dropdown: ["is", "is_not", "is_empty", "is_not_empty"],
1467
+ labels: ["contains", "does_not_contain", "is_empty", "is_not_empty"],
1468
+ number: ["is", "is_not", "greater_than", "less_than", "greater_than_or_equal", "less_than_or_equal", "is_empty", "is_not_empty"],
1469
+ date: ["is_before", "is_after", "is_on_or_before", "is_on_or_after", "is_empty", "is_not_empty"],
1470
+ };
1471
+ const CAPTURE_SUBMISSION_FILTER_OPERATORS = CAPTURE_ITEMS_FILTER_OPERATORS;
1472
+ function createSubmissionActionStatusFilter() {
1473
+ return {
1474
+ id: "with-actions",
1475
+ logic: "and",
1476
+ conditions: [{ id: "action-status-present", fieldKey: "actionStatus", operator: "is_not_empty" }],
1477
+ };
1478
+ }
1479
+ function buildSubmissionWorkbenchFields(fields) {
1480
+ return [
1481
+ ...CAPTURE_SUBMISSION_SYSTEM_FIELDS,
1482
+ ...fields
1483
+ .filter((field) => field.type !== "honeypot")
1484
+ .map((field) => ({
1485
+ key: field.key,
1486
+ label: field.label || field.key,
1487
+ type: field.type === "rating" ? "number" : field.type === "select" ? "dropdown" : "text",
1488
+ group: "Answers",
1489
+ sortable: true,
1490
+ filterable: true,
1491
+ system: false,
1492
+ options: field.options?.map((option) => ({ value: option, label: option })),
1493
+ })),
1494
+ ];
1495
+ }
1496
+ function getSubmissionFieldValue(submission, field) {
1497
+ if (field === "submittedAt")
1498
+ return new Date(submission.submittedAt).getTime();
1499
+ if (field === "definitionName")
1500
+ return submission.definitionName;
1501
+ if (field === "actionStatus")
1502
+ return submission.actionStatus ?? "";
1503
+ if (field === "contactId")
1504
+ return submission.contactId ?? "";
1505
+ const value = submission.answers[field];
1506
+ if (typeof value === "number")
1507
+ return value;
1508
+ if (Array.isArray(value))
1509
+ return value.join(", ");
1510
+ return String(value ?? "");
1511
+ }
1512
+ function compareSubmissions(a, b, sort) {
1513
+ const aValue = getSubmissionFieldValue(a, sort.field);
1514
+ const bValue = getSubmissionFieldValue(b, sort.field);
1515
+ const comparison = typeof aValue === "number" && typeof bValue === "number"
1516
+ ? aValue - bValue
1517
+ : String(aValue).localeCompare(String(bValue), undefined, { sensitivity: "base", numeric: true });
1518
+ return sort.direction === "asc" ? comparison : -comparison;
1519
+ }
1520
+ function filterSubmissions(submissions, filters) {
1521
+ if (!filters || filters.conditions.length === 0)
1522
+ return submissions;
1523
+ return submissions.filter((submission) => evaluateWorkbenchFilter((fieldKey) => getSubmissionFieldValue(submission, fieldKey), filters));
1524
+ }
1525
+ function evaluateWorkbenchFilter(resolveValue, filter) {
1526
+ const evaluate = (node) => {
1527
+ if ("conditions" in node) {
1528
+ const results = node.conditions.map(evaluate);
1529
+ return node.logic === "and" ? results.every(Boolean) : results.some(Boolean);
1530
+ }
1531
+ const value = resolveValue(node.fieldKey);
1532
+ const raw = String(value ?? "");
1533
+ const expected = String(node.value ?? "");
1534
+ const numericValue = Number(value);
1535
+ const numericExpected = Number(expected);
1536
+ if (node.operator === "is_empty")
1537
+ return raw.length === 0;
1538
+ if (node.operator === "is_not_empty")
1539
+ return raw.length > 0;
1540
+ if (node.operator === "is")
1541
+ return raw.toLowerCase() === expected.toLowerCase();
1542
+ if (node.operator === "is_not")
1543
+ return raw.toLowerCase() !== expected.toLowerCase();
1544
+ if (node.operator === "contains")
1545
+ return raw.toLowerCase().includes(expected.toLowerCase());
1546
+ if (node.operator === "does_not_contain")
1547
+ return !raw.toLowerCase().includes(expected.toLowerCase());
1548
+ if (node.operator === "starts_with")
1549
+ return raw.toLowerCase().startsWith(expected.toLowerCase());
1550
+ if (node.operator === "ends_with")
1551
+ return raw.toLowerCase().endsWith(expected.toLowerCase());
1552
+ if (node.operator === "greater_than")
1553
+ return numericValue > numericExpected;
1554
+ if (node.operator === "less_than")
1555
+ return numericValue < numericExpected;
1556
+ if (node.operator === "greater_than_or_equal")
1557
+ return numericValue >= numericExpected;
1558
+ if (node.operator === "less_than_or_equal")
1559
+ return numericValue <= numericExpected;
1560
+ if (node.operator === "is_before")
1561
+ return numericValue < new Date(expected).getTime();
1562
+ if (node.operator === "is_after")
1563
+ return numericValue > new Date(expected).getTime();
1564
+ if (node.operator === "is_on_or_before")
1565
+ return numericValue <= new Date(expected).getTime();
1566
+ if (node.operator === "is_on_or_after")
1567
+ return numericValue >= new Date(expected).getTime();
1568
+ return true;
1569
+ };
1570
+ const results = filter.conditions.map(evaluate);
1571
+ return filter.logic === "and" ? results.every(Boolean) : results.some(Boolean);
1572
+ }
1573
+ function renderSubmissionCell(submission, column) {
1574
+ if (column.fieldKey.startsWith("__"))
1575
+ return null;
1576
+ if (column.fieldKey === "submittedAt")
1577
+ return _jsx("span", { className: "block text-sm whitespace-nowrap text-muted-foreground", children: new Date(submission.submittedAt).toLocaleString() });
1578
+ if (column.fieldKey === "definitionName")
1579
+ return _jsx("span", { className: "text-sm font-medium text-foreground", children: submission.definitionName });
1580
+ if (column.fieldKey === "actionStatus")
1581
+ return _jsx("span", { className: "text-sm capitalize text-muted-foreground", children: submission.actionStatus ?? "—" });
1582
+ const value = getSubmissionFieldValue(submission, column.fieldKey);
1583
+ return _jsx("span", { className: "text-sm text-muted-foreground", children: String(value || "—") });
1584
+ }
1585
+ function matchesCaptureItemsView(definition, viewId) {
1586
+ if (viewId === "forms")
1587
+ return definition.surface === "form";
1588
+ if (viewId === "quizzes")
1589
+ return definition.surface === "quiz";
1590
+ if (viewId === "published")
1591
+ return definition.status === "published";
1592
+ if (viewId === "drafts")
1593
+ return definition.status === "draft";
1594
+ return true;
1595
+ }
1596
+ function getCaptureItemFieldValue(definition, field) {
1597
+ if (field === "tags")
1598
+ return (definition.tags ?? []).join(" ");
1599
+ if (field === "fieldCount")
1600
+ return definition.fieldCount;
1601
+ if (field === "submissionCount")
1602
+ return definition.submissionCount;
1603
+ if (field === "updatedAt")
1604
+ return new Date(definition.updatedAt ?? definition.createdAt ?? 0).getTime();
1605
+ return String(definition[field] ?? "");
1606
+ }
1607
+ function parseCaptureTagsValue(value) {
1608
+ if (!value)
1609
+ return [];
1610
+ try {
1611
+ const parsed = JSON.parse(value);
1612
+ if (Array.isArray(parsed))
1613
+ return normalizeCaptureTagValues(parsed.map(String));
1614
+ }
1615
+ catch {
1616
+ return normalizeCaptureTagValues(value.split(","));
1617
+ }
1618
+ return [];
1619
+ }
1620
+ function normalizeCaptureTagValues(tags) {
1621
+ const seen = new Set();
1622
+ const normalized = [];
1623
+ for (const tag of tags) {
1624
+ const value = tag.trim().replace(/\s+/g, " ").slice(0, 60);
1625
+ const key = value.toLowerCase();
1626
+ if (!value || seen.has(key))
1627
+ continue;
1628
+ seen.add(key);
1629
+ normalized.push(value);
1630
+ }
1631
+ return normalized;
1632
+ }
1633
+ function buildCaptureTagOptions(definitions) {
1634
+ const tags = normalizeCaptureTagValues(definitions.flatMap((definition) => definition.tags ?? []));
1635
+ return tags.map((tag) => ({ value: tag, label: tag, color: captureTagColor(tag) }));
1636
+ }
1637
+ function withCaptureTagOptions(fields, tagOptions) {
1638
+ return fields.map((field) => field.key === "tags" ? { ...field, options: tagOptions } : field);
1639
+ }
1640
+ function captureTagColor(tag) {
1641
+ const palette = ["#64748b", "#2563eb", "#7c3aed", "#db2777", "#dc2626", "#ea580c", "#16a34a", "#0891b2"];
1642
+ let hash = 0;
1643
+ for (const char of tag)
1644
+ hash = (hash * 31 + char.charCodeAt(0)) >>> 0;
1645
+ return palette[hash % palette.length];
1646
+ }
1647
+ function CaptureTagBadge({ tag }) {
1648
+ return (_jsx("span", { className: "inline-flex items-center rounded-md px-1.5 py-0.5 text-[10px] font-medium text-white", style: { backgroundColor: captureTagColor(tag) }, children: tag }));
1649
+ }
1650
+ function compareCaptureItems(a, b, sort) {
1651
+ const aValue = getCaptureItemFieldValue(a, sort.field);
1652
+ const bValue = getCaptureItemFieldValue(b, sort.field);
1653
+ const comparison = typeof aValue === "number" && typeof bValue === "number"
1654
+ ? aValue - bValue
1655
+ : String(aValue).localeCompare(String(bValue), undefined, { sensitivity: "base", numeric: true });
1656
+ return sort.direction === "asc" ? comparison : -comparison;
1657
+ }
1658
+ function evaluateCaptureItemsFilter(definition, filter) {
1659
+ if (!filter || filter.conditions.length === 0)
1660
+ return true;
1661
+ const evaluate = (node) => {
1662
+ if ("conditions" in node) {
1663
+ const results = node.conditions.map(evaluate);
1664
+ return node.logic === "and" ? results.every(Boolean) : results.some(Boolean);
1665
+ }
1666
+ const value = getCaptureItemFieldValue(definition, node.fieldKey);
1667
+ const raw = String(value ?? "");
1668
+ const expected = String(node.value ?? "");
1669
+ const numericValue = Number(value);
1670
+ const numericExpected = Number(expected);
1671
+ if (node.operator === "is_empty")
1672
+ return raw.length === 0;
1673
+ if (node.operator === "is_not_empty")
1674
+ return raw.length > 0;
1675
+ if (node.operator === "is")
1676
+ return raw.toLowerCase() === expected.toLowerCase();
1677
+ if (node.operator === "is_not")
1678
+ return raw.toLowerCase() !== expected.toLowerCase();
1679
+ if (node.operator === "contains")
1680
+ return raw.toLowerCase().includes(expected.toLowerCase());
1681
+ if (node.operator === "does_not_contain")
1682
+ return !raw.toLowerCase().includes(expected.toLowerCase());
1683
+ if (node.operator === "starts_with")
1684
+ return raw.toLowerCase().startsWith(expected.toLowerCase());
1685
+ if (node.operator === "ends_with")
1686
+ return raw.toLowerCase().endsWith(expected.toLowerCase());
1687
+ if (node.operator === "greater_than")
1688
+ return numericValue > numericExpected;
1689
+ if (node.operator === "less_than")
1690
+ return numericValue < numericExpected;
1691
+ if (node.operator === "greater_than_or_equal")
1692
+ return numericValue >= numericExpected;
1693
+ if (node.operator === "less_than_or_equal")
1694
+ return numericValue <= numericExpected;
1695
+ if (node.operator === "is_before")
1696
+ return numericValue < new Date(expected).getTime();
1697
+ if (node.operator === "is_after")
1698
+ return numericValue > new Date(expected).getTime();
1699
+ if (node.operator === "is_on_or_before")
1700
+ return numericValue <= new Date(expected).getTime();
1701
+ if (node.operator === "is_on_or_after")
1702
+ return numericValue >= new Date(expected).getTime();
1703
+ return true;
1704
+ };
1705
+ const results = filter.conditions.map(evaluate);
1706
+ return filter.logic === "and" ? results.every(Boolean) : results.some(Boolean);
1707
+ }
1708
+ function StartPanel({ definitions, isLoading, isError, onRetryDefinitions, search, selectedDefinition, activeDetailTab, activeWorkbenchViewId, activeWorkbenchPanel, onActiveWorkbenchPanelChange, filters, onFiltersChange, columns, onColumnsChange, sort, onSortChange, onSelectDefinition, onCloseDetails, onDetailTabChange, onCreate, isCreating, onOpen, onTagsChange, workflows, workflowsLoading, workflowsError, onRetryWorkflows, contactCustomFields, contactCustomFieldsLoading, contactCustomFieldsError, onRetryContactCustomFields, onActionConfigChange, isActionConfigSaving, }) {
226
1709
  const filteredDefinitions = useMemo(() => {
227
1710
  const query = search.trim().toLowerCase();
228
- if (!query)
229
- return definitions;
230
- return definitions.filter((definition) => [definition.name, definition.surface, definition.status, definition.slug]
231
- .some((value) => value.toLowerCase().includes(query)));
232
- }, [definitions, search]);
233
- const showEmptyState = !isLoading && definitions.length === 0;
234
- const showNoResults = !isLoading && definitions.length > 0 && filteredDefinitions.length === 0;
235
- return (_jsxs("div", { className: "flex min-h-0 flex-1 flex-col gap-4", children: [showEmptyState && (_jsxs("div", { className: "flex flex-col items-center justify-center rounded-2xl border border-border bg-card py-20 text-center shadow-sm", children: [_jsx(Inbox, { className: "mb-4 h-12 w-12 text-muted-foreground/40" }), _jsx("h3", { className: "mb-1 text-sm font-semibold text-foreground", children: "No capture items yet" }), _jsx("p", { className: "mb-4 text-sm text-muted-foreground", children: "Create your first form to get started." }), _jsxs(Button, { onClick: onCreate, children: [_jsx(Plus, { className: "h-4 w-4" }), "Create your first form"] })] })), showNoResults && (_jsxs("div", { className: "flex flex-col items-center justify-center rounded-2xl border border-border bg-card py-20 text-center shadow-sm", children: [_jsx(Search, { className: "mb-3 h-10 w-10 text-muted-foreground/40" }), _jsx("h3", { className: "mb-1 text-sm font-semibold text-foreground", children: "No capture items match your search" }), _jsx("p", { className: "text-sm text-muted-foreground", children: "Try adjusting your search." })] })), isLoading && (_jsx("div", { className: "rounded-xl border border-border bg-card p-4 text-sm text-muted-foreground shadow-sm", children: "Loading capture items\u2026" })), !isLoading && filteredDefinitions.length > 0 && (_jsxs("div", { className: "flex min-h-0 flex-1 items-stretch gap-4", children: [_jsxs("div", { className: "flex min-h-0 min-w-0 flex-1 flex-col overflow-hidden rounded-xl border border-border bg-card shadow-sm", children: [_jsx("div", { className: "hidden min-h-0 flex-1 overflow-auto sm:block", children: _jsxs("table", { className: "w-full", children: [_jsx("thead", { children: _jsxs("tr", { className: "border-b border-border", children: [_jsx("th", { className: "px-5 py-3 text-left text-xs font-semibold uppercase tracking-wider text-muted-foreground", children: "Name" }), _jsx("th", { className: "px-5 py-3 text-left text-xs font-semibold uppercase tracking-wider text-muted-foreground", children: "Type" }), _jsx("th", { className: "px-5 py-3 text-left text-xs font-semibold uppercase tracking-wider text-muted-foreground", children: "Status" }), _jsx("th", { className: "px-5 py-3 text-right text-xs font-semibold uppercase tracking-wider text-muted-foreground", children: "Fields" }), _jsx("th", { className: "px-5 py-3 text-right text-xs font-semibold uppercase tracking-wider text-muted-foreground", children: "Last Modified" })] }) }), _jsx("tbody", { className: "divide-y divide-border/50", children: filteredDefinitions.map((definition) => {
236
- const SurfaceIcon = definition.surface === "quiz" ? Sparkles : FileText;
237
- return (_jsxs("tr", { className: cn("cursor-pointer transition-colors hover:bg-muted/50", selectedDefinition?.id === definition.id && "bg-muted/60"), onClick: () => onSelectDefinition(definition), children: [_jsx("td", { className: "px-5 py-3.5", children: _jsxs("div", { className: "flex min-w-0 items-center gap-3", children: [_jsx("div", { className: "flex h-8 w-8 shrink-0 items-center justify-center rounded-lg bg-primary/10", children: _jsx(SurfaceIcon, { className: "h-4 w-4 text-primary" }) }), _jsxs("div", { className: "min-w-0", children: [_jsx("div", { className: "truncate text-sm font-medium text-foreground", children: definition.name }), _jsxs("div", { className: "truncate text-xs text-muted-foreground", children: ["/", definition.slug] })] })] }) }), _jsx("td", { className: "px-5 py-3.5 text-sm capitalize text-muted-foreground", children: definition.surface }), _jsx("td", { className: "px-5 py-3.5", children: _jsx(CaptureStatusBadge, { status: definition.status }) }), _jsx("td", { className: "px-5 py-3.5 text-right text-sm tabular-nums text-muted-foreground", children: definition.fieldCount }), _jsx("td", { className: "px-5 py-3.5 text-right text-sm whitespace-nowrap text-muted-foreground", children: formatRelativeTime(definition.updatedAt ?? definition.createdAt ?? new Date()) })] }, definition.id));
238
- }) })] }) }), _jsx("div", { className: "min-h-0 flex-1 divide-y divide-border/50 overflow-auto sm:hidden", children: filteredDefinitions.map((definition) => {
1711
+ return definitions
1712
+ .filter((definition) => matchesCaptureItemsView(definition, activeWorkbenchViewId))
1713
+ .filter((definition) => evaluateCaptureItemsFilter(definition, filters))
1714
+ .filter((definition) => {
1715
+ if (!query)
1716
+ return true;
1717
+ return [definition.name, definition.surface, definition.status, definition.slug]
1718
+ .some((value) => value.toLowerCase().includes(query));
1719
+ })
1720
+ .sort((a, b) => compareCaptureItems(a, b, sort));
1721
+ }, [activeWorkbenchViewId, definitions, filters, search, sort]);
1722
+ const pagination = useWorkbenchOffsetPagination({
1723
+ pageSize: 25,
1724
+ total: filteredDefinitions.length,
1725
+ resetKeys: [activeWorkbenchViewId, filters, search, sort],
1726
+ });
1727
+ const pageDefinitions = filteredDefinitions.slice(pagination.offset, pagination.offset + pagination.pageSize);
1728
+ const tagOptions = useMemo(() => buildCaptureTagOptions(definitions), [definitions]);
1729
+ const tagField = useMemo(() => ({ id: "tags", name: "Tags", type: "labels", options: tagOptions }), [tagOptions]);
1730
+ const [actionDraftConfig, setActionDraftConfig] = useState(selectedDefinition?.actionConfig ?? {});
1731
+ useEffect(() => {
1732
+ setActionDraftConfig(selectedDefinition?.actionConfig ?? {});
1733
+ }, [selectedDefinition?.id, selectedDefinition?.actionConfig]);
1734
+ const actionTabs = useMemo(() => buildCaptureListPanelTabs(actionDraftConfig), [actionDraftConfig]);
1735
+ useEffect(() => {
1736
+ if (!selectedDefinition)
1737
+ return;
1738
+ if (!actionTabs.some((tab) => tab.id === activeDetailTab))
1739
+ onDetailTabChange("actions");
1740
+ }, [activeDetailTab, actionTabs, onDetailTabChange, selectedDefinition]);
1741
+ const toggleLeadAction = useCallback((kind, enabled) => {
1742
+ setActionDraftConfig((current) => {
1743
+ const actionConfig = current[kind] ?? defaultActionConfigFor(kind);
1744
+ return { ...current, [kind]: { ...actionConfig, enabled } };
1745
+ });
1746
+ if (enabled)
1747
+ onDetailTabChange(kind);
1748
+ }, [onDetailTabChange]);
1749
+ const showEmptyState = !isLoading && !isError && definitions.length === 0;
1750
+ const showNoResults = !isLoading && !isError && definitions.length > 0 && filteredDefinitions.length === 0;
1751
+ const renderCell = (definition, column) => {
1752
+ const fieldKey = column.fieldKey;
1753
+ if (fieldKey === "name") {
1754
+ const SurfaceIcon = definition.surface === "quiz" ? Sparkles : FileText;
1755
+ return (_jsxs("div", { className: "flex min-w-0 items-center gap-3", children: [_jsx("div", { className: "flex h-8 w-8 shrink-0 items-center justify-center rounded-lg bg-primary/10", children: _jsx(SurfaceIcon, { className: "h-4 w-4 text-primary" }) }), _jsxs("div", { className: "min-w-0", children: [_jsx("div", { className: "truncate text-sm font-medium text-foreground", children: definition.name }), _jsxs("div", { className: "truncate text-xs text-muted-foreground", children: ["/", definition.slug] })] })] }));
1756
+ }
1757
+ if (fieldKey === "status")
1758
+ return _jsx(CaptureStatusBadge, { status: definition.status });
1759
+ if (fieldKey === "surface")
1760
+ return _jsx("span", { className: "text-sm capitalize text-muted-foreground", children: definition.surface });
1761
+ if (fieldKey === "tags") {
1762
+ return (_jsx("div", { className: "group/cell relative min-w-40", children: _jsx(InlineCellEditor, { field: tagField, value: JSON.stringify(definition.tags ?? []), onChange: (value) => onTagsChange(definition.id, parseCaptureTagsValue(value)), onCreateOption: (option) => onTagsChange(definition.id, [...(definition.tags ?? []), option.label]) }) }));
1763
+ }
1764
+ if (fieldKey === "fieldCount")
1765
+ return _jsx("span", { className: "block text-right text-sm tabular-nums text-muted-foreground", children: definition.fieldCount });
1766
+ if (fieldKey === "submissionCount")
1767
+ return _jsx("span", { className: "block text-right text-sm tabular-nums text-muted-foreground", children: definition.submissionCount });
1768
+ return _jsx("span", { className: "block text-right text-sm whitespace-nowrap text-muted-foreground", children: formatRelativeTime(definition.updatedAt ?? definition.createdAt ?? new Date()) });
1769
+ };
1770
+ return (_jsxs("div", { className: "flex min-h-0 flex-1 flex-col gap-4", children: [isError && (_jsx(QueryErrorState, { title: "Could not load capture items", description: "Capture Items could not be loaded. Try again to refresh the list.", onRetry: onRetryDefinitions })), showEmptyState && (_jsxs("div", { className: "flex flex-col items-center justify-center rounded-2xl border border-border bg-card py-20 text-center shadow-sm", children: [_jsx(Inbox, { className: "mb-4 h-12 w-12 text-muted-foreground/40" }), _jsx("h3", { className: "mb-1 text-sm font-semibold text-foreground", children: "No capture items yet" }), _jsx("p", { className: "mb-4 text-sm text-muted-foreground", children: "Create your first form to get started." }), _jsxs(Button, { onClick: onCreate, disabled: isCreating, children: [_jsx(Plus, { className: "h-4 w-4" }), isCreating ? "Creating…" : "Create your first form"] })] })), showNoResults && (_jsxs("div", { className: "flex flex-col items-center justify-center rounded-2xl border border-border bg-card py-20 text-center shadow-sm", children: [_jsx(Search, { className: "mb-3 h-10 w-10 text-muted-foreground/40" }), _jsx("h3", { className: "mb-1 text-sm font-semibold text-foreground", children: "No capture items match your search" }), _jsx("p", { className: "text-sm text-muted-foreground", children: "Try adjusting your search." })] })), isLoading && _jsx(CaptureItemsSkeleton, {}), !isLoading && !isError && filteredDefinitions.length > 0 && (_jsxs("div", { className: "flex min-h-0 flex-1 items-stretch gap-4", children: [_jsxs("div", { className: "min-h-0 min-w-0 flex-1 overflow-hidden", children: [_jsx("div", { className: "hidden h-full min-h-0 sm:block", children: _jsx(WorkbenchTable, { data: pageDefinitions, columns: columns, getRowId: (definition) => definition.id, renderCell: renderCell, getColumnClassName: (column) => ["fieldCount", "submissionCount", "updatedAt"].includes(column.fieldKey) ? "text-right" : undefined, onRowClick: onSelectDefinition, onRowDoubleClick: (definition) => onOpen(definition.id), sorts: [{ field: sort.field, direction: sort.direction }], onSortChange: (nextSorts) => {
1771
+ const next = nextSorts[0];
1772
+ if (next)
1773
+ onSortChange({ field: next.field, direction: next.direction });
1774
+ }, total: filteredDefinitions.length, page: pagination.page, pageSize: pagination.pageSize, onPageChange: pagination.setPage, className: "h-full" }) }), _jsx("div", { className: "min-h-0 flex-1 divide-y divide-border/50 overflow-auto rounded-xl border border-border bg-card shadow-sm sm:hidden", children: pageDefinitions.map((definition) => {
239
1775
  const SurfaceIcon = definition.surface === "quiz" ? Sparkles : FileText;
240
- return (_jsxs("button", { type: "button", onClick: () => onSelectDefinition(definition), className: "flex w-full items-center gap-3 px-4 py-3 text-left active:bg-muted/50", children: [_jsx("span", { className: "flex h-9 w-9 shrink-0 items-center justify-center rounded-lg bg-primary/10", children: _jsx(SurfaceIcon, { className: "h-4 w-4 text-primary" }) }), _jsxs("span", { className: "min-w-0 flex-1", children: [_jsx("span", { className: "block truncate text-sm font-medium text-foreground", children: definition.name }), _jsxs("span", { className: "block text-xs capitalize text-muted-foreground", children: [definition.surface, " \u00B7 ", definition.fieldCount, " fields \u00B7 ", definition.status] })] })] }, definition.id));
241
- }) })] }), _jsx(TabbedPanel, { open: selectedDefinition != null, onClose: onCloseDetails, tabs: CAPTURE_LIST_PANEL_TABS, activeTab: activeDetailTab, onTabChange: onDetailTabChange, width: "380px", inline: true, flush: true, className: "self-stretch rounded-xl border shadow-sm", header: selectedDefinition ? (_jsxs("div", { className: "border-b border-border px-4 py-3", children: [_jsx("h2", { className: "truncate text-sm font-semibold text-foreground", children: selectedDefinition.name }), _jsxs("p", { className: "mt-1 text-xs capitalize text-muted-foreground", children: [selectedDefinition.surface, " \u00B7 ", selectedDefinition.status] })] })) : undefined, footer: selectedDefinition ? (_jsx("div", { className: "flex items-center justify-end gap-2 border-t border-border px-4 py-3", children: _jsx(Button, { size: "sm", onClick: () => onOpen(selectedDefinition.id), children: "Open builder" }) })) : undefined, children: selectedDefinition ? (_jsx(CaptureListDetailPanel, { definition: selectedDefinition, activeTab: activeDetailTab })) : null })] }))] }));
1776
+ return (_jsxs("button", { type: "button", onClick: () => onSelectDefinition(definition), onDoubleClick: () => onOpen(definition.id), className: "flex w-full items-center gap-3 px-4 py-3 text-left active:bg-muted/50", children: [_jsx("span", { className: "flex h-9 w-9 shrink-0 items-center justify-center rounded-lg bg-primary/10", children: _jsx(SurfaceIcon, { className: "h-4 w-4 text-primary" }) }), _jsxs("span", { className: "min-w-0 flex-1", children: [_jsx("span", { className: "block truncate text-sm font-medium text-foreground", children: definition.name }), _jsxs("span", { className: "block text-xs capitalize text-muted-foreground", children: [definition.surface, " \u00B7 ", definition.fieldCount, " fields \u00B7 ", definition.submissionCount, " submissions \u00B7 ", definition.status] }), (definition.tags?.length ?? 0) > 0 && (_jsx("span", { className: "mt-1 flex flex-wrap gap-1", children: definition.tags?.slice(0, 3).map((tag) => _jsx(CaptureTagBadge, { tag: tag }, tag)) }))] })] }, definition.id));
1777
+ }) })] }), _jsx(TabbedPanel, { open: selectedDefinition != null, onClose: onCloseDetails, tabs: actionTabs, activeTab: activeDetailTab, onTabChange: onDetailTabChange, width: "456px", inline: true, flush: true, className: "self-stretch rounded-xl border shadow-sm", header: selectedDefinition ? (_jsxs("div", { className: "border-b border-border px-4 py-3", children: [_jsx("h2", { className: "truncate text-sm font-semibold text-foreground", children: selectedDefinition.name }), _jsxs("p", { className: "mt-1 text-xs capitalize text-muted-foreground", children: [selectedDefinition.surface, " \u00B7 ", selectedDefinition.status] })] })) : undefined, footer: selectedDefinition ? (_jsx("div", { className: "flex items-center justify-end gap-2 border-t border-border px-4 py-3", children: _jsx(Button, { size: "sm", onClick: () => onOpen(selectedDefinition.id), children: "Open builder" }) })) : undefined, children: selectedDefinition ? (_jsx(CaptureListDetailPanel, { definition: selectedDefinition, activeTab: activeDetailTab, actionConfig: actionDraftConfig, persistedActionConfig: selectedDefinition.actionConfig ?? {}, workflows: workflows, workflowsLoading: workflowsLoading, workflowsError: workflowsError, onRetryWorkflows: onRetryWorkflows, contactCustomFields: contactCustomFields, contactCustomFieldsLoading: contactCustomFieldsLoading, contactCustomFieldsError: contactCustomFieldsError, onRetryContactCustomFields: onRetryContactCustomFields, onActionConfigChange: setActionDraftConfig, onToggleLeadAction: toggleLeadAction, onSaveActionConfig: (actionConfig) => onActionConfigChange(selectedDefinition.id, actionConfig), isActionConfigSaving: isActionConfigSaving })) : null }), _jsx(TabbedPanel, { open: activeWorkbenchPanel != null, onClose: () => onActiveWorkbenchPanelChange(null), tabs: [
1778
+ { id: "filters", icon: Filter, label: "Filters" },
1779
+ { id: "columns", icon: Columns3, label: "Columns" },
1780
+ ], activeTab: activeWorkbenchPanel ?? "filters", onTabChange: (tabId) => onActiveWorkbenchPanelChange(tabId), width: "420px", header: (_jsxs("div", { className: "border-b border-border px-4 py-3", children: [_jsx("h2", { className: "text-sm font-semibold text-foreground", children: "Capture Items view" }), _jsx("p", { className: "mt-1 text-xs text-muted-foreground", children: "Adjust filters and visible columns for this workbench." })] })), children: activeWorkbenchPanel === "columns" ? (_jsx(WorkbenchColumnPanel, { columns: columns, onColumnsChange: onColumnsChange })) : (_jsx(WorkbenchFilterPanel, { filters: filters, onFiltersChange: onFiltersChange, fields: fieldsToFilterFields(withCaptureTagOptions(CAPTURE_ITEMS_FIELDS, tagOptions)), operatorsByFieldType: CAPTURE_ITEMS_FILTER_OPERATORS, resultCount: filteredDefinitions.length })) })] }))] }));
242
1781
  }
243
- function CaptureListDetailPanel({ definition, activeTab, }) {
1782
+ function CaptureListDetailPanel({ definition, activeTab, actionConfig, persistedActionConfig, workflows, workflowsLoading, workflowsError, onRetryWorkflows, contactCustomFields, contactCustomFieldsLoading, contactCustomFieldsError, onRetryContactCustomFields, onActionConfigChange, onToggleLeadAction, onSaveActionConfig, isActionConfigSaving, }) {
244
1783
  if (activeTab === "overview") {
245
- return (_jsxs("div", { className: "space-y-4 p-4", children: [_jsxs("div", { className: "rounded-xl border border-border bg-background p-3", children: [_jsx("h3", { className: "text-xs font-semibold uppercase tracking-wide text-muted-foreground", children: "Summary" }), _jsxs("dl", { className: "mt-3 grid gap-3 text-sm", children: [_jsxs("div", { className: "flex items-center justify-between gap-3", children: [_jsx("dt", { className: "text-muted-foreground", children: "Type" }), _jsx("dd", { className: "font-medium capitalize text-foreground", children: definition.surface })] }), _jsxs("div", { className: "flex items-center justify-between gap-3", children: [_jsx("dt", { className: "text-muted-foreground", children: "Status" }), _jsx("dd", { children: _jsx(CaptureStatusBadge, { status: definition.status }) })] }), _jsxs("div", { className: "flex items-center justify-between gap-3", children: [_jsx("dt", { className: "text-muted-foreground", children: "Fields" }), _jsx("dd", { className: "font-medium tabular-nums text-foreground", children: definition.fieldCount })] }), _jsxs("div", { className: "flex items-center justify-between gap-3", children: [_jsx("dt", { className: "text-muted-foreground", children: "Last modified" }), _jsx("dd", { className: "font-medium text-foreground", children: formatRelativeTime(definition.updatedAt ?? definition.createdAt ?? new Date()) })] })] })] }), _jsxs("div", { className: "rounded-xl border border-border bg-background p-3", children: [_jsx("h3", { className: "text-xs font-semibold uppercase tracking-wide text-muted-foreground", children: "Slug" }), _jsxs("p", { className: "mt-2 break-all rounded-lg bg-muted/50 px-2 py-1.5 font-mono text-xs text-muted-foreground", children: ["/", definition.slug] })] })] }));
1784
+ return (_jsxs("div", { className: "space-y-4 p-4", children: [_jsxs("div", { className: "rounded-xl border border-border bg-background p-3", children: [_jsx("h3", { className: "text-xs font-semibold uppercase tracking-wide text-muted-foreground", children: "Summary" }), _jsxs("dl", { className: "mt-3 grid gap-3 text-sm", children: [_jsxs("div", { className: "flex items-center justify-between gap-3", children: [_jsx("dt", { className: "text-muted-foreground", children: "Type" }), _jsx("dd", { className: "font-medium capitalize text-foreground", children: definition.surface })] }), _jsxs("div", { className: "flex items-center justify-between gap-3", children: [_jsx("dt", { className: "text-muted-foreground", children: "Status" }), _jsx("dd", { children: _jsx(CaptureStatusBadge, { status: definition.status }) })] }), _jsxs("div", { className: "flex items-center justify-between gap-3", children: [_jsx("dt", { className: "text-muted-foreground", children: "Submissions" }), _jsx("dd", { className: "font-medium tabular-nums text-foreground", children: definition.submissionCount })] }), _jsxs("div", { className: "flex items-center justify-between gap-3", children: [_jsx("dt", { className: "text-muted-foreground", children: "Fields" }), _jsx("dd", { className: "font-medium tabular-nums text-foreground", children: definition.fieldCount })] }), _jsxs("div", { className: "flex items-center justify-between gap-3", children: [_jsx("dt", { className: "text-muted-foreground", children: "Last modified" }), _jsx("dd", { className: "font-medium text-foreground", children: formatRelativeTime(definition.updatedAt ?? definition.createdAt ?? new Date()) })] })] })] }), _jsxs("div", { className: "rounded-xl border border-border bg-background p-3", children: [_jsx("h3", { className: "text-xs font-semibold uppercase tracking-wide text-muted-foreground", children: "Slug" }), _jsxs("p", { className: "mt-2 break-all rounded-lg bg-muted/50 px-2 py-1.5 font-mono text-xs text-muted-foreground", children: ["/", definition.slug] })] })] }));
246
1785
  }
247
1786
  if (activeTab === "publish") {
248
1787
  return (_jsx("div", { className: "space-y-4 p-4", children: _jsxs("div", { className: "rounded-xl border border-border bg-background p-3", children: [_jsx("h3", { className: "text-xs font-semibold uppercase tracking-wide text-muted-foreground", children: "Publication" }), definition.publicationSlug ? (_jsxs("p", { className: "mt-2 break-all text-sm text-foreground", children: ["/", definition.surface === "quiz" ? "quizzes" : "forms", "/", definition.publicationSlug] })) : (_jsx("p", { className: "mt-2 text-sm text-muted-foreground", children: "This capture item has not been published yet." }))] }) }));
@@ -250,19 +1789,54 @@ function CaptureListDetailPanel({ definition, activeTab, }) {
250
1789
  if (activeTab === "submissions") {
251
1790
  return (_jsx("div", { className: "p-4", children: _jsxs("div", { className: "rounded-xl border border-dashed border-border bg-muted/25 p-6 text-center", children: [_jsx(Inbox, { className: "mx-auto mb-3 h-8 w-8 text-muted-foreground" }), _jsx("h3", { className: "text-sm font-semibold text-foreground", children: "Submission rollup" }), _jsx("p", { className: "mt-1 text-sm text-muted-foreground", children: "Submission counts and recent responses will live here once the list endpoint returns per-item metrics." })] }) }));
252
1791
  }
253
- return (_jsx("div", { className: "p-4", children: _jsxs("div", { className: "rounded-xl border border-dashed border-border bg-muted/25 p-6 text-center", children: [_jsx(GitBranch, { className: "mx-auto mb-3 h-8 w-8 text-muted-foreground" }), _jsx("h3", { className: "text-sm font-semibold text-foreground", children: "Actions overview" }), _jsx("p", { className: "mt-1 text-sm text-muted-foreground", children: "Action health, workflow hooks, and destination status will be summarized here." })] }) }));
1792
+ if (activeTab === "actions") {
1793
+ return (_jsx(LeadCaptureActionsSection, { config: actionConfig, persistedConfig: persistedActionConfig, onToggle: onToggleLeadAction, onReset: () => onActionConfigChange(persistedActionConfig), onSave: () => onSaveActionConfig(actionConfig), isSaving: isActionConfigSaving }));
1794
+ }
1795
+ if (isLeadActionKind(activeTab)) {
1796
+ return (_jsxs("div", { className: "space-y-4 p-4", children: [_jsx(ActionsPanel, { fields: definition.fields ?? [], config: actionConfig, workflows: workflows, workflowsLoading: workflowsLoading, workflowsError: workflowsError, onRetryWorkflows: onRetryWorkflows, contactCustomFields: contactCustomFields, contactCustomFieldsLoading: contactCustomFieldsLoading, contactCustomFieldsError: contactCustomFieldsError, onRetryContactCustomFields: onRetryContactCustomFields, onChange: onActionConfigChange, visibleActions: [activeTab], showSectionSwitches: false, compact: true }), _jsx(ActionConfigFooter, { isDirty: isActionConfigDirty(actionConfig, persistedActionConfig), isSaving: isActionConfigSaving, onReset: () => onActionConfigChange(persistedActionConfig), onSave: () => onSaveActionConfig(actionConfig) })] }));
1797
+ }
1798
+ return null;
1799
+ }
1800
+ function QueryErrorState({ title, description, onRetry, }) {
1801
+ return (_jsxs("div", { className: "flex min-h-[320px] flex-col items-center justify-center rounded-2xl border border-border bg-card p-8 text-center shadow-sm", children: [_jsx(AlertCircle, { className: "mb-3 h-10 w-10 text-destructive" }), _jsx("h3", { className: "text-sm font-semibold text-foreground", children: title }), _jsx("p", { className: "mt-1 max-w-md text-sm text-muted-foreground", children: description }), _jsx(Button, { type: "button", variant: "outline", size: "sm", className: "mt-4", onClick: onRetry, children: "Try again" })] }));
1802
+ }
1803
+ function CaptureItemsSkeleton() {
1804
+ return (_jsxs("div", { className: "flex min-h-0 flex-1 items-stretch gap-4", children: [_jsxs("div", { className: "flex min-h-0 min-w-0 flex-1 flex-col overflow-hidden rounded-xl border border-border bg-card shadow-sm", children: [_jsx("div", { className: "border-b border-border px-5 py-3", children: _jsx("div", { className: "grid grid-cols-[minmax(0,1fr)_120px_120px_80px_140px] gap-4", children: Array.from({ length: 5 }).map((_, index) => _jsx(Skeleton, { className: "h-4" }, index)) }) }), _jsx("div", { className: "divide-y divide-border/50", children: Array.from({ length: 6 }).map((_, row) => (_jsxs("div", { className: "grid grid-cols-[minmax(0,1fr)_120px_120px_80px_140px] items-center gap-4 px-5 py-3.5", children: [_jsxs("div", { className: "flex items-center gap-3", children: [_jsx(Skeleton, { className: "h-8 w-8 rounded-lg" }), _jsxs("div", { className: "min-w-0 flex-1 space-y-2", children: [_jsx(Skeleton, { className: "h-4 w-2/3" }), _jsx(Skeleton, { className: "h-3 w-1/3" })] })] }), _jsx(Skeleton, { className: "h-4" }), _jsx(Skeleton, { className: "h-5 w-20 rounded-full" }), _jsx(Skeleton, { className: "h-4 w-8 justify-self-end" }), _jsx(Skeleton, { className: "h-4" })] }, row))) })] }), _jsx(Skeleton, { className: "hidden w-[380px] rounded-xl lg:block" })] }));
1805
+ }
1806
+ function BuilderWorkspaceSkeleton() {
1807
+ return (_jsxs("div", { className: "flex min-h-0 flex-1 flex-col gap-4", children: [_jsxs("section", { className: "rounded-2xl border border-border bg-card p-4 shadow-sm", children: [_jsx(Skeleton, { className: "h-4 w-24" }), _jsx(Skeleton, { className: "mt-2 h-9 max-w-xl" })] }), _jsxs("div", { className: "flex min-h-0 flex-1 gap-4", children: [_jsxs("section", { className: "flex min-h-0 min-w-0 flex-1 flex-col rounded-2xl border border-border bg-card p-4 shadow-sm", children: [_jsxs("div", { className: "mb-4 flex items-center justify-between", children: [_jsxs("div", { className: "space-y-2", children: [_jsx(Skeleton, { className: "h-4 w-40" }), _jsx(Skeleton, { className: "h-3 w-72" })] }), _jsx(Skeleton, { className: "h-6 w-16 rounded-full" })] }), _jsx("div", { className: "flex flex-1 flex-col gap-3 rounded-2xl border border-dashed border-border bg-muted/25 p-4", children: Array.from({ length: 4 }).map((_, index) => _jsx(Skeleton, { className: "h-20 rounded-xl" }, index)) })] }), _jsx(Skeleton, { className: "hidden w-[380px] rounded-2xl lg:block" })] })] }));
254
1808
  }
255
1809
  function CaptureStatusBadge({ status }) {
256
1810
  const isPublished = status === "published";
257
1811
  return (_jsx("span", { className: cn("inline-block rounded-full px-1.5 py-0.5 text-[10px] font-medium capitalize", isPublished ? "bg-success/10 text-success-foreground" : "bg-muted text-muted-foreground"), children: status }));
258
1812
  }
259
- function ActionsPanel({ fields, config, workflows, onChange, }) {
1813
+ function isLeadActionKind(value) {
1814
+ return value === "contacts" || value === "email" || value === "workflow";
1815
+ }
1816
+ function isActionConfigDirty(current, saved) {
1817
+ return JSON.stringify(current) !== JSON.stringify(saved);
1818
+ }
1819
+ function LeadCaptureActionsSection({ config, persistedConfig, onToggle, onReset, onSave, isSaving, }) {
1820
+ return (_jsxs("div", { className: "space-y-4 p-4", children: [_jsxs("section", { className: "space-y-3 rounded-xl border border-border bg-background p-3", children: [_jsxs("div", { children: [_jsx("h3", { className: "text-sm font-semibold text-foreground", children: "Lead capture actions" }), _jsx("p", { className: "mt-1 text-xs text-muted-foreground", children: "Toggle the actions this form should run after each submission. Active actions get their own configuration tabs." })] }), _jsx("div", { className: "space-y-2", children: CAPTURE_LEAD_ACTIONS.map((action) => {
1821
+ const Icon = action.icon;
1822
+ const enabled = Boolean(config[action.id]?.enabled);
1823
+ return (_jsxs("div", { className: "flex items-start justify-between gap-3 rounded-lg border border-border bg-muted/20 p-3", children: [_jsxs("div", { className: "flex min-w-0 gap-3", children: [_jsx("span", { className: "mt-0.5 flex h-8 w-8 shrink-0 items-center justify-center rounded-lg bg-primary/10 text-primary", children: _jsx(Icon, { className: "h-4 w-4" }) }), _jsxs("div", { className: "min-w-0", children: [_jsx("p", { className: "text-sm font-medium text-foreground", children: action.label }), _jsx("p", { className: "mt-0.5 text-xs text-muted-foreground", children: action.description })] })] }), _jsx(Switch, { checked: enabled, onCheckedChange: (checked) => onToggle(action.id, checked), disabled: isSaving })] }, action.id));
1824
+ }) })] }), _jsx(ActionConfigFooter, { isDirty: isActionConfigDirty(config, persistedConfig), isSaving: isSaving, onReset: onReset, onSave: onSave })] }));
1825
+ }
1826
+ function ActionConfigFooter({ isDirty, isSaving, onReset, onSave, }) {
1827
+ return (_jsxs("div", { className: "flex items-center justify-end gap-2 border-t border-border pt-3", children: [_jsx(Button, { type: "button", variant: "outline", size: "sm", onClick: onReset, disabled: !isDirty || isSaving, children: "Reset" }), _jsxs(Button, { type: "button", size: "sm", onClick: onSave, disabled: !isDirty || isSaving, children: [_jsx(Save, { className: "h-4 w-4" }), isSaving ? "Saving…" : "Save actions"] })] }));
1828
+ }
1829
+ function defaultActionConfigFor(kind) {
1830
+ if (kind === "contacts")
1831
+ return { enabled: false, customFieldMappings: {} };
1832
+ if (kind === "email")
1833
+ return { enabled: false, subjectTemplate: "New Capture submission", bodyTemplate: "A new Capture submission was received.\n\n{{answers}}" };
1834
+ return { enabled: false, execute: true };
1835
+ }
1836
+ function ActionsPanel({ fields, config, workflows, workflowsLoading, workflowsError, onRetryWorkflows, contactCustomFields, contactCustomFieldsLoading, contactCustomFieldsError, onRetryContactCustomFields, onChange, visibleActions, showSectionSwitches = true, compact = false, }) {
260
1837
  const contacts = config.contacts ?? { enabled: false, customFieldMappings: {} };
261
1838
  const email = config.email ?? { enabled: false, subjectTemplate: "New Capture submission", bodyTemplate: "A new Capture submission was received.\n\n{{answers}}" };
262
1839
  const workflow = config.workflow ?? { enabled: false, execute: true };
263
- const customMappingsText = Object.entries(contacts.customFieldMappings ?? {})
264
- .map(([contactField, answerField]) => `${contactField}=${answerField}`)
265
- .join("\n");
266
1840
  const patchContacts = (patch) => {
267
1841
  onChange({ ...config, contacts: { ...contacts, ...patch } });
268
1842
  };
@@ -280,27 +1854,179 @@ function ActionsPanel({ fields, config, workflows, onChange, }) {
280
1854
  answers: Object.fromEntries(fields.map((field) => [field.key, `<${field.label}>`])),
281
1855
  },
282
1856
  };
283
- return (_jsxs("div", { className: "space-y-4", children: [_jsxs("section", { className: "rounded-2xl border border-border bg-card p-4 shadow-sm", children: [_jsxs("div", { className: "flex items-start justify-between gap-4", children: [_jsxs("div", { children: [_jsx("h3", { className: "text-sm font-semibold text-foreground", children: "Contacts create/update action" }), _jsx("p", { className: "mt-1 text-sm text-muted-foreground", children: "After a public submission is stored, map answers into Contacts. Failures are recorded as action runs without deleting the canonical submission." })] }), _jsx(Switch, { checked: contacts.enabled, onCheckedChange: (enabled) => patchContacts({ enabled }) })] }), _jsxs("div", { className: "mt-4 grid gap-4 md:grid-cols-3", children: [_jsx(FieldSelect, { label: "Name answer", value: contacts.nameFieldKey ?? "", fields: fields, onChange: (nameFieldKey) => patchContacts({ nameFieldKey }) }), _jsx(FieldSelect, { label: "Email answer", value: contacts.emailFieldKey ?? "", fields: fields, onChange: (emailFieldKey) => patchContacts({ emailFieldKey }) }), _jsx(FieldSelect, { label: "Phone answer", value: contacts.phoneFieldKey ?? "", fields: fields, onChange: (phoneFieldKey) => patchContacts({ phoneFieldKey }) })] }), _jsxs("div", { className: "mt-4 space-y-2", children: [_jsx(Label, { htmlFor: "contact-custom-field-mappings", children: "Custom field mappings" }), _jsx(Textarea, { id: "contact-custom-field-mappings", placeholder: "contact_custom_slug=capture_field_key\nlead_source=source_field", value: customMappingsText, onChange: (event) => {
284
- const customFieldMappings = {};
285
- for (const line of event.target.value.split("\n")) {
286
- const [contactField, answerField] = line.split("=").map((part) => part?.trim());
287
- if (contactField && answerField)
288
- customFieldMappings[contactField] = answerField;
289
- }
290
- patchContacts({ customFieldMappings });
291
- } }), _jsx("p", { className: "text-xs text-muted-foreground", children: "Use one mapping per line: Contacts custom field slug/key on the left, Capture field key on the right. Quiz virtual keys include outcome.label, outcome.key, outcome.bucket, and outcome.score." })] })] }), _jsxs("section", { className: "rounded-2xl border border-border bg-card p-4 shadow-sm", children: [_jsxs("div", { className: "flex items-start justify-between gap-4", children: [_jsxs("div", { children: [_jsx("h3", { className: "text-sm font-semibold text-foreground", children: "Email action" }), _jsx("p", { className: "mt-1 text-sm text-muted-foreground", children: "Send an operator email, respondent email, or both after the canonical submission is stored. Templates can include outcomeLabel, outcomeBucket, and outcomeScore." })] }), _jsx(Switch, { checked: email.enabled, onCheckedChange: (enabled) => patchEmail({ enabled }) })] }), _jsxs("div", { className: "mt-4 grid gap-4 md:grid-cols-2", children: [_jsxs("div", { className: "space-y-2", children: [_jsx(Label, { htmlFor: "capture-email-from", children: "From override" }), _jsx(Input, { id: "capture-email-from", placeholder: "Forms <forms@example.com>", value: email.from ?? "", onChange: (event) => patchEmail({ from: event.target.value || undefined }) })] }), _jsxs("div", { className: "space-y-2", children: [_jsx(Label, { htmlFor: "capture-email-internal", children: "Operator recipient" }), _jsx(Input, { id: "capture-email-internal", type: "email", placeholder: "ops@example.com", value: email.internalRecipientEmail ?? "", onChange: (event) => patchEmail({ internalRecipientEmail: event.target.value || undefined }) })] }), _jsx(FieldSelect, { label: "Respondent email answer", value: email.respondentEmailFieldKey ?? "", fields: fields, onChange: (respondentEmailFieldKey) => patchEmail({ respondentEmailFieldKey }) }), _jsxs("div", { className: "space-y-2", children: [_jsx(Label, { htmlFor: "capture-email-subject", children: "Subject template" }), _jsx(Input, { id: "capture-email-subject", value: email.subjectTemplate ?? "", onChange: (event) => patchEmail({ subjectTemplate: event.target.value || undefined }) })] })] }), _jsxs("div", { className: "mt-4 space-y-2", children: [_jsx(Label, { htmlFor: "capture-email-body", children: "Body template" }), _jsx(Textarea, { id: "capture-email-body", value: email.bodyTemplate ?? "", onChange: (event) => patchEmail({ bodyTemplate: event.target.value || undefined }) }), _jsxs("p", { className: "text-xs text-muted-foreground", children: ["Template tokens can reference field keys like ", "{{email_ab12}}", "; ", "{{answers}}", " inserts all answers."] })] })] }), _jsxs("section", { className: "rounded-2xl border border-border bg-card p-4 shadow-sm", children: [_jsxs("div", { className: "flex items-start justify-between gap-4", children: [_jsxs("div", { children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(GitBranch, { className: "h-4 w-4 text-primary" }), _jsx("h3", { className: "text-sm font-semibold text-foreground", children: "Workflow trigger action" })] }), _jsx("p", { className: "mt-1 text-sm text-muted-foreground", children: "Start a current-scope workflow after submission. API trigger-compatible workflows receive answers plus quiz outcome summary." })] }), _jsx(Switch, { checked: workflow.enabled, onCheckedChange: (enabled) => patchWorkflow({ enabled }) })] }), _jsxs("div", { className: "mt-4 grid gap-4 md:grid-cols-[minmax(0,1fr)_220px]", children: [_jsxs("div", { className: "space-y-2", children: [_jsx(Label, { children: "Workflow definition" }), _jsxs("select", { className: "h-9 w-full rounded-md border border-input bg-background px-3 py-1 text-sm shadow-xs outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50", value: workflow.workflowDefinitionId ?? "", onChange: (event) => {
292
- const next = workflows.find((candidate) => candidate.id === event.target.value);
293
- patchWorkflow({
294
- workflowDefinitionId: next?.id,
295
- triggerNodeId: next?.apiTriggerNodeId,
296
- });
297
- }, children: [_jsx("option", { value: "", children: "Select workflow\u2026" }), workflows.map((candidate) => (_jsxs("option", { value: candidate.id, children: [candidate.name, " \u00B7 ", candidate.compatible ? "API trigger" : "not API-ready"] }, candidate.id)))] })] }), _jsxs("div", { className: "flex items-center justify-between gap-3 rounded-lg border border-border p-3", children: [_jsx("span", { className: "text-sm text-muted-foreground", children: "Execute immediately" }), _jsx(Switch, { checked: workflow.execute ?? true, onCheckedChange: (execute) => patchWorkflow({ execute }) })] })] }), _jsxs("div", { className: "mt-4 grid gap-4 lg:grid-cols-2", children: [_jsxs("div", { className: "rounded-xl border border-border bg-background p-3", children: [_jsx("h4", { className: "text-xs font-semibold uppercase tracking-wide text-muted-foreground", children: "Compatibility" }), _jsx("p", { className: cn("mt-2 text-sm", selectedWorkflow?.compatible ? "text-foreground" : "text-destructive"), children: selectedWorkflow?.summary ?? "Select a workflow to inspect trigger compatibility." }), selectedWorkflow && (_jsxs("p", { className: "mt-1 text-xs text-muted-foreground", children: ["Version ", selectedWorkflow.currentVersion, " \u00B7 Status ", selectedWorkflow.status, " \u00B7 Triggers ", selectedWorkflow.triggerTypes.join(", ") || "none"] }))] }), _jsxs("div", { className: "rounded-xl border border-border bg-muted/30 p-3", children: [_jsx("h4", { className: "text-xs font-semibold uppercase tracking-wide text-muted-foreground", children: "Payload preview" }), _jsx("pre", { className: "mt-2 max-h-48 overflow-auto rounded-lg bg-background p-2 text-xs text-muted-foreground", children: JSON.stringify(workflowPreview, null, 2) })] })] })] })] }));
1857
+ const visible = new Set(visibleActions ?? ["contacts", "email", "workflow"]);
1858
+ const sectionClassName = compact ? "rounded-xl border border-border bg-background p-3" : "rounded-2xl border border-border bg-card p-4 shadow-sm";
1859
+ const gridClassName = compact ? "mt-4 grid gap-4" : "mt-4 grid gap-4 md:grid-cols-3";
1860
+ const twoColumnGridClassName = compact ? "mt-4 grid gap-4" : "mt-4 grid gap-4 md:grid-cols-2";
1861
+ return (_jsxs("div", { className: "space-y-4", children: [visible.has("contacts") && _jsxs("section", { className: sectionClassName, children: [_jsxs("div", { className: "flex items-start justify-between gap-4", children: [_jsxs("div", { children: [_jsx("h3", { className: "text-sm font-semibold text-foreground", children: "Contacts create/update action" }), _jsx("p", { className: "mt-1 text-sm text-muted-foreground", children: "After a public submission is stored, map answers into Contacts. Failures are recorded as action runs without deleting the canonical submission." })] }), showSectionSwitches && _jsx(Switch, { checked: contacts.enabled, onCheckedChange: (enabled) => patchContacts({ enabled }) })] }), _jsxs("div", { className: gridClassName, children: [_jsx(FieldSelect, { label: "Name answer", value: contacts.nameFieldKey ?? "", fields: fields, onChange: (nameFieldKey) => patchContacts({ nameFieldKey }) }), _jsx(FieldSelect, { label: "Email answer", value: contacts.emailFieldKey ?? "", fields: fields, onChange: (emailFieldKey) => patchContacts({ emailFieldKey }) }), _jsx(FieldSelect, { label: "Phone answer", value: contacts.phoneFieldKey ?? "", fields: fields, onChange: (phoneFieldKey) => patchContacts({ phoneFieldKey }) })] }), _jsx(CustomFieldMappingsEditor, { fields: fields, contactFields: contactCustomFields, isLoading: contactCustomFieldsLoading, isError: contactCustomFieldsError, mappings: contacts.customFieldMappings ?? {}, onRetry: onRetryContactCustomFields, onChange: (customFieldMappings) => patchContacts({ customFieldMappings }) })] }), visible.has("email") && _jsxs("section", { className: sectionClassName, children: [_jsxs("div", { className: "flex items-start justify-between gap-4", children: [_jsxs("div", { children: [_jsx("h3", { className: "text-sm font-semibold text-foreground", children: "Email action" }), _jsx("p", { className: "mt-1 text-sm text-muted-foreground", children: "Send an operator email, respondent email, or both after the canonical submission is stored. Templates can include outcomeLabel, outcomeBucket, and outcomeScore." })] }), showSectionSwitches && _jsx(Switch, { checked: email.enabled, onCheckedChange: (enabled) => patchEmail({ enabled }) })] }), _jsxs("div", { className: twoColumnGridClassName, children: [_jsxs("div", { className: "space-y-2", children: [_jsx(Label, { htmlFor: "capture-email-from", children: "From override" }), _jsx(Input, { id: "capture-email-from", placeholder: "Forms <forms@example.com>", value: email.from ?? "", onChange: (event) => patchEmail({ from: event.target.value || undefined }) })] }), _jsxs("div", { className: "space-y-2", children: [_jsx(Label, { htmlFor: "capture-email-internal", children: "Operator recipient" }), _jsx(Input, { id: "capture-email-internal", type: "email", placeholder: "ops@example.com", value: email.internalRecipientEmail ?? "", onChange: (event) => patchEmail({ internalRecipientEmail: event.target.value || undefined }) })] }), _jsx(FieldSelect, { label: "Respondent email answer", value: email.respondentEmailFieldKey ?? "", fields: fields, onChange: (respondentEmailFieldKey) => patchEmail({ respondentEmailFieldKey }) }), _jsxs("div", { className: "space-y-2", children: [_jsx(Label, { htmlFor: "capture-email-subject", children: "Subject template" }), _jsx(Input, { id: "capture-email-subject", value: email.subjectTemplate ?? "", onChange: (event) => patchEmail({ subjectTemplate: event.target.value || undefined }) })] })] }), _jsxs("div", { className: "mt-4 space-y-2", children: [_jsx(Label, { htmlFor: "capture-email-body", children: "Body template" }), _jsx(Textarea, { id: "capture-email-body", value: email.bodyTemplate ?? "", onChange: (event) => patchEmail({ bodyTemplate: event.target.value || undefined }) }), _jsxs("p", { className: "text-xs text-muted-foreground", children: ["Template tokens can reference field keys like ", "{{email_ab12}}", "; ", "{{answers}}", " inserts all answers."] })] })] }), visible.has("workflow") && _jsxs("section", { className: sectionClassName, children: [_jsxs("div", { className: "flex items-start justify-between gap-4", children: [_jsxs("div", { children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(GitBranch, { className: "h-4 w-4 text-primary" }), _jsx("h3", { className: "text-sm font-semibold text-foreground", children: "Workflow trigger action" })] }), _jsx("p", { className: "mt-1 text-sm text-muted-foreground", children: "Start a current-scope workflow after submission. API trigger-compatible workflows receive answers plus quiz outcome summary." })] }), showSectionSwitches && _jsx(Switch, { checked: workflow.enabled, onCheckedChange: (enabled) => patchWorkflow({ enabled }) })] }), workflowsError ? (_jsx("div", { className: "mt-4 rounded-xl border border-destructive/30 bg-destructive/5 p-4", children: _jsxs("div", { className: "flex items-start gap-3", children: [_jsx(AlertCircle, { className: "mt-0.5 h-4 w-4 shrink-0 text-destructive" }), _jsxs("div", { className: "min-w-0 flex-1", children: [_jsx("h4", { className: "text-sm font-semibold text-foreground", children: "Could not load workflows" }), _jsx("p", { className: "mt-1 text-sm text-muted-foreground", children: "Workflow actions are optional. Retry to select a workflow trigger, or keep this action disabled and save other settings." }), _jsx(Button, { type: "button", variant: "outline", size: "sm", className: "mt-3", onClick: onRetryWorkflows, children: "Try again" })] })] }) })) : (_jsxs(_Fragment, { children: [_jsxs("div", { className: compact ? "mt-4 grid gap-4" : "mt-4 grid gap-4 md:grid-cols-[minmax(0,1fr)_220px]", children: [_jsxs("div", { className: "space-y-2", children: [_jsx(Label, { children: "Workflow definition" }), _jsxs("select", { className: "h-9 w-full rounded-md border border-input bg-background px-3 py-1 text-sm shadow-xs outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50", value: workflow.workflowDefinitionId ?? "", disabled: workflowsLoading || workflows.length === 0, onChange: (event) => {
1862
+ const next = workflows.find((candidate) => candidate.id === event.target.value);
1863
+ patchWorkflow({
1864
+ workflowDefinitionId: next?.id,
1865
+ triggerNodeId: next?.apiTriggerNodeId,
1866
+ });
1867
+ }, children: [_jsx("option", { value: "", children: workflowsLoading ? "Loading workflows…" : workflows.length === 0 ? "No compatible workflows available" : "Select workflow…" }), workflows.map((candidate) => (_jsxs("option", { value: candidate.id, children: [candidate.name, " \u00B7 ", candidate.compatible ? "API trigger" : "not API-ready"] }, candidate.id)))] }), !workflowsLoading && workflows.length === 0 && (_jsx("p", { className: "text-xs text-muted-foreground", children: "Workflow triggering is optional. Create an API-trigger-compatible workflow to enable this action." }))] }), _jsxs("div", { className: "flex items-center justify-between gap-3 rounded-lg border border-border p-3", children: [_jsx("span", { className: "text-sm text-muted-foreground", children: "Execute immediately" }), _jsx(Switch, { checked: workflow.execute ?? true, onCheckedChange: (execute) => patchWorkflow({ execute }), disabled: workflowsLoading || workflows.length === 0 })] })] }), _jsxs("div", { className: compact ? "mt-4 grid gap-4" : "mt-4 grid gap-4 lg:grid-cols-2", children: [_jsxs("div", { className: "rounded-xl border border-border bg-background p-3", children: [_jsx("h4", { className: "text-xs font-semibold uppercase tracking-wide text-muted-foreground", children: "Compatibility" }), workflowsLoading ? (_jsxs("div", { className: "mt-2 space-y-2", children: [_jsx(Skeleton, { className: "h-4 w-2/3" }), _jsx(Skeleton, { className: "h-3 w-1/2" })] })) : (_jsxs(_Fragment, { children: [_jsx("p", { className: cn("mt-2 text-sm", selectedWorkflow?.compatible ? "text-foreground" : selectedWorkflow ? "text-destructive" : "text-muted-foreground"), children: selectedWorkflow?.summary ?? "Select a workflow to inspect trigger compatibility." }), selectedWorkflow && (_jsxs("p", { className: "mt-1 text-xs text-muted-foreground", children: ["Version ", selectedWorkflow.currentVersion, " \u00B7 Status ", selectedWorkflow.status, " \u00B7 Triggers ", selectedWorkflow.triggerTypes.join(", ") || "none"] }))] }))] }), _jsxs("div", { className: "rounded-xl border border-border bg-muted/30 p-3", children: [_jsx("h4", { className: "text-xs font-semibold uppercase tracking-wide text-muted-foreground", children: "Payload preview" }), _jsx("pre", { className: "mt-2 max-h-48 overflow-auto rounded-lg bg-background p-2 text-xs text-muted-foreground", children: JSON.stringify(workflowPreview, null, 2) })] })] })] }))] })] }));
1868
+ }
1869
+ function CustomFieldMappingsEditor({ fields, contactFields, mappings, isLoading, isError, onRetry, onChange, }) {
1870
+ const mappedKeys = Object.keys(mappings);
1871
+ const availableContactFields = contactFields.filter((field) => !mappedKeys.includes(field.key));
1872
+ const updateMapping = (contactFieldKey, answerFieldKey) => {
1873
+ const next = { ...mappings };
1874
+ if (answerFieldKey)
1875
+ next[contactFieldKey] = answerFieldKey;
1876
+ else
1877
+ delete next[contactFieldKey];
1878
+ onChange(next);
1879
+ };
1880
+ const renameMapping = (previousKey, nextKey) => {
1881
+ if (!nextKey || nextKey === previousKey)
1882
+ return;
1883
+ const next = { ...mappings };
1884
+ const answerFieldKey = next[previousKey];
1885
+ delete next[previousKey];
1886
+ if (answerFieldKey)
1887
+ next[nextKey] = answerFieldKey;
1888
+ onChange(next);
1889
+ };
1890
+ return (_jsxs("div", { className: "mt-4 space-y-3", children: [_jsxs("div", { children: [_jsx(Label, { children: "Custom field mappings" }), _jsx("p", { className: "mt-1 text-xs text-muted-foreground", children: "Map Contacts custom fields to Capture answers. Add as many mappings as this form needs." })] }), isError ? (_jsxs("div", { className: "rounded-lg border border-destructive/30 bg-destructive/5 p-3", children: [_jsx("p", { className: "text-sm text-muted-foreground", children: "Could not load Contacts custom fields." }), _jsx(Button, { type: "button", variant: "outline", size: "sm", className: "mt-2", onClick: onRetry, children: "Try again" })] })) : isLoading ? (_jsxs("div", { className: "space-y-2", children: [_jsx(Skeleton, { className: "h-9 w-full" }), _jsx(Skeleton, { className: "h-9 w-full" })] })) : contactFields.length === 0 ? (_jsx("div", { className: "rounded-lg border border-dashed border-border bg-muted/25 p-3 text-sm text-muted-foreground", children: "No Contacts custom fields are available yet." })) : (_jsxs(_Fragment, { children: [_jsx("div", { className: "space-y-2", children: mappedKeys.length === 0 ? (_jsx("p", { className: "rounded-lg border border-dashed border-border bg-muted/25 p-3 text-sm text-muted-foreground", children: "No custom fields mapped." })) : mappedKeys.map((contactFieldKey) => {
1891
+ const selectedContactField = contactFields.find((field) => field.key === contactFieldKey);
1892
+ const selectableContactFields = [
1893
+ ...(selectedContactField ? [selectedContactField] : [{ id: contactFieldKey, key: contactFieldKey, label: contactFieldKey, type: "text" }]),
1894
+ ...availableContactFields,
1895
+ ];
1896
+ return (_jsxs("div", { className: "grid gap-2 rounded-lg border border-border bg-muted/20 p-2 sm:grid-cols-[minmax(0,1fr)_minmax(0,1fr)_auto]", children: [_jsxs("div", { className: "space-y-1", children: [_jsx(Label, { children: "Contact field" }), _jsx("select", { className: "h-9 w-full rounded-md border border-input bg-background px-3 py-1 text-sm shadow-xs outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50", value: contactFieldKey, onChange: (event) => renameMapping(contactFieldKey, event.target.value), children: selectableContactFields.map((field) => _jsx("option", { value: field.key, children: field.label }, field.key)) })] }), _jsx(FieldSelect, { label: "Capture answer", value: mappings[contactFieldKey] ?? "", fields: fields, onChange: (answerFieldKey) => updateMapping(contactFieldKey, answerFieldKey) }), _jsx("div", { className: "flex items-end", children: _jsx(Button, { type: "button", variant: "outline", size: "icon-sm", onClick: () => updateMapping(contactFieldKey, undefined), "aria-label": "Remove mapping", title: "Remove mapping", children: _jsx(Trash2, { className: "h-4 w-4" }) }) })] }, contactFieldKey));
1897
+ }) }), _jsxs(Button, { type: "button", variant: "outline", size: "sm", onClick: () => {
1898
+ const nextField = availableContactFields[0];
1899
+ const firstAnswerField = fields[0];
1900
+ if (nextField && firstAnswerField)
1901
+ updateMapping(nextField.key, firstAnswerField.key);
1902
+ }, disabled: availableContactFields.length === 0 || fields.length === 0, children: [_jsx(Plus, { className: "h-4 w-4" }), "Add custom field mapping"] })] }))] }));
298
1903
  }
299
1904
  function FieldSelect({ label, value, fields, onChange, }) {
300
1905
  return (_jsxs("div", { className: "space-y-2", children: [_jsx(Label, { children: label }), _jsxs("select", { className: "h-9 w-full rounded-md border border-input bg-background px-3 py-1 text-sm shadow-xs outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50", value: value, onChange: (event) => onChange(event.target.value || undefined), children: [_jsx("option", { value: "", children: "Not mapped" }), fields.map((field) => _jsxs("option", { value: field.key, children: [field.label, " (", field.key, ")"] }, field.id))] })] }));
301
1906
  }
302
- function SubmissionPreview({ submissions }) {
303
- return (_jsxs("div", { className: "rounded-2xl border border-border bg-card shadow-sm", children: [_jsx("div", { className: "border-b border-border px-4 py-3", children: _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Inbox, { className: "h-4 w-4 text-primary" }), _jsx("h3", { className: "text-sm font-semibold text-foreground", children: "Recent submissions" })] }) }), submissions.length === 0 ? (_jsx("p", { className: "p-4 text-sm text-muted-foreground", children: "No public form submissions yet." })) : (_jsx("div", { className: "divide-y divide-border", children: submissions.map((submission) => (_jsxs("div", { className: "p-4", children: [_jsxs("div", { className: "flex items-center justify-between gap-3", children: [_jsx("p", { className: "text-sm font-semibold text-foreground", children: submission.definitionName }), _jsx("p", { className: "text-xs text-muted-foreground", children: new Date(submission.submittedAt).toLocaleString() })] }), submission.actionStatus && (_jsxs("p", { className: "mt-1 text-xs text-muted-foreground", children: ["Contacts action: ", submission.actionStatus, submission.contactId ? ` · Contact ${submission.contactId}` : ""] })), _jsx("dl", { className: "mt-2 grid gap-2 sm:grid-cols-2", children: Object.entries(submission.answers).map(([key, value]) => (_jsxs("div", { className: "rounded-lg bg-muted/50 p-2 text-xs", children: [_jsx("dt", { className: "font-semibold text-muted-foreground", children: key }), _jsx("dd", { className: "mt-0.5 text-foreground", children: String(value ?? "—") })] }, key))) })] }, submission.id))) }))] }));
1907
+ function ImageBlockControls({ item, onChange, }) {
1908
+ const resolveStorageImageMutation = useMutation(captureApi.definitions.resolveStorageImageUrl.mutationOptions());
1909
+ const handleUpload = (event) => {
1910
+ const file = event.target.files?.[0];
1911
+ if (!file)
1912
+ return;
1913
+ const reader = new FileReader();
1914
+ reader.onloadend = () => {
1915
+ if (typeof reader.result === "string")
1916
+ onChange({ imageUrl: reader.result, source: { type: "upload" } });
1917
+ };
1918
+ reader.readAsDataURL(file);
1919
+ event.target.value = "";
1920
+ };
1921
+ return (_jsxs("div", { className: "space-y-4", children: [_jsxs("div", { children: [_jsx("h3", { className: "text-sm font-semibold text-foreground", children: "Image" }), _jsx("p", { className: "mt-1 text-xs text-muted-foreground", children: "Upload a standalone image and choose its shape." })] }), _jsx("div", { className: "flex justify-center", children: item.imageUrl ? (_jsx("img", { src: item.imageUrl, alt: item.altText ?? "", className: getImageClassName(item) })) : (_jsx("div", { className: cn("flex items-center justify-center border border-dashed border-border bg-muted/30 text-sm text-muted-foreground", getImageClassName(item)), children: "No image" })) }), _jsxs("div", { className: "space-y-1.5", children: [_jsx(Label, { children: "Image source" }), _jsx(AttachImageButton, { hasImage: !!item.imageUrl || item.source?.type === "storage", onUpload: handleUpload, onSelect: (picked) => {
1922
+ if (picked.kind === "external") {
1923
+ onChange({ imageUrl: picked.url, source: { type: "url" } });
1924
+ return;
1925
+ }
1926
+ const patch = {
1927
+ altText: item.altText || picked.name,
1928
+ source: { type: "storage", storageItemId: picked.itemId },
1929
+ };
1930
+ onChange(patch);
1931
+ void resolveStorageImageMutation.mutateAsync({ itemId: picked.itemId })
1932
+ .then((result) => {
1933
+ onChange({ ...patch, imageUrl: result.url });
1934
+ toast.success("Storage image selected");
1935
+ })
1936
+ .catch(() => {
1937
+ toast.error("Image selected, but preview URL could not be loaded");
1938
+ });
1939
+ } })] }), _jsxs("div", { className: "space-y-1.5", children: [_jsx(Label, { htmlFor: "image-alt", children: "Alt text" }), _jsx(Input, { id: "image-alt", value: item.altText ?? "", placeholder: "Describe the image", onChange: (event) => onChange({ altText: event.target.value || undefined }) })] }), _jsx(SegmentedControl, { label: "Size", value: item.size ?? "medium", options: [["small", "Small"], ["medium", "Medium"], ["large", "Large"], ["full", "Full"]], onChange: (value) => onChange({ size: value }) }), _jsx(SegmentedControl, { label: "Fit", value: item.fit ?? "cover", options: [["cover", "Fill frame"], ["contain", "Fit image"]], onChange: (value) => onChange({ fit: value }) }), _jsx(SegmentedControl, { label: "Shape", value: item.shape ?? "square", options: [["square", "Square"], ["circle", "Circle"]], onChange: (value) => onChange({ shape: value }) }), _jsx(SegmentedControl, { label: "Alignment", value: item.align ?? "center", options: [["left", "Left"], ["center", "Center"], ["right", "Right"]], onChange: (value) => onChange({ align: value }) })] }));
1940
+ }
1941
+ function AttachImageButton({ hasImage, onSelect, onUpload, }) {
1942
+ const [open, setOpen] = useState(false);
1943
+ const inputId = useMemo(() => `capture-local-image-${Math.random().toString(36).slice(2)}`, []);
1944
+ let hasStoragePicker = true;
1945
+ try {
1946
+ useAssetPickerHooks();
1947
+ }
1948
+ catch {
1949
+ hasStoragePicker = false;
1950
+ }
1951
+ const buttonLabel = hasImage ? "Replace image" : "Attach image";
1952
+ if (!hasStoragePicker) {
1953
+ return (_jsxs("div", { children: [_jsx("input", { id: inputId, type: "file", accept: "image/*", className: "sr-only", onChange: onUpload }), _jsx(Button, { type: "button", variant: "outline", className: "w-full justify-start", asChild: true, children: _jsxs("label", { htmlFor: inputId, className: "cursor-pointer", children: [_jsx(ImageIcon, { className: "h-4 w-4" }), buttonLabel] }) }), _jsx("p", { className: "mt-1 text-xs text-muted-foreground", children: "Uses local upload when storage is not configured." })] }));
1954
+ }
1955
+ return (_jsxs(_Fragment, { children: [_jsxs(Button, { type: "button", variant: "outline", className: "w-full justify-start", onClick: () => setOpen(true), children: [_jsx(ImageIcon, { className: "h-4 w-4" }), buttonLabel] }), _jsx(StorageAssetPicker, { open: open, onOpenChange: setOpen, mode: "upload-or-pick", initialDestination: ["capture-images"], ensureInitialDestination: true, mimePrefix: "image/", allowExternalUrl: true, onSelect: (item) => {
1956
+ onSelect(item);
1957
+ setOpen(false);
1958
+ } })] }));
1959
+ }
1960
+ function IconBlockControls({ item, onChange, }) {
1961
+ const [iconSearch, setIconSearch] = useState("");
1962
+ const pickerData = {
1963
+ icon: item.icon,
1964
+ iconType: "lucide",
1965
+ iconColor: item.iconColor ?? "indigo",
1966
+ backgroundColor: "slate",
1967
+ };
1968
+ const filteredIcons = ICON_LIST.filter((icon) => icon.name.toLowerCase().includes(iconSearch.toLowerCase()));
1969
+ return (_jsxs("div", { className: "space-y-4", children: [_jsxs("div", { children: [_jsx("h3", { className: "text-sm font-semibold text-foreground", children: "Icon" }), _jsx("p", { className: "mt-1 text-xs text-muted-foreground", children: "Add a standalone visual marker between form items." })] }), _jsxs("div", { className: "flex items-center gap-3", children: [_jsx(AvatarPreview, { data: pickerData, size: "md" }), _jsxs("div", { className: "min-w-0", children: [_jsx("p", { className: "truncate text-sm font-medium text-foreground", children: item.icon }), _jsx("p", { className: "text-xs text-muted-foreground", children: item.iconColor ?? "indigo" })] })] }), _jsxs("div", { className: "space-y-1.5", children: [_jsx(Label, { className: "text-sm font-medium text-foreground", children: "Icon color" }), _jsx("div", { className: "grid grid-cols-10 gap-2", children: COLORS.map((color) => (_jsx("button", { type: "button", onClick: () => onChange({ iconColor: color.value }), style: { backgroundColor: getColorStyle(color.value).hex }, className: cn("h-6 w-6 rounded-full border border-border transition-all", (item.iconColor ?? "indigo") === color.value
1970
+ ? "scale-110 ring-2 ring-muted-foreground ring-offset-2"
1971
+ : "opacity-90 hover:scale-110 hover:opacity-100"), title: color.name, "aria-label": color.name, "aria-pressed": (item.iconColor ?? "indigo") === color.value }, color.value))) })] }), _jsxs("div", { className: "space-y-1.5", children: [_jsx(Label, { htmlFor: "icon-search", className: "text-sm font-medium text-foreground", children: "Choose icon" }), _jsxs("div", { className: "relative", children: [_jsx(Search, { className: "absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" }), _jsx(Input, { id: "icon-search", value: iconSearch, onChange: (event) => setIconSearch(event.target.value), placeholder: "Search icons...", className: "h-9 pl-9" })] }), _jsx("div", { className: "max-h-64 overflow-y-auto rounded-lg border border-border p-2", children: _jsxs("div", { className: "grid grid-cols-5 gap-2", children: [filteredIcons.map((icon) => {
1972
+ const IconComponent = icon.icon;
1973
+ const selected = item.icon === icon.name;
1974
+ return (_jsx("button", { type: "button", onClick: () => onChange({ icon: icon.name }), className: cn("flex aspect-square items-center justify-center rounded-lg border transition-colors", selected ? "border-primary bg-primary/10 text-primary" : "border-border text-muted-foreground hover:bg-muted/50 hover:text-foreground"), title: icon.name, "aria-label": icon.name, "aria-pressed": selected, children: _jsx(IconComponent, { className: "h-5 w-5" }) }, icon.name));
1975
+ }), filteredIcons.length === 0 && _jsx("p", { className: "col-span-full py-4 text-center text-sm text-muted-foreground", children: "No icons found" })] }) })] }), _jsxs("div", { className: "space-y-1.5", children: [_jsx(Label, { className: "text-sm font-medium text-foreground", children: "Alignment" }), _jsx("div", { className: "grid grid-cols-3 gap-1 rounded-md border border-input bg-muted/40 p-1", role: "group", "aria-label": "Icon alignment", children: [
1976
+ ["left", "Left"],
1977
+ ["center", "Center"],
1978
+ ["right", "Right"],
1979
+ ].map(([value, label]) => (_jsx("button", { type: "button", className: cn("h-8 rounded-sm px-2 text-xs font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1", item.align === value
1980
+ ? "bg-background text-foreground shadow-sm"
1981
+ : "text-muted-foreground hover:bg-background/60 hover:text-foreground"), onClick: () => onChange({ align: value }), "aria-pressed": item.align === value, children: label }, value))) })] })] }));
1982
+ }
1983
+ function SegmentedControl({ label, value, options, onChange, }) {
1984
+ return (_jsxs("div", { className: "space-y-1.5", children: [_jsx(Label, { className: "text-sm font-medium text-foreground", children: label }), _jsx("div", { className: "grid gap-1 rounded-md border border-input bg-muted/40 p-1", style: { gridTemplateColumns: `repeat(${options.length}, minmax(0, 1fr))` }, role: "group", "aria-label": label, children: options.map(([optionValue, optionLabel]) => (_jsx("button", { type: "button", className: cn("h-8 rounded-sm px-2 text-xs font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1", value === optionValue
1985
+ ? "bg-background text-foreground shadow-sm"
1986
+ : "text-muted-foreground hover:bg-background/60 hover:text-foreground"), onClick: () => onChange(optionValue), "aria-pressed": value === optionValue, children: optionLabel }, optionValue))) })] }));
1987
+ }
1988
+ function DeleteSelectedItemButton({ onDelete, label }) {
1989
+ return (_jsxs(Button, { type: "button", variant: "outline", className: "w-full justify-start text-destructive hover:text-destructive", onClick: onDelete, children: [_jsx(Trash2, { className: "h-4 w-4" }), label] }));
1990
+ }
1991
+ function FieldKeyControl({ field, locked, duplicate, onChange, }) {
1992
+ return (_jsxs("div", { className: "space-y-1", children: [_jsxs("div", { className: "flex items-center justify-between gap-3", children: [_jsx(Label, { htmlFor: "field-key", children: "Key" }), _jsx("span", { className: "text-xs text-muted-foreground", children: locked ? "Locked after save" : duplicate ? "Must be unique" : "Unique in this form" })] }), _jsx(Input, { id: "field-key", value: field.key, disabled: locked, "aria-invalid": duplicate || undefined, onChange: (event) => onChange(event.target.value) }), duplicate && _jsx("p", { className: "text-xs text-destructive", children: "This key is already used by another field in this form." })] }));
1993
+ }
1994
+ function RatingFieldControls({ field, onChange, }) {
1995
+ const icon = getRatingIcon(field);
1996
+ const align = getRatingAlign(field);
1997
+ const color = getRatingColor(field);
1998
+ return (_jsxs("div", { className: "space-y-3", children: [_jsxs("div", { className: "space-y-1.5", children: [_jsx(Label, { className: "text-sm font-medium text-foreground", children: "Rating icon" }), _jsx("div", { className: "grid grid-cols-4 gap-1 rounded-md border border-input bg-muted/40 p-1", role: "group", "aria-label": "Rating icon", children: [
1999
+ ["star", "Star"],
2000
+ ["circle", "Circle"],
2001
+ ["square", "Square"],
2002
+ ["heart", "Heart"],
2003
+ ].map(([value, label]) => (_jsx("button", { type: "button", className: cn("h-8 rounded-sm px-2 text-xs font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1", icon === value
2004
+ ? "bg-background text-foreground shadow-sm"
2005
+ : "text-muted-foreground hover:bg-background/60 hover:text-foreground"), onClick: () => onChange({ ratingIcon: value }), "aria-pressed": icon === value, children: label }, value))) })] }), _jsxs("div", { className: "space-y-1.5", children: [_jsx(Label, { className: "text-sm font-medium text-foreground", children: "Color" }), _jsx("div", { className: "grid grid-cols-9 gap-2", role: "group", "aria-label": "Rating color", children: CAPTURE_RATING_COLORS.map((option) => (_jsx("button", { type: "button", onClick: () => onChange({ ratingColor: option.value }), style: { backgroundColor: option.hex }, className: cn("h-6 w-6 rounded-full border border-border transition-all", color === option.value
2006
+ ? "scale-110 ring-2 ring-muted-foreground ring-offset-2"
2007
+ : "opacity-90 hover:scale-110 hover:opacity-100"), title: option.name, "aria-label": option.name, "aria-pressed": color === option.value }, option.value))) })] }), _jsxs("div", { className: "space-y-1.5", children: [_jsx(Label, { className: "text-sm font-medium text-foreground", children: "Alignment" }), _jsx("div", { className: "grid grid-cols-3 gap-1 rounded-md border border-input bg-muted/40 p-1", role: "group", "aria-label": "Rating alignment", children: [
2008
+ ["left", "Left"],
2009
+ ["center", "Center"],
2010
+ ["right", "Right"],
2011
+ ].map(([value, label]) => (_jsx("button", { type: "button", className: cn("h-8 rounded-sm px-2 text-xs font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1", align === value
2012
+ ? "bg-background text-foreground shadow-sm"
2013
+ : "text-muted-foreground hover:bg-background/60 hover:text-foreground"), onClick: () => onChange({ ratingAlign: value }), "aria-pressed": align === value, children: label }, value))) })] })] }));
2014
+ }
2015
+ function FieldLayoutControls({ field, onChange, }) {
2016
+ const layout = getFieldLayout(field);
2017
+ return (_jsx("div", { className: "space-y-3", children: _jsxs("div", { className: "space-y-1.5", children: [_jsxs("div", { className: "flex items-center justify-between gap-3", children: [_jsx(Label, { className: "text-sm font-medium text-foreground", children: "Width" }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx("span", { className: "text-sm font-medium text-foreground", children: "Wrap" }), _jsx(Switch, { checked: layout.wrapAfter, onCheckedChange: (checked) => onChange({ wrapAfter: checked }) })] })] }), _jsx("div", { className: "grid grid-cols-3 gap-1 rounded-md border border-input bg-muted/40 p-1", role: "group", "aria-label": "Field width", children: [
2018
+ ["1/3", "⅓"],
2019
+ ["1/2", "½"],
2020
+ ["full", "Full"],
2021
+ ].map(([value, label]) => (_jsx("button", { type: "button", className: cn("h-8 rounded-sm px-2 text-xs font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1", layout.width === value
2022
+ ? "bg-background text-foreground shadow-sm"
2023
+ : "text-muted-foreground hover:bg-background/60 hover:text-foreground"), onClick: () => onChange({ width: value }), "aria-pressed": layout.width === value, children: label }, value))) })] }) }));
2024
+ }
2025
+ function supportsPlaceholder(field) {
2026
+ return field.type === "text" || field.type === "email" || field.type === "phone" || field.type === "textarea";
2027
+ }
2028
+ function StaticVisibilityControl({ hidden, onHiddenChange, }) {
2029
+ return (_jsxs("div", { className: "flex items-center justify-between gap-3", children: [_jsx("span", { className: "text-sm font-medium text-foreground", children: "Visible" }), _jsx(Switch, { checked: !hidden, onCheckedChange: (checked) => onHiddenChange(!checked) })] }));
304
2030
  }
305
2031
  function EmptyPanelText({ children }) {
306
2032
  return _jsx("p", { className: "rounded-xl bg-muted/40 p-3 text-sm text-muted-foreground", children: children });