@axzydev/axzy_ui_system 1.3.1 → 1.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/README.md +6 -2
  2. package/dist/index.cjs +930 -544
  3. package/dist/index.cjs.map +1 -1
  4. package/dist/index.css +1 -1
  5. package/dist/index.d.cts +187 -9
  6. package/dist/index.d.ts +187 -9
  7. package/dist/index.js +881 -495
  8. package/dist/index.js.map +1 -1
  9. package/package.json +1 -1
  10. package/src/App.tsx +150 -154
  11. package/src/components/molecules/table/table.doc.mdx +64 -21
  12. package/src/components/molecules/table/table.props.ts +81 -2
  13. package/src/components/molecules/table/table.stories.tsx +155 -0
  14. package/src/components/molecules/table/table.tsx +177 -36
  15. package/src/components/organisms/data-table/ITDataTable.doc.mdx +37 -3
  16. package/src/components/organisms/data-table/ITDataTable.stories.tsx +43 -0
  17. package/src/components/organisms/data-table/dataTable.props.ts +64 -0
  18. package/src/components/organisms/data-table/dataTable.tsx +240 -82
  19. package/src/components/organisms/sidebar/sidebar.doc.mdx +5 -0
  20. package/src/components/organisms/sidebar/sidebar.props.ts +5 -0
  21. package/src/components/organisms/sidebar/sidebar.stories.tsx +1 -1
  22. package/src/components/organisms/sidebar/sidebar.tsx +27 -7
  23. package/src/components/organisms/topbar/topbar.doc.mdx +3 -0
  24. package/src/components/organisms/topbar/topbar.props.ts +16 -0
  25. package/src/components/organisms/topbar/topbar.tsx +133 -98
  26. package/src/components/templates/layout/layout.doc.mdx +2 -0
  27. package/src/components/templates/layout/layout.tsx +7 -1
  28. package/src/components/templates/page/page.doc.mdx +8 -2
  29. package/src/components/templates/page/page.props.ts +11 -1
  30. package/src/components/templates/page/page.tsx +11 -1
  31. package/src/dev.css +58 -1
  32. package/src/hooks/useElementSize.ts +46 -0
  33. package/src/hooks/useVirtualRows.ts +123 -0
  34. package/src/index.ts +2 -0
  35. package/src/sandbox/LandingShowcase.tsx +465 -0
  36. package/src/sandbox/NotFoundShowcase.tsx +70 -0
  37. package/src/sandbox/TopbarSearch.tsx +85 -0
  38. package/src/sandbox/navigation.ts +148 -0
  39. package/src/sandbox/useHashRoute.ts +149 -0
  40. package/src/showcases/DataShowcases.tsx +321 -24
  41. package/src/showcases/PageShowcases.tsx +31 -0
  42. package/src/showcases/StructureShowcases.tsx +42 -4
  43. package/src/types/table.types.ts +48 -0
  44. package/src/utils/styles.ts +9 -4
  45. package/src/utils/table.utils.ts +31 -0
@@ -0,0 +1,465 @@
1
+ import { useEffect, useRef, useState, type ReactNode } from "react";
2
+ import {
3
+ FaCheck,
4
+ FaCopy,
5
+ FaCreditCard,
6
+ FaGithub,
7
+ FaHome,
8
+ FaKeyboard,
9
+ FaLayerGroup,
10
+ FaNpm,
11
+ FaRegBell,
12
+ FaRocket,
13
+ FaSlidersH,
14
+ FaTable,
15
+ } from "react-icons/fa";
16
+ import ITBadget from "@/components/atoms/badget/badget";
17
+ import ITButton from "@/components/atoms/button/button";
18
+ import ITFlex from "@/components/atoms/flex/flex";
19
+ import ITGrid from "@/components/atoms/grid/grid";
20
+ import ITStack from "@/components/atoms/stack/stack";
21
+ import ITText from "@/components/atoms/text/text";
22
+ import {
23
+ SANDBOX_GROUPS,
24
+ TOTAL_ITEMS,
25
+ } from "./navigation";
26
+
27
+ /** Props for the sandbox landing page. */
28
+ export interface LandingShowcaseProps {
29
+ /** Open a component page by group id + item id. */
30
+ onOpenItem: (group: string, item: string) => void;
31
+ /** Navigate back to the portfolio. */
32
+ onOpenPortfolio: () => void;
33
+ }
34
+
35
+ /** Icon per sandbox group id. */
36
+ const GROUP_ICONS: Record<string, ReactNode> = {
37
+ general: <FaHome />,
38
+ struc: <FaCreditCard />,
39
+ forms: <FaKeyboard />,
40
+ data: <FaTable />,
41
+ nav: <FaSlidersH />,
42
+ feed: <FaRegBell />,
43
+ };
44
+
45
+ /**
46
+ * Literal per-index gradient classes for the family cards. Tailwind v4 cannot
47
+ * see runtime-built class names, so every gradient is written out in full.
48
+ */
49
+ const GROUP_ACCENTS = [
50
+ "from-primary-500 to-info-500",
51
+ "from-purple-500 to-primary-500",
52
+ "from-primary-600 to-purple-600",
53
+ "from-info-500 to-purple-500",
54
+ "from-purple-600 to-info-600",
55
+ "from-primary-500 to-purple-500",
56
+ ];
57
+
58
+ const GITHUB_URL = "https://github.com/axzydev/axzy_ui_system";
59
+ const INSTALL_CMD = "pnpm add @axzydev/axzy_ui_system";
60
+
61
+ /**
62
+ * Copy text to the clipboard, falling back to a hidden textarea +
63
+ * `execCommand` when the async Clipboard API is unavailable (non-secure
64
+ * contexts, embedded webviews). Returns whether the copy succeeded.
65
+ */
66
+ const copyText = async (text: string): Promise<boolean> => {
67
+ try {
68
+ if (navigator.clipboard?.writeText) {
69
+ await navigator.clipboard.writeText(text);
70
+ return true;
71
+ }
72
+ } catch {
73
+ /* fall through to the legacy path */
74
+ }
75
+
76
+ let textarea: HTMLTextAreaElement | null = null;
77
+ try {
78
+ textarea = document.createElement("textarea");
79
+ textarea.value = text;
80
+ textarea.setAttribute("readonly", "");
81
+ textarea.style.position = "fixed";
82
+ textarea.style.opacity = "0";
83
+ document.body.appendChild(textarea);
84
+ textarea.select();
85
+ return document.execCommand("copy");
86
+ } catch {
87
+ return false;
88
+ } finally {
89
+ textarea?.remove();
90
+ }
91
+ };
92
+
93
+ const CopyButton = ({ text }: { text: string }) => {
94
+ const [copied, setCopied] = useState(false);
95
+ const timeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
96
+
97
+ useEffect(
98
+ () => () => {
99
+ if (timeoutRef.current) clearTimeout(timeoutRef.current);
100
+ },
101
+ [],
102
+ );
103
+
104
+ const handleCopy = async () => {
105
+ const ok = await copyText(text);
106
+ if (!ok) return;
107
+ setCopied(true);
108
+ if (timeoutRef.current) clearTimeout(timeoutRef.current);
109
+ timeoutRef.current = setTimeout(() => setCopied(false), 2000);
110
+ };
111
+
112
+ return (
113
+ <ITButton variant="text" color="gray" size="sm" onClick={handleCopy}>
114
+ <ITFlex align="center" gap={1.5}>
115
+ {copied ? (
116
+ <FaCheck size={10} className="text-emerald-400" />
117
+ ) : (
118
+ <FaCopy size={10} />
119
+ )}
120
+ {copied ? "Copiado" : "Copiar"}
121
+ </ITFlex>
122
+ </ITButton>
123
+ );
124
+ };
125
+
126
+ const CodeBlock = ({ code, filename }: { code: string; filename?: string }) => (
127
+ <div className="overflow-hidden rounded-xl border border-slate-200 dark:border-slate-700 bg-white dark:bg-slate-900">
128
+ {filename && (
129
+ <div className="px-4 py-2 border-b border-slate-200 dark:border-slate-700 bg-slate-50 dark:bg-slate-800/80">
130
+ <ITText as="span" className="!text-xs !font-mono !text-slate-400">
131
+ {filename}
132
+ </ITText>
133
+ </div>
134
+ )}
135
+ <div className="relative bg-slate-950">
136
+ <pre className="p-4 text-xs font-mono overflow-x-auto leading-relaxed text-slate-200">
137
+ <code>{code}</code>
138
+ </pre>
139
+ </div>
140
+ </div>
141
+ );
142
+
143
+ const STATS = [
144
+ { label: "Componentes", value: `${TOTAL_ITEMS}`, hint: "páginas documentadas" },
145
+ { label: "Familias", value: `${SANDBOX_GROUPS.length}`, hint: "grupos de navegación" },
146
+ { label: "TypeScript", value: "100%", hint: "tipado estricto" },
147
+ { label: "UI deps", value: "0", hint: "sin runtime extra" },
148
+ ];
149
+
150
+ const FEATURES = [
151
+ "Tailwind CSS v4 con tokens de tema en runtime",
152
+ "Accesibilidad: roles, foco y nombres accesibles",
153
+ "Dark mode automático vía ITThemeProvider",
154
+ "Tree-shakeable: solo importas lo que usas",
155
+ "Sin dependencias de UI de terceros",
156
+ "Documentado con ejemplos ejecutables",
157
+ ];
158
+
159
+ const QUICKSTART_CODE = `import { ITThemeProvider, ITButton } from "@axzydev/axzy_ui_system"
160
+ import "@axzydev/axzy_ui_system/dist/index.css"
161
+
162
+ export default function App() {
163
+ return (
164
+ <ITThemeProvider>
165
+ <ITButton variant="rounded">Empezar</ITButton>
166
+ </ITThemeProvider>
167
+ )
168
+ }`;
169
+
170
+ /**
171
+ * Sandbox landing page (`#ui-system`): a dashboard-style entry point with a
172
+ * hero, real derived stats, deep-linking family cards, quickstart, and links.
173
+ */
174
+ export function LandingShowcase({
175
+ onOpenItem,
176
+ onOpenPortfolio,
177
+ }: LandingShowcaseProps) {
178
+ return (
179
+ <ITStack spacing={10} className="animate-fadeIn">
180
+ {/* ─── HERO ─── */}
181
+ <section className="relative overflow-hidden rounded-3xl bg-gradient-to-br from-primary-50 via-white to-purple-50 dark:from-primary-950/30 dark:via-slate-900 dark:to-purple-950/30 px-8 py-14 sm:px-14">
182
+ <div
183
+ aria-hidden
184
+ className="it-landing-float pointer-events-none absolute -top-24 -right-16 h-72 w-72 rounded-full bg-primary-400/20 blur-3xl"
185
+ />
186
+ <div
187
+ aria-hidden
188
+ className="it-landing-float pointer-events-none absolute -bottom-24 -left-16 h-72 w-72 rounded-full bg-purple-400/20 blur-3xl"
189
+ style={{ animationDelay: "1.5s" }}
190
+ />
191
+ <div
192
+ aria-hidden
193
+ className="it-landing-grid pointer-events-none absolute inset-0 text-slate-400/50 dark:text-slate-600/40"
194
+ />
195
+
196
+ <div className="relative z-10 max-w-3xl">
197
+ <div
198
+ className="it-landing-rise"
199
+ style={{ animationDelay: "0ms" }}
200
+ >
201
+ <ITBadget
202
+ label="v1.3.0 · Tailwind v4"
203
+ color="primary"
204
+ variant="outlined"
205
+ className="w-fit"
206
+ />
207
+ </div>
208
+
209
+ <ITText
210
+ as="h1"
211
+ className="it-landing-rise !mt-5 !text-4xl sm:!text-5xl !font-extrabold !tracking-tight text-slate-900 dark:text-white"
212
+ style={{ animationDelay: "70ms" }}
213
+ >
214
+ Componentes para{" "}
215
+ <span className="bg-gradient-to-r from-primary-600 via-primary-500 to-purple-500 bg-clip-text text-transparent">
216
+ interfaces serias
217
+ </span>
218
+ </ITText>
219
+
220
+ <ITText
221
+ as="p"
222
+ className="it-landing-rise !mt-4 !text-base !leading-relaxed max-w-xl text-slate-600 dark:text-slate-300"
223
+ style={{ animationDelay: "140ms" }}
224
+ >
225
+ Una librería React + TypeScript con Tailwind CSS v4: accesible,
226
+ temable y lista para producción. Explora {TOTAL_ITEMS} páginas en{" "}
227
+ {SANDBOX_GROUPS.length} familias.
228
+ </ITText>
229
+
230
+ <ITFlex
231
+ gap={3}
232
+ wrap="wrap"
233
+ align="center"
234
+ className="it-landing-rise !mt-7"
235
+ style={{ animationDelay: "210ms" }}
236
+ >
237
+ <ITButton
238
+ variant="filled"
239
+ color="primary"
240
+ size="lg"
241
+ icon={<FaRocket size={13} />}
242
+ onClick={() => onOpenItem("general", "getting-started")}
243
+ >
244
+ Explorar componentes
245
+ </ITButton>
246
+ <ITButton
247
+ variant="outlined"
248
+ color="secondary"
249
+ size="lg"
250
+ icon={<FaLayerGroup size={13} />}
251
+ onClick={onOpenPortfolio}
252
+ >
253
+ Ver portafolio
254
+ </ITButton>
255
+ </ITFlex>
256
+
257
+ <div
258
+ className="it-landing-rise !mt-7 inline-flex items-center gap-2 rounded-xl border border-primary-200 dark:border-primary-800/50 bg-gradient-to-r from-primary-50 to-purple-50 dark:from-primary-950/30 dark:to-purple-950/30 px-4 py-2 shadow-sm"
259
+ style={{ animationDelay: "280ms" }}
260
+ >
261
+ <FaNpm size={16} className="text-primary-500" />
262
+ <code className="text-sm font-mono text-slate-800 dark:text-slate-200">
263
+ {INSTALL_CMD}
264
+ </code>
265
+ <CopyButton text={INSTALL_CMD} />
266
+ </div>
267
+ </div>
268
+ </section>
269
+
270
+ {/* ─── STAT STRIP ─── */}
271
+ <ITGrid container spacing={4}>
272
+ {STATS.map((stat) => (
273
+ <ITGrid item xs={6} lg={3} key={stat.label}>
274
+ <div className="h-full rounded-2xl border border-slate-200 dark:border-slate-800 bg-white dark:bg-slate-900 p-5 shadow-sm">
275
+ <ITText
276
+ as="span"
277
+ className="block !text-3xl !font-extrabold !tracking-tight text-slate-900 dark:text-white"
278
+ >
279
+ {stat.value}
280
+ </ITText>
281
+ <ITText
282
+ as="span"
283
+ className="mt-1 block !text-sm !font-semibold text-primary-600 dark:text-primary-400"
284
+ >
285
+ {stat.label}
286
+ </ITText>
287
+ <ITText
288
+ as="span"
289
+ className="block !text-[11px] text-slate-400 dark:text-slate-500"
290
+ >
291
+ {stat.hint}
292
+ </ITText>
293
+ </div>
294
+ </ITGrid>
295
+ ))}
296
+ </ITGrid>
297
+
298
+ {/* ─── EXPLORE BY FAMILY ─── */}
299
+ <section>
300
+ <ITStack spacing={1} className="!mb-5">
301
+ <ITText
302
+ as="h2"
303
+ className="!text-xl !font-bold !tracking-tight text-slate-900 dark:text-white"
304
+ >
305
+ Explora por familia
306
+ </ITText>
307
+ <ITText as="p" className="!text-sm text-slate-500 dark:text-slate-400">
308
+ Cada tarjeta te lleva directo al primer componente del grupo.
309
+ </ITText>
310
+ </ITStack>
311
+
312
+ <ITGrid container spacing={5}>
313
+ {SANDBOX_GROUPS.map((group, index) => (
314
+ <ITGrid item xs={12} sm={6} lg={4} key={group.id}>
315
+ <button
316
+ type="button"
317
+ onClick={() => onOpenItem(group.id, group.subitems[0].id)}
318
+ className="group flex h-full w-full flex-col gap-4 rounded-2xl border border-slate-200 dark:border-slate-800 bg-white dark:bg-slate-900 p-5 text-left transition-all duration-300 hover:-translate-y-1 hover:border-primary-300 dark:hover:border-primary-700 hover:shadow-lg"
319
+ >
320
+ <div className="flex items-center gap-3">
321
+ <span
322
+ className={`flex h-10 w-10 flex-shrink-0 items-center justify-center rounded-xl bg-gradient-to-br ${
323
+ GROUP_ACCENTS[index % GROUP_ACCENTS.length]
324
+ } text-white shadow-sm`}
325
+ >
326
+ {GROUP_ICONS[group.id]}
327
+ </span>
328
+ <span className="min-w-0 flex-1">
329
+ <ITText
330
+ as="span"
331
+ className="block truncate !text-sm !font-semibold text-slate-900 dark:text-white"
332
+ >
333
+ {group.label}
334
+ </ITText>
335
+ <ITText
336
+ as="span"
337
+ className="block !text-[11px] text-slate-400 dark:text-slate-500"
338
+ >
339
+ {group.subitems.length} componentes
340
+ </ITText>
341
+ </span>
342
+ <ITBadget
343
+ label={`${group.subitems.length}`}
344
+ color="secondary"
345
+ variant="outlined"
346
+ size="sm"
347
+ />
348
+ </div>
349
+
350
+ <div className="flex flex-wrap gap-1.5">
351
+ {group.subitems.slice(0, 4).map((sub) => (
352
+ <span
353
+ key={sub.id}
354
+ className="rounded-md bg-slate-100 dark:bg-slate-800 px-2 py-0.5 text-[10px] font-medium text-slate-500 dark:text-slate-400"
355
+ >
356
+ {sub.label}
357
+ </span>
358
+ ))}
359
+ {group.subitems.length > 4 && (
360
+ <span className="rounded-md bg-primary-50 dark:bg-primary-950/40 px-2 py-0.5 text-[10px] font-semibold text-primary-600 dark:text-primary-400">
361
+ +{group.subitems.length - 4} más
362
+ </span>
363
+ )}
364
+ </div>
365
+ </button>
366
+ </ITGrid>
367
+ ))}
368
+ </ITGrid>
369
+ </section>
370
+
371
+ {/* ─── QUICKSTART ─── */}
372
+ <ITGrid container spacing={6}>
373
+ <ITGrid item xs={12} lg={7}>
374
+ <ITStack spacing={3}>
375
+ <ITFlex align="center" justify="between" gap={3}>
376
+ <ITText
377
+ as="h2"
378
+ className="!text-lg !font-bold !tracking-tight text-slate-900 dark:text-white"
379
+ >
380
+ Quickstart
381
+ </ITText>
382
+ <CopyButton text={QUICKSTART_CODE} />
383
+ </ITFlex>
384
+ <CodeBlock filename="src/App.tsx" code={QUICKSTART_CODE} />
385
+ </ITStack>
386
+ </ITGrid>
387
+
388
+ <ITGrid item xs={12} lg={5}>
389
+ <div className="h-full rounded-2xl border border-slate-200 dark:border-slate-800 bg-white dark:bg-slate-900 p-6 shadow-sm">
390
+ <ITText
391
+ as="h3"
392
+ className="!mb-4 !text-sm !font-bold uppercase !tracking-wider text-slate-400 dark:text-slate-500"
393
+ >
394
+ Incluido de fábrica
395
+ </ITText>
396
+ <ul className="space-y-3">
397
+ {FEATURES.map((feature) => (
398
+ <li key={feature} className="flex items-start gap-3">
399
+ <span className="mt-0.5 flex h-5 w-5 flex-shrink-0 items-center justify-center rounded-full bg-success-100 text-success-600 dark:bg-success-950/40 dark:text-success-400">
400
+ <FaCheck size={10} />
401
+ </span>
402
+ <span className="text-sm text-slate-600 dark:text-slate-300">
403
+ {feature}
404
+ </span>
405
+ </li>
406
+ ))}
407
+ </ul>
408
+ </div>
409
+ </ITGrid>
410
+ </ITGrid>
411
+
412
+ {/* ─── LINKS ─── */}
413
+ <div className="rounded-2xl border border-slate-200 dark:border-slate-800 bg-white dark:bg-slate-900 p-6 shadow-sm">
414
+ <ITFlex wrap="wrap" gap={4} justify="between" align="center">
415
+ <ITStack spacing={1}>
416
+ <ITText
417
+ as="h3"
418
+ className="!text-base !font-bold !tracking-tight text-slate-900 dark:text-white"
419
+ >
420
+ ¿Listo para construir?
421
+ </ITText>
422
+ <ITText
423
+ as="p"
424
+ className="!text-sm text-slate-500 dark:text-slate-400"
425
+ >
426
+ Revisa el portafolio, el código fuente o levanta Storybook.
427
+ </ITText>
428
+ </ITStack>
429
+
430
+ <ITFlex gap={3} wrap="wrap" align="center">
431
+ <ITButton
432
+ variant="filled"
433
+ color="primary"
434
+ icon={<FaLayerGroup size={13} />}
435
+ onClick={onOpenPortfolio}
436
+ >
437
+ Portafolio
438
+ </ITButton>
439
+ <ITButton
440
+ variant="outlined"
441
+ color="secondary"
442
+ icon={<FaGithub size={13} />}
443
+ onClick={() =>
444
+ window.open(GITHUB_URL, "_blank", "noopener,noreferrer")
445
+ }
446
+ >
447
+ GitHub
448
+ </ITButton>
449
+ </ITFlex>
450
+ </ITFlex>
451
+
452
+ <ITText
453
+ as="p"
454
+ className="!mt-5 !text-xs text-slate-400 dark:text-slate-500"
455
+ >
456
+ Ejecuta{" "}
457
+ <code className="rounded bg-slate-100 dark:bg-slate-800 px-1.5 py-0.5 font-mono text-slate-600 dark:text-slate-300">
458
+ pnpm storybook
459
+ </code>{" "}
460
+ para ver cada componente aislado.
461
+ </ITText>
462
+ </div>
463
+ </ITStack>
464
+ );
465
+ }
@@ -0,0 +1,70 @@
1
+ import { FaHome, FaLayerGroup } from "react-icons/fa";
2
+ import ITButton from "@/components/atoms/button/button";
3
+ import ITFlex from "@/components/atoms/flex/flex";
4
+ import ITStack from "@/components/atoms/stack/stack";
5
+ import ITText from "@/components/atoms/text/text";
6
+
7
+ /** Props for the sandbox 404 page. */
8
+ export interface NotFoundShowcaseProps {
9
+ /** Raw hash that failed to resolve (shown inside a `<code>` block). */
10
+ attempted: string;
11
+ /** Navigate back to the `#ui-system` landing. */
12
+ onGoLanding: () => void;
13
+ /** Navigate back to the portfolio. */
14
+ onGoPortfolio: () => void;
15
+ }
16
+
17
+ /**
18
+ * Sandbox 404 page rendered when a hash route does not resolve to a page.
19
+ *
20
+ * Shows the attempted path, a friendly message, and buttons to recover.
21
+ */
22
+ export default function NotFoundShowcase({
23
+ attempted,
24
+ onGoLanding,
25
+ onGoPortfolio,
26
+ }: NotFoundShowcaseProps) {
27
+ return (
28
+ <div className="animate-fadeIn flex min-h-[60vh] items-center justify-center px-4">
29
+ <ITStack spacing={5} className="w-full max-w-xl text-center items-center">
30
+ <ITText
31
+ as="div"
32
+ className="!text-7xl !font-extrabold !tracking-tight bg-gradient-to-br from-primary-500 via-primary-600 to-purple-600 bg-clip-text text-transparent"
33
+ >
34
+ 404
35
+ </ITText>
36
+
37
+ <ITStack spacing={2} className="items-center">
38
+ <ITText as="h1" className="!text-2xl !font-bold text-slate-900 dark:text-white">
39
+ Ruta no encontrada
40
+ </ITText>
41
+ <ITText as="p" className="!text-sm text-slate-500 dark:text-slate-400">
42
+ No existe una página para este hash. Revisa el enlace o vuelve al inicio.
43
+ </ITText>
44
+ <code className="mt-1 max-w-full truncate rounded-lg border border-slate-200 bg-slate-50 px-3 py-1.5 font-mono text-xs text-slate-600 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-300">
45
+ {attempted || "#"}
46
+ </code>
47
+ </ITStack>
48
+
49
+ <ITFlex gap={3} wrap="wrap" justify="center" align="center">
50
+ <ITButton
51
+ variant="filled"
52
+ color="primary"
53
+ icon={<FaHome size={13} />}
54
+ onClick={onGoLanding}
55
+ >
56
+ Volver al inicio
57
+ </ITButton>
58
+ <ITButton
59
+ variant="outlined"
60
+ color="secondary"
61
+ icon={<FaLayerGroup size={13} />}
62
+ onClick={onGoPortfolio}
63
+ >
64
+ Ver portafolio
65
+ </ITButton>
66
+ </ITFlex>
67
+ </ITStack>
68
+ </div>
69
+ );
70
+ }
@@ -0,0 +1,85 @@
1
+ import type { ChangeEvent } from "react";
2
+ import { FaSearch, FaTimes } from "react-icons/fa";
3
+ import ITButton from "@/components/atoms/button/button";
4
+ import ITInput from "@/components/atoms/input/input";
5
+
6
+ /** Props for the sandbox topbar search filter. */
7
+ export interface TopbarSearchProps {
8
+ /** Current (live) search term shown in the input. */
9
+ value: string;
10
+ /** Called with the new term on every keystroke. */
11
+ onChange: (value: string) => void;
12
+ /** Called when the user clears the search. */
13
+ onClear: () => void;
14
+ /** Number of matching navigation entries for the current term. */
15
+ resultCount: number;
16
+ /**
17
+ * When true, the debounced filter has not caught up with the live term yet.
18
+ * The result count is hidden so it never announces a stale number.
19
+ */
20
+ pending?: boolean;
21
+ /** Additional CSS classes on the form wrapper. Defaults to `"w-full"`. */
22
+ className?: string;
23
+ }
24
+
25
+ /**
26
+ * Topbar search box that filters every sandbox component live.
27
+ *
28
+ * Renders a search input with a clear button and an accessible live-region
29
+ * announcement of the result count. It lives in the topbar center slot
30
+ * (`topBar.centerContent`), so it stays visible while the sidebar collapses.
31
+ */
32
+ export default function TopbarSearch({
33
+ value,
34
+ onChange,
35
+ onClear,
36
+ resultCount,
37
+ pending = false,
38
+ className = "w-full",
39
+ }: TopbarSearchProps) {
40
+ const isActive = value.trim().length > 0;
41
+ const showCount = isActive && !pending;
42
+
43
+ return (
44
+ <form
45
+ role="search"
46
+ onSubmit={(event) => event.preventDefault()}
47
+ className={className}
48
+ >
49
+ <div className="flex items-center gap-1.5">
50
+ <div className="flex-1 min-w-0">
51
+ <ITInput
52
+ name="sandbox-topbar-search"
53
+ label="Buscar componentes"
54
+ labelClassName="sr-only"
55
+ placeholder="Buscar componente…"
56
+ value={value}
57
+ onChange={(event: ChangeEvent<HTMLInputElement>) =>
58
+ onChange(event.target.value)
59
+ }
60
+ iconLeft={<FaSearch size={12} />}
61
+ size="sm"
62
+ containerClassName="w-full"
63
+ />
64
+ </div>
65
+
66
+ {isActive && (
67
+ <ITButton
68
+ variant="text"
69
+ color="danger"
70
+ size="sm"
71
+ ariaLabel="Limpiar búsqueda"
72
+ onClick={onClear}
73
+ className="flex-shrink-0"
74
+ >
75
+ <FaTimes size={11} />
76
+ </ITButton>
77
+ )}
78
+ </div>
79
+
80
+ <span role="status" aria-live="polite" className="sr-only">
81
+ {showCount ? `${resultCount} resultados` : ""}
82
+ </span>
83
+ </form>
84
+ );
85
+ }