@fayz-ai/plugin-forms 0.9.4 → 0.10.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.
@@ -1 +1 @@
1
- {"version":3,"file":"AddDocumentDropdown.d.ts","sourceRoot":"","sources":["../../src/components/AddDocumentDropdown.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAwC,MAAM,OAAO,CAAA;AAY5D,OAAO,EAAuB,KAAK,kBAAkB,EAAE,MAAM,uBAAuB,CAAA;AAEpF,UAAU,wBAAwB;IAChC,QAAQ,EAAE,MAAM,CAAA;IAChB,QAAQ,EAAE,CAAC,MAAM,EAAE,kBAAkB,KAAK,IAAI,CAAA;CAC/C;AAED,wBAAgB,mBAAmB,CAAC,EAAE,QAAQ,EAAE,QAAQ,EAAE,EAAE,wBAAwB,qBAwEnF"}
1
+ {"version":3,"file":"AddDocumentDropdown.d.ts","sourceRoot":"","sources":["../../src/components/AddDocumentDropdown.tsx"],"names":[],"mappings":"AAAA,OAAO,KAA4D,MAAM,OAAO,CAAA;AAWhF,OAAO,EAAuB,KAAK,kBAAkB,EAAE,MAAM,uBAAuB,CAAA;AAEpF,UAAU,wBAAwB;IAChC,QAAQ,EAAE,MAAM,CAAA;IAChB,QAAQ,EAAE,CAAC,MAAM,EAAE,kBAAkB,KAAK,IAAI,CAAA;CAC/C;AAQD,wBAAgB,mBAAmB,CAAC,EAAE,QAAQ,EAAE,QAAQ,EAAE,EAAE,wBAAwB,qBA6KnF"}
package/dist/index.js CHANGED
@@ -1,10 +1,10 @@
1
1
  import { createOrUpdateFormTool, prefillFromPerson, FormRenderer, printDocument } from './chunk-RBLXIKWB.js';
2
- import React2, { useState, useCallback, useRef, useEffect } from 'react';
3
- import { registerTranslations, createSafeDataProvider, getSupabaseClientOptional, useTranslation, getActiveTenantId } from '@fayz-ai/core';
4
- import { PluginSettingsPanel, registerTimelineSource, useLimitGuard, invalidateLimit, PermissionGate } from '@fayz-ai/admin';
2
+ import React2, { useState, useCallback, useRef, useMemo, useEffect } from 'react';
3
+ import { registerTranslations, createSafeDataProvider, getSupabaseClientOptional, useTranslation, getActiveTenantId, emitDomainEvent, useDataVersion } from '@fayz-ai/core';
4
+ import { PluginSettingsPanel, registerTimelineSource, useLimitGuard, invalidateLimit, askAI, PermissionGate, QuickActionsButton } from '@fayz-ai/admin';
5
5
  import { create } from 'zustand';
6
- import { FileText, Image, Paperclip, Search, Plus, Pencil, Archive, ArrowLeft, Printer, ChevronDown, Upload, Eye } from 'lucide-react';
7
- import { Input, Button, Card, CardContent, Badge, useSaveBar, Dropdown, DropdownTrigger, DropdownContent, DropdownSeparator, DropdownLabel, DropdownItem } from '@fayz-ai/ui';
6
+ import { FileText, Image, Paperclip, Pencil, Archive, Plus, Search, ArrowLeft, Printer, ChevronDown, Upload, Eye } from 'lucide-react';
7
+ import { Badge, Button, PageHeaderActions, Card, DataTable, useSaveBar, Dropdown, DropdownTrigger, DropdownContent, DropdownSeparator, DropdownLabel } from '@fayz-ai/ui';
8
8
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
9
9
 
10
10
  // src/data/tables.ts
@@ -503,6 +503,14 @@ function createSupabaseFormsProvider() {
503
503
  }
504
504
  };
505
505
  }
506
+ var FORM_TEMPLATE_EVENT = "forms.template.";
507
+ function emitTemplateEvent(action, id, name) {
508
+ emitDomainEvent(
509
+ `${FORM_TEMPLATE_EVENT}${action}`,
510
+ { record_id: id, record_name: name ?? null },
511
+ { tenantId: getActiveTenantId() ?? null, subject: { type: "form_template", id } }
512
+ );
513
+ }
506
514
  function createCustomFormsStore(provider) {
507
515
  return create((set) => ({
508
516
  templates: [],
@@ -557,6 +565,7 @@ function createCustomFormsStore(provider) {
557
565
  async createTemplate(input) {
558
566
  const template = await provider.createTemplate(input);
559
567
  set((s) => ({ templates: [template, ...s.templates], templatesTotal: s.templatesTotal + 1 }));
568
+ emitTemplateEvent("created", template.id, template.name);
560
569
  return template;
561
570
  },
562
571
  async updateTemplate(id, input) {
@@ -565,10 +574,12 @@ function createCustomFormsStore(provider) {
565
574
  templates: s.templates.map((t) => t.id === id ? template : t),
566
575
  activeTemplate: s.activeTemplate?.id === id ? template : s.activeTemplate
567
576
  }));
577
+ emitTemplateEvent("updated", template.id, template.name);
568
578
  return template;
569
579
  },
570
580
  async deleteTemplate(id) {
571
581
  await provider.deleteTemplate(id);
582
+ emitTemplateEvent("deleted", id);
572
583
  set((s) => ({
573
584
  templates: s.templates.filter((t) => t.id !== id),
574
585
  templatesTotal: s.templatesTotal - 1
@@ -628,6 +639,16 @@ var en = {
628
639
  "customForms.settingsSubtitle": "Create and manage custom forms for your business",
629
640
  // Templates
630
641
  "customForms.templates": "Templates",
642
+ "customForms.newWithAIHint": "Describe the form and I build the fields.",
643
+ "customForms.newByHand": "Create by hand",
644
+ "customForms.newByHandHint": "Build it field by field in the builder.",
645
+ "customForms.askPlaceholder": "Describe the form\u2026",
646
+ "customForms.askPersona": "Form builder",
647
+ "customForms.askPitch": "Tell me the form you need.",
648
+ "customForms.askDescription": "Describe what it is for and I propose the fields, their types and options in one go. You check and adjust before saving.",
649
+ "customForms.askSuggestion.0": "A full intake form",
650
+ "customForms.askSuggestion.1": "Consent form for an aesthetic procedure",
651
+ "customForms.askSuggestion.2": "Session progress note",
631
652
  "customForms.newTemplate": "New Template",
632
653
  "customForms.editTemplate": "Edit Template",
633
654
  "customForms.templateName": "Form name",
@@ -691,6 +712,8 @@ var en = {
691
712
  "customForms.documents": "Documents",
692
713
  "customForms.addDocument": "Add Document",
693
714
  "customForms.selectTemplate": "Select a form",
715
+ "customForms.searchDocumentTypes": "Search forms or files...",
716
+ "customForms.noResults": "No results",
694
717
  "customForms.noDocuments": "No documents yet",
695
718
  "customForms.noDocumentsDescription": "Add a document to start recording information.",
696
719
  "customForms.documentTitle": "Document title",
@@ -702,6 +725,10 @@ var en = {
702
725
  "customForms.printPdf": "Print / PDF",
703
726
  "customForms.usageCount": "{{count}} filled in",
704
727
  "customForms.usageLast": "last on {{date}}",
728
+ // Template list columns
729
+ "customForms.template": "Template",
730
+ "customForms.categoryColumn": "Category",
731
+ "customForms.usageColumn": "Usage",
705
732
  // Registry
706
733
  "registry.form-categories": "Categories",
707
734
  "registry.form-categories.description": "Categories for organizing form templates",
@@ -719,6 +746,16 @@ var ptBR = {
719
746
  "customForms.settingsSubtitle": "Crie e gerencie formul\xE1rios personalizados para o seu neg\xF3cio",
720
747
  // Templates
721
748
  "customForms.templates": "Modelos",
749
+ "customForms.newWithAIHint": "Descreva o formul\xE1rio e eu monto os campos.",
750
+ "customForms.newByHand": "Criar manualmente",
751
+ "customForms.newByHandHint": "Montar campo a campo no construtor.",
752
+ "customForms.askPlaceholder": "Descreva o formul\xE1rio\u2026",
753
+ "customForms.askPersona": "Montador de formul\xE1rios",
754
+ "customForms.askPitch": "Diga o formul\xE1rio que voc\xEA precisa.",
755
+ "customForms.askDescription": "Descreva para que serve e eu proponho os campos, os tipos e as op\xE7\xF5es de uma vez. Voc\xEA confere e ajusta antes de salvar.",
756
+ "customForms.askSuggestion.0": "Uma ficha de anamnese completa",
757
+ "customForms.askSuggestion.1": "Termo de consentimento para procedimento est\xE9tico",
758
+ "customForms.askSuggestion.2": "Ficha de evolu\xE7\xE3o de atendimento",
722
759
  "customForms.newTemplate": "Novo Modelo",
723
760
  "customForms.editTemplate": "Editar Modelo",
724
761
  "customForms.templateName": "Nome do formul\xE1rio",
@@ -782,6 +819,8 @@ var ptBR = {
782
819
  "customForms.documents": "Documentos",
783
820
  "customForms.addDocument": "Novo Documento",
784
821
  "customForms.selectTemplate": "Selecione um formul\xE1rio",
822
+ "customForms.searchDocumentTypes": "Buscar formul\xE1rio ou arquivo...",
823
+ "customForms.noResults": "Nenhum resultado",
785
824
  "customForms.noDocuments": "Nenhum documento",
786
825
  "customForms.noDocumentsDescription": "Adicione um documento para come\xE7ar a registrar informa\xE7\xF5es.",
787
826
  "customForms.documentTitle": "T\xEDtulo do documento",
@@ -793,6 +832,10 @@ var ptBR = {
793
832
  "customForms.printPdf": "Imprimir / PDF",
794
833
  "customForms.usageCount": "{{count}} preenchidos",
795
834
  "customForms.usageLast": "\xFAltimo em {{date}}",
835
+ // Colunas da lista de modelos
836
+ "customForms.template": "Modelo",
837
+ "customForms.categoryColumn": "Categoria",
838
+ "customForms.usageColumn": "Uso",
796
839
  // Registry
797
840
  "registry.form-categories": "Categorias",
798
841
  "registry.form-categories.description": "Categorias para organizar modelos de formul\xE1rios",
@@ -838,13 +881,40 @@ function TemplateListView({ store, config, onEdit, onNew }) {
838
881
  const loading = store((s) => s.templatesLoading);
839
882
  const usage = store((s) => s.templateUsage);
840
883
  const orgId = getFormsTenantId();
884
+ const createActions = useMemo(() => [
885
+ {
886
+ id: "with-ai",
887
+ label: t("assistant.askAI"),
888
+ description: t("customForms.newWithAIHint"),
889
+ icon: "WandSparkles",
890
+ highlight: true,
891
+ action: () => askAI({
892
+ hint: t("customForms.askPlaceholder"),
893
+ intro: {
894
+ role: "setup",
895
+ persona: t("customForms.askPersona"),
896
+ pitch: t("customForms.askPitch"),
897
+ description: t("customForms.askDescription"),
898
+ suggestions: [0, 1, 2].map((i) => ({ label: t(`customForms.askSuggestion.${i}`) }))
899
+ }
900
+ })
901
+ },
902
+ {
903
+ id: "by-hand",
904
+ label: t("customForms.newByHand"),
905
+ description: t("customForms.newByHandHint"),
906
+ icon: "SlidersHorizontal",
907
+ action: onNew
908
+ }
909
+ ], [t, onNew]);
910
+ const version = useDataVersion(`${FORM_TEMPLATE_EVENT}*`);
841
911
  useEffect(() => {
842
912
  if (!orgId) return;
843
913
  const timer = setTimeout(() => {
844
914
  store.getState().fetchTemplates({ search: search || void 0 });
845
915
  }, 50);
846
916
  return () => clearTimeout(timer);
847
- }, [orgId, search]);
917
+ }, [orgId, search, version]);
848
918
  useEffect(() => {
849
919
  if (!orgId) return;
850
920
  void store.getState().fetchTemplateUsage();
@@ -855,106 +925,123 @@ function TemplateListView({ store, config, onEdit, onNew }) {
855
925
  }, [store, t]);
856
926
  const visibleTemplates = templates.filter((tpl) => tpl.isActive !== false);
857
927
  const categoryLabel = (cat) => t(`customForms.category.${cat}`);
928
+ const columns = useMemo(() => [
929
+ {
930
+ id: "name",
931
+ header: t("customForms.template"),
932
+ cell: ({ row }) => /* @__PURE__ */ jsxs("div", { className: "min-w-0", children: [
933
+ /* @__PURE__ */ jsx("span", { className: "font-medium", children: row.original.name }),
934
+ row.original.description && /* @__PURE__ */ jsx("p", { className: "truncate text-xs text-muted-foreground", children: row.original.description })
935
+ ] })
936
+ },
937
+ {
938
+ id: "category",
939
+ header: t("customForms.categoryColumn"),
940
+ cell: ({ row }) => /* @__PURE__ */ jsx(Badge, { variant: "secondary", className: `text-[10px] ${CATEGORY_COLORS[row.original.category]}`, children: categoryLabel(row.original.category) })
941
+ },
942
+ {
943
+ id: "fields",
944
+ header: t("customForms.builder.fields"),
945
+ cell: ({ row }) => /* @__PURE__ */ jsxs("span", { className: "tabular-nums text-muted-foreground", children: [
946
+ row.original.schema.fields.length,
947
+ row.original.version > 1 && /* @__PURE__ */ jsxs("span", { className: "ml-2", children: [
948
+ "v",
949
+ row.original.version
950
+ ] })
951
+ ] })
952
+ },
953
+ {
954
+ id: "usage",
955
+ header: t("customForms.usageColumn"),
956
+ // "0 preenchimentos" é informação, não ausência dela: é o que separa um
957
+ // formulário que o salão usa todo dia de um que sobrou de uma importação.
958
+ cell: ({ row }) => {
959
+ const u = usage[row.original.id];
960
+ return /* @__PURE__ */ jsxs("span", { className: "text-xs text-muted-foreground", children: [
961
+ t("customForms.usageCount", { count: String(u?.documentCount ?? 0) }),
962
+ u?.lastDocumentAt && /* @__PURE__ */ jsxs(Fragment, { children: [
963
+ /* @__PURE__ */ jsx("span", { className: "px-1", children: "\xB7" }),
964
+ t("customForms.usageLast", { date: new Date(u.lastDocumentAt).toLocaleDateString() })
965
+ ] })
966
+ ] });
967
+ }
968
+ },
969
+ {
970
+ id: "actions",
971
+ header: "",
972
+ cell: ({ row }) => /* @__PURE__ */ jsxs("div", { className: "flex justify-end gap-1", children: [
973
+ /* @__PURE__ */ jsx(
974
+ "button",
975
+ {
976
+ onClick: (e) => {
977
+ e.stopPropagation();
978
+ onEdit(row.original);
979
+ },
980
+ className: "rounded-md p-1.5 hover:bg-muted",
981
+ title: t("common.edit"),
982
+ children: /* @__PURE__ */ jsx(Pencil, { className: "h-3.5 w-3.5 text-muted-foreground" })
983
+ }
984
+ ),
985
+ /* @__PURE__ */ jsx(
986
+ "button",
987
+ {
988
+ onClick: (e) => {
989
+ e.stopPropagation();
990
+ void handleArchive(row.original);
991
+ },
992
+ className: "rounded-md p-1.5 hover:bg-muted",
993
+ title: t("customForms.archive"),
994
+ children: /* @__PURE__ */ jsx(Archive, { className: "h-3.5 w-3.5 text-muted-foreground" })
995
+ }
996
+ )
997
+ ] })
998
+ }
999
+ ], [t, usage, onEdit, handleArchive]);
1000
+ const emptyState = /* @__PURE__ */ jsxs("div", { className: "flex flex-col items-center justify-center py-6 text-center", children: [
1001
+ /* @__PURE__ */ jsx("div", { className: "mb-4 flex h-12 w-12 items-center justify-center rounded-full bg-muted", children: /* @__PURE__ */ jsx(FileText, { className: "h-5 w-5 text-muted-foreground" }) }),
1002
+ /* @__PURE__ */ jsx("p", { className: "font-medium text-foreground", children: t("customForms.noTemplates") }),
1003
+ /* @__PURE__ */ jsx("p", { className: "mt-1 max-w-sm text-sm text-muted-foreground", children: t("customForms.noTemplatesDescription") }),
1004
+ /* @__PURE__ */ jsx(PermissionGate, { feature: "custom_forms", action: "create", children: /* @__PURE__ */ jsxs(Button, { onClick: onNew, size: "sm", className: "mt-4", children: [
1005
+ /* @__PURE__ */ jsx(Plus, { className: "mr-1.5 h-3.5 w-3.5" }),
1006
+ t("customForms.newTemplate")
1007
+ ] }) })
1008
+ ] });
858
1009
  return /* @__PURE__ */ jsxs("div", { className: "space-y-4", children: [
859
- /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3", children: [
860
- /* @__PURE__ */ jsxs("div", { className: "relative flex-1 max-w-xs", children: [
861
- /* @__PURE__ */ jsx(Search, { className: "absolute left-2.5 top-1/2 -translate-y-1/2 h-3.5 w-3.5 text-muted-foreground" }),
1010
+ /* @__PURE__ */ jsx(PermissionGate, { feature: "custom_forms", action: "create", children: /* @__PURE__ */ jsx(PageHeaderActions, { children: /* @__PURE__ */ jsx(
1011
+ QuickActionsButton,
1012
+ {
1013
+ label: t("customForms.newTemplate"),
1014
+ variant: "default",
1015
+ actions: createActions
1016
+ }
1017
+ ) }) }),
1018
+ /* @__PURE__ */ jsxs(Card, { className: "overflow-hidden", children: [
1019
+ /* @__PURE__ */ jsx("div", { className: "flex flex-wrap items-center gap-2 border-b border-border px-3 py-2", children: /* @__PURE__ */ jsxs("div", { className: "relative min-w-[10rem] flex-1", children: [
1020
+ /* @__PURE__ */ jsx(Search, { className: "pointer-events-none absolute left-2.5 top-1/2 h-3.5 w-3.5 -translate-y-1/2 text-muted-foreground" }),
862
1021
  /* @__PURE__ */ jsx(
863
- Input,
1022
+ "input",
864
1023
  {
1024
+ type: "text",
865
1025
  value: search,
866
1026
  onChange: (e) => setSearch(e.target.value),
867
1027
  placeholder: t("common.search"),
868
- className: "h-9 pl-8 text-sm"
1028
+ className: "w-full bg-transparent py-1.5 pl-8 pr-3 text-sm placeholder:text-muted-foreground focus:outline-none"
869
1029
  }
870
1030
  )
871
- ] }),
872
- /* @__PURE__ */ jsx(PermissionGate, { feature: "custom_forms", action: "create", children: /* @__PURE__ */ jsxs(Button, { onClick: onNew, size: "sm", className: "h-9", children: [
873
- /* @__PURE__ */ jsx(Plus, { className: "h-3.5 w-3.5 mr-1.5" }),
874
- t("customForms.newTemplate")
875
- ] }) })
876
- ] }),
877
- !loading && visibleTemplates.length === 0 && /* @__PURE__ */ jsx(Card, { children: /* @__PURE__ */ jsxs(CardContent, { className: "flex flex-col items-center justify-center py-16 text-center", children: [
878
- /* @__PURE__ */ jsx("div", { className: "flex h-12 w-12 items-center justify-center rounded-full bg-muted mb-4", children: /* @__PURE__ */ jsx(FileText, { className: "h-5 w-5 text-muted-foreground" }) }),
879
- /* @__PURE__ */ jsx("p", { className: "font-medium", children: t("customForms.noTemplates") }),
880
- /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground mt-1 max-w-sm", children: t("customForms.noTemplatesDescription") }),
881
- /* @__PURE__ */ jsx(PermissionGate, { feature: "custom_forms", action: "create", children: /* @__PURE__ */ jsxs(Button, { onClick: onNew, size: "sm", className: "mt-4", children: [
882
- /* @__PURE__ */ jsx(Plus, { className: "h-3.5 w-3.5 mr-1.5" }),
883
- t("customForms.newTemplate")
884
- ] }) })
885
- ] }) }),
886
- visibleTemplates.length > 0 && /* @__PURE__ */ jsx("div", { className: "grid gap-3 sm:grid-cols-2 lg:grid-cols-3", children: visibleTemplates.map((template) => /* @__PURE__ */ jsx(
887
- Card,
888
- {
889
- className: "group cursor-pointer hover:shadow-md transition-shadow",
890
- onClick: () => onEdit(template),
891
- children: /* @__PURE__ */ jsxs(CardContent, { className: "p-4", children: [
892
- /* @__PURE__ */ jsxs("div", { className: "flex items-start justify-between gap-2", children: [
893
- /* @__PURE__ */ jsxs("div", { className: "min-w-0 flex-1", children: [
894
- /* @__PURE__ */ jsx("h3", { className: "font-medium text-sm truncate", children: template.name }),
895
- template.description && /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground mt-0.5 line-clamp-2", children: template.description })
896
- ] }),
897
- /* @__PURE__ */ jsxs("div", { className: "flex gap-1 opacity-0 group-hover:opacity-100 transition-opacity", children: [
898
- /* @__PURE__ */ jsx(
899
- "button",
900
- {
901
- onClick: (e) => {
902
- e.stopPropagation();
903
- onEdit(template);
904
- },
905
- className: "p-1.5 rounded-md hover:bg-muted",
906
- children: /* @__PURE__ */ jsx(Pencil, { className: "h-3.5 w-3.5 text-muted-foreground" })
907
- }
908
- ),
909
- /* @__PURE__ */ jsx(
910
- "button",
911
- {
912
- onClick: (e) => {
913
- e.stopPropagation();
914
- handleArchive(template);
915
- },
916
- className: "p-1.5 rounded-md hover:bg-muted",
917
- title: t("customForms.archive"),
918
- children: /* @__PURE__ */ jsx(Archive, { className: "h-3.5 w-3.5 text-muted-foreground" })
919
- }
920
- )
921
- ] })
922
- ] }),
923
- /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 mt-3", children: [
924
- /* @__PURE__ */ jsx(Badge, { variant: "secondary", className: `text-[10px] ${CATEGORY_COLORS[template.category]}`, children: categoryLabel(template.category) }),
925
- /* @__PURE__ */ jsxs("span", { className: "text-[10px] text-muted-foreground", children: [
926
- template.schema.fields.length,
927
- " ",
928
- t("customForms.builder.fields").toLowerCase()
929
- ] }),
930
- template.version > 1 && /* @__PURE__ */ jsxs("span", { className: "text-[10px] text-muted-foreground", children: [
931
- "v",
932
- template.version
933
- ] })
934
- ] }),
935
- /* @__PURE__ */ jsxs("p", { className: "mt-2 text-[10px] text-muted-foreground", children: [
936
- t("customForms.usageCount", {
937
- count: String(usage[template.id]?.documentCount ?? 0)
938
- }),
939
- usage[template.id]?.lastDocumentAt && /* @__PURE__ */ jsxs(Fragment, { children: [
940
- /* @__PURE__ */ jsx("span", { className: "px-1", children: "\xB7" }),
941
- t("customForms.usageLast", {
942
- date: new Date(usage[template.id].lastDocumentAt).toLocaleDateString()
943
- })
944
- ] })
945
- ] })
946
- ] })
947
- },
948
- template.id
949
- )) }),
950
- loading && /* @__PURE__ */ jsx("div", { className: "grid gap-3 sm:grid-cols-2 lg:grid-cols-3", children: Array.from({ length: 3 }).map((_, i) => /* @__PURE__ */ jsx(Card, { children: /* @__PURE__ */ jsxs(CardContent, { className: "p-4 space-y-3", children: [
951
- /* @__PURE__ */ jsx("div", { className: "h-4 w-2/3 bg-muted animate-pulse rounded" }),
952
- /* @__PURE__ */ jsx("div", { className: "h-3 w-full bg-muted animate-pulse rounded" }),
953
- /* @__PURE__ */ jsxs("div", { className: "flex gap-2", children: [
954
- /* @__PURE__ */ jsx("div", { className: "h-5 w-16 bg-muted animate-pulse rounded-full" }),
955
- /* @__PURE__ */ jsx("div", { className: "h-5 w-12 bg-muted animate-pulse rounded-full" })
956
- ] })
957
- ] }) }, i)) })
1031
+ ] }) }),
1032
+ /* @__PURE__ */ jsx(
1033
+ DataTable,
1034
+ {
1035
+ variant: "bare",
1036
+ columns,
1037
+ data: loading ? [] : visibleTemplates,
1038
+ loading,
1039
+ skeletonRows: 3,
1040
+ emptyMessage: emptyState,
1041
+ onRowClick: (row) => onEdit(row)
1042
+ }
1043
+ )
1044
+ ] })
958
1045
  ] });
959
1046
  }
960
1047
  var ROUTE_BASE = "/settings/custom_forms";
@@ -1431,13 +1518,24 @@ async function getAllDocumentTypes(personId) {
1431
1518
  all.sort((a, b) => (a.groupOrder ?? 50) - (b.groupOrder ?? 50) || (a.order ?? 0) - (b.order ?? 0));
1432
1519
  return all;
1433
1520
  }
1521
+ var SEARCH_THRESHOLD = 8;
1522
+ var normalize = (s) => s.normalize("NFD").replace(/[\u0300-\u036f]/g, "").toLowerCase();
1434
1523
  function AddDocumentDropdown({ personId, onSelect }) {
1435
1524
  const t = useTranslation();
1525
+ const [open, setOpen] = useState(false);
1436
1526
  const [options, setOptions] = useState([]);
1437
1527
  const [loaded, setLoaded] = useState(false);
1528
+ const [query, setQuery] = useState("");
1529
+ const [activeIndex, setActiveIndex] = useState(0);
1438
1530
  const loadingRef = useRef(false);
1439
- const handleOpen = useCallback((open) => {
1440
- if (!open || loaded || loadingRef.current) return;
1531
+ const inputRef = useRef(null);
1532
+ const listRef = useRef(null);
1533
+ const handleOpenChange = useCallback((next) => {
1534
+ setOpen(next);
1535
+ if (!next) return;
1536
+ setQuery("");
1537
+ setActiveIndex(0);
1538
+ if (loaded || loadingRef.current) return;
1441
1539
  loadingRef.current = true;
1442
1540
  getAllDocumentTypes(personId).then((opts) => {
1443
1541
  setOptions(opts);
@@ -1445,38 +1543,110 @@ function AddDocumentDropdown({ personId, onSelect }) {
1445
1543
  loadingRef.current = false;
1446
1544
  });
1447
1545
  }, [personId, loaded]);
1448
- const groups = [];
1449
- for (const opt of options) {
1450
- let group = groups.find((g) => g.label === opt.group);
1451
- if (!group) {
1452
- group = { label: opt.group, items: [] };
1453
- groups.push(group);
1546
+ const filtered = useMemo(() => {
1547
+ const q = normalize(query.trim());
1548
+ if (!q) return options;
1549
+ return options.filter(
1550
+ (o) => normalize(o.label).includes(q) || (o.description ? normalize(o.description).includes(q) : false)
1551
+ );
1552
+ }, [options, query]);
1553
+ const groups = useMemo(() => {
1554
+ const acc = [];
1555
+ for (const opt of filtered) {
1556
+ let group = acc.find((g) => g.label === opt.group);
1557
+ if (!group) {
1558
+ group = { label: opt.group, items: [] };
1559
+ acc.push(group);
1560
+ }
1561
+ group.items.push(opt);
1454
1562
  }
1455
- group.items.push(opt);
1456
- }
1457
- return /* @__PURE__ */ jsxs(Dropdown, { onOpenChange: handleOpen, children: [
1563
+ return acc;
1564
+ }, [filtered]);
1565
+ const showSearch = loaded && options.length > SEARCH_THRESHOLD;
1566
+ useEffect(() => {
1567
+ if (!open || !showSearch) return;
1568
+ const id = requestAnimationFrame(() => inputRef.current?.focus());
1569
+ return () => cancelAnimationFrame(id);
1570
+ }, [open, showSearch]);
1571
+ useEffect(() => {
1572
+ listRef.current?.querySelector(`[data-index="${activeIndex}"]`)?.scrollIntoView({ block: "nearest" });
1573
+ }, [activeIndex]);
1574
+ const choose = useCallback((opt) => {
1575
+ setOpen(false);
1576
+ onSelect(opt);
1577
+ }, [onSelect]);
1578
+ const handleInputKeyDown = (e) => {
1579
+ if (e.key === "Escape") return;
1580
+ if (e.key === "ArrowDown") {
1581
+ e.preventDefault();
1582
+ setActiveIndex((i) => Math.min(filtered.length - 1, i + 1));
1583
+ } else if (e.key === "ArrowUp") {
1584
+ e.preventDefault();
1585
+ setActiveIndex((i) => Math.max(0, i - 1));
1586
+ } else if (e.key === "Enter") {
1587
+ e.preventDefault();
1588
+ const opt = filtered[activeIndex];
1589
+ if (opt) choose(opt);
1590
+ }
1591
+ e.stopPropagation();
1592
+ };
1593
+ let flatIndex = -1;
1594
+ return /* @__PURE__ */ jsxs(Dropdown, { open, onOpenChange: handleOpenChange, children: [
1458
1595
  /* @__PURE__ */ jsx(DropdownTrigger, { asChild: true, children: /* @__PURE__ */ jsxs(Button, { size: "sm", className: "h-8", children: [
1459
1596
  /* @__PURE__ */ jsx(Plus, { className: "h-3.5 w-3.5 mr-1" }),
1460
1597
  t("customForms.addDocument"),
1461
1598
  /* @__PURE__ */ jsx(ChevronDown, { className: "h-3 w-3 ml-1 opacity-50" })
1462
1599
  ] }) }),
1463
- /* @__PURE__ */ jsxs(DropdownContent, { align: "end", className: "w-56", children: [
1464
- groups.map((group, gi) => /* @__PURE__ */ jsxs(React2.Fragment, { children: [
1465
- gi > 0 && /* @__PURE__ */ jsx(DropdownSeparator, {}),
1466
- /* @__PURE__ */ jsx(DropdownLabel, { className: "text-[10px] uppercase tracking-wider", children: group.label }),
1467
- group.items.map((opt) => {
1468
- const Icon = opt.icon;
1469
- return /* @__PURE__ */ jsxs(DropdownItem, { onClick: () => onSelect(opt), className: "gap-2", children: [
1470
- /* @__PURE__ */ jsx(Icon, { className: "h-4 w-4 text-muted-foreground shrink-0" }),
1471
- /* @__PURE__ */ jsxs("div", { className: "flex-1 min-w-0", children: [
1472
- /* @__PURE__ */ jsx("p", { className: "text-sm truncate", children: opt.label }),
1473
- opt.description && /* @__PURE__ */ jsx("p", { className: "text-[10px] text-muted-foreground truncate", children: opt.description })
1474
- ] })
1475
- ] }, opt.id);
1476
- })
1477
- ] }, group.label)),
1478
- !loaded && /* @__PURE__ */ jsx("div", { className: "flex items-center justify-center py-3", children: /* @__PURE__ */ jsx("div", { className: "h-4 w-4 border-2 border-primary border-t-transparent animate-spin rounded-full" }) }),
1479
- loaded && options.length === 0 && /* @__PURE__ */ jsx("div", { className: "px-2 py-3 text-center text-xs text-muted-foreground", children: t("customForms.noTemplates") })
1600
+ /* @__PURE__ */ jsxs(DropdownContent, { align: "end", className: "w-72 p-0", children: [
1601
+ showSearch && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 border-b px-2.5 py-2", children: [
1602
+ /* @__PURE__ */ jsx(Search, { className: "h-3.5 w-3.5 text-muted-foreground shrink-0" }),
1603
+ /* @__PURE__ */ jsx(
1604
+ "input",
1605
+ {
1606
+ ref: inputRef,
1607
+ value: query,
1608
+ onChange: (e) => {
1609
+ setQuery(e.target.value);
1610
+ setActiveIndex(0);
1611
+ },
1612
+ onKeyDown: handleInputKeyDown,
1613
+ placeholder: t("customForms.searchDocumentTypes"),
1614
+ className: "flex-1 bg-transparent text-sm outline-none placeholder:text-muted-foreground"
1615
+ }
1616
+ )
1617
+ ] }),
1618
+ /* @__PURE__ */ jsxs("div", { ref: listRef, className: "max-h-[min(60vh,20rem)] overflow-y-auto overscroll-contain p-1", children: [
1619
+ groups.map((group, gi) => /* @__PURE__ */ jsxs(React2.Fragment, { children: [
1620
+ gi > 0 && /* @__PURE__ */ jsx(DropdownSeparator, {}),
1621
+ /* @__PURE__ */ jsx(DropdownLabel, { className: "text-[10px] uppercase tracking-wider", children: group.label }),
1622
+ group.items.map((opt) => {
1623
+ const Icon = opt.icon;
1624
+ flatIndex += 1;
1625
+ const index = flatIndex;
1626
+ return /* @__PURE__ */ jsxs(
1627
+ "button",
1628
+ {
1629
+ type: "button",
1630
+ "data-index": index,
1631
+ onClick: () => choose(opt),
1632
+ onMouseEnter: () => setActiveIndex(index),
1633
+ className: `relative flex w-full cursor-default select-none items-center gap-2 rounded-button px-2 py-1.5 text-left text-sm outline-none transition-colors ${index === activeIndex ? "bg-muted" : ""}`,
1634
+ children: [
1635
+ /* @__PURE__ */ jsx(Icon, { className: "h-4 w-4 text-muted-foreground shrink-0" }),
1636
+ /* @__PURE__ */ jsxs("div", { className: "flex-1 min-w-0", children: [
1637
+ /* @__PURE__ */ jsx("p", { className: "text-sm truncate", children: opt.label }),
1638
+ opt.description && /* @__PURE__ */ jsx("p", { className: "text-[10px] text-muted-foreground truncate", children: opt.description })
1639
+ ] })
1640
+ ]
1641
+ },
1642
+ opt.id
1643
+ );
1644
+ })
1645
+ ] }, group.label)),
1646
+ !loaded && /* @__PURE__ */ jsx("div", { className: "flex items-center justify-center py-3", children: /* @__PURE__ */ jsx("div", { className: "h-4 w-4 border-2 border-primary border-t-transparent animate-spin rounded-full" }) }),
1647
+ loaded && options.length === 0 && /* @__PURE__ */ jsx("div", { className: "px-2 py-3 text-center text-xs text-muted-foreground", children: t("customForms.noTemplates") }),
1648
+ loaded && options.length > 0 && filtered.length === 0 && /* @__PURE__ */ jsx("div", { className: "px-2 py-3 text-center text-xs text-muted-foreground", children: t("customForms.noResults") })
1649
+ ] })
1480
1650
  ] })
1481
1651
  ] });
1482
1652
  }
@@ -1747,7 +1917,6 @@ function createCustomFormsPlugin(options) {
1747
1917
  const registries = [...formRegistries, ...options?.settingsRegistries ?? []];
1748
1918
  const SettingsComponent = () => React2.createElement(PluginSettingsPanel, {
1749
1919
  title: config.labels.settingsLabel,
1750
- subtitle: config.labels.settingsSubtitle,
1751
1920
  customTabs: [
1752
1921
  {
1753
1922
  id: "forms",
@@ -1765,6 +1934,7 @@ function createCustomFormsPlugin(options) {
1765
1934
  PersonDocumentsWidget2.displayName = "PersonDocumentsWidget";
1766
1935
  return {
1767
1936
  id: "custom_forms",
1937
+ defaultAgentRole: "setup",
1768
1938
  name: config.labels.pageTitle,
1769
1939
  icon: "FileText",
1770
1940
  version: "1.0.0",