flysoft-react-ui 1.3.2 → 1.4.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/AI_CONTEXT.md CHANGED
@@ -1,1574 +1,1575 @@
1
- # Flysoft React UI - AI Context & Documentation
2
-
3
- This document serves as the source of truth for AI models (Gemini, Claude, GPT, etc.) when generating code that consumes the `flysoft-react-ui` library.
4
-
5
- ## Library Philosophy
6
-
7
- `flysoft-react-ui` is a React component library built with TypeScript. It emphasizes a consistent look and feel, ease of use, and "premium" aesthetics out of the box. All components use CSS variables for theming and FontAwesome 5 (light/outlined style) for icons.
8
-
9
- ## Critical Rules for AI
10
-
11
- 1. **Top-Level Imports Only**: Always import from `'flysoft-react-ui'`.
12
- - CORRECT: `import { Button, Card } from 'flysoft-react-ui';`
13
- - INCORRECT: `import { Button } from 'flysoft-react-ui/components/Button';`
14
- 2. **TypeScript First**: Use the exported types (e.g., `ButtonProps`, `DataTableColumn<T>`) to ensure type safety.
15
- 3. **Style Import at App Root Only**: Add `import 'flysoft-react-ui/styles';` once at the app root. Never import CSS in individual components.
16
- 4. **Do Not Use Docs Internals**: Never import or reference anything from `docs/*` or `src/docs/*`.
17
- 5. **FontAwesome 5 Only**: Use `fa-*` icon classes. Components normalize to light style (`fal`) automatically. Never use other icon libraries.
18
- 6. **Theme CSS Variables**: Use `var(--color-*)`, `var(--shadow-*)`, `var(--radius-*)`, `var(--font-*)` for custom styling. Never hardcode colors.
19
-
20
- ---
21
-
22
- ## Form Controls
23
-
24
- ### Button
25
-
26
- Customizable button with variants, colors, icons, and ripple effect.
27
-
28
- ```typescript
29
- interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
30
- variant?: "primary" | "outline" | "ghost"; // default: "primary"
31
- size?: "sm" | "md" | "lg"; // default: "md"
32
- color?: "primary" | "secondary" | "success" | "warning" | "danger" | "info"; // default: "primary"
33
- bg?: string; // Custom background color (hex, rgb, rgba, hsl, or color name)
34
- textColor?: string; // Custom text color
35
- icon?: string; // FontAwesome icon class (e.g. "fa-save")
36
- iconPosition?: "left" | "right"; // default: "left"
37
- loading?: boolean; // Shows spinner, disables button. default: false
38
- children?: React.ReactNode;
39
- }
40
- ```
41
-
42
- ```tsx
43
- <Button variant="primary" icon="fa-save" loading={isLoading} onClick={handleSave}>
44
- Guardar
45
- </Button>
46
- <Button variant="outline" color="danger" icon="fa-trash">Eliminar</Button>
47
- <Button variant="ghost" size="sm">Cancelar</Button>
48
- <Button bg="#8b5cf6" textColor="#fff">Custom Color</Button>
49
- ```
50
-
51
- ### LinkButton
52
-
53
- Anchor-styled button that uses React Router `<Link>` for internal routes and `<a>` for external URLs.
54
-
55
- ```typescript
56
- interface LinkButtonProps extends Omit<React.AnchorHTMLAttributes<HTMLAnchorElement>, "href"> {
57
- to: string; // Route or URL (required)
58
- target?: string;
59
- variant?: "primary" | "outline" | "ghost"; // default: "primary"
60
- size?: "sm" | "md" | "lg"; // default: "md"
61
- color?: "primary" | "secondary" | "success" | "warning" | "danger" | "info";
62
- bg?: string;
63
- textColor?: string;
64
- icon?: string;
65
- iconPosition?: "left" | "right"; // default: "left"
66
- children?: React.ReactNode;
67
- }
68
- ```
69
-
70
- ```tsx
71
- <LinkButton to="/users" icon="fa-users">Ver Usuarios</LinkButton>
72
- <LinkButton to="https://example.com" target="_blank">Sitio Externo</LinkButton>
73
- ```
74
-
75
- ### Input
76
-
77
- Text input with labels, icons, error states, and ref forwarding.
78
-
79
- ```typescript
80
- interface InputProps extends Omit<React.InputHTMLAttributes<HTMLInputElement>, "size"> {
81
- label?: string; // Label text above input
82
- error?: string; // Error message below input
83
- icon?: string; // FontAwesome icon class
84
- iconPosition?: "left" | "right"; // default: "left"
85
- size?: "sm" | "md" | "lg"; // default: "md"
86
- children?: React.ReactNode;
87
- onIconClick?: (event: React.MouseEvent<HTMLElement>) => void; // Makes icon clickable
88
- readOnly?: boolean; // Read-only without disabled appearance
89
- }
90
- ```
91
-
92
- ```tsx
93
- <Input label="Email" type="email" icon="fa-envelope" placeholder="usuario@email.com" />
94
- <Input label="Búsqueda" icon="fa-search" iconPosition="right" onIconClick={handleSearch} />
95
- <Input label="Nombre" error="Campo requerido" />
96
- ```
97
-
98
- ### AutocompleteInput
99
-
100
- Searchable dropdown with single and multiple selection support.
101
-
102
- ```typescript
103
- interface AutocompleteOption {
104
- label: string;
105
- value: string;
106
- description?: string | number;
107
- icon?: string;
108
- }
109
-
110
- interface AutocompleteInputProps<T = AutocompleteOption, K = string>
111
- extends Omit<InputProps, "onChange" | "value" | "ref"> {
112
- options: T[]; // Options array (required)
113
- value?: string | string[]; // String for single, array for multiple
114
- onChange?: ((value: string | string[]) => void) | React.ChangeEventHandler<HTMLInputElement>;
115
- onSelectOption?: (option: T, value: K) => void;
116
- noResultsText?: string; // default: "Sin resultados"
117
- getOptionLabel?: (item: T) => string;
118
- getOptionValue?: (item: T) => K;
119
- getOptionDescription?: (item: T) => string | number | undefined;
120
- renderOption?: (item: T) => React.ReactNode;
121
- readOnly?: boolean;
122
- multiple?: boolean; // Multi-select with checkboxes. default: false
123
- }
124
- ```
125
-
126
- ```tsx
127
- // Single selection
128
- <AutocompleteInput
129
- label="País"
130
- options={[{ label: "Argentina", value: "AR" }, { label: "Brasil", value: "BR" }]}
131
- value={selectedCountry}
132
- onChange={setSelectedCountry}
133
- />
134
-
135
- // Multiple selection
136
- <AutocompleteInput
137
- label="Categorías"
138
- options={categories}
139
- multiple
140
- value={selectedCategories}
141
- onChange={setSelectedCategories}
142
- />
143
-
144
- // Custom objects
145
- <AutocompleteInput<User, number>
146
- label="Usuario"
147
- options={users}
148
- getOptionLabel={(u) => u.fullName}
149
- getOptionValue={(u) => u.id}
150
- getOptionDescription={(u) => u.email}
151
- />
152
- ```
153
-
154
- ### SearchSelectInput
155
-
156
- Opens a dialog modal for selecting from async search results. Ideal for large datasets.
157
-
158
- ```typescript
159
- interface SearchSelectOption {
160
- label: string;
161
- value?: string;
162
- description?: string | number;
163
- icon?: string;
164
- }
165
-
166
- interface SearchSelectInputProps<T = SearchSelectOption, K = string>
167
- extends Omit<InputProps, "onChange" | "value" | "ref"> {
168
- value?: T | K | string;
169
- onChange?: ((value: T | K) => void) | React.ChangeEventHandler<HTMLInputElement>;
170
- onSearchPromiseFn: (text: string) => Promise<Array<T> | PaginationInterface<T>>; // required
171
- onSingleSearchPromiseFn: (value: K) => Promise<T | undefined>; // required
172
- onSelectOption?: (option: T, value: K) => void;
173
- dialogTitle?: string; // default: "Seleccione una opción"
174
- icon?: string; // default: "fa-search"
175
- iconPosition?: "left" | "right"; // default: "right"
176
- noResultsText?: string; // default: "Sin resultados"
177
- getOptionLabel?: (item: T) => string;
178
- getOptionValue?: (item: T) => K;
179
- getOptionDescription?: (item: T) => string | number | undefined;
180
- renderOption?: (item: T) => React.ReactNode;
181
- readOnly?: boolean;
182
- }
183
- ```
184
-
185
- ```tsx
186
- <SearchSelectInput<Product, number>
187
- label="Producto"
188
- onSearchPromiseFn={(text) => apiClient.get({ url: `/api/products?q=${text}` })}
189
- onSingleSearchPromiseFn={(id) => apiClient.get({ url: `/api/products/${id}` })}
190
- getOptionLabel={(p) => p.name}
191
- getOptionValue={(p) => p.id}
192
- getOptionDescription={(p) => `$${p.price}`}
193
- onChange={(value) => setProductId(value)}
194
- />
195
- ```
196
-
197
- ### DatePicker
198
-
199
- Standalone calendar component for date selection.
200
-
201
- ```typescript
202
- interface DatePickerProps {
203
- value?: Dayjs | null;
204
- onChange?: (date: Dayjs) => void;
205
- initialViewDate?: Dayjs; // Initial month/year when value is null
206
- startWeekOn?: "monday" | "sunday"; // default: "sunday"
207
- className?: string;
208
- }
209
- ```
210
-
211
- ### DateInput
212
-
213
- Input field with integrated DatePicker dropdown. Accepts manual text and Dayjs objects.
214
-
215
- ```typescript
216
- type DateInputFormat = "dd/mm/yyyy" | "mm/dd/yyyy";
217
-
218
- interface DateInputProps extends Omit<InputProps, "type" | "value" | "onChange" | "ref"> {
219
- value?: Dayjs | null | string;
220
- onChange?: ((date: Dayjs | null) => void) | React.ChangeEventHandler<HTMLInputElement>;
221
- format?: DateInputFormat; // default: "dd/mm/yyyy"
222
- datePickerProps?: Omit<DatePickerProps, "value" | "onChange">;
223
- readOnly?: boolean;
224
- }
225
- ```
226
-
227
- ```tsx
228
- <DateInput label="Fecha de nacimiento" value={birthDate} onChange={setBirthDate} />
229
- <DateInput label="Start Date" format="mm/dd/yyyy" />
230
- ```
231
-
232
- ### Checkbox
233
-
234
- Boolean checkbox with label and error support. Ref forwarding supported.
235
-
236
- ```typescript
237
- interface CheckboxProps extends Omit<React.InputHTMLAttributes<HTMLInputElement>, "type" | "size"> {
238
- label?: string;
239
- labelPosition?: "left" | "right"; // default: "right"
240
- error?: string;
241
- size?: "sm" | "md" | "lg"; // default: "md"
242
- readOnly?: boolean;
243
- }
244
- ```
245
-
246
- ```tsx
247
- <Checkbox label="Acepto los términos" checked={accepted} onChange={handleChange} />
248
- <Checkbox label="Activo" size="lg" readOnly />
249
- ```
250
-
251
- ### RadioButtonGroup
252
-
253
- Single selection from a group of radio options.
254
-
255
- ```typescript
256
- interface RadioOption {
257
- label: string;
258
- value: string | number;
259
- disabled?: boolean;
260
- }
261
-
262
- interface RadioButtonGroupProps extends Omit<React.HTMLAttributes<HTMLDivElement>, "onChange" | "children"> {
263
- options: RadioOption[]; // required
264
- value?: string | number;
265
- onChange?: ((value: string | number) => void) | React.ChangeEventHandler<HTMLInputElement>;
266
- labelPosition?: "left" | "right"; // default: "right"
267
- size?: "sm" | "md" | "lg"; // default: "md"
268
- error?: string;
269
- direction?: "vertical" | "horizontal"; // default: "vertical"
270
- gap?: "sm" | "md" | "lg"; // default: "md"
271
- name?: string;
272
- disabled?: boolean;
273
- onBlur?: (() => void) | React.FocusEventHandler<HTMLInputElement>;
274
- readOnly?: boolean;
275
- }
276
- ```
277
-
278
- ```tsx
279
- <RadioButtonGroup
280
- options={[
281
- { label: "Masculino", value: "M" },
282
- { label: "Femenino", value: "F" },
283
- { label: "Otro", value: "O" },
284
- ]}
285
- value={gender}
286
- onChange={setGender}
287
- direction="horizontal"
288
- />
289
- ```
290
-
291
- ### CurrencyInput
292
-
293
- Numeric input with currency formatting (Argentine locale: 1.234,56). Ref forwarding supported.
294
-
295
- ```typescript
296
- interface CurrencyInputProps extends Omit<InputProps, "value" | "onChange" | "type"> {
297
- value?: number | null;
298
- onChange?: (value: any) => void; // Receives parsed numeric value
299
- }
300
- ```
301
-
302
- ```tsx
303
- <CurrencyInput label="Monto" value={amount} onChange={setAmount} icon="fa-dollar-sign" />
304
- ```
305
-
306
- ### Pagination
307
-
308
- URL-based pagination controls using react-router-dom's `useSearchParams`.
309
-
310
- ```typescript
311
- interface PaginationProps {
312
- fieldName?: string; // URL param name. default: "pagina"
313
- page?: number; // default: 1
314
- pages?: number; // default: 1
315
- total?: number; // default: 0
316
- isLoading?: boolean; // default: false
317
- }
318
- ```
319
-
320
- ```tsx
321
- <Pagination page={currentPage} pages={totalPages} total={totalItems} />
322
- ```
323
-
324
- ---
325
-
326
- ## Layout Components
327
-
328
- ### Card
329
-
330
- Generic container with header, content, footer, and variants.
331
-
332
- ```typescript
333
- interface CardProps {
334
- title?: string | React.ReactNode;
335
- subtitle?: string | React.ReactNode;
336
- children?: React.ReactNode;
337
- className?: string;
338
- headerActions?: React.ReactNode;
339
- footer?: React.ReactNode;
340
- variant?: "default" | "elevated" | "outlined"; // default: "default"
341
- alwaysDisplayHeaderActions?: boolean; // default: false (shows on hover on lg+)
342
- headerClassName?: string;
343
- contentClassName?: string;
344
- footerClassName?: string;
345
- /**
346
- * Override local de densidad: cuando es true, fuerza el preset "compact" en
347
- * las variables --flysoft-density-* dentro de esta Card y sus descendientes
348
- * (paddings, gaps, tipografía). No depende de la densidad global.
349
- */
350
- compact?: boolean; // default: false
351
- }
352
- ```
353
-
354
- ```tsx
355
- <Card title="Usuarios" headerActions={<Button size="sm" icon="fa-plus">Nuevo</Button>}>
356
- <p>Contenido</p>
357
- </Card>
358
- <Card variant="elevated" compact footer={<Button variant="primary">Guardar</Button>}>
359
- <Input label="Nombre" />
360
- </Card>
361
- // Card densa que afecta también a los DataField dentro
362
- <Card title="Datos personales" compact>
363
- <Collection direction="row" wrap gap="md">
364
- <DataField label="CUIL" value="20-17990271-1" size="sm" />
365
- <DataField label="Edad" value={59} size="sm" />
366
- </Collection>
367
- </Card>
368
- ```
369
-
370
- ### AppLayout
371
-
372
- Main application layout with responsive navbar and sidebar drawer.
373
-
374
- ```typescript
375
- interface AppLayoutProps {
376
- navbar?: NavbarInterface;
377
- leftDrawer?: LeftDrawerInterface;
378
- contentFooter?: React.ReactNode;
379
- children: React.ReactNode; // required
380
- className?: string;
381
- isLeftDrawerOpen?: boolean; // controlled mobile drawer state
382
- onLeftDrawerOpenChange?: (isOpen: boolean) => void;
383
- }
384
-
385
- interface NavbarInterface {
386
- navBarLeftNode?: React.ReactNode;
387
- navBarRightNode?: React.ReactNode;
388
- fullWidthNavbar?: boolean; // Fixed full-width (true) or relative (false)
389
- height?: string; // default: "64px"
390
- className?: string;
391
- }
392
-
393
- interface LeftDrawerInterface {
394
- headerNode?: React.ReactNode;
395
- contentNode?: React.ReactNode;
396
- footerNode?: React.ReactNode;
397
- className?: string;
398
- width?: string; // default: "256px"
399
- }
400
- ```
401
-
402
- ```tsx
403
- <AppLayout
404
- navbar={{
405
- navBarLeftNode: <h1>Mi App</h1>,
406
- navBarRightNode: <Avatar text="Admin" />,
407
- fullWidthNavbar: true,
408
- }}
409
- leftDrawer={{
410
- headerNode: <h2>Menú</h2>,
411
- contentNode: <nav>...</nav>,
412
- }}
413
- >
414
- <main>Contenido</main>
415
- </AppLayout>
416
- ```
417
-
418
- **Behaviors**: Navbar auto-hides/shows on scroll. Mobile drawer with overlay. Responsive breakpoints. The mobile drawer closes automatically when switching to desktop.
419
-
420
- **Closing the drawer from inside**: any component rendered inside `AppLayout` (drawer content, navbar nodes, footer or `children`) can control the drawer with `useLeftDrawer()`:
421
-
422
- ```typescript
423
- interface LeftDrawerContextType {
424
- isLeftDrawerOpen: boolean;
425
- isLeftDrawerCollapsible: boolean; // true on mobile/tablet with drawer content
426
- openLeftDrawer: () => void;
427
- closeLeftDrawer: () => void;
428
- toggleLeftDrawer: () => void;
429
- }
430
-
431
- const useLeftDrawer: () => LeftDrawerContextType; // throws outside AppLayout
432
- const useOptionalLeftDrawer: () => LeftDrawerContextType | undefined; // returns undefined
433
- ```
434
-
435
- ```tsx
436
- // Menú lateral: cerrar el panel al navegar
437
- const AppMenu = () => {
438
- const { closeLeftDrawer } = useLeftDrawer();
439
- return (
440
- <nav>
441
- <LinkButton to="/inicio" onClick={closeLeftDrawer}>Inicio</LinkButton>
442
- <LinkButton to="/clientes" onClick={closeLeftDrawer}>Clientes</LinkButton>
443
- </nav>
444
- );
445
- };
446
- ```
447
-
448
- Calling `closeLeftDrawer()` on desktop is safe — the drawer is always visible there, so nothing changes.
449
-
450
- ### Collection
451
-
452
- Flex container for rendering lists of items, density-aware.
453
-
454
- ```typescript
455
- interface CollectionProps {
456
- children: React.ReactNode; // required
457
- /**
458
- * Presets semánticos ligados a densidad o cualquier valor CSS arbitrario.
459
- * "tight" = 0, "sm"/"md"/"lg" leen --flysoft-density-gap-*.
460
- */
461
- gap?: "tight" | "sm" | "md" | "lg" | string; // default: "md"
462
- direction?: "column" | "row"; // default: "column"
463
- wrap?: boolean; // default: false
464
- className?: string;
465
- /**
466
- * Override local: redefine --flysoft-density-* para esta Collection y
467
- * descendientes. Útil para tener una sección densa dentro de un layout cómodo.
468
- */
469
- density?: "comfortable" | "compact" | "dense";
470
- }
471
- ```
472
-
473
- ```tsx
474
- // Default
475
- <Collection><DataField label="A" value="1" /><DataField label="B" value="2" /></Collection>
476
-
477
- // Horizontal con wrap, gap chico
478
- <Collection direction="row" wrap gap="sm">
479
- <Badge>Activo</Badge><Badge color="info">Verificado</Badge>
480
- </Collection>
481
-
482
- // Sección densa dentro de Card comfortable
483
- <Collection density="dense">
484
- <DataField label="CUIL" value="..." />
485
- <DataField label="Edad" value={59} />
486
- </Collection>
487
- ```
488
-
489
- ### DataField
490
-
491
- Label + value pair display for detail views. Density-aware.
492
-
493
- ```typescript
494
- interface DataFieldProps {
495
- label?: string;
496
- value?: string | number | React.ReactNode;
497
- inline?: boolean; // Horizontal layout. default: false
498
- align?: "left" | "right" | "center"; // default: "left"
499
- title?: string; // HTML title tooltip
500
- link?: string; // Opens URL in new tab
501
- className?: string;
502
- labelClassName?: string;
503
- /**
504
- * Override local de tipografía:
505
- * - "md" (default): label = font-sm, value = font-base.
506
- * - "sm": baja un nivel — label = font-xs, value = font-sm.
507
- */
508
- size?: "sm" | "md";
509
- /** Separación entre label y value en modo stack. "tight" = 0. */
510
- gap?: "tight" | "sm" | "md"; // default: "md"
511
- /** Oculta el ":" después del label en modo inline. */
512
- hideColon?: boolean; // default: false
513
- }
514
- ```
515
-
516
- ```tsx
517
- <DataField label="Nombre" value="Juan Pérez" />
518
- <DataField label="Email" value="juan@email.com" link="mailto:juan@email.com" inline />
519
- // Modo compacto para listas densas
520
- <DataField label="CUIL" value="20-17990271-1" size="sm" />
521
- <DataField label="Estado" value="Activo" inline hideColon />
522
- ```
523
-
524
- ### TabsGroup / TabPanel
525
-
526
- Tabbed interfaces with optional URL persistence.
527
-
528
- ```typescript
529
- interface Tab {
530
- id: string | number;
531
- label: string;
532
- }
533
-
534
- interface TabsGroupProps {
535
- children?: React.ReactNode;
536
- tabs: Tab[]; // required
537
- paramName?: string; // URL search param for persistence
538
- headerNode?: React.ReactNode; // Right-aligned header content
539
- onChangeTab?: (selectedTab: string) => void;
540
- }
541
-
542
- interface TabPanelProps {
543
- children?: React.ReactNode;
544
- tabId: string | number; // Must match a Tab.id (required)
545
- }
546
- ```
547
-
548
- ```tsx
549
- <TabsGroup tabs={[{ id: "info", label: "Información" }, { id: "history", label: "Historial" }]}>
550
- <TabPanel tabId="info">
551
- <p>Información del usuario</p>
552
- </TabPanel>
553
- <TabPanel tabId="history">
554
- <p>Historial de actividad</p>
555
- </TabPanel>
556
- </TabsGroup>
557
- ```
558
-
559
- ### DataTable\<T\>
560
-
561
- High-performance data table with sorting, formatting, actions, and skeleton loading.
562
-
563
- ```typescript
564
- interface DataTableColumn<T> {
565
- align?: "left" | "right" | "center"; // Auto-set for date/currency/numeric
566
- width?: string;
567
- header?: string | React.ReactNode;
568
- footer?: string | React.ReactNode;
569
- value?: string | number | ((row: T) => string | React.ReactNode);
570
- tooltip?: (row: T) => string | React.ReactNode;
571
- type?: "text" | "numeric" | "currency" | "date";
572
- actions?: (row: T) => Array<React.ReactNode>;
573
- headerActions?: () => Array<React.ReactNode>;
574
- }
575
-
576
- interface DataTableProps<T> {
577
- columns: DataTableColumn<T>[]; // required
578
- rows: T[]; // required
579
- className?: string;
580
- maxRows?: number; // Enables sticky header with scroll
581
- locale?: string; // default: "es-AR"
582
- isLoading?: boolean; // Shows skeleton rows. default: false
583
- loadingRows?: number; // default: 5
584
- rowClassName?: (row: T) => string;
585
- headerClassName?: string;
586
- footerClassName?: string;
587
- headerCellClassName?: string;
588
- footerCellClassName?: string;
589
- cellClassName?: string | ((row: T, column: DataTableColumn<T>) => string);
590
- /**
591
- * Override local de densidad: cuando es true, fuerza el preset "compact" en
592
- * las variables --flysoft-density-* dentro de esta DataTable (paddings,
593
- * tipografía, altura de fila). También se propaga a los DropdownMenu de
594
- * acciones. Independiente de la densidad global del ThemeProvider.
595
- */
596
- compact?: boolean; // default: false
597
- }
598
- ```
599
-
600
- ```tsx
601
- interface User { id: number; name: string; salary: number; createdAt: string; }
602
-
603
- const columns: DataTableColumn<User>[] = [
604
- { header: "ID", value: "id", width: "60px" },
605
- { header: "Nombre", value: (row) => row.name },
606
- { header: "Salario", value: "salary", type: "currency" },
607
- { header: "Fecha", value: "createdAt", type: "date" },
608
- {
609
- header: "Acciones",
610
- actions: (row) => [
611
- <Button key="edit" variant="ghost" size="sm" icon="fa-edit" onClick={() => edit(row)}>Editar</Button>,
612
- <Button key="del" variant="ghost" size="sm" icon="fa-trash" color="danger" onClick={() => del(row)}>Eliminar</Button>,
613
- ],
614
- },
615
- ];
616
-
617
- <DataTable<User> columns={columns} rows={users} isLoading={loading} maxRows={10} />
618
- ```
619
-
620
- **Type formatting**: `currency` → thousands separator, no symbol. `numeric` → locale formatting. `date` → DD/MM/YYYY.
621
-
622
- ### Accordion
623
-
624
- Collapsible content section with smooth animation.
625
-
626
- ```typescript
627
- interface AccordionProps {
628
- title: string | React.ReactNode; // required
629
- children: React.ReactNode; // required
630
- icon?: string; // FontAwesome icon
631
- rightNode?: React.ReactNode;
632
- defaultOpen?: boolean; // default: false
633
- className?: string;
634
- headerClassName?: string; // clases para el header (botón)
635
- contentClassName?: string; // clases para el contenedor del contenido
636
- variant?: "default" | "elevated" | "outlined"; // default: "default"
637
- onToggle?: (isOpen: boolean) => void;
638
- }
639
- ```
640
-
641
- ```tsx
642
- <Accordion title="Detalles" icon="fa-info-circle" defaultOpen>
643
- <p>Contenido colapsable</p>
644
- </Accordion>
645
- ```
646
-
647
- ### Menu
648
-
649
- Simple menu list for displaying options.
650
-
651
- ```typescript
652
- interface MenuProps<T = { label: string }> {
653
- options: T[]; // required
654
- onOptionSelected: (item: T) => void; // required
655
- getOptionLabel?: (item: T) => string;
656
- renderOption?: (item: T) => React.ReactNode;
657
- className?: string;
658
- style?: React.CSSProperties;
659
- itemClassName?: string;
660
- }
661
- ```
662
-
663
- ### DropdownMenu
664
-
665
- Portal-based dropdown menu triggered by a button. Auto-positions above/below.
666
-
667
- ```typescript
668
- interface DropdownMenuProps<T = { label: string }> {
669
- options: T[]; // required
670
- onOptionSelected: (item: T) => void; // required
671
- renderNode?: React.ReactNode; // Custom trigger (default: ellipsis icon button)
672
- getOptionLabel?: (item: T) => string;
673
- renderOption?: (item: T) => React.ReactNode;
674
- replaceOnSingleOption?: boolean; // Show single option inline. default: false
675
- openOnHover?: boolean; // default: false
676
- }
677
- ```
678
-
679
- ```tsx
680
- <DropdownMenu
681
- options={[{ label: "Editar" }, { label: "Eliminar" }]}
682
- onOptionSelected={(item) => handleAction(item.label)}
683
- renderNode={<Button variant="ghost" icon="fa-cog" size="sm" />}
684
- />
685
- ```
686
-
687
- ### DropdownPanel
688
-
689
- Portal-based dropdown that renders arbitrary content (not a list).
690
-
691
- ```typescript
692
- interface DropdownPanelProps {
693
- renderNode?: React.ReactNode; // Custom trigger (default: ellipsis icon button)
694
- children: React.ReactNode; // required
695
- openOnHover?: boolean; // default: false
696
- }
697
- ```
698
-
699
- ### Filter
700
-
701
- Versatile filtering component with multiple filter types and optional URL persistence.
702
-
703
- ```typescript
704
- // Discriminated union by filterType
705
- type FilterProps =
706
- | TextFilterProps // filterType?: "text" (default)
707
- | NumberFilterProps // filterType: "number" (+ min?, max?)
708
- | DateFilterProps // filterType: "date"
709
- | AutocompleteFilterProps // filterType: "autocomplete" (+ options, multiple?)
710
- | SearchFilterProps // filterType: "search"
711
- | SearchSelectFilterProps // filterType: "searchSelect" (+ onSearchPromiseFn, onSingleSearchPromiseFn)
712
-
713
- // Common props for all filter types:
714
- interface BaseFilterProps {
715
- paramName?: string; // URL search param for persistence
716
- label?: string;
717
- staticOptions?: Array<{ text: string; value: string }>;
718
- inputWidth?: string;
719
- value?: string; // Controlled value
720
- onChange?: (value: string | undefined) => void;
721
- hideEmpty?: boolean; // default: false
722
- disabled?: boolean; // default: false
723
- compact?: boolean; // default: false — fuerza densidad compacta local
724
- bgColor?: string; // Fondo del badge e input (no del panel flotante). Ej: "#f5f5f5" o "var(--color-bg-secondary)"
725
- }
726
- ```
727
-
728
- ```tsx
729
- <Filter filterType="text" paramName="nombre" label="Nombre" />
730
- // Fondo personalizado cuando el filtro va sobre una Card blanca:
731
- <Filter filterType="search" paramName="q" label="Buscar" bgColor="var(--color-bg-secondary)" />
732
- <Filter filterType="number" paramName="edad" label="Edad" min={0} max={120} />
733
- <Filter filterType="date" paramName="fecha" label="Fecha" />
734
- <Filter
735
- filterType="autocomplete"
736
- paramName="estado"
737
- label="Estado"
738
- options={[{ label: "Activo", value: "1" }, { label: "Inactivo", value: "0" }]}
739
- />
740
- <Filter
741
- filterType="searchSelect"
742
- paramName="cliente"
743
- label="Cliente"
744
- onSearchPromiseFn={(text) => apiClient.get({ url: `/api/clients?q=${text}` })}
745
- onSingleSearchPromiseFn={(id) => apiClient.get({ url: `/api/clients/${id}` })}
746
- />
747
- ```
748
-
749
- ---
750
-
751
- ## Utility Components
752
-
753
- ### Badge
754
-
755
- Status/category label with variants and custom colors.
756
-
757
- ```typescript
758
- interface BadgeProps {
759
- children: React.ReactNode; // required
760
- variant?: "primary" | "secondary" | "success" | "warning" | "danger" | "info"; // default: "primary"
761
- size?: "sm" | "md" | "lg"; // default: "md"
762
- rounded?: boolean; // Full border radius. default: false
763
- className?: string;
764
- icon?: string;
765
- iconPosition?: "left" | "right"; // default: "left"
766
- iconLabel?: string; // aria-label for icon
767
- bg?: string; // Custom background color
768
- textColor?: string; // Custom text color
769
- onClick?: (event: React.MouseEvent<HTMLElement>) => void;
770
- }
771
- ```
772
-
773
- ```tsx
774
- <Badge variant="success" icon="fa-check">Activo</Badge>
775
- <Badge variant="danger" rounded>3</Badge>
776
- <Badge bg="#8b5cf6" textColor="#fff">Custom</Badge>
777
- ```
778
-
779
- ### Avatar
780
-
781
- User profile display with initials fallback when image fails.
782
-
783
- ```typescript
784
- interface AvatarProps {
785
- text: string; // Name for initials extraction (required)
786
- image?: string; // Image URL
787
- bgColor?: string; // default: "#4b5563"
788
- textColor?: string; // default: "#ffffff"
789
- size?: "sm" | "md" | "lg"; // default: "md" (sm=32px, md=40px, lg=48px)
790
- className?: string;
791
- }
792
- ```
793
-
794
- ```tsx
795
- <Avatar text="Juan Pérez" image="/avatars/juan.jpg" />
796
- <Avatar text="Admin User" bgColor="#3b82f6" size="lg" />
797
- ```
798
-
799
- ### RoadMap
800
-
801
- Progress/stage visualization with connected circles and gradient lines.
802
-
803
- ```typescript
804
- interface RoadMapStage {
805
- name: string; // required
806
- description?: string;
807
- icon?: string;
808
- disabled?: boolean; // Grayed out at 50% opacity
809
- variant?: "primary" | "secondary" | "success" | "warning" | "danger" | "info";
810
- bg?: string; // Custom color (overrides variant)
811
- }
812
-
813
- interface RoadMapProps {
814
- stages: RoadMapStage[]; // required
815
- className?: string;
816
- }
817
- ```
818
-
819
- ```tsx
820
- <RoadMap stages={[
821
- { name: "Creado", icon: "fa-plus", variant: "info" },
822
- { name: "En Proceso", icon: "fa-cog", variant: "warning" },
823
- { name: "Completado", icon: "fa-check", variant: "success" },
824
- { name: "Archivado", icon: "fa-archive", disabled: true },
825
- ]} />
826
- ```
827
-
828
- ### Dialog
829
-
830
- Modal window with overlay, escape-to-close, and scroll lock.
831
-
832
- ```typescript
833
- interface DialogProps {
834
- isOpen: boolean; // required
835
- title: React.ReactNode; // required
836
- children: React.ReactNode; // required
837
- footer?: React.ReactNode;
838
- onClose?: () => void;
839
- closeOnOverlayClick?: boolean; // default: false
840
- /**
841
- * Override local de densidad: cuando es true, fuerza el preset "compact" en
842
- * --flysoft-density-* dentro del Dialog (paddings header/body/footer,
843
- * tamaño del título, gaps). Independiente de la densidad global.
844
- */
845
- compact?: boolean; // default: false
846
- bodyWidth?: string | number; // Custom dialog width (e.g. "800px", "80vw", 600). Default: max-w-lg
847
- }
848
- ```
849
-
850
- ```tsx
851
- <Dialog isOpen={showDialog} title="Confirmar" onClose={() => setShowDialog(false)}
852
- footer={
853
- <>
854
- <Button variant="ghost" onClick={() => setShowDialog(false)}>Cancelar</Button>
855
- <Button variant="primary" color="danger" onClick={handleDelete}>Eliminar</Button>
856
- </>
857
- }
858
- >
859
- <p>¿Está seguro que desea eliminar este registro?</p>
860
- </Dialog>
861
- ```
862
-
863
- ### Loader
864
-
865
- Loading indicator with progress bar. Can wrap content with overlay.
866
-
867
- ```typescript
868
- interface LoaderProps {
869
- isLoading?: boolean; // default: false
870
- text?: string; // Text below progress bar
871
- children?: React.ReactNode;
872
- keepContentWhileLoading?: boolean; // Show content faded at 50% opacity
873
- contentLoadingNode?: React.ReactNode; // Custom loading content
874
- overlayClassName?: string; // default: "bg-black/50 backdrop-blur-sm"
875
- }
876
- ```
877
-
878
- ```tsx
879
- <Loader isLoading={loading} text="Cargando datos...">
880
- <DataTable ... />
881
- </Loader>
882
- <Loader isLoading={loading} keepContentWhileLoading>
883
- <Card>...</Card>
884
- </Loader>
885
- ```
886
-
887
- ### FiltersDialog
888
-
889
- Dialog that groups multiple Filter components. Syncs values from/to URL search params.
890
-
891
- ```typescript
892
- interface FilterConfig {
893
- filterType: "text" | "number" | "date" | "autocomplete";
894
- paramName: string; // required
895
- label?: string;
896
- staticOptions?: Array<{ text: string; value: string }>;
897
- inputWidth?: string;
898
- min?: number; // For number filters
899
- max?: number; // For number filters
900
- options?: any[]; // For autocomplete
901
- getOptionLabel?: (item: any) => string;
902
- getOptionValue?: (item: any) => any;
903
- renderOption?: (item: any) => React.ReactNode;
904
- noResultsText?: string;
905
- }
906
-
907
- interface FiltersDialogProps {
908
- filters: FilterConfig[]; // required
909
- }
910
- ```
911
-
912
- ```tsx
913
- <FiltersDialog filters={[
914
- { filterType: "text", paramName: "nombre", label: "Nombre" },
915
- { filterType: "number", paramName: "edad", label: "Edad", min: 0, max: 120 },
916
- { filterType: "date", paramName: "fecha", label: "Fecha" },
917
- { filterType: "autocomplete", paramName: "estado", label: "Estado",
918
- options: [{ label: "Activo", value: "1" }, { label: "Inactivo", value: "0" }] },
919
- ]} />
920
- ```
921
-
922
- ### Snackbar / SnackbarContainer
923
-
924
- Toast notification system. SnackbarContainer must be at the app root.
925
-
926
- ```typescript
927
- interface SnackbarContainerProps {
928
- position?: "top-right" | "top-left" | "bottom-right" | "bottom-left" | "top-center" | "bottom-center"; // default: "top-right"
929
- maxSnackbars?: number; // default: 5
930
- }
931
-
932
- // Usage via hook (not direct Snackbar component):
933
- const { showSnackbar } = useSnackbar();
934
- showSnackbar("Operación exitosa", "success");
935
- showSnackbar("Error al guardar", "danger", { duration: 5000, icon: "fa-exclamation" });
936
- ```
937
-
938
- **Variants**: `"primary"` | `"secondary"` | `"success"` | `"warning"` | `"danger"` | `"info"`
939
- **Default icons**: success=fa-check-circle, danger=fa-times-circle, warning=fa-exclamation-triangle, info/primary/secondary=fa-info-circle
940
-
941
- ### Skeleton
942
-
943
- Loading placeholder with pulse animation. Fully customizable via className.
944
-
945
- ```typescript
946
- interface SkeletonProps {
947
- className?: string; // Tailwind classes to control width, height, shape
948
- }
949
- ```
950
-
951
- ```tsx
952
- <Skeleton className="h-4 w-3/4" /> {/* Text line */}
953
- <Skeleton className="h-10 w-full" /> {/* Input placeholder */}
954
- <Skeleton className="h-32 w-32 rounded-full" /> {/* Avatar placeholder */}
955
- ```
956
-
957
- ### ThemeSwitcher
958
-
959
- Self-contained theme toggle. No props. Displays available themes with switch buttons and current theme info.
960
-
961
- ```tsx
962
- <ThemeSwitcher />
963
- ```
964
-
965
- ---
966
-
967
- ## Contexts & State Management
968
-
969
- ### ThemeProvider / useTheme
970
-
971
- Manages application theme with CSS variable injection, presets, and localStorage persistence.
972
-
973
- ```typescript
974
- type Density = "comfortable" | "compact" | "dense";
975
-
976
- // Provider props
977
- interface ThemeProviderProps {
978
- children: ReactNode;
979
- initialTheme?: string | Theme; // default: "light"
980
- storageKey?: string; // localStorage key. default: "flysoft-theme"
981
- forceInitialTheme?: boolean; // Ignore localStorage. default: false
982
- onThemeChange?: (theme: Theme) => void;
983
- density?: Density; // Global density. default: "comfortable"
984
- densityStorageKey?: string; // default: "flysoft-density"
985
- forceInitialDensity?: boolean; // default: false
986
- onDensityChange?: (density: Density) => void;
987
- }
988
-
989
- // Hook return
990
- interface ThemeContextType {
991
- theme: Theme; // Current theme object
992
- setTheme: (theme: Theme | string) => void; // Switch theme by name or object
993
- updateTheme: (updates: Partial<Theme> | ((prev: Theme) => Theme)) => void;
994
- currentThemeName: string;
995
- availableThemes: string[]; // ["light", "dark", "blue", "green"]
996
- resetToDefault: () => void;
997
- isDark: boolean;
998
- density: Density;
999
- setDensity: (density: Density) => void;
1000
- }
1001
- ```
1002
-
1003
- ```tsx
1004
- // App root - default density
1005
- <ThemeProvider initialTheme="light">
1006
- <App />
1007
- </ThemeProvider>
1008
-
1009
- // App root - data-heavy app (CRUD admin, dashboards)
1010
- <ThemeProvider initialTheme="light" density="dense">
1011
- <App />
1012
- </ThemeProvider>
1013
-
1014
- // Runtime toggle
1015
- const { theme, setTheme, isDark, density, setDensity } = useTheme();
1016
- <Button onClick={() => setTheme(isDark ? "light" : "dark")}>Toggle Theme</Button>
1017
- <Button onClick={() => setDensity(density === "dense" ? "comfortable" : "dense")}>
1018
- Toggle Density
1019
- </Button>
1020
- ```
1021
-
1022
- **Preset themes**: `lightTheme`, `darkTheme`, `blueTheme`, `greenTheme` (importable).
1023
- **Density presets**: `comfortableDensity`, `compactDensity`, `denseDensity`, `densityPresets` (importable).
1024
-
1025
- **Density CSS variables** (inyectadas automáticamente según la densidad activa):
1026
- `--flysoft-density-padding-x-{sm|md|lg}`, `--flysoft-density-padding-y-{sm|md|lg}`,
1027
- `--flysoft-density-container-padding-{x|y}`,
1028
- `--flysoft-density-gap-{sm|md|lg}`, `--flysoft-density-font-{xs|sm|base|lg|xl}`,
1029
- `--flysoft-density-control-height-{sm|md|lg}`, `--flysoft-density-datatable-row`,
1030
- `--flysoft-density-datatable-header`, `--flysoft-density-card-gap`.
1031
-
1032
- **Componentes que ya consumen densidad automáticamente** (sin necesidad de prop):
1033
- Card, DataField, Collection, Button, LinkButton, Input, AutocompleteInput,
1034
- SearchSelectInput, DateInput, CurrencyInput, DatePicker, DataTable, Dialog,
1035
- Filter (incluye los paneles flotantes), FiltersDialog, Accordion, Menu,
1036
- DropdownMenu, DropdownPanel, TabsGroup, Badge, Checkbox, RadioButtonGroup,
1037
- Pagination, Avatar, RoadMap, Snackbar, Skeleton, Loader. **Toda la librería
1038
- es density-aware.** El default `comfortable` preserva el aspecto previo de
1039
- cada componente, así que los consumidores existentes no ven cambios visuales
1040
- hasta que activan `density="compact"` o `density="dense"`.
1041
-
1042
- **Tipografía global**: dentro del wrapper `.flysoft-theme-reset` (cualquier
1043
- ThemeProvider/AppLayoutProvider lo crea automáticamente), los headings y
1044
- elementos de texto sin clase específica escalan con densidad:
1045
- - `h1` = `font-xl × 1.5`, `h2` = `font-xl × 1.25`, `h3` = `font-xl`,
1046
- `h4` = `font-lg`, `h5` = `font-base`, `h6` = `font-sm`
1047
- - `p` = `font-base`, `small` = `font-xs`
1048
- - `span`/`div` heredan `font-base` del wrapper
1049
-
1050
- Las reglas son de baja specificity: cualquier `className` Tailwind (`text-lg`,
1051
- `text-2xl`, etc.) o `style` inline las pisa.
1052
-
1053
- **Componentes con prop `compact` como override local de densidad** (fuerzan
1054
- preset compact en `--flysoft-density-*` dentro de sí y descendientes,
1055
- ignorando la densidad global): Card, DataTable, Dialog, Filter, Accordion,
1056
- Menu, DropdownMenu, DropdownPanel, TabsGroup.
1057
-
1058
- **Override de fondo/estilos en form-controls vía `className`**: los form-controls
1059
- (Input, CurrencyInput, DateInput, AutocompleteInput, SearchSelectInput, Button,
1060
- LinkButton, Checkbox, RadioButtonGroup, DatePicker) combinan sus clases con
1061
- `twMerge`, así que un `className` con clase en conflicto pisa la default de forma
1062
- confiable. Para cambiar el fondo por defecto (`bg-[var(--color-bg-default)]`) —por
1063
- ej. cuando el control va sobre una Card del mismo color— pasá un `bg-*`:
1064
- `<Input className="bg-[var(--color-bg-secondary)]" />` o `<Input className="bg-[#f5f5f5]" />`.
1065
- El `Filter` no toma `className` para esto; usa su prop `bgColor`.
1066
-
1067
- ### AuthProvider / AuthContext
1068
-
1069
- Manages authentication with automatic token validation and refresh.
1070
-
1071
- ```typescript
1072
- interface AuthProviderProps {
1073
- children: React.ReactNode;
1074
- getToken: (username: string, password: string) => Promise<AuthTokenInterface>; // required
1075
- getUserData: (auth: AuthTokenInterface) => Promise<AuthContextUserInterface>; // required
1076
- refreshToken?: (auth: AuthTokenInterface) => Promise<AuthTokenInterface>;
1077
- removeToken?: (auth: AuthTokenInterface) => Promise<void>;
1078
- showLog?: boolean; // default: false
1079
- }
1080
-
1081
- interface AuthContextType {
1082
- user: AuthContextUserInterface | null;
1083
- login: (username: string, password: string) => Promise<void>;
1084
- logout: () => void;
1085
- isAuthenticated: boolean;
1086
- isLoading: boolean;
1087
- }
1088
-
1089
- interface AuthContextUserInterface {
1090
- id?: number | string;
1091
- name?: string;
1092
- aditionalData?: any;
1093
- token?: AuthTokenInterface;
1094
- }
1095
-
1096
- interface AuthTokenInterface {
1097
- accessToken?: string;
1098
- expires?: string; // ISO 8601
1099
- tokenType?: string;
1100
- refreshToken?: string;
1101
- aditionalData?: any;
1102
- }
1103
- ```
1104
-
1105
- ```tsx
1106
- <AuthProvider
1107
- getToken={async (user, pass) => {
1108
- const res = await apiClient.post({ url: "/auth/login", body: { user, pass } });
1109
- return res.token;
1110
- }}
1111
- getUserData={async (auth) => {
1112
- return await apiClient.get({ url: "/auth/me" });
1113
- }}
1114
- refreshToken={async (auth) => {
1115
- return await apiClient.post({ url: "/auth/refresh", body: { token: auth.refreshToken } });
1116
- }}
1117
- >
1118
- <App />
1119
- </AuthProvider>
1120
-
1121
- // In components
1122
- const { user, login, logout, isAuthenticated } = useContext(AuthContext);
1123
- ```
1124
-
1125
- **Behaviors**: Validates token on mount. Checks expiration every 60s. Auto-refreshes if `refreshToken` provided. Stores in localStorage as `"auth"`.
1126
-
1127
- ### CrudProvider / useCrud\<T\>
1128
-
1129
- Generic CRUD context with automatic pagination, URL parameter sync, and snackbar notifications.
1130
-
1131
- ```typescript
1132
- interface CrudProviderProps<T> {
1133
- children: ReactNode;
1134
- getPromise?: Function | { execute: Function; successMessage?: string; errorMessage?: string | ((error: any) => string) };
1135
- getItemPromise?: Function | { execute: Function; successMessage?: string; errorMessage?: string | ((error: any) => string) };
1136
- postPromise?: Function | { execute: Function; successMessage?: string; errorMessage?: string | ((error: any) => string) };
1137
- putPromise?: Function | { execute: Function; successMessage?: string; errorMessage?: string | ((error: any) => string) };
1138
- deletePromise?: Function | { execute: Function; successMessage?: string; errorMessage?: string | ((error: any) => string) };
1139
- urlParams?: Array<string>; // URL params to watch. default: []
1140
- limit?: number; // Items per page. default: 15
1141
- pageParam?: string; // URL page param. default: "pagina"
1142
- singleItemId?: string | number;
1143
- extraData?: Record<string, any>;
1144
- }
1145
-
1146
- interface CrudContextType<T> {
1147
- list: Array<T> | undefined;
1148
- item: T | undefined;
1149
- page: number;
1150
- pages: number;
1151
- total: number;
1152
- limit: number;
1153
- isLoading: boolean;
1154
- pagination: ReactNode; // Pre-built Pagination component
1155
- params: Record<string, any>;
1156
- extraData?: Record<string, any>;
1157
- setExtraData: Dispatch<SetStateAction<Record<string, any> | undefined>>;
1158
- fetchItems: { execute: (params?: Record<string, any>) => Promise<void>; isLoading: boolean };
1159
- fetchItem: { execute: (params?: Record<string, any> | string | number) => Promise<T | undefined>; isLoading: boolean };
1160
- createItem: { execute: (item: T) => Promise<T | undefined | null>; isLoading: boolean };
1161
- updateItem: { execute: (item: T) => Promise<T | undefined | null>; isLoading: boolean };
1162
- deleteItem: { execute: (item: T) => Promise<void>; isLoading: boolean };
1163
- }
1164
- ```
1165
-
1166
- ```tsx
1167
- <CrudProvider<User>
1168
- getPromise={(params) => apiClient.get({ url: "/api/users", params })}
1169
- getItemPromise={(id) => apiClient.get({ url: `/api/users/${id}` })}
1170
- postPromise={{ execute: (item) => apiClient.post({ url: "/api/users", body: item }), successMessage: "Usuario creado" }}
1171
- putPromise={{ execute: (item) => apiClient.put({ url: `/api/users/${item.id}`, body: item }), successMessage: "Usuario actualizado" }}
1172
- deletePromise={{ execute: (item) => apiClient.del({ url: `/api/users/${item.id}` }), successMessage: "Usuario eliminado" }}
1173
- urlParams={["nombre", "estado"]}
1174
- limit={20}
1175
- >
1176
- <UserList />
1177
- </CrudProvider>
1178
-
1179
- // In child components
1180
- const { list, isLoading, pagination, createItem, deleteItem } = useCrud<User>();
1181
- ```
1182
-
1183
- **Behaviors**: Auto-fetches when URL params change. Resets pagination on filter change. Shows snackbar on success/error.
1184
-
1185
- ### SnackbarProvider / useSnackbar
1186
-
1187
- Manages toast notifications.
1188
-
1189
- ```typescript
1190
- interface SnackbarActionsType {
1191
- showSnackbar: (
1192
- message: string,
1193
- variant?: SnackbarVariant,
1194
- options?: { duration?: number; icon?: string; iconLabel?: string }
1195
- ) => void;
1196
- removeSnackbar: (id: string) => void;
1197
- }
1198
- ```
1199
-
1200
- ```tsx
1201
- // App root
1202
- <SnackbarProvider>
1203
- <SnackbarContainer position="bottom-right" maxSnackbars={3} />
1204
- <App />
1205
- </SnackbarProvider>
1206
-
1207
- // In components
1208
- const { showSnackbar } = useSnackbar();
1209
- showSnackbar("Guardado exitosamente", "success");
1210
- showSnackbar("Error de conexión", "danger", { duration: 5000 });
1211
- ```
1212
-
1213
- ### AppLayoutProvider / useAppLayout
1214
-
1215
- Combines ThemeProvider + SnackbarProvider + AppLayout into a single provider.
1216
-
1217
- ```typescript
1218
- interface AppLayoutProviderProps {
1219
- children: ReactNode;
1220
- initialTheme?: string | Theme;
1221
- storageKey?: string;
1222
- forceInitialTheme?: boolean;
1223
- // Densidad global (propagada al ThemeProvider interno)
1224
- density?: "comfortable" | "compact" | "dense"; // default: "comfortable"
1225
- densityStorageKey?: string; // default: "flysoft-density"
1226
- forceInitialDensity?: boolean;
1227
- onDensityChange?: (density: "comfortable" | "compact" | "dense") => void;
1228
- initialNavbar?: NavbarInterface;
1229
- initialLeftDrawer?: LeftDrawerInterface;
1230
- initialContentFooter?: ReactNode;
1231
- className?: string;
1232
- }
1233
-
1234
- interface AppLayoutContextType extends ThemeContextType {
1235
- navbar: NavbarInterface | undefined;
1236
- leftDrawer: LeftDrawerInterface | undefined;
1237
- contentFooter: ReactNode | undefined;
1238
- className: string;
1239
- setNavbar: Dispatch<SetStateAction<NavbarInterface | undefined>>;
1240
- setLeftDrawer: Dispatch<SetStateAction<LeftDrawerInterface | undefined>>;
1241
- setContentFooter: (node: ReactNode | undefined) => void;
1242
- setClassName: (className: string) => void;
1243
- setNavBarLeftNode: (node: ReactNode | undefined) => void;
1244
- setNavbarRightNode: (node: ReactNode | undefined) => void;
1245
- // Left drawer commands (same state as useLeftDrawer())
1246
- isLeftDrawerOpen: boolean;
1247
- openLeftDrawer: () => void;
1248
- closeLeftDrawer: () => void;
1249
- toggleLeftDrawer: () => void;
1250
- }
1251
- ```
1252
-
1253
- ```tsx
1254
- <AppLayoutProvider
1255
- initialTheme="light"
1256
- density="dense" // CRUDs / dashboards / pantallas con mucha info
1257
- initialNavbar={{ navBarLeftNode: <h1>Mi App</h1>, fullWidthNavbar: true }}
1258
- initialLeftDrawer={{ contentNode: <nav>...</nav> }}
1259
- >
1260
- <Routes />
1261
- </AppLayoutProvider>
1262
-
1263
- // In pages - dynamically update layout
1264
- const { setNavBarLeftNode, setNavbarRightNode } = useAppLayout();
1265
- useEffect(() => {
1266
- setNavBarLeftNode(<h1>Dashboard</h1>);
1267
- }, []);
1268
-
1269
- // Close the mobile drawer from anywhere inside the layout
1270
- const { closeLeftDrawer } = useAppLayout(); // or useLeftDrawer()
1271
- <LinkButton to="/clientes" onClick={closeLeftDrawer}>Clientes</LinkButton>
1272
- ```
1273
-
1274
- ---
1275
-
1276
- ## Hooks
1277
-
1278
- ### useThemeOverride
1279
-
1280
- Applies granular CSS variable overrides without changing the entire theme.
1281
-
1282
- ```typescript
1283
- function useThemeOverride(options?: {
1284
- scope?: "global" | "local"; // default: "global"
1285
- element?: HTMLElement | null;
1286
- prefix?: string; // default: "flysoft"
1287
- }): {
1288
- applyOverride: (overrides: Record<string, string | number>) => void;
1289
- revertOverride: (keys: string[]) => void;
1290
- revertAllOverrides: () => void;
1291
- getCSSVariable: (key: string) => string | null;
1292
- isOverrideApplied: (key: string) => boolean;
1293
- appliedOverridesCount: number;
1294
- }
1295
- ```
1296
-
1297
- ### useTemporaryOverride
1298
-
1299
- Applies CSS variable overrides that auto-revert after a duration.
1300
-
1301
- ```typescript
1302
- function useTemporaryOverride(
1303
- overrides: Record<string, string | number>,
1304
- duration?: number, // default: 3000
1305
- options?: { scope?: "global" | "local"; element?: HTMLElement | null; prefix?: string }
1306
- ): { applyTemporaryOverride: () => Function }
1307
- ```
1308
-
1309
- ### useBreakpoint
1310
-
1311
- Returns current viewport breakpoint and device type.
1312
-
1313
- ```typescript
1314
- type Breakpoint = "xs" | "sm" | "md" | "lg" | "xl" | "2xl";
1315
-
1316
- function useBreakpoint(): {
1317
- breakpoint: Breakpoint;
1318
- windowSize: { width: number; height: number };
1319
- isMobile: boolean; // xs or sm
1320
- isTablet: boolean; // md
1321
- isDesktop: boolean; // lg, xl, or 2xl
1322
- }
1323
- ```
1324
-
1325
- ### useElementScroll
1326
-
1327
- Tracks scroll position and direction with requestAnimationFrame optimization.
1328
-
1329
- ```typescript
1330
- function useElementScroll(elementRef: React.RefObject<HTMLElement | null>): {
1331
- scrollY: number;
1332
- scrollDirection: "up" | "down" | null;
1333
- }
1334
- ```
1335
-
1336
- ### useAsyncRequest
1337
-
1338
- Manages async operations with loading state and snackbar notifications.
1339
-
1340
- ```typescript
1341
- interface AsyncRequestOptions {
1342
- successMessage?: string;
1343
- errorMessage?: string | ((error: any) => string);
1344
- successVariant?: SnackbarVariant; // default: "success"
1345
- errorVariant?: SnackbarVariant; // default: "danger"
1346
- onSuccess?: (data: any) => void;
1347
- onError?: (error: any) => void;
1348
- onFinally?: () => void;
1349
- }
1350
-
1351
- function useAsyncRequest(options?: AsyncRequestOptions): {
1352
- isLoading: boolean;
1353
- execute: <T>(requestFn: () => Promise<T>) => Promise<T | undefined>;
1354
- setLoading: (loading: boolean) => void;
1355
- }
1356
- ```
1357
-
1358
- ```tsx
1359
- const { execute, isLoading } = useAsyncRequest({
1360
- successMessage: "Guardado exitosamente",
1361
- errorMessage: (err) => getErrorMessage(err),
1362
- });
1363
- await execute(() => apiClient.post({ url: "/api/data", body: formData }));
1364
- ```
1365
-
1366
- ### useEnum
1367
-
1368
- Converts TypeScript enums to arrays for form select options.
1369
-
1370
- ```typescript
1371
- function useEnum(baseEnum: any): {
1372
- getArray: () => Array<NameValueInterface<number>>;
1373
- getInstance: (id: number) => NameValueInterface<number> | undefined;
1374
- }
1375
- ```
1376
-
1377
- ### useGlobalThemeStyles
1378
-
1379
- Applies theme colors to `<body>` and `<html>` for full-page theming. No return value.
1380
-
1381
- ```tsx
1382
- function useGlobalThemeStyles(): void;
1383
- ```
1384
-
1385
- ---
1386
-
1387
- ## Services
1388
-
1389
- ### apiClient
1390
-
1391
- Singleton HTTP client (Axios-based) with automatic Bearer token injection.
1392
-
1393
- ```typescript
1394
- // Main methods
1395
- apiClient.get<T>(options: { url: string; params?: Record<string, unknown>; headers?: Record<string, string> }): Promise<T>;
1396
- apiClient.post<T>(options: { url: string; body?: unknown; headers?: Record<string, string> }): Promise<T>;
1397
- apiClient.put<T>(options: { url: string; body?: unknown; headers?: Record<string, string> }): Promise<T>;
1398
- apiClient.del<T>(options: { url: string; headers?: Record<string, string> }): Promise<T>;
1399
-
1400
- // File operations
1401
- apiClient.getFile(options): Promise<{ data: Blob; headers: any }>;
1402
- apiClient.getFileAsUrl(options): Promise<string>;
1403
- apiClient.openFile(options): Promise<void>;
1404
- apiClient.downloadFile(options): Promise<void>;
1405
- apiClient.uploadFile<T>(options: { url: string; files: FileList | File[]; headers?: { paramName?: string } }): Promise<T>;
1406
-
1407
- // Token management
1408
- setApiClientTokenProvider(provider?: () => string | undefined): void;
1409
- clearApiClientTokenProvider(): void;
1410
-
1411
- // Create isolated instances
1412
- createApiClient(config?: { baseURL?: string; timeout?: number; headers?: Record<string, string> }): ApiClientService;
1413
- ```
1414
-
1415
- ```tsx
1416
- // Setup token globally
1417
- setApiClientTokenProvider(() => user?.token?.accessToken);
1418
-
1419
- // API calls
1420
- const users = await apiClient.get<User[]>({ url: "/api/users", params: { page: 1 } });
1421
- await apiClient.post({ url: "/api/users", body: { name: "Juan" } });
1422
- await apiClient.downloadFile({ url: "/api/reports/pdf" });
1423
- await apiClient.uploadFile({ url: "/api/upload", files: fileInput.files });
1424
- ```
1425
-
1426
- ---
1427
-
1428
- ## Helpers
1429
-
1430
- | Function | Signature | Description |
1431
- |----------|-----------|-------------|
1432
- | `currencyFormat` | `(value: number) => string` | Formats as `"1.234,56"` (es-AR locale) |
1433
- | `getErrorMessage` | `(error: any) => string` | Extracts message from AxiosError. Default: `"Ha ocurrido un error"` |
1434
- | `getInitialLetters` | `(text: string) => string` | `"Juan Pérez"` `"JP"` |
1435
- | `getQueryString` | `(params: URLSearchParams, newParams: any) => string` | Merges params, returns `"?key=value"` |
1436
- | `objectToQueryString` | `(source: any) => string` | Object to `"a=1&b=2"` (no leading `?`) |
1437
- | `queryStringToObject` | `(params: string) => Record<string, string>` | `"a=1&b=2"` `{a: "1", b: "2"}` |
1438
- | `nameValueArrayToObject` | `<T>(arr: NameValueInterface<T>[]) => Record<string, T>` | Array of {name, value} to object |
1439
- | `promiseMapper` | `<T, K>(promise, mapper) => Promise<K \| K[] \| PaginationInterface<K>>` | Maps promise results (arrays, pagination, single) |
1440
- | `RegularExpressions` | Object | `.email`, `.dateString`, `.password(config)` regex patterns |
1441
-
1442
- ## Interfaces
1443
-
1444
- ```typescript
1445
- interface NameValueInterface<T> {
1446
- name: string;
1447
- value: T;
1448
- extras?: any;
1449
- }
1450
-
1451
- interface PaginationInterface<T> {
1452
- list: Array<T>;
1453
- limit: number;
1454
- page: number;
1455
- pages: number;
1456
- total: number;
1457
- }
1458
- ```
1459
-
1460
- ---
1461
-
1462
- ## Templates
1463
-
1464
- ### LoginForm
1465
-
1466
- ```typescript
1467
- interface LoginFormProps {
1468
- onSubmit?: (data: { email: string; password: string }) => void;
1469
- loading?: boolean;
1470
- error?: string;
1471
- className?: string;
1472
- }
1473
- ```
1474
-
1475
- ### RegistrationForm
1476
-
1477
- ```typescript
1478
- interface RegistrationFormProps {
1479
- onSubmit?: (data: { firstName: string; lastName: string; email: string; password: string; confirmPassword: string }) => void;
1480
- loading?: boolean;
1481
- error?: string;
1482
- className?: string;
1483
- }
1484
- ```
1485
-
1486
- ### ContactForm
1487
-
1488
- ```typescript
1489
- interface ContactFormProps {
1490
- onSubmit?: (data: { name: string; email: string; subject: string; message: string }) => void;
1491
- loading?: boolean;
1492
- success?: boolean;
1493
- error?: string;
1494
- className?: string;
1495
- }
1496
- ```
1497
-
1498
- ### DashboardLayout
1499
-
1500
- ```typescript
1501
- interface DashboardStat {
1502
- title: string;
1503
- value: string | number;
1504
- change?: string; // e.g. "+12%"
1505
- changeType?: "positive" | "negative" | "neutral";
1506
- icon?: string;
1507
- }
1508
-
1509
- interface DashboardLayoutProps {
1510
- title: string; // required
1511
- subtitle?: string;
1512
- stats?: DashboardStat[];
1513
- actions?: React.ReactNode;
1514
- children: React.ReactNode; // required
1515
- className?: string;
1516
- }
1517
- ```
1518
-
1519
- ### SidebarLayout
1520
-
1521
- ```typescript
1522
- interface MenuItem {
1523
- label: string;
1524
- icon: string;
1525
- href: string;
1526
- badge?: string | number;
1527
- children?: MenuItem[];
1528
- }
1529
-
1530
- interface User {
1531
- name: string;
1532
- email?: string;
1533
- avatar?: string;
1534
- }
1535
-
1536
- interface SidebarLayoutProps {
1537
- title: string; // required
1538
- menuItems: MenuItem[]; // required
1539
- user: User; // required
1540
- children: React.ReactNode; // required
1541
- className?: string;
1542
- onLogout?: () => void;
1543
- }
1544
- ```
1545
-
1546
- ### FormPattern
1547
-
1548
- ```typescript
1549
- interface FormField {
1550
- name: string;
1551
- label: string;
1552
- type?: string; // default: "text"
1553
- placeholder?: string;
1554
- icon?: string;
1555
- required?: boolean;
1556
- validation?: (value: string) => string | undefined;
1557
- multiline?: boolean;
1558
- rows?: number; // default: 4
1559
- }
1560
-
1561
- interface FormPatternProps {
1562
- title: string; // required
1563
- subtitle?: string;
1564
- fields: FormField[]; // required
1565
- onSubmit: (data: Record<string, string>) => void; // required
1566
- submitText?: string; // default: "Enviar"
1567
- submitIcon?: string; // default: "fa-paper-plane"
1568
- loading?: boolean;
1569
- error?: string;
1570
- success?: boolean;
1571
- className?: string;
1572
- gridCols?: 1 | 2; // default: 1
1573
- }
1574
- ```
1
+ # Flysoft React UI - AI Context & Documentation
2
+
3
+ This document serves as the source of truth for AI models (Gemini, Claude, GPT, etc.) when generating code that consumes the `flysoft-react-ui` library.
4
+
5
+ ## Library Philosophy
6
+
7
+ `flysoft-react-ui` is a React component library built with TypeScript. It emphasizes a consistent look and feel, ease of use, and "premium" aesthetics out of the box. All components use CSS variables for theming and FontAwesome 5 (light/outlined style) for icons.
8
+
9
+ ## Critical Rules for AI
10
+
11
+ 1. **Top-Level Imports Only**: Always import from `'flysoft-react-ui'`.
12
+ - CORRECT: `import { Button, Card } from 'flysoft-react-ui';`
13
+ - INCORRECT: `import { Button } from 'flysoft-react-ui/components/Button';`
14
+ 2. **TypeScript First**: Use the exported types (e.g., `ButtonProps`, `DataTableColumn<T>`) to ensure type safety.
15
+ 3. **Style Import at App Root Only**: Add `import 'flysoft-react-ui/styles';` once at the app root. Never import CSS in individual components.
16
+ 4. **Do Not Use Docs Internals**: Never import or reference anything from `docs/*` or `src/docs/*`.
17
+ 5. **FontAwesome 5 Only**: Use `fa-*` icon classes. Components normalize to light style (`fal`) automatically. Never use other icon libraries.
18
+ 6. **Theme CSS Variables**: Use `var(--color-*)`, `var(--shadow-*)`, `var(--radius-*)`, `var(--font-*)` for custom styling. Never hardcode colors.
19
+
20
+ ---
21
+
22
+ ## Form Controls
23
+
24
+ ### Button
25
+
26
+ Customizable button with variants, colors, icons, and ripple effect.
27
+
28
+ ```typescript
29
+ interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
30
+ variant?: "primary" | "outline" | "ghost"; // default: "primary"
31
+ size?: "sm" | "md" | "lg"; // default: "md"
32
+ color?: "primary" | "secondary" | "success" | "warning" | "danger" | "info"; // default: "primary"
33
+ bg?: string; // Custom background color (hex, rgb, rgba, hsl, or color name)
34
+ textColor?: string; // Custom text color
35
+ icon?: string; // FontAwesome icon class (e.g. "fa-save")
36
+ iconPosition?: "left" | "right"; // default: "left"
37
+ loading?: boolean; // Shows spinner, disables button. default: false
38
+ children?: React.ReactNode;
39
+ }
40
+ ```
41
+
42
+ ```tsx
43
+ <Button variant="primary" icon="fa-save" loading={isLoading} onClick={handleSave}>
44
+ Guardar
45
+ </Button>
46
+ <Button variant="outline" color="danger" icon="fa-trash">Eliminar</Button>
47
+ <Button variant="ghost" size="sm">Cancelar</Button>
48
+ <Button bg="#8b5cf6" textColor="#fff">Custom Color</Button>
49
+ ```
50
+
51
+ ### LinkButton
52
+
53
+ Anchor-styled button that uses React Router `<Link>` for internal routes and `<a>` for external URLs.
54
+
55
+ ```typescript
56
+ interface LinkButtonProps extends Omit<React.AnchorHTMLAttributes<HTMLAnchorElement>, "href"> {
57
+ to: string; // Route or URL (required)
58
+ target?: string;
59
+ variant?: "primary" | "outline" | "ghost"; // default: "primary"
60
+ size?: "sm" | "md" | "lg"; // default: "md"
61
+ color?: "primary" | "secondary" | "success" | "warning" | "danger" | "info";
62
+ bg?: string;
63
+ textColor?: string;
64
+ icon?: string;
65
+ iconPosition?: "left" | "right"; // default: "left"
66
+ children?: React.ReactNode;
67
+ }
68
+ ```
69
+
70
+ ```tsx
71
+ <LinkButton to="/users" icon="fa-users">Ver Usuarios</LinkButton>
72
+ <LinkButton to="https://example.com" target="_blank">Sitio Externo</LinkButton>
73
+ ```
74
+
75
+ ### Input
76
+
77
+ Text input with labels, icons, error states, and ref forwarding.
78
+
79
+ ```typescript
80
+ interface InputProps extends Omit<React.InputHTMLAttributes<HTMLInputElement>, "size"> {
81
+ label?: string; // Label text above input
82
+ error?: string; // Error message below input
83
+ icon?: string; // FontAwesome icon class
84
+ iconPosition?: "left" | "right"; // default: "left"
85
+ size?: "sm" | "md" | "lg"; // default: "md"
86
+ children?: React.ReactNode;
87
+ onIconClick?: (event: React.MouseEvent<HTMLElement>) => void; // Makes icon clickable
88
+ readOnly?: boolean; // Read-only without disabled appearance
89
+ }
90
+ ```
91
+
92
+ ```tsx
93
+ <Input label="Email" type="email" icon="fa-envelope" placeholder="usuario@email.com" />
94
+ <Input label="Búsqueda" icon="fa-search" iconPosition="right" onIconClick={handleSearch} />
95
+ <Input label="Nombre" error="Campo requerido" />
96
+ ```
97
+
98
+ ### AutocompleteInput
99
+
100
+ Searchable dropdown with single and multiple selection support.
101
+
102
+ ```typescript
103
+ interface AutocompleteOption {
104
+ label: string;
105
+ value: string;
106
+ description?: string | number;
107
+ icon?: string;
108
+ }
109
+
110
+ interface AutocompleteInputProps<T = AutocompleteOption, K = string>
111
+ extends Omit<InputProps, "onChange" | "value" | "ref"> {
112
+ options: T[]; // Options array (required)
113
+ value?: string | string[]; // String for single, array for multiple
114
+ onChange?: ((value: string | string[]) => void) | React.ChangeEventHandler<HTMLInputElement>;
115
+ onSelectOption?: (option: T, value: K) => void;
116
+ noResultsText?: string; // default: "Sin resultados"
117
+ getOptionLabel?: (item: T) => string;
118
+ getOptionValue?: (item: T) => K;
119
+ getOptionDescription?: (item: T) => string | number | undefined;
120
+ renderOption?: (item: T) => React.ReactNode;
121
+ readOnly?: boolean;
122
+ multiple?: boolean; // Multi-select with checkboxes. default: false
123
+ }
124
+ ```
125
+
126
+ ```tsx
127
+ // Single selection
128
+ <AutocompleteInput
129
+ label="País"
130
+ options={[{ label: "Argentina", value: "AR" }, { label: "Brasil", value: "BR" }]}
131
+ value={selectedCountry}
132
+ onChange={setSelectedCountry}
133
+ />
134
+
135
+ // Multiple selection
136
+ <AutocompleteInput
137
+ label="Categorías"
138
+ options={categories}
139
+ multiple
140
+ value={selectedCategories}
141
+ onChange={setSelectedCategories}
142
+ />
143
+
144
+ // Custom objects
145
+ <AutocompleteInput<User, number>
146
+ label="Usuario"
147
+ options={users}
148
+ getOptionLabel={(u) => u.fullName}
149
+ getOptionValue={(u) => u.id}
150
+ getOptionDescription={(u) => u.email}
151
+ />
152
+ ```
153
+
154
+ ### SearchSelectInput
155
+
156
+ Opens a dialog modal for selecting from async search results. Ideal for large datasets.
157
+
158
+ ```typescript
159
+ interface SearchSelectOption {
160
+ label: string;
161
+ value?: string;
162
+ description?: string | number;
163
+ icon?: string;
164
+ }
165
+
166
+ interface SearchSelectInputProps<T = SearchSelectOption, K = string>
167
+ extends Omit<InputProps, "onChange" | "value" | "ref"> {
168
+ value?: T | K | string;
169
+ onChange?: ((value: T | K) => void) | React.ChangeEventHandler<HTMLInputElement>;
170
+ onSearchPromiseFn: (text: string) => Promise<Array<T> | PaginationInterface<T>>; // required
171
+ onSingleSearchPromiseFn: (value: K) => Promise<T | undefined>; // required
172
+ onSelectOption?: (option: T, value: K) => void;
173
+ dialogTitle?: string; // default: "Seleccione una opción"
174
+ icon?: string; // default: "fa-search"
175
+ iconPosition?: "left" | "right"; // default: "right"
176
+ noResultsText?: string; // default: "Sin resultados"
177
+ getOptionLabel?: (item: T) => string;
178
+ getOptionValue?: (item: T) => K;
179
+ getOptionDescription?: (item: T) => string | number | undefined;
180
+ renderOption?: (item: T) => React.ReactNode;
181
+ readOnly?: boolean;
182
+ }
183
+ ```
184
+
185
+ ```tsx
186
+ <SearchSelectInput<Product, number>
187
+ label="Producto"
188
+ onSearchPromiseFn={(text) => apiClient.get({ url: `/api/products?q=${text}` })}
189
+ onSingleSearchPromiseFn={(id) => apiClient.get({ url: `/api/products/${id}` })}
190
+ getOptionLabel={(p) => p.name}
191
+ getOptionValue={(p) => p.id}
192
+ getOptionDescription={(p) => `$${p.price}`}
193
+ onChange={(value) => setProductId(value)}
194
+ />
195
+ ```
196
+
197
+ ### DatePicker
198
+
199
+ Standalone calendar component for date selection.
200
+
201
+ ```typescript
202
+ interface DatePickerProps {
203
+ value?: Dayjs | null;
204
+ onChange?: (date: Dayjs) => void;
205
+ initialViewDate?: Dayjs; // Initial month/year when value is null
206
+ startWeekOn?: "monday" | "sunday"; // default: "sunday"
207
+ className?: string;
208
+ }
209
+ ```
210
+
211
+ ### DateInput
212
+
213
+ Input field with integrated DatePicker dropdown. Accepts manual text and Dayjs objects.
214
+
215
+ ```typescript
216
+ type DateInputFormat = "dd/mm/yyyy" | "mm/dd/yyyy";
217
+
218
+ interface DateInputProps extends Omit<InputProps, "type" | "value" | "onChange" | "ref"> {
219
+ value?: Dayjs | null | string;
220
+ onChange?: ((date: Dayjs | null) => void) | React.ChangeEventHandler<HTMLInputElement>;
221
+ format?: DateInputFormat; // default: "dd/mm/yyyy"
222
+ datePickerProps?: Omit<DatePickerProps, "value" | "onChange">;
223
+ readOnly?: boolean;
224
+ }
225
+ ```
226
+
227
+ ```tsx
228
+ <DateInput label="Fecha de nacimiento" value={birthDate} onChange={setBirthDate} />
229
+ <DateInput label="Start Date" format="mm/dd/yyyy" />
230
+ ```
231
+
232
+ ### Checkbox
233
+
234
+ Boolean checkbox with label and error support. Ref forwarding supported.
235
+
236
+ ```typescript
237
+ interface CheckboxProps extends Omit<React.InputHTMLAttributes<HTMLInputElement>, "type" | "size"> {
238
+ label?: string;
239
+ labelPosition?: "left" | "right"; // default: "right"
240
+ error?: string;
241
+ size?: "sm" | "md" | "lg"; // default: "md"
242
+ readOnly?: boolean;
243
+ }
244
+ ```
245
+
246
+ ```tsx
247
+ <Checkbox label="Acepto los términos" checked={accepted} onChange={handleChange} />
248
+ <Checkbox label="Activo" size="lg" readOnly />
249
+ ```
250
+
251
+ ### RadioButtonGroup
252
+
253
+ Single selection from a group of radio options.
254
+
255
+ ```typescript
256
+ interface RadioOption {
257
+ label: string;
258
+ value: string | number;
259
+ disabled?: boolean;
260
+ }
261
+
262
+ interface RadioButtonGroupProps extends Omit<React.HTMLAttributes<HTMLDivElement>, "onChange" | "children"> {
263
+ options: RadioOption[]; // required
264
+ value?: string | number;
265
+ onChange?: ((value: string | number) => void) | React.ChangeEventHandler<HTMLInputElement>;
266
+ labelPosition?: "left" | "right"; // default: "right"
267
+ size?: "sm" | "md" | "lg"; // default: "md"
268
+ error?: string;
269
+ direction?: "vertical" | "horizontal"; // default: "vertical"
270
+ gap?: "sm" | "md" | "lg"; // default: "md"
271
+ name?: string;
272
+ disabled?: boolean;
273
+ onBlur?: (() => void) | React.FocusEventHandler<HTMLInputElement>;
274
+ readOnly?: boolean;
275
+ }
276
+ ```
277
+
278
+ ```tsx
279
+ <RadioButtonGroup
280
+ options={[
281
+ { label: "Masculino", value: "M" },
282
+ { label: "Femenino", value: "F" },
283
+ { label: "Otro", value: "O" },
284
+ ]}
285
+ value={gender}
286
+ onChange={setGender}
287
+ direction="horizontal"
288
+ />
289
+ ```
290
+
291
+ ### CurrencyInput
292
+
293
+ Numeric input with currency formatting (Argentine locale: 1.234,56). Ref forwarding supported.
294
+
295
+ ```typescript
296
+ interface CurrencyInputProps extends Omit<InputProps, "value" | "onChange" | "type"> {
297
+ value?: number | null;
298
+ onChange?: (value: any) => void; // Receives parsed numeric value
299
+ }
300
+ ```
301
+
302
+ ```tsx
303
+ <CurrencyInput label="Monto" value={amount} onChange={setAmount} icon="fa-dollar-sign" />
304
+ ```
305
+
306
+ ### Pagination
307
+
308
+ URL-based pagination controls using react-router-dom's `useSearchParams`.
309
+
310
+ ```typescript
311
+ interface PaginationProps {
312
+ fieldName?: string; // URL param name. default: "pagina"
313
+ page?: number; // default: 1
314
+ pages?: number; // default: 1
315
+ total?: number; // default: 0
316
+ isLoading?: boolean; // default: false
317
+ }
318
+ ```
319
+
320
+ ```tsx
321
+ <Pagination page={currentPage} pages={totalPages} total={totalItems} />
322
+ ```
323
+
324
+ ---
325
+
326
+ ## Layout Components
327
+
328
+ ### Card
329
+
330
+ Generic container with header, content, footer, and variants.
331
+
332
+ ```typescript
333
+ interface CardProps {
334
+ title?: string | React.ReactNode;
335
+ subtitle?: string | React.ReactNode;
336
+ children?: React.ReactNode;
337
+ className?: string;
338
+ headerActions?: React.ReactNode;
339
+ footer?: React.ReactNode;
340
+ variant?: "default" | "elevated" | "outlined"; // default: "default"
341
+ alwaysDisplayHeaderActions?: boolean; // default: false (shows on hover on lg+)
342
+ headerClassName?: string;
343
+ contentClassName?: string;
344
+ footerClassName?: string;
345
+ /**
346
+ * Override local de densidad: cuando es true, fuerza el preset "compact" en
347
+ * las variables --flysoft-density-* dentro de esta Card y sus descendientes
348
+ * (paddings, gaps, tipografía). No depende de la densidad global.
349
+ */
350
+ compact?: boolean; // default: false
351
+ }
352
+ ```
353
+
354
+ ```tsx
355
+ <Card title="Usuarios" headerActions={<Button size="sm" icon="fa-plus">Nuevo</Button>}>
356
+ <p>Contenido</p>
357
+ </Card>
358
+ <Card variant="elevated" compact footer={<Button variant="primary">Guardar</Button>}>
359
+ <Input label="Nombre" />
360
+ </Card>
361
+ // Card densa que afecta también a los DataField dentro
362
+ <Card title="Datos personales" compact>
363
+ <Collection direction="row" wrap gap="md">
364
+ <DataField label="CUIL" value="20-17990271-1" size="sm" />
365
+ <DataField label="Edad" value={59} size="sm" />
366
+ </Collection>
367
+ </Card>
368
+ ```
369
+
370
+ ### AppLayout
371
+
372
+ Main application layout with responsive navbar and sidebar drawer.
373
+
374
+ ```typescript
375
+ interface AppLayoutProps {
376
+ navbar?: NavbarInterface;
377
+ leftDrawer?: LeftDrawerInterface;
378
+ contentFooter?: React.ReactNode;
379
+ children: React.ReactNode; // required
380
+ className?: string;
381
+ isLeftDrawerOpen?: boolean; // controlled mobile drawer state
382
+ onLeftDrawerOpenChange?: (isOpen: boolean) => void;
383
+ }
384
+
385
+ interface NavbarInterface {
386
+ navBarLeftNode?: React.ReactNode;
387
+ navBarRightNode?: React.ReactNode;
388
+ fullWidthNavbar?: boolean; // Fixed full-width (true) or relative (false)
389
+ height?: string; // default: "64px"
390
+ className?: string;
391
+ }
392
+
393
+ interface LeftDrawerInterface {
394
+ headerNode?: React.ReactNode;
395
+ contentNode?: React.ReactNode;
396
+ footerNode?: React.ReactNode;
397
+ className?: string;
398
+ width?: string; // default: "256px"
399
+ }
400
+ ```
401
+
402
+ ```tsx
403
+ <AppLayout
404
+ navbar={{
405
+ navBarLeftNode: <h1>Mi App</h1>,
406
+ navBarRightNode: <Avatar text="Admin" />,
407
+ fullWidthNavbar: true,
408
+ }}
409
+ leftDrawer={{
410
+ headerNode: <h2>Menú</h2>,
411
+ contentNode: <nav>...</nav>,
412
+ }}
413
+ >
414
+ <main>Contenido</main>
415
+ </AppLayout>
416
+ ```
417
+
418
+ **Behaviors**: Navbar auto-hides/shows on scroll. Mobile drawer with overlay. Responsive breakpoints. The mobile drawer closes automatically when switching to desktop.
419
+
420
+ **Closing the drawer from inside**: any component rendered inside `AppLayout` (drawer content, navbar nodes, footer or `children`) can control the drawer with `useLeftDrawer()`:
421
+
422
+ ```typescript
423
+ interface LeftDrawerContextType {
424
+ isLeftDrawerOpen: boolean;
425
+ isLeftDrawerCollapsible: boolean; // true on mobile/tablet with drawer content
426
+ openLeftDrawer: () => void;
427
+ closeLeftDrawer: () => void;
428
+ toggleLeftDrawer: () => void;
429
+ }
430
+
431
+ const useLeftDrawer: () => LeftDrawerContextType; // throws outside AppLayout
432
+ const useOptionalLeftDrawer: () => LeftDrawerContextType | undefined; // returns undefined
433
+ ```
434
+
435
+ ```tsx
436
+ // Menú lateral: cerrar el panel al navegar
437
+ const AppMenu = () => {
438
+ const { closeLeftDrawer } = useLeftDrawer();
439
+ return (
440
+ <nav>
441
+ <LinkButton to="/inicio" onClick={closeLeftDrawer}>Inicio</LinkButton>
442
+ <LinkButton to="/clientes" onClick={closeLeftDrawer}>Clientes</LinkButton>
443
+ </nav>
444
+ );
445
+ };
446
+ ```
447
+
448
+ Calling `closeLeftDrawer()` on desktop is safe — the drawer is always visible there, so nothing changes.
449
+
450
+ ### Collection
451
+
452
+ Flex container for rendering lists of items, density-aware.
453
+
454
+ ```typescript
455
+ interface CollectionProps {
456
+ children: React.ReactNode; // required
457
+ /**
458
+ * Presets semánticos ligados a densidad o cualquier valor CSS arbitrario.
459
+ * "tight" = 0, "sm"/"md"/"lg" leen --flysoft-density-gap-*.
460
+ */
461
+ gap?: "tight" | "sm" | "md" | "lg" | string; // default: "md"
462
+ direction?: "column" | "row"; // default: "column"
463
+ wrap?: boolean; // default: false
464
+ className?: string;
465
+ /**
466
+ * Override local: redefine --flysoft-density-* para esta Collection y
467
+ * descendientes. Útil para tener una sección densa dentro de un layout cómodo.
468
+ */
469
+ density?: "comfortable" | "compact" | "dense";
470
+ }
471
+ ```
472
+
473
+ ```tsx
474
+ // Default
475
+ <Collection><DataField label="A" value="1" /><DataField label="B" value="2" /></Collection>
476
+
477
+ // Horizontal con wrap, gap chico
478
+ <Collection direction="row" wrap gap="sm">
479
+ <Badge>Activo</Badge><Badge color="info">Verificado</Badge>
480
+ </Collection>
481
+
482
+ // Sección densa dentro de Card comfortable
483
+ <Collection density="dense">
484
+ <DataField label="CUIL" value="..." />
485
+ <DataField label="Edad" value={59} />
486
+ </Collection>
487
+ ```
488
+
489
+ ### DataField
490
+
491
+ Label + value pair display for detail views. Density-aware.
492
+
493
+ ```typescript
494
+ interface DataFieldProps {
495
+ label?: string;
496
+ value?: string | number | React.ReactNode;
497
+ inline?: boolean; // Horizontal layout. default: false
498
+ align?: "left" | "right" | "center"; // default: "left"
499
+ title?: string; // HTML title tooltip
500
+ link?: string; // Opens URL in new tab
501
+ className?: string;
502
+ labelClassName?: string;
503
+ /**
504
+ * Override local de tipografía:
505
+ * - "md" (default): label = font-sm, value = font-base.
506
+ * - "sm": baja un nivel — label = font-xs, value = font-sm.
507
+ */
508
+ size?: "sm" | "md";
509
+ /** Separación entre label y value en modo stack. "tight" = 0. */
510
+ gap?: "tight" | "sm" | "md"; // default: "md"
511
+ /** Oculta el ":" después del label en modo inline. */
512
+ hideColon?: boolean; // default: false
513
+ }
514
+ ```
515
+
516
+ ```tsx
517
+ <DataField label="Nombre" value="Juan Pérez" />
518
+ <DataField label="Email" value="juan@email.com" link="mailto:juan@email.com" inline />
519
+ // Modo compacto para listas densas
520
+ <DataField label="CUIL" value="20-17990271-1" size="sm" />
521
+ <DataField label="Estado" value="Activo" inline hideColon />
522
+ ```
523
+
524
+ ### TabsGroup / TabPanel
525
+
526
+ Tabbed interfaces with optional URL persistence.
527
+
528
+ ```typescript
529
+ interface Tab {
530
+ id: string | number;
531
+ label: string;
532
+ }
533
+
534
+ interface TabsGroupProps {
535
+ children?: React.ReactNode;
536
+ tabs: Tab[]; // required
537
+ paramName?: string; // URL search param for persistence
538
+ headerNode?: React.ReactNode; // Right-aligned header content
539
+ onChangeTab?: (selectedTab: string) => void;
540
+ }
541
+
542
+ interface TabPanelProps {
543
+ children?: React.ReactNode;
544
+ tabId: string | number; // Must match a Tab.id (required)
545
+ }
546
+ ```
547
+
548
+ ```tsx
549
+ <TabsGroup tabs={[{ id: "info", label: "Información" }, { id: "history", label: "Historial" }]}>
550
+ <TabPanel tabId="info">
551
+ <p>Información del usuario</p>
552
+ </TabPanel>
553
+ <TabPanel tabId="history">
554
+ <p>Historial de actividad</p>
555
+ </TabPanel>
556
+ </TabsGroup>
557
+ ```
558
+
559
+ ### DataTable\<T\>
560
+
561
+ High-performance data table with sorting, formatting, actions, and skeleton loading.
562
+
563
+ ```typescript
564
+ interface DataTableColumn<T> {
565
+ align?: "left" | "right" | "center"; // Auto-set for date/currency/numeric
566
+ width?: string;
567
+ header?: string | React.ReactNode;
568
+ footer?: string | React.ReactNode;
569
+ value?: string | number | ((row: T) => string | React.ReactNode);
570
+ tooltip?: (row: T) => string | React.ReactNode;
571
+ type?: "text" | "numeric" | "currency" | "date";
572
+ actions?: (row: T) => Array<React.ReactNode>;
573
+ headerActions?: () => Array<React.ReactNode>;
574
+ }
575
+
576
+ interface DataTableProps<T> {
577
+ columns: DataTableColumn<T>[]; // required
578
+ rows: T[]; // required
579
+ className?: string;
580
+ maxRows?: number; // Enables sticky header with scroll
581
+ locale?: string; // default: "es-AR"
582
+ isLoading?: boolean; // Shows skeleton rows. default: false
583
+ loadingRows?: number; // default: 5
584
+ rowClassName?: (row: T) => string;
585
+ headerClassName?: string;
586
+ footerClassName?: string;
587
+ headerCellClassName?: string;
588
+ footerCellClassName?: string;
589
+ cellClassName?: string | ((row: T, column: DataTableColumn<T>) => string);
590
+ /**
591
+ * Override local de densidad: cuando es true, fuerza el preset "compact" en
592
+ * las variables --flysoft-density-* dentro de esta DataTable (paddings,
593
+ * tipografía, altura de fila). También se propaga a los DropdownMenu de
594
+ * acciones. Independiente de la densidad global del ThemeProvider.
595
+ */
596
+ compact?: boolean; // default: false
597
+ }
598
+ ```
599
+
600
+ ```tsx
601
+ interface User { id: number; name: string; salary: number; createdAt: string; }
602
+
603
+ const columns: DataTableColumn<User>[] = [
604
+ { header: "ID", value: "id", width: "60px" },
605
+ { header: "Nombre", value: (row) => row.name },
606
+ { header: "Salario", value: "salary", type: "currency" },
607
+ { header: "Fecha", value: "createdAt", type: "date" },
608
+ {
609
+ header: "Acciones",
610
+ actions: (row) => [
611
+ <Button key="edit" variant="ghost" size="sm" icon="fa-edit" onClick={() => edit(row)}>Editar</Button>,
612
+ <Button key="del" variant="ghost" size="sm" icon="fa-trash" color="danger" onClick={() => del(row)}>Eliminar</Button>,
613
+ ],
614
+ },
615
+ ];
616
+
617
+ <DataTable<User> columns={columns} rows={users} isLoading={loading} maxRows={10} />
618
+ ```
619
+
620
+ **Type formatting**: `currency` → thousands separator, no symbol. `numeric` → locale formatting. `date` → DD/MM/YYYY.
621
+
622
+ ### Accordion
623
+
624
+ Collapsible content section with smooth animation.
625
+
626
+ ```typescript
627
+ interface AccordionProps {
628
+ title: string | React.ReactNode; // required
629
+ children: React.ReactNode; // required
630
+ icon?: string; // FontAwesome icon
631
+ rightNode?: React.ReactNode;
632
+ defaultOpen?: boolean; // default: false
633
+ className?: string;
634
+ headerClassName?: string; // clases para el header (botón)
635
+ contentClassName?: string; // clases para el contenedor del contenido
636
+ variant?: "default" | "elevated" | "outlined"; // default: "default"
637
+ onToggle?: (isOpen: boolean) => void;
638
+ }
639
+ ```
640
+
641
+ ```tsx
642
+ <Accordion title="Detalles" icon="fa-info-circle" defaultOpen>
643
+ <p>Contenido colapsable</p>
644
+ </Accordion>
645
+ ```
646
+
647
+ ### Menu
648
+
649
+ Simple menu list for displaying options.
650
+
651
+ ```typescript
652
+ interface MenuProps<T = { label: string }> {
653
+ options: T[]; // required
654
+ onOptionSelected: (item: T) => void; // required
655
+ getOptionLabel?: (item: T) => string;
656
+ renderOption?: (item: T) => React.ReactNode;
657
+ className?: string;
658
+ style?: React.CSSProperties;
659
+ itemClassName?: string;
660
+ }
661
+ ```
662
+
663
+ ### DropdownMenu
664
+
665
+ Portal-based dropdown menu triggered by a button. Auto-positions above/below.
666
+
667
+ ```typescript
668
+ interface DropdownMenuProps<T = { label: string }> {
669
+ options: T[]; // required
670
+ onOptionSelected: (item: T) => void; // required
671
+ renderNode?: React.ReactNode; // Custom trigger (default: ellipsis icon button)
672
+ getOptionLabel?: (item: T) => string;
673
+ renderOption?: (item: T) => React.ReactNode;
674
+ replaceOnSingleOption?: boolean; // Show single option inline. default: false
675
+ openOnHover?: boolean; // default: false
676
+ }
677
+ ```
678
+
679
+ ```tsx
680
+ <DropdownMenu
681
+ options={[{ label: "Editar" }, { label: "Eliminar" }]}
682
+ onOptionSelected={(item) => handleAction(item.label)}
683
+ renderNode={<Button variant="ghost" icon="fa-cog" size="sm" />}
684
+ />
685
+ ```
686
+
687
+ ### DropdownPanel
688
+
689
+ Portal-based dropdown that renders arbitrary content (not a list).
690
+
691
+ ```typescript
692
+ interface DropdownPanelProps {
693
+ renderNode?: React.ReactNode; // Custom trigger (default: ellipsis icon button)
694
+ children: React.ReactNode; // required
695
+ openOnHover?: boolean; // default: false
696
+ }
697
+ ```
698
+
699
+ ### Filter
700
+
701
+ Versatile filtering component with multiple filter types and optional URL persistence.
702
+
703
+ ```typescript
704
+ // Discriminated union by filterType
705
+ type FilterProps =
706
+ | TextFilterProps // filterType?: "text" (default)
707
+ | NumberFilterProps // filterType: "number" (+ min?, max?)
708
+ | DateFilterProps // filterType: "date"
709
+ | AutocompleteFilterProps // filterType: "autocomplete" (+ options, multiple?)
710
+ | SearchFilterProps // filterType: "search"
711
+ | SearchSelectFilterProps // filterType: "searchSelect" (+ onSearchPromiseFn, onSingleSearchPromiseFn)
712
+
713
+ // Common props for all filter types:
714
+ interface BaseFilterProps {
715
+ paramName?: string; // URL search param for persistence
716
+ label?: string;
717
+ staticOptions?: Array<{ text: string; value: string }>;
718
+ inputWidth?: string;
719
+ value?: string; // Controlled value
720
+ onChange?: (value: string | undefined) => void;
721
+ hideEmpty?: boolean; // default: false
722
+ disabled?: boolean; // default: false
723
+ compact?: boolean; // default: false — fuerza densidad compacta local
724
+ bgColor?: string; // Fondo del badge e input (no del panel flotante). Ej: "#f5f5f5" o "var(--color-bg-secondary)"
725
+ }
726
+ ```
727
+
728
+ ```tsx
729
+ <Filter filterType="text" paramName="nombre" label="Nombre" />
730
+ // Fondo personalizado cuando el filtro va sobre una Card blanca:
731
+ <Filter filterType="search" paramName="q" label="Buscar" bgColor="var(--color-bg-secondary)" />
732
+ <Filter filterType="number" paramName="edad" label="Edad" min={0} max={120} />
733
+ <Filter filterType="date" paramName="fecha" label="Fecha" />
734
+ <Filter
735
+ filterType="autocomplete"
736
+ paramName="estado"
737
+ label="Estado"
738
+ options={[{ label: "Activo", value: "1" }, { label: "Inactivo", value: "0" }]}
739
+ />
740
+ <Filter
741
+ filterType="searchSelect"
742
+ paramName="cliente"
743
+ label="Cliente"
744
+ onSearchPromiseFn={(text) => apiClient.get({ url: `/api/clients?q=${text}` })}
745
+ onSingleSearchPromiseFn={(id) => apiClient.get({ url: `/api/clients/${id}` })}
746
+ />
747
+ ```
748
+
749
+ ---
750
+
751
+ ## Utility Components
752
+
753
+ ### Badge
754
+
755
+ Status/category label with variants and custom colors.
756
+
757
+ ```typescript
758
+ interface BadgeProps {
759
+ children: React.ReactNode; // required
760
+ variant?: "primary" | "secondary" | "success" | "warning" | "danger" | "info"; // default: "primary"
761
+ size?: "sm" | "md" | "lg"; // default: "md"
762
+ rounded?: boolean; // Full border radius. default: false
763
+ className?: string;
764
+ icon?: string;
765
+ iconPosition?: "left" | "right"; // default: "left"
766
+ iconLabel?: string; // aria-label for icon
767
+ bg?: string; // Custom background color
768
+ textColor?: string; // Custom text color
769
+ onClick?: (event: React.MouseEvent<HTMLElement>) => void;
770
+ }
771
+ ```
772
+
773
+ ```tsx
774
+ <Badge variant="success" icon="fa-check">Activo</Badge>
775
+ <Badge variant="danger" rounded>3</Badge>
776
+ <Badge bg="#8b5cf6" textColor="#fff">Custom</Badge>
777
+ ```
778
+
779
+ ### Avatar
780
+
781
+ User profile display with initials fallback when image fails.
782
+
783
+ ```typescript
784
+ interface AvatarProps {
785
+ text: string; // Name for initials extraction (required)
786
+ image?: string; // Image URL
787
+ bgColor?: string; // default: "#4b5563"
788
+ textColor?: string; // default: "#ffffff"
789
+ size?: "sm" | "md" | "lg"; // default: "md" (sm=32px, md=40px, lg=48px)
790
+ className?: string;
791
+ }
792
+ ```
793
+
794
+ ```tsx
795
+ <Avatar text="Juan Pérez" image="/avatars/juan.jpg" />
796
+ <Avatar text="Admin User" bgColor="#3b82f6" size="lg" />
797
+ ```
798
+
799
+ ### RoadMap
800
+
801
+ Progress/stage visualization with connected circles and gradient lines.
802
+
803
+ ```typescript
804
+ interface RoadMapStage {
805
+ name: string; // required
806
+ description?: string;
807
+ icon?: string;
808
+ disabled?: boolean; // Grayed out at 50% opacity
809
+ variant?: "primary" | "secondary" | "success" | "warning" | "danger" | "info";
810
+ bg?: string; // Custom color (overrides variant)
811
+ }
812
+
813
+ interface RoadMapProps {
814
+ stages: RoadMapStage[]; // required
815
+ className?: string;
816
+ }
817
+ ```
818
+
819
+ ```tsx
820
+ <RoadMap stages={[
821
+ { name: "Creado", icon: "fa-plus", variant: "info" },
822
+ { name: "En Proceso", icon: "fa-cog", variant: "warning" },
823
+ { name: "Completado", icon: "fa-check", variant: "success" },
824
+ { name: "Archivado", icon: "fa-archive", disabled: true },
825
+ ]} />
826
+ ```
827
+
828
+ ### Dialog
829
+
830
+ Modal window with overlay, escape-to-close, and scroll lock.
831
+
832
+ ```typescript
833
+ interface DialogProps {
834
+ isOpen: boolean; // required
835
+ title: React.ReactNode; // required
836
+ children: React.ReactNode; // required
837
+ footer?: React.ReactNode;
838
+ onClose?: () => void;
839
+ closeOnOverlayClick?: boolean; // default: false
840
+ /**
841
+ * Override local de densidad: cuando es true, fuerza el preset "compact" en
842
+ * --flysoft-density-* dentro del Dialog (paddings header/body/footer,
843
+ * tamaño del título, gaps). Independiente de la densidad global.
844
+ */
845
+ compact?: boolean; // default: false
846
+ bodyWidth?: string | number; // Custom dialog width (e.g. "800px", "80vw", 600). Default: max-w-lg
847
+ }
848
+ ```
849
+
850
+ ```tsx
851
+ <Dialog isOpen={showDialog} title="Confirmar" onClose={() => setShowDialog(false)}
852
+ footer={
853
+ <>
854
+ <Button variant="ghost" onClick={() => setShowDialog(false)}>Cancelar</Button>
855
+ <Button variant="primary" color="danger" onClick={handleDelete}>Eliminar</Button>
856
+ </>
857
+ }
858
+ >
859
+ <p>¿Está seguro que desea eliminar este registro?</p>
860
+ </Dialog>
861
+ ```
862
+
863
+ ### Loader
864
+
865
+ Loading indicator with progress bar. Can wrap content with overlay.
866
+
867
+ ```typescript
868
+ interface LoaderProps {
869
+ isLoading?: boolean; // default: false
870
+ text?: string; // Text below progress bar
871
+ children?: React.ReactNode;
872
+ keepContentWhileLoading?: boolean; // Show content faded at 50% opacity
873
+ contentLoadingNode?: React.ReactNode; // Custom loading content
874
+ overlayClassName?: string; // default: "bg-black/50 backdrop-blur-sm"
875
+ }
876
+ ```
877
+
878
+ ```tsx
879
+ <Loader isLoading={loading} text="Cargando datos...">
880
+ <DataTable ... />
881
+ </Loader>
882
+ <Loader isLoading={loading} keepContentWhileLoading>
883
+ <Card>...</Card>
884
+ </Loader>
885
+ ```
886
+
887
+ ### FiltersDialog
888
+
889
+ Dialog that groups multiple Filter components. Syncs values from/to URL search params.
890
+
891
+ ```typescript
892
+ interface FilterConfig {
893
+ filterType: "text" | "number" | "date" | "autocomplete";
894
+ paramName: string; // required
895
+ label?: string;
896
+ staticOptions?: Array<{ text: string; value: string }>;
897
+ inputWidth?: string;
898
+ min?: number; // For number filters
899
+ max?: number; // For number filters
900
+ options?: any[]; // For autocomplete
901
+ getOptionLabel?: (item: any) => string;
902
+ getOptionValue?: (item: any) => any;
903
+ renderOption?: (item: any) => React.ReactNode;
904
+ noResultsText?: string;
905
+ }
906
+
907
+ interface FiltersDialogProps {
908
+ filters: FilterConfig[]; // required
909
+ }
910
+ ```
911
+
912
+ ```tsx
913
+ <FiltersDialog filters={[
914
+ { filterType: "text", paramName: "nombre", label: "Nombre" },
915
+ { filterType: "number", paramName: "edad", label: "Edad", min: 0, max: 120 },
916
+ { filterType: "date", paramName: "fecha", label: "Fecha" },
917
+ { filterType: "autocomplete", paramName: "estado", label: "Estado",
918
+ options: [{ label: "Activo", value: "1" }, { label: "Inactivo", value: "0" }] },
919
+ ]} />
920
+ ```
921
+
922
+ ### Snackbar / SnackbarContainer
923
+
924
+ Toast notification system. SnackbarContainer must be at the app root.
925
+
926
+ ```typescript
927
+ interface SnackbarContainerProps {
928
+ position?: "top-right" | "top-left" | "bottom-right" | "bottom-left" | "top-center" | "bottom-center"; // default: "top-right"
929
+ maxSnackbars?: number; // default: 5
930
+ }
931
+
932
+ // Usage via hook (not direct Snackbar component):
933
+ const { showSnackbar } = useSnackbar();
934
+ showSnackbar("Operación exitosa", "success");
935
+ showSnackbar("Error al guardar", "danger", { duration: 5000, icon: "fa-exclamation" });
936
+ ```
937
+
938
+ **Variants**: `"primary"` | `"secondary"` | `"success"` | `"warning"` | `"danger"` | `"info"`
939
+ **Default icons**: success=fa-check-circle, danger=fa-times-circle, warning=fa-exclamation-triangle, info/primary/secondary=fa-info-circle
940
+
941
+ ### Skeleton
942
+
943
+ Loading placeholder with pulse animation. Fully customizable via className.
944
+
945
+ ```typescript
946
+ interface SkeletonProps {
947
+ className?: string; // Tailwind classes to control width, height, shape
948
+ }
949
+ ```
950
+
951
+ ```tsx
952
+ <Skeleton className="h-4 w-3/4" /> {/* Text line */}
953
+ <Skeleton className="h-10 w-full" /> {/* Input placeholder */}
954
+ <Skeleton className="h-32 w-32 rounded-full" /> {/* Avatar placeholder */}
955
+ ```
956
+
957
+ ### ThemeSwitcher
958
+
959
+ Self-contained theme toggle. No props. Displays available themes with switch buttons and current theme info.
960
+
961
+ ```tsx
962
+ <ThemeSwitcher />
963
+ ```
964
+
965
+ ---
966
+
967
+ ## Contexts & State Management
968
+
969
+ ### ThemeProvider / useTheme
970
+
971
+ Manages application theme with CSS variable injection, presets, and localStorage persistence.
972
+
973
+ ```typescript
974
+ type Density = "comfortable" | "compact" | "dense";
975
+
976
+ // Provider props
977
+ interface ThemeProviderProps {
978
+ children: ReactNode;
979
+ initialTheme?: string | Theme; // default: "light"
980
+ storageKey?: string; // localStorage key. default: "flysoft-theme"
981
+ forceInitialTheme?: boolean; // Ignore localStorage. default: false
982
+ onThemeChange?: (theme: Theme) => void;
983
+ density?: Density; // Global density. default: "comfortable"
984
+ densityStorageKey?: string; // default: "flysoft-density"
985
+ forceInitialDensity?: boolean; // default: false
986
+ onDensityChange?: (density: Density) => void;
987
+ }
988
+
989
+ // Hook return
990
+ interface ThemeContextType {
991
+ theme: Theme; // Current theme object
992
+ setTheme: (theme: Theme | string) => void; // Switch theme by name or object
993
+ updateTheme: (updates: Partial<Theme> | ((prev: Theme) => Theme)) => void;
994
+ currentThemeName: string;
995
+ availableThemes: string[]; // ["light", "dark", "blue", "green"]
996
+ resetToDefault: () => void;
997
+ isDark: boolean;
998
+ density: Density;
999
+ setDensity: (density: Density) => void;
1000
+ }
1001
+ ```
1002
+
1003
+ ```tsx
1004
+ // App root - default density
1005
+ <ThemeProvider initialTheme="light">
1006
+ <App />
1007
+ </ThemeProvider>
1008
+
1009
+ // App root - data-heavy app (CRUD admin, dashboards)
1010
+ <ThemeProvider initialTheme="light" density="dense">
1011
+ <App />
1012
+ </ThemeProvider>
1013
+
1014
+ // Runtime toggle
1015
+ const { theme, setTheme, isDark, density, setDensity } = useTheme();
1016
+ <Button onClick={() => setTheme(isDark ? "light" : "dark")}>Toggle Theme</Button>
1017
+ <Button onClick={() => setDensity(density === "dense" ? "comfortable" : "dense")}>
1018
+ Toggle Density
1019
+ </Button>
1020
+ ```
1021
+
1022
+ **Preset themes**: `lightTheme`, `darkTheme`, `blueTheme`, `greenTheme` (importable).
1023
+ **Density presets**: `comfortableDensity`, `compactDensity`, `denseDensity`, `densityPresets` (importable).
1024
+
1025
+ **Density CSS variables** (inyectadas automáticamente según la densidad activa):
1026
+ `--flysoft-density-padding-x-{sm|md|lg}`, `--flysoft-density-padding-y-{sm|md|lg}`,
1027
+ `--flysoft-density-container-padding-{x|y}`,
1028
+ `--flysoft-density-gap-{sm|md|lg}`, `--flysoft-density-font-{xs|sm|base|lg|xl}`,
1029
+ `--flysoft-density-control-height-{sm|md|lg}`, `--flysoft-density-datatable-row`,
1030
+ `--flysoft-density-datatable-header`, `--flysoft-density-card-gap`.
1031
+
1032
+ **Componentes que ya consumen densidad automáticamente** (sin necesidad de prop):
1033
+ Card, DataField, Collection, Button, LinkButton, Input, AutocompleteInput,
1034
+ SearchSelectInput, DateInput, CurrencyInput, DatePicker, DataTable, Dialog,
1035
+ Filter (incluye los paneles flotantes), FiltersDialog, Accordion, Menu,
1036
+ DropdownMenu, DropdownPanel, TabsGroup, Badge, Checkbox, RadioButtonGroup,
1037
+ Pagination, Avatar, RoadMap, Snackbar, Skeleton, Loader. **Toda la librería
1038
+ es density-aware.** El default `comfortable` preserva el aspecto previo de
1039
+ cada componente, así que los consumidores existentes no ven cambios visuales
1040
+ hasta que activan `density="compact"` o `density="dense"`.
1041
+
1042
+ **Tipografía global**: dentro del wrapper `.flysoft-theme-reset` (cualquier
1043
+ ThemeProvider/AppLayoutProvider lo crea automáticamente), los headings y
1044
+ elementos de texto sin clase específica escalan con densidad:
1045
+ - `h1` = `font-xl × 1.5`, `h2` = `font-xl × 1.25`, `h3` = `font-xl`,
1046
+ `h4` = `font-lg`, `h5` = `font-base`, `h6` = `font-sm`
1047
+ - `p` = `font-base`, `small` = `font-xs`
1048
+ - `span`/`div` heredan `font-base` del wrapper
1049
+
1050
+ Las reglas son de baja specificity: cualquier `className` Tailwind (`text-lg`,
1051
+ `text-2xl`, etc.) o `style` inline las pisa.
1052
+
1053
+ **Componentes con prop `compact` como override local de densidad** (fuerzan
1054
+ preset compact en `--flysoft-density-*` dentro de sí y descendientes,
1055
+ ignorando la densidad global): Card, DataTable, Dialog, Filter, Accordion,
1056
+ Menu, DropdownMenu, DropdownPanel, TabsGroup.
1057
+
1058
+ **Override de fondo/estilos en form-controls vía `className`**: los form-controls
1059
+ (Input, CurrencyInput, DateInput, AutocompleteInput, SearchSelectInput, Button,
1060
+ LinkButton, Checkbox, RadioButtonGroup, DatePicker) combinan sus clases con
1061
+ `twMerge`, así que un `className` con clase en conflicto pisa la default de forma
1062
+ confiable. Para cambiar el fondo por defecto (`bg-[var(--color-bg-default)]`) —por
1063
+ ej. cuando el control va sobre una Card del mismo color— pasá un `bg-*`:
1064
+ `<Input className="bg-[var(--color-bg-secondary)]" />` o `<Input className="bg-[#f5f5f5]" />`.
1065
+ El `Filter` no toma `className` para esto; usa su prop `bgColor`.
1066
+
1067
+ ### AuthProvider / AuthContext
1068
+
1069
+ Manages authentication with automatic token validation and refresh.
1070
+
1071
+ ```typescript
1072
+ interface AuthProviderProps {
1073
+ children: React.ReactNode;
1074
+ getToken: (username: string, password: string) => Promise<AuthTokenInterface>; // required
1075
+ getUserData: (auth: AuthTokenInterface) => Promise<AuthContextUserInterface>; // required
1076
+ refreshToken?: (auth: AuthTokenInterface) => Promise<AuthTokenInterface>;
1077
+ removeToken?: (auth: AuthTokenInterface) => Promise<void>;
1078
+ showLog?: boolean; // default: false
1079
+ }
1080
+
1081
+ interface AuthContextType {
1082
+ user: AuthContextUserInterface | null;
1083
+ login: (username: string, password: string) => Promise<void>;
1084
+ logout: () => void;
1085
+ isAuthenticated: boolean;
1086
+ isLoading: boolean;
1087
+ }
1088
+
1089
+ interface AuthContextUserInterface {
1090
+ id?: number | string;
1091
+ name?: string;
1092
+ aditionalData?: any;
1093
+ token?: AuthTokenInterface;
1094
+ }
1095
+
1096
+ interface AuthTokenInterface {
1097
+ accessToken?: string;
1098
+ expires?: string; // ISO 8601
1099
+ tokenType?: string;
1100
+ refreshToken?: string;
1101
+ aditionalData?: any;
1102
+ }
1103
+ ```
1104
+
1105
+ ```tsx
1106
+ <AuthProvider
1107
+ getToken={async (user, pass) => {
1108
+ const res = await apiClient.post({ url: "/auth/login", body: { user, pass } });
1109
+ return res.token;
1110
+ }}
1111
+ getUserData={async (auth) => {
1112
+ return await apiClient.get({ url: "/auth/me" });
1113
+ }}
1114
+ refreshToken={async (auth) => {
1115
+ return await apiClient.post({ url: "/auth/refresh", body: { token: auth.refreshToken } });
1116
+ }}
1117
+ >
1118
+ <App />
1119
+ </AuthProvider>
1120
+
1121
+ // In components
1122
+ const { user, login, logout, isAuthenticated } = useContext(AuthContext);
1123
+ ```
1124
+
1125
+ **Behaviors**: Validates token on mount. Checks expiration every 60s. Auto-refreshes if `refreshToken` provided. Stores in localStorage as `"auth"`.
1126
+
1127
+ ### CrudProvider / useCrud\<T\>
1128
+
1129
+ Generic CRUD context with automatic pagination, URL parameter sync, and snackbar notifications.
1130
+
1131
+ ```typescript
1132
+ interface CrudProviderProps<T> {
1133
+ children: ReactNode;
1134
+ getPromise?: Function | { execute: Function; successMessage?: string; errorMessage?: string | ((error: any) => string) };
1135
+ getItemPromise?: Function | { execute: Function; successMessage?: string; errorMessage?: string | ((error: any) => string) };
1136
+ postPromise?: Function | { execute: Function; successMessage?: string; errorMessage?: string | ((error: any) => string) };
1137
+ putPromise?: Function | { execute: Function; successMessage?: string; errorMessage?: string | ((error: any) => string) };
1138
+ deletePromise?: Function | { execute: Function; successMessage?: string; errorMessage?: string | ((error: any) => string) };
1139
+ urlParams?: Array<string>; // URL params to watch. default: []
1140
+ limit?: number; // Items per page. default: 15
1141
+ pageParam?: string; // URL page param. default: "pagina"
1142
+ singleItemId?: string | number;
1143
+ extraData?: Record<string, any>;
1144
+ }
1145
+
1146
+ interface CrudContextType<T> {
1147
+ list: Array<T> | undefined;
1148
+ item: T | undefined;
1149
+ page: number;
1150
+ pages: number;
1151
+ total: number;
1152
+ limit: number;
1153
+ isLoading: boolean;
1154
+ pagination: ReactNode; // Pre-built Pagination component
1155
+ params: Record<string, any>;
1156
+ extraData?: Record<string, any>;
1157
+ setExtraData: Dispatch<SetStateAction<Record<string, any> | undefined>>;
1158
+ fetchItems: { execute: (params?: Record<string, any>) => Promise<void>; isLoading: boolean };
1159
+ fetchItem: { execute: (params?: Record<string, any> | string | number) => Promise<T | undefined>; isLoading: boolean };
1160
+ createItem: { execute: (item: T) => Promise<T | undefined | null>; isLoading: boolean };
1161
+ updateItem: { execute: (item: T) => Promise<T | undefined | null>; isLoading: boolean };
1162
+ deleteItem: { execute: (item: T) => Promise<void>; isLoading: boolean };
1163
+ }
1164
+ ```
1165
+
1166
+ ```tsx
1167
+ <CrudProvider<User>
1168
+ getPromise={(params) => apiClient.get({ url: "/api/users", params })}
1169
+ getItemPromise={(id) => apiClient.get({ url: `/api/users/${id}` })}
1170
+ postPromise={{ execute: (item) => apiClient.post({ url: "/api/users", body: item }), successMessage: "Usuario creado" }}
1171
+ putPromise={{ execute: (item) => apiClient.put({ url: `/api/users/${item.id}`, body: item }), successMessage: "Usuario actualizado" }}
1172
+ deletePromise={{ execute: (item) => apiClient.del({ url: `/api/users/${item.id}` }), successMessage: "Usuario eliminado" }}
1173
+ urlParams={["nombre", "estado"]}
1174
+ limit={20}
1175
+ >
1176
+ <UserList />
1177
+ </CrudProvider>
1178
+
1179
+ // In child components
1180
+ const { list, isLoading, pagination, createItem, deleteItem } = useCrud<User>();
1181
+ ```
1182
+
1183
+ **Behaviors**: Auto-fetches when URL params change. Resets pagination on filter change. Shows snackbar on success/error.
1184
+
1185
+ ### SnackbarProvider / useSnackbar
1186
+
1187
+ Manages toast notifications.
1188
+
1189
+ ```typescript
1190
+ interface SnackbarActionsType {
1191
+ showSnackbar: (
1192
+ message: string,
1193
+ variant?: SnackbarVariant,
1194
+ options?: { duration?: number; icon?: string; iconLabel?: string }
1195
+ ) => void;
1196
+ removeSnackbar: (id: string) => void;
1197
+ }
1198
+ ```
1199
+
1200
+ ```tsx
1201
+ // App root
1202
+ <SnackbarProvider>
1203
+ <SnackbarContainer position="bottom-right" maxSnackbars={3} />
1204
+ <App />
1205
+ </SnackbarProvider>
1206
+
1207
+ // In components
1208
+ const { showSnackbar } = useSnackbar();
1209
+ showSnackbar("Guardado exitosamente", "success");
1210
+ showSnackbar("Error de conexión", "danger", { duration: 5000 });
1211
+ ```
1212
+
1213
+ ### AppLayoutProvider / useAppLayout
1214
+
1215
+ Combines ThemeProvider + SnackbarProvider + AppLayout into a single provider.
1216
+
1217
+ ```typescript
1218
+ interface AppLayoutProviderProps {
1219
+ children: ReactNode;
1220
+ initialTheme?: string | Theme;
1221
+ storageKey?: string;
1222
+ forceInitialTheme?: boolean;
1223
+ // Densidad global (propagada al ThemeProvider interno)
1224
+ density?: "comfortable" | "compact" | "dense"; // default: "comfortable"
1225
+ densityStorageKey?: string; // default: "flysoft-density"
1226
+ forceInitialDensity?: boolean;
1227
+ onDensityChange?: (density: "comfortable" | "compact" | "dense") => void;
1228
+ initialNavbar?: NavbarInterface;
1229
+ initialLeftDrawer?: LeftDrawerInterface;
1230
+ initialContentFooter?: ReactNode;
1231
+ className?: string;
1232
+ }
1233
+
1234
+ interface AppLayoutContextType extends ThemeContextType {
1235
+ navbar: NavbarInterface | undefined;
1236
+ leftDrawer: LeftDrawerInterface | undefined;
1237
+ contentFooter: ReactNode | undefined;
1238
+ className: string;
1239
+ setNavbar: Dispatch<SetStateAction<NavbarInterface | undefined>>;
1240
+ setLeftDrawer: Dispatch<SetStateAction<LeftDrawerInterface | undefined>>;
1241
+ setContentFooter: (node: ReactNode | undefined) => void;
1242
+ setClassName: (className: string) => void;
1243
+ setNavBarLeftNode: (node: ReactNode | undefined) => void;
1244
+ setNavbarRightNode: (node: ReactNode | undefined) => void;
1245
+ // Left drawer commands (same state as useLeftDrawer())
1246
+ isLeftDrawerOpen: boolean;
1247
+ openLeftDrawer: () => void;
1248
+ closeLeftDrawer: () => void;
1249
+ toggleLeftDrawer: () => void;
1250
+ }
1251
+ ```
1252
+
1253
+ ```tsx
1254
+ <AppLayoutProvider
1255
+ initialTheme="light"
1256
+ density="dense" // CRUDs / dashboards / pantallas con mucha info
1257
+ initialNavbar={{ navBarLeftNode: <h1>Mi App</h1>, fullWidthNavbar: true }}
1258
+ initialLeftDrawer={{ contentNode: <nav>...</nav> }}
1259
+ >
1260
+ <Routes />
1261
+ </AppLayoutProvider>
1262
+
1263
+ // In pages - dynamically update layout
1264
+ const { setNavBarLeftNode, setNavbarRightNode } = useAppLayout();
1265
+ useEffect(() => {
1266
+ setNavBarLeftNode(<h1>Dashboard</h1>);
1267
+ }, []);
1268
+
1269
+ // Close the mobile drawer from anywhere inside the layout
1270
+ const { closeLeftDrawer } = useAppLayout(); // or useLeftDrawer()
1271
+ <LinkButton to="/clientes" onClick={closeLeftDrawer}>Clientes</LinkButton>
1272
+ ```
1273
+
1274
+ ---
1275
+
1276
+ ## Hooks
1277
+
1278
+ ### useThemeOverride
1279
+
1280
+ Applies granular CSS variable overrides without changing the entire theme.
1281
+
1282
+ ```typescript
1283
+ function useThemeOverride(options?: {
1284
+ scope?: "global" | "local"; // default: "global"
1285
+ element?: HTMLElement | null;
1286
+ prefix?: string; // default: "flysoft"
1287
+ }): {
1288
+ applyOverride: (overrides: Record<string, string | number>) => void;
1289
+ revertOverride: (keys: string[]) => void;
1290
+ revertAllOverrides: () => void;
1291
+ getCSSVariable: (key: string) => string | null;
1292
+ isOverrideApplied: (key: string) => boolean;
1293
+ appliedOverridesCount: number;
1294
+ }
1295
+ ```
1296
+
1297
+ ### useTemporaryOverride
1298
+
1299
+ Applies CSS variable overrides that auto-revert after a duration.
1300
+
1301
+ ```typescript
1302
+ function useTemporaryOverride(
1303
+ overrides: Record<string, string | number>,
1304
+ duration?: number, // default: 3000
1305
+ options?: { scope?: "global" | "local"; element?: HTMLElement | null; prefix?: string }
1306
+ ): { applyTemporaryOverride: () => Function }
1307
+ ```
1308
+
1309
+ ### useBreakpoint
1310
+
1311
+ Returns current viewport breakpoint and device type.
1312
+
1313
+ ```typescript
1314
+ type Breakpoint = "xs" | "sm" | "md" | "lg" | "xl" | "2xl";
1315
+
1316
+ function useBreakpoint(): {
1317
+ breakpoint: Breakpoint;
1318
+ windowSize: { width: number; height: number };
1319
+ isMobile: boolean; // xs or sm
1320
+ isTablet: boolean; // md
1321
+ isDesktop: boolean; // lg, xl, or 2xl
1322
+ }
1323
+ ```
1324
+
1325
+ ### useElementScroll
1326
+
1327
+ Tracks scroll position and direction with requestAnimationFrame optimization.
1328
+
1329
+ ```typescript
1330
+ function useElementScroll(elementRef: React.RefObject<HTMLElement | null>): {
1331
+ scrollY: number;
1332
+ scrollDirection: "up" | "down" | null;
1333
+ }
1334
+ ```
1335
+
1336
+ ### useAsyncRequest
1337
+
1338
+ Manages async operations with loading state and snackbar notifications.
1339
+
1340
+ ```typescript
1341
+ interface AsyncRequestOptions {
1342
+ successMessage?: string;
1343
+ errorMessage?: string | ((error: any) => string);
1344
+ successVariant?: SnackbarVariant; // default: "success"
1345
+ errorVariant?: SnackbarVariant; // default: "danger"
1346
+ onSuccess?: (data: any) => void;
1347
+ onError?: (error: any) => void;
1348
+ onFinally?: () => void;
1349
+ }
1350
+
1351
+ function useAsyncRequest(options?: AsyncRequestOptions): {
1352
+ isLoading: boolean;
1353
+ execute: <T>(requestFn: () => Promise<T>) => Promise<T | undefined>;
1354
+ setLoading: (loading: boolean) => void;
1355
+ }
1356
+ ```
1357
+
1358
+ ```tsx
1359
+ const { execute, isLoading } = useAsyncRequest({
1360
+ successMessage: "Guardado exitosamente",
1361
+ errorMessage: (err) => getErrorMessage(err),
1362
+ });
1363
+ await execute(() => apiClient.post({ url: "/api/data", body: formData }));
1364
+ ```
1365
+
1366
+ ### useEnum
1367
+
1368
+ Converts TypeScript enums to arrays for form select options.
1369
+
1370
+ ```typescript
1371
+ function useEnum(baseEnum: any): {
1372
+ getArray: () => Array<NameValueInterface<number>>;
1373
+ getInstance: (id: number) => NameValueInterface<number> | undefined;
1374
+ }
1375
+ ```
1376
+
1377
+ ### useGlobalThemeStyles
1378
+
1379
+ Applies theme colors to `<body>` and `<html>` for full-page theming. No return value.
1380
+
1381
+ ```tsx
1382
+ function useGlobalThemeStyles(): void;
1383
+ ```
1384
+
1385
+ ---
1386
+
1387
+ ## Services
1388
+
1389
+ ### apiClient
1390
+
1391
+ Singleton HTTP client (Axios-based) with automatic Bearer token injection.
1392
+
1393
+ ```typescript
1394
+ // Main methods
1395
+ apiClient.get<T>(options: { url: string; params?: Record<string, unknown>; headers?: Record<string, string> }): Promise<T>;
1396
+ apiClient.post<T>(options: { url: string; body?: unknown; headers?: Record<string, string> }): Promise<T>;
1397
+ apiClient.put<T>(options: { url: string; body?: unknown; headers?: Record<string, string> }): Promise<T>;
1398
+ apiClient.patch<T>(options: { url: string; body?: unknown; headers?: Record<string, string> }): Promise<T>;
1399
+ apiClient.del<T>(options: { url: string; headers?: Record<string, string> }): Promise<T>;
1400
+
1401
+ // File operations
1402
+ apiClient.getFile(options): Promise<{ data: Blob; headers: any }>;
1403
+ apiClient.getFileAsUrl(options): Promise<string>;
1404
+ apiClient.openFile(options): Promise<void>;
1405
+ apiClient.downloadFile(options): Promise<void>;
1406
+ apiClient.uploadFile<T>(options: { url: string; files: FileList | File[]; headers?: { paramName?: string } }): Promise<T>;
1407
+
1408
+ // Token management
1409
+ setApiClientTokenProvider(provider?: () => string | undefined): void;
1410
+ clearApiClientTokenProvider(): void;
1411
+
1412
+ // Create isolated instances
1413
+ createApiClient(config?: { baseURL?: string; timeout?: number; headers?: Record<string, string> }): ApiClientService;
1414
+ ```
1415
+
1416
+ ```tsx
1417
+ // Setup token globally
1418
+ setApiClientTokenProvider(() => user?.token?.accessToken);
1419
+
1420
+ // API calls
1421
+ const users = await apiClient.get<User[]>({ url: "/api/users", params: { page: 1 } });
1422
+ await apiClient.post({ url: "/api/users", body: { name: "Juan" } });
1423
+ await apiClient.downloadFile({ url: "/api/reports/pdf" });
1424
+ await apiClient.uploadFile({ url: "/api/upload", files: fileInput.files });
1425
+ ```
1426
+
1427
+ ---
1428
+
1429
+ ## Helpers
1430
+
1431
+ | Function | Signature | Description |
1432
+ |----------|-----------|-------------|
1433
+ | `currencyFormat` | `(value: number) => string` | Formats as `"1.234,56"` (es-AR locale) |
1434
+ | `getErrorMessage` | `(error: any) => string` | Extracts message from AxiosError. Default: `"Ha ocurrido un error"` |
1435
+ | `getInitialLetters` | `(text: string) => string` | `"Juan Pérez"` `"JP"` |
1436
+ | `getQueryString` | `(params: URLSearchParams, newParams: any) => string` | Merges params, returns `"?key=value"` |
1437
+ | `objectToQueryString` | `(source: any) => string` | Object to `"a=1&b=2"` (no leading `?`) |
1438
+ | `queryStringToObject` | `(params: string) => Record<string, string>` | `"a=1&b=2"` `{a: "1", b: "2"}` |
1439
+ | `nameValueArrayToObject` | `<T>(arr: NameValueInterface<T>[]) => Record<string, T>` | Array of {name, value} to object |
1440
+ | `promiseMapper` | `<T, K>(promise, mapper) => Promise<K \| K[] \| PaginationInterface<K>>` | Maps promise results (arrays, pagination, single) |
1441
+ | `RegularExpressions` | Object | `.email`, `.dateString`, `.password(config)` regex patterns |
1442
+
1443
+ ## Interfaces
1444
+
1445
+ ```typescript
1446
+ interface NameValueInterface<T> {
1447
+ name: string;
1448
+ value: T;
1449
+ extras?: any;
1450
+ }
1451
+
1452
+ interface PaginationInterface<T> {
1453
+ list: Array<T>;
1454
+ limit: number;
1455
+ page: number;
1456
+ pages: number;
1457
+ total: number;
1458
+ }
1459
+ ```
1460
+
1461
+ ---
1462
+
1463
+ ## Templates
1464
+
1465
+ ### LoginForm
1466
+
1467
+ ```typescript
1468
+ interface LoginFormProps {
1469
+ onSubmit?: (data: { email: string; password: string }) => void;
1470
+ loading?: boolean;
1471
+ error?: string;
1472
+ className?: string;
1473
+ }
1474
+ ```
1475
+
1476
+ ### RegistrationForm
1477
+
1478
+ ```typescript
1479
+ interface RegistrationFormProps {
1480
+ onSubmit?: (data: { firstName: string; lastName: string; email: string; password: string; confirmPassword: string }) => void;
1481
+ loading?: boolean;
1482
+ error?: string;
1483
+ className?: string;
1484
+ }
1485
+ ```
1486
+
1487
+ ### ContactForm
1488
+
1489
+ ```typescript
1490
+ interface ContactFormProps {
1491
+ onSubmit?: (data: { name: string; email: string; subject: string; message: string }) => void;
1492
+ loading?: boolean;
1493
+ success?: boolean;
1494
+ error?: string;
1495
+ className?: string;
1496
+ }
1497
+ ```
1498
+
1499
+ ### DashboardLayout
1500
+
1501
+ ```typescript
1502
+ interface DashboardStat {
1503
+ title: string;
1504
+ value: string | number;
1505
+ change?: string; // e.g. "+12%"
1506
+ changeType?: "positive" | "negative" | "neutral";
1507
+ icon?: string;
1508
+ }
1509
+
1510
+ interface DashboardLayoutProps {
1511
+ title: string; // required
1512
+ subtitle?: string;
1513
+ stats?: DashboardStat[];
1514
+ actions?: React.ReactNode;
1515
+ children: React.ReactNode; // required
1516
+ className?: string;
1517
+ }
1518
+ ```
1519
+
1520
+ ### SidebarLayout
1521
+
1522
+ ```typescript
1523
+ interface MenuItem {
1524
+ label: string;
1525
+ icon: string;
1526
+ href: string;
1527
+ badge?: string | number;
1528
+ children?: MenuItem[];
1529
+ }
1530
+
1531
+ interface User {
1532
+ name: string;
1533
+ email?: string;
1534
+ avatar?: string;
1535
+ }
1536
+
1537
+ interface SidebarLayoutProps {
1538
+ title: string; // required
1539
+ menuItems: MenuItem[]; // required
1540
+ user: User; // required
1541
+ children: React.ReactNode; // required
1542
+ className?: string;
1543
+ onLogout?: () => void;
1544
+ }
1545
+ ```
1546
+
1547
+ ### FormPattern
1548
+
1549
+ ```typescript
1550
+ interface FormField {
1551
+ name: string;
1552
+ label: string;
1553
+ type?: string; // default: "text"
1554
+ placeholder?: string;
1555
+ icon?: string;
1556
+ required?: boolean;
1557
+ validation?: (value: string) => string | undefined;
1558
+ multiline?: boolean;
1559
+ rows?: number; // default: 4
1560
+ }
1561
+
1562
+ interface FormPatternProps {
1563
+ title: string; // required
1564
+ subtitle?: string;
1565
+ fields: FormField[]; // required
1566
+ onSubmit: (data: Record<string, string>) => void; // required
1567
+ submitText?: string; // default: "Enviar"
1568
+ submitIcon?: string; // default: "fa-paper-plane"
1569
+ loading?: boolean;
1570
+ error?: string;
1571
+ success?: boolean;
1572
+ className?: string;
1573
+ gridCols?: 1 | 2; // default: 1
1574
+ }
1575
+ ```