flysoft-react-ui 1.2.10 → 1.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/AI_CONTEXT.md +1533 -1401
  2. package/AI_INTEGRATION_GUIDE.md +358 -343
  3. package/README.md +498 -464
  4. package/dist/components/form-controls/Button.d.ts.map +1 -1
  5. package/dist/components/form-controls/Checkbox.d.ts.map +1 -1
  6. package/dist/components/form-controls/CurrencyInput.d.ts.map +1 -1
  7. package/dist/components/form-controls/DatePicker.d.ts.map +1 -1
  8. package/dist/components/form-controls/Input.d.ts.map +1 -1
  9. package/dist/components/form-controls/LinkButton.d.ts.map +1 -1
  10. package/dist/components/form-controls/Pagination.d.ts.map +1 -1
  11. package/dist/components/form-controls/RadioButtonGroup.d.ts.map +1 -1
  12. package/dist/components/layout/Accordion.d.ts +8 -0
  13. package/dist/components/layout/Accordion.d.ts.map +1 -1
  14. package/dist/components/layout/Card.d.ts +5 -0
  15. package/dist/components/layout/Card.d.ts.map +1 -1
  16. package/dist/components/layout/Collection.d.ts +17 -1
  17. package/dist/components/layout/Collection.d.ts.map +1 -1
  18. package/dist/components/layout/DataField.d.ts +19 -0
  19. package/dist/components/layout/DataField.d.ts.map +1 -1
  20. package/dist/components/layout/DataTable.d.ts.map +1 -1
  21. package/dist/components/layout/DropdownMenu.d.ts.map +1 -1
  22. package/dist/components/layout/DropdownPanel.d.ts.map +1 -1
  23. package/dist/components/layout/Filter.d.ts +8 -0
  24. package/dist/components/layout/Filter.d.ts.map +1 -1
  25. package/dist/components/layout/Menu.d.ts.map +1 -1
  26. package/dist/components/layout/TabsGroup.d.ts.map +1 -1
  27. package/dist/components/utils/Avatar.d.ts.map +1 -1
  28. package/dist/components/utils/Badge.d.ts.map +1 -1
  29. package/dist/components/utils/Dialog.d.ts.map +1 -1
  30. package/dist/components/utils/Loader.d.ts.map +1 -1
  31. package/dist/components/utils/RoadMap.d.ts.map +1 -1
  32. package/dist/components/utils/Snackbar.d.ts.map +1 -1
  33. package/dist/contexts/AppLayoutContext.d.ts +5 -1
  34. package/dist/contexts/AppLayoutContext.d.ts.map +1 -1
  35. package/dist/contexts/ThemeContext.d.ts +11 -1
  36. package/dist/contexts/ThemeContext.d.ts.map +1 -1
  37. package/dist/contexts/index.d.ts +2 -2
  38. package/dist/contexts/index.d.ts.map +1 -1
  39. package/dist/contexts/presets.d.ts +5 -1
  40. package/dist/contexts/presets.d.ts.map +1 -1
  41. package/dist/contexts/types.d.ts +51 -0
  42. package/dist/contexts/types.d.ts.map +1 -1
  43. package/dist/index.css +1 -1
  44. package/dist/index.js +8020 -7426
  45. package/dist/index.js.map +1 -1
  46. package/package.json +1 -1
package/AI_CONTEXT.md CHANGED
@@ -1,1401 +1,1533 @@
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
- compact?: boolean; // Reduced padding. default: false
346
- }
347
- ```
348
-
349
- ```tsx
350
- <Card title="Usuarios" headerActions={<Button size="sm" icon="fa-plus">Nuevo</Button>}>
351
- <p>Contenido</p>
352
- </Card>
353
- <Card variant="elevated" compact footer={<Button variant="primary">Guardar</Button>}>
354
- <Input label="Nombre" />
355
- </Card>
356
- ```
357
-
358
- ### AppLayout
359
-
360
- Main application layout with responsive navbar and sidebar drawer.
361
-
362
- ```typescript
363
- interface AppLayoutProps {
364
- navbar?: NavbarInterface;
365
- leftDrawer?: LeftDrawerInterface;
366
- contentFooter?: React.ReactNode;
367
- children: React.ReactNode; // required
368
- className?: string;
369
- }
370
-
371
- interface NavbarInterface {
372
- navBarLeftNode?: React.ReactNode;
373
- navBarRightNode?: React.ReactNode;
374
- fullWidthNavbar?: boolean; // Fixed full-width (true) or relative (false)
375
- height?: string; // default: "64px"
376
- className?: string;
377
- }
378
-
379
- interface LeftDrawerInterface {
380
- headerNode?: React.ReactNode;
381
- contentNode?: React.ReactNode;
382
- footerNode?: React.ReactNode;
383
- className?: string;
384
- width?: string; // default: "256px"
385
- }
386
- ```
387
-
388
- ```tsx
389
- <AppLayout
390
- navbar={{
391
- navBarLeftNode: <h1>Mi App</h1>,
392
- navBarRightNode: <Avatar text="Admin" />,
393
- fullWidthNavbar: true,
394
- }}
395
- leftDrawer={{
396
- headerNode: <h2>Menú</h2>,
397
- contentNode: <nav>...</nav>,
398
- }}
399
- >
400
- <main>Contenido</main>
401
- </AppLayout>
402
- ```
403
-
404
- **Behaviors**: Navbar auto-hides/shows on scroll. Mobile drawer with overlay. Responsive breakpoints.
405
-
406
- ### Collection
407
-
408
- Flex container for rendering lists of items.
409
-
410
- ```typescript
411
- interface CollectionProps {
412
- children: React.ReactNode; // required
413
- gap?: string; // CSS gap value. default: "1rem"
414
- direction?: "column" | "row"; // default: "column"
415
- wrap?: boolean; // default: false
416
- className?: string;
417
- }
418
- ```
419
-
420
- ### DataField
421
-
422
- Label + value pair display for detail views.
423
-
424
- ```typescript
425
- interface DataFieldProps {
426
- label?: string;
427
- value?: string | number | React.ReactNode;
428
- inline?: boolean; // Horizontal layout. default: false
429
- align?: "left" | "right" | "center"; // default: "left"
430
- title?: string; // HTML title tooltip
431
- link?: string; // Opens URL in new tab
432
- className?: string;
433
- labelClassName?: string;
434
- }
435
- ```
436
-
437
- ```tsx
438
- <DataField label="Nombre" value="Juan Pérez" />
439
- <DataField label="Email" value="juan@email.com" link="mailto:juan@email.com" inline />
440
- ```
441
-
442
- ### TabsGroup / TabPanel
443
-
444
- Tabbed interfaces with optional URL persistence.
445
-
446
- ```typescript
447
- interface Tab {
448
- id: string | number;
449
- label: string;
450
- }
451
-
452
- interface TabsGroupProps {
453
- children?: React.ReactNode;
454
- tabs: Tab[]; // required
455
- paramName?: string; // URL search param for persistence
456
- headerNode?: React.ReactNode; // Right-aligned header content
457
- onChangeTab?: (selectedTab: string) => void;
458
- }
459
-
460
- interface TabPanelProps {
461
- children?: React.ReactNode;
462
- tabId: string | number; // Must match a Tab.id (required)
463
- }
464
- ```
465
-
466
- ```tsx
467
- <TabsGroup tabs={[{ id: "info", label: "Información" }, { id: "history", label: "Historial" }]}>
468
- <TabPanel tabId="info">
469
- <p>Información del usuario</p>
470
- </TabPanel>
471
- <TabPanel tabId="history">
472
- <p>Historial de actividad</p>
473
- </TabPanel>
474
- </TabsGroup>
475
- ```
476
-
477
- ### DataTable\<T\>
478
-
479
- High-performance data table with sorting, formatting, actions, and skeleton loading.
480
-
481
- ```typescript
482
- interface DataTableColumn<T> {
483
- align?: "left" | "right" | "center"; // Auto-set for date/currency/numeric
484
- width?: string;
485
- header?: string | React.ReactNode;
486
- footer?: string | React.ReactNode;
487
- value?: string | number | ((row: T) => string | React.ReactNode);
488
- tooltip?: (row: T) => string | React.ReactNode;
489
- type?: "text" | "numeric" | "currency" | "date";
490
- actions?: (row: T) => Array<React.ReactNode>;
491
- headerActions?: () => Array<React.ReactNode>;
492
- }
493
-
494
- interface DataTableProps<T> {
495
- columns: DataTableColumn<T>[]; // required
496
- rows: T[]; // required
497
- className?: string;
498
- maxRows?: number; // Enables sticky header with scroll
499
- locale?: string; // default: "es-AR"
500
- isLoading?: boolean; // Shows skeleton rows. default: false
501
- loadingRows?: number; // default: 5
502
- rowClassName?: (row: T) => string;
503
- headerClassName?: string;
504
- footerClassName?: string;
505
- headerCellClassName?: string;
506
- footerCellClassName?: string;
507
- cellClassName?: string | ((row: T, column: DataTableColumn<T>) => string);
508
- compact?: boolean; // default: false
509
- }
510
- ```
511
-
512
- ```tsx
513
- interface User { id: number; name: string; salary: number; createdAt: string; }
514
-
515
- const columns: DataTableColumn<User>[] = [
516
- { header: "ID", value: "id", width: "60px" },
517
- { header: "Nombre", value: (row) => row.name },
518
- { header: "Salario", value: "salary", type: "currency" },
519
- { header: "Fecha", value: "createdAt", type: "date" },
520
- {
521
- header: "Acciones",
522
- actions: (row) => [
523
- <Button key="edit" variant="ghost" size="sm" icon="fa-edit" onClick={() => edit(row)}>Editar</Button>,
524
- <Button key="del" variant="ghost" size="sm" icon="fa-trash" color="danger" onClick={() => del(row)}>Eliminar</Button>,
525
- ],
526
- },
527
- ];
528
-
529
- <DataTable<User> columns={columns} rows={users} isLoading={loading} maxRows={10} />
530
- ```
531
-
532
- **Type formatting**: `currency` → thousands separator, no symbol. `numeric` → locale formatting. `date` → DD/MM/YYYY.
533
-
534
- ### Accordion
535
-
536
- Collapsible content section with smooth animation.
537
-
538
- ```typescript
539
- interface AccordionProps {
540
- title: string | React.ReactNode; // required
541
- children: React.ReactNode; // required
542
- icon?: string; // FontAwesome icon
543
- rightNode?: React.ReactNode;
544
- defaultOpen?: boolean; // default: false
545
- className?: string;
546
- variant?: "default" | "elevated" | "outlined"; // default: "default"
547
- onToggle?: (isOpen: boolean) => void;
548
- }
549
- ```
550
-
551
- ```tsx
552
- <Accordion title="Detalles" icon="fa-info-circle" defaultOpen>
553
- <p>Contenido colapsable</p>
554
- </Accordion>
555
- ```
556
-
557
- ### Menu
558
-
559
- Simple menu list for displaying options.
560
-
561
- ```typescript
562
- interface MenuProps<T = { label: string }> {
563
- options: T[]; // required
564
- onOptionSelected: (item: T) => void; // required
565
- getOptionLabel?: (item: T) => string;
566
- renderOption?: (item: T) => React.ReactNode;
567
- className?: string;
568
- style?: React.CSSProperties;
569
- itemClassName?: string;
570
- }
571
- ```
572
-
573
- ### DropdownMenu
574
-
575
- Portal-based dropdown menu triggered by a button. Auto-positions above/below.
576
-
577
- ```typescript
578
- interface DropdownMenuProps<T = { label: string }> {
579
- options: T[]; // required
580
- onOptionSelected: (item: T) => void; // required
581
- renderNode?: React.ReactNode; // Custom trigger (default: ellipsis icon button)
582
- getOptionLabel?: (item: T) => string;
583
- renderOption?: (item: T) => React.ReactNode;
584
- replaceOnSingleOption?: boolean; // Show single option inline. default: false
585
- openOnHover?: boolean; // default: false
586
- }
587
- ```
588
-
589
- ```tsx
590
- <DropdownMenu
591
- options={[{ label: "Editar" }, { label: "Eliminar" }]}
592
- onOptionSelected={(item) => handleAction(item.label)}
593
- renderNode={<Button variant="ghost" icon="fa-cog" size="sm" />}
594
- />
595
- ```
596
-
597
- ### DropdownPanel
598
-
599
- Portal-based dropdown that renders arbitrary content (not a list).
600
-
601
- ```typescript
602
- interface DropdownPanelProps {
603
- renderNode?: React.ReactNode; // Custom trigger (default: ellipsis icon button)
604
- children: React.ReactNode; // required
605
- openOnHover?: boolean; // default: false
606
- }
607
- ```
608
-
609
- ### Filter
610
-
611
- Versatile filtering component with multiple filter types and optional URL persistence.
612
-
613
- ```typescript
614
- // Discriminated union by filterType
615
- type FilterProps =
616
- | TextFilterProps // filterType?: "text" (default)
617
- | NumberFilterProps // filterType: "number" (+ min?, max?)
618
- | DateFilterProps // filterType: "date"
619
- | AutocompleteFilterProps // filterType: "autocomplete" (+ options, multiple?)
620
- | SearchFilterProps // filterType: "search"
621
- | SearchSelectFilterProps // filterType: "searchSelect" (+ onSearchPromiseFn, onSingleSearchPromiseFn)
622
-
623
- // Common props for all filter types:
624
- interface BaseFilterProps {
625
- paramName?: string; // URL search param for persistence
626
- label?: string;
627
- staticOptions?: Array<{ text: string; value: string }>;
628
- inputWidth?: string;
629
- value?: string; // Controlled value
630
- onChange?: (value: string | undefined) => void;
631
- hideEmpty?: boolean; // default: false
632
- disabled?: boolean; // default: false
633
- }
634
- ```
635
-
636
- ```tsx
637
- <Filter filterType="text" paramName="nombre" label="Nombre" />
638
- <Filter filterType="number" paramName="edad" label="Edad" min={0} max={120} />
639
- <Filter filterType="date" paramName="fecha" label="Fecha" />
640
- <Filter
641
- filterType="autocomplete"
642
- paramName="estado"
643
- label="Estado"
644
- options={[{ label: "Activo", value: "1" }, { label: "Inactivo", value: "0" }]}
645
- />
646
- <Filter
647
- filterType="searchSelect"
648
- paramName="cliente"
649
- label="Cliente"
650
- onSearchPromiseFn={(text) => apiClient.get({ url: `/api/clients?q=${text}` })}
651
- onSingleSearchPromiseFn={(id) => apiClient.get({ url: `/api/clients/${id}` })}
652
- />
653
- ```
654
-
655
- ---
656
-
657
- ## Utility Components
658
-
659
- ### Badge
660
-
661
- Status/category label with variants and custom colors.
662
-
663
- ```typescript
664
- interface BadgeProps {
665
- children: React.ReactNode; // required
666
- variant?: "primary" | "secondary" | "success" | "warning" | "danger" | "info"; // default: "primary"
667
- size?: "sm" | "md" | "lg"; // default: "md"
668
- rounded?: boolean; // Full border radius. default: false
669
- className?: string;
670
- icon?: string;
671
- iconPosition?: "left" | "right"; // default: "left"
672
- iconLabel?: string; // aria-label for icon
673
- bg?: string; // Custom background color
674
- textColor?: string; // Custom text color
675
- onClick?: (event: React.MouseEvent<HTMLElement>) => void;
676
- }
677
- ```
678
-
679
- ```tsx
680
- <Badge variant="success" icon="fa-check">Activo</Badge>
681
- <Badge variant="danger" rounded>3</Badge>
682
- <Badge bg="#8b5cf6" textColor="#fff">Custom</Badge>
683
- ```
684
-
685
- ### Avatar
686
-
687
- User profile display with initials fallback when image fails.
688
-
689
- ```typescript
690
- interface AvatarProps {
691
- text: string; // Name for initials extraction (required)
692
- image?: string; // Image URL
693
- bgColor?: string; // default: "#4b5563"
694
- textColor?: string; // default: "#ffffff"
695
- size?: "sm" | "md" | "lg"; // default: "md" (sm=32px, md=40px, lg=48px)
696
- className?: string;
697
- }
698
- ```
699
-
700
- ```tsx
701
- <Avatar text="Juan Pérez" image="/avatars/juan.jpg" />
702
- <Avatar text="Admin User" bgColor="#3b82f6" size="lg" />
703
- ```
704
-
705
- ### RoadMap
706
-
707
- Progress/stage visualization with connected circles and gradient lines.
708
-
709
- ```typescript
710
- interface RoadMapStage {
711
- name: string; // required
712
- description?: string;
713
- icon?: string;
714
- disabled?: boolean; // Grayed out at 50% opacity
715
- variant?: "primary" | "secondary" | "success" | "warning" | "danger" | "info";
716
- bg?: string; // Custom color (overrides variant)
717
- }
718
-
719
- interface RoadMapProps {
720
- stages: RoadMapStage[]; // required
721
- className?: string;
722
- }
723
- ```
724
-
725
- ```tsx
726
- <RoadMap stages={[
727
- { name: "Creado", icon: "fa-plus", variant: "info" },
728
- { name: "En Proceso", icon: "fa-cog", variant: "warning" },
729
- { name: "Completado", icon: "fa-check", variant: "success" },
730
- { name: "Archivado", icon: "fa-archive", disabled: true },
731
- ]} />
732
- ```
733
-
734
- ### Dialog
735
-
736
- Modal window with overlay, escape-to-close, and scroll lock.
737
-
738
- ```typescript
739
- interface DialogProps {
740
- isOpen: boolean; // required
741
- title: React.ReactNode; // required
742
- children: React.ReactNode; // required
743
- footer?: React.ReactNode;
744
- onClose?: () => void;
745
- closeOnOverlayClick?: boolean; // default: false
746
- compact?: boolean; // default: false
747
- bodyWidth?: string | number; // Custom dialog width (e.g. "800px", "80vw", 600). Default: max-w-lg
748
- }
749
- ```
750
-
751
- ```tsx
752
- <Dialog isOpen={showDialog} title="Confirmar" onClose={() => setShowDialog(false)}
753
- footer={
754
- <>
755
- <Button variant="ghost" onClick={() => setShowDialog(false)}>Cancelar</Button>
756
- <Button variant="primary" color="danger" onClick={handleDelete}>Eliminar</Button>
757
- </>
758
- }
759
- >
760
- <p>¿Está seguro que desea eliminar este registro?</p>
761
- </Dialog>
762
- ```
763
-
764
- ### Loader
765
-
766
- Loading indicator with progress bar. Can wrap content with overlay.
767
-
768
- ```typescript
769
- interface LoaderProps {
770
- isLoading?: boolean; // default: false
771
- text?: string; // Text below progress bar
772
- children?: React.ReactNode;
773
- keepContentWhileLoading?: boolean; // Show content faded at 50% opacity
774
- contentLoadingNode?: React.ReactNode; // Custom loading content
775
- overlayClassName?: string; // default: "bg-black/50 backdrop-blur-sm"
776
- }
777
- ```
778
-
779
- ```tsx
780
- <Loader isLoading={loading} text="Cargando datos...">
781
- <DataTable ... />
782
- </Loader>
783
- <Loader isLoading={loading} keepContentWhileLoading>
784
- <Card>...</Card>
785
- </Loader>
786
- ```
787
-
788
- ### FiltersDialog
789
-
790
- Dialog that groups multiple Filter components. Syncs values from/to URL search params.
791
-
792
- ```typescript
793
- interface FilterConfig {
794
- filterType: "text" | "number" | "date" | "autocomplete";
795
- paramName: string; // required
796
- label?: string;
797
- staticOptions?: Array<{ text: string; value: string }>;
798
- inputWidth?: string;
799
- min?: number; // For number filters
800
- max?: number; // For number filters
801
- options?: any[]; // For autocomplete
802
- getOptionLabel?: (item: any) => string;
803
- getOptionValue?: (item: any) => any;
804
- renderOption?: (item: any) => React.ReactNode;
805
- noResultsText?: string;
806
- }
807
-
808
- interface FiltersDialogProps {
809
- filters: FilterConfig[]; // required
810
- }
811
- ```
812
-
813
- ```tsx
814
- <FiltersDialog filters={[
815
- { filterType: "text", paramName: "nombre", label: "Nombre" },
816
- { filterType: "number", paramName: "edad", label: "Edad", min: 0, max: 120 },
817
- { filterType: "date", paramName: "fecha", label: "Fecha" },
818
- { filterType: "autocomplete", paramName: "estado", label: "Estado",
819
- options: [{ label: "Activo", value: "1" }, { label: "Inactivo", value: "0" }] },
820
- ]} />
821
- ```
822
-
823
- ### Snackbar / SnackbarContainer
824
-
825
- Toast notification system. SnackbarContainer must be at the app root.
826
-
827
- ```typescript
828
- interface SnackbarContainerProps {
829
- position?: "top-right" | "top-left" | "bottom-right" | "bottom-left" | "top-center" | "bottom-center"; // default: "top-right"
830
- maxSnackbars?: number; // default: 5
831
- }
832
-
833
- // Usage via hook (not direct Snackbar component):
834
- const { showSnackbar } = useSnackbar();
835
- showSnackbar("Operación exitosa", "success");
836
- showSnackbar("Error al guardar", "danger", { duration: 5000, icon: "fa-exclamation" });
837
- ```
838
-
839
- **Variants**: `"primary"` | `"secondary"` | `"success"` | `"warning"` | `"danger"` | `"info"`
840
- **Default icons**: success=fa-check-circle, danger=fa-times-circle, warning=fa-exclamation-triangle, info/primary/secondary=fa-info-circle
841
-
842
- ### Skeleton
843
-
844
- Loading placeholder with pulse animation. Fully customizable via className.
845
-
846
- ```typescript
847
- interface SkeletonProps {
848
- className?: string; // Tailwind classes to control width, height, shape
849
- }
850
- ```
851
-
852
- ```tsx
853
- <Skeleton className="h-4 w-3/4" /> {/* Text line */}
854
- <Skeleton className="h-10 w-full" /> {/* Input placeholder */}
855
- <Skeleton className="h-32 w-32 rounded-full" /> {/* Avatar placeholder */}
856
- ```
857
-
858
- ### ThemeSwitcher
859
-
860
- Self-contained theme toggle. No props. Displays available themes with switch buttons and current theme info.
861
-
862
- ```tsx
863
- <ThemeSwitcher />
864
- ```
865
-
866
- ---
867
-
868
- ## Contexts & State Management
869
-
870
- ### ThemeProvider / useTheme
871
-
872
- Manages application theme with CSS variable injection, presets, and localStorage persistence.
873
-
874
- ```typescript
875
- // Provider props
876
- interface ThemeProviderProps {
877
- children: ReactNode;
878
- initialTheme?: string | Theme; // default: "light"
879
- storageKey?: string; // localStorage key. default: "flysoft-theme"
880
- forceInitialTheme?: boolean; // Ignore localStorage. default: false
881
- onThemeChange?: (theme: Theme) => void;
882
- }
883
-
884
- // Hook return
885
- interface ThemeContextType {
886
- theme: Theme; // Current theme object
887
- setTheme: (theme: Theme | string) => void; // Switch theme by name or object
888
- updateTheme: (updates: Partial<Theme> | ((prev: Theme) => Theme)) => void;
889
- currentThemeName: string;
890
- availableThemes: string[]; // ["light", "dark", "blue", "green"]
891
- resetToDefault: () => void;
892
- isDark: boolean;
893
- }
894
- ```
895
-
896
- ```tsx
897
- // App root
898
- <ThemeProvider initialTheme="light">
899
- <App />
900
- </ThemeProvider>
901
-
902
- // In components
903
- const { theme, setTheme, isDark } = useTheme();
904
- <Button onClick={() => setTheme(isDark ? "light" : "dark")}>Toggle Theme</Button>
905
- ```
906
-
907
- **Preset themes**: `lightTheme`, `darkTheme`, `blueTheme`, `greenTheme` (importable).
908
-
909
- ### AuthProvider / AuthContext
910
-
911
- Manages authentication with automatic token validation and refresh.
912
-
913
- ```typescript
914
- interface AuthProviderProps {
915
- children: React.ReactNode;
916
- getToken: (username: string, password: string) => Promise<AuthTokenInterface>; // required
917
- getUserData: (auth: AuthTokenInterface) => Promise<AuthContextUserInterface>; // required
918
- refreshToken?: (auth: AuthTokenInterface) => Promise<AuthTokenInterface>;
919
- removeToken?: (auth: AuthTokenInterface) => Promise<void>;
920
- showLog?: boolean; // default: false
921
- }
922
-
923
- interface AuthContextType {
924
- user: AuthContextUserInterface | null;
925
- login: (username: string, password: string) => Promise<void>;
926
- logout: () => void;
927
- isAuthenticated: boolean;
928
- isLoading: boolean;
929
- }
930
-
931
- interface AuthContextUserInterface {
932
- id?: number | string;
933
- name?: string;
934
- aditionalData?: any;
935
- token?: AuthTokenInterface;
936
- }
937
-
938
- interface AuthTokenInterface {
939
- accessToken?: string;
940
- expires?: string; // ISO 8601
941
- tokenType?: string;
942
- refreshToken?: string;
943
- aditionalData?: any;
944
- }
945
- ```
946
-
947
- ```tsx
948
- <AuthProvider
949
- getToken={async (user, pass) => {
950
- const res = await apiClient.post({ url: "/auth/login", body: { user, pass } });
951
- return res.token;
952
- }}
953
- getUserData={async (auth) => {
954
- return await apiClient.get({ url: "/auth/me" });
955
- }}
956
- refreshToken={async (auth) => {
957
- return await apiClient.post({ url: "/auth/refresh", body: { token: auth.refreshToken } });
958
- }}
959
- >
960
- <App />
961
- </AuthProvider>
962
-
963
- // In components
964
- const { user, login, logout, isAuthenticated } = useContext(AuthContext);
965
- ```
966
-
967
- **Behaviors**: Validates token on mount. Checks expiration every 60s. Auto-refreshes if `refreshToken` provided. Stores in localStorage as `"auth"`.
968
-
969
- ### CrudProvider / useCrud\<T\>
970
-
971
- Generic CRUD context with automatic pagination, URL parameter sync, and snackbar notifications.
972
-
973
- ```typescript
974
- interface CrudProviderProps<T> {
975
- children: ReactNode;
976
- getPromise?: Function | { execute: Function; successMessage?: string; errorMessage?: string | ((error: any) => string) };
977
- getItemPromise?: Function | { execute: Function; successMessage?: string; errorMessage?: string | ((error: any) => string) };
978
- postPromise?: Function | { execute: Function; successMessage?: string; errorMessage?: string | ((error: any) => string) };
979
- putPromise?: Function | { execute: Function; successMessage?: string; errorMessage?: string | ((error: any) => string) };
980
- deletePromise?: Function | { execute: Function; successMessage?: string; errorMessage?: string | ((error: any) => string) };
981
- urlParams?: Array<string>; // URL params to watch. default: []
982
- limit?: number; // Items per page. default: 15
983
- pageParam?: string; // URL page param. default: "pagina"
984
- singleItemId?: string | number;
985
- extraData?: Record<string, any>;
986
- }
987
-
988
- interface CrudContextType<T> {
989
- list: Array<T> | undefined;
990
- item: T | undefined;
991
- page: number;
992
- pages: number;
993
- total: number;
994
- limit: number;
995
- isLoading: boolean;
996
- pagination: ReactNode; // Pre-built Pagination component
997
- params: Record<string, any>;
998
- extraData?: Record<string, any>;
999
- setExtraData: Dispatch<SetStateAction<Record<string, any> | undefined>>;
1000
- fetchItems: { execute: (params?: Record<string, any>) => Promise<void>; isLoading: boolean };
1001
- fetchItem: { execute: (params?: Record<string, any> | string | number) => Promise<T | undefined>; isLoading: boolean };
1002
- createItem: { execute: (item: T) => Promise<T | undefined | null>; isLoading: boolean };
1003
- updateItem: { execute: (item: T) => Promise<T | undefined | null>; isLoading: boolean };
1004
- deleteItem: { execute: (item: T) => Promise<void>; isLoading: boolean };
1005
- }
1006
- ```
1007
-
1008
- ```tsx
1009
- <CrudProvider<User>
1010
- getPromise={(params) => apiClient.get({ url: "/api/users", params })}
1011
- getItemPromise={(id) => apiClient.get({ url: `/api/users/${id}` })}
1012
- postPromise={{ execute: (item) => apiClient.post({ url: "/api/users", body: item }), successMessage: "Usuario creado" }}
1013
- putPromise={{ execute: (item) => apiClient.put({ url: `/api/users/${item.id}`, body: item }), successMessage: "Usuario actualizado" }}
1014
- deletePromise={{ execute: (item) => apiClient.del({ url: `/api/users/${item.id}` }), successMessage: "Usuario eliminado" }}
1015
- urlParams={["nombre", "estado"]}
1016
- limit={20}
1017
- >
1018
- <UserList />
1019
- </CrudProvider>
1020
-
1021
- // In child components
1022
- const { list, isLoading, pagination, createItem, deleteItem } = useCrud<User>();
1023
- ```
1024
-
1025
- **Behaviors**: Auto-fetches when URL params change. Resets pagination on filter change. Shows snackbar on success/error.
1026
-
1027
- ### SnackbarProvider / useSnackbar
1028
-
1029
- Manages toast notifications.
1030
-
1031
- ```typescript
1032
- interface SnackbarActionsType {
1033
- showSnackbar: (
1034
- message: string,
1035
- variant?: SnackbarVariant,
1036
- options?: { duration?: number; icon?: string; iconLabel?: string }
1037
- ) => void;
1038
- removeSnackbar: (id: string) => void;
1039
- }
1040
- ```
1041
-
1042
- ```tsx
1043
- // App root
1044
- <SnackbarProvider>
1045
- <SnackbarContainer position="bottom-right" maxSnackbars={3} />
1046
- <App />
1047
- </SnackbarProvider>
1048
-
1049
- // In components
1050
- const { showSnackbar } = useSnackbar();
1051
- showSnackbar("Guardado exitosamente", "success");
1052
- showSnackbar("Error de conexión", "danger", { duration: 5000 });
1053
- ```
1054
-
1055
- ### AppLayoutProvider / useAppLayout
1056
-
1057
- Combines ThemeProvider + SnackbarProvider + AppLayout into a single provider.
1058
-
1059
- ```typescript
1060
- interface AppLayoutProviderProps {
1061
- children: ReactNode;
1062
- initialTheme?: string | Theme;
1063
- storageKey?: string;
1064
- forceInitialTheme?: boolean;
1065
- initialNavbar?: NavbarInterface;
1066
- initialLeftDrawer?: LeftDrawerInterface;
1067
- initialContentFooter?: ReactNode;
1068
- className?: string;
1069
- }
1070
-
1071
- interface AppLayoutContextType extends ThemeContextType {
1072
- navbar: NavbarInterface | undefined;
1073
- leftDrawer: LeftDrawerInterface | undefined;
1074
- contentFooter: ReactNode | undefined;
1075
- className: string;
1076
- setNavbar: Dispatch<SetStateAction<NavbarInterface | undefined>>;
1077
- setLeftDrawer: Dispatch<SetStateAction<LeftDrawerInterface | undefined>>;
1078
- setContentFooter: (node: ReactNode | undefined) => void;
1079
- setClassName: (className: string) => void;
1080
- setNavBarLeftNode: (node: ReactNode | undefined) => void;
1081
- setNavbarRightNode: (node: ReactNode | undefined) => void;
1082
- }
1083
- ```
1084
-
1085
- ```tsx
1086
- <AppLayoutProvider
1087
- initialTheme="light"
1088
- initialNavbar={{ navBarLeftNode: <h1>Mi App</h1>, fullWidthNavbar: true }}
1089
- initialLeftDrawer={{ contentNode: <nav>...</nav> }}
1090
- >
1091
- <Routes />
1092
- </AppLayoutProvider>
1093
-
1094
- // In pages - dynamically update layout
1095
- const { setNavBarLeftNode, setNavbarRightNode } = useAppLayout();
1096
- useEffect(() => {
1097
- setNavBarLeftNode(<h1>Dashboard</h1>);
1098
- }, []);
1099
- ```
1100
-
1101
- ---
1102
-
1103
- ## Hooks
1104
-
1105
- ### useThemeOverride
1106
-
1107
- Applies granular CSS variable overrides without changing the entire theme.
1108
-
1109
- ```typescript
1110
- function useThemeOverride(options?: {
1111
- scope?: "global" | "local"; // default: "global"
1112
- element?: HTMLElement | null;
1113
- prefix?: string; // default: "flysoft"
1114
- }): {
1115
- applyOverride: (overrides: Record<string, string | number>) => void;
1116
- revertOverride: (keys: string[]) => void;
1117
- revertAllOverrides: () => void;
1118
- getCSSVariable: (key: string) => string | null;
1119
- isOverrideApplied: (key: string) => boolean;
1120
- appliedOverridesCount: number;
1121
- }
1122
- ```
1123
-
1124
- ### useTemporaryOverride
1125
-
1126
- Applies CSS variable overrides that auto-revert after a duration.
1127
-
1128
- ```typescript
1129
- function useTemporaryOverride(
1130
- overrides: Record<string, string | number>,
1131
- duration?: number, // default: 3000
1132
- options?: { scope?: "global" | "local"; element?: HTMLElement | null; prefix?: string }
1133
- ): { applyTemporaryOverride: () => Function }
1134
- ```
1135
-
1136
- ### useBreakpoint
1137
-
1138
- Returns current viewport breakpoint and device type.
1139
-
1140
- ```typescript
1141
- type Breakpoint = "xs" | "sm" | "md" | "lg" | "xl" | "2xl";
1142
-
1143
- function useBreakpoint(): {
1144
- breakpoint: Breakpoint;
1145
- windowSize: { width: number; height: number };
1146
- isMobile: boolean; // xs or sm
1147
- isTablet: boolean; // md
1148
- isDesktop: boolean; // lg, xl, or 2xl
1149
- }
1150
- ```
1151
-
1152
- ### useElementScroll
1153
-
1154
- Tracks scroll position and direction with requestAnimationFrame optimization.
1155
-
1156
- ```typescript
1157
- function useElementScroll(elementRef: React.RefObject<HTMLElement | null>): {
1158
- scrollY: number;
1159
- scrollDirection: "up" | "down" | null;
1160
- }
1161
- ```
1162
-
1163
- ### useAsyncRequest
1164
-
1165
- Manages async operations with loading state and snackbar notifications.
1166
-
1167
- ```typescript
1168
- interface AsyncRequestOptions {
1169
- successMessage?: string;
1170
- errorMessage?: string | ((error: any) => string);
1171
- successVariant?: SnackbarVariant; // default: "success"
1172
- errorVariant?: SnackbarVariant; // default: "danger"
1173
- onSuccess?: (data: any) => void;
1174
- onError?: (error: any) => void;
1175
- onFinally?: () => void;
1176
- }
1177
-
1178
- function useAsyncRequest(options?: AsyncRequestOptions): {
1179
- isLoading: boolean;
1180
- execute: <T>(requestFn: () => Promise<T>) => Promise<T | undefined>;
1181
- setLoading: (loading: boolean) => void;
1182
- }
1183
- ```
1184
-
1185
- ```tsx
1186
- const { execute, isLoading } = useAsyncRequest({
1187
- successMessage: "Guardado exitosamente",
1188
- errorMessage: (err) => getErrorMessage(err),
1189
- });
1190
- await execute(() => apiClient.post({ url: "/api/data", body: formData }));
1191
- ```
1192
-
1193
- ### useEnum
1194
-
1195
- Converts TypeScript enums to arrays for form select options.
1196
-
1197
- ```typescript
1198
- function useEnum(baseEnum: any): {
1199
- getArray: () => Array<NameValueInterface<number>>;
1200
- getInstance: (id: number) => NameValueInterface<number> | undefined;
1201
- }
1202
- ```
1203
-
1204
- ### useGlobalThemeStyles
1205
-
1206
- Applies theme colors to `<body>` and `<html>` for full-page theming. No return value.
1207
-
1208
- ```tsx
1209
- function useGlobalThemeStyles(): void;
1210
- ```
1211
-
1212
- ---
1213
-
1214
- ## Services
1215
-
1216
- ### apiClient
1217
-
1218
- Singleton HTTP client (Axios-based) with automatic Bearer token injection.
1219
-
1220
- ```typescript
1221
- // Main methods
1222
- apiClient.get<T>(options: { url: string; params?: Record<string, unknown>; headers?: Record<string, string> }): Promise<T>;
1223
- apiClient.post<T>(options: { url: string; body?: unknown; headers?: Record<string, string> }): Promise<T>;
1224
- apiClient.put<T>(options: { url: string; body?: unknown; headers?: Record<string, string> }): Promise<T>;
1225
- apiClient.del<T>(options: { url: string; headers?: Record<string, string> }): Promise<T>;
1226
-
1227
- // File operations
1228
- apiClient.getFile(options): Promise<{ data: Blob; headers: any }>;
1229
- apiClient.getFileAsUrl(options): Promise<string>;
1230
- apiClient.openFile(options): Promise<void>;
1231
- apiClient.downloadFile(options): Promise<void>;
1232
- apiClient.uploadFile<T>(options: { url: string; files: FileList | File[]; headers?: { paramName?: string } }): Promise<T>;
1233
-
1234
- // Token management
1235
- setApiClientTokenProvider(provider?: () => string | undefined): void;
1236
- clearApiClientTokenProvider(): void;
1237
-
1238
- // Create isolated instances
1239
- createApiClient(config?: { baseURL?: string; timeout?: number; headers?: Record<string, string> }): ApiClientService;
1240
- ```
1241
-
1242
- ```tsx
1243
- // Setup token globally
1244
- setApiClientTokenProvider(() => user?.token?.accessToken);
1245
-
1246
- // API calls
1247
- const users = await apiClient.get<User[]>({ url: "/api/users", params: { page: 1 } });
1248
- await apiClient.post({ url: "/api/users", body: { name: "Juan" } });
1249
- await apiClient.downloadFile({ url: "/api/reports/pdf" });
1250
- await apiClient.uploadFile({ url: "/api/upload", files: fileInput.files });
1251
- ```
1252
-
1253
- ---
1254
-
1255
- ## Helpers
1256
-
1257
- | Function | Signature | Description |
1258
- |----------|-----------|-------------|
1259
- | `currencyFormat` | `(value: number) => string` | Formats as `"1.234,56"` (es-AR locale) |
1260
- | `getErrorMessage` | `(error: any) => string` | Extracts message from AxiosError. Default: `"Ha ocurrido un error"` |
1261
- | `getInitialLetters` | `(text: string) => string` | `"Juan Pérez"` → `"JP"` |
1262
- | `getQueryString` | `(params: URLSearchParams, newParams: any) => string` | Merges params, returns `"?key=value"` |
1263
- | `objectToQueryString` | `(source: any) => string` | Object to `"a=1&b=2"` (no leading `?`) |
1264
- | `queryStringToObject` | `(params: string) => Record<string, string>` | `"a=1&b=2"` `{a: "1", b: "2"}` |
1265
- | `nameValueArrayToObject` | `<T>(arr: NameValueInterface<T>[]) => Record<string, T>` | Array of {name, value} to object |
1266
- | `promiseMapper` | `<T, K>(promise, mapper) => Promise<K \| K[] \| PaginationInterface<K>>` | Maps promise results (arrays, pagination, single) |
1267
- | `RegularExpressions` | Object | `.email`, `.dateString`, `.password(config)` regex patterns |
1268
-
1269
- ## Interfaces
1270
-
1271
- ```typescript
1272
- interface NameValueInterface<T> {
1273
- name: string;
1274
- value: T;
1275
- extras?: any;
1276
- }
1277
-
1278
- interface PaginationInterface<T> {
1279
- list: Array<T>;
1280
- limit: number;
1281
- page: number;
1282
- pages: number;
1283
- total: number;
1284
- }
1285
- ```
1286
-
1287
- ---
1288
-
1289
- ## Templates
1290
-
1291
- ### LoginForm
1292
-
1293
- ```typescript
1294
- interface LoginFormProps {
1295
- onSubmit?: (data: { email: string; password: string }) => void;
1296
- loading?: boolean;
1297
- error?: string;
1298
- className?: string;
1299
- }
1300
- ```
1301
-
1302
- ### RegistrationForm
1303
-
1304
- ```typescript
1305
- interface RegistrationFormProps {
1306
- onSubmit?: (data: { firstName: string; lastName: string; email: string; password: string; confirmPassword: string }) => void;
1307
- loading?: boolean;
1308
- error?: string;
1309
- className?: string;
1310
- }
1311
- ```
1312
-
1313
- ### ContactForm
1314
-
1315
- ```typescript
1316
- interface ContactFormProps {
1317
- onSubmit?: (data: { name: string; email: string; subject: string; message: string }) => void;
1318
- loading?: boolean;
1319
- success?: boolean;
1320
- error?: string;
1321
- className?: string;
1322
- }
1323
- ```
1324
-
1325
- ### DashboardLayout
1326
-
1327
- ```typescript
1328
- interface DashboardStat {
1329
- title: string;
1330
- value: string | number;
1331
- change?: string; // e.g. "+12%"
1332
- changeType?: "positive" | "negative" | "neutral";
1333
- icon?: string;
1334
- }
1335
-
1336
- interface DashboardLayoutProps {
1337
- title: string; // required
1338
- subtitle?: string;
1339
- stats?: DashboardStat[];
1340
- actions?: React.ReactNode;
1341
- children: React.ReactNode; // required
1342
- className?: string;
1343
- }
1344
- ```
1345
-
1346
- ### SidebarLayout
1347
-
1348
- ```typescript
1349
- interface MenuItem {
1350
- label: string;
1351
- icon: string;
1352
- href: string;
1353
- badge?: string | number;
1354
- children?: MenuItem[];
1355
- }
1356
-
1357
- interface User {
1358
- name: string;
1359
- email?: string;
1360
- avatar?: string;
1361
- }
1362
-
1363
- interface SidebarLayoutProps {
1364
- title: string; // required
1365
- menuItems: MenuItem[]; // required
1366
- user: User; // required
1367
- children: React.ReactNode; // required
1368
- className?: string;
1369
- onLogout?: () => void;
1370
- }
1371
- ```
1372
-
1373
- ### FormPattern
1374
-
1375
- ```typescript
1376
- interface FormField {
1377
- name: string;
1378
- label: string;
1379
- type?: string; // default: "text"
1380
- placeholder?: string;
1381
- icon?: string;
1382
- required?: boolean;
1383
- validation?: (value: string) => string | undefined;
1384
- multiline?: boolean;
1385
- rows?: number; // default: 4
1386
- }
1387
-
1388
- interface FormPatternProps {
1389
- title: string; // required
1390
- subtitle?: string;
1391
- fields: FormField[]; // required
1392
- onSubmit: (data: Record<string, string>) => void; // required
1393
- submitText?: string; // default: "Enviar"
1394
- submitIcon?: string; // default: "fa-paper-plane"
1395
- loading?: boolean;
1396
- error?: string;
1397
- success?: boolean;
1398
- className?: string;
1399
- gridCols?: 1 | 2; // default: 1
1400
- }
1401
- ```
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
+ }
382
+
383
+ interface NavbarInterface {
384
+ navBarLeftNode?: React.ReactNode;
385
+ navBarRightNode?: React.ReactNode;
386
+ fullWidthNavbar?: boolean; // Fixed full-width (true) or relative (false)
387
+ height?: string; // default: "64px"
388
+ className?: string;
389
+ }
390
+
391
+ interface LeftDrawerInterface {
392
+ headerNode?: React.ReactNode;
393
+ contentNode?: React.ReactNode;
394
+ footerNode?: React.ReactNode;
395
+ className?: string;
396
+ width?: string; // default: "256px"
397
+ }
398
+ ```
399
+
400
+ ```tsx
401
+ <AppLayout
402
+ navbar={{
403
+ navBarLeftNode: <h1>Mi App</h1>,
404
+ navBarRightNode: <Avatar text="Admin" />,
405
+ fullWidthNavbar: true,
406
+ }}
407
+ leftDrawer={{
408
+ headerNode: <h2>Menú</h2>,
409
+ contentNode: <nav>...</nav>,
410
+ }}
411
+ >
412
+ <main>Contenido</main>
413
+ </AppLayout>
414
+ ```
415
+
416
+ **Behaviors**: Navbar auto-hides/shows on scroll. Mobile drawer with overlay. Responsive breakpoints.
417
+
418
+ ### Collection
419
+
420
+ Flex container for rendering lists of items, density-aware.
421
+
422
+ ```typescript
423
+ interface CollectionProps {
424
+ children: React.ReactNode; // required
425
+ /**
426
+ * Presets semánticos ligados a densidad o cualquier valor CSS arbitrario.
427
+ * "tight" = 0, "sm"/"md"/"lg" leen --flysoft-density-gap-*.
428
+ */
429
+ gap?: "tight" | "sm" | "md" | "lg" | string; // default: "md"
430
+ direction?: "column" | "row"; // default: "column"
431
+ wrap?: boolean; // default: false
432
+ className?: string;
433
+ /**
434
+ * Override local: redefine --flysoft-density-* para esta Collection y
435
+ * descendientes. Útil para tener una sección densa dentro de un layout cómodo.
436
+ */
437
+ density?: "comfortable" | "compact" | "dense";
438
+ }
439
+ ```
440
+
441
+ ```tsx
442
+ // Default
443
+ <Collection><DataField label="A" value="1" /><DataField label="B" value="2" /></Collection>
444
+
445
+ // Horizontal con wrap, gap chico
446
+ <Collection direction="row" wrap gap="sm">
447
+ <Badge>Activo</Badge><Badge color="info">Verificado</Badge>
448
+ </Collection>
449
+
450
+ // Sección densa dentro de Card comfortable
451
+ <Collection density="dense">
452
+ <DataField label="CUIL" value="..." />
453
+ <DataField label="Edad" value={59} />
454
+ </Collection>
455
+ ```
456
+
457
+ ### DataField
458
+
459
+ Label + value pair display for detail views. Density-aware.
460
+
461
+ ```typescript
462
+ interface DataFieldProps {
463
+ label?: string;
464
+ value?: string | number | React.ReactNode;
465
+ inline?: boolean; // Horizontal layout. default: false
466
+ align?: "left" | "right" | "center"; // default: "left"
467
+ title?: string; // HTML title tooltip
468
+ link?: string; // Opens URL in new tab
469
+ className?: string;
470
+ labelClassName?: string;
471
+ /**
472
+ * Override local de tipografía:
473
+ * - "md" (default): label = font-sm, value = font-base.
474
+ * - "sm": baja un nivel — label = font-xs, value = font-sm.
475
+ */
476
+ size?: "sm" | "md";
477
+ /** Separación entre label y value en modo stack. "tight" = 0. */
478
+ gap?: "tight" | "sm" | "md"; // default: "md"
479
+ /** Oculta el ":" después del label en modo inline. */
480
+ hideColon?: boolean; // default: false
481
+ }
482
+ ```
483
+
484
+ ```tsx
485
+ <DataField label="Nombre" value="Juan Pérez" />
486
+ <DataField label="Email" value="juan@email.com" link="mailto:juan@email.com" inline />
487
+ // Modo compacto para listas densas
488
+ <DataField label="CUIL" value="20-17990271-1" size="sm" />
489
+ <DataField label="Estado" value="Activo" inline hideColon />
490
+ ```
491
+
492
+ ### TabsGroup / TabPanel
493
+
494
+ Tabbed interfaces with optional URL persistence.
495
+
496
+ ```typescript
497
+ interface Tab {
498
+ id: string | number;
499
+ label: string;
500
+ }
501
+
502
+ interface TabsGroupProps {
503
+ children?: React.ReactNode;
504
+ tabs: Tab[]; // required
505
+ paramName?: string; // URL search param for persistence
506
+ headerNode?: React.ReactNode; // Right-aligned header content
507
+ onChangeTab?: (selectedTab: string) => void;
508
+ }
509
+
510
+ interface TabPanelProps {
511
+ children?: React.ReactNode;
512
+ tabId: string | number; // Must match a Tab.id (required)
513
+ }
514
+ ```
515
+
516
+ ```tsx
517
+ <TabsGroup tabs={[{ id: "info", label: "Información" }, { id: "history", label: "Historial" }]}>
518
+ <TabPanel tabId="info">
519
+ <p>Información del usuario</p>
520
+ </TabPanel>
521
+ <TabPanel tabId="history">
522
+ <p>Historial de actividad</p>
523
+ </TabPanel>
524
+ </TabsGroup>
525
+ ```
526
+
527
+ ### DataTable\<T\>
528
+
529
+ High-performance data table with sorting, formatting, actions, and skeleton loading.
530
+
531
+ ```typescript
532
+ interface DataTableColumn<T> {
533
+ align?: "left" | "right" | "center"; // Auto-set for date/currency/numeric
534
+ width?: string;
535
+ header?: string | React.ReactNode;
536
+ footer?: string | React.ReactNode;
537
+ value?: string | number | ((row: T) => string | React.ReactNode);
538
+ tooltip?: (row: T) => string | React.ReactNode;
539
+ type?: "text" | "numeric" | "currency" | "date";
540
+ actions?: (row: T) => Array<React.ReactNode>;
541
+ headerActions?: () => Array<React.ReactNode>;
542
+ }
543
+
544
+ interface DataTableProps<T> {
545
+ columns: DataTableColumn<T>[]; // required
546
+ rows: T[]; // required
547
+ className?: string;
548
+ maxRows?: number; // Enables sticky header with scroll
549
+ locale?: string; // default: "es-AR"
550
+ isLoading?: boolean; // Shows skeleton rows. default: false
551
+ loadingRows?: number; // default: 5
552
+ rowClassName?: (row: T) => string;
553
+ headerClassName?: string;
554
+ footerClassName?: string;
555
+ headerCellClassName?: string;
556
+ footerCellClassName?: string;
557
+ cellClassName?: string | ((row: T, column: DataTableColumn<T>) => string);
558
+ /**
559
+ * Override local de densidad: cuando es true, fuerza el preset "compact" en
560
+ * las variables --flysoft-density-* dentro de esta DataTable (paddings,
561
+ * tipografía, altura de fila). También se propaga a los DropdownMenu de
562
+ * acciones. Independiente de la densidad global del ThemeProvider.
563
+ */
564
+ compact?: boolean; // default: false
565
+ }
566
+ ```
567
+
568
+ ```tsx
569
+ interface User { id: number; name: string; salary: number; createdAt: string; }
570
+
571
+ const columns: DataTableColumn<User>[] = [
572
+ { header: "ID", value: "id", width: "60px" },
573
+ { header: "Nombre", value: (row) => row.name },
574
+ { header: "Salario", value: "salary", type: "currency" },
575
+ { header: "Fecha", value: "createdAt", type: "date" },
576
+ {
577
+ header: "Acciones",
578
+ actions: (row) => [
579
+ <Button key="edit" variant="ghost" size="sm" icon="fa-edit" onClick={() => edit(row)}>Editar</Button>,
580
+ <Button key="del" variant="ghost" size="sm" icon="fa-trash" color="danger" onClick={() => del(row)}>Eliminar</Button>,
581
+ ],
582
+ },
583
+ ];
584
+
585
+ <DataTable<User> columns={columns} rows={users} isLoading={loading} maxRows={10} />
586
+ ```
587
+
588
+ **Type formatting**: `currency` → thousands separator, no symbol. `numeric` → locale formatting. `date` → DD/MM/YYYY.
589
+
590
+ ### Accordion
591
+
592
+ Collapsible content section with smooth animation.
593
+
594
+ ```typescript
595
+ interface AccordionProps {
596
+ title: string | React.ReactNode; // required
597
+ children: React.ReactNode; // required
598
+ icon?: string; // FontAwesome icon
599
+ rightNode?: React.ReactNode;
600
+ defaultOpen?: boolean; // default: false
601
+ className?: string;
602
+ headerClassName?: string; // clases para el header (botón)
603
+ contentClassName?: string; // clases para el contenedor del contenido
604
+ variant?: "default" | "elevated" | "outlined"; // default: "default"
605
+ onToggle?: (isOpen: boolean) => void;
606
+ }
607
+ ```
608
+
609
+ ```tsx
610
+ <Accordion title="Detalles" icon="fa-info-circle" defaultOpen>
611
+ <p>Contenido colapsable</p>
612
+ </Accordion>
613
+ ```
614
+
615
+ ### Menu
616
+
617
+ Simple menu list for displaying options.
618
+
619
+ ```typescript
620
+ interface MenuProps<T = { label: string }> {
621
+ options: T[]; // required
622
+ onOptionSelected: (item: T) => void; // required
623
+ getOptionLabel?: (item: T) => string;
624
+ renderOption?: (item: T) => React.ReactNode;
625
+ className?: string;
626
+ style?: React.CSSProperties;
627
+ itemClassName?: string;
628
+ }
629
+ ```
630
+
631
+ ### DropdownMenu
632
+
633
+ Portal-based dropdown menu triggered by a button. Auto-positions above/below.
634
+
635
+ ```typescript
636
+ interface DropdownMenuProps<T = { label: string }> {
637
+ options: T[]; // required
638
+ onOptionSelected: (item: T) => void; // required
639
+ renderNode?: React.ReactNode; // Custom trigger (default: ellipsis icon button)
640
+ getOptionLabel?: (item: T) => string;
641
+ renderOption?: (item: T) => React.ReactNode;
642
+ replaceOnSingleOption?: boolean; // Show single option inline. default: false
643
+ openOnHover?: boolean; // default: false
644
+ }
645
+ ```
646
+
647
+ ```tsx
648
+ <DropdownMenu
649
+ options={[{ label: "Editar" }, { label: "Eliminar" }]}
650
+ onOptionSelected={(item) => handleAction(item.label)}
651
+ renderNode={<Button variant="ghost" icon="fa-cog" size="sm" />}
652
+ />
653
+ ```
654
+
655
+ ### DropdownPanel
656
+
657
+ Portal-based dropdown that renders arbitrary content (not a list).
658
+
659
+ ```typescript
660
+ interface DropdownPanelProps {
661
+ renderNode?: React.ReactNode; // Custom trigger (default: ellipsis icon button)
662
+ children: React.ReactNode; // required
663
+ openOnHover?: boolean; // default: false
664
+ }
665
+ ```
666
+
667
+ ### Filter
668
+
669
+ Versatile filtering component with multiple filter types and optional URL persistence.
670
+
671
+ ```typescript
672
+ // Discriminated union by filterType
673
+ type FilterProps =
674
+ | TextFilterProps // filterType?: "text" (default)
675
+ | NumberFilterProps // filterType: "number" (+ min?, max?)
676
+ | DateFilterProps // filterType: "date"
677
+ | AutocompleteFilterProps // filterType: "autocomplete" (+ options, multiple?)
678
+ | SearchFilterProps // filterType: "search"
679
+ | SearchSelectFilterProps // filterType: "searchSelect" (+ onSearchPromiseFn, onSingleSearchPromiseFn)
680
+
681
+ // Common props for all filter types:
682
+ interface BaseFilterProps {
683
+ paramName?: string; // URL search param for persistence
684
+ label?: string;
685
+ staticOptions?: Array<{ text: string; value: string }>;
686
+ inputWidth?: string;
687
+ value?: string; // Controlled value
688
+ onChange?: (value: string | undefined) => void;
689
+ hideEmpty?: boolean; // default: false
690
+ disabled?: boolean; // default: false
691
+ compact?: boolean; // default: false fuerza densidad compacta local
692
+ bgColor?: string; // Fondo del badge e input (no del panel flotante). Ej: "#f5f5f5" o "var(--color-bg-secondary)"
693
+ }
694
+ ```
695
+
696
+ ```tsx
697
+ <Filter filterType="text" paramName="nombre" label="Nombre" />
698
+ // Fondo personalizado cuando el filtro va sobre una Card blanca:
699
+ <Filter filterType="search" paramName="q" label="Buscar" bgColor="var(--color-bg-secondary)" />
700
+ <Filter filterType="number" paramName="edad" label="Edad" min={0} max={120} />
701
+ <Filter filterType="date" paramName="fecha" label="Fecha" />
702
+ <Filter
703
+ filterType="autocomplete"
704
+ paramName="estado"
705
+ label="Estado"
706
+ options={[{ label: "Activo", value: "1" }, { label: "Inactivo", value: "0" }]}
707
+ />
708
+ <Filter
709
+ filterType="searchSelect"
710
+ paramName="cliente"
711
+ label="Cliente"
712
+ onSearchPromiseFn={(text) => apiClient.get({ url: `/api/clients?q=${text}` })}
713
+ onSingleSearchPromiseFn={(id) => apiClient.get({ url: `/api/clients/${id}` })}
714
+ />
715
+ ```
716
+
717
+ ---
718
+
719
+ ## Utility Components
720
+
721
+ ### Badge
722
+
723
+ Status/category label with variants and custom colors.
724
+
725
+ ```typescript
726
+ interface BadgeProps {
727
+ children: React.ReactNode; // required
728
+ variant?: "primary" | "secondary" | "success" | "warning" | "danger" | "info"; // default: "primary"
729
+ size?: "sm" | "md" | "lg"; // default: "md"
730
+ rounded?: boolean; // Full border radius. default: false
731
+ className?: string;
732
+ icon?: string;
733
+ iconPosition?: "left" | "right"; // default: "left"
734
+ iconLabel?: string; // aria-label for icon
735
+ bg?: string; // Custom background color
736
+ textColor?: string; // Custom text color
737
+ onClick?: (event: React.MouseEvent<HTMLElement>) => void;
738
+ }
739
+ ```
740
+
741
+ ```tsx
742
+ <Badge variant="success" icon="fa-check">Activo</Badge>
743
+ <Badge variant="danger" rounded>3</Badge>
744
+ <Badge bg="#8b5cf6" textColor="#fff">Custom</Badge>
745
+ ```
746
+
747
+ ### Avatar
748
+
749
+ User profile display with initials fallback when image fails.
750
+
751
+ ```typescript
752
+ interface AvatarProps {
753
+ text: string; // Name for initials extraction (required)
754
+ image?: string; // Image URL
755
+ bgColor?: string; // default: "#4b5563"
756
+ textColor?: string; // default: "#ffffff"
757
+ size?: "sm" | "md" | "lg"; // default: "md" (sm=32px, md=40px, lg=48px)
758
+ className?: string;
759
+ }
760
+ ```
761
+
762
+ ```tsx
763
+ <Avatar text="Juan Pérez" image="/avatars/juan.jpg" />
764
+ <Avatar text="Admin User" bgColor="#3b82f6" size="lg" />
765
+ ```
766
+
767
+ ### RoadMap
768
+
769
+ Progress/stage visualization with connected circles and gradient lines.
770
+
771
+ ```typescript
772
+ interface RoadMapStage {
773
+ name: string; // required
774
+ description?: string;
775
+ icon?: string;
776
+ disabled?: boolean; // Grayed out at 50% opacity
777
+ variant?: "primary" | "secondary" | "success" | "warning" | "danger" | "info";
778
+ bg?: string; // Custom color (overrides variant)
779
+ }
780
+
781
+ interface RoadMapProps {
782
+ stages: RoadMapStage[]; // required
783
+ className?: string;
784
+ }
785
+ ```
786
+
787
+ ```tsx
788
+ <RoadMap stages={[
789
+ { name: "Creado", icon: "fa-plus", variant: "info" },
790
+ { name: "En Proceso", icon: "fa-cog", variant: "warning" },
791
+ { name: "Completado", icon: "fa-check", variant: "success" },
792
+ { name: "Archivado", icon: "fa-archive", disabled: true },
793
+ ]} />
794
+ ```
795
+
796
+ ### Dialog
797
+
798
+ Modal window with overlay, escape-to-close, and scroll lock.
799
+
800
+ ```typescript
801
+ interface DialogProps {
802
+ isOpen: boolean; // required
803
+ title: React.ReactNode; // required
804
+ children: React.ReactNode; // required
805
+ footer?: React.ReactNode;
806
+ onClose?: () => void;
807
+ closeOnOverlayClick?: boolean; // default: false
808
+ /**
809
+ * Override local de densidad: cuando es true, fuerza el preset "compact" en
810
+ * --flysoft-density-* dentro del Dialog (paddings header/body/footer,
811
+ * tamaño del título, gaps). Independiente de la densidad global.
812
+ */
813
+ compact?: boolean; // default: false
814
+ bodyWidth?: string | number; // Custom dialog width (e.g. "800px", "80vw", 600). Default: max-w-lg
815
+ }
816
+ ```
817
+
818
+ ```tsx
819
+ <Dialog isOpen={showDialog} title="Confirmar" onClose={() => setShowDialog(false)}
820
+ footer={
821
+ <>
822
+ <Button variant="ghost" onClick={() => setShowDialog(false)}>Cancelar</Button>
823
+ <Button variant="primary" color="danger" onClick={handleDelete}>Eliminar</Button>
824
+ </>
825
+ }
826
+ >
827
+ <p>¿Está seguro que desea eliminar este registro?</p>
828
+ </Dialog>
829
+ ```
830
+
831
+ ### Loader
832
+
833
+ Loading indicator with progress bar. Can wrap content with overlay.
834
+
835
+ ```typescript
836
+ interface LoaderProps {
837
+ isLoading?: boolean; // default: false
838
+ text?: string; // Text below progress bar
839
+ children?: React.ReactNode;
840
+ keepContentWhileLoading?: boolean; // Show content faded at 50% opacity
841
+ contentLoadingNode?: React.ReactNode; // Custom loading content
842
+ overlayClassName?: string; // default: "bg-black/50 backdrop-blur-sm"
843
+ }
844
+ ```
845
+
846
+ ```tsx
847
+ <Loader isLoading={loading} text="Cargando datos...">
848
+ <DataTable ... />
849
+ </Loader>
850
+ <Loader isLoading={loading} keepContentWhileLoading>
851
+ <Card>...</Card>
852
+ </Loader>
853
+ ```
854
+
855
+ ### FiltersDialog
856
+
857
+ Dialog that groups multiple Filter components. Syncs values from/to URL search params.
858
+
859
+ ```typescript
860
+ interface FilterConfig {
861
+ filterType: "text" | "number" | "date" | "autocomplete";
862
+ paramName: string; // required
863
+ label?: string;
864
+ staticOptions?: Array<{ text: string; value: string }>;
865
+ inputWidth?: string;
866
+ min?: number; // For number filters
867
+ max?: number; // For number filters
868
+ options?: any[]; // For autocomplete
869
+ getOptionLabel?: (item: any) => string;
870
+ getOptionValue?: (item: any) => any;
871
+ renderOption?: (item: any) => React.ReactNode;
872
+ noResultsText?: string;
873
+ }
874
+
875
+ interface FiltersDialogProps {
876
+ filters: FilterConfig[]; // required
877
+ }
878
+ ```
879
+
880
+ ```tsx
881
+ <FiltersDialog filters={[
882
+ { filterType: "text", paramName: "nombre", label: "Nombre" },
883
+ { filterType: "number", paramName: "edad", label: "Edad", min: 0, max: 120 },
884
+ { filterType: "date", paramName: "fecha", label: "Fecha" },
885
+ { filterType: "autocomplete", paramName: "estado", label: "Estado",
886
+ options: [{ label: "Activo", value: "1" }, { label: "Inactivo", value: "0" }] },
887
+ ]} />
888
+ ```
889
+
890
+ ### Snackbar / SnackbarContainer
891
+
892
+ Toast notification system. SnackbarContainer must be at the app root.
893
+
894
+ ```typescript
895
+ interface SnackbarContainerProps {
896
+ position?: "top-right" | "top-left" | "bottom-right" | "bottom-left" | "top-center" | "bottom-center"; // default: "top-right"
897
+ maxSnackbars?: number; // default: 5
898
+ }
899
+
900
+ // Usage via hook (not direct Snackbar component):
901
+ const { showSnackbar } = useSnackbar();
902
+ showSnackbar("Operación exitosa", "success");
903
+ showSnackbar("Error al guardar", "danger", { duration: 5000, icon: "fa-exclamation" });
904
+ ```
905
+
906
+ **Variants**: `"primary"` | `"secondary"` | `"success"` | `"warning"` | `"danger"` | `"info"`
907
+ **Default icons**: success=fa-check-circle, danger=fa-times-circle, warning=fa-exclamation-triangle, info/primary/secondary=fa-info-circle
908
+
909
+ ### Skeleton
910
+
911
+ Loading placeholder with pulse animation. Fully customizable via className.
912
+
913
+ ```typescript
914
+ interface SkeletonProps {
915
+ className?: string; // Tailwind classes to control width, height, shape
916
+ }
917
+ ```
918
+
919
+ ```tsx
920
+ <Skeleton className="h-4 w-3/4" /> {/* Text line */}
921
+ <Skeleton className="h-10 w-full" /> {/* Input placeholder */}
922
+ <Skeleton className="h-32 w-32 rounded-full" /> {/* Avatar placeholder */}
923
+ ```
924
+
925
+ ### ThemeSwitcher
926
+
927
+ Self-contained theme toggle. No props. Displays available themes with switch buttons and current theme info.
928
+
929
+ ```tsx
930
+ <ThemeSwitcher />
931
+ ```
932
+
933
+ ---
934
+
935
+ ## Contexts & State Management
936
+
937
+ ### ThemeProvider / useTheme
938
+
939
+ Manages application theme with CSS variable injection, presets, and localStorage persistence.
940
+
941
+ ```typescript
942
+ type Density = "comfortable" | "compact" | "dense";
943
+
944
+ // Provider props
945
+ interface ThemeProviderProps {
946
+ children: ReactNode;
947
+ initialTheme?: string | Theme; // default: "light"
948
+ storageKey?: string; // localStorage key. default: "flysoft-theme"
949
+ forceInitialTheme?: boolean; // Ignore localStorage. default: false
950
+ onThemeChange?: (theme: Theme) => void;
951
+ density?: Density; // Global density. default: "comfortable"
952
+ densityStorageKey?: string; // default: "flysoft-density"
953
+ forceInitialDensity?: boolean; // default: false
954
+ onDensityChange?: (density: Density) => void;
955
+ }
956
+
957
+ // Hook return
958
+ interface ThemeContextType {
959
+ theme: Theme; // Current theme object
960
+ setTheme: (theme: Theme | string) => void; // Switch theme by name or object
961
+ updateTheme: (updates: Partial<Theme> | ((prev: Theme) => Theme)) => void;
962
+ currentThemeName: string;
963
+ availableThemes: string[]; // ["light", "dark", "blue", "green"]
964
+ resetToDefault: () => void;
965
+ isDark: boolean;
966
+ density: Density;
967
+ setDensity: (density: Density) => void;
968
+ }
969
+ ```
970
+
971
+ ```tsx
972
+ // App root - default density
973
+ <ThemeProvider initialTheme="light">
974
+ <App />
975
+ </ThemeProvider>
976
+
977
+ // App root - data-heavy app (CRUD admin, dashboards)
978
+ <ThemeProvider initialTheme="light" density="dense">
979
+ <App />
980
+ </ThemeProvider>
981
+
982
+ // Runtime toggle
983
+ const { theme, setTheme, isDark, density, setDensity } = useTheme();
984
+ <Button onClick={() => setTheme(isDark ? "light" : "dark")}>Toggle Theme</Button>
985
+ <Button onClick={() => setDensity(density === "dense" ? "comfortable" : "dense")}>
986
+ Toggle Density
987
+ </Button>
988
+ ```
989
+
990
+ **Preset themes**: `lightTheme`, `darkTheme`, `blueTheme`, `greenTheme` (importable).
991
+ **Density presets**: `comfortableDensity`, `compactDensity`, `denseDensity`, `densityPresets` (importable).
992
+
993
+ **Density CSS variables** (inyectadas automáticamente según la densidad activa):
994
+ `--flysoft-density-padding-x-{sm|md|lg}`, `--flysoft-density-padding-y-{sm|md|lg}`,
995
+ `--flysoft-density-container-padding-{x|y}`,
996
+ `--flysoft-density-gap-{sm|md|lg}`, `--flysoft-density-font-{xs|sm|base|lg|xl}`,
997
+ `--flysoft-density-control-height-{sm|md|lg}`, `--flysoft-density-datatable-row`,
998
+ `--flysoft-density-datatable-header`, `--flysoft-density-card-gap`.
999
+
1000
+ **Componentes que ya consumen densidad automáticamente** (sin necesidad de prop):
1001
+ Card, DataField, Collection, Button, LinkButton, Input, AutocompleteInput,
1002
+ SearchSelectInput, DateInput, CurrencyInput, DatePicker, DataTable, Dialog,
1003
+ Filter (incluye los paneles flotantes), FiltersDialog, Accordion, Menu,
1004
+ DropdownMenu, DropdownPanel, TabsGroup, Badge, Checkbox, RadioButtonGroup,
1005
+ Pagination, Avatar, RoadMap, Snackbar, Skeleton, Loader. **Toda la librería
1006
+ es density-aware.** El default `comfortable` preserva el aspecto previo de
1007
+ cada componente, así que los consumidores existentes no ven cambios visuales
1008
+ hasta que activan `density="compact"` o `density="dense"`.
1009
+
1010
+ **Tipografía global**: dentro del wrapper `.flysoft-theme-reset` (cualquier
1011
+ ThemeProvider/AppLayoutProvider lo crea automáticamente), los headings y
1012
+ elementos de texto sin clase específica escalan con densidad:
1013
+ - `h1` = `font-xl × 1.5`, `h2` = `font-xl × 1.25`, `h3` = `font-xl`,
1014
+ `h4` = `font-lg`, `h5` = `font-base`, `h6` = `font-sm`
1015
+ - `p` = `font-base`, `small` = `font-xs`
1016
+ - `span`/`div` heredan `font-base` del wrapper
1017
+
1018
+ Las reglas son de baja specificity: cualquier `className` Tailwind (`text-lg`,
1019
+ `text-2xl`, etc.) o `style` inline las pisa.
1020
+
1021
+ **Componentes con prop `compact` como override local de densidad** (fuerzan
1022
+ preset compact en `--flysoft-density-*` dentro de y descendientes,
1023
+ ignorando la densidad global): Card, DataTable, Dialog, Filter, Accordion,
1024
+ Menu, DropdownMenu, DropdownPanel, TabsGroup.
1025
+
1026
+ **Override de fondo/estilos en form-controls vía `className`**: los form-controls
1027
+ (Input, CurrencyInput, DateInput, AutocompleteInput, SearchSelectInput, Button,
1028
+ LinkButton, Checkbox, RadioButtonGroup, DatePicker) combinan sus clases con
1029
+ `twMerge`, así que un `className` con clase en conflicto pisa la default de forma
1030
+ confiable. Para cambiar el fondo por defecto (`bg-[var(--color-bg-default)]`) —por
1031
+ ej. cuando el control va sobre una Card del mismo color— pasá un `bg-*`:
1032
+ `<Input className="bg-[var(--color-bg-secondary)]" />` o `<Input className="bg-[#f5f5f5]" />`.
1033
+ El `Filter` no toma `className` para esto; usa su prop `bgColor`.
1034
+
1035
+ ### AuthProvider / AuthContext
1036
+
1037
+ Manages authentication with automatic token validation and refresh.
1038
+
1039
+ ```typescript
1040
+ interface AuthProviderProps {
1041
+ children: React.ReactNode;
1042
+ getToken: (username: string, password: string) => Promise<AuthTokenInterface>; // required
1043
+ getUserData: (auth: AuthTokenInterface) => Promise<AuthContextUserInterface>; // required
1044
+ refreshToken?: (auth: AuthTokenInterface) => Promise<AuthTokenInterface>;
1045
+ removeToken?: (auth: AuthTokenInterface) => Promise<void>;
1046
+ showLog?: boolean; // default: false
1047
+ }
1048
+
1049
+ interface AuthContextType {
1050
+ user: AuthContextUserInterface | null;
1051
+ login: (username: string, password: string) => Promise<void>;
1052
+ logout: () => void;
1053
+ isAuthenticated: boolean;
1054
+ isLoading: boolean;
1055
+ }
1056
+
1057
+ interface AuthContextUserInterface {
1058
+ id?: number | string;
1059
+ name?: string;
1060
+ aditionalData?: any;
1061
+ token?: AuthTokenInterface;
1062
+ }
1063
+
1064
+ interface AuthTokenInterface {
1065
+ accessToken?: string;
1066
+ expires?: string; // ISO 8601
1067
+ tokenType?: string;
1068
+ refreshToken?: string;
1069
+ aditionalData?: any;
1070
+ }
1071
+ ```
1072
+
1073
+ ```tsx
1074
+ <AuthProvider
1075
+ getToken={async (user, pass) => {
1076
+ const res = await apiClient.post({ url: "/auth/login", body: { user, pass } });
1077
+ return res.token;
1078
+ }}
1079
+ getUserData={async (auth) => {
1080
+ return await apiClient.get({ url: "/auth/me" });
1081
+ }}
1082
+ refreshToken={async (auth) => {
1083
+ return await apiClient.post({ url: "/auth/refresh", body: { token: auth.refreshToken } });
1084
+ }}
1085
+ >
1086
+ <App />
1087
+ </AuthProvider>
1088
+
1089
+ // In components
1090
+ const { user, login, logout, isAuthenticated } = useContext(AuthContext);
1091
+ ```
1092
+
1093
+ **Behaviors**: Validates token on mount. Checks expiration every 60s. Auto-refreshes if `refreshToken` provided. Stores in localStorage as `"auth"`.
1094
+
1095
+ ### CrudProvider / useCrud\<T\>
1096
+
1097
+ Generic CRUD context with automatic pagination, URL parameter sync, and snackbar notifications.
1098
+
1099
+ ```typescript
1100
+ interface CrudProviderProps<T> {
1101
+ children: ReactNode;
1102
+ getPromise?: Function | { execute: Function; successMessage?: string; errorMessage?: string | ((error: any) => string) };
1103
+ getItemPromise?: Function | { execute: Function; successMessage?: string; errorMessage?: string | ((error: any) => string) };
1104
+ postPromise?: Function | { execute: Function; successMessage?: string; errorMessage?: string | ((error: any) => string) };
1105
+ putPromise?: Function | { execute: Function; successMessage?: string; errorMessage?: string | ((error: any) => string) };
1106
+ deletePromise?: Function | { execute: Function; successMessage?: string; errorMessage?: string | ((error: any) => string) };
1107
+ urlParams?: Array<string>; // URL params to watch. default: []
1108
+ limit?: number; // Items per page. default: 15
1109
+ pageParam?: string; // URL page param. default: "pagina"
1110
+ singleItemId?: string | number;
1111
+ extraData?: Record<string, any>;
1112
+ }
1113
+
1114
+ interface CrudContextType<T> {
1115
+ list: Array<T> | undefined;
1116
+ item: T | undefined;
1117
+ page: number;
1118
+ pages: number;
1119
+ total: number;
1120
+ limit: number;
1121
+ isLoading: boolean;
1122
+ pagination: ReactNode; // Pre-built Pagination component
1123
+ params: Record<string, any>;
1124
+ extraData?: Record<string, any>;
1125
+ setExtraData: Dispatch<SetStateAction<Record<string, any> | undefined>>;
1126
+ fetchItems: { execute: (params?: Record<string, any>) => Promise<void>; isLoading: boolean };
1127
+ fetchItem: { execute: (params?: Record<string, any> | string | number) => Promise<T | undefined>; isLoading: boolean };
1128
+ createItem: { execute: (item: T) => Promise<T | undefined | null>; isLoading: boolean };
1129
+ updateItem: { execute: (item: T) => Promise<T | undefined | null>; isLoading: boolean };
1130
+ deleteItem: { execute: (item: T) => Promise<void>; isLoading: boolean };
1131
+ }
1132
+ ```
1133
+
1134
+ ```tsx
1135
+ <CrudProvider<User>
1136
+ getPromise={(params) => apiClient.get({ url: "/api/users", params })}
1137
+ getItemPromise={(id) => apiClient.get({ url: `/api/users/${id}` })}
1138
+ postPromise={{ execute: (item) => apiClient.post({ url: "/api/users", body: item }), successMessage: "Usuario creado" }}
1139
+ putPromise={{ execute: (item) => apiClient.put({ url: `/api/users/${item.id}`, body: item }), successMessage: "Usuario actualizado" }}
1140
+ deletePromise={{ execute: (item) => apiClient.del({ url: `/api/users/${item.id}` }), successMessage: "Usuario eliminado" }}
1141
+ urlParams={["nombre", "estado"]}
1142
+ limit={20}
1143
+ >
1144
+ <UserList />
1145
+ </CrudProvider>
1146
+
1147
+ // In child components
1148
+ const { list, isLoading, pagination, createItem, deleteItem } = useCrud<User>();
1149
+ ```
1150
+
1151
+ **Behaviors**: Auto-fetches when URL params change. Resets pagination on filter change. Shows snackbar on success/error.
1152
+
1153
+ ### SnackbarProvider / useSnackbar
1154
+
1155
+ Manages toast notifications.
1156
+
1157
+ ```typescript
1158
+ interface SnackbarActionsType {
1159
+ showSnackbar: (
1160
+ message: string,
1161
+ variant?: SnackbarVariant,
1162
+ options?: { duration?: number; icon?: string; iconLabel?: string }
1163
+ ) => void;
1164
+ removeSnackbar: (id: string) => void;
1165
+ }
1166
+ ```
1167
+
1168
+ ```tsx
1169
+ // App root
1170
+ <SnackbarProvider>
1171
+ <SnackbarContainer position="bottom-right" maxSnackbars={3} />
1172
+ <App />
1173
+ </SnackbarProvider>
1174
+
1175
+ // In components
1176
+ const { showSnackbar } = useSnackbar();
1177
+ showSnackbar("Guardado exitosamente", "success");
1178
+ showSnackbar("Error de conexión", "danger", { duration: 5000 });
1179
+ ```
1180
+
1181
+ ### AppLayoutProvider / useAppLayout
1182
+
1183
+ Combines ThemeProvider + SnackbarProvider + AppLayout into a single provider.
1184
+
1185
+ ```typescript
1186
+ interface AppLayoutProviderProps {
1187
+ children: ReactNode;
1188
+ initialTheme?: string | Theme;
1189
+ storageKey?: string;
1190
+ forceInitialTheme?: boolean;
1191
+ // Densidad global (propagada al ThemeProvider interno)
1192
+ density?: "comfortable" | "compact" | "dense"; // default: "comfortable"
1193
+ densityStorageKey?: string; // default: "flysoft-density"
1194
+ forceInitialDensity?: boolean;
1195
+ onDensityChange?: (density: "comfortable" | "compact" | "dense") => void;
1196
+ initialNavbar?: NavbarInterface;
1197
+ initialLeftDrawer?: LeftDrawerInterface;
1198
+ initialContentFooter?: ReactNode;
1199
+ className?: string;
1200
+ }
1201
+
1202
+ interface AppLayoutContextType extends ThemeContextType {
1203
+ navbar: NavbarInterface | undefined;
1204
+ leftDrawer: LeftDrawerInterface | undefined;
1205
+ contentFooter: ReactNode | undefined;
1206
+ className: string;
1207
+ setNavbar: Dispatch<SetStateAction<NavbarInterface | undefined>>;
1208
+ setLeftDrawer: Dispatch<SetStateAction<LeftDrawerInterface | undefined>>;
1209
+ setContentFooter: (node: ReactNode | undefined) => void;
1210
+ setClassName: (className: string) => void;
1211
+ setNavBarLeftNode: (node: ReactNode | undefined) => void;
1212
+ setNavbarRightNode: (node: ReactNode | undefined) => void;
1213
+ }
1214
+ ```
1215
+
1216
+ ```tsx
1217
+ <AppLayoutProvider
1218
+ initialTheme="light"
1219
+ density="dense" // CRUDs / dashboards / pantallas con mucha info
1220
+ initialNavbar={{ navBarLeftNode: <h1>Mi App</h1>, fullWidthNavbar: true }}
1221
+ initialLeftDrawer={{ contentNode: <nav>...</nav> }}
1222
+ >
1223
+ <Routes />
1224
+ </AppLayoutProvider>
1225
+
1226
+ // In pages - dynamically update layout
1227
+ const { setNavBarLeftNode, setNavbarRightNode } = useAppLayout();
1228
+ useEffect(() => {
1229
+ setNavBarLeftNode(<h1>Dashboard</h1>);
1230
+ }, []);
1231
+ ```
1232
+
1233
+ ---
1234
+
1235
+ ## Hooks
1236
+
1237
+ ### useThemeOverride
1238
+
1239
+ Applies granular CSS variable overrides without changing the entire theme.
1240
+
1241
+ ```typescript
1242
+ function useThemeOverride(options?: {
1243
+ scope?: "global" | "local"; // default: "global"
1244
+ element?: HTMLElement | null;
1245
+ prefix?: string; // default: "flysoft"
1246
+ }): {
1247
+ applyOverride: (overrides: Record<string, string | number>) => void;
1248
+ revertOverride: (keys: string[]) => void;
1249
+ revertAllOverrides: () => void;
1250
+ getCSSVariable: (key: string) => string | null;
1251
+ isOverrideApplied: (key: string) => boolean;
1252
+ appliedOverridesCount: number;
1253
+ }
1254
+ ```
1255
+
1256
+ ### useTemporaryOverride
1257
+
1258
+ Applies CSS variable overrides that auto-revert after a duration.
1259
+
1260
+ ```typescript
1261
+ function useTemporaryOverride(
1262
+ overrides: Record<string, string | number>,
1263
+ duration?: number, // default: 3000
1264
+ options?: { scope?: "global" | "local"; element?: HTMLElement | null; prefix?: string }
1265
+ ): { applyTemporaryOverride: () => Function }
1266
+ ```
1267
+
1268
+ ### useBreakpoint
1269
+
1270
+ Returns current viewport breakpoint and device type.
1271
+
1272
+ ```typescript
1273
+ type Breakpoint = "xs" | "sm" | "md" | "lg" | "xl" | "2xl";
1274
+
1275
+ function useBreakpoint(): {
1276
+ breakpoint: Breakpoint;
1277
+ windowSize: { width: number; height: number };
1278
+ isMobile: boolean; // xs or sm
1279
+ isTablet: boolean; // md
1280
+ isDesktop: boolean; // lg, xl, or 2xl
1281
+ }
1282
+ ```
1283
+
1284
+ ### useElementScroll
1285
+
1286
+ Tracks scroll position and direction with requestAnimationFrame optimization.
1287
+
1288
+ ```typescript
1289
+ function useElementScroll(elementRef: React.RefObject<HTMLElement | null>): {
1290
+ scrollY: number;
1291
+ scrollDirection: "up" | "down" | null;
1292
+ }
1293
+ ```
1294
+
1295
+ ### useAsyncRequest
1296
+
1297
+ Manages async operations with loading state and snackbar notifications.
1298
+
1299
+ ```typescript
1300
+ interface AsyncRequestOptions {
1301
+ successMessage?: string;
1302
+ errorMessage?: string | ((error: any) => string);
1303
+ successVariant?: SnackbarVariant; // default: "success"
1304
+ errorVariant?: SnackbarVariant; // default: "danger"
1305
+ onSuccess?: (data: any) => void;
1306
+ onError?: (error: any) => void;
1307
+ onFinally?: () => void;
1308
+ }
1309
+
1310
+ function useAsyncRequest(options?: AsyncRequestOptions): {
1311
+ isLoading: boolean;
1312
+ execute: <T>(requestFn: () => Promise<T>) => Promise<T | undefined>;
1313
+ setLoading: (loading: boolean) => void;
1314
+ }
1315
+ ```
1316
+
1317
+ ```tsx
1318
+ const { execute, isLoading } = useAsyncRequest({
1319
+ successMessage: "Guardado exitosamente",
1320
+ errorMessage: (err) => getErrorMessage(err),
1321
+ });
1322
+ await execute(() => apiClient.post({ url: "/api/data", body: formData }));
1323
+ ```
1324
+
1325
+ ### useEnum
1326
+
1327
+ Converts TypeScript enums to arrays for form select options.
1328
+
1329
+ ```typescript
1330
+ function useEnum(baseEnum: any): {
1331
+ getArray: () => Array<NameValueInterface<number>>;
1332
+ getInstance: (id: number) => NameValueInterface<number> | undefined;
1333
+ }
1334
+ ```
1335
+
1336
+ ### useGlobalThemeStyles
1337
+
1338
+ Applies theme colors to `<body>` and `<html>` for full-page theming. No return value.
1339
+
1340
+ ```tsx
1341
+ function useGlobalThemeStyles(): void;
1342
+ ```
1343
+
1344
+ ---
1345
+
1346
+ ## Services
1347
+
1348
+ ### apiClient
1349
+
1350
+ Singleton HTTP client (Axios-based) with automatic Bearer token injection.
1351
+
1352
+ ```typescript
1353
+ // Main methods
1354
+ apiClient.get<T>(options: { url: string; params?: Record<string, unknown>; headers?: Record<string, string> }): Promise<T>;
1355
+ apiClient.post<T>(options: { url: string; body?: unknown; headers?: Record<string, string> }): Promise<T>;
1356
+ apiClient.put<T>(options: { url: string; body?: unknown; headers?: Record<string, string> }): Promise<T>;
1357
+ apiClient.del<T>(options: { url: string; headers?: Record<string, string> }): Promise<T>;
1358
+
1359
+ // File operations
1360
+ apiClient.getFile(options): Promise<{ data: Blob; headers: any }>;
1361
+ apiClient.getFileAsUrl(options): Promise<string>;
1362
+ apiClient.openFile(options): Promise<void>;
1363
+ apiClient.downloadFile(options): Promise<void>;
1364
+ apiClient.uploadFile<T>(options: { url: string; files: FileList | File[]; headers?: { paramName?: string } }): Promise<T>;
1365
+
1366
+ // Token management
1367
+ setApiClientTokenProvider(provider?: () => string | undefined): void;
1368
+ clearApiClientTokenProvider(): void;
1369
+
1370
+ // Create isolated instances
1371
+ createApiClient(config?: { baseURL?: string; timeout?: number; headers?: Record<string, string> }): ApiClientService;
1372
+ ```
1373
+
1374
+ ```tsx
1375
+ // Setup token globally
1376
+ setApiClientTokenProvider(() => user?.token?.accessToken);
1377
+
1378
+ // API calls
1379
+ const users = await apiClient.get<User[]>({ url: "/api/users", params: { page: 1 } });
1380
+ await apiClient.post({ url: "/api/users", body: { name: "Juan" } });
1381
+ await apiClient.downloadFile({ url: "/api/reports/pdf" });
1382
+ await apiClient.uploadFile({ url: "/api/upload", files: fileInput.files });
1383
+ ```
1384
+
1385
+ ---
1386
+
1387
+ ## Helpers
1388
+
1389
+ | Function | Signature | Description |
1390
+ |----------|-----------|-------------|
1391
+ | `currencyFormat` | `(value: number) => string` | Formats as `"1.234,56"` (es-AR locale) |
1392
+ | `getErrorMessage` | `(error: any) => string` | Extracts message from AxiosError. Default: `"Ha ocurrido un error"` |
1393
+ | `getInitialLetters` | `(text: string) => string` | `"Juan Pérez"` → `"JP"` |
1394
+ | `getQueryString` | `(params: URLSearchParams, newParams: any) => string` | Merges params, returns `"?key=value"` |
1395
+ | `objectToQueryString` | `(source: any) => string` | Object to `"a=1&b=2"` (no leading `?`) |
1396
+ | `queryStringToObject` | `(params: string) => Record<string, string>` | `"a=1&b=2"` → `{a: "1", b: "2"}` |
1397
+ | `nameValueArrayToObject` | `<T>(arr: NameValueInterface<T>[]) => Record<string, T>` | Array of {name, value} to object |
1398
+ | `promiseMapper` | `<T, K>(promise, mapper) => Promise<K \| K[] \| PaginationInterface<K>>` | Maps promise results (arrays, pagination, single) |
1399
+ | `RegularExpressions` | Object | `.email`, `.dateString`, `.password(config)` regex patterns |
1400
+
1401
+ ## Interfaces
1402
+
1403
+ ```typescript
1404
+ interface NameValueInterface<T> {
1405
+ name: string;
1406
+ value: T;
1407
+ extras?: any;
1408
+ }
1409
+
1410
+ interface PaginationInterface<T> {
1411
+ list: Array<T>;
1412
+ limit: number;
1413
+ page: number;
1414
+ pages: number;
1415
+ total: number;
1416
+ }
1417
+ ```
1418
+
1419
+ ---
1420
+
1421
+ ## Templates
1422
+
1423
+ ### LoginForm
1424
+
1425
+ ```typescript
1426
+ interface LoginFormProps {
1427
+ onSubmit?: (data: { email: string; password: string }) => void;
1428
+ loading?: boolean;
1429
+ error?: string;
1430
+ className?: string;
1431
+ }
1432
+ ```
1433
+
1434
+ ### RegistrationForm
1435
+
1436
+ ```typescript
1437
+ interface RegistrationFormProps {
1438
+ onSubmit?: (data: { firstName: string; lastName: string; email: string; password: string; confirmPassword: string }) => void;
1439
+ loading?: boolean;
1440
+ error?: string;
1441
+ className?: string;
1442
+ }
1443
+ ```
1444
+
1445
+ ### ContactForm
1446
+
1447
+ ```typescript
1448
+ interface ContactFormProps {
1449
+ onSubmit?: (data: { name: string; email: string; subject: string; message: string }) => void;
1450
+ loading?: boolean;
1451
+ success?: boolean;
1452
+ error?: string;
1453
+ className?: string;
1454
+ }
1455
+ ```
1456
+
1457
+ ### DashboardLayout
1458
+
1459
+ ```typescript
1460
+ interface DashboardStat {
1461
+ title: string;
1462
+ value: string | number;
1463
+ change?: string; // e.g. "+12%"
1464
+ changeType?: "positive" | "negative" | "neutral";
1465
+ icon?: string;
1466
+ }
1467
+
1468
+ interface DashboardLayoutProps {
1469
+ title: string; // required
1470
+ subtitle?: string;
1471
+ stats?: DashboardStat[];
1472
+ actions?: React.ReactNode;
1473
+ children: React.ReactNode; // required
1474
+ className?: string;
1475
+ }
1476
+ ```
1477
+
1478
+ ### SidebarLayout
1479
+
1480
+ ```typescript
1481
+ interface MenuItem {
1482
+ label: string;
1483
+ icon: string;
1484
+ href: string;
1485
+ badge?: string | number;
1486
+ children?: MenuItem[];
1487
+ }
1488
+
1489
+ interface User {
1490
+ name: string;
1491
+ email?: string;
1492
+ avatar?: string;
1493
+ }
1494
+
1495
+ interface SidebarLayoutProps {
1496
+ title: string; // required
1497
+ menuItems: MenuItem[]; // required
1498
+ user: User; // required
1499
+ children: React.ReactNode; // required
1500
+ className?: string;
1501
+ onLogout?: () => void;
1502
+ }
1503
+ ```
1504
+
1505
+ ### FormPattern
1506
+
1507
+ ```typescript
1508
+ interface FormField {
1509
+ name: string;
1510
+ label: string;
1511
+ type?: string; // default: "text"
1512
+ placeholder?: string;
1513
+ icon?: string;
1514
+ required?: boolean;
1515
+ validation?: (value: string) => string | undefined;
1516
+ multiline?: boolean;
1517
+ rows?: number; // default: 4
1518
+ }
1519
+
1520
+ interface FormPatternProps {
1521
+ title: string; // required
1522
+ subtitle?: string;
1523
+ fields: FormField[]; // required
1524
+ onSubmit: (data: Record<string, string>) => void; // required
1525
+ submitText?: string; // default: "Enviar"
1526
+ submitIcon?: string; // default: "fa-paper-plane"
1527
+ loading?: boolean;
1528
+ error?: string;
1529
+ success?: boolean;
1530
+ className?: string;
1531
+ gridCols?: 1 | 2; // default: 1
1532
+ }
1533
+ ```