@squaredr/fieldcraft-pro 1.1.1 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -1,5 +1,5 @@
1
- export { DEFAULT_PALETTE, DEFAULT_SCHEMA, FormBuilder, FormBuilderThemeProvider, QUESTION_TYPE_INFO, addOption, addQuestion, addSection, cleanPreset, duplicateQuestion, duplicateSection, findQuestion, findSection, generateId, generateOptionId, generateQuestionId, generateSectionId, moveOption, moveQuestion, moveSection, removeOption, removeQuestion, removeSection, squaredrDarkPreset, updateOption, updateQuestion, updateSection, useBuilderState, useBuilderTheme, useDragDrop, useUndoRedo } from './chunk-CJBC3KWB.mjs';
2
- export { ResponseViewer } from './chunk-RSU3X25P.mjs';
1
+ export { DEFAULT_PALETTE, DEFAULT_SCHEMA, FormBuilder, FormBuilderThemeProvider, QUESTION_TYPE_INFO, addOption, addQuestion, addSection, cleanPreset, duplicateQuestion, duplicateSection, findQuestion, findSection, generateId, generateOptionId, generateQuestionId, generateSectionId, moveOption, moveQuestion, moveSection, removeOption, removeQuestion, removeSection, squaredrDarkPreset, updateOption, updateQuestion, updateSection, useBuilderState, useBuilderTheme, useDragDrop, useUndoRedo } from './chunk-GNBXIG63.mjs';
2
+ export { ResponseViewer } from './chunk-4BX7FCXH.mjs';
3
3
  export { cn } from './chunk-QQ4JZGTD.mjs';
4
4
  export { PREVIEW_SCHEMA, ThemeEditor, ThemeEditorInner } from './chunk-7LQW5QYT.mjs';
5
5
  export { FieldCraftProProvider, UnlicensedOverlay, isProductionEnvironment, requireLicense, useLicense, validateLicense } from './chunk-VECQKSWS.mjs';
@@ -7,7 +7,7 @@ export { FieldCraftProProvider, UnlicensedOverlay, isProductionEnvironment, requ
7
7
  // src/index.ts
8
8
  if (typeof globalThis !== "undefined" && typeof globalThis.process !== "undefined" && globalThis.process.env?.NODE_ENV !== "production") {
9
9
  const _fc_banner = `
10
- %c FieldCraft Pro %c v1.0.1
10
+ %c FieldCraft Pro %c v1.3.0
11
11
 
12
12
  %cForm Builder \xB7 Response Viewer \xB7 Theme Editor
13
13
 
@@ -10,6 +10,16 @@ type ResponseViewerProps = {
10
10
  width?: string | number;
11
11
  /** Custom per-type field renderers. Keys are field type strings (e.g. "vitals_entry"). */
12
12
  fieldRenderers?: Record<string, FieldRenderer>;
13
+ /** Filename for CSV/JSON downloads. Defaults to "responses.csv" / "responses.json". */
14
+ filename?: string;
15
+ /** Date format for CSV export. "locale" uses toLocaleString(), "iso" uses toISOString(). */
16
+ dateFormat?: "locale" | "iso";
17
+ /** Override CSV column headers. Maps question ID to display label. */
18
+ columnLabels?: Record<string, string>;
19
+ /** Callback fired after CSV or JSON export completes. */
20
+ onExport?: (format: "csv" | "json", count: number) => void;
21
+ /** Number of responses per page in table/card views. Defaults to 25. */
22
+ pageSize?: 10 | 25 | 50 | 100;
13
23
  };
14
24
  type ViewMode = "table" | "card" | "detail";
15
25
  type ResponseField = {
@@ -10,6 +10,16 @@ type ResponseViewerProps = {
10
10
  width?: string | number;
11
11
  /** Custom per-type field renderers. Keys are field type strings (e.g. "vitals_entry"). */
12
12
  fieldRenderers?: Record<string, FieldRenderer>;
13
+ /** Filename for CSV/JSON downloads. Defaults to "responses.csv" / "responses.json". */
14
+ filename?: string;
15
+ /** Date format for CSV export. "locale" uses toLocaleString(), "iso" uses toISOString(). */
16
+ dateFormat?: "locale" | "iso";
17
+ /** Override CSV column headers. Maps question ID to display label. */
18
+ columnLabels?: Record<string, string>;
19
+ /** Callback fired after CSV or JSON export completes. */
20
+ onExport?: (format: "csv" | "json", count: number) => void;
21
+ /** Number of responses per page in table/card views. Defaults to 25. */
22
+ pageSize?: 10 | 25 | 50 | 100;
13
23
  };
14
24
  type ViewMode = "table" | "card" | "detail";
15
25
  type ResponseField = {
@@ -1057,16 +1057,24 @@ function formatFallbackValue(value) {
1057
1057
  }
1058
1058
 
1059
1059
  // src/response-viewer/export-utils.ts
1060
- function exportToCsv(schema, responses, filename = "responses.csv") {
1060
+ function buildCsvContent(schema, responses, options = {}) {
1061
+ const { dateFormat = "locale", columnLabels } = options;
1061
1062
  const questions = getExportableQuestions(schema);
1062
- const headers = ["Submitted", ...questions.map((q) => q.label), "Score"];
1063
+ const headers = [
1064
+ "Submitted",
1065
+ ...questions.map((q) => columnLabels?.[q.id] ?? q.label),
1066
+ "Score"
1067
+ ];
1063
1068
  const rows = responses.map((r) => {
1064
- const submitted = new Date(r.submittedAt).toLocaleString();
1069
+ const submitted = dateFormat === "iso" ? new Date(r.submittedAt).toISOString() : new Date(r.submittedAt).toLocaleString();
1065
1070
  const values = questions.map((q) => formatExportValue(r.values[q.id], q.type));
1066
1071
  const score = r.totalScore != null ? String(r.totalScore) : "";
1067
1072
  return [submitted, ...values, score];
1068
1073
  });
1069
- const csvContent = [headers, ...rows].map((row) => row.map(escapeCsvField).join(",")).join("\n");
1074
+ return [headers, ...rows].map((row) => row.map(escapeCsvField).join(",")).join("\n");
1075
+ }
1076
+ function exportToCsv(schema, responses, filename = "responses.csv", options = {}) {
1077
+ const csvContent = buildCsvContent(schema, responses, options);
1070
1078
  downloadBlob(csvContent, filename, "text/csv;charset=utf-8;");
1071
1079
  }
1072
1080
  function exportToJson(responses, filename = "responses.json") {
@@ -1161,18 +1169,148 @@ function downloadBlob(content, filename, mimeType) {
1161
1169
  document.body.removeChild(link);
1162
1170
  URL.revokeObjectURL(url);
1163
1171
  }
1172
+
1173
+ // src/response-viewer/pagination-utils.ts
1174
+ function paginate(items, currentPage, pageSize) {
1175
+ const totalItems = items.length;
1176
+ const totalPages = Math.max(1, Math.ceil(totalItems / pageSize));
1177
+ const page = Math.max(1, Math.min(currentPage, totalPages));
1178
+ const startIndex = (page - 1) * pageSize;
1179
+ const endIndex = Math.min(startIndex + pageSize, totalItems);
1180
+ const pageItems = items.slice(startIndex, endIndex);
1181
+ return {
1182
+ pageItems,
1183
+ currentPage: page,
1184
+ totalPages,
1185
+ startItem: totalItems === 0 ? 0 : startIndex + 1,
1186
+ endItem: endIndex,
1187
+ totalItems,
1188
+ showPagination: totalItems > pageSize
1189
+ };
1190
+ }
1191
+
1192
+ // src/response-viewer/filter-utils.ts
1193
+ function createEmptyFilterState() {
1194
+ return { filters: [], dateRange: {} };
1195
+ }
1196
+ function hasActiveFilters(state) {
1197
+ return state.filters.length > 0 || !!state.dateRange.from || !!state.dateRange.to;
1198
+ }
1199
+ function applyFilters(responses, state) {
1200
+ if (!hasActiveFilters(state)) return responses;
1201
+ return responses.filter((r) => {
1202
+ if (!matchesDateRange(r.submittedAt, state.dateRange)) return false;
1203
+ for (const filter of state.filters) {
1204
+ if (!matchesFieldFilter(r.values[filter.fieldId], filter)) return false;
1205
+ }
1206
+ return true;
1207
+ });
1208
+ }
1209
+ function matchesDateRange(submittedAt, range) {
1210
+ if (!range.from && !range.to) return true;
1211
+ const submitted = new Date(submittedAt);
1212
+ if (range.from) {
1213
+ const from = new Date(range.from);
1214
+ from.setHours(0, 0, 0, 0);
1215
+ if (submitted < from) return false;
1216
+ }
1217
+ if (range.to) {
1218
+ const to = new Date(range.to);
1219
+ to.setHours(23, 59, 59, 999);
1220
+ if (submitted > to) return false;
1221
+ }
1222
+ return true;
1223
+ }
1224
+ function matchesFieldFilter(value, filter) {
1225
+ switch (filter.operator) {
1226
+ case "equals":
1227
+ if (value == null) return false;
1228
+ return String(value).toLowerCase() === String(filter.value).toLowerCase();
1229
+ case "contains":
1230
+ if (value == null) return false;
1231
+ return String(value).toLowerCase().includes(String(filter.value).toLowerCase());
1232
+ case "gt":
1233
+ if (typeof value !== "number") return false;
1234
+ return value > Number(filter.value);
1235
+ case "lt":
1236
+ if (typeof value !== "number") return false;
1237
+ return value < Number(filter.value);
1238
+ case "is_true":
1239
+ return value === true || value === "true" || value === "agreed";
1240
+ case "is_false":
1241
+ return value === false || value === "false" || value == null;
1242
+ default:
1243
+ return true;
1244
+ }
1245
+ }
1164
1246
  function cn(...inputs) {
1165
1247
  return tailwindMerge.twMerge(clsx.clsx(inputs));
1166
1248
  }
1249
+ var PAGE_SIZE_OPTIONS = [10, 25, 50, 100];
1250
+ var FIELD_FILTER_OPERATORS = {
1251
+ text: [
1252
+ { label: "equals", value: "equals" },
1253
+ { label: "contains", value: "contains" }
1254
+ ],
1255
+ number: [
1256
+ { label: "equals", value: "equals" },
1257
+ { label: "greater than", value: "gt" },
1258
+ { label: "less than", value: "lt" }
1259
+ ],
1260
+ boolean: [
1261
+ { label: "is true", value: "is_true" },
1262
+ { label: "is false", value: "is_false" }
1263
+ ],
1264
+ select: [
1265
+ { label: "equals", value: "equals" }
1266
+ ]
1267
+ };
1268
+ function getOperatorsForType(type) {
1269
+ if (type === "number" || type === "slider" || type === "rating" || type === "nps" || type === "opinion_scale" || type === "pain_scale") {
1270
+ return FIELD_FILTER_OPERATORS.number;
1271
+ }
1272
+ if (type === "boolean" || type === "consent") {
1273
+ return FIELD_FILTER_OPERATORS.boolean;
1274
+ }
1275
+ if (type === "single_select" || type === "dropdown" || type === "country_select") {
1276
+ return FIELD_FILTER_OPERATORS.select;
1277
+ }
1278
+ return FIELD_FILTER_OPERATORS.text;
1279
+ }
1280
+ function operatorNeedsValue(operator) {
1281
+ return operator !== "is_true" && operator !== "is_false";
1282
+ }
1167
1283
  function ResponseViewerInner({
1168
1284
  schema,
1169
1285
  responses,
1170
1286
  onResponseSelect,
1171
1287
  height = "500px",
1172
- width = "100%"
1288
+ width = "100%",
1289
+ filename,
1290
+ dateFormat,
1291
+ columnLabels,
1292
+ onExport,
1293
+ pageSize = 25
1173
1294
  }) {
1174
1295
  const [viewMode, setViewMode] = react.useState("table");
1175
1296
  const [selectedResponse, setSelectedResponse] = react.useState(null);
1297
+ const [currentPage, setCurrentPage] = react.useState(1);
1298
+ const [effectivePageSize, setEffectivePageSize] = react.useState(pageSize);
1299
+ const [filterState, setFilterState] = react.useState(createEmptyFilterState);
1300
+ const [showFilterPanel, setShowFilterPanel] = react.useState(false);
1301
+ const [draftFieldId, setDraftFieldId] = react.useState("");
1302
+ const [draftOperator, setDraftOperator] = react.useState("contains");
1303
+ const [draftValue, setDraftValue] = react.useState("");
1304
+ react.useEffect(() => {
1305
+ setCurrentPage(1);
1306
+ }, [responses]);
1307
+ react.useEffect(() => {
1308
+ setEffectivePageSize(pageSize);
1309
+ setCurrentPage(1);
1310
+ }, [pageSize]);
1311
+ const isFiltered = hasActiveFilters(filterState);
1312
+ const filteredResponses = applyFilters(responses, filterState);
1313
+ const pag = paginate(filteredResponses, currentPage, effectivePageSize);
1176
1314
  function handleSelect(response) {
1177
1315
  setSelectedResponse(response);
1178
1316
  onResponseSelect?.(response);
@@ -1180,6 +1318,45 @@ function ResponseViewerInner({
1180
1318
  function handleBack() {
1181
1319
  setSelectedResponse(null);
1182
1320
  }
1321
+ function handlePageSizeChange(newSize) {
1322
+ setEffectivePageSize(Number(newSize));
1323
+ setCurrentPage(1);
1324
+ }
1325
+ function handleAddFilter() {
1326
+ if (!draftFieldId) return;
1327
+ if (operatorNeedsValue(draftOperator) && !draftValue) return;
1328
+ const newFilter = {
1329
+ fieldId: draftFieldId,
1330
+ operator: draftOperator,
1331
+ value: draftOperator === "gt" || draftOperator === "lt" ? Number(draftValue) : draftValue
1332
+ };
1333
+ setFilterState((prev) => ({
1334
+ ...prev,
1335
+ filters: [...prev.filters, newFilter]
1336
+ }));
1337
+ setDraftFieldId("");
1338
+ setDraftOperator("contains");
1339
+ setDraftValue("");
1340
+ setCurrentPage(1);
1341
+ }
1342
+ function handleRemoveFilter(index) {
1343
+ setFilterState((prev) => ({
1344
+ ...prev,
1345
+ filters: prev.filters.filter((_, i) => i !== index)
1346
+ }));
1347
+ setCurrentPage(1);
1348
+ }
1349
+ function handleDateRangeChange(field, value) {
1350
+ setFilterState((prev) => ({
1351
+ ...prev,
1352
+ dateRange: { ...prev.dateRange, [field]: value || void 0 }
1353
+ }));
1354
+ setCurrentPage(1);
1355
+ }
1356
+ function handleClearAllFilters() {
1357
+ setFilterState(createEmptyFilterState());
1358
+ setCurrentPage(1);
1359
+ }
1183
1360
  const questions = getAllQuestions2(schema);
1184
1361
  function getFields(response) {
1185
1362
  return questions.map((q) => ({
@@ -1197,12 +1374,8 @@ function ResponseViewerInner({
1197
1374
  className: "flex flex-col border border-border rounded-lg overflow-hidden bg-background text-foreground",
1198
1375
  style: { height: heightValue, width: widthValue },
1199
1376
  children: [
1200
- !selectedResponse && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex justify-between items-center px-3 py-2 border-b border-border", children: [
1201
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "text-xs text-muted-foreground", children: [
1202
- responses.length,
1203
- " response",
1204
- responses.length !== 1 ? "s" : ""
1205
- ] }),
1377
+ !selectedResponse && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-wrap items-center gap-x-2 gap-y-1 px-3 py-2 border-b border-border", children: [
1378
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "text-xs text-muted-foreground mr-auto", children: isFiltered ? `Showing ${filteredResponses.length} of ${responses.length} responses (filtered)` : `${responses.length} response${responses.length !== 1 ? "s" : ""}` }),
1206
1379
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-1", children: [
1207
1380
  /* @__PURE__ */ jsxRuntime.jsx(
1208
1381
  fieldcraftReact.Button,
@@ -1229,15 +1402,50 @@ function ResponseViewerInner({
1229
1402
  onClick: () => setViewMode("card"),
1230
1403
  children: "Cards"
1231
1404
  }
1405
+ )
1406
+ ] }),
1407
+ /* @__PURE__ */ jsxRuntime.jsx(fieldcraftReact.Separator, { orientation: "vertical", className: "h-5 hidden sm:block" }),
1408
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-1", children: [
1409
+ /* @__PURE__ */ jsxRuntime.jsxs(
1410
+ fieldcraftReact.Button,
1411
+ {
1412
+ variant: showFilterPanel ? "secondary" : "outline",
1413
+ size: "sm",
1414
+ className: cn(
1415
+ "fc-rv-filter__button h-7 text-xs",
1416
+ showFilterPanel && "font-semibold border border-ring"
1417
+ ),
1418
+ onClick: () => setShowFilterPanel((v) => !v),
1419
+ children: [
1420
+ "Filter",
1421
+ isFiltered ? ` (${filterState.filters.length + (filterState.dateRange.from || filterState.dateRange.to ? 1 : 0)})` : ""
1422
+ ]
1423
+ }
1232
1424
  ),
1233
- /* @__PURE__ */ jsxRuntime.jsx(fieldcraftReact.Separator, { orientation: "vertical", className: "h-5 mx-1" }),
1425
+ isFiltered && /* @__PURE__ */ jsxRuntime.jsx(
1426
+ fieldcraftReact.Button,
1427
+ {
1428
+ variant: "ghost",
1429
+ size: "sm",
1430
+ className: "h-7 text-xs text-destructive",
1431
+ onClick: handleClearAllFilters,
1432
+ children: "Clear all"
1433
+ }
1434
+ )
1435
+ ] }),
1436
+ /* @__PURE__ */ jsxRuntime.jsx(fieldcraftReact.Separator, { orientation: "vertical", className: "h-5 hidden sm:block" }),
1437
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-1", children: [
1234
1438
  /* @__PURE__ */ jsxRuntime.jsx(
1235
1439
  fieldcraftReact.Button,
1236
1440
  {
1237
1441
  variant: "outline",
1238
1442
  size: "sm",
1239
1443
  className: "h-7 text-xs",
1240
- onClick: () => exportToCsv(schema, responses),
1444
+ onClick: () => {
1445
+ const csvFilename = filename ? filename.replace(/\.\w+$/, ".csv") : "responses.csv";
1446
+ exportToCsv(schema, responses, csvFilename, { dateFormat, columnLabels });
1447
+ onExport?.("csv", responses.length);
1448
+ },
1241
1449
  disabled: responses.length === 0,
1242
1450
  children: "Export CSV"
1243
1451
  }
@@ -1248,14 +1456,146 @@ function ResponseViewerInner({
1248
1456
  variant: "outline",
1249
1457
  size: "sm",
1250
1458
  className: "h-7 text-xs",
1251
- onClick: () => exportToJson(responses),
1459
+ onClick: () => {
1460
+ const jsonFilename = filename ? filename.replace(/\.\w+$/, ".json") : "responses.json";
1461
+ exportToJson(responses, jsonFilename);
1462
+ onExport?.("json", responses.length);
1463
+ },
1252
1464
  disabled: responses.length === 0,
1253
1465
  children: "Export JSON"
1254
1466
  }
1255
1467
  )
1256
1468
  ] })
1257
1469
  ] }),
1258
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: cn("flex-1 overflow-auto", selectedResponse && "p-4"), children: selectedResponse ? /* @__PURE__ */ jsxRuntime.jsx(
1470
+ !selectedResponse && showFilterPanel && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "fc-rv-filter__panel px-3 py-2 border-b border-border bg-muted/50", children: [
1471
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-wrap items-center gap-2 mb-2", children: [
1472
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs text-muted-foreground w-16 shrink-0", children: "Date:" }),
1473
+ /* @__PURE__ */ jsxRuntime.jsx(
1474
+ fieldcraftReact.Input,
1475
+ {
1476
+ type: "date",
1477
+ className: "h-7 text-xs w-36",
1478
+ value: filterState.dateRange.from ?? "",
1479
+ onChange: (e) => handleDateRangeChange("from", e.target.value)
1480
+ }
1481
+ ),
1482
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs text-muted-foreground", children: "to" }),
1483
+ /* @__PURE__ */ jsxRuntime.jsx(
1484
+ fieldcraftReact.Input,
1485
+ {
1486
+ type: "date",
1487
+ className: "h-7 text-xs w-36",
1488
+ value: filterState.dateRange.to ?? "",
1489
+ onChange: (e) => handleDateRangeChange("to", e.target.value)
1490
+ }
1491
+ )
1492
+ ] }),
1493
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [
1494
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs text-muted-foreground w-16 shrink-0", children: "Field:" }),
1495
+ /* @__PURE__ */ jsxRuntime.jsxs(
1496
+ fieldcraftReact.Select,
1497
+ {
1498
+ value: draftFieldId || void 0,
1499
+ onValueChange: (val) => {
1500
+ setDraftFieldId(val);
1501
+ const q = questions.find((q2) => q2.id === val);
1502
+ if (q) {
1503
+ const ops = getOperatorsForType(q.type);
1504
+ setDraftOperator(ops[0].value);
1505
+ }
1506
+ setDraftValue("");
1507
+ },
1508
+ children: [
1509
+ /* @__PURE__ */ jsxRuntime.jsx(fieldcraftReact.SelectTrigger, { size: "sm", className: "h-7 text-xs w-40", children: /* @__PURE__ */ jsxRuntime.jsx(fieldcraftReact.SelectValue, { placeholder: "Select field" }) }),
1510
+ /* @__PURE__ */ jsxRuntime.jsx(fieldcraftReact.SelectContent, { children: questions.map((q) => /* @__PURE__ */ jsxRuntime.jsx(fieldcraftReact.SelectItem, { value: q.id, children: q.label }, q.id)) })
1511
+ ]
1512
+ }
1513
+ ),
1514
+ draftFieldId && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
1515
+ /* @__PURE__ */ jsxRuntime.jsxs(
1516
+ fieldcraftReact.Select,
1517
+ {
1518
+ value: draftOperator,
1519
+ onValueChange: (val) => setDraftOperator(val),
1520
+ children: [
1521
+ /* @__PURE__ */ jsxRuntime.jsx(fieldcraftReact.SelectTrigger, { size: "sm", className: "h-7 text-xs w-32", children: /* @__PURE__ */ jsxRuntime.jsx(fieldcraftReact.SelectValue, {}) }),
1522
+ /* @__PURE__ */ jsxRuntime.jsx(fieldcraftReact.SelectContent, { children: getOperatorsForType(
1523
+ questions.find((q) => q.id === draftFieldId)?.type ?? "short_text"
1524
+ ).map((op) => /* @__PURE__ */ jsxRuntime.jsx(fieldcraftReact.SelectItem, { value: op.value, children: op.label }, op.value)) })
1525
+ ]
1526
+ }
1527
+ ),
1528
+ operatorNeedsValue(draftOperator) && /* @__PURE__ */ jsxRuntime.jsx(
1529
+ fieldcraftReact.Input,
1530
+ {
1531
+ className: "h-7 text-xs w-32",
1532
+ placeholder: "Value",
1533
+ value: draftValue,
1534
+ onChange: (e) => setDraftValue(e.target.value),
1535
+ onKeyDown: (e) => e.key === "Enter" && handleAddFilter()
1536
+ }
1537
+ ),
1538
+ /* @__PURE__ */ jsxRuntime.jsx(
1539
+ fieldcraftReact.Button,
1540
+ {
1541
+ variant: "outline",
1542
+ size: "sm",
1543
+ className: "h-7 text-xs",
1544
+ onClick: handleAddFilter,
1545
+ children: "Add"
1546
+ }
1547
+ )
1548
+ ] })
1549
+ ] })
1550
+ ] }),
1551
+ !selectedResponse && isFiltered && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-1 px-3 py-1.5 border-b border-border flex-wrap", children: [
1552
+ filterState.dateRange.from && /* @__PURE__ */ jsxRuntime.jsxs(
1553
+ "span",
1554
+ {
1555
+ className: "fc-rv-filter__chip inline-flex items-center gap-1 px-2 py-0.5 rounded-full bg-accent text-xs text-foreground cursor-pointer hover:bg-destructive/20",
1556
+ onClick: () => handleDateRangeChange("from", ""),
1557
+ children: [
1558
+ "From: ",
1559
+ filterState.dateRange.from,
1560
+ " \xD7"
1561
+ ]
1562
+ }
1563
+ ),
1564
+ filterState.dateRange.to && /* @__PURE__ */ jsxRuntime.jsxs(
1565
+ "span",
1566
+ {
1567
+ className: "fc-rv-filter__chip inline-flex items-center gap-1 px-2 py-0.5 rounded-full bg-accent text-xs text-foreground cursor-pointer hover:bg-destructive/20",
1568
+ onClick: () => handleDateRangeChange("to", ""),
1569
+ children: [
1570
+ "To: ",
1571
+ filterState.dateRange.to,
1572
+ " \xD7"
1573
+ ]
1574
+ }
1575
+ ),
1576
+ filterState.filters.map((f, i) => {
1577
+ const q = questions.find((q2) => q2.id === f.fieldId);
1578
+ const label = q?.label ?? f.fieldId;
1579
+ const opLabel = f.operator.replace("_", " ");
1580
+ const valueStr = operatorNeedsValue(f.operator) ? ` "${f.value}"` : "";
1581
+ return /* @__PURE__ */ jsxRuntime.jsxs(
1582
+ "span",
1583
+ {
1584
+ className: "fc-rv-filter__chip inline-flex items-center gap-1 px-2 py-0.5 rounded-full bg-accent text-xs text-foreground cursor-pointer hover:bg-destructive/20",
1585
+ onClick: () => handleRemoveFilter(i),
1586
+ children: [
1587
+ label,
1588
+ " ",
1589
+ opLabel,
1590
+ valueStr,
1591
+ " \xD7"
1592
+ ]
1593
+ },
1594
+ i
1595
+ );
1596
+ })
1597
+ ] }),
1598
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: cn("flex-1 min-w-0 overflow-auto", selectedResponse && "p-4"), children: selectedResponse ? /* @__PURE__ */ jsxRuntime.jsx(
1259
1599
  ResponseDetail,
1260
1600
  {
1261
1601
  response: selectedResponse,
@@ -1266,11 +1606,11 @@ function ResponseViewerInner({
1266
1606
  ResponseTable,
1267
1607
  {
1268
1608
  schema,
1269
- responses,
1609
+ responses: pag.pageItems,
1270
1610
  onRowClick: handleSelect
1271
1611
  }
1272
1612
  ) : /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "grid gap-3 p-3 grid-cols-[repeat(auto-fill,minmax(280px,1fr))]", children: [
1273
- responses.map((response, idx) => /* @__PURE__ */ jsxRuntime.jsx(
1613
+ pag.pageItems.map((response, idx) => /* @__PURE__ */ jsxRuntime.jsx(
1274
1614
  ResponseCard,
1275
1615
  {
1276
1616
  response,
@@ -1279,8 +1619,63 @@ function ResponseViewerInner({
1279
1619
  },
1280
1620
  response.sessionToken || idx
1281
1621
  )),
1282
- responses.length === 0 && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "p-8 text-center text-muted-foreground", children: "No responses yet" })
1283
- ] }) })
1622
+ filteredResponses.length === 0 && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "p-8 text-center text-muted-foreground", children: isFiltered ? "No matching responses" : "No responses yet" })
1623
+ ] }) }),
1624
+ !selectedResponse && pag.showPagination && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "fc-rv-pagination flex flex-wrap items-center gap-x-2 gap-y-1 px-3 py-2 border-t border-border", children: [
1625
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "fc-rv-pagination__info text-xs text-muted-foreground mr-auto", children: [
1626
+ "Showing ",
1627
+ pag.startItem,
1628
+ "\u2013",
1629
+ pag.endItem,
1630
+ " of ",
1631
+ pag.totalItems,
1632
+ " responses"
1633
+ ] }),
1634
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2", children: [
1635
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "fc-rv-pagination__size-select flex items-center gap-1", children: [
1636
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs text-muted-foreground", children: "Per page:" }),
1637
+ /* @__PURE__ */ jsxRuntime.jsxs(
1638
+ fieldcraftReact.Select,
1639
+ {
1640
+ value: String(effectivePageSize),
1641
+ onValueChange: handlePageSizeChange,
1642
+ children: [
1643
+ /* @__PURE__ */ jsxRuntime.jsx(fieldcraftReact.SelectTrigger, { size: "sm", className: "h-7 w-16 text-xs", children: /* @__PURE__ */ jsxRuntime.jsx(fieldcraftReact.SelectValue, {}) }),
1644
+ /* @__PURE__ */ jsxRuntime.jsx(fieldcraftReact.SelectContent, { children: PAGE_SIZE_OPTIONS.map((size) => /* @__PURE__ */ jsxRuntime.jsx(fieldcraftReact.SelectItem, { value: String(size), children: size }, size)) })
1645
+ ]
1646
+ }
1647
+ )
1648
+ ] }),
1649
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "text-xs text-muted-foreground", children: [
1650
+ "Page ",
1651
+ pag.currentPage,
1652
+ " of ",
1653
+ pag.totalPages
1654
+ ] }),
1655
+ /* @__PURE__ */ jsxRuntime.jsx(
1656
+ fieldcraftReact.Button,
1657
+ {
1658
+ variant: "outline",
1659
+ size: "sm",
1660
+ className: "fc-rv-pagination__button h-7 text-xs",
1661
+ onClick: () => setCurrentPage((p) => p - 1),
1662
+ disabled: pag.currentPage <= 1,
1663
+ children: "Previous"
1664
+ }
1665
+ ),
1666
+ /* @__PURE__ */ jsxRuntime.jsx(
1667
+ fieldcraftReact.Button,
1668
+ {
1669
+ variant: "outline",
1670
+ size: "sm",
1671
+ className: "fc-rv-pagination__button h-7 text-xs",
1672
+ onClick: () => setCurrentPage((p) => p + 1),
1673
+ disabled: pag.currentPage >= pag.totalPages,
1674
+ children: "Next"
1675
+ }
1676
+ )
1677
+ ] })
1678
+ ] })
1284
1679
  ]
1285
1680
  }
1286
1681
  );
@@ -1,3 +1,3 @@
1
- export { ResponseViewer } from '../chunk-RSU3X25P.mjs';
1
+ export { ResponseViewer } from '../chunk-4BX7FCXH.mjs';
2
2
  import '../chunk-QQ4JZGTD.mjs';
3
3
  import '../chunk-VECQKSWS.mjs';