@squaredr/fieldcraft-pro 1.1.1 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -3444,6 +3444,17 @@ function FormBuilderCore(props) {
3444
3444
  const builderState = useBuilderState(initialSchema);
3445
3445
  const dragDrop = useDragDrop(builderState);
3446
3446
  const fileInputRef = react.useRef(null);
3447
+ const [mobilePanel, setMobilePanel] = react.useState("none");
3448
+ react.useEffect(() => {
3449
+ if (builderState.selectedItem && window.innerWidth < 768) {
3450
+ setMobilePanel("properties");
3451
+ }
3452
+ }, [builderState.selectedItem]);
3453
+ react.useEffect(() => {
3454
+ if (!dragDrop.activeDragItem && mobilePanel === "palette") {
3455
+ setMobilePanel("none");
3456
+ }
3457
+ }, [dragDrop.activeDragItem]);
3447
3458
  react.useEffect(() => {
3448
3459
  if (onChange && builderState.isDirty) {
3449
3460
  onChange(builderState.schema);
@@ -3565,7 +3576,7 @@ ${details}`);
3565
3576
  role: "application",
3566
3577
  "aria-label": "Form Builder",
3567
3578
  children: [
3568
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "h-12 shrink-0 grid px-4 bg-card border-b border-border", style: { gridTemplateColumns: "1fr auto 1fr" }, role: "toolbar", "aria-label": "Builder toolbar", children: [
3579
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "h-12 shrink-0 hidden md:grid px-4 bg-card border-b border-border", style: { gridTemplateColumns: "1fr auto 1fr" }, role: "toolbar", "aria-label": "Builder toolbar", children: [
3569
3580
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-1", children: [
3570
3581
  /* @__PURE__ */ jsxRuntime.jsx(
3571
3582
  fieldcraftReact.Button,
@@ -3625,23 +3636,124 @@ ${details}`);
3625
3636
  /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Save, { size: 14, strokeWidth: 2 }),
3626
3637
  "Save"
3627
3638
  ] })
3639
+ ] })
3640
+ ] }),
3641
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "shrink-0 md:hidden bg-card border-b border-border", role: "toolbar", "aria-label": "Builder toolbar", children: [
3642
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "h-10 grid px-2", style: { gridTemplateColumns: "auto 1fr auto" }, children: [
3643
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex items-center", children: /* @__PURE__ */ jsxRuntime.jsx(
3644
+ fieldcraftReact.Button,
3645
+ {
3646
+ variant: mobilePanel === "palette" ? "secondary" : "ghost",
3647
+ size: "icon-sm",
3648
+ onClick: () => setMobilePanel((p) => p === "palette" ? "none" : "palette"),
3649
+ title: "Toggle field palette",
3650
+ "aria-label": "Toggle field palette",
3651
+ children: mobilePanel === "palette" ? /* @__PURE__ */ jsxRuntime.jsx(lucideReact.X, { size: 16, strokeWidth: 1.75 }) : /* @__PURE__ */ jsxRuntime.jsx(lucideReact.PanelLeft, { size: 16, strokeWidth: 1.75 })
3652
+ }
3653
+ ) }),
3654
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-center gap-1", children: [
3655
+ toolbarExtra,
3656
+ /* @__PURE__ */ jsxRuntime.jsxs(fieldcraftReact.Button, { onClick: handleSave, size: "sm", className: "border-0 shadow-sm fcb-glow ml-1", "aria-label": "Save form", children: [
3657
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Save, { size: 14, strokeWidth: 2 }),
3658
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "hidden sm:inline", children: "Save" })
3659
+ ] })
3660
+ ] }),
3661
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex items-center", children: /* @__PURE__ */ jsxRuntime.jsx(
3662
+ fieldcraftReact.Button,
3663
+ {
3664
+ variant: mobilePanel === "properties" ? "secondary" : "ghost",
3665
+ size: "icon-sm",
3666
+ onClick: () => setMobilePanel((p) => p === "properties" ? "none" : "properties"),
3667
+ title: "Toggle properties panel",
3668
+ "aria-label": "Toggle properties panel",
3669
+ children: mobilePanel === "properties" ? /* @__PURE__ */ jsxRuntime.jsx(lucideReact.X, { size: 16, strokeWidth: 1.75 }) : /* @__PURE__ */ jsxRuntime.jsx(lucideReact.PanelRight, { size: 16, strokeWidth: 1.75 })
3670
+ }
3671
+ ) })
3628
3672
  ] }),
3629
- /* @__PURE__ */ jsxRuntime.jsx(
3630
- "input",
3673
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "h-9 flex items-center justify-between px-2 border-t border-border/50", children: [
3674
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-1", children: [
3675
+ /* @__PURE__ */ jsxRuntime.jsx(
3676
+ fieldcraftReact.Button,
3677
+ {
3678
+ variant: "ghost",
3679
+ size: "icon-sm",
3680
+ onClick: builderState.undo,
3681
+ disabled: !builderState.canUndo,
3682
+ className: "disabled:opacity-30",
3683
+ title: "Undo (Ctrl+Z)",
3684
+ "aria-label": "Undo",
3685
+ children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Undo2, { size: 15, strokeWidth: 1.75 })
3686
+ }
3687
+ ),
3688
+ /* @__PURE__ */ jsxRuntime.jsx(
3689
+ fieldcraftReact.Button,
3690
+ {
3691
+ variant: "ghost",
3692
+ size: "icon-sm",
3693
+ onClick: builderState.redo,
3694
+ disabled: !builderState.canRedo,
3695
+ className: "disabled:opacity-30",
3696
+ title: "Redo (Ctrl+Shift+Z)",
3697
+ "aria-label": "Redo",
3698
+ children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Redo2, { size: 15, strokeWidth: 1.75 })
3699
+ }
3700
+ ),
3701
+ builderState.isDirty && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
3702
+ /* @__PURE__ */ jsxRuntime.jsx(fieldcraftReact.Separator, { orientation: "vertical", className: "mx-1 h-4" }),
3703
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs text-primary", role: "status", children: "Unsaved" })
3704
+ ] })
3705
+ ] }),
3706
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-1", children: [
3707
+ /* @__PURE__ */ jsxRuntime.jsx(
3708
+ fieldcraftReact.Button,
3709
+ {
3710
+ variant: "ghost",
3711
+ size: "icon-sm",
3712
+ onClick: handleImport,
3713
+ title: "Import schema JSON",
3714
+ "aria-label": "Import schema",
3715
+ children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Upload, { size: 14, strokeWidth: 1.75 })
3716
+ }
3717
+ ),
3718
+ /* @__PURE__ */ jsxRuntime.jsx(
3719
+ fieldcraftReact.Button,
3720
+ {
3721
+ variant: "ghost",
3722
+ size: "icon-sm",
3723
+ onClick: handleExport,
3724
+ title: "Export schema JSON",
3725
+ "aria-label": "Export schema",
3726
+ children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Download, { size: 14, strokeWidth: 1.75 })
3727
+ }
3728
+ )
3729
+ ] })
3730
+ ] })
3731
+ ] }),
3732
+ /* @__PURE__ */ jsxRuntime.jsx(
3733
+ "input",
3734
+ {
3735
+ ref: fileInputRef,
3736
+ type: "file",
3737
+ accept: ".json,application/json",
3738
+ onChange: handleFileChange,
3739
+ className: "hidden",
3740
+ "aria-hidden": "true"
3741
+ }
3742
+ ),
3743
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex-1 flex overflow-hidden relative", children: [
3744
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "hidden md:flex", children: /* @__PURE__ */ jsxRuntime.jsx(QuestionPalette, { questionTypes: mergedQuestionTypes, palette }) }),
3745
+ /* @__PURE__ */ jsxRuntime.jsx(FormCanvas, { builderState }),
3746
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "hidden md:flex", children: /* @__PURE__ */ jsxRuntime.jsx(PropertiesPanel, { builderState }) }),
3747
+ mobilePanel === "palette" && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "absolute inset-y-0 left-0 z-20 md:hidden shadow-lg", children: /* @__PURE__ */ jsxRuntime.jsx(QuestionPalette, { questionTypes: mergedQuestionTypes, palette }) }),
3748
+ mobilePanel === "properties" && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "absolute inset-y-0 right-0 z-20 md:hidden shadow-lg", children: /* @__PURE__ */ jsxRuntime.jsx(PropertiesPanel, { builderState }) }),
3749
+ mobilePanel !== "none" && /* @__PURE__ */ jsxRuntime.jsx(
3750
+ "div",
3631
3751
  {
3632
- ref: fileInputRef,
3633
- type: "file",
3634
- accept: ".json,application/json",
3635
- onChange: handleFileChange,
3636
- className: "hidden",
3752
+ className: "absolute inset-0 z-10 bg-black/20 md:hidden",
3753
+ onClick: () => setMobilePanel("none"),
3637
3754
  "aria-hidden": "true"
3638
3755
  }
3639
3756
  )
3640
- ] }),
3641
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex-1 flex overflow-hidden", children: [
3642
- /* @__PURE__ */ jsxRuntime.jsx(QuestionPalette, { questionTypes: mergedQuestionTypes, palette }),
3643
- /* @__PURE__ */ jsxRuntime.jsx(FormCanvas, { builderState }),
3644
- /* @__PURE__ */ jsxRuntime.jsx(PropertiesPanel, { builderState })
3645
3757
  ] })
3646
3758
  ]
3647
3759
  }
@@ -4501,16 +4613,24 @@ function formatFallbackValue(value) {
4501
4613
  }
4502
4614
 
4503
4615
  // src/response-viewer/export-utils.ts
4504
- function exportToCsv(schema, responses, filename = "responses.csv") {
4616
+ function buildCsvContent(schema, responses, options = {}) {
4617
+ const { dateFormat = "locale", columnLabels } = options;
4505
4618
  const questions = getExportableQuestions(schema);
4506
- const headers = ["Submitted", ...questions.map((q) => q.label), "Score"];
4619
+ const headers = [
4620
+ "Submitted",
4621
+ ...questions.map((q) => columnLabels?.[q.id] ?? q.label),
4622
+ "Score"
4623
+ ];
4507
4624
  const rows = responses.map((r) => {
4508
- const submitted = new Date(r.submittedAt).toLocaleString();
4625
+ const submitted = dateFormat === "iso" ? new Date(r.submittedAt).toISOString() : new Date(r.submittedAt).toLocaleString();
4509
4626
  const values = questions.map((q) => formatExportValue(r.values[q.id], q.type));
4510
4627
  const score = r.totalScore != null ? String(r.totalScore) : "";
4511
4628
  return [submitted, ...values, score];
4512
4629
  });
4513
- const csvContent = [headers, ...rows].map((row) => row.map(escapeCsvField).join(",")).join("\n");
4630
+ return [headers, ...rows].map((row) => row.map(escapeCsvField).join(",")).join("\n");
4631
+ }
4632
+ function exportToCsv(schema, responses, filename = "responses.csv", options = {}) {
4633
+ const csvContent = buildCsvContent(schema, responses, options);
4514
4634
  downloadBlob(csvContent, filename, "text/csv;charset=utf-8;");
4515
4635
  }
4516
4636
  function exportToJson(responses, filename = "responses.json") {
@@ -4605,15 +4725,145 @@ function downloadBlob(content, filename, mimeType) {
4605
4725
  document.body.removeChild(link);
4606
4726
  URL.revokeObjectURL(url);
4607
4727
  }
4728
+
4729
+ // src/response-viewer/pagination-utils.ts
4730
+ function paginate(items, currentPage, pageSize) {
4731
+ const totalItems = items.length;
4732
+ const totalPages = Math.max(1, Math.ceil(totalItems / pageSize));
4733
+ const page = Math.max(1, Math.min(currentPage, totalPages));
4734
+ const startIndex = (page - 1) * pageSize;
4735
+ const endIndex = Math.min(startIndex + pageSize, totalItems);
4736
+ const pageItems = items.slice(startIndex, endIndex);
4737
+ return {
4738
+ pageItems,
4739
+ currentPage: page,
4740
+ totalPages,
4741
+ startItem: totalItems === 0 ? 0 : startIndex + 1,
4742
+ endItem: endIndex,
4743
+ totalItems,
4744
+ showPagination: totalItems > pageSize
4745
+ };
4746
+ }
4747
+
4748
+ // src/response-viewer/filter-utils.ts
4749
+ function createEmptyFilterState() {
4750
+ return { filters: [], dateRange: {} };
4751
+ }
4752
+ function hasActiveFilters(state) {
4753
+ return state.filters.length > 0 || !!state.dateRange.from || !!state.dateRange.to;
4754
+ }
4755
+ function applyFilters(responses, state) {
4756
+ if (!hasActiveFilters(state)) return responses;
4757
+ return responses.filter((r) => {
4758
+ if (!matchesDateRange(r.submittedAt, state.dateRange)) return false;
4759
+ for (const filter of state.filters) {
4760
+ if (!matchesFieldFilter(r.values[filter.fieldId], filter)) return false;
4761
+ }
4762
+ return true;
4763
+ });
4764
+ }
4765
+ function matchesDateRange(submittedAt, range) {
4766
+ if (!range.from && !range.to) return true;
4767
+ const submitted = new Date(submittedAt);
4768
+ if (range.from) {
4769
+ const from = new Date(range.from);
4770
+ from.setHours(0, 0, 0, 0);
4771
+ if (submitted < from) return false;
4772
+ }
4773
+ if (range.to) {
4774
+ const to = new Date(range.to);
4775
+ to.setHours(23, 59, 59, 999);
4776
+ if (submitted > to) return false;
4777
+ }
4778
+ return true;
4779
+ }
4780
+ function matchesFieldFilter(value, filter) {
4781
+ switch (filter.operator) {
4782
+ case "equals":
4783
+ if (value == null) return false;
4784
+ return String(value).toLowerCase() === String(filter.value).toLowerCase();
4785
+ case "contains":
4786
+ if (value == null) return false;
4787
+ return String(value).toLowerCase().includes(String(filter.value).toLowerCase());
4788
+ case "gt":
4789
+ if (typeof value !== "number") return false;
4790
+ return value > Number(filter.value);
4791
+ case "lt":
4792
+ if (typeof value !== "number") return false;
4793
+ return value < Number(filter.value);
4794
+ case "is_true":
4795
+ return value === true || value === "true" || value === "agreed";
4796
+ case "is_false":
4797
+ return value === false || value === "false" || value == null;
4798
+ default:
4799
+ return true;
4800
+ }
4801
+ }
4802
+ var PAGE_SIZE_OPTIONS = [10, 25, 50, 100];
4803
+ var FIELD_FILTER_OPERATORS = {
4804
+ text: [
4805
+ { label: "equals", value: "equals" },
4806
+ { label: "contains", value: "contains" }
4807
+ ],
4808
+ number: [
4809
+ { label: "equals", value: "equals" },
4810
+ { label: "greater than", value: "gt" },
4811
+ { label: "less than", value: "lt" }
4812
+ ],
4813
+ boolean: [
4814
+ { label: "is true", value: "is_true" },
4815
+ { label: "is false", value: "is_false" }
4816
+ ],
4817
+ select: [
4818
+ { label: "equals", value: "equals" }
4819
+ ]
4820
+ };
4821
+ function getOperatorsForType(type) {
4822
+ if (type === "number" || type === "slider" || type === "rating" || type === "nps" || type === "opinion_scale" || type === "pain_scale") {
4823
+ return FIELD_FILTER_OPERATORS.number;
4824
+ }
4825
+ if (type === "boolean" || type === "consent") {
4826
+ return FIELD_FILTER_OPERATORS.boolean;
4827
+ }
4828
+ if (type === "single_select" || type === "dropdown" || type === "country_select") {
4829
+ return FIELD_FILTER_OPERATORS.select;
4830
+ }
4831
+ return FIELD_FILTER_OPERATORS.text;
4832
+ }
4833
+ function operatorNeedsValue(operator) {
4834
+ return operator !== "is_true" && operator !== "is_false";
4835
+ }
4608
4836
  function ResponseViewerInner({
4609
4837
  schema,
4610
4838
  responses,
4611
4839
  onResponseSelect,
4612
4840
  height = "500px",
4613
- width = "100%"
4841
+ width = "100%",
4842
+ filename,
4843
+ dateFormat,
4844
+ columnLabels,
4845
+ onExport,
4846
+ pageSize = 25
4614
4847
  }) {
4615
4848
  const [viewMode, setViewMode] = react.useState("table");
4616
4849
  const [selectedResponse, setSelectedResponse] = react.useState(null);
4850
+ const [currentPage, setCurrentPage] = react.useState(1);
4851
+ const [effectivePageSize, setEffectivePageSize] = react.useState(pageSize);
4852
+ const [filterState, setFilterState] = react.useState(createEmptyFilterState);
4853
+ const [showFilterPanel, setShowFilterPanel] = react.useState(false);
4854
+ const [draftFieldId, setDraftFieldId] = react.useState("");
4855
+ const [draftOperator, setDraftOperator] = react.useState("contains");
4856
+ const [draftValue, setDraftValue] = react.useState("");
4857
+ react.useEffect(() => {
4858
+ setCurrentPage(1);
4859
+ }, [responses]);
4860
+ react.useEffect(() => {
4861
+ setEffectivePageSize(pageSize);
4862
+ setCurrentPage(1);
4863
+ }, [pageSize]);
4864
+ const isFiltered = hasActiveFilters(filterState);
4865
+ const filteredResponses = applyFilters(responses, filterState);
4866
+ const pag = paginate(filteredResponses, currentPage, effectivePageSize);
4617
4867
  function handleSelect(response) {
4618
4868
  setSelectedResponse(response);
4619
4869
  onResponseSelect?.(response);
@@ -4621,6 +4871,45 @@ function ResponseViewerInner({
4621
4871
  function handleBack() {
4622
4872
  setSelectedResponse(null);
4623
4873
  }
4874
+ function handlePageSizeChange(newSize) {
4875
+ setEffectivePageSize(Number(newSize));
4876
+ setCurrentPage(1);
4877
+ }
4878
+ function handleAddFilter() {
4879
+ if (!draftFieldId) return;
4880
+ if (operatorNeedsValue(draftOperator) && !draftValue) return;
4881
+ const newFilter = {
4882
+ fieldId: draftFieldId,
4883
+ operator: draftOperator,
4884
+ value: draftOperator === "gt" || draftOperator === "lt" ? Number(draftValue) : draftValue
4885
+ };
4886
+ setFilterState((prev) => ({
4887
+ ...prev,
4888
+ filters: [...prev.filters, newFilter]
4889
+ }));
4890
+ setDraftFieldId("");
4891
+ setDraftOperator("contains");
4892
+ setDraftValue("");
4893
+ setCurrentPage(1);
4894
+ }
4895
+ function handleRemoveFilter(index) {
4896
+ setFilterState((prev) => ({
4897
+ ...prev,
4898
+ filters: prev.filters.filter((_, i) => i !== index)
4899
+ }));
4900
+ setCurrentPage(1);
4901
+ }
4902
+ function handleDateRangeChange(field, value) {
4903
+ setFilterState((prev) => ({
4904
+ ...prev,
4905
+ dateRange: { ...prev.dateRange, [field]: value || void 0 }
4906
+ }));
4907
+ setCurrentPage(1);
4908
+ }
4909
+ function handleClearAllFilters() {
4910
+ setFilterState(createEmptyFilterState());
4911
+ setCurrentPage(1);
4912
+ }
4624
4913
  const questions = getAllQuestions2(schema);
4625
4914
  function getFields(response) {
4626
4915
  return questions.map((q) => ({
@@ -4638,12 +4927,8 @@ function ResponseViewerInner({
4638
4927
  className: "flex flex-col border border-border rounded-lg overflow-hidden bg-background text-foreground",
4639
4928
  style: { height: heightValue, width: widthValue },
4640
4929
  children: [
4641
- !selectedResponse && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex justify-between items-center px-3 py-2 border-b border-border", children: [
4642
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "text-xs text-muted-foreground", children: [
4643
- responses.length,
4644
- " response",
4645
- responses.length !== 1 ? "s" : ""
4646
- ] }),
4930
+ !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: [
4931
+ /* @__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" : ""}` }),
4647
4932
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-1", children: [
4648
4933
  /* @__PURE__ */ jsxRuntime.jsx(
4649
4934
  fieldcraftReact.Button,
@@ -4670,15 +4955,50 @@ function ResponseViewerInner({
4670
4955
  onClick: () => setViewMode("card"),
4671
4956
  children: "Cards"
4672
4957
  }
4958
+ )
4959
+ ] }),
4960
+ /* @__PURE__ */ jsxRuntime.jsx(fieldcraftReact.Separator, { orientation: "vertical", className: "h-5 hidden sm:block" }),
4961
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-1", children: [
4962
+ /* @__PURE__ */ jsxRuntime.jsxs(
4963
+ fieldcraftReact.Button,
4964
+ {
4965
+ variant: showFilterPanel ? "secondary" : "outline",
4966
+ size: "sm",
4967
+ className: cn(
4968
+ "fc-rv-filter__button h-7 text-xs",
4969
+ showFilterPanel && "font-semibold border border-ring"
4970
+ ),
4971
+ onClick: () => setShowFilterPanel((v) => !v),
4972
+ children: [
4973
+ "Filter",
4974
+ isFiltered ? ` (${filterState.filters.length + (filterState.dateRange.from || filterState.dateRange.to ? 1 : 0)})` : ""
4975
+ ]
4976
+ }
4673
4977
  ),
4674
- /* @__PURE__ */ jsxRuntime.jsx(fieldcraftReact.Separator, { orientation: "vertical", className: "h-5 mx-1" }),
4978
+ isFiltered && /* @__PURE__ */ jsxRuntime.jsx(
4979
+ fieldcraftReact.Button,
4980
+ {
4981
+ variant: "ghost",
4982
+ size: "sm",
4983
+ className: "h-7 text-xs text-destructive",
4984
+ onClick: handleClearAllFilters,
4985
+ children: "Clear all"
4986
+ }
4987
+ )
4988
+ ] }),
4989
+ /* @__PURE__ */ jsxRuntime.jsx(fieldcraftReact.Separator, { orientation: "vertical", className: "h-5 hidden sm:block" }),
4990
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-1", children: [
4675
4991
  /* @__PURE__ */ jsxRuntime.jsx(
4676
4992
  fieldcraftReact.Button,
4677
4993
  {
4678
4994
  variant: "outline",
4679
4995
  size: "sm",
4680
4996
  className: "h-7 text-xs",
4681
- onClick: () => exportToCsv(schema, responses),
4997
+ onClick: () => {
4998
+ const csvFilename = filename ? filename.replace(/\.\w+$/, ".csv") : "responses.csv";
4999
+ exportToCsv(schema, responses, csvFilename, { dateFormat, columnLabels });
5000
+ onExport?.("csv", responses.length);
5001
+ },
4682
5002
  disabled: responses.length === 0,
4683
5003
  children: "Export CSV"
4684
5004
  }
@@ -4689,14 +5009,146 @@ function ResponseViewerInner({
4689
5009
  variant: "outline",
4690
5010
  size: "sm",
4691
5011
  className: "h-7 text-xs",
4692
- onClick: () => exportToJson(responses),
5012
+ onClick: () => {
5013
+ const jsonFilename = filename ? filename.replace(/\.\w+$/, ".json") : "responses.json";
5014
+ exportToJson(responses, jsonFilename);
5015
+ onExport?.("json", responses.length);
5016
+ },
4693
5017
  disabled: responses.length === 0,
4694
5018
  children: "Export JSON"
4695
5019
  }
4696
5020
  )
4697
5021
  ] })
4698
5022
  ] }),
4699
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: cn("flex-1 overflow-auto", selectedResponse && "p-4"), children: selectedResponse ? /* @__PURE__ */ jsxRuntime.jsx(
5023
+ !selectedResponse && showFilterPanel && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "fc-rv-filter__panel px-3 py-2 border-b border-border bg-muted/50", children: [
5024
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-wrap items-center gap-2 mb-2", children: [
5025
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs text-muted-foreground w-16 shrink-0", children: "Date:" }),
5026
+ /* @__PURE__ */ jsxRuntime.jsx(
5027
+ fieldcraftReact.Input,
5028
+ {
5029
+ type: "date",
5030
+ className: "h-7 text-xs w-36",
5031
+ value: filterState.dateRange.from ?? "",
5032
+ onChange: (e) => handleDateRangeChange("from", e.target.value)
5033
+ }
5034
+ ),
5035
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs text-muted-foreground", children: "to" }),
5036
+ /* @__PURE__ */ jsxRuntime.jsx(
5037
+ fieldcraftReact.Input,
5038
+ {
5039
+ type: "date",
5040
+ className: "h-7 text-xs w-36",
5041
+ value: filterState.dateRange.to ?? "",
5042
+ onChange: (e) => handleDateRangeChange("to", e.target.value)
5043
+ }
5044
+ )
5045
+ ] }),
5046
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [
5047
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs text-muted-foreground w-16 shrink-0", children: "Field:" }),
5048
+ /* @__PURE__ */ jsxRuntime.jsxs(
5049
+ fieldcraftReact.Select,
5050
+ {
5051
+ value: draftFieldId || void 0,
5052
+ onValueChange: (val) => {
5053
+ setDraftFieldId(val);
5054
+ const q = questions.find((q2) => q2.id === val);
5055
+ if (q) {
5056
+ const ops = getOperatorsForType(q.type);
5057
+ setDraftOperator(ops[0].value);
5058
+ }
5059
+ setDraftValue("");
5060
+ },
5061
+ children: [
5062
+ /* @__PURE__ */ jsxRuntime.jsx(fieldcraftReact.SelectTrigger, { size: "sm", className: "h-7 text-xs w-40", children: /* @__PURE__ */ jsxRuntime.jsx(fieldcraftReact.SelectValue, { placeholder: "Select field" }) }),
5063
+ /* @__PURE__ */ jsxRuntime.jsx(fieldcraftReact.SelectContent, { children: questions.map((q) => /* @__PURE__ */ jsxRuntime.jsx(fieldcraftReact.SelectItem, { value: q.id, children: q.label }, q.id)) })
5064
+ ]
5065
+ }
5066
+ ),
5067
+ draftFieldId && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
5068
+ /* @__PURE__ */ jsxRuntime.jsxs(
5069
+ fieldcraftReact.Select,
5070
+ {
5071
+ value: draftOperator,
5072
+ onValueChange: (val) => setDraftOperator(val),
5073
+ children: [
5074
+ /* @__PURE__ */ jsxRuntime.jsx(fieldcraftReact.SelectTrigger, { size: "sm", className: "h-7 text-xs w-32", children: /* @__PURE__ */ jsxRuntime.jsx(fieldcraftReact.SelectValue, {}) }),
5075
+ /* @__PURE__ */ jsxRuntime.jsx(fieldcraftReact.SelectContent, { children: getOperatorsForType(
5076
+ questions.find((q) => q.id === draftFieldId)?.type ?? "short_text"
5077
+ ).map((op) => /* @__PURE__ */ jsxRuntime.jsx(fieldcraftReact.SelectItem, { value: op.value, children: op.label }, op.value)) })
5078
+ ]
5079
+ }
5080
+ ),
5081
+ operatorNeedsValue(draftOperator) && /* @__PURE__ */ jsxRuntime.jsx(
5082
+ fieldcraftReact.Input,
5083
+ {
5084
+ className: "h-7 text-xs w-32",
5085
+ placeholder: "Value",
5086
+ value: draftValue,
5087
+ onChange: (e) => setDraftValue(e.target.value),
5088
+ onKeyDown: (e) => e.key === "Enter" && handleAddFilter()
5089
+ }
5090
+ ),
5091
+ /* @__PURE__ */ jsxRuntime.jsx(
5092
+ fieldcraftReact.Button,
5093
+ {
5094
+ variant: "outline",
5095
+ size: "sm",
5096
+ className: "h-7 text-xs",
5097
+ onClick: handleAddFilter,
5098
+ children: "Add"
5099
+ }
5100
+ )
5101
+ ] })
5102
+ ] })
5103
+ ] }),
5104
+ !selectedResponse && isFiltered && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-1 px-3 py-1.5 border-b border-border flex-wrap", children: [
5105
+ filterState.dateRange.from && /* @__PURE__ */ jsxRuntime.jsxs(
5106
+ "span",
5107
+ {
5108
+ 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",
5109
+ onClick: () => handleDateRangeChange("from", ""),
5110
+ children: [
5111
+ "From: ",
5112
+ filterState.dateRange.from,
5113
+ " \xD7"
5114
+ ]
5115
+ }
5116
+ ),
5117
+ filterState.dateRange.to && /* @__PURE__ */ jsxRuntime.jsxs(
5118
+ "span",
5119
+ {
5120
+ 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",
5121
+ onClick: () => handleDateRangeChange("to", ""),
5122
+ children: [
5123
+ "To: ",
5124
+ filterState.dateRange.to,
5125
+ " \xD7"
5126
+ ]
5127
+ }
5128
+ ),
5129
+ filterState.filters.map((f, i) => {
5130
+ const q = questions.find((q2) => q2.id === f.fieldId);
5131
+ const label = q?.label ?? f.fieldId;
5132
+ const opLabel = f.operator.replace("_", " ");
5133
+ const valueStr = operatorNeedsValue(f.operator) ? ` "${f.value}"` : "";
5134
+ return /* @__PURE__ */ jsxRuntime.jsxs(
5135
+ "span",
5136
+ {
5137
+ 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",
5138
+ onClick: () => handleRemoveFilter(i),
5139
+ children: [
5140
+ label,
5141
+ " ",
5142
+ opLabel,
5143
+ valueStr,
5144
+ " \xD7"
5145
+ ]
5146
+ },
5147
+ i
5148
+ );
5149
+ })
5150
+ ] }),
5151
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: cn("flex-1 min-w-0 overflow-auto", selectedResponse && "p-4"), children: selectedResponse ? /* @__PURE__ */ jsxRuntime.jsx(
4700
5152
  ResponseDetail,
4701
5153
  {
4702
5154
  response: selectedResponse,
@@ -4707,11 +5159,11 @@ function ResponseViewerInner({
4707
5159
  ResponseTable,
4708
5160
  {
4709
5161
  schema,
4710
- responses,
5162
+ responses: pag.pageItems,
4711
5163
  onRowClick: handleSelect
4712
5164
  }
4713
5165
  ) : /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "grid gap-3 p-3 grid-cols-[repeat(auto-fill,minmax(280px,1fr))]", children: [
4714
- responses.map((response, idx) => /* @__PURE__ */ jsxRuntime.jsx(
5166
+ pag.pageItems.map((response, idx) => /* @__PURE__ */ jsxRuntime.jsx(
4715
5167
  ResponseCard,
4716
5168
  {
4717
5169
  response,
@@ -4720,8 +5172,63 @@ function ResponseViewerInner({
4720
5172
  },
4721
5173
  response.sessionToken || idx
4722
5174
  )),
4723
- responses.length === 0 && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "p-8 text-center text-muted-foreground", children: "No responses yet" })
4724
- ] }) })
5175
+ filteredResponses.length === 0 && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "p-8 text-center text-muted-foreground", children: isFiltered ? "No matching responses" : "No responses yet" })
5176
+ ] }) }),
5177
+ !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: [
5178
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "fc-rv-pagination__info text-xs text-muted-foreground mr-auto", children: [
5179
+ "Showing ",
5180
+ pag.startItem,
5181
+ "\u2013",
5182
+ pag.endItem,
5183
+ " of ",
5184
+ pag.totalItems,
5185
+ " responses"
5186
+ ] }),
5187
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2", children: [
5188
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "fc-rv-pagination__size-select flex items-center gap-1", children: [
5189
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs text-muted-foreground", children: "Per page:" }),
5190
+ /* @__PURE__ */ jsxRuntime.jsxs(
5191
+ fieldcraftReact.Select,
5192
+ {
5193
+ value: String(effectivePageSize),
5194
+ onValueChange: handlePageSizeChange,
5195
+ children: [
5196
+ /* @__PURE__ */ jsxRuntime.jsx(fieldcraftReact.SelectTrigger, { size: "sm", className: "h-7 w-16 text-xs", children: /* @__PURE__ */ jsxRuntime.jsx(fieldcraftReact.SelectValue, {}) }),
5197
+ /* @__PURE__ */ jsxRuntime.jsx(fieldcraftReact.SelectContent, { children: PAGE_SIZE_OPTIONS.map((size) => /* @__PURE__ */ jsxRuntime.jsx(fieldcraftReact.SelectItem, { value: String(size), children: size }, size)) })
5198
+ ]
5199
+ }
5200
+ )
5201
+ ] }),
5202
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "text-xs text-muted-foreground", children: [
5203
+ "Page ",
5204
+ pag.currentPage,
5205
+ " of ",
5206
+ pag.totalPages
5207
+ ] }),
5208
+ /* @__PURE__ */ jsxRuntime.jsx(
5209
+ fieldcraftReact.Button,
5210
+ {
5211
+ variant: "outline",
5212
+ size: "sm",
5213
+ className: "fc-rv-pagination__button h-7 text-xs",
5214
+ onClick: () => setCurrentPage((p) => p - 1),
5215
+ disabled: pag.currentPage <= 1,
5216
+ children: "Previous"
5217
+ }
5218
+ ),
5219
+ /* @__PURE__ */ jsxRuntime.jsx(
5220
+ fieldcraftReact.Button,
5221
+ {
5222
+ variant: "outline",
5223
+ size: "sm",
5224
+ className: "fc-rv-pagination__button h-7 text-xs",
5225
+ onClick: () => setCurrentPage((p) => p + 1),
5226
+ disabled: pag.currentPage >= pag.totalPages,
5227
+ children: "Next"
5228
+ }
5229
+ )
5230
+ ] })
5231
+ ] })
4725
5232
  ]
4726
5233
  }
4727
5234
  );