@fayz-ai/plugin-forms 0.9.0 → 0.9.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 (59) hide show
  1. package/README.md +3 -3
  2. package/dist/{FormBuilder-7SWZQKFN.js → FormBuilder-JCNHXQXT.js} +4 -4
  3. package/dist/{FormBuilder-7SWZQKFN.js.map → FormBuilder-JCNHXQXT.js.map} +1 -1
  4. package/dist/{chunk-W2ZNJGQC.js → chunk-JCAWSDX6.js} +3 -3
  5. package/dist/chunk-JCAWSDX6.js.map +1 -0
  6. package/dist/components/AddDocumentDropdown.d.ts +1 -1
  7. package/dist/components/AddDocumentDropdown.d.ts.map +1 -1
  8. package/dist/{CustomFormsContext.d.ts → context.d.ts} +1 -1
  9. package/dist/context.d.ts.map +1 -0
  10. package/dist/index.d.ts +1 -1
  11. package/dist/index.d.ts.map +1 -1
  12. package/dist/index.js +4 -4
  13. package/dist/index.js.map +1 -1
  14. package/dist/lib/agent-tools.d.ts.map +1 -0
  15. package/dist/lib/document-types.d.ts.map +1 -0
  16. package/package.json +5 -7
  17. package/dist/CustomFormsContext.d.ts.map +0 -1
  18. package/dist/agent-tools.d.ts.map +0 -1
  19. package/dist/chunk-W2ZNJGQC.js.map +0 -1
  20. package/dist/document-types.d.ts.map +0 -1
  21. package/src/CustomFormsContext.tsx +0 -28
  22. package/src/agent-tools.ts +0 -73
  23. package/src/components/AddDocumentDropdown.tsx +0 -92
  24. package/src/components/BuilderCanvas.tsx +0 -87
  25. package/src/components/BuilderField.tsx +0 -81
  26. package/src/components/DocumentFormDialog.tsx +0 -155
  27. package/src/components/DocumentList.tsx +0 -254
  28. package/src/components/FieldConfigDialog.tsx +0 -243
  29. package/src/components/FieldPalette.tsx +0 -81
  30. package/src/components/FormBuilder.tsx +0 -427
  31. package/src/components/FormRenderer.tsx +0 -256
  32. package/src/components/FormViewer.tsx +0 -94
  33. package/src/components/PersonDocumentsWidget.tsx +0 -229
  34. package/src/components/TemplateSelector.tsx +0 -91
  35. package/src/config.ts +0 -43
  36. package/src/data/index.ts +0 -3
  37. package/src/data/mock.ts +0 -193
  38. package/src/data/supabase.ts +0 -408
  39. package/src/data/tables.ts +0 -7
  40. package/src/data/types.ts +0 -33
  41. package/src/document-types.ts +0 -51
  42. package/src/index.ts +0 -234
  43. package/src/lib/person-fields.ts +0 -67
  44. package/src/lib/print.ts +0 -207
  45. package/src/lib/tenant.ts +0 -9
  46. package/src/locales/en.ts +0 -95
  47. package/src/locales/index.ts +0 -7
  48. package/src/locales/pt-BR.ts +0 -95
  49. package/src/migrations/000_plg_rename.sql +0 -21
  50. package/src/migrations/001_frm_base.sql +0 -174
  51. package/src/migrations/002_document_archetype.sql +0 -223
  52. package/src/migrations/003_agent_rpcs.sql +0 -174
  53. package/src/migrations/index.ts +0 -610
  54. package/src/store.ts +0 -167
  55. package/src/types.ts +0 -217
  56. package/src/views/CustomFormsSettingsTab.tsx +0 -105
  57. package/src/views/TemplateListView.tsx +0 -174
  58. /package/dist/{agent-tools.d.ts → lib/agent-tools.d.ts} +0 -0
  59. /package/dist/{document-types.d.ts → lib/document-types.d.ts} +0 -0
@@ -1,256 +0,0 @@
1
- import React, { useCallback } from 'react'
2
- import { Input } from '@fayz-ai/ui'
3
- import {
4
- Select,
5
- SelectContent,
6
- SelectItem,
7
- SelectTrigger,
8
- SelectValue,
9
- } from '@fayz-ai/ui'
10
- import { Badge } from '@fayz-ai/ui'
11
- import { useTranslation } from '@fayz-ai/core'
12
- import type { FormFieldDef, FormSchema } from '../types'
13
-
14
- interface FormRendererProps {
15
- schema: FormSchema
16
- data: Record<string, unknown>
17
- onChange: (data: Record<string, unknown>) => void
18
- readOnly?: boolean
19
- }
20
-
21
- function FieldRenderer({
22
- field,
23
- value,
24
- onChange,
25
- readOnly,
26
- }: {
27
- field: FormFieldDef
28
- value: unknown
29
- onChange: (value: unknown) => void
30
- readOnly?: boolean
31
- }) {
32
- const t = useTranslation()
33
-
34
- switch (field.type) {
35
- case 'title':
36
- return (
37
- <h3 className="text-base font-semibold pt-2 pb-1 border-b">
38
- {field.label}
39
- </h3>
40
- )
41
-
42
- case 'text':
43
- return (
44
- <div className="space-y-1.5">
45
- <label className="text-xs font-medium block">
46
- {field.label}
47
- {field.required && <span className="text-destructive ml-0.5">*</span>}
48
- </label>
49
- <Input
50
- value={(value as string) ?? ''}
51
- onChange={(e) => onChange(e.target.value)}
52
- placeholder={field.placeholder}
53
- className="h-9 text-sm"
54
- readOnly={readOnly}
55
- />
56
- </div>
57
- )
58
-
59
- case 'memo':
60
- case 'richtext':
61
- return (
62
- <div className="space-y-1.5">
63
- <label className="text-xs font-medium block">
64
- {field.label}
65
- {field.required && <span className="text-destructive ml-0.5">*</span>}
66
- </label>
67
- <textarea
68
- value={(value as string) ?? ''}
69
- onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) => onChange(e.target.value)}
70
- placeholder={field.placeholder}
71
- className="flex w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 min-h-[100px]"
72
- readOnly={readOnly}
73
- />
74
- </div>
75
- )
76
-
77
- case 'date':
78
- return (
79
- <div className="space-y-1.5">
80
- <label className="text-xs font-medium block">
81
- {field.label}
82
- {field.required && <span className="text-destructive ml-0.5">*</span>}
83
- </label>
84
- <Input
85
- type="date"
86
- value={(value as string) ?? ''}
87
- onChange={(e) => onChange(e.target.value)}
88
- className="h-9 text-sm"
89
- readOnly={readOnly}
90
- />
91
- </div>
92
- )
93
-
94
- case 'select':
95
- return (
96
- <div className="space-y-1.5">
97
- <label className="text-xs font-medium block">
98
- {field.label}
99
- {field.required && <span className="text-destructive ml-0.5">*</span>}
100
- </label>
101
- {readOnly ? (
102
- <p className="text-sm py-1">{(value as string) ?? '-'}</p>
103
- ) : (
104
- <Select value={(value as string) ?? ''} onValueChange={onChange}>
105
- <SelectTrigger className="h-9 text-sm">
106
- <SelectValue placeholder={field.placeholder ?? '...'} />
107
- </SelectTrigger>
108
- <SelectContent>
109
- {(field.options ?? []).map((opt) => (
110
- <SelectItem key={opt.value} value={opt.value}>
111
- {opt.label}
112
- </SelectItem>
113
- ))}
114
- </SelectContent>
115
- </Select>
116
- )}
117
- </div>
118
- )
119
-
120
- case 'radio':
121
- return (
122
- <div className="space-y-1.5">
123
- <label className="text-xs font-medium block">
124
- {field.label}
125
- {field.required && <span className="text-destructive ml-0.5">*</span>}
126
- </label>
127
- <div className="space-y-1">
128
- {(field.options ?? []).map((opt) => (
129
- <label key={opt.value} className="flex items-center gap-2 cursor-pointer">
130
- <input
131
- type="radio"
132
- name={field.id}
133
- value={opt.value}
134
- checked={value === opt.value}
135
- onChange={() => onChange(opt.value)}
136
- disabled={readOnly}
137
- className="text-primary"
138
- />
139
- <span className="text-sm">{opt.label}</span>
140
- </label>
141
- ))}
142
- </div>
143
- </div>
144
- )
145
-
146
- case 'tags': {
147
- const selectedTags = Array.isArray(value) ? (value as string[]) : []
148
- return (
149
- <div className="space-y-1.5">
150
- <label className="text-xs font-medium block">
151
- {field.label}
152
- {field.required && <span className="text-destructive ml-0.5">*</span>}
153
- </label>
154
- <div className="flex flex-wrap gap-1.5">
155
- {(field.options ?? []).map((opt) => {
156
- const active = selectedTags.includes(opt.value)
157
- return (
158
- <Badge
159
- key={opt.value}
160
- variant={active ? 'default' : 'outline'}
161
- className={`cursor-pointer text-xs ${readOnly ? 'pointer-events-none' : ''}`}
162
- onClick={() => {
163
- if (readOnly) return
164
- onChange(
165
- active
166
- ? selectedTags.filter((t) => t !== opt.value)
167
- : [...selectedTags, opt.value],
168
- )
169
- }}
170
- >
171
- {opt.label}
172
- </Badge>
173
- )
174
- })}
175
- </div>
176
- </div>
177
- )
178
- }
179
-
180
- case 'checkbox':
181
- return (
182
- <label className="flex items-center gap-2 cursor-pointer py-1">
183
- <input
184
- type="checkbox"
185
- checked={!!value}
186
- onChange={(e) => onChange(e.target.checked)}
187
- disabled={readOnly}
188
- className="rounded border-input"
189
- />
190
- <span className="text-sm font-medium">
191
- {field.label}
192
- {field.required && <span className="text-destructive ml-0.5">*</span>}
193
- </span>
194
- </label>
195
- )
196
-
197
- case 'image':
198
- case 'gallery':
199
- return (
200
- <div className="space-y-1.5">
201
- <label className="text-xs font-medium block">
202
- {field.label}
203
- {field.required && <span className="text-destructive ml-0.5">*</span>}
204
- </label>
205
- <div className="flex items-center justify-center rounded-lg border-2 border-dashed py-8 text-sm text-muted-foreground">
206
- {field.type === 'gallery' ? 'Gallery upload' : 'Image upload'} — v2
207
- </div>
208
- </div>
209
- )
210
-
211
- case 'budget':
212
- return (
213
- <div className="space-y-1.5">
214
- <label className="text-xs font-medium block">
215
- {field.label}
216
- {field.required && <span className="text-destructive ml-0.5">*</span>}
217
- </label>
218
- <div className="flex items-center justify-center rounded-lg border-2 border-dashed py-8 text-sm text-muted-foreground">
219
- Budget / line items — v2
220
- </div>
221
- </div>
222
- )
223
-
224
- default:
225
- return null
226
- }
227
- }
228
-
229
- export function FormRenderer({ schema, data, onChange, readOnly }: FormRendererProps) {
230
- const sortedFields = [...schema.fields].sort((a, b) => a.row - b.row || a.col - b.col)
231
-
232
- const handleFieldChange = useCallback(
233
- (fieldId: string, value: unknown) => {
234
- onChange({ ...data, [fieldId]: value })
235
- },
236
- [data, onChange],
237
- )
238
-
239
- return (
240
- <div
241
- className="grid gap-x-4 gap-y-3"
242
- style={{ gridTemplateColumns: 'repeat(12, 1fr)' }}
243
- >
244
- {sortedFields.map((field) => (
245
- <div key={field.id} style={{ gridColumn: `span ${field.colSpan}` }}>
246
- <FieldRenderer
247
- field={field}
248
- value={data[field.id]}
249
- onChange={(v) => handleFieldChange(field.id, v)}
250
- readOnly={readOnly}
251
- />
252
- </div>
253
- ))}
254
- </div>
255
- )
256
- }
@@ -1,94 +0,0 @@
1
- import React, { useEffect, useState } from 'react'
2
- import { ArrowLeft, Pencil, Printer } from 'lucide-react'
3
- import { Button } from '@fayz-ai/ui'
4
- import { Badge } from '@fayz-ai/ui'
5
- import { useTranslation } from '@fayz-ai/core'
6
- import type { CustomFormsDataProvider } from '../data/types'
7
- import type { FormDocument, FormTemplate } from '../types'
8
- import { FormRenderer } from './FormRenderer'
9
- import { printDocument } from '../lib/print'
10
-
11
- interface FormViewerProps {
12
- document: FormDocument
13
- provider: CustomFormsDataProvider
14
- onBack: () => void
15
- onEdit?: (doc: FormDocument) => void
16
- }
17
-
18
- const STATUS_COLORS: Record<string, string> = {
19
- draft: 'bg-warning-soft text-warning-soft-foreground',
20
- completed: 'bg-success-soft text-success-soft-foreground',
21
- signed: 'bg-info-soft text-info-soft-foreground',
22
- archived: 'bg-muted text-muted-foreground',
23
- }
24
-
25
- export function FormViewer({ document: doc, provider, onBack, onEdit }: FormViewerProps) {
26
- const t = useTranslation()
27
- const [template, setTemplate] = useState<FormTemplate | null>(null)
28
-
29
- useEffect(() => {
30
- if (!doc.templateId) return
31
- const timer = setTimeout(() => {
32
- provider.getTemplateById(doc.templateId).then(setTemplate)
33
- }, 50)
34
- return () => clearTimeout(timer)
35
- }, [doc.templateId, provider])
36
-
37
- if (!template) {
38
- return (
39
- <div className="flex items-center justify-center py-12">
40
- <div className="h-5 w-5 border-2 border-primary border-t-transparent animate-spin rounded-full" />
41
- </div>
42
- )
43
- }
44
-
45
- return (
46
- <div className="space-y-4">
47
- {/* Header */}
48
- <div className="flex items-center gap-3">
49
- <button
50
- onClick={onBack}
51
- className="flex items-center gap-1 text-sm text-muted-foreground hover:text-foreground transition-colors"
52
- >
53
- <ArrowLeft className="h-3.5 w-3.5" />
54
- </button>
55
- <div className="flex-1 min-w-0">
56
- <h3 className="font-semibold text-sm truncate">
57
- {doc.title ?? template.name}
58
- </h3>
59
- <p className="text-xs text-muted-foreground">
60
- {new Date(doc.createdAt).toLocaleDateString()}
61
- </p>
62
- </div>
63
- <Badge variant="secondary" className={`text-[10px] ${STATUS_COLORS[doc.status] ?? ''}`}>
64
- {t(`customForms.status.${doc.status}`)}
65
- </Badge>
66
- <Button
67
- variant="outline"
68
- size="sm"
69
- className="h-8"
70
- onClick={() => printDocument(template, doc)}
71
- >
72
- <Printer className="h-3 w-3 mr-1" />
73
- {t('customForms.printPdf')}
74
- </Button>
75
- {onEdit && doc.status === 'draft' && (
76
- <Button variant="outline" size="sm" className="h-8" onClick={() => onEdit(doc)}>
77
- <Pencil className="h-3 w-3 mr-1" />
78
- {t('common.edit')}
79
- </Button>
80
- )}
81
- </div>
82
-
83
- {/* Form content (read-only) */}
84
- <div className="rounded-xl border p-4">
85
- <FormRenderer
86
- schema={template.schema}
87
- data={doc.data}
88
- onChange={() => {}}
89
- readOnly
90
- />
91
- </div>
92
- </div>
93
- )
94
- }
@@ -1,229 +0,0 @@
1
- import React, { useState, useCallback, useRef } from 'react'
2
- import { ArrowLeft, Image as ImageIcon, Paperclip } from 'lucide-react'
3
- import { Badge } from '@fayz-ai/ui'
4
- import { useTranslation } from '@fayz-ai/core'
5
- import { useLimitGuard, invalidateLimit } from '@fayz-ai/saas'
6
- import type { CustomFormsConfig } from '../config'
7
- import type { CustomFormsDataProvider } from '../data/types'
8
- import type { CustomFormsStore } from '../store'
9
- import type { FormDocument, FormTemplate } from '../types'
10
- import type { DocumentTypeOption } from '../document-types'
11
- import { DocumentList } from './DocumentList'
12
- import { DocumentFormDialog } from './DocumentFormDialog'
13
- import { FormViewer } from './FormViewer'
14
- import { AddDocumentDropdown } from './AddDocumentDropdown'
15
-
16
- interface PersonDocumentsWidgetProps {
17
- item: { id: string; name?: string; [key: string]: any }
18
- config: CustomFormsConfig
19
- provider: CustomFormsDataProvider
20
- store: CustomFormsStore
21
- }
22
-
23
- type WidgetView =
24
- | { type: 'list' }
25
- | { type: 'fill'; template: FormTemplate }
26
- | { type: 'view'; document: FormDocument }
27
- | { type: 'edit'; document: FormDocument }
28
-
29
- export function PersonDocumentsWidget({ item, config, provider, store }: PersonDocumentsWidgetProps) {
30
- const t = useTranslation()
31
- const guardDocuments = useLimitGuard('documents_month')
32
- const [view, setView] = useState<WidgetView>({ type: 'list' })
33
- const fileInputRef = useRef<HTMLInputElement>(null)
34
- const [pendingFileAccept, setPendingFileAccept] = useState<string>('*/*')
35
-
36
- const handleDocTypeSelect = useCallback(async (option: DocumentTypeOption) => {
37
- // Form template
38
- if (option.id.startsWith('template:')) {
39
- const templateId = option.id.replace('template:', '')
40
- const template = await provider.getTemplateById(templateId)
41
- if (template) {
42
- setView({ type: 'fill', template })
43
- }
44
- return
45
- }
46
-
47
- // Image — open file picker for images
48
- if (option.id === 'core:image') {
49
- setPendingFileAccept('image/*')
50
- setTimeout(() => fileInputRef.current?.click(), 0)
51
- return
52
- }
53
-
54
- // Attachment — open file picker for any file
55
- if (option.id === 'core:attachment') {
56
- setPendingFileAccept('*/*')
57
- setTimeout(() => fileInputRef.current?.click(), 0)
58
- return
59
- }
60
- }, [provider])
61
-
62
- const handleFiles = useCallback(async (files: File[]) => {
63
- // Plan monthly-quota guard (client-side, before the provider call) — count
64
- // the whole batch. Opens the global UpgradeModal and aborts when the cap
65
- // would be exceeded.
66
- if ((await guardDocuments(files.length)) === 'blocked') return
67
- for (const file of files) {
68
- const isImage = file.type.startsWith('image/')
69
- await store.getState().createDocument({
70
- kind: isImage ? 'image' : 'attachment',
71
- personId: item.id,
72
- title: file.name,
73
- fileName: file.name,
74
- fileSize: file.size,
75
- mimeType: file.type,
76
- status: 'completed',
77
- })
78
- }
79
- invalidateLimit('documents_month')
80
- }, [store, item.id, guardDocuments])
81
-
82
- const handleFileInputChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
83
- const files = Array.from(e.target.files ?? [])
84
- if (files.length > 0) handleFiles(files)
85
- e.target.value = ''
86
- }, [handleFiles])
87
-
88
- const handleViewDoc = useCallback((doc: FormDocument) => {
89
- setView({ type: 'view', document: doc })
90
- }, [])
91
-
92
- const handleEditDoc = useCallback((doc: FormDocument) => {
93
- setView({ type: 'edit', document: doc })
94
- }, [])
95
-
96
- const handleBack = useCallback(() => {
97
- setView({ type: 'list' })
98
- }, [])
99
-
100
- const handleSaved = useCallback(() => {
101
- setView({ type: 'list' })
102
- }, [])
103
-
104
- // Fill form
105
- if (view.type === 'fill') {
106
- return (
107
- <DocumentFormDialog
108
- template={view.template}
109
- personId={item.id}
110
- personName={item.name}
111
- person={item}
112
- provider={provider}
113
- store={store}
114
- onSaved={handleSaved}
115
- onBack={handleBack}
116
- />
117
- )
118
- }
119
-
120
- // View document — route by kind
121
- if (view.type === 'view') {
122
- const doc = view.document
123
- const isForm = doc.kind === 'form' || (!doc.kind && doc.templateId)
124
-
125
- if (!isForm) {
126
- // File document (image/attachment) — simple preview
127
- return (
128
- <FileDocumentViewer document={doc} onBack={handleBack} />
129
- )
130
- }
131
-
132
- return (
133
- <FormViewer
134
- document={doc}
135
- provider={provider}
136
- onBack={handleBack}
137
- onEdit={handleEditDoc}
138
- />
139
- )
140
- }
141
-
142
- // Edit document — only for form docs
143
- if (view.type === 'edit') {
144
- return (
145
- <DocumentFormDialog
146
- template={null}
147
- existingDocument={view.document}
148
- personId={item.id}
149
- personName={item.name}
150
- person={item}
151
- provider={provider}
152
- store={store}
153
- onSaved={handleSaved}
154
- onBack={handleBack}
155
- />
156
- )
157
- }
158
-
159
- // Default: list with dropdown + drag zone
160
- return (
161
- <div className="space-y-3">
162
- <div className="flex items-center justify-between">
163
- <h3 className="text-sm font-semibold">{t('customForms.documents')}</h3>
164
- <AddDocumentDropdown personId={item.id} onSelect={handleDocTypeSelect} />
165
- </div>
166
- <DocumentList
167
- personId={item.id}
168
- provider={provider}
169
- store={store}
170
- onView={handleViewDoc}
171
- onFileDrop={handleFiles}
172
- />
173
- {/* Hidden file input for image/attachment upload */}
174
- <input
175
- ref={fileInputRef}
176
- type="file"
177
- className="hidden"
178
- accept={pendingFileAccept}
179
- multiple
180
- onChange={handleFileInputChange}
181
- />
182
- </div>
183
- )
184
- }
185
-
186
- function FileDocumentViewer({ document: doc, onBack }: { document: FormDocument; onBack: () => void }) {
187
- const isImage = doc.kind === 'image' || doc.mimeType?.startsWith('image/')
188
- const Icon = isImage ? ImageIcon : Paperclip
189
-
190
- return (
191
- <div className="space-y-4">
192
- <div className="flex items-center gap-3">
193
- <button
194
- onClick={onBack}
195
- className="flex items-center gap-1 text-sm text-muted-foreground hover:text-foreground transition-colors"
196
- >
197
- <ArrowLeft className="h-3.5 w-3.5" />
198
- </button>
199
- <div className="flex-1 min-w-0">
200
- <h3 className="font-semibold text-sm truncate">{doc.title ?? doc.fileName ?? 'Document'}</h3>
201
- <p className="text-xs text-muted-foreground">
202
- {new Date(doc.createdAt).toLocaleDateString()}
203
- </p>
204
- </div>
205
- <Badge variant="secondary" className="text-[10px] bg-success/15 text-success">
206
- {doc.status === 'completed' ? 'Finalizado' : doc.status}
207
- </Badge>
208
- </div>
209
-
210
- <div className="rounded-xl border p-6 flex flex-col items-center gap-4">
211
- {isImage && doc.fileUrl ? (
212
- <img src={doc.fileUrl} alt={doc.fileName ?? ''} className="max-w-full max-h-[500px] rounded-lg object-contain" />
213
- ) : (
214
- <div className="flex flex-col items-center gap-3 py-8">
215
- <div className="flex h-16 w-16 items-center justify-center rounded-full bg-muted">
216
- <Icon className="h-7 w-7 text-muted-foreground" />
217
- </div>
218
- <p className="text-sm font-medium">{doc.fileName ?? doc.title ?? 'Arquivo'}</p>
219
- {doc.fileSize && (
220
- <p className="text-xs text-muted-foreground">
221
- {(doc.fileSize / 1024).toFixed(1)} KB
222
- </p>
223
- )}
224
- </div>
225
- )}
226
- </div>
227
- </div>
228
- )
229
- }
@@ -1,91 +0,0 @@
1
- import React, { useEffect, useState } from 'react'
2
- import { FileText, Search } from 'lucide-react'
3
- import { Input } from '@fayz-ai/ui'
4
- import { Badge } from '@fayz-ai/ui'
5
- import { useTranslation } from '@fayz-ai/core'
6
- import type { CustomFormsDataProvider } from '../data/types'
7
- import type { FormTemplate } from '../types'
8
-
9
- interface TemplateSelectorProps {
10
- provider: CustomFormsDataProvider
11
- onSelect: (template: FormTemplate) => void
12
- onClose: () => void
13
- }
14
-
15
- export function TemplateSelector({ provider, onSelect, onClose }: TemplateSelectorProps) {
16
- const t = useTranslation()
17
- const [templates, setTemplates] = useState<FormTemplate[]>([])
18
- const [search, setSearch] = useState('')
19
- const [loading, setLoading] = useState(true)
20
-
21
- useEffect(() => {
22
- setLoading(true)
23
- provider
24
- .getTemplates({ search: search || undefined })
25
- .then((result) => setTemplates(result.data))
26
- .finally(() => setLoading(false))
27
- }, [provider, search])
28
-
29
- return (
30
- <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50" onClick={onClose}>
31
- <div
32
- className="bg-card rounded-xl border shadow-2xl w-full max-w-md max-h-[70vh] flex flex-col"
33
- onClick={(e) => e.stopPropagation()}
34
- >
35
- {/* Header */}
36
- <div className="p-4 border-b">
37
- <h3 className="font-semibold text-sm mb-3">{t('customForms.selectTemplate')}</h3>
38
- <div className="relative">
39
- <Search className="absolute left-2.5 top-1/2 -translate-y-1/2 h-3.5 w-3.5 text-muted-foreground" />
40
- <Input
41
- value={search}
42
- onChange={(e) => setSearch(e.target.value)}
43
- placeholder={t('common.search')}
44
- className="h-9 pl-8 text-sm"
45
- autoFocus
46
- />
47
- </div>
48
- </div>
49
-
50
- {/* List */}
51
- <div className="flex-1 overflow-y-auto p-2">
52
- {loading && (
53
- <div className="flex items-center justify-center py-8">
54
- <div className="h-5 w-5 border-2 border-primary border-t-transparent animate-spin rounded-full" />
55
- </div>
56
- )}
57
-
58
- {!loading && templates.length === 0 && (
59
- <div className="flex flex-col items-center justify-center py-8 text-center">
60
- <FileText className="h-8 w-8 text-muted-foreground/50 mb-2" />
61
- <p className="text-sm text-muted-foreground">{t('customForms.noTemplates')}</p>
62
- </div>
63
- )}
64
-
65
- {templates.map((tpl) => (
66
- <button
67
- key={tpl.id}
68
- onClick={() => onSelect(tpl)}
69
- className="flex items-center gap-3 w-full px-3 py-2.5 rounded-lg hover:bg-muted transition-colors text-left"
70
- >
71
- <div className="flex h-8 w-8 items-center justify-center rounded-lg bg-primary/10 shrink-0">
72
- <FileText className="h-4 w-4 text-primary" />
73
- </div>
74
- <div className="flex-1 min-w-0">
75
- <p className="text-sm font-medium truncate">{tpl.name}</p>
76
- <div className="flex items-center gap-2 mt-0.5">
77
- <Badge variant="secondary" className="text-[10px]">
78
- {t(`customForms.category.${tpl.category}`)}
79
- </Badge>
80
- <span className="text-[10px] text-muted-foreground">
81
- {tpl.schema.fields.length} {t('customForms.builder.fields').toLowerCase()}
82
- </span>
83
- </div>
84
- </div>
85
- </button>
86
- ))}
87
- </div>
88
- </div>
89
- </div>
90
- )
91
- }