flysoft-react-ui 1.2.10 → 1.3.0

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