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