@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.
@@ -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 • &lt;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
+ }