@giouwur/biometric-sdk 1.0.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/README.md +215 -0
- package/bin/init.js +50 -0
- package/dist/index.d.mts +569 -0
- package/dist/index.d.ts +569 -0
- package/dist/index.js +4725 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +4667 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +45 -0
- package/templates/test-page.tsx +1219 -0
|
@@ -0,0 +1,1219 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState, useCallback, useEffect } from "react";
|
|
4
|
+
import {
|
|
5
|
+
// Provider & Config
|
|
6
|
+
useBiometricConfig,
|
|
7
|
+
|
|
8
|
+
// Pre-built Modules
|
|
9
|
+
FingerEnrollModule,
|
|
10
|
+
FaceEnrollModule,
|
|
11
|
+
PalmEnrollModule,
|
|
12
|
+
IrisEnrollModule,
|
|
13
|
+
FingerRollEnrollModule,
|
|
14
|
+
|
|
15
|
+
// Stores
|
|
16
|
+
useEnrollmentStore,
|
|
17
|
+
useBiometricStore,
|
|
18
|
+
useUiStore,
|
|
19
|
+
|
|
20
|
+
// UI Components
|
|
21
|
+
Button,
|
|
22
|
+
Card,
|
|
23
|
+
Input,
|
|
24
|
+
Select,
|
|
25
|
+
QualityBadge,
|
|
26
|
+
StatusChip,
|
|
27
|
+
ProgressBar,
|
|
28
|
+
Modal,
|
|
29
|
+
Loader,
|
|
30
|
+
BiometricSlot,
|
|
31
|
+
|
|
32
|
+
// Utilities
|
|
33
|
+
getImageSrcForDisplay,
|
|
34
|
+
normalizeImageForStorage,
|
|
35
|
+
|
|
36
|
+
// Types
|
|
37
|
+
type BiometricConfig,
|
|
38
|
+
type BiometricItem,
|
|
39
|
+
type EnrollmentState,
|
|
40
|
+
type BiometricState,
|
|
41
|
+
} from "@giouwur/biometric-sdk";
|
|
42
|
+
|
|
43
|
+
import {
|
|
44
|
+
TbFingerprint,
|
|
45
|
+
TbFaceId,
|
|
46
|
+
TbHandStop,
|
|
47
|
+
TbEye,
|
|
48
|
+
TbScan,
|
|
49
|
+
TbCode,
|
|
50
|
+
TbApi,
|
|
51
|
+
TbPackage,
|
|
52
|
+
TbChevronDown,
|
|
53
|
+
TbChevronRight,
|
|
54
|
+
TbComponents,
|
|
55
|
+
TbPuzzle,
|
|
56
|
+
TbPalette,
|
|
57
|
+
TbDatabase,
|
|
58
|
+
TbBraces,
|
|
59
|
+
TbPhoto,
|
|
60
|
+
TbSettings,
|
|
61
|
+
TbSearch,
|
|
62
|
+
TbX,
|
|
63
|
+
TbTerminal2,
|
|
64
|
+
TbColorSwatch,
|
|
65
|
+
} from "react-icons/tb";
|
|
66
|
+
|
|
67
|
+
// ─── Constants & Types ───────────────────────────────────
|
|
68
|
+
type SectionCategory = "basics" | "data" | "advanced" | "modules";
|
|
69
|
+
|
|
70
|
+
interface SectionInfo {
|
|
71
|
+
id: string;
|
|
72
|
+
title: string;
|
|
73
|
+
category: SectionCategory;
|
|
74
|
+
icon: React.ElementType;
|
|
75
|
+
component: React.ReactNode;
|
|
76
|
+
keywords: string[];
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// ─── Collapsible Section ─────────────────────────────────
|
|
80
|
+
function Section({
|
|
81
|
+
title,
|
|
82
|
+
icon: Icon,
|
|
83
|
+
children,
|
|
84
|
+
isOpen,
|
|
85
|
+
onToggle,
|
|
86
|
+
}: {
|
|
87
|
+
title: string;
|
|
88
|
+
icon: React.ElementType;
|
|
89
|
+
children: React.ReactNode;
|
|
90
|
+
isOpen: boolean;
|
|
91
|
+
onToggle: () => void;
|
|
92
|
+
}) {
|
|
93
|
+
return (
|
|
94
|
+
<section className="bg-card border border-border/50 rounded-xl shadow-sm overflow-hidden animate-in fade-in slide-in-from-bottom-2 duration-300">
|
|
95
|
+
<button
|
|
96
|
+
onClick={onToggle}
|
|
97
|
+
className="w-full flex items-center justify-between px-6 py-4 hover:bg-secondary/30 transition-colors"
|
|
98
|
+
>
|
|
99
|
+
<div className="flex items-center gap-3">
|
|
100
|
+
<div className="p-2 rounded-lg bg-primary/10 text-primary">
|
|
101
|
+
<Icon size={20} />
|
|
102
|
+
</div>
|
|
103
|
+
<h2 className="text-lg font-semibold text-foreground">{title}</h2>
|
|
104
|
+
</div>
|
|
105
|
+
{isOpen ? <TbChevronDown size={20} /> : <TbChevronRight size={20} />}
|
|
106
|
+
</button>
|
|
107
|
+
{isOpen && (
|
|
108
|
+
<div className="px-6 pb-6 border-t border-border/30 pt-4">
|
|
109
|
+
{children}
|
|
110
|
+
</div>
|
|
111
|
+
)}
|
|
112
|
+
</section>
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// ─── Code Block ──────────────────────────────────────────
|
|
117
|
+
function CodeBlock({ code, title }: { code: string; title?: string }) {
|
|
118
|
+
return (
|
|
119
|
+
<div className="rounded-lg bg-[#0d1117] border border-white/10 overflow-hidden">
|
|
120
|
+
{title && (
|
|
121
|
+
<div className="px-4 py-2 bg-white/5 border-b border-white/10 text-xs font-mono text-white/50">
|
|
122
|
+
{title}
|
|
123
|
+
</div>
|
|
124
|
+
)}
|
|
125
|
+
<pre className="p-4 text-sm font-mono text-green-400 overflow-x-auto whitespace-pre-wrap">
|
|
126
|
+
{code}
|
|
127
|
+
</pre>
|
|
128
|
+
</div>
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// ─── 1. Provider Example ──────────────────────────────────
|
|
133
|
+
function ProviderExample() {
|
|
134
|
+
const config = useBiometricConfig();
|
|
135
|
+
return (
|
|
136
|
+
<div className="space-y-4">
|
|
137
|
+
<p className="text-sm text-foreground/60">
|
|
138
|
+
Tu app debe estar envuelta con{" "}
|
|
139
|
+
<code className="px-1.5 py-0.5 bg-secondary rounded text-xs font-mono">
|
|
140
|
+
{"<BiometricProvider>"}
|
|
141
|
+
</code>.
|
|
142
|
+
Aquí está la configuración activa:
|
|
143
|
+
</p>
|
|
144
|
+
<div className="grid grid-cols-3 gap-3">
|
|
145
|
+
{[
|
|
146
|
+
{ label: "WebSocket", value: config.wsUrl },
|
|
147
|
+
{ label: "API Base", value: config.apiBaseUrl || "—" },
|
|
148
|
+
{ label: "Device ID", value: config.deviceId || "—" },
|
|
149
|
+
].map(({ label, value }) => (
|
|
150
|
+
<div key={label} className="p-3 bg-secondary/30 rounded-lg border border-border/30">
|
|
151
|
+
<p className="text-[10px] font-bold text-foreground/40 uppercase tracking-wider">{label}</p>
|
|
152
|
+
<p className="text-sm font-mono text-primary truncate">{value}</p>
|
|
153
|
+
</div>
|
|
154
|
+
))}
|
|
155
|
+
</div>
|
|
156
|
+
<CodeBlock
|
|
157
|
+
title="layout.tsx — Ejemplo de configuración"
|
|
158
|
+
code={`import { BiometricProvider } from '@intell/biometric-sdk';
|
|
159
|
+
|
|
160
|
+
export default function Layout({ children }) {
|
|
161
|
+
return (
|
|
162
|
+
<BiometricProvider config={{
|
|
163
|
+
wsUrl: "ws://127.0.0.1:5000/biometric",
|
|
164
|
+
apiBaseUrl: "http://localhost:8080/api/v1",
|
|
165
|
+
deviceId: "scanner_01"
|
|
166
|
+
}}>
|
|
167
|
+
{children}
|
|
168
|
+
</BiometricProvider>
|
|
169
|
+
);
|
|
170
|
+
}`}
|
|
171
|
+
/>
|
|
172
|
+
</div>
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// ─── 2. UI Components Showcase ───────────────────────────
|
|
177
|
+
function UIComponentsShowcase() {
|
|
178
|
+
const [modalOpen, setModalOpen] = useState(false);
|
|
179
|
+
const [selectValue, setSelectValue] = useState("opt1");
|
|
180
|
+
const [inputValue, setInputValue] = useState("");
|
|
181
|
+
|
|
182
|
+
return (
|
|
183
|
+
<div className="space-y-8">
|
|
184
|
+
{/* Buttons */}
|
|
185
|
+
<div>
|
|
186
|
+
<h3 className="text-sm font-bold text-foreground/70 mb-3 uppercase tracking-wider">
|
|
187
|
+
Button — 6 variantes × 3 tamaños
|
|
188
|
+
</h3>
|
|
189
|
+
<div className="flex flex-wrap gap-3 mb-3">
|
|
190
|
+
<Button variant="primary">Primary</Button>
|
|
191
|
+
<Button variant="secondary">Secondary</Button>
|
|
192
|
+
<Button variant="success">Success</Button>
|
|
193
|
+
<Button variant="danger">Danger</Button>
|
|
194
|
+
<Button variant="warning">Warning</Button>
|
|
195
|
+
<Button variant="ghost">Ghost</Button>
|
|
196
|
+
</div>
|
|
197
|
+
<div className="flex flex-wrap items-center gap-3">
|
|
198
|
+
<Button size="sm">Small</Button>
|
|
199
|
+
<Button size="md">Medium</Button>
|
|
200
|
+
<Button size="lg">Large</Button>
|
|
201
|
+
<Button isLoading>Loading...</Button>
|
|
202
|
+
<Button disabled>Disabled</Button>
|
|
203
|
+
<Button icon={<TbFingerprint />}>Con Icono</Button>
|
|
204
|
+
</div>
|
|
205
|
+
</div>
|
|
206
|
+
|
|
207
|
+
{/* StatusChip */}
|
|
208
|
+
<div>
|
|
209
|
+
<h3 className="text-sm font-bold text-foreground/70 mb-3 uppercase tracking-wider">
|
|
210
|
+
StatusChip — 5 estados
|
|
211
|
+
</h3>
|
|
212
|
+
<div className="flex flex-wrap gap-3">
|
|
213
|
+
<StatusChip status="success" label="Conectado" />
|
|
214
|
+
<StatusChip status="warning" label="Advertencia" />
|
|
215
|
+
<StatusChip status="danger" label="Error" />
|
|
216
|
+
<StatusChip status="info" label="Información" />
|
|
217
|
+
<StatusChip status="neutral" label="Neutral" />
|
|
218
|
+
<StatusChip status="success" label="Cargando..." isLoading />
|
|
219
|
+
<StatusChip status="info" label="Mini" size="sm" />
|
|
220
|
+
</div>
|
|
221
|
+
</div>
|
|
222
|
+
|
|
223
|
+
{/* QualityBadge */}
|
|
224
|
+
<div>
|
|
225
|
+
<h3 className="text-sm font-bold text-foreground/70 mb-3 uppercase tracking-wider">
|
|
226
|
+
QualityBadge — Calidad dinámica
|
|
227
|
+
</h3>
|
|
228
|
+
<div className="flex gap-4 items-center">
|
|
229
|
+
<QualityBadge quality={95} />
|
|
230
|
+
<QualityBadge quality={72} />
|
|
231
|
+
<QualityBadge quality={35} />
|
|
232
|
+
<span className="text-xs text-foreground/40">≥80 Verde • ≥50 Amarillo • <50 Rojo</span>
|
|
233
|
+
</div>
|
|
234
|
+
</div>
|
|
235
|
+
|
|
236
|
+
{/* ProgressBar */}
|
|
237
|
+
<div>
|
|
238
|
+
<h3 className="text-sm font-bold text-foreground/70 mb-3 uppercase tracking-wider">
|
|
239
|
+
ProgressBar
|
|
240
|
+
</h3>
|
|
241
|
+
<ProgressBar current={7} total={10} label="Biométricos capturados" />
|
|
242
|
+
</div>
|
|
243
|
+
|
|
244
|
+
{/* Input */}
|
|
245
|
+
<div>
|
|
246
|
+
<h3 className="text-sm font-bold text-foreground/70 mb-3 uppercase tracking-wider">
|
|
247
|
+
Input
|
|
248
|
+
</h3>
|
|
249
|
+
<div className="max-w-md">
|
|
250
|
+
<Input
|
|
251
|
+
label="Nombre del solicitante"
|
|
252
|
+
placeholder="Ej. Juan Pérez"
|
|
253
|
+
icon={<TbFingerprint />}
|
|
254
|
+
value={inputValue}
|
|
255
|
+
onChange={(e) => setInputValue(e.target.value)}
|
|
256
|
+
/>
|
|
257
|
+
</div>
|
|
258
|
+
</div>
|
|
259
|
+
|
|
260
|
+
{/* Select */}
|
|
261
|
+
<div>
|
|
262
|
+
<h3 className="text-sm font-bold text-foreground/70 mb-3 uppercase tracking-wider">
|
|
263
|
+
Select
|
|
264
|
+
</h3>
|
|
265
|
+
<div className="max-w-md">
|
|
266
|
+
<Select
|
|
267
|
+
value={selectValue}
|
|
268
|
+
onChange={setSelectValue}
|
|
269
|
+
options={[
|
|
270
|
+
{ value: "opt1", label: "Huella Plana", icon: <TbFingerprint /> },
|
|
271
|
+
{ value: "opt2", label: "Huella Rolada", icon: <TbScan /> },
|
|
272
|
+
{ value: "opt3", label: "Rostro", icon: <TbFaceId /> },
|
|
273
|
+
]}
|
|
274
|
+
/>
|
|
275
|
+
</div>
|
|
276
|
+
</div>
|
|
277
|
+
|
|
278
|
+
{/* Card */}
|
|
279
|
+
<div>
|
|
280
|
+
<h3 className="text-sm font-bold text-foreground/70 mb-3 uppercase tracking-wider">
|
|
281
|
+
Card
|
|
282
|
+
</h3>
|
|
283
|
+
<div className="grid grid-cols-3 gap-3">
|
|
284
|
+
<Card className="p-4">
|
|
285
|
+
<p className="text-sm font-bold">Default Card</p>
|
|
286
|
+
<p className="text-xs text-foreground/50">Contenido</p>
|
|
287
|
+
</Card>
|
|
288
|
+
<Card variant="glass" className="p-4">
|
|
289
|
+
<p className="text-sm font-bold">Glass Card</p>
|
|
290
|
+
<p className="text-xs text-foreground/50">Translúcida</p>
|
|
291
|
+
</Card>
|
|
292
|
+
<Card variant="outlined" className="p-4">
|
|
293
|
+
<p className="text-sm font-bold">Outlined Card</p>
|
|
294
|
+
<p className="text-xs text-foreground/50">Solo borde</p>
|
|
295
|
+
</Card>
|
|
296
|
+
</div>
|
|
297
|
+
</div>
|
|
298
|
+
|
|
299
|
+
{/* Loader */}
|
|
300
|
+
<div>
|
|
301
|
+
<h3 className="text-sm font-bold text-foreground/70 mb-3 uppercase tracking-wider">
|
|
302
|
+
Loader
|
|
303
|
+
</h3>
|
|
304
|
+
<div className="flex gap-6 items-center">
|
|
305
|
+
<Loader size={16} />
|
|
306
|
+
<Loader size={24} text="Cargando..." />
|
|
307
|
+
<Loader size={32} />
|
|
308
|
+
</div>
|
|
309
|
+
</div>
|
|
310
|
+
|
|
311
|
+
{/* Modal */}
|
|
312
|
+
<div>
|
|
313
|
+
<h3 className="text-sm font-bold text-foreground/70 mb-3 uppercase tracking-wider">
|
|
314
|
+
Modal
|
|
315
|
+
</h3>
|
|
316
|
+
<Button onClick={() => setModalOpen(true)}>Abrir Modal</Button>
|
|
317
|
+
<Modal isOpen={modalOpen} onClose={() => setModalOpen(false)} title="Modal de Ejemplo">
|
|
318
|
+
<p className="text-foreground/70 mb-4">Este es un modal reutilizable del SDK.</p>
|
|
319
|
+
<div className="flex gap-3 justify-end">
|
|
320
|
+
<Button variant="ghost" onClick={() => setModalOpen(false)}>Cancelar</Button>
|
|
321
|
+
<Button onClick={() => setModalOpen(false)}>Confirmar</Button>
|
|
322
|
+
</div>
|
|
323
|
+
</Modal>
|
|
324
|
+
</div>
|
|
325
|
+
</div>
|
|
326
|
+
);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// ─── 3. Style Customization ──────────────────────────────
|
|
330
|
+
function StyleCustomizationExample() {
|
|
331
|
+
return (
|
|
332
|
+
<div className="space-y-4">
|
|
333
|
+
<p className="text-sm text-foreground/60">
|
|
334
|
+
Todos los componentes aceptan <code className="px-1.5 py-0.5 bg-secondary rounded text-xs font-mono">className</code>.
|
|
335
|
+
Como usan Tailwind, puedes extender/sobreescribir estilos:
|
|
336
|
+
</p>
|
|
337
|
+
<div className="grid grid-cols-2 gap-4">
|
|
338
|
+
<div>
|
|
339
|
+
<p className="text-xs font-bold text-foreground/40 mb-2">Default</p>
|
|
340
|
+
<Button>Default Button</Button>
|
|
341
|
+
</div>
|
|
342
|
+
<div>
|
|
343
|
+
<p className="text-xs font-bold text-foreground/40 mb-2">Custom className</p>
|
|
344
|
+
<Button className="!bg-gradient-to-r !from-pink-500 !to-violet-500 !rounded-full !px-8">
|
|
345
|
+
Custom Style
|
|
346
|
+
</Button>
|
|
347
|
+
</div>
|
|
348
|
+
</div>
|
|
349
|
+
<div className="grid grid-cols-2 gap-4">
|
|
350
|
+
<div>
|
|
351
|
+
<p className="text-xs font-bold text-foreground/40 mb-2">Default Card</p>
|
|
352
|
+
<Card className="p-4">
|
|
353
|
+
<p className="text-sm">Card normal</p>
|
|
354
|
+
</Card>
|
|
355
|
+
</div>
|
|
356
|
+
<div>
|
|
357
|
+
<p className="text-xs font-bold text-foreground/40 mb-2">Custom Card</p>
|
|
358
|
+
<Card className="p-4 !bg-gradient-to-br !from-emerald-500/20 !to-teal-500/20 !border-emerald-500/30">
|
|
359
|
+
<p className="text-sm text-emerald-400">Card personalizada ✨</p>
|
|
360
|
+
</Card>
|
|
361
|
+
</div>
|
|
362
|
+
</div>
|
|
363
|
+
<CodeBlock
|
|
364
|
+
title="Personalizando estilos"
|
|
365
|
+
code={`// Sobreescribir con className + !important de Tailwind
|
|
366
|
+
<Button className="!bg-gradient-to-r !from-pink-500 !to-violet-500 !rounded-full">
|
|
367
|
+
Custom Button
|
|
368
|
+
</Button>
|
|
369
|
+
|
|
370
|
+
// Card personalizada
|
|
371
|
+
<Card className="!bg-emerald-500/20 !border-emerald-500/30">
|
|
372
|
+
<p className="text-emerald-400">Themed Card</p>
|
|
373
|
+
</Card>
|
|
374
|
+
|
|
375
|
+
// Módulo con className
|
|
376
|
+
<FingerEnrollModule className="max-w-3xl mx-auto" />`}
|
|
377
|
+
/>
|
|
378
|
+
</div>
|
|
379
|
+
);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// ─── 4. Store & Data Access ──────────────────────────────
|
|
383
|
+
function StoreAccessExample() {
|
|
384
|
+
const enrollment = useEnrollmentStore();
|
|
385
|
+
const biometric = useBiometricStore();
|
|
386
|
+
const ui = useUiStore();
|
|
387
|
+
|
|
388
|
+
const fingerCount = Object.keys(enrollment.fingerprints).length;
|
|
389
|
+
const rolledCount = Object.keys(enrollment.rolledFingerprints).length;
|
|
390
|
+
const palmCount = Object.keys(enrollment.palms).length;
|
|
391
|
+
const faceCount = enrollment.faces.length;
|
|
392
|
+
const irisCount = [enrollment.irises.left, enrollment.irises.right].filter(Boolean).length;
|
|
393
|
+
const missingCount = Object.keys(enrollment.missingFingers).length;
|
|
394
|
+
|
|
395
|
+
return (
|
|
396
|
+
<div className="space-y-4">
|
|
397
|
+
<p className="text-sm text-foreground/60">
|
|
398
|
+
Acceso directo a <strong>todo el estado</strong> del SDK via stores de Zustand:
|
|
399
|
+
</p>
|
|
400
|
+
|
|
401
|
+
{/* Live State Dashboard */}
|
|
402
|
+
<div className="grid grid-cols-2 md:grid-cols-4 gap-3">
|
|
403
|
+
{[
|
|
404
|
+
{ label: "Huellas Planas", value: fingerCount, icon: <TbFingerprint />, color: "text-blue-400" },
|
|
405
|
+
{ label: "Huellas Roladas", value: rolledCount, icon: <TbScan />, color: "text-purple-400" },
|
|
406
|
+
{ label: "Palmas", value: palmCount, icon: <TbHandStop />, color: "text-orange-400" },
|
|
407
|
+
{ label: "Rostros", value: faceCount, icon: <TbFaceId />, color: "text-pink-400" },
|
|
408
|
+
{ label: "Iris", value: irisCount, icon: <TbEye />, color: "text-cyan-400" },
|
|
409
|
+
{ label: "Faltantes", value: missingCount, icon: <TbFingerprint />, color: "text-red-400" },
|
|
410
|
+
{ label: "External ID", value: enrollment.externalId || "—", icon: <TbDatabase />, color: "text-green-400" },
|
|
411
|
+
{ label: "Modo", value: enrollment.isUpdateMode ? "Update" : "New", icon: <TbSettings />, color: "text-yellow-400" },
|
|
412
|
+
].map(({ label, value, icon, color }) => (
|
|
413
|
+
<div key={label} className="p-3 bg-secondary/30 rounded-lg border border-border/30 flex items-center gap-3">
|
|
414
|
+
<div className={`${color}`}>{icon}</div>
|
|
415
|
+
<div>
|
|
416
|
+
<p className="text-[10px] text-foreground/40 uppercase tracking-wider">{label}</p>
|
|
417
|
+
<p className="text-sm font-bold text-foreground">{String(value)}</p>
|
|
418
|
+
</div>
|
|
419
|
+
</div>
|
|
420
|
+
))}
|
|
421
|
+
</div>
|
|
422
|
+
|
|
423
|
+
{/* Device Status */}
|
|
424
|
+
<div className="flex items-center gap-4">
|
|
425
|
+
<StatusChip
|
|
426
|
+
status={biometric.isConnected ? "success" : "danger"}
|
|
427
|
+
label={biometric.isConnected ? "WebSocket Online" : "WebSocket Offline"}
|
|
428
|
+
/>
|
|
429
|
+
<span className="text-xs text-foreground/40">
|
|
430
|
+
Mensajes UI: {ui.messages.length}
|
|
431
|
+
</span>
|
|
432
|
+
</div>
|
|
433
|
+
|
|
434
|
+
<CodeBlock
|
|
435
|
+
title="Acceso a stores"
|
|
436
|
+
code={`import { useEnrollmentStore, useBiometricStore, useUiStore } from '@intell/biometric-sdk';
|
|
437
|
+
import type { BiometricItem, EnrollmentState, BiometricState } from '@intell/biometric-sdk';
|
|
438
|
+
|
|
439
|
+
// Leer estado
|
|
440
|
+
const { fingerprints, faces, irises } = useEnrollmentStore();
|
|
441
|
+
const { isConnected, deviceStatus } = useBiometricStore();
|
|
442
|
+
|
|
443
|
+
// Tipar custom components
|
|
444
|
+
const myFingers: Record<string, BiometricItem> = fingerprints;
|
|
445
|
+
|
|
446
|
+
// Suscripción selectiva (Zustand selector)
|
|
447
|
+
const count = useEnrollmentStore(s => Object.keys(s.fingerprints).length);
|
|
448
|
+
|
|
449
|
+
// Acceso fuera de React
|
|
450
|
+
const state = useEnrollmentStore.getState();
|
|
451
|
+
const payload = state.buildEnrollmentRequest();`}
|
|
452
|
+
/>
|
|
453
|
+
</div>
|
|
454
|
+
);
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
// ─── 5. Custom Slot Grid ─────────────────────────────────
|
|
458
|
+
function CustomSlotGrid() {
|
|
459
|
+
const { fingerprints, addFingerprint, removeFingerprint, addMissingFinger, removeMissingFinger, missingFingers } =
|
|
460
|
+
useEnrollmentStore();
|
|
461
|
+
const { sendMessage, registerHandler, isConnected } = useBiometricStore();
|
|
462
|
+
|
|
463
|
+
const positions = [
|
|
464
|
+
{ key: "RIGHT_THUMB", label: "Pulgar D." },
|
|
465
|
+
{ key: "RIGHT_INDEX", label: "Índice D." },
|
|
466
|
+
{ key: "RIGHT_MIDDLE", label: "Medio D." },
|
|
467
|
+
{ key: "RIGHT_RING", label: "Anular D." },
|
|
468
|
+
{ key: "RIGHT_LITTLE", label: "Meñique D." },
|
|
469
|
+
];
|
|
470
|
+
|
|
471
|
+
const handleCapture = useCallback(
|
|
472
|
+
(pos: string) => {
|
|
473
|
+
if (!isConnected) return;
|
|
474
|
+
registerHandler("fingerprint_captured", (msg: any) => {
|
|
475
|
+
addFingerprint(pos, msg.image, msg.quality);
|
|
476
|
+
});
|
|
477
|
+
sendMessage({ messageType: "capture_fingerprint", position: pos });
|
|
478
|
+
},
|
|
479
|
+
[isConnected, registerHandler, sendMessage, addFingerprint]
|
|
480
|
+
);
|
|
481
|
+
|
|
482
|
+
return (
|
|
483
|
+
<div className="space-y-4">
|
|
484
|
+
<p className="text-sm text-foreground/60">
|
|
485
|
+
Grid custom usando <code className="px-1.5 py-0.5 bg-secondary rounded text-xs font-mono">BiometricSlot</code> + stores.
|
|
486
|
+
Tú decides el layout, posiciones y callbacks:
|
|
487
|
+
</p>
|
|
488
|
+
<div className="grid grid-cols-5 gap-3">
|
|
489
|
+
{positions.map(({ key, label }) => {
|
|
490
|
+
const fp = fingerprints[key];
|
|
491
|
+
const missing = missingFingers[key];
|
|
492
|
+
return (
|
|
493
|
+
<BiometricSlot
|
|
494
|
+
key={key}
|
|
495
|
+
title={label}
|
|
496
|
+
position={key}
|
|
497
|
+
iconType="finger"
|
|
498
|
+
status={fp ? "captured" : "pending"}
|
|
499
|
+
quality={fp?.quality}
|
|
500
|
+
imageUrl={fp ? getImageSrcForDisplay(fp.image) : null}
|
|
501
|
+
onCapture={() => handleCapture(key)}
|
|
502
|
+
onDelete={() => removeFingerprint(key)}
|
|
503
|
+
onRequestMissing={() => addMissingFinger(key, "Amputación")}
|
|
504
|
+
onRestore={() => removeMissingFinger(key)}
|
|
505
|
+
missingReason={missing}
|
|
506
|
+
/>
|
|
507
|
+
);
|
|
508
|
+
})}
|
|
509
|
+
</div>
|
|
510
|
+
<CodeBlock
|
|
511
|
+
title="Código del grid custom"
|
|
512
|
+
code={`import { BiometricSlot, useEnrollmentStore, useBiometricStore, getImageSrcForDisplay } from '@intell/biometric-sdk';
|
|
513
|
+
|
|
514
|
+
const positions = ['RIGHT_THUMB', 'RIGHT_INDEX', 'RIGHT_MIDDLE'];
|
|
515
|
+
|
|
516
|
+
function MyGrid() {
|
|
517
|
+
const { fingerprints, addFingerprint, removeFingerprint } = useEnrollmentStore();
|
|
518
|
+
const { sendMessage, registerHandler } = useBiometricStore();
|
|
519
|
+
|
|
520
|
+
return (
|
|
521
|
+
<div className="grid grid-cols-5 gap-3">
|
|
522
|
+
{positions.map(pos => {
|
|
523
|
+
const fp = fingerprints[pos];
|
|
524
|
+
return (
|
|
525
|
+
<BiometricSlot
|
|
526
|
+
key={pos}
|
|
527
|
+
title={pos}
|
|
528
|
+
position={pos}
|
|
529
|
+
iconType="finger"
|
|
530
|
+
status={fp ? 'captured' : 'pending'}
|
|
531
|
+
quality={fp?.quality}
|
|
532
|
+
imageUrl={fp ? getImageSrcForDisplay(fp.image) : null}
|
|
533
|
+
onCapture={() => { /* tu lógica */ }}
|
|
534
|
+
onDelete={() => removeFingerprint(pos)}
|
|
535
|
+
/>
|
|
536
|
+
);
|
|
537
|
+
})}
|
|
538
|
+
</div>
|
|
539
|
+
);
|
|
540
|
+
}`}
|
|
541
|
+
/>
|
|
542
|
+
</div>
|
|
543
|
+
);
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
// ─── 6. Custom Capture Modal ─────────────────────────────
|
|
547
|
+
function CustomCaptureModalExample() {
|
|
548
|
+
return (
|
|
549
|
+
<div className="space-y-4">
|
|
550
|
+
<p className="text-sm text-foreground/60">
|
|
551
|
+
Crea tu propio flujo de captura con las primitivas del SDK:
|
|
552
|
+
</p>
|
|
553
|
+
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
|
554
|
+
<Card className="p-4 space-y-2">
|
|
555
|
+
<h4 className="font-bold text-sm text-primary">sendMessage()</h4>
|
|
556
|
+
<p className="text-xs text-foreground/60">Envía comandos al dispositivo vía WebSocket</p>
|
|
557
|
+
<CodeBlock code={`sendMessage({
|
|
558
|
+
messageType: 'capture_fingerprint',
|
|
559
|
+
position: 'RIGHT_INDEX'
|
|
560
|
+
});`} />
|
|
561
|
+
</Card>
|
|
562
|
+
<Card className="p-4 space-y-2">
|
|
563
|
+
<h4 className="font-bold text-sm text-primary">registerHandler()</h4>
|
|
564
|
+
<p className="text-xs text-foreground/60">Recibe respuestas del dispositivo</p>
|
|
565
|
+
<CodeBlock code={`registerHandler('fingerprint_captured',
|
|
566
|
+
(msg) => {
|
|
567
|
+
// msg.image = base64
|
|
568
|
+
// msg.quality = 0-100
|
|
569
|
+
addFingerprint(pos, msg.image,
|
|
570
|
+
msg.quality);
|
|
571
|
+
}
|
|
572
|
+
);`} />
|
|
573
|
+
</Card>
|
|
574
|
+
<Card className="p-4 space-y-2">
|
|
575
|
+
<h4 className="font-bold text-sm text-primary">Store Methods</h4>
|
|
576
|
+
<p className="text-xs text-foreground/60">Agregar/remover datos biométricos</p>
|
|
577
|
+
<CodeBlock code={`// Huellas
|
|
578
|
+
addFingerprint(pos, image, quality)
|
|
579
|
+
removeFingerprint(pos)
|
|
580
|
+
addMissingFinger(pos, reason)
|
|
581
|
+
|
|
582
|
+
// Rostros
|
|
583
|
+
setFaces([{image, quality}])
|
|
584
|
+
removeFace(index)
|
|
585
|
+
|
|
586
|
+
// Iris
|
|
587
|
+
setIrises(right, left)
|
|
588
|
+
removeIris('left')
|
|
589
|
+
|
|
590
|
+
// Palmas
|
|
591
|
+
addPalm(pos, image, quality)`} />
|
|
592
|
+
</Card>
|
|
593
|
+
</div>
|
|
594
|
+
<CodeBlock
|
|
595
|
+
title="Tu propio modal de captura facial"
|
|
596
|
+
code={`import { Modal, Button, useBiometricStore, useEnrollmentStore } from '@intell/biometric-sdk';
|
|
597
|
+
|
|
598
|
+
function MyFaceCapture({ isOpen, onClose }) {
|
|
599
|
+
const { sendMessage, registerHandler, unregisterHandler } = useBiometricStore();
|
|
600
|
+
const { setFaces } = useEnrollmentStore();
|
|
601
|
+
const [preview, setPreview] = useState(null);
|
|
602
|
+
|
|
603
|
+
useEffect(() => {
|
|
604
|
+
registerHandler('face_captured', (msg) => setPreview(msg.image));
|
|
605
|
+
return () => unregisterHandler('face_captured');
|
|
606
|
+
}, []);
|
|
607
|
+
|
|
608
|
+
return (
|
|
609
|
+
<Modal isOpen={isOpen} onClose={onClose} title="Mi Captura">
|
|
610
|
+
{preview ? <img src={preview} /> : <Loader text="Esperando..." />}
|
|
611
|
+
<Button onClick={() => sendMessage({ messageType: 'start_face' })}>Capturar</Button>
|
|
612
|
+
<Button onClick={() => { setFaces([{ image: preview, quality: 95 }]); onClose(); }}>
|
|
613
|
+
Confirmar
|
|
614
|
+
</Button>
|
|
615
|
+
</Modal>
|
|
616
|
+
);
|
|
617
|
+
}`}
|
|
618
|
+
/>
|
|
619
|
+
</div>
|
|
620
|
+
);
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
// ─── 7. Types & Extensions ───────────────────────────────
|
|
624
|
+
function TypesExample() {
|
|
625
|
+
return (
|
|
626
|
+
<div className="space-y-4">
|
|
627
|
+
<p className="text-sm text-foreground/60">
|
|
628
|
+
Todos los tipos del SDK están exportados para tipar tus custom components:
|
|
629
|
+
</p>
|
|
630
|
+
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
631
|
+
<CodeBlock
|
|
632
|
+
title="Tipos de Store disponibles"
|
|
633
|
+
code={`import type {
|
|
634
|
+
// Estado de enrolamiento
|
|
635
|
+
BiometricItem, // { image, quality, impressionType? }
|
|
636
|
+
EnrollmentState, // Estado completo del enrollment
|
|
637
|
+
OriginalSnapshot, // Snapshot para dirty tracking
|
|
638
|
+
|
|
639
|
+
// Estado del dispositivo
|
|
640
|
+
BiometricState, // WebSocket + device status
|
|
641
|
+
DeviceStatus, // { fingerprintDevices, irisDeviceOpen }
|
|
642
|
+
SocketMessage, // { messageType, ...data }
|
|
643
|
+
MessageHandler, // (message) => void
|
|
644
|
+
|
|
645
|
+
// UI state
|
|
646
|
+
UiState, // { messages, isConsoleOpen, ... }
|
|
647
|
+
} from '@intell/biometric-sdk';`}
|
|
648
|
+
/>
|
|
649
|
+
<CodeBlock
|
|
650
|
+
title="Tipos de Componentes"
|
|
651
|
+
code={`import type {
|
|
652
|
+
// Props de componentes
|
|
653
|
+
ButtonProps, // extends HTMLButtonAttributes
|
|
654
|
+
CardProps, // variant, padding, rounded, ...
|
|
655
|
+
InputProps, // extends HTMLInputAttributes
|
|
656
|
+
ModalProps, // isOpen, onClose, title, ...
|
|
657
|
+
BiometricSlotProps, // title, status, quality, ...
|
|
658
|
+
BiometricConfig, // wsUrl, apiBaseUrl, deviceId
|
|
659
|
+
|
|
660
|
+
// ABIS Protocol
|
|
661
|
+
EnrollApplicantRequest,
|
|
662
|
+
FingerprintPosition,
|
|
663
|
+
FingerprintImpressionType,
|
|
664
|
+
IrisPosition,
|
|
665
|
+
EnrollAction,
|
|
666
|
+
} from '@intell/biometric-sdk';`}
|
|
667
|
+
/>
|
|
668
|
+
</div>
|
|
669
|
+
<CodeBlock
|
|
670
|
+
title="Ejemplo: Extender un componente del SDK"
|
|
671
|
+
code={`import { Button, type ButtonProps } from '@intell/biometric-sdk';
|
|
672
|
+
|
|
673
|
+
// Componente que extiende Button con funcionalidad extra
|
|
674
|
+
interface MyButtonProps extends ButtonProps {
|
|
675
|
+
tooltip?: string;
|
|
676
|
+
analytics?: string;
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
function MyButton({ tooltip, analytics, onClick, ...rest }: MyButtonProps) {
|
|
680
|
+
const handleClick = (e) => {
|
|
681
|
+
if (analytics) trackEvent(analytics);
|
|
682
|
+
onClick?.(e);
|
|
683
|
+
};
|
|
684
|
+
return (
|
|
685
|
+
<div title={tooltip}>
|
|
686
|
+
<Button onClick={handleClick} {...rest} />
|
|
687
|
+
</div>
|
|
688
|
+
);
|
|
689
|
+
}`}
|
|
690
|
+
/>
|
|
691
|
+
</div>
|
|
692
|
+
);
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
// ─── 8. API Integration ──────────────────────────────────
|
|
696
|
+
function ApiCallExample() {
|
|
697
|
+
const config = useBiometricConfig();
|
|
698
|
+
const { buildEnrollmentRequest, loadApplicantData, setDemographics } =
|
|
699
|
+
useEnrollmentStore();
|
|
700
|
+
const [response, setResponse] = useState("");
|
|
701
|
+
|
|
702
|
+
const handleBuildPayload = () => {
|
|
703
|
+
const payload = buildEnrollmentRequest();
|
|
704
|
+
setResponse(JSON.stringify(payload, null, 2));
|
|
705
|
+
};
|
|
706
|
+
|
|
707
|
+
const handleSimulateLoad = async () => {
|
|
708
|
+
setDemographics("EXT-001", "Juan Pérez");
|
|
709
|
+
setResponse(
|
|
710
|
+
`✅ Datos cargados.\n\nEn producción:\nconst res = await fetch('${config.apiBaseUrl}/applicant/EXT-001');\nconst data = await res.json();\nawait loadApplicantData(data);`
|
|
711
|
+
);
|
|
712
|
+
};
|
|
713
|
+
|
|
714
|
+
const handleSimulateEnroll = () => {
|
|
715
|
+
const payload = buildEnrollmentRequest();
|
|
716
|
+
setResponse(
|
|
717
|
+
`📤 En producción:\n\nawait fetch('${config.apiBaseUrl}/biometric/enrollment', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(payload)\n});\n\n${JSON.stringify(payload, null, 2).slice(0, 400)}...`
|
|
718
|
+
);
|
|
719
|
+
};
|
|
720
|
+
|
|
721
|
+
return (
|
|
722
|
+
<div className="space-y-4">
|
|
723
|
+
<p className="text-sm text-foreground/60">
|
|
724
|
+
El SDK <strong>NO</strong> hace llamadas API. Tu front es quien se comunica con el backend.
|
|
725
|
+
</p>
|
|
726
|
+
<CodeBlock
|
|
727
|
+
title="Flujo completo"
|
|
728
|
+
code={`const { apiBaseUrl } = useBiometricConfig();
|
|
729
|
+
|
|
730
|
+
// 1. Tu front busca datos existentes
|
|
731
|
+
const res = await fetch(\`\${apiBaseUrl}/applicant/\${id}\`);
|
|
732
|
+
const data = await res.json();
|
|
733
|
+
|
|
734
|
+
// 2. El SDK mapea los datos al estado interno
|
|
735
|
+
await useEnrollmentStore.getState().loadApplicantData(data);
|
|
736
|
+
|
|
737
|
+
// 3. El usuario captura biometría con los módulos...
|
|
738
|
+
|
|
739
|
+
// 4. El SDK arma el payload
|
|
740
|
+
const payload = useEnrollmentStore.getState().buildEnrollmentRequest();
|
|
741
|
+
|
|
742
|
+
// 5. Tu front envía
|
|
743
|
+
await fetch(\`\${apiBaseUrl}/biometric/enrollment\`, {
|
|
744
|
+
method: 'POST', body: JSON.stringify(payload)
|
|
745
|
+
});`}
|
|
746
|
+
/>
|
|
747
|
+
<div className="flex gap-3 flex-wrap">
|
|
748
|
+
<Button onClick={handleSimulateLoad} variant="secondary" size="sm">loadApplicantData()</Button>
|
|
749
|
+
<Button onClick={handleBuildPayload} variant="secondary" size="sm">buildEnrollmentRequest()</Button>
|
|
750
|
+
<Button onClick={handleSimulateEnroll} variant="primary" size="sm">Simular Envío</Button>
|
|
751
|
+
</div>
|
|
752
|
+
{response && (
|
|
753
|
+
<pre className="p-4 bg-[#0d1117] rounded-lg text-xs font-mono text-green-400 max-h-60 overflow-auto border border-white/10 whitespace-pre-wrap">
|
|
754
|
+
{response}
|
|
755
|
+
</pre>
|
|
756
|
+
)}
|
|
757
|
+
</div>
|
|
758
|
+
);
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
// ─── 9. Utilities ────────────────────────────────────────
|
|
762
|
+
function UtilitiesExample() {
|
|
763
|
+
return (
|
|
764
|
+
<div className="space-y-4">
|
|
765
|
+
<p className="text-sm text-foreground/60">
|
|
766
|
+
Utilidades de imagen exportadas para manipular datos biométricos:
|
|
767
|
+
</p>
|
|
768
|
+
<CodeBlock
|
|
769
|
+
title="Utilidades de imagen disponibles"
|
|
770
|
+
code={`import {
|
|
771
|
+
normalizeImageForStorage, // Limpia prefijos data:image/... para storage
|
|
772
|
+
getImageSrcForDisplay, // Agrega prefijo data:image/... para <img>
|
|
773
|
+
isRemoteImage, // Detecta si es URL HTTP(s)
|
|
774
|
+
downloadImageAsBase64, // Descarga imagen remota a base64
|
|
775
|
+
canvasToBase64, // Canvas → base64
|
|
776
|
+
base64toBlob, // base64 → Blob (para FormData)
|
|
777
|
+
detectImageFormat, // Detecta formato (png, jpg, bmp, wsq)
|
|
778
|
+
isValidBase64, // Valida string base64
|
|
779
|
+
} from '@intell/biometric-sdk';
|
|
780
|
+
|
|
781
|
+
// Ejemplo: Convertir captura a Blob para subir por FormData
|
|
782
|
+
const blob = base64toBlob(fingerprint.image);
|
|
783
|
+
const formData = new FormData();
|
|
784
|
+
formData.append('image', blob, 'fingerprint.png');
|
|
785
|
+
|
|
786
|
+
// Ejemplo: Mostrar imagen en <img>
|
|
787
|
+
const src = getImageSrcForDisplay(fingerprint.image);
|
|
788
|
+
return <img src={src} alt="Huella" />;`}
|
|
789
|
+
/>
|
|
790
|
+
</div>
|
|
791
|
+
);
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
// ─── 10. Advanced: Full Custom Components ─────────────────
|
|
795
|
+
function FullCustomComponentExample() {
|
|
796
|
+
const { fingerprints, removeFingerprint } = useEnrollmentStore();
|
|
797
|
+
const { isConnected, sendMessage } = useBiometricStore();
|
|
798
|
+
|
|
799
|
+
const handleManualCapture = (pos: string) => {
|
|
800
|
+
sendMessage({ messageType: "capture_fingerprint", position: pos });
|
|
801
|
+
};
|
|
802
|
+
|
|
803
|
+
return (
|
|
804
|
+
<div className="space-y-6">
|
|
805
|
+
<div className="p-4 bg-amber-500/10 border border-amber-500/20 rounded-lg">
|
|
806
|
+
<p className="text-xs text-amber-500 font-bold uppercase tracking-widest mb-1">Concepto Avanzado</p>
|
|
807
|
+
<p className="text-sm text-foreground/80">
|
|
808
|
+
A veces no quieres usar <code className="text-xs font-mono bg-amber-500/10 px-1 rounded">BiometricSlot</code>.
|
|
809
|
+
Aquí tienes un ejemplo de cómo construir tu propio UI interactivo directamente sobre los stores.
|
|
810
|
+
</p>
|
|
811
|
+
</div>
|
|
812
|
+
|
|
813
|
+
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
814
|
+
{/* Concept 1: Minimalist Row Slot */}
|
|
815
|
+
<div className="space-y-3">
|
|
816
|
+
<h4 className="text-xs font-bold uppercase text-foreground/40">1. Minimalist Row Slot</h4>
|
|
817
|
+
<div className="p-3 bg-secondary/20 rounded-lg border border-border/30 flex items-center justify-between hover:border-primary/50 transition-all group">
|
|
818
|
+
<div className="flex items-center gap-3">
|
|
819
|
+
<div className={`w-10 h-10 rounded-full flex items-center justify-center ${fingerprints['RIGHT_THUMB'] ? 'bg-success/20 text-success' : 'bg-foreground/5 text-foreground/20'}`}>
|
|
820
|
+
<TbFingerprint size={24} />
|
|
821
|
+
</div>
|
|
822
|
+
<div>
|
|
823
|
+
<p className="text-sm font-bold">Pulgar Derecho</p>
|
|
824
|
+
<p className="text-[10px] text-foreground/40 uppercase tracking-tighter">
|
|
825
|
+
{fingerprints['RIGHT_THUMB'] ? `Calidad: ${fingerprints['RIGHT_THUMB'].quality}%` : 'Pendiente de captura'}
|
|
826
|
+
</p>
|
|
827
|
+
</div>
|
|
828
|
+
</div>
|
|
829
|
+
<div className="flex items-center gap-2">
|
|
830
|
+
{fingerprints['RIGHT_THUMB'] ? (
|
|
831
|
+
<Button variant="ghost" size="sm" onClick={() => removeFingerprint('RIGHT_THUMB')}>
|
|
832
|
+
Descartar
|
|
833
|
+
</Button>
|
|
834
|
+
) : (
|
|
835
|
+
<Button variant="primary" size="sm" onClick={() => handleManualCapture('RIGHT_THUMB')} disabled={!isConnected}>
|
|
836
|
+
Capturar
|
|
837
|
+
</Button>
|
|
838
|
+
)}
|
|
839
|
+
</div>
|
|
840
|
+
</div>
|
|
841
|
+
<CodeBlock title="Código: Slot minimalista custom" code={`function MyMinimalSlot({ pos }) {
|
|
842
|
+
const fp = useEnrollmentStore(s => s.fingerprints[pos]);
|
|
843
|
+
const capture = () => sendMessage({
|
|
844
|
+
messageType: 'capture_fingerprint', position: pos
|
|
845
|
+
});
|
|
846
|
+
|
|
847
|
+
return (
|
|
848
|
+
<div className="flex justify-between items-center">
|
|
849
|
+
<span>{pos}</span>
|
|
850
|
+
<button onClick={capture}>{fp ? 'Repetir' : 'Capturar'}</button>
|
|
851
|
+
</div>
|
|
852
|
+
);
|
|
853
|
+
}`} />
|
|
854
|
+
</div>
|
|
855
|
+
|
|
856
|
+
{/* Concept 2: Full Custom Modal Overlay */}
|
|
857
|
+
<div className="space-y-3">
|
|
858
|
+
<h4 className="text-xs font-bold uppercase text-foreground/40">2. Custom Modal Overlay</h4>
|
|
859
|
+
<div className="aspect-video bg-black rounded-lg relative overflow-hidden flex items-center justify-center border border-white/10 group">
|
|
860
|
+
{/* Camera Simulation */}
|
|
861
|
+
<div className="absolute inset-0 bg-blue-500/5 flex items-center justify-center">
|
|
862
|
+
<TbFaceId size={60} className="text-white/10 animate-pulse" />
|
|
863
|
+
</div>
|
|
864
|
+
|
|
865
|
+
{/* Custom UI Overlays */}
|
|
866
|
+
<div className="absolute top-4 left-4">
|
|
867
|
+
<div className="px-2 py-1 bg-black/60 backdrop-blur-md rounded border border-white/20 text-[10px] font-bold text-white uppercase tracking-widest">
|
|
868
|
+
REC ● LIVE
|
|
869
|
+
</div>
|
|
870
|
+
</div>
|
|
871
|
+
|
|
872
|
+
<div className="absolute bottom-4 inset-x-4 flex justify-between items-end">
|
|
873
|
+
<div className="p-2 bg-black/40 rounded text-[10px] text-white/60 font-mono">
|
|
874
|
+
30 FPS <br/> ISO 400
|
|
875
|
+
</div>
|
|
876
|
+
<button className="w-12 h-12 rounded-full bg-white border-4 border-white/20 shadow-xl active:scale-95 transition-all" />
|
|
877
|
+
<div className="p-2 bg-black/40 rounded text-[10px] text-white/60 text-right font-mono">
|
|
878
|
+
85% <br/> MATCH
|
|
879
|
+
</div>
|
|
880
|
+
</div>
|
|
881
|
+
|
|
882
|
+
{/* Scanning line effect */}
|
|
883
|
+
<div className="absolute top-0 inset-x-0 h-1 bg-primary/40 shadow-[0_0_15px_rgba(var(--primary),0.8)] animate-scan-line" />
|
|
884
|
+
</div>
|
|
885
|
+
<p className="text-[10px] text-foreground/40 italic">Muestra de cómo puedes crear tu propio visor de captura con overlays personalizados.</p>
|
|
886
|
+
</div>
|
|
887
|
+
</div>
|
|
888
|
+
</div>
|
|
889
|
+
);
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
// ─── 10. Theming & Custom CSS Variables ───────────────────
|
|
893
|
+
function ThemingExample() {
|
|
894
|
+
return (
|
|
895
|
+
<div className="space-y-4">
|
|
896
|
+
<p className="text-sm text-foreground/60">
|
|
897
|
+
El SDK usa variables CSS nativas para sus colores. Puedes cambiarlas globalmente o solo para un módulo:
|
|
898
|
+
</p>
|
|
899
|
+
<div className="p-5 bg-[#0d1117] rounded-xl border border-white/10">
|
|
900
|
+
<h4 className="flex items-center gap-2 text-xs font-bold text-white/60 uppercase mb-4">
|
|
901
|
+
<TbColorSwatch className="text-primary" /> Variables CSS Disponibles
|
|
902
|
+
</h4>
|
|
903
|
+
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
904
|
+
<div className="space-y-4">
|
|
905
|
+
<div className="space-y-1">
|
|
906
|
+
<p className="text-[10px] text-white/30 font-mono">--primary</p>
|
|
907
|
+
<div className="flex gap-2 items-center">
|
|
908
|
+
<div className="w-4 h-4 rounded bg-primary" />
|
|
909
|
+
<span className="text-xs text-white/70">Color principal / acción</span>
|
|
910
|
+
</div>
|
|
911
|
+
</div>
|
|
912
|
+
<div className="space-y-1">
|
|
913
|
+
<p className="text-[10px] text-white/30 font-mono">--success</p>
|
|
914
|
+
<div className="flex gap-2 items-center">
|
|
915
|
+
<div className="w-4 h-4 rounded bg-success" />
|
|
916
|
+
<span className="text-xs text-white/70">Calidad alta / éxito</span>
|
|
917
|
+
</div>
|
|
918
|
+
</div>
|
|
919
|
+
<div className="space-y-1">
|
|
920
|
+
<p className="text-[10px] text-white/30 font-mono">--danger</p>
|
|
921
|
+
<div className="flex gap-2 items-center">
|
|
922
|
+
<div className="w-4 h-4 rounded bg-danger" />
|
|
923
|
+
<span className="text-xs text-white/70">Error / Omisión</span>
|
|
924
|
+
</div>
|
|
925
|
+
</div>
|
|
926
|
+
</div>
|
|
927
|
+
<div className="space-y-4">
|
|
928
|
+
<div className="space-y-1">
|
|
929
|
+
<p className="text-[10px] text-white/30 font-mono">--background / --foreground</p>
|
|
930
|
+
<span className="text-xs text-white/70">Colores base (textos y fondos)</span>
|
|
931
|
+
</div>
|
|
932
|
+
<div className="space-y-1">
|
|
933
|
+
<p className="text-[10px] text-white/30 font-mono">--card / --border</p>
|
|
934
|
+
<span className="text-xs text-white/70">Superficies y divisiones</span>
|
|
935
|
+
</div>
|
|
936
|
+
</div>
|
|
937
|
+
</div>
|
|
938
|
+
</div>
|
|
939
|
+
|
|
940
|
+
<CodeBlock title="Inyectando tu tema" code={`/* En tu globals.css o un style tag */
|
|
941
|
+
.my-custom-theme {
|
|
942
|
+
--primary: 219 255 0; /* Un verde vibrante */
|
|
943
|
+
--primary-foreground: 0 0 0;
|
|
944
|
+
--success: 34 197 94;
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
// Aplicar solo a un módulo
|
|
948
|
+
<FingerEnrollModule className="my-custom-theme" />`} />
|
|
949
|
+
</div>
|
|
950
|
+
);
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
// ─── Main Test Page ──────────────────────────────────────
|
|
954
|
+
export default function TestSDKPage() {
|
|
955
|
+
const [mounted, setMounted] = useState(false);
|
|
956
|
+
const [search, setSearch] = useState("");
|
|
957
|
+
const [openSections, setOpenSections] = useState<Record<string, boolean>>({
|
|
958
|
+
"intro": true,
|
|
959
|
+
});
|
|
960
|
+
|
|
961
|
+
const config = useBiometricConfig();
|
|
962
|
+
|
|
963
|
+
useEffect(() => {
|
|
964
|
+
setMounted(true);
|
|
965
|
+
}, []);
|
|
966
|
+
|
|
967
|
+
const sections: SectionInfo[] = [
|
|
968
|
+
{
|
|
969
|
+
id: "config",
|
|
970
|
+
title: "1. Configuración (Provider)",
|
|
971
|
+
category: "basics",
|
|
972
|
+
icon: TbSettings,
|
|
973
|
+
component: <ProviderExample />,
|
|
974
|
+
keywords: ["config", "provider", "wsurl", "apibaseurl", "setup", "init"],
|
|
975
|
+
},
|
|
976
|
+
{
|
|
977
|
+
id: "ui",
|
|
978
|
+
title: "2. Componentes UI",
|
|
979
|
+
category: "basics",
|
|
980
|
+
icon: TbComponents,
|
|
981
|
+
component: <UIComponentsShowcase />,
|
|
982
|
+
keywords: ["button", "card", "input", "select", "badge", "chip", "loader", "modal", "ui"],
|
|
983
|
+
},
|
|
984
|
+
{
|
|
985
|
+
id: "styling",
|
|
986
|
+
title: "3. Personalización de Estilos",
|
|
987
|
+
category: "basics",
|
|
988
|
+
icon: TbPalette,
|
|
989
|
+
component: <StyleCustomizationExample />,
|
|
990
|
+
keywords: ["style", "css", "tailwind", "classname", "theme", "colors"],
|
|
991
|
+
},
|
|
992
|
+
{
|
|
993
|
+
id: "theming",
|
|
994
|
+
title: "4. Tematización (CSS Vars)",
|
|
995
|
+
category: "basics",
|
|
996
|
+
icon: TbColorSwatch,
|
|
997
|
+
component: <ThemingExample />,
|
|
998
|
+
keywords: ["theme", "variables", "css", "colors", "primary", "branding"],
|
|
999
|
+
},
|
|
1000
|
+
{
|
|
1001
|
+
id: "data",
|
|
1002
|
+
title: "5. Acceso a Datos y Stores",
|
|
1003
|
+
category: "data",
|
|
1004
|
+
icon: TbDatabase,
|
|
1005
|
+
component: <StoreAccessExample />,
|
|
1006
|
+
keywords: ["store", "data", "zustand", "enrollment", "biometric", "state"],
|
|
1007
|
+
},
|
|
1008
|
+
{
|
|
1009
|
+
id: "api",
|
|
1010
|
+
title: "6. Integración API",
|
|
1011
|
+
category: "data",
|
|
1012
|
+
icon: TbApi,
|
|
1013
|
+
component: <ApiCallExample />,
|
|
1014
|
+
keywords: ["api", "fetch", "enrollment", "payload", "loadapplicantdata", "backend"],
|
|
1015
|
+
},
|
|
1016
|
+
{
|
|
1017
|
+
id: "utils",
|
|
1018
|
+
title: "7. Utilidades de Imagen",
|
|
1019
|
+
category: "data",
|
|
1020
|
+
icon: TbPhoto,
|
|
1021
|
+
component: <UtilitiesExample />,
|
|
1022
|
+
keywords: ["image", "base64", "photo", "convert", "blob", "src", "utility"],
|
|
1023
|
+
},
|
|
1024
|
+
{
|
|
1025
|
+
id: "custom-grid",
|
|
1026
|
+
title: "8. Grid Custom de Slots",
|
|
1027
|
+
category: "advanced",
|
|
1028
|
+
icon: TbFingerprint,
|
|
1029
|
+
component: <CustomSlotGrid />,
|
|
1030
|
+
keywords: ["custom", "slot", "grid", "position", "finger", "build"],
|
|
1031
|
+
},
|
|
1032
|
+
{
|
|
1033
|
+
id: "advanced-custom",
|
|
1034
|
+
title: "9. Componentes 100% Custom",
|
|
1035
|
+
category: "advanced",
|
|
1036
|
+
icon: TbTerminal2,
|
|
1037
|
+
component: <FullCustomComponentExample />,
|
|
1038
|
+
keywords: ["advanced", "scratch", "manual", "custom", "example", "architecture"],
|
|
1039
|
+
},
|
|
1040
|
+
{
|
|
1041
|
+
id: "capture-modal",
|
|
1042
|
+
title: "10. Modal de Captura Custom",
|
|
1043
|
+
category: "advanced",
|
|
1044
|
+
icon: TbPuzzle,
|
|
1045
|
+
component: <CustomCaptureModalExample />,
|
|
1046
|
+
keywords: ["modal", "capture", "websocket", "sendmessage", "registerhandler", "custom"],
|
|
1047
|
+
},
|
|
1048
|
+
{
|
|
1049
|
+
id: "types",
|
|
1050
|
+
title: "11. Tipos y Extensiones",
|
|
1051
|
+
category: "advanced",
|
|
1052
|
+
icon: TbBraces,
|
|
1053
|
+
component: <TypesExample />,
|
|
1054
|
+
keywords: ["types", "typescript", "interfaces", "props", "extending"],
|
|
1055
|
+
},
|
|
1056
|
+
{
|
|
1057
|
+
id: "mod-finger",
|
|
1058
|
+
title: "12. Módulo: Huellas Planas",
|
|
1059
|
+
category: "modules",
|
|
1060
|
+
icon: TbFingerprint,
|
|
1061
|
+
component: <FingerEnrollModule />,
|
|
1062
|
+
keywords: ["finger", "fingerprint", "huella", "plana", "module"],
|
|
1063
|
+
},
|
|
1064
|
+
{
|
|
1065
|
+
id: "mod-rolled",
|
|
1066
|
+
title: "13. Módulo: Huellas Roladas",
|
|
1067
|
+
category: "modules",
|
|
1068
|
+
icon: TbScan,
|
|
1069
|
+
component: <FingerRollEnrollModule />,
|
|
1070
|
+
keywords: ["rolled", "fingerprint", "huella", "rolada", "module"],
|
|
1071
|
+
},
|
|
1072
|
+
{
|
|
1073
|
+
id: "mod-palm",
|
|
1074
|
+
title: "14. Módulo: Palmas",
|
|
1075
|
+
category: "modules",
|
|
1076
|
+
icon: TbHandStop,
|
|
1077
|
+
component: <PalmEnrollModule />,
|
|
1078
|
+
keywords: ["palm", "hand", "palma", "module"],
|
|
1079
|
+
},
|
|
1080
|
+
{
|
|
1081
|
+
id: "mod-iris",
|
|
1082
|
+
title: "15. Módulo: Iris",
|
|
1083
|
+
category: "modules",
|
|
1084
|
+
icon: TbEye,
|
|
1085
|
+
component: <IrisEnrollModule />,
|
|
1086
|
+
keywords: ["iris", "eye", "ojo", "module"],
|
|
1087
|
+
},
|
|
1088
|
+
{
|
|
1089
|
+
id: "mod-face",
|
|
1090
|
+
title: "16. Módulo: Facial",
|
|
1091
|
+
category: "modules",
|
|
1092
|
+
icon: TbFaceId,
|
|
1093
|
+
component: <FaceEnrollModule />,
|
|
1094
|
+
keywords: ["face", "facial", "rostro", "camera", "module"],
|
|
1095
|
+
},
|
|
1096
|
+
];
|
|
1097
|
+
|
|
1098
|
+
const filteredSections = sections.filter(s => {
|
|
1099
|
+
if (!search) return true;
|
|
1100
|
+
const query = search.toLowerCase();
|
|
1101
|
+
return (
|
|
1102
|
+
s.title.toLowerCase().includes(query) ||
|
|
1103
|
+
s.keywords.some(k => k.includes(query))
|
|
1104
|
+
);
|
|
1105
|
+
});
|
|
1106
|
+
|
|
1107
|
+
const categories: { id: SectionCategory; label: string; icon: any }[] = [
|
|
1108
|
+
{ id: "basics", label: "Basics & Layout", icon: <TbPalette /> },
|
|
1109
|
+
{ id: "data", label: "Data & API", icon: <TbDatabase /> },
|
|
1110
|
+
{ id: "advanced", label: "Advanced Customization", icon: <TbCode /> },
|
|
1111
|
+
{ id: "modules", label: "Pre-built Modules", icon: <TbPackage /> },
|
|
1112
|
+
];
|
|
1113
|
+
|
|
1114
|
+
const toggleSection = (id: string) => {
|
|
1115
|
+
setOpenSections(prev => ({ ...prev, [id]: !prev[id] }));
|
|
1116
|
+
};
|
|
1117
|
+
|
|
1118
|
+
if (!mounted) return null;
|
|
1119
|
+
|
|
1120
|
+
return (
|
|
1121
|
+
<div className="min-h-screen bg-background p-4 md:p-8 font-sans transition-colors duration-500">
|
|
1122
|
+
<div className="max-w-5xl mx-auto space-y-10">
|
|
1123
|
+
|
|
1124
|
+
{/* Header */}
|
|
1125
|
+
<div className="relative pt-12 text-center space-y-4">
|
|
1126
|
+
<div className="inline-flex items-center gap-2 px-4 py-1.5 bg-primary/10 rounded-full text-primary text-xs font-bold uppercase tracking-wider animate-in fade-in duration-700">
|
|
1127
|
+
<TbPackage size={14} />
|
|
1128
|
+
@intell/biometric-sdk v1.0.0
|
|
1129
|
+
</div>
|
|
1130
|
+
<h1 className="text-4xl md:text-5xl font-black text-foreground tracking-tight animate-in slide-in-from-top-4 duration-500">
|
|
1131
|
+
SDK <span className="text-primary italic">Documentation</span>
|
|
1132
|
+
</h1>
|
|
1133
|
+
<p className="text-foreground/60 max-w-2xl mx-auto text-lg leading-relaxed">
|
|
1134
|
+
Explora todas las capacidades, desde componentes pre-hechos hasta personalización total desde cero.
|
|
1135
|
+
</p>
|
|
1136
|
+
</div>
|
|
1137
|
+
|
|
1138
|
+
{/* Search Bar */}
|
|
1139
|
+
<div className="sticky top-6 z-40">
|
|
1140
|
+
<div className="relative group">
|
|
1141
|
+
<div className="absolute inset-0 bg-primary/20 rounded-2xl blur-xl opacity-0 group-focus-within:opacity-100 transition-opacity" />
|
|
1142
|
+
<div className="relative flex items-center bg-card border border-border/50 rounded-2xl p-2 shadow-2xl backdrop-blur-3xl">
|
|
1143
|
+
<div className="flex-1 flex items-center px-4 gap-4">
|
|
1144
|
+
<TbSearch className="text-foreground/40" size={20} />
|
|
1145
|
+
<input
|
|
1146
|
+
type="text"
|
|
1147
|
+
placeholder="Busca por título, componente o funcionalidad... (ej. 'button', 'finger', 'api')"
|
|
1148
|
+
className="flex-1 bg-transparent border-none outline-none text-foreground placeholder:text-foreground/30 py-2 text-md"
|
|
1149
|
+
value={search}
|
|
1150
|
+
onChange={(e) => setSearch(e.target.value)}
|
|
1151
|
+
/>
|
|
1152
|
+
{search && (
|
|
1153
|
+
<button
|
|
1154
|
+
onClick={() => setSearch("")}
|
|
1155
|
+
className="p-1.5 hover:bg-secondary rounded-full transition-colors text-foreground/40 hover:text-foreground"
|
|
1156
|
+
>
|
|
1157
|
+
<TbX size={16} />
|
|
1158
|
+
</button>
|
|
1159
|
+
)}
|
|
1160
|
+
</div>
|
|
1161
|
+
<div className="hidden md:flex px-4 py-2 border-l border-border/50 text-[10px] items-center gap-3 text-foreground/30 font-bold uppercase tracking-widest">
|
|
1162
|
+
<span>{filteredSections.length} Resultados</span>
|
|
1163
|
+
</div>
|
|
1164
|
+
</div>
|
|
1165
|
+
</div>
|
|
1166
|
+
</div>
|
|
1167
|
+
|
|
1168
|
+
{/* Categories / Grid */}
|
|
1169
|
+
<div className="space-y-12 pb-24">
|
|
1170
|
+
{categories.map((cat) => {
|
|
1171
|
+
const catSections = filteredSections.filter(s => s.category === cat.id);
|
|
1172
|
+
if (catSections.length === 0) return null;
|
|
1173
|
+
|
|
1174
|
+
return (
|
|
1175
|
+
<div key={cat.id} className="space-y-6">
|
|
1176
|
+
<div className="flex items-center gap-3 pl-2">
|
|
1177
|
+
<div className="text-primary">{cat.icon}</div>
|
|
1178
|
+
<h2 className="text-sm font-black uppercase tracking-[0.2em] text-foreground/40">
|
|
1179
|
+
{cat.label}
|
|
1180
|
+
</h2>
|
|
1181
|
+
<div className="flex-1 h-px bg-gradient-to-r from-border/50 to-transparent ml-4" />
|
|
1182
|
+
</div>
|
|
1183
|
+
|
|
1184
|
+
<div className="grid grid-cols-1 gap-6">
|
|
1185
|
+
{catSections.map((s) => (
|
|
1186
|
+
<Section
|
|
1187
|
+
key={s.id}
|
|
1188
|
+
title={s.title}
|
|
1189
|
+
icon={s.icon}
|
|
1190
|
+
isOpen={!!openSections[s.id] || !!search}
|
|
1191
|
+
onToggle={() => toggleSection(s.id)}
|
|
1192
|
+
>
|
|
1193
|
+
{s.component}
|
|
1194
|
+
</Section>
|
|
1195
|
+
))}
|
|
1196
|
+
</div>
|
|
1197
|
+
</div>
|
|
1198
|
+
);
|
|
1199
|
+
})}
|
|
1200
|
+
|
|
1201
|
+
{filteredSections.length === 0 && (
|
|
1202
|
+
<div className="py-20 text-center space-y-4 animate-in fade-in zoom-in duration-300">
|
|
1203
|
+
<div className="w-20 h-20 bg-secondary/50 rounded-full flex items-center justify-center mx-auto text-foreground/20">
|
|
1204
|
+
<TbSearch size={40} />
|
|
1205
|
+
</div>
|
|
1206
|
+
<div>
|
|
1207
|
+
<h3 className="text-xl font-bold">No se encontraron resultados</h3>
|
|
1208
|
+
<p className="text-foreground/40 text-sm">Intenta con otros términos de búsqueda.</p>
|
|
1209
|
+
</div>
|
|
1210
|
+
<Button variant="ghost" onClick={() => setSearch("")}>Limpiar Búsqueda</Button>
|
|
1211
|
+
</div>
|
|
1212
|
+
)}
|
|
1213
|
+
</div>
|
|
1214
|
+
</div>
|
|
1215
|
+
|
|
1216
|
+
{/* Scroll to top fab can be added here if needed */}
|
|
1217
|
+
</div>
|
|
1218
|
+
);
|
|
1219
|
+
}
|