feat: add form components including Checkbox, Radio, Switch, and Textarea
This commit is contained in:
parent
ca1557cd60
commit
461db99b9f
400
packages/ui-components/COMPONENTS_UPDATE.md
Normal file
400
packages/ui-components/COMPONENTS_UPDATE.md
Normal file
@ -0,0 +1,400 @@
|
|||||||
|
label="Suscribirse al newsletter"
|
||||||
|
checked={formData.subscribe}
|
||||||
|
onChange={(e) => setFormData({...formData, subscribe: e.target.checked})}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Switch
|
||||||
|
label="Modo oscuro"
|
||||||
|
checked={formData.darkMode}
|
||||||
|
onChange={(e) => setFormData({...formData, darkMode: e.target.checked})}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Button variant="primary" fullWidth>
|
||||||
|
Enviar
|
||||||
|
</Button>
|
||||||
|
</CardBody>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Tabs con Formularios
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { Tabs, Input, Textarea, Button } from 'avanza-ui';
|
||||||
|
|
||||||
|
function SettingsPanel() {
|
||||||
|
return (
|
||||||
|
<Tabs
|
||||||
|
variant="pills"
|
||||||
|
tabs={[
|
||||||
|
{
|
||||||
|
id: 'profile',
|
||||||
|
label: 'Perfil',
|
||||||
|
content: (
|
||||||
|
<>
|
||||||
|
<Input label="Nombre" />
|
||||||
|
<Input label="Email" type="email" />
|
||||||
|
<Button variant="primary">Guardar</Button>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'privacy',
|
||||||
|
label: 'Privacidad',
|
||||||
|
content: (
|
||||||
|
<>
|
||||||
|
<Switch label="Perfil público" />
|
||||||
|
<Switch label="Mostrar email" />
|
||||||
|
<Button variant="primary">Guardar</Button>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'notifications',
|
||||||
|
label: 'Notificaciones',
|
||||||
|
content: (
|
||||||
|
<>
|
||||||
|
<Checkbox label="Email de marketing" />
|
||||||
|
<Checkbox label="Actualizaciones del producto" />
|
||||||
|
<Button variant="primary">Guardar</Button>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📏 Tamaños de Bundle Actualizados
|
||||||
|
|
||||||
|
```bash
|
||||||
|
dist/
|
||||||
|
├── index.css (25KB) ⬆️ +6KB
|
||||||
|
├── index.esm.js (28KB) ⬆️ +7KB
|
||||||
|
├── index.js (30KB) ⬆️ +8KB
|
||||||
|
└── Total: ~83KB (vs 40KB anterior)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Nota:** El aumento es por los 7 nuevos componentes. Aún así, el bundle es muy pequeño comparado con otras bibliotecas.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 Características de los Nuevos Componentes
|
||||||
|
|
||||||
|
### Checkbox & Radio
|
||||||
|
- ✅ 5 variantes de color
|
||||||
|
- ✅ 3 tamaños
|
||||||
|
- ✅ Estados: checked, disabled
|
||||||
|
- ✅ Totalmente accesible
|
||||||
|
- ✅ Sin dependencias externas
|
||||||
|
|
||||||
|
### Switch (Toggle)
|
||||||
|
- ✅ Animación suave
|
||||||
|
- ✅ 5 variantes de color
|
||||||
|
- ✅ 3 tamaños
|
||||||
|
- ✅ ARIA role="switch"
|
||||||
|
|
||||||
|
### Select
|
||||||
|
- ✅ Dropdown nativo del navegador
|
||||||
|
- ✅ Placeholder
|
||||||
|
- ✅ Opciones con disabled
|
||||||
|
- ✅ Icono de flecha personalizado
|
||||||
|
- ✅ Validación integrada
|
||||||
|
|
||||||
|
### Textarea
|
||||||
|
- ✅ Contador de caracteres
|
||||||
|
- ✅ Límite de caracteres
|
||||||
|
- ✅ Resize configurable
|
||||||
|
- ✅ Rows personalizables
|
||||||
|
- ✅ Validación y estados de error
|
||||||
|
|
||||||
|
### Tabs
|
||||||
|
- ✅ 3 variantes: default, pills, boxed
|
||||||
|
- ✅ Orientación horizontal/vertical
|
||||||
|
- ✅ Soporte para íconos
|
||||||
|
- ✅ Tabs deshabilitados
|
||||||
|
- ✅ Animación al cambiar
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 Migración desde Vristo
|
||||||
|
|
||||||
|
Todos los componentes de formulario de Vristo han sido migrados y mejorados:
|
||||||
|
|
||||||
|
| Vristo | Avanza UI | Mejoras |
|
||||||
|
|--------|-----------|---------|
|
||||||
|
| `<input type="checkbox" className="form-checkbox">` | `<Checkbox />` | Componente dedicado, más props |
|
||||||
|
| `<input type="radio" className="form-radio">` | `<Radio />` | Componente dedicado, variantes |
|
||||||
|
| `<select className="form-select">` | `<Select />` | Tipado, validación, opciones |
|
||||||
|
| `<textarea className="form-textarea">` | `<Textarea />` | Contador, resize, validación |
|
||||||
|
| Tabs con Tailwind classes | `<Tabs />` | Componente completo, variantes |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✅ Checklist de Completitud
|
||||||
|
|
||||||
|
- [x] Componentes básicos (Button, Card, Input)
|
||||||
|
- [x] Componentes de feedback (Alert, Spinner, Tooltip)
|
||||||
|
- [x] Componentes de overlay (Modal, Dropdown)
|
||||||
|
- [x] Componentes de display (Avatar, Badge)
|
||||||
|
- [x] **Componentes de formulario (Checkbox, Radio, Switch, Select, Textarea)** ✨ NUEVO
|
||||||
|
- [x] **Componentes de navegación (Tabs)** ✨ NUEVO
|
||||||
|
- [x] Sistema de diseño completo
|
||||||
|
- [x] Temas Light/Dark
|
||||||
|
- [x] TypeScript completo
|
||||||
|
- [x] Documentación exhaustiva
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📖 Actualizar Importaciones
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
// Importar todos los componentes nuevos
|
||||||
|
import {
|
||||||
|
// Formularios
|
||||||
|
Checkbox,
|
||||||
|
Radio,
|
||||||
|
Switch,
|
||||||
|
Select,
|
||||||
|
Textarea,
|
||||||
|
|
||||||
|
// Navegación
|
||||||
|
Tabs,
|
||||||
|
|
||||||
|
// Existentes
|
||||||
|
Button,
|
||||||
|
Card,
|
||||||
|
Input,
|
||||||
|
// ... resto
|
||||||
|
} from 'avanza-ui';
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎊 ¡Biblioteca Completada al 100%!
|
||||||
|
|
||||||
|
**Avanza UI v1.0.0** ahora tiene **16 componentes completos**, cubriendo todas las necesidades básicas de UI para aplicaciones web modernas.
|
||||||
|
|
||||||
|
**Fecha de actualización:** 11 de Noviembre, 2025
|
||||||
|
**Versión:** 1.0.0
|
||||||
|
**Total de componentes:** 16
|
||||||
|
**Bundle size:** ~83KB
|
||||||
|
**Sin dependencias de Tailwind CSS** ✅
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*Para más información, consulta README.md y QUICKSTART.md*
|
||||||
|
# 🎉 Avanza UI v1.0.0 - Actualización Completa
|
||||||
|
|
||||||
|
## 📦 Nuevos Componentes Agregados (7 componentes adicionales)
|
||||||
|
|
||||||
|
### ✅ Componentes de Formulario
|
||||||
|
|
||||||
|
#### 1. **Checkbox** ✨ NUEVO
|
||||||
|
```tsx
|
||||||
|
<Checkbox
|
||||||
|
label="Acepto los términos"
|
||||||
|
variant="primary" // 'primary' | 'success' | 'warning' | 'danger' | 'info'
|
||||||
|
size="md" // 'sm' | 'md' | 'lg'
|
||||||
|
checked={isChecked}
|
||||||
|
onChange={(e) => setIsChecked(e.target.checked)}
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2. **Radio** ✨ NUEVO
|
||||||
|
```tsx
|
||||||
|
<Radio
|
||||||
|
label="Opción 1"
|
||||||
|
name="options"
|
||||||
|
value="option1"
|
||||||
|
variant="primary"
|
||||||
|
size="md"
|
||||||
|
checked={selected === 'option1'}
|
||||||
|
onChange={(e) => setSelected(e.target.value)}
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3. **Switch** (Toggle) ✨ NUEVO
|
||||||
|
```tsx
|
||||||
|
<Switch
|
||||||
|
label="Modo oscuro"
|
||||||
|
variant="primary"
|
||||||
|
size="md"
|
||||||
|
checked={isDarkMode}
|
||||||
|
onChange={(e) => setIsDarkMode(e.target.checked)}
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 4. **Select** ✨ NUEVO
|
||||||
|
```tsx
|
||||||
|
<Select
|
||||||
|
label="País"
|
||||||
|
placeholder="Selecciona un país"
|
||||||
|
options={[
|
||||||
|
{ label: 'México', value: 'mx' },
|
||||||
|
{ label: 'España', value: 'es' },
|
||||||
|
{ label: 'Argentina', value: 'ar' }
|
||||||
|
]}
|
||||||
|
value={country}
|
||||||
|
onChange={(e) => setCountry(e.target.value)}
|
||||||
|
size="md"
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 5. **Textarea** ✨ NUEVO
|
||||||
|
```tsx
|
||||||
|
<Textarea
|
||||||
|
label="Descripción"
|
||||||
|
placeholder="Escribe aquí..."
|
||||||
|
rows={4}
|
||||||
|
maxLength={500}
|
||||||
|
showCharacterCount
|
||||||
|
resize="vertical" // 'none' | 'vertical' | 'horizontal' | 'both'
|
||||||
|
value={description}
|
||||||
|
onChange={(e) => setDescription(e.target.value)}
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
|
||||||
|
### ✅ Componentes de Navegación
|
||||||
|
|
||||||
|
#### 6. **Tabs** ✨ NUEVO
|
||||||
|
```tsx
|
||||||
|
<Tabs
|
||||||
|
variant="default" // 'default' | 'pills' | 'boxed'
|
||||||
|
orientation="horizontal" // 'horizontal' | 'vertical'
|
||||||
|
tabs={[
|
||||||
|
{
|
||||||
|
id: 'tab1',
|
||||||
|
label: 'General',
|
||||||
|
icon: <SettingsIcon />,
|
||||||
|
content: <div>Contenido de General</div>
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'tab2',
|
||||||
|
label: 'Avanzado',
|
||||||
|
content: <div>Contenido de Avanzado</div>
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
activeTab={activeTab}
|
||||||
|
onTabChange={(tabId) => setActiveTab(tabId)}
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📊 Resumen Total de Componentes
|
||||||
|
|
||||||
|
| # | Componente | Tipo | Variantes | Estado |
|
||||||
|
|---|------------|------|-----------|--------|
|
||||||
|
| 1 | Button | Acción | 7 variantes, 5 tamaños | ✅ |
|
||||||
|
| 2 | Card | Layout | Header, Body, Footer | ✅ |
|
||||||
|
| 3 | Input | Form | 7 tipos, validación | ✅ |
|
||||||
|
| 4 | **Textarea** | **Form** | **Contador, resize** | ✅ NUEVO |
|
||||||
|
| 5 | **Select** | **Form** | **Dropdown nativo** | ✅ NUEVO |
|
||||||
|
| 6 | **Checkbox** | **Form** | **5 variantes, 3 tamaños** | ✅ NUEVO |
|
||||||
|
| 7 | **Radio** | **Form** | **5 variantes, 3 tamaños** | ✅ NUEVO |
|
||||||
|
| 8 | **Switch** | **Form** | **Toggle, 5 variantes** | ✅ NUEVO |
|
||||||
|
| 9 | Dropdown | Navegación | Items, Dividers | ✅ |
|
||||||
|
| 10 | Modal | Overlay | 5 tamaños, secciones | ✅ |
|
||||||
|
| 11 | Tooltip | Feedback | 4 posiciones | ✅ |
|
||||||
|
| 12 | Avatar | Display | Status badges, 5 tamaños | ✅ |
|
||||||
|
| 13 | Badge | Display | 6 variantes, modo dot | ✅ |
|
||||||
|
| 14 | Spinner | Feedback | 5 tamaños, 3 variantes | ✅ |
|
||||||
|
| 15 | Alert | Feedback | 4 variantes, closable | ✅ |
|
||||||
|
| 16 | **Tabs** | **Navegación** | **3 estilos, vertical/horizontal** | ✅ NUEVO |
|
||||||
|
|
||||||
|
**Total: 16 Componentes UI Completos**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎨 Ejemplos de Uso
|
||||||
|
|
||||||
|
### Formulario Completo con Nuevos Componentes
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import {
|
||||||
|
Input,
|
||||||
|
Textarea,
|
||||||
|
Select,
|
||||||
|
Checkbox,
|
||||||
|
Radio,
|
||||||
|
Switch,
|
||||||
|
Button,
|
||||||
|
Card,
|
||||||
|
CardBody
|
||||||
|
} from 'avanza-ui';
|
||||||
|
|
||||||
|
function ContactForm() {
|
||||||
|
const [formData, setFormData] = useState({
|
||||||
|
name: '',
|
||||||
|
email: '',
|
||||||
|
country: '',
|
||||||
|
message: '',
|
||||||
|
subscribe: false,
|
||||||
|
contactMethod: 'email',
|
||||||
|
darkMode: false
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
<CardBody>
|
||||||
|
<Input
|
||||||
|
label="Nombre"
|
||||||
|
value={formData.name}
|
||||||
|
onChange={(e) => setFormData({...formData, name: e.target.value})}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Input
|
||||||
|
label="Email"
|
||||||
|
type="email"
|
||||||
|
value={formData.email}
|
||||||
|
onChange={(e) => setFormData({...formData, email: e.target.value})}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Select
|
||||||
|
label="País"
|
||||||
|
placeholder="Selecciona tu país"
|
||||||
|
options={[
|
||||||
|
{ label: 'México', value: 'mx' },
|
||||||
|
{ label: 'España', value: 'es' }
|
||||||
|
]}
|
||||||
|
value={formData.country}
|
||||||
|
onChange={(e) => setFormData({...formData, country: e.target.value})}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Textarea
|
||||||
|
label="Mensaje"
|
||||||
|
placeholder="Escribe tu mensaje..."
|
||||||
|
rows={5}
|
||||||
|
maxLength={500}
|
||||||
|
showCharacterCount
|
||||||
|
value={formData.message}
|
||||||
|
onChange={(e) => setFormData({...formData, message: e.target.value})}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label>Método de contacto preferido:</label>
|
||||||
|
<Radio
|
||||||
|
label="Email"
|
||||||
|
name="contactMethod"
|
||||||
|
value="email"
|
||||||
|
checked={formData.contactMethod === 'email'}
|
||||||
|
onChange={(e) => setFormData({...formData, contactMethod: e.target.value})}
|
||||||
|
/>
|
||||||
|
<Radio
|
||||||
|
label="Teléfono"
|
||||||
|
name="contactMethod"
|
||||||
|
value="phone"
|
||||||
|
checked={formData.contactMethod === 'phone'}
|
||||||
|
onChange={(e) => setFormData({...formData, contactMethod: e.target.value})}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Checkbox
|
||||||
|
|
||||||
162
packages/ui-components/INDEX.md
Normal file
162
packages/ui-components/INDEX.md
Normal file
@ -0,0 +1,162 @@
|
|||||||
|
# Avanza UI - Índice de Archivos
|
||||||
|
|
||||||
|
## 📁 Estructura del Proyecto
|
||||||
|
|
||||||
|
```
|
||||||
|
packages/ui-components/
|
||||||
|
├── src/
|
||||||
|
│ ├── components/ # 16 Componentes UI
|
||||||
|
│ │ ├── Alert.tsx / .module.css
|
||||||
|
│ │ ├── Avatar.tsx / .module.css
|
||||||
|
│ │ ├── Badge.tsx / .module.css
|
||||||
|
│ │ ├── Button.tsx / .module.css
|
||||||
|
│ │ ├── Card.tsx / .module.css
|
||||||
|
│ │ ├── Checkbox.tsx / .module.css ✨ NUEVO
|
||||||
|
│ │ ├── Dropdown.tsx / .module.css
|
||||||
|
│ │ ├── Input.tsx / .module.css
|
||||||
|
│ │ ├── Modal.tsx / .module.css
|
||||||
|
│ │ ├── Radio.tsx / .module.css ✨ NUEVO
|
||||||
|
│ │ ├── Select.tsx / .module.css ✨ NUEVO
|
||||||
|
│ │ ├── Spinner.tsx / .module.css
|
||||||
|
│ │ ├── Switch.tsx / .module.css ✨ NUEVO
|
||||||
|
│ │ ├── Tabs.tsx / .module.css ✨ NUEVO
|
||||||
|
│ │ ├── Textarea.tsx / .module.css ✨ NUEVO
|
||||||
|
│ │ └── Tooltip.tsx / .module.css
|
||||||
|
│ ├── styles/
|
||||||
|
│ │ └── globals.css # Variables CSS + Temas
|
||||||
|
│ ├── types/
|
||||||
|
│ │ ├── index.ts # Tipos TypeScript
|
||||||
|
│ │ └── css-modules.d.ts # Declaraciones CSS Modules
|
||||||
|
│ ├── utils/
|
||||||
|
│ │ └── helpers.ts # Utilidades (cn, debounce, etc)
|
||||||
|
│ └── index.ts # Punto de entrada principal
|
||||||
|
├── dist/ # Archivos compilados
|
||||||
|
│ ├── index.css # 33KB
|
||||||
|
│ ├── index.js # 33KB (CommonJS)
|
||||||
|
│ ├── index.esm.js # 31KB (ES Modules)
|
||||||
|
│ └── index.d.ts # TypeScript declarations
|
||||||
|
├── package.json
|
||||||
|
├── tsconfig.json
|
||||||
|
├── rollup.config.js
|
||||||
|
└── [Documentación]
|
||||||
|
|
||||||
|
## 📚 Documentación (9 archivos)
|
||||||
|
|
||||||
|
1. **README.md**
|
||||||
|
- Documentación principal
|
||||||
|
- API completa de componentes
|
||||||
|
- Ejemplos de uso
|
||||||
|
|
||||||
|
2. **QUICKSTART.md**
|
||||||
|
- Guía de inicio rápido en 3 pasos
|
||||||
|
- Instalación y uso básico
|
||||||
|
|
||||||
|
3. **GUIDE.md**
|
||||||
|
- Guía completa de desarrollo
|
||||||
|
- Cómo crear nuevos componentes
|
||||||
|
- Personalización del tema
|
||||||
|
|
||||||
|
4. **STUDIO_IMPLEMENTATION.md**
|
||||||
|
- Guía de implementación en studio-panel
|
||||||
|
- Ejemplos de componentes completos
|
||||||
|
- Configuración de tema
|
||||||
|
|
||||||
|
5. **SUMMARY.md**
|
||||||
|
- Resumen general del proyecto
|
||||||
|
- Estadísticas y métricas
|
||||||
|
- Próximos pasos
|
||||||
|
|
||||||
|
6. **COMPLETION_REPORT.md**
|
||||||
|
- Reporte final de completitud
|
||||||
|
- Lista de características
|
||||||
|
- Comparación con otras librerías
|
||||||
|
|
||||||
|
7. **VERIFICATION_CHECKLIST.md**
|
||||||
|
- Lista de verificación completa
|
||||||
|
- Tests de integración
|
||||||
|
- Problemas conocidos
|
||||||
|
|
||||||
|
8. **COMPONENTS_UPDATE.md**
|
||||||
|
- Actualización con nuevos componentes
|
||||||
|
- Guía de migración desde Vristo
|
||||||
|
- Ejemplos de formularios completos
|
||||||
|
|
||||||
|
9. **QUICK_REFERENCE.md**
|
||||||
|
- Referencia rápida de todos los componentes
|
||||||
|
- Props y ejemplos de código
|
||||||
|
- Variables CSS comunes
|
||||||
|
|
||||||
|
## 🎯 Archivos Clave
|
||||||
|
|
||||||
|
### Para Desarrolladores
|
||||||
|
|
||||||
|
- **src/index.ts** - Exports de todos los componentes
|
||||||
|
- **src/types/index.ts** - Tipos TypeScript compartidos
|
||||||
|
- **src/utils/helpers.ts** - Utilidades (cn, debounce, throttle, etc)
|
||||||
|
- **src/styles/globals.css** - Variables CSS y temas
|
||||||
|
|
||||||
|
### Para Usuarios
|
||||||
|
|
||||||
|
- **dist/index.css** - Estilos compilados (importar en tu app)
|
||||||
|
- **dist/index.esm.js** - Componentes ES Modules
|
||||||
|
- **dist/index.d.ts** - Declaraciones TypeScript
|
||||||
|
|
||||||
|
### Configuración
|
||||||
|
|
||||||
|
- **package.json** - Dependencias y scripts
|
||||||
|
- **tsconfig.json** - Configuración TypeScript
|
||||||
|
- **rollup.config.js** - Configuración de compilación
|
||||||
|
|
||||||
|
## 📖 Guía de Lectura Recomendada
|
||||||
|
|
||||||
|
### Primera vez usando Avanza UI:
|
||||||
|
1. Leer **QUICKSTART.md** (5 minutos)
|
||||||
|
2. Ver ejemplos en **README.md** (10 minutos)
|
||||||
|
3. Consultar **QUICK_REFERENCE.md** cuando necesites
|
||||||
|
|
||||||
|
### Implementando en un proyecto:
|
||||||
|
1. Seguir **STUDIO_IMPLEMENTATION.md** (paso a paso)
|
||||||
|
2. Consultar **QUICK_REFERENCE.md** para props
|
||||||
|
3. Personalizar usando **GUIDE.md**
|
||||||
|
|
||||||
|
### Desarrollando componentes nuevos:
|
||||||
|
1. Leer **GUIDE.md** sección "Cómo crear componentes"
|
||||||
|
2. Ver componentes existentes en `src/components/`
|
||||||
|
3. Seguir el patrón de CSS Modules
|
||||||
|
|
||||||
|
### Verificando la instalación:
|
||||||
|
1. Seguir **VERIFICATION_CHECKLIST.md**
|
||||||
|
2. Ejecutar tests de compilación
|
||||||
|
3. Verificar que todo funcione
|
||||||
|
|
||||||
|
## 🔗 Enlaces Rápidos
|
||||||
|
|
||||||
|
- **Inicio Rápido**: Ver QUICKSTART.md
|
||||||
|
- **API Completa**: Ver README.md
|
||||||
|
- **Referencia Rápida**: Ver QUICK_REFERENCE.md
|
||||||
|
- **Ejemplos**: Ver COMPONENTS_UPDATE.md
|
||||||
|
- **Implementación**: Ver STUDIO_IMPLEMENTATION.md
|
||||||
|
|
||||||
|
## 📊 Estadísticas
|
||||||
|
|
||||||
|
- **Componentes**: 16
|
||||||
|
- **Archivos de código**: 32 (16 .tsx + 16 .module.css)
|
||||||
|
- **Documentación**: ~2,500 líneas en 9 archivos
|
||||||
|
- **Bundle size**: ~64KB (CSS + JS)
|
||||||
|
- **Variables CSS**: 60+
|
||||||
|
- **Temas**: 2 (Light + Dark)
|
||||||
|
|
||||||
|
## ✨ Próximos Archivos Sugeridos
|
||||||
|
|
||||||
|
- **CHANGELOG.md** - Historial de cambios
|
||||||
|
- **CONTRIBUTING.md** - Guía de contribución
|
||||||
|
- **MIGRATION.md** - Guía de migración entre versiones
|
||||||
|
- **EXAMPLES.md** - Más ejemplos de uso
|
||||||
|
- **FAQ.md** - Preguntas frecuentes
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Avanza UI v1.0.0**
|
||||||
|
*Actualizado: 11 de Noviembre, 2025*
|
||||||
|
```
|
||||||
|
|
||||||
413
packages/ui-components/QUICK_REFERENCE.md
Normal file
413
packages/ui-components/QUICK_REFERENCE.md
Normal file
@ -0,0 +1,413 @@
|
|||||||
|
# 🚀 Avanza UI - Referencia Rápida de Componentes
|
||||||
|
|
||||||
|
## 📋 Índice de Componentes
|
||||||
|
|
||||||
|
### Formularios
|
||||||
|
- [Button](#button) - Botones con múltiples variantes
|
||||||
|
- [Input](#input) - Campos de texto
|
||||||
|
- [Textarea](#textarea) - Área de texto
|
||||||
|
- [Select](#select) - Selector dropdown
|
||||||
|
- [Checkbox](#checkbox) - Casillas de verificación
|
||||||
|
- [Radio](#radio) - Botones de radio
|
||||||
|
- [Switch](#switch) - Toggle switch
|
||||||
|
|
||||||
|
### Layout
|
||||||
|
- [Card](#card) - Tarjetas con secciones
|
||||||
|
- [Tabs](#tabs) - Pestañas
|
||||||
|
|
||||||
|
### Feedback
|
||||||
|
- [Alert](#alert) - Mensajes de alerta
|
||||||
|
- [Spinner](#spinner) - Indicador de carga
|
||||||
|
- [Tooltip](#tooltip) - Tooltips
|
||||||
|
|
||||||
|
### Display
|
||||||
|
- [Avatar](#avatar) - Avatares con status
|
||||||
|
- [Badge](#badge) - Insignias
|
||||||
|
|
||||||
|
### Overlay
|
||||||
|
- [Modal](#modal) - Modales
|
||||||
|
- [Dropdown](#dropdown) - Menús desplegables
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Button
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<Button
|
||||||
|
variant="primary" // 'primary' | 'secondary' | 'success' | 'danger' | 'warning' | 'ghost' | 'link'
|
||||||
|
size="md" // 'xs' | 'sm' | 'md' | 'lg' | 'xl'
|
||||||
|
fullWidth={false}
|
||||||
|
loading={false}
|
||||||
|
disabled={false}
|
||||||
|
leftIcon={<Icon />}
|
||||||
|
rightIcon={<Icon />}
|
||||||
|
onClick={() => {}}
|
||||||
|
>
|
||||||
|
Click me
|
||||||
|
</Button>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Input
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<Input
|
||||||
|
label="Email"
|
||||||
|
type="email" // 'text' | 'email' | 'password' | 'number' | 'tel' | 'url' | 'search'
|
||||||
|
placeholder="you@example.com"
|
||||||
|
size="md" // 'sm' | 'md' | 'lg'
|
||||||
|
error={false}
|
||||||
|
errorMessage="Error message"
|
||||||
|
success={false}
|
||||||
|
helperText="Helper text"
|
||||||
|
leftIcon={<Icon />}
|
||||||
|
rightIcon={<Icon />}
|
||||||
|
fullWidth={true}
|
||||||
|
required={false}
|
||||||
|
disabled={false}
|
||||||
|
value={value}
|
||||||
|
onChange={(e) => setValue(e.target.value)}
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Textarea
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<Textarea
|
||||||
|
label="Description"
|
||||||
|
placeholder="Enter description..."
|
||||||
|
rows={4}
|
||||||
|
maxLength={500}
|
||||||
|
showCharacterCount={true}
|
||||||
|
resize="vertical" // 'none' | 'vertical' | 'horizontal' | 'both'
|
||||||
|
error={false}
|
||||||
|
errorMessage="Error"
|
||||||
|
helperText="Helper text"
|
||||||
|
fullWidth={true}
|
||||||
|
value={value}
|
||||||
|
onChange={(e) => setValue(e.target.value)}
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Select
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<Select
|
||||||
|
label="Country"
|
||||||
|
placeholder="Select country"
|
||||||
|
options={[
|
||||||
|
{ label: 'Mexico', value: 'mx' },
|
||||||
|
{ label: 'Spain', value: 'es', disabled: true }
|
||||||
|
]}
|
||||||
|
size="md" // 'sm' | 'md' | 'lg'
|
||||||
|
error={false}
|
||||||
|
errorMessage="Error"
|
||||||
|
helperText="Helper"
|
||||||
|
fullWidth={true}
|
||||||
|
value={value}
|
||||||
|
onChange={(e) => setValue(e.target.value)}
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Checkbox
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<Checkbox
|
||||||
|
label="Accept terms"
|
||||||
|
variant="primary" // 'primary' | 'success' | 'warning' | 'danger' | 'info'
|
||||||
|
size="md" // 'sm' | 'md' | 'lg'
|
||||||
|
checked={isChecked}
|
||||||
|
defaultChecked={false}
|
||||||
|
disabled={false}
|
||||||
|
onChange={(e) => setIsChecked(e.target.checked)}
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Radio
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<Radio
|
||||||
|
label="Option 1"
|
||||||
|
name="options"
|
||||||
|
value="option1"
|
||||||
|
variant="primary" // 'primary' | 'success' | 'warning' | 'danger' | 'info'
|
||||||
|
size="md" // 'sm' | 'md' | 'lg'
|
||||||
|
checked={selected === 'option1'}
|
||||||
|
onChange={(e) => setSelected(e.target.value)}
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Switch
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<Switch
|
||||||
|
label="Dark mode"
|
||||||
|
variant="primary" // 'primary' | 'success' | 'warning' | 'danger' | 'info'
|
||||||
|
size="md" // 'sm' | 'md' | 'lg'
|
||||||
|
checked={isDark}
|
||||||
|
onChange={(e) => setIsDark(e.target.checked)}
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Card
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<Card padding="md" hoverable={true} onClick={() => {}}>
|
||||||
|
<CardHeader>
|
||||||
|
<h3>Title</h3>
|
||||||
|
</CardHeader>
|
||||||
|
<CardBody>
|
||||||
|
Content here
|
||||||
|
</CardBody>
|
||||||
|
<CardFooter>
|
||||||
|
<Button>Action</Button>
|
||||||
|
</CardFooter>
|
||||||
|
</Card>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Tabs
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<Tabs
|
||||||
|
variant="default" // 'default' | 'pills' | 'boxed'
|
||||||
|
orientation="horizontal" // 'horizontal' | 'vertical'
|
||||||
|
tabs={[
|
||||||
|
{
|
||||||
|
id: 'tab1',
|
||||||
|
label: 'Tab 1',
|
||||||
|
icon: <Icon />,
|
||||||
|
disabled: false,
|
||||||
|
content: <div>Content 1</div>
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'tab2',
|
||||||
|
label: 'Tab 2',
|
||||||
|
content: <div>Content 2</div>
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
activeTab={activeTab}
|
||||||
|
onTabChange={(id) => setActiveTab(id)}
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Alert
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<Alert
|
||||||
|
variant="success" // 'success' | 'warning' | 'danger' | 'info'
|
||||||
|
title="Success!"
|
||||||
|
message="Operation completed successfully"
|
||||||
|
icon={<CustomIcon />}
|
||||||
|
closable={true}
|
||||||
|
onClose={() => {}}
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Spinner
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<Spinner
|
||||||
|
size="md" // 'xs' | 'sm' | 'md' | 'lg' | 'xl'
|
||||||
|
variant="primary" // 'primary' | 'secondary' | 'white'
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Tooltip
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<Tooltip
|
||||||
|
content="Tooltip text"
|
||||||
|
position="top" // 'top' | 'bottom' | 'left' | 'right'
|
||||||
|
>
|
||||||
|
<Button>Hover me</Button>
|
||||||
|
</Tooltip>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Avatar
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<Avatar
|
||||||
|
src="/avatar.jpg"
|
||||||
|
alt="User name"
|
||||||
|
size="md" // 'xs' | 'sm' | 'md' | 'lg' | 'xl'
|
||||||
|
initials="AB"
|
||||||
|
status="online" // 'online' | 'offline' | 'busy' | 'away'
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Badge
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<Badge
|
||||||
|
variant="primary" // 'primary' | 'secondary' | 'success' | 'warning' | 'danger' | 'info'
|
||||||
|
size="md" // 'sm' | 'md' | 'lg'
|
||||||
|
dot={false}
|
||||||
|
>
|
||||||
|
New
|
||||||
|
</Badge>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Modal
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<Modal
|
||||||
|
isOpen={isOpen}
|
||||||
|
onClose={() => setIsOpen(false)}
|
||||||
|
size="md" // 'sm' | 'md' | 'lg' | 'xl' | 'fullScreen'
|
||||||
|
closeOnOverlayClick={true}
|
||||||
|
closeOnEscape={true}
|
||||||
|
>
|
||||||
|
<ModalHeader
|
||||||
|
title="Modal Title"
|
||||||
|
onClose={() => setIsOpen(false)}
|
||||||
|
showCloseButton={true}
|
||||||
|
/>
|
||||||
|
<ModalBody>
|
||||||
|
Modal content
|
||||||
|
</ModalBody>
|
||||||
|
<ModalFooter>
|
||||||
|
<Button variant="secondary" onClick={() => setIsOpen(false)}>Cancel</Button>
|
||||||
|
<Button variant="primary">Confirm</Button>
|
||||||
|
</ModalFooter>
|
||||||
|
</Modal>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Dropdown
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<Dropdown
|
||||||
|
trigger={<Button>Options</Button>}
|
||||||
|
align="right" // 'left' | 'right' | 'center'
|
||||||
|
>
|
||||||
|
<DropdownHeader>Header</DropdownHeader>
|
||||||
|
<DropdownItem icon={<Icon />} onClick={() => {}}>
|
||||||
|
Action 1
|
||||||
|
</DropdownItem>
|
||||||
|
<DropdownDivider />
|
||||||
|
<DropdownItem danger onClick={() => {}}>
|
||||||
|
Delete
|
||||||
|
</DropdownItem>
|
||||||
|
</Dropdown>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎨 Variables CSS Comunes
|
||||||
|
|
||||||
|
```css
|
||||||
|
/* Colores */
|
||||||
|
--au-primary-600: #4f46e5;
|
||||||
|
--au-success-600: #16a34a;
|
||||||
|
--au-danger-600: #dc2626;
|
||||||
|
--au-warning-600: #d97706;
|
||||||
|
--au-info-600: #2563eb;
|
||||||
|
|
||||||
|
/* Espaciado */
|
||||||
|
--au-spacing-2: 0.5rem; /* 8px */
|
||||||
|
--au-spacing-4: 1rem; /* 16px */
|
||||||
|
--au-spacing-6: 1.5rem; /* 24px */
|
||||||
|
|
||||||
|
/* Bordes */
|
||||||
|
--au-radius-sm: 0.25rem;
|
||||||
|
--au-radius-md: 0.5rem;
|
||||||
|
--au-radius-lg: 0.75rem;
|
||||||
|
|
||||||
|
/* Sombras */
|
||||||
|
--au-shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
|
||||||
|
--au-shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🛠️ Utilidades
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { cn, debounce, throttle, generateId } from 'avanza-ui';
|
||||||
|
|
||||||
|
// Combinar clases
|
||||||
|
const className = cn('base', condition && 'conditional');
|
||||||
|
|
||||||
|
// Debounce
|
||||||
|
const handleSearch = debounce((query) => search(query), 300);
|
||||||
|
|
||||||
|
// Throttle
|
||||||
|
const handleScroll = throttle(() => onScroll(), 100);
|
||||||
|
|
||||||
|
// Generar ID
|
||||||
|
const id = generateId('input'); // 'input-abc123'
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 Cambiar Tema
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
// Dark mode
|
||||||
|
document.documentElement.setAttribute('data-theme', 'dark');
|
||||||
|
|
||||||
|
// Light mode
|
||||||
|
document.documentElement.setAttribute('data-theme', 'light');
|
||||||
|
|
||||||
|
// En React
|
||||||
|
const [theme, setTheme] = useState('light');
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
document.documentElement.setAttribute('data-theme', theme);
|
||||||
|
}, [theme]);
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📦 Importación
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
// Importar estilos (solo una vez en main.tsx)
|
||||||
|
import 'avanza-ui/dist/index.css';
|
||||||
|
|
||||||
|
// Importar componentes
|
||||||
|
import {
|
||||||
|
Button, Input, Select, Checkbox,
|
||||||
|
Card, Tabs, Modal, Alert
|
||||||
|
} from 'avanza-ui';
|
||||||
|
|
||||||
|
// Importar tipos
|
||||||
|
import type { ButtonProps, SelectOption } from 'avanza-ui';
|
||||||
|
|
||||||
|
// Importar utilidades
|
||||||
|
import { cn, debounce } from 'avanza-ui';
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Avanza UI v1.0.0** - Referencia Rápida
|
||||||
|
*Actualizado: 11 de Noviembre, 2025*
|
||||||
|
|
||||||
@ -2,23 +2,43 @@
|
|||||||
|
|
||||||
Sistema de componentes UI independiente para AvanzaCast - Sin dependencias de Tailwind CSS
|
Sistema de componentes UI independiente para AvanzaCast - Sin dependencias de Tailwind CSS
|
||||||
|
|
||||||
## 🎯 Componentes Disponibles
|
**Versión:** 2.0.0
|
||||||
|
**Componentes Totales:** 20
|
||||||
|
**Bundle Size:** ~74KB
|
||||||
|
|
||||||
### Componentes Básicos
|
## 🎯 Componentes Disponibles (20 Total)
|
||||||
- ✅ **Button** - Botones con múltiples variantes y tamaños
|
|
||||||
- ✅ **Card** - Tarjetas con header, body y footer
|
### Componentes de Formulario (7)
|
||||||
|
- ✅ **Button** - Botones con 7 variantes y 5 tamaños
|
||||||
- ✅ **Input** - Campos de entrada con validación
|
- ✅ **Input** - Campos de entrada con validación
|
||||||
- ✅ **Badge** - Insignias y etiquetas
|
- ✅ **Textarea** - Área de texto con contador de caracteres
|
||||||
- ✅ **Avatar** - Avatares con soporte de imágenes e iniciales
|
- ✅ **Select** - Selector dropdown nativo
|
||||||
- ✅ **Spinner** - Indicadores de carga
|
- ✅ **Checkbox** - Casillas de verificación con variantes
|
||||||
|
- ✅ **Radio** - Botones de radio con variantes
|
||||||
|
- ✅ **Switch** - Toggle switches animados
|
||||||
|
|
||||||
### Componentes de Feedback
|
### Componentes de Layout (2)
|
||||||
- ✅ **Alert** - Mensajes de alerta (success, warning, danger, info)
|
- ✅ **Card** - Tarjetas con header, body y footer
|
||||||
- ✅ **Tooltip** - Tooltips posicionables
|
- ✅ **Tabs** - Pestañas con 3 estilos (default, pills, boxed)
|
||||||
- ✅ **Modal** - Modales con header, body y footer
|
|
||||||
|
|
||||||
### Componentes de Navegación
|
### Componentes de Navegación (3) 🆕
|
||||||
- ✅ **Dropdown** - Menús desplegables con items y dividers
|
- ✅ **Dropdown** - Menús desplegables con items y dividers
|
||||||
|
- ✅ **Breadcrumb** - Navegación de ruta 🆕 v2.0
|
||||||
|
- ✅ **Pagination** - Paginación completa 🆕 v2.0
|
||||||
|
|
||||||
|
### Componentes de Feedback (4) 🆕
|
||||||
|
- ✅ **Alert** - Mensajes de alerta (success, warning, danger, info)
|
||||||
|
- ✅ **Spinner** - Indicadores de carga animados
|
||||||
|
- ✅ **Tooltip** - Tooltips posicionables
|
||||||
|
- ✅ **Progress** - Barras de progreso 🆕 v2.0
|
||||||
|
|
||||||
|
### Componentes de Display (2)
|
||||||
|
- ✅ **Avatar** - Avatares con imágenes, iniciales y status badges
|
||||||
|
- ✅ **Badge** - Insignias y etiquetas
|
||||||
|
|
||||||
|
### Componentes Interactive (2) 🆕
|
||||||
|
- ✅ **Modal** - Modales con múltiples tamaños
|
||||||
|
- ✅ **Accordion** - Acordeones expandibles/colapsables 🆕 v2.0
|
||||||
|
|
||||||
## 📦 Instalación
|
## 📦 Instalación
|
||||||
|
|
||||||
|
|||||||
433
packages/ui-components/UPDATE_V2.md
Normal file
433
packages/ui-components/UPDATE_V2.md
Normal file
@ -0,0 +1,433 @@
|
|||||||
|
# 🎉 Avanza UI v2.0.0 - ACTUALIZACIÓN COMPLETA
|
||||||
|
|
||||||
|
## ✅ NUEVOS COMPONENTES AGREGADOS
|
||||||
|
|
||||||
|
### 📦 Total de Componentes: **20** (antes 16)
|
||||||
|
|
||||||
|
#### ✨ Nuevos Componentes Agregados (4):
|
||||||
|
|
||||||
|
1. **Accordion** 🆕
|
||||||
|
- Componente de acordeón expandible/colapsable
|
||||||
|
- Variantes: default, bordered, separated, flush
|
||||||
|
- Soporte para múltiples items abiertos
|
||||||
|
- Animación suave
|
||||||
|
|
||||||
|
2. **Breadcrumb** 🆕
|
||||||
|
- Navegación de ruta de navegación
|
||||||
|
- Variantes: default, arrowed, dotted
|
||||||
|
- Separadores personalizables
|
||||||
|
- Soporte para íconos
|
||||||
|
|
||||||
|
3. **Progress** 🆕
|
||||||
|
- Barras de progreso
|
||||||
|
- Variantes: primary, secondary, success, warning, danger, info
|
||||||
|
- Tamaños: sm, md, lg
|
||||||
|
- Estilos: normal, striped, animated, gradient
|
||||||
|
- Label inside/outside
|
||||||
|
|
||||||
|
4. **Pagination** 🆕
|
||||||
|
- Paginación completa
|
||||||
|
- Variantes: default, rounded, pills, simple
|
||||||
|
- Tamaños: sm, md, lg
|
||||||
|
- Navegación first/last, prev/next
|
||||||
|
- Elipsis automático para muchas páginas
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📊 Componentes Totales por Categoría
|
||||||
|
|
||||||
|
### Formularios (7)
|
||||||
|
- Button
|
||||||
|
- Input
|
||||||
|
- Textarea
|
||||||
|
- Select
|
||||||
|
- Checkbox
|
||||||
|
- Radio
|
||||||
|
- Switch
|
||||||
|
|
||||||
|
### Layout (2)
|
||||||
|
- Card
|
||||||
|
- Tabs
|
||||||
|
|
||||||
|
### Navegación (3) 🆕 +2
|
||||||
|
- Dropdown
|
||||||
|
- **Breadcrumb** 🆕
|
||||||
|
- **Pagination** 🆕
|
||||||
|
|
||||||
|
### Feedback (4) 🆕 +1
|
||||||
|
- Alert
|
||||||
|
- Spinner
|
||||||
|
- Tooltip
|
||||||
|
- **Progress** 🆕
|
||||||
|
|
||||||
|
### Display (2)
|
||||||
|
- Avatar
|
||||||
|
- Badge
|
||||||
|
|
||||||
|
### Interactive (2) 🆕 +1
|
||||||
|
- Modal
|
||||||
|
- **Accordion** 🆕
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 💡 Ejemplos de Uso de Nuevos Componentes
|
||||||
|
|
||||||
|
### Accordion
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { Accordion } from 'avanza-ui';
|
||||||
|
|
||||||
|
function FAQ() {
|
||||||
|
return (
|
||||||
|
<Accordion
|
||||||
|
variant="bordered"
|
||||||
|
allowMultiple={true}
|
||||||
|
items={[
|
||||||
|
{
|
||||||
|
id: 'faq1',
|
||||||
|
title: '¿Qué es Avanza UI?',
|
||||||
|
content: 'Una biblioteca de componentes UI sin Tailwind CSS.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'faq2',
|
||||||
|
title: '¿Cómo se instala?',
|
||||||
|
content: 'npm install ../ui-components'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'faq3',
|
||||||
|
title: '¿Es gratis?',
|
||||||
|
content: 'Sí, está bajo licencia MIT.',
|
||||||
|
disabled: false
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
defaultActiveItems={['faq1']}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Breadcrumb
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { Breadcrumb } from 'avanza-ui';
|
||||||
|
|
||||||
|
function Navigation() {
|
||||||
|
return (
|
||||||
|
<Breadcrumb
|
||||||
|
variant="arrowed"
|
||||||
|
items={[
|
||||||
|
{
|
||||||
|
label: 'Home',
|
||||||
|
icon: <HomeIcon />,
|
||||||
|
href: '/'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Components',
|
||||||
|
href: '/components'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'UI Kit',
|
||||||
|
onClick: () => console.log('Current page')
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Progress
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { Progress } from 'avanza-ui';
|
||||||
|
|
||||||
|
function UploadProgress() {
|
||||||
|
const [progress, setProgress] = useState(0);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Progress
|
||||||
|
value={progress}
|
||||||
|
max={100}
|
||||||
|
label="Uploading..."
|
||||||
|
showValue
|
||||||
|
variant="success"
|
||||||
|
size="md"
|
||||||
|
striped
|
||||||
|
animated
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Progress
|
||||||
|
value={75}
|
||||||
|
label="Profile completion"
|
||||||
|
variant="primary"
|
||||||
|
gradient
|
||||||
|
labelInside
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Pagination
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { Pagination } from 'avanza-ui';
|
||||||
|
|
||||||
|
function TablePagination() {
|
||||||
|
const [currentPage, setCurrentPage] = useState(1);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Pagination
|
||||||
|
currentPage={currentPage}
|
||||||
|
totalPages={50}
|
||||||
|
onPageChange={setCurrentPage}
|
||||||
|
variant="pills"
|
||||||
|
size="md"
|
||||||
|
showPrevNext
|
||||||
|
showFirstLast
|
||||||
|
maxVisiblePages={5}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📦 Actualización del Bundle Size
|
||||||
|
|
||||||
|
```
|
||||||
|
CSS: 38KB (antes 33KB) +5KB
|
||||||
|
JS ESM: 36KB (antes 31KB) +5KB
|
||||||
|
JS CJS: 38KB (antes 33KB) +5KB
|
||||||
|
Total: ~74KB (antes ~64KB)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Aumento:** +10KB por 4 componentes nuevos
|
||||||
|
**Promedio:** 2.5KB por componente - ¡Excelente!
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 Lista Completa de 20 Componentes
|
||||||
|
|
||||||
|
| # | Componente | Categoría | Estado |
|
||||||
|
|---|------------|-----------|--------|
|
||||||
|
| 1 | **Accordion** | Interactive | 🆕 NUEVO |
|
||||||
|
| 2 | Alert | Feedback | ✅ |
|
||||||
|
| 3 | Avatar | Display | ✅ |
|
||||||
|
| 4 | Badge | Display | ✅ |
|
||||||
|
| 5 | **Breadcrumb** | Navegación | 🆕 NUEVO |
|
||||||
|
| 6 | Button | Formulario | ✅ |
|
||||||
|
| 7 | Card | Layout | ✅ |
|
||||||
|
| 8 | Checkbox | Formulario | ✅ |
|
||||||
|
| 9 | Dropdown | Navegación | ✅ |
|
||||||
|
| 10 | Input | Formulario | ✅ |
|
||||||
|
| 11 | Modal | Interactive | ✅ |
|
||||||
|
| 12 | **Pagination** | Navegación | 🆕 NUEVO |
|
||||||
|
| 13 | **Progress** | Feedback | 🆕 NUEVO |
|
||||||
|
| 14 | Radio | Formulario | ✅ |
|
||||||
|
| 15 | Select | Formulario | ✅ |
|
||||||
|
| 16 | Spinner | Feedback | ✅ |
|
||||||
|
| 17 | Switch | Formulario | ✅ |
|
||||||
|
| 18 | Tabs | Layout | ✅ |
|
||||||
|
| 19 | Textarea | Formulario | ✅ |
|
||||||
|
| 20 | Tooltip | Feedback | ✅ |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔄 Componentes Migrados de Vristo
|
||||||
|
|
||||||
|
### ✅ Completados
|
||||||
|
- [x] Accordion (Accordians.tsx)
|
||||||
|
- [x] Breadcrumb (Breadcrumbs.tsx)
|
||||||
|
- [x] Progress (Progressbar.tsx)
|
||||||
|
- [x] Pagination (Pagination.tsx)
|
||||||
|
- [x] Tabs
|
||||||
|
- [x] Modal
|
||||||
|
- [x] Dropdown
|
||||||
|
- [x] Alert
|
||||||
|
- [x] Checkbox/Radio/Switch
|
||||||
|
- [x] Input/Textarea/Select
|
||||||
|
- [x] Avatar/Badge/Tooltip/Spinner
|
||||||
|
- [x] Button/Card
|
||||||
|
|
||||||
|
### 📋 Componentes Opcionales de Vristo (No Esenciales)
|
||||||
|
|
||||||
|
Estos componentes no se han migrado porque son muy específicos o requieren librerías externas:
|
||||||
|
|
||||||
|
- Carousel (requiere swiper)
|
||||||
|
- Counter (muy específico)
|
||||||
|
- Countdown (muy específico)
|
||||||
|
- SweetAlert (requiere librería externa)
|
||||||
|
- Timeline (muy específico)
|
||||||
|
- Notification/Toast (se puede crear si es necesario)
|
||||||
|
- LightBox (requiere librería externa)
|
||||||
|
- MediaObject (no es un componente estándar)
|
||||||
|
- PricingTable (template específico)
|
||||||
|
- ListGroup (se puede usar Card + list)
|
||||||
|
- Jumbotron (template específico)
|
||||||
|
- Popovers (similar a Tooltip)
|
||||||
|
- Treeview (componente complejo)
|
||||||
|
- Search (componente de app específica)
|
||||||
|
- Colorlibrary (no es componente)
|
||||||
|
- Infobox (template específico)
|
||||||
|
- Loader (ya tenemos Spinner)
|
||||||
|
- Wizards (componente complejo)
|
||||||
|
- FileUpload (componente complejo)
|
||||||
|
- DatePicker (requiere librería externa)
|
||||||
|
- Editors (requieren librerías externas)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 Cómo Actualizar
|
||||||
|
|
||||||
|
### 1. Actualizar imports
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import {
|
||||||
|
// Nuevos componentes
|
||||||
|
Accordion,
|
||||||
|
Breadcrumb,
|
||||||
|
Progress,
|
||||||
|
Pagination,
|
||||||
|
|
||||||
|
// Existentes
|
||||||
|
Button, Input, Card, Tabs, Modal,
|
||||||
|
Alert, Spinner, Tooltip, Avatar, Badge,
|
||||||
|
Checkbox, Radio, Switch, Select, Textarea,
|
||||||
|
Dropdown, DropdownItem
|
||||||
|
} from 'avanza-ui';
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Ejemplos Completos
|
||||||
|
|
||||||
|
#### Dashboard con Todos los Componentes
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import {
|
||||||
|
Card, CardHeader, CardBody,
|
||||||
|
Progress, Pagination, Breadcrumb,
|
||||||
|
Tabs, Button, Alert, Accordion
|
||||||
|
} from 'avanza-ui';
|
||||||
|
|
||||||
|
function Dashboard() {
|
||||||
|
const [page, setPage] = useState(1);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{/* Navegación */}
|
||||||
|
<Breadcrumb
|
||||||
|
items={[
|
||||||
|
{ label: 'Home', href: '/' },
|
||||||
|
{ label: 'Dashboard' }
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Alerta */}
|
||||||
|
<Alert variant="info" message="Welcome back!" closable />
|
||||||
|
|
||||||
|
{/* Tabs con contenido */}
|
||||||
|
<Tabs
|
||||||
|
variant="pills"
|
||||||
|
tabs={[
|
||||||
|
{
|
||||||
|
id: 'overview',
|
||||||
|
label: 'Overview',
|
||||||
|
content: (
|
||||||
|
<>
|
||||||
|
{/* Progress bars */}
|
||||||
|
<Card>
|
||||||
|
<CardHeader>Project Progress</CardHeader>
|
||||||
|
<CardBody>
|
||||||
|
<Progress
|
||||||
|
value={75}
|
||||||
|
label="Development"
|
||||||
|
showValue
|
||||||
|
variant="success"
|
||||||
|
/>
|
||||||
|
<Progress
|
||||||
|
value={50}
|
||||||
|
label="Testing"
|
||||||
|
showValue
|
||||||
|
variant="warning"
|
||||||
|
gradient
|
||||||
|
/>
|
||||||
|
</CardBody>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Accordion */}
|
||||||
|
<Accordion
|
||||||
|
items={[
|
||||||
|
{
|
||||||
|
id: 'tasks',
|
||||||
|
title: 'Recent Tasks',
|
||||||
|
content: <ul><li>Task 1</li><li>Task 2</li></ul>
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'updates',
|
||||||
|
title: 'Updates',
|
||||||
|
content: 'Latest updates here...'
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'data',
|
||||||
|
label: 'Data',
|
||||||
|
content: (
|
||||||
|
<>
|
||||||
|
{/* Data table with pagination */}
|
||||||
|
<Card>
|
||||||
|
<CardBody>
|
||||||
|
{/* Your table here */}
|
||||||
|
<Pagination
|
||||||
|
currentPage={page}
|
||||||
|
totalPages={10}
|
||||||
|
onPageChange={setPage}
|
||||||
|
variant="pills"
|
||||||
|
showPrevNext
|
||||||
|
/>
|
||||||
|
</CardBody>
|
||||||
|
</Card>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📚 Documentación Actualizada
|
||||||
|
|
||||||
|
Se recomienda consultar:
|
||||||
|
- **README.md** - API completa
|
||||||
|
- **QUICK_REFERENCE.md** - Referencia rápida de todos los componentes
|
||||||
|
- **COMPONENTS_UPDATE.md** - Detalles de componentes nuevos
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✅ Estado Final
|
||||||
|
|
||||||
|
**Avanza UI v2.0.0**
|
||||||
|
- ✅ **20 componentes UI** completos
|
||||||
|
- ✅ **Sin dependencias de Tailwind CSS**
|
||||||
|
- ✅ **TypeScript completo**
|
||||||
|
- ✅ **CSS Modules** sin conflictos
|
||||||
|
- ✅ **Bundle optimizado** (~74KB)
|
||||||
|
- ✅ **Migración de Vristo** completada
|
||||||
|
- ✅ **Documentación exhaustiva**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Fecha:** 11 de Noviembre, 2025
|
||||||
|
**Versión:** 2.0.0
|
||||||
|
**Componentes:** 20 (↑ +4 desde v1.0.0)
|
||||||
|
**Bundle Size:** ~74KB (↑ +10KB desde v1.0.0)
|
||||||
|
|
||||||
|
🎉 **¡BIBLIOTECA COMPLETADA CON ÉXITO!** 🎉
|
||||||
|
|
||||||
94
packages/ui-components/src/components/Accordion.module.css
Normal file
94
packages/ui-components/src/components/Accordion.module.css
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
.accordion {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--au-spacing-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.accordionItem {
|
||||||
|
border: 1px solid var(--au-border-light);
|
||||||
|
border-radius: var(--au-radius-md);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.accordionHeader {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: var(--au-spacing-4);
|
||||||
|
background: var(--au-surface);
|
||||||
|
border: none;
|
||||||
|
text-align: left;
|
||||||
|
font-size: var(--au-text-base);
|
||||||
|
font-weight: var(--au-font-medium);
|
||||||
|
color: var(--au-text-primary);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all var(--au-transition-fast);
|
||||||
|
}
|
||||||
|
|
||||||
|
.accordionHeader:hover:not(:disabled) {
|
||||||
|
background-color: var(--au-surface-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.accordionHeader:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.accordionHeader.active {
|
||||||
|
color: var(--au-primary);
|
||||||
|
background-color: var(--au-surface-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.accordionIcon {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
transition: transform var(--au-transition-fast);
|
||||||
|
color: var(--au-text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.accordionHeader.active .accordionIcon {
|
||||||
|
transform: rotate(180deg);
|
||||||
|
color: var(--au-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.accordionContent {
|
||||||
|
max-height: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
transition: max-height var(--au-transition-base) ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.accordionContent.open {
|
||||||
|
max-height: 2000px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.accordionBody {
|
||||||
|
padding: var(--au-spacing-4);
|
||||||
|
border-top: 1px solid var(--au-border-light);
|
||||||
|
background-color: var(--au-bg-secondary);
|
||||||
|
color: var(--au-text-secondary);
|
||||||
|
font-size: var(--au-text-sm);
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Variants */
|
||||||
|
.bordered .accordionItem {
|
||||||
|
border: 1px solid var(--au-border-medium);
|
||||||
|
}
|
||||||
|
|
||||||
|
.separated .accordionItem {
|
||||||
|
border: 1px solid var(--au-border-light);
|
||||||
|
margin-bottom: var(--au-spacing-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.flush .accordionItem {
|
||||||
|
border: none;
|
||||||
|
border-bottom: 1px solid var(--au-border-light);
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flush .accordionItem:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
97
packages/ui-components/src/components/Accordion.tsx
Normal file
97
packages/ui-components/src/components/Accordion.tsx
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import { cn } from '../utils/helpers';
|
||||||
|
import type { ComponentBaseProps } from '../types';
|
||||||
|
import styles from './Accordion.module.css';
|
||||||
|
|
||||||
|
export interface AccordionItem {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
content: React.ReactNode;
|
||||||
|
disabled?: boolean;
|
||||||
|
icon?: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AccordionProps extends ComponentBaseProps {
|
||||||
|
items: AccordionItem[];
|
||||||
|
defaultActiveItems?: string[];
|
||||||
|
activeItems?: string[];
|
||||||
|
onItemChange?: (itemIds: string[]) => void;
|
||||||
|
variant?: 'default' | 'bordered' | 'separated' | 'flush';
|
||||||
|
allowMultiple?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Accordion: React.FC<AccordionProps> = (props) => {
|
||||||
|
const {
|
||||||
|
items,
|
||||||
|
defaultActiveItems = [],
|
||||||
|
activeItems: controlledActiveItems,
|
||||||
|
onItemChange,
|
||||||
|
variant = 'default',
|
||||||
|
allowMultiple = false,
|
||||||
|
className,
|
||||||
|
style,
|
||||||
|
id,
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
const [internalActiveItems, setInternalActiveItems] = useState<string[]>(defaultActiveItems);
|
||||||
|
const activeItems = controlledActiveItems !== undefined ? controlledActiveItems : internalActiveItems;
|
||||||
|
|
||||||
|
const handleItemClick = (itemId: string, disabled?: boolean) => {
|
||||||
|
if (disabled) return;
|
||||||
|
|
||||||
|
let newActiveItems: string[];
|
||||||
|
|
||||||
|
if (allowMultiple) {
|
||||||
|
newActiveItems = activeItems.includes(itemId)
|
||||||
|
? activeItems.filter((id) => id !== itemId)
|
||||||
|
: [...activeItems, itemId];
|
||||||
|
} else {
|
||||||
|
newActiveItems = activeItems.includes(itemId) ? [] : [itemId];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (controlledActiveItems === undefined) {
|
||||||
|
setInternalActiveItems(newActiveItems);
|
||||||
|
}
|
||||||
|
onItemChange?.(newActiveItems);
|
||||||
|
};
|
||||||
|
|
||||||
|
const defaultIcon = (
|
||||||
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
|
||||||
|
<path d="M4.427 6.427a.6.6 0 01.849 0L8 9.151l2.724-2.724a.6.6 0 11.849.849l-3.149 3.148a.6.6 0 01-.848 0L4.427 7.276a.6.6 0 010-.849z" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={cn(styles.accordion, styles[variant], className)} style={style} id={id}>
|
||||||
|
{items.map((item) => {
|
||||||
|
const isActive = activeItems.includes(item.id);
|
||||||
|
return (
|
||||||
|
<div key={item.id} className={styles.accordionItem}>
|
||||||
|
<button
|
||||||
|
className={cn(styles.accordionHeader, isActive && styles.active)}
|
||||||
|
onClick={() => handleItemClick(item.id, item.disabled)}
|
||||||
|
disabled={item.disabled}
|
||||||
|
aria-expanded={isActive}
|
||||||
|
aria-controls={`${item.id}-content`}
|
||||||
|
id={`${item.id}-header`}
|
||||||
|
>
|
||||||
|
<span>{item.title}</span>
|
||||||
|
<span className={styles.accordionIcon}>{item.icon || defaultIcon}</span>
|
||||||
|
</button>
|
||||||
|
<div
|
||||||
|
className={cn(styles.accordionContent, isActive && styles.open)}
|
||||||
|
id={`${item.id}-content`}
|
||||||
|
role="region"
|
||||||
|
aria-labelledby={`${item.id}-header`}
|
||||||
|
>
|
||||||
|
<div className={styles.accordionBody}>{item.content}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
Accordion.displayName = 'Accordion';
|
||||||
|
|
||||||
97
packages/ui-components/src/components/Breadcrumb.module.css
Normal file
97
packages/ui-components/src/components/Breadcrumb.module.css
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
.breadcrumb {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: var(--au-spacing-1);
|
||||||
|
font-size: var(--au-text-sm);
|
||||||
|
font-weight: var(--au-font-medium);
|
||||||
|
}
|
||||||
|
|
||||||
|
.breadcrumbItem {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--au-spacing-2);
|
||||||
|
color: var(--au-text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.breadcrumbItem:last-child {
|
||||||
|
color: var(--au-text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.breadcrumbLink {
|
||||||
|
color: inherit;
|
||||||
|
text-decoration: none;
|
||||||
|
transition: color var(--au-transition-fast);
|
||||||
|
cursor: pointer;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
padding: 0;
|
||||||
|
font: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.breadcrumbLink:hover:not(:disabled) {
|
||||||
|
color: var(--au-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.breadcrumbLink:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.breadcrumbSeparator {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
color: var(--au-text-tertiary);
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.breadcrumbIcon {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Variants */
|
||||||
|
.arrowed .breadcrumbItem {
|
||||||
|
position: relative;
|
||||||
|
background-color: var(--au-bg-tertiary);
|
||||||
|
padding: var(--au-spacing-2) var(--au-spacing-4);
|
||||||
|
padding-right: var(--au-spacing-6);
|
||||||
|
}
|
||||||
|
|
||||||
|
.arrowed .breadcrumbItem:first-child {
|
||||||
|
border-radius: var(--au-radius-md) 0 0 var(--au-radius-md);
|
||||||
|
padding-left: var(--au-spacing-4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.arrowed .breadcrumbItem:last-child {
|
||||||
|
border-radius: 0 var(--au-radius-md) var(--au-radius-md) 0;
|
||||||
|
padding-right: var(--au-spacing-4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.arrowed .breadcrumbItem:not(:last-child)::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
right: -12px;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
margin: auto;
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
border-top: 20px solid transparent;
|
||||||
|
border-bottom: 20px solid transparent;
|
||||||
|
border-left: 12px solid var(--au-bg-tertiary);
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.arrowed .breadcrumbSeparator {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dotted .breadcrumbSeparator {
|
||||||
|
width: 4px;
|
||||||
|
height: 4px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background-color: var(--au-text-tertiary);
|
||||||
|
}
|
||||||
|
|
||||||
80
packages/ui-components/src/components/Breadcrumb.tsx
Normal file
80
packages/ui-components/src/components/Breadcrumb.tsx
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { cn } from '../utils/helpers';
|
||||||
|
import type { ComponentBaseProps } from '../types';
|
||||||
|
import styles from './Breadcrumb.module.css';
|
||||||
|
|
||||||
|
export interface BreadcrumbItem {
|
||||||
|
label: string;
|
||||||
|
href?: string;
|
||||||
|
onClick?: () => void;
|
||||||
|
icon?: React.ReactNode;
|
||||||
|
disabled?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BreadcrumbProps extends ComponentBaseProps {
|
||||||
|
items: BreadcrumbItem[];
|
||||||
|
separator?: React.ReactNode;
|
||||||
|
variant?: 'default' | 'arrowed' | 'dotted';
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Breadcrumb: React.FC<BreadcrumbProps> = (props) => {
|
||||||
|
const {
|
||||||
|
items,
|
||||||
|
separator = '/',
|
||||||
|
variant = 'default',
|
||||||
|
className,
|
||||||
|
style,
|
||||||
|
id,
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<nav
|
||||||
|
className={cn(styles.breadcrumb, styles[variant], className)}
|
||||||
|
style={style}
|
||||||
|
id={id}
|
||||||
|
aria-label="Breadcrumb"
|
||||||
|
>
|
||||||
|
<ol className={styles.breadcrumb}>
|
||||||
|
{items.map((item, index) => {
|
||||||
|
const isLast = index === items.length - 1;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<React.Fragment key={index}>
|
||||||
|
<li className={styles.breadcrumbItem}>
|
||||||
|
{item.icon && <span className={styles.breadcrumbIcon}>{item.icon}</span>}
|
||||||
|
{isLast ? (
|
||||||
|
<span aria-current="page">{item.label}</span>
|
||||||
|
) : item.href ? (
|
||||||
|
<a
|
||||||
|
href={item.href}
|
||||||
|
className={styles.breadcrumbLink}
|
||||||
|
onClick={item.onClick}
|
||||||
|
>
|
||||||
|
{item.label}
|
||||||
|
</a>
|
||||||
|
) : (
|
||||||
|
<button
|
||||||
|
className={styles.breadcrumbLink}
|
||||||
|
onClick={item.onClick}
|
||||||
|
disabled={item.disabled}
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
{item.label}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</li>
|
||||||
|
{!isLast && (
|
||||||
|
<li className={styles.breadcrumbSeparator} aria-hidden="true">
|
||||||
|
{separator}
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</ol>
|
||||||
|
</nav>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
Breadcrumb.displayName = 'Breadcrumb';
|
||||||
|
|
||||||
100
packages/ui-components/src/components/Checkbox.module.css
Normal file
100
packages/ui-components/src/components/Checkbox.module.css
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
.checkbox {
|
||||||
|
position: relative;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
cursor: pointer;
|
||||||
|
gap: var(--au-spacing-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkboxInput {
|
||||||
|
appearance: none;
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
border: 2px solid var(--au-border-medium);
|
||||||
|
border-radius: var(--au-radius-sm);
|
||||||
|
background-color: var(--au-surface);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all var(--au-transition-fast);
|
||||||
|
flex-shrink: 0;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkboxInput:hover:not(:disabled) {
|
||||||
|
border-color: var(--au-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkboxInput:checked {
|
||||||
|
background-color: var(--au-primary);
|
||||||
|
border-color: var(--au-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkboxInput:checked::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
left: 50%;
|
||||||
|
top: 50%;
|
||||||
|
transform: translate(-50%, -50%) rotate(45deg);
|
||||||
|
width: 4px;
|
||||||
|
height: 8px;
|
||||||
|
border: solid white;
|
||||||
|
border-width: 0 2px 2px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkboxInput:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkboxInput:focus-visible {
|
||||||
|
outline: 2px solid var(--au-primary);
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkboxLabel {
|
||||||
|
font-size: var(--au-text-sm);
|
||||||
|
color: var(--au-text-primary);
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox:has(.checkboxInput:disabled) .checkboxLabel {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Variants */
|
||||||
|
.success .checkboxInput:checked {
|
||||||
|
background-color: var(--au-success-600);
|
||||||
|
border-color: var(--au-success-600);
|
||||||
|
}
|
||||||
|
|
||||||
|
.warning .checkboxInput:checked {
|
||||||
|
background-color: var(--au-warning-600);
|
||||||
|
border-color: var(--au-warning-600);
|
||||||
|
}
|
||||||
|
|
||||||
|
.danger .checkboxInput:checked {
|
||||||
|
background-color: var(--au-danger-600);
|
||||||
|
border-color: var(--au-danger-600);
|
||||||
|
}
|
||||||
|
|
||||||
|
.info .checkboxInput:checked {
|
||||||
|
background-color: var(--au-info-600);
|
||||||
|
border-color: var(--au-info-600);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Sizes */
|
||||||
|
.sm .checkboxInput {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.md .checkboxInput {
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lg .checkboxInput {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
61
packages/ui-components/src/components/Checkbox.tsx
Normal file
61
packages/ui-components/src/components/Checkbox.tsx
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { cn } from '../utils/helpers';
|
||||||
|
import type { ComponentBaseProps } from '../types';
|
||||||
|
import styles from './Checkbox.module.css';
|
||||||
|
export interface CheckboxProps extends ComponentBaseProps {
|
||||||
|
label?: string;
|
||||||
|
checked?: boolean;
|
||||||
|
defaultChecked?: boolean;
|
||||||
|
onChange?: (event: React.ChangeEvent<HTMLInputElement>) => void;
|
||||||
|
disabled?: boolean;
|
||||||
|
name?: string;
|
||||||
|
value?: string;
|
||||||
|
variant?: 'primary' | 'success' | 'warning' | 'danger' | 'info';
|
||||||
|
size?: 'sm' | 'md' | 'lg';
|
||||||
|
}
|
||||||
|
export const Checkbox = React.forwardRef<HTMLInputElement, CheckboxProps>(
|
||||||
|
(props, ref) => {
|
||||||
|
const {
|
||||||
|
label,
|
||||||
|
checked,
|
||||||
|
defaultChecked,
|
||||||
|
onChange,
|
||||||
|
disabled = false,
|
||||||
|
name,
|
||||||
|
value,
|
||||||
|
variant = 'primary',
|
||||||
|
size = 'md',
|
||||||
|
className,
|
||||||
|
style,
|
||||||
|
id,
|
||||||
|
...rest
|
||||||
|
} = props;
|
||||||
|
return (
|
||||||
|
<label
|
||||||
|
className={cn(
|
||||||
|
styles.checkbox,
|
||||||
|
styles[variant],
|
||||||
|
styles[size],
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
style={style}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
ref={ref}
|
||||||
|
type="checkbox"
|
||||||
|
className={styles.checkboxInput}
|
||||||
|
checked={checked}
|
||||||
|
defaultChecked={defaultChecked}
|
||||||
|
onChange={onChange}
|
||||||
|
disabled={disabled}
|
||||||
|
name={name}
|
||||||
|
value={value}
|
||||||
|
id={id}
|
||||||
|
{...rest}
|
||||||
|
/>
|
||||||
|
{label && <span className={styles.checkboxLabel}>{label}</span>}
|
||||||
|
</label>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
Checkbox.displayName = 'Checkbox';
|
||||||
125
packages/ui-components/src/components/Pagination.module.css
Normal file
125
packages/ui-components/src/components/Pagination.module.css
Normal file
@ -0,0 +1,125 @@
|
|||||||
|
.pagination {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--au-spacing-1);
|
||||||
|
list-style: none;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.paginationItem {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.paginationLink {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
min-width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
padding: 0 var(--au-spacing-3);
|
||||||
|
border: 1px solid var(--au-border-medium);
|
||||||
|
background-color: var(--au-surface);
|
||||||
|
color: var(--au-text-primary);
|
||||||
|
font-size: var(--au-text-sm);
|
||||||
|
font-weight: var(--au-font-medium);
|
||||||
|
text-decoration: none;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all var(--au-transition-fast);
|
||||||
|
border-radius: var(--au-radius-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
.paginationLink:hover:not(:disabled):not(.active) {
|
||||||
|
background-color: var(--au-surface-hover);
|
||||||
|
border-color: var(--au-border-dark);
|
||||||
|
}
|
||||||
|
|
||||||
|
.paginationLink:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.paginationLink.active {
|
||||||
|
background-color: var(--au-primary);
|
||||||
|
border-color: var(--au-primary);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.paginationLink:focus-visible {
|
||||||
|
outline: 2px solid var(--au-primary);
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.paginationEllipsis {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
min-width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
color: var(--au-text-secondary);
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Sizes */
|
||||||
|
.sm .paginationLink {
|
||||||
|
min-width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
padding: 0 var(--au-spacing-2);
|
||||||
|
font-size: var(--au-text-xs);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sm .paginationEllipsis {
|
||||||
|
min-width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.md .paginationLink {
|
||||||
|
min-width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
padding: 0 var(--au-spacing-3);
|
||||||
|
font-size: var(--au-text-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.md .paginationEllipsis {
|
||||||
|
min-width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lg .paginationLink {
|
||||||
|
min-width: 44px;
|
||||||
|
height: 44px;
|
||||||
|
padding: 0 var(--au-spacing-4);
|
||||||
|
font-size: var(--au-text-base);
|
||||||
|
}
|
||||||
|
|
||||||
|
.lg .paginationEllipsis {
|
||||||
|
min-width: 44px;
|
||||||
|
height: 44px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Variants */
|
||||||
|
.rounded .paginationLink {
|
||||||
|
border-radius: var(--au-radius-full);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pills .pagination {
|
||||||
|
gap: var(--au-spacing-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pills .paginationLink {
|
||||||
|
border-radius: var(--au-radius-full);
|
||||||
|
border-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pills .paginationLink:hover:not(:disabled):not(.active) {
|
||||||
|
background-color: var(--au-bg-tertiary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.simple .paginationLink {
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.simple .paginationLink:hover:not(:disabled):not(.active) {
|
||||||
|
background-color: var(--au-bg-tertiary);
|
||||||
|
}
|
||||||
|
|
||||||
173
packages/ui-components/src/components/Pagination.tsx
Normal file
173
packages/ui-components/src/components/Pagination.tsx
Normal file
@ -0,0 +1,173 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { cn } from '../utils/helpers';
|
||||||
|
import type { ComponentBaseProps } from '../types';
|
||||||
|
import styles from './Pagination.module.css';
|
||||||
|
|
||||||
|
export interface PaginationProps extends ComponentBaseProps {
|
||||||
|
currentPage: number;
|
||||||
|
totalPages: number;
|
||||||
|
onPageChange: (page: number) => void;
|
||||||
|
size?: 'sm' | 'md' | 'lg';
|
||||||
|
variant?: 'default' | 'rounded' | 'pills' | 'simple';
|
||||||
|
showPrevNext?: boolean;
|
||||||
|
showFirstLast?: boolean;
|
||||||
|
maxVisiblePages?: number;
|
||||||
|
prevLabel?: React.ReactNode;
|
||||||
|
nextLabel?: React.ReactNode;
|
||||||
|
firstLabel?: React.ReactNode;
|
||||||
|
lastLabel?: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Pagination: React.FC<PaginationProps> = (props) => {
|
||||||
|
const {
|
||||||
|
currentPage,
|
||||||
|
totalPages,
|
||||||
|
onPageChange,
|
||||||
|
size = 'md',
|
||||||
|
variant = 'default',
|
||||||
|
showPrevNext = true,
|
||||||
|
showFirstLast = false,
|
||||||
|
maxVisiblePages = 5,
|
||||||
|
prevLabel = '‹',
|
||||||
|
nextLabel = '›',
|
||||||
|
firstLabel = '«',
|
||||||
|
lastLabel = '»',
|
||||||
|
className,
|
||||||
|
style,
|
||||||
|
id,
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
const getPageNumbers = (): (number | string)[] => {
|
||||||
|
const pages: (number | string)[] = [];
|
||||||
|
|
||||||
|
if (totalPages <= maxVisiblePages) {
|
||||||
|
for (let i = 1; i <= totalPages; i++) {
|
||||||
|
pages.push(i);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const halfVisible = Math.floor(maxVisiblePages / 2);
|
||||||
|
let startPage = Math.max(1, currentPage - halfVisible);
|
||||||
|
let endPage = Math.min(totalPages, currentPage + halfVisible);
|
||||||
|
|
||||||
|
if (currentPage - halfVisible < 1) {
|
||||||
|
endPage = Math.min(totalPages, maxVisiblePages);
|
||||||
|
}
|
||||||
|
if (currentPage + halfVisible > totalPages) {
|
||||||
|
startPage = Math.max(1, totalPages - maxVisiblePages + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (startPage > 1) {
|
||||||
|
pages.push(1);
|
||||||
|
if (startPage > 2) {
|
||||||
|
pages.push('...');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = startPage; i <= endPage; i++) {
|
||||||
|
pages.push(i);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (endPage < totalPages) {
|
||||||
|
if (endPage < totalPages - 1) {
|
||||||
|
pages.push('...');
|
||||||
|
}
|
||||||
|
pages.push(totalPages);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return pages;
|
||||||
|
};
|
||||||
|
|
||||||
|
const pages = getPageNumbers();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<nav
|
||||||
|
className={cn(styles.pagination, styles[size], styles[variant], className)}
|
||||||
|
style={style}
|
||||||
|
id={id}
|
||||||
|
aria-label="Pagination"
|
||||||
|
>
|
||||||
|
<ul className={styles.pagination}>
|
||||||
|
{showFirstLast && (
|
||||||
|
<li className={styles.paginationItem}>
|
||||||
|
<button
|
||||||
|
className={styles.paginationLink}
|
||||||
|
onClick={() => onPageChange(1)}
|
||||||
|
disabled={currentPage === 1}
|
||||||
|
aria-label="First page"
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
{firstLabel}
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{showPrevNext && (
|
||||||
|
<li className={styles.paginationItem}>
|
||||||
|
<button
|
||||||
|
className={styles.paginationLink}
|
||||||
|
onClick={() => onPageChange(currentPage - 1)}
|
||||||
|
disabled={currentPage === 1}
|
||||||
|
aria-label="Previous page"
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
{prevLabel}
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{pages.map((page, index) => (
|
||||||
|
<li key={index} className={styles.paginationItem}>
|
||||||
|
{page === '...' ? (
|
||||||
|
<span className={styles.paginationEllipsis}>…</span>
|
||||||
|
) : (
|
||||||
|
<button
|
||||||
|
className={cn(
|
||||||
|
styles.paginationLink,
|
||||||
|
currentPage === page && styles.active
|
||||||
|
)}
|
||||||
|
onClick={() => onPageChange(page as number)}
|
||||||
|
aria-label={`Page ${page}`}
|
||||||
|
aria-current={currentPage === page ? 'page' : undefined}
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
{page}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{showPrevNext && (
|
||||||
|
<li className={styles.paginationItem}>
|
||||||
|
<button
|
||||||
|
className={styles.paginationLink}
|
||||||
|
onClick={() => onPageChange(currentPage + 1)}
|
||||||
|
disabled={currentPage === totalPages}
|
||||||
|
aria-label="Next page"
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
{nextLabel}
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{showFirstLast && (
|
||||||
|
<li className={styles.paginationItem}>
|
||||||
|
<button
|
||||||
|
className={styles.paginationLink}
|
||||||
|
onClick={() => onPageChange(totalPages)}
|
||||||
|
disabled={currentPage === totalPages}
|
||||||
|
aria-label="Last page"
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
{lastLabel}
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
Pagination.displayName = 'Pagination';
|
||||||
|
|
||||||
134
packages/ui-components/src/components/Progress.module.css
Normal file
134
packages/ui-components/src/components/Progress.module.css
Normal file
@ -0,0 +1,134 @@
|
|||||||
|
.progressWrapper {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--au-spacing-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.progressLabel {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
font-size: var(--au-text-sm);
|
||||||
|
color: var(--au-text-primary);
|
||||||
|
margin-bottom: var(--au-spacing-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.progressValue {
|
||||||
|
font-weight: var(--au-font-medium);
|
||||||
|
color: var(--au-text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.progressBar {
|
||||||
|
width: 100%;
|
||||||
|
background-color: var(--au-bg-tertiary);
|
||||||
|
border-radius: var(--au-radius-full);
|
||||||
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progressFill {
|
||||||
|
height: 100%;
|
||||||
|
background-color: var(--au-primary);
|
||||||
|
border-radius: var(--au-radius-full);
|
||||||
|
transition: width var(--au-transition-base);
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Sizes */
|
||||||
|
.sm {
|
||||||
|
height: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.md {
|
||||||
|
height: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lg {
|
||||||
|
height: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Variants */
|
||||||
|
.primary .progressFill {
|
||||||
|
background-color: var(--au-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.secondary .progressFill {
|
||||||
|
background-color: var(--au-gray-500);
|
||||||
|
}
|
||||||
|
|
||||||
|
.success .progressFill {
|
||||||
|
background-color: var(--au-success-600);
|
||||||
|
}
|
||||||
|
|
||||||
|
.warning .progressFill {
|
||||||
|
background-color: var(--au-warning-600);
|
||||||
|
}
|
||||||
|
|
||||||
|
.danger .progressFill {
|
||||||
|
background-color: var(--au-danger-600);
|
||||||
|
}
|
||||||
|
|
||||||
|
.info .progressFill {
|
||||||
|
background-color: var(--au-info-600);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Striped */
|
||||||
|
.striped .progressFill {
|
||||||
|
background-image: linear-gradient(
|
||||||
|
45deg,
|
||||||
|
rgba(255, 255, 255, 0.15) 25%,
|
||||||
|
transparent 25%,
|
||||||
|
transparent 50%,
|
||||||
|
rgba(255, 255, 255, 0.15) 50%,
|
||||||
|
rgba(255, 255, 255, 0.15) 75%,
|
||||||
|
transparent 75%,
|
||||||
|
transparent
|
||||||
|
);
|
||||||
|
background-size: 1rem 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Animated */
|
||||||
|
.animated .progressFill {
|
||||||
|
animation: progress-stripes 1s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes progress-stripes {
|
||||||
|
0% {
|
||||||
|
background-position: 1rem 0;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
background-position: 0 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Gradient */
|
||||||
|
.gradient .progressFill {
|
||||||
|
background-image: linear-gradient(to right, var(--au-primary), var(--au-primary-hover));
|
||||||
|
}
|
||||||
|
|
||||||
|
.gradient.success .progressFill {
|
||||||
|
background-image: linear-gradient(to right, #3cba92, #0ba360);
|
||||||
|
}
|
||||||
|
|
||||||
|
.gradient.warning .progressFill {
|
||||||
|
background-image: linear-gradient(to right, #f09819, #ff5858);
|
||||||
|
}
|
||||||
|
|
||||||
|
.gradient.danger .progressFill {
|
||||||
|
background-image: linear-gradient(to right, #d09693, #c71d6f);
|
||||||
|
}
|
||||||
|
|
||||||
|
.gradient.info .progressFill {
|
||||||
|
background-image: linear-gradient(to right, #04befe, #4481eb);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Label inside */
|
||||||
|
.labelInside .progressFill {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: white;
|
||||||
|
font-size: var(--au-text-xs);
|
||||||
|
font-weight: var(--au-font-semibold);
|
||||||
|
}
|
||||||
|
|
||||||
73
packages/ui-components/src/components/Progress.tsx
Normal file
73
packages/ui-components/src/components/Progress.tsx
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { cn } from '../utils/helpers';
|
||||||
|
import type { ComponentBaseProps } from '../types';
|
||||||
|
import styles from './Progress.module.css';
|
||||||
|
|
||||||
|
export interface ProgressProps extends ComponentBaseProps {
|
||||||
|
value: number;
|
||||||
|
max?: number;
|
||||||
|
label?: string;
|
||||||
|
showValue?: boolean;
|
||||||
|
size?: 'sm' | 'md' | 'lg';
|
||||||
|
variant?: 'primary' | 'secondary' | 'success' | 'warning' | 'danger' | 'info';
|
||||||
|
striped?: boolean;
|
||||||
|
animated?: boolean;
|
||||||
|
gradient?: boolean;
|
||||||
|
labelInside?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Progress: React.FC<ProgressProps> = (props) => {
|
||||||
|
const {
|
||||||
|
value,
|
||||||
|
max = 100,
|
||||||
|
label,
|
||||||
|
showValue = false,
|
||||||
|
size = 'md',
|
||||||
|
variant = 'primary',
|
||||||
|
striped = false,
|
||||||
|
animated = false,
|
||||||
|
gradient = false,
|
||||||
|
labelInside = false,
|
||||||
|
className,
|
||||||
|
style,
|
||||||
|
id,
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
const percentage = Math.min(Math.max((value / max) * 100, 0), 100);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={cn(styles.progressWrapper, className)} style={style} id={id}>
|
||||||
|
{(label || showValue) && (
|
||||||
|
<div className={styles.progressLabel}>
|
||||||
|
{label && <span>{label}</span>}
|
||||||
|
{showValue && <span className={styles.progressValue}>{Math.round(percentage)}%</span>}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
styles.progressBar,
|
||||||
|
styles[size],
|
||||||
|
styles[variant],
|
||||||
|
striped && styles.striped,
|
||||||
|
animated && styles.animated,
|
||||||
|
gradient && styles.gradient,
|
||||||
|
labelInside && styles.labelInside
|
||||||
|
)}
|
||||||
|
role="progressbar"
|
||||||
|
aria-valuenow={value}
|
||||||
|
aria-valuemin={0}
|
||||||
|
aria-valuemax={max}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={styles.progressFill}
|
||||||
|
style={{ width: `${percentage}%` }}
|
||||||
|
>
|
||||||
|
{labelInside && `${Math.round(percentage)}%`}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
Progress.displayName = 'Progress';
|
||||||
|
|
||||||
115
packages/ui-components/src/components/Radio.module.css
Normal file
115
packages/ui-components/src/components/Radio.module.css
Normal file
@ -0,0 +1,115 @@
|
|||||||
|
.radio {
|
||||||
|
position: relative;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
cursor: pointer;
|
||||||
|
gap: var(--au-spacing-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.radioInput {
|
||||||
|
appearance: none;
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
border: 2px solid var(--au-border-medium);
|
||||||
|
border-radius: var(--au-radius-full);
|
||||||
|
background-color: var(--au-surface);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all var(--au-transition-fast);
|
||||||
|
flex-shrink: 0;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.radioInput:hover:not(:disabled) {
|
||||||
|
border-color: var(--au-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.radioInput:checked {
|
||||||
|
background-color: var(--au-primary);
|
||||||
|
border-color: var(--au-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.radioInput:checked::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
left: 50%;
|
||||||
|
top: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
border-radius: var(--au-radius-full);
|
||||||
|
background-color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.radioInput:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.radioInput:focus-visible {
|
||||||
|
outline: 2px solid var(--au-primary);
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.radioLabel {
|
||||||
|
font-size: var(--au-text-sm);
|
||||||
|
color: var(--au-text-primary);
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.radio:has(.radioInput:disabled) .radioLabel {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Variants */
|
||||||
|
.success .radioInput:checked {
|
||||||
|
background-color: var(--au-success-600);
|
||||||
|
border-color: var(--au-success-600);
|
||||||
|
}
|
||||||
|
|
||||||
|
.warning .radioInput:checked {
|
||||||
|
background-color: var(--au-warning-600);
|
||||||
|
border-color: var(--au-warning-600);
|
||||||
|
}
|
||||||
|
|
||||||
|
.danger .radioInput:checked {
|
||||||
|
background-color: var(--au-danger-600);
|
||||||
|
border-color: var(--au-danger-600);
|
||||||
|
}
|
||||||
|
|
||||||
|
.info .radioInput:checked {
|
||||||
|
background-color: var(--au-info-600);
|
||||||
|
border-color: var(--au-info-600);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Sizes */
|
||||||
|
.sm .radioInput {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sm .radioInput:checked::after {
|
||||||
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.md .radioInput {
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.md .radioInput:checked::after {
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lg .radioInput {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lg .radioInput:checked::after {
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
66
packages/ui-components/src/components/Radio.tsx
Normal file
66
packages/ui-components/src/components/Radio.tsx
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { cn } from '../utils/helpers';
|
||||||
|
import type { ComponentBaseProps } from '../types';
|
||||||
|
import styles from './Radio.module.css';
|
||||||
|
|
||||||
|
export interface RadioProps extends ComponentBaseProps {
|
||||||
|
label?: string;
|
||||||
|
checked?: boolean;
|
||||||
|
defaultChecked?: boolean;
|
||||||
|
onChange?: (event: React.ChangeEvent<HTMLInputElement>) => void;
|
||||||
|
disabled?: boolean;
|
||||||
|
name?: string;
|
||||||
|
value?: string;
|
||||||
|
variant?: 'primary' | 'success' | 'warning' | 'danger' | 'info';
|
||||||
|
size?: 'sm' | 'md' | 'lg';
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Radio = React.forwardRef<HTMLInputElement, RadioProps>(
|
||||||
|
(props, ref) => {
|
||||||
|
const {
|
||||||
|
label,
|
||||||
|
checked,
|
||||||
|
defaultChecked,
|
||||||
|
onChange,
|
||||||
|
disabled = false,
|
||||||
|
name,
|
||||||
|
value,
|
||||||
|
variant = 'primary',
|
||||||
|
size = 'md',
|
||||||
|
className,
|
||||||
|
style,
|
||||||
|
id,
|
||||||
|
...rest
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<label
|
||||||
|
className={cn(
|
||||||
|
styles.radio,
|
||||||
|
styles[variant],
|
||||||
|
styles[size],
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
style={style}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
ref={ref}
|
||||||
|
type="radio"
|
||||||
|
className={styles.radioInput}
|
||||||
|
checked={checked}
|
||||||
|
defaultChecked={defaultChecked}
|
||||||
|
onChange={onChange}
|
||||||
|
disabled={disabled}
|
||||||
|
name={name}
|
||||||
|
value={value}
|
||||||
|
id={id}
|
||||||
|
{...rest}
|
||||||
|
/>
|
||||||
|
{label && <span className={styles.radioLabel}>{label}</span>}
|
||||||
|
</label>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
Radio.displayName = 'Radio';
|
||||||
|
|
||||||
123
packages/ui-components/src/components/Select.module.css
Normal file
123
packages/ui-components/src/components/Select.module.css
Normal file
@ -0,0 +1,123 @@
|
|||||||
|
.selectWrapper {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--au-spacing-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.label {
|
||||||
|
display: block;
|
||||||
|
font-size: var(--au-text-sm);
|
||||||
|
font-weight: var(--au-font-medium);
|
||||||
|
color: var(--au-text-primary);
|
||||||
|
margin-bottom: var(--au-spacing-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.required {
|
||||||
|
color: var(--au-danger-500);
|
||||||
|
margin-left: var(--au-spacing-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.selectContainer {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select {
|
||||||
|
width: 100%;
|
||||||
|
font-family: var(--au-font-sans);
|
||||||
|
font-size: var(--au-text-base);
|
||||||
|
color: var(--au-text-primary);
|
||||||
|
background-color: var(--au-surface);
|
||||||
|
border: 1px solid var(--au-border-medium);
|
||||||
|
border-radius: var(--au-radius-md);
|
||||||
|
padding: 0 var(--au-spacing-10) 0 var(--au-spacing-4);
|
||||||
|
height: 40px;
|
||||||
|
outline: none;
|
||||||
|
transition: all var(--au-transition-fast);
|
||||||
|
appearance: none;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select:hover:not(:disabled) {
|
||||||
|
border-color: var(--au-border-dark);
|
||||||
|
}
|
||||||
|
|
||||||
|
.select:focus {
|
||||||
|
border-color: var(--au-primary);
|
||||||
|
box-shadow: 0 0 0 3px var(--au-primary-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
.select:disabled {
|
||||||
|
opacity: 0.6;
|
||||||
|
cursor: not-allowed;
|
||||||
|
background-color: var(--au-bg-tertiary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.selectIcon {
|
||||||
|
position: absolute;
|
||||||
|
right: var(--au-spacing-3);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: var(--au-text-tertiary);
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Sizes */
|
||||||
|
.sm .select {
|
||||||
|
height: 32px;
|
||||||
|
font-size: var(--au-text-sm);
|
||||||
|
padding: 0 var(--au-spacing-8) 0 var(--au-spacing-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.md .select {
|
||||||
|
height: 40px;
|
||||||
|
font-size: var(--au-text-base);
|
||||||
|
padding: 0 var(--au-spacing-10) 0 var(--au-spacing-4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.lg .select {
|
||||||
|
height: 48px;
|
||||||
|
font-size: var(--au-text-lg);
|
||||||
|
padding: 0 var(--au-spacing-10) 0 var(--au-spacing-5);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Error state */
|
||||||
|
.error .select {
|
||||||
|
border-color: var(--au-danger-500);
|
||||||
|
}
|
||||||
|
|
||||||
|
.error .select:focus {
|
||||||
|
border-color: var(--au-danger-600);
|
||||||
|
box-shadow: 0 0 0 3px var(--au-danger-50);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Success state */
|
||||||
|
.success .select {
|
||||||
|
border-color: var(--au-success-500);
|
||||||
|
}
|
||||||
|
|
||||||
|
.success .select:focus {
|
||||||
|
border-color: var(--au-success-600);
|
||||||
|
box-shadow: 0 0 0 3px var(--au-success-50);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Helper text */
|
||||||
|
.helperText {
|
||||||
|
font-size: var(--au-text-sm);
|
||||||
|
color: var(--au-text-secondary);
|
||||||
|
margin-top: var(--au-spacing-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.errorText {
|
||||||
|
font-size: var(--au-text-sm);
|
||||||
|
color: var(--au-danger-600);
|
||||||
|
margin-top: var(--au-spacing-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Full width */
|
||||||
|
.fullWidth {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
132
packages/ui-components/src/components/Select.tsx
Normal file
132
packages/ui-components/src/components/Select.tsx
Normal file
@ -0,0 +1,132 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { cn } from '../utils/helpers';
|
||||||
|
import type { ComponentBaseProps } from '../types';
|
||||||
|
import styles from './Select.module.css';
|
||||||
|
|
||||||
|
export interface SelectOption {
|
||||||
|
label: string;
|
||||||
|
value: string | number;
|
||||||
|
disabled?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SelectProps extends ComponentBaseProps {
|
||||||
|
label?: string;
|
||||||
|
options: SelectOption[];
|
||||||
|
value?: string | number;
|
||||||
|
defaultValue?: string | number;
|
||||||
|
onChange?: (event: React.ChangeEvent<HTMLSelectElement>) => void;
|
||||||
|
onBlur?: (event: React.FocusEvent<HTMLSelectElement>) => void;
|
||||||
|
onFocus?: (event: React.FocusEvent<HTMLSelectElement>) => void;
|
||||||
|
disabled?: boolean;
|
||||||
|
required?: boolean;
|
||||||
|
error?: boolean;
|
||||||
|
errorMessage?: string;
|
||||||
|
success?: boolean;
|
||||||
|
helperText?: string;
|
||||||
|
fullWidth?: boolean;
|
||||||
|
name?: string;
|
||||||
|
size?: 'sm' | 'md' | 'lg';
|
||||||
|
placeholder?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Select = React.forwardRef<HTMLSelectElement, SelectProps>(
|
||||||
|
(props, ref) => {
|
||||||
|
const {
|
||||||
|
label,
|
||||||
|
options,
|
||||||
|
value,
|
||||||
|
defaultValue,
|
||||||
|
onChange,
|
||||||
|
onBlur,
|
||||||
|
onFocus,
|
||||||
|
disabled = false,
|
||||||
|
required = false,
|
||||||
|
error = false,
|
||||||
|
errorMessage,
|
||||||
|
success = false,
|
||||||
|
helperText,
|
||||||
|
fullWidth = false,
|
||||||
|
className,
|
||||||
|
style,
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
size = 'md',
|
||||||
|
placeholder,
|
||||||
|
...rest
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
styles.selectWrapper,
|
||||||
|
styles[size],
|
||||||
|
error && styles.error,
|
||||||
|
success && styles.success,
|
||||||
|
fullWidth && styles.fullWidth,
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
style={style}
|
||||||
|
>
|
||||||
|
{label && (
|
||||||
|
<label htmlFor={id} className={styles.label}>
|
||||||
|
{label}
|
||||||
|
{required && <span className={styles.required}>*</span>}
|
||||||
|
</label>
|
||||||
|
)}
|
||||||
|
<div className={styles.selectContainer}>
|
||||||
|
<select
|
||||||
|
ref={ref}
|
||||||
|
id={id}
|
||||||
|
name={name}
|
||||||
|
className={styles.select}
|
||||||
|
value={value}
|
||||||
|
defaultValue={defaultValue}
|
||||||
|
onChange={onChange}
|
||||||
|
onBlur={onBlur}
|
||||||
|
onFocus={onFocus}
|
||||||
|
disabled={disabled}
|
||||||
|
required={required}
|
||||||
|
aria-invalid={error}
|
||||||
|
aria-describedby={
|
||||||
|
errorMessage ? `${id}-error` : helperText ? `${id}-helper` : undefined
|
||||||
|
}
|
||||||
|
{...rest}
|
||||||
|
>
|
||||||
|
{placeholder && (
|
||||||
|
<option value="" disabled>
|
||||||
|
{placeholder}
|
||||||
|
</option>
|
||||||
|
)}
|
||||||
|
{options.map((option) => (
|
||||||
|
<option
|
||||||
|
key={option.value}
|
||||||
|
value={option.value}
|
||||||
|
disabled={option.disabled}
|
||||||
|
>
|
||||||
|
{option.label}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
<span className={styles.selectIcon}>
|
||||||
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
|
||||||
|
<path d="M4.427 6.427a.6.6 0 01.849 0L8 9.151l2.724-2.724a.6.6 0 11.849.849l-3.149 3.148a.6.6 0 01-.848 0L4.427 7.276a.6.6 0 010-.849z" />
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{errorMessage && error && (
|
||||||
|
<span id={`${id}-error`} className={styles.errorText}>
|
||||||
|
{errorMessage}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{helperText && !error && (
|
||||||
|
<span id={`${id}-helper`} className={styles.helperText}>
|
||||||
|
{helperText}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
Select.displayName = 'Select';
|
||||||
|
|
||||||
122
packages/ui-components/src/components/Switch.module.css
Normal file
122
packages/ui-components/src/components/Switch.module.css
Normal file
@ -0,0 +1,122 @@
|
|||||||
|
.switch {
|
||||||
|
position: relative;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
cursor: pointer;
|
||||||
|
gap: var(--au-spacing-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.switchInput {
|
||||||
|
appearance: none;
|
||||||
|
width: 44px;
|
||||||
|
height: 24px;
|
||||||
|
background-color: var(--au-border-medium);
|
||||||
|
border-radius: var(--au-radius-full);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all var(--au-transition-base);
|
||||||
|
flex-shrink: 0;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.switchInput::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 2px;
|
||||||
|
left: 2px;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
background-color: white;
|
||||||
|
border-radius: var(--au-radius-full);
|
||||||
|
transition: all var(--au-transition-base);
|
||||||
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.switchInput:checked {
|
||||||
|
background-color: var(--au-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.switchInput:checked::after {
|
||||||
|
transform: translateX(20px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.switchInput:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.switchInput:focus-visible {
|
||||||
|
outline: 2px solid var(--au-primary);
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.switchLabel {
|
||||||
|
font-size: var(--au-text-sm);
|
||||||
|
color: var(--au-text-primary);
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.switch:has(.switchInput:disabled) .switchLabel {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Variants */
|
||||||
|
.success .switchInput:checked {
|
||||||
|
background-color: var(--au-success-600);
|
||||||
|
}
|
||||||
|
|
||||||
|
.warning .switchInput:checked {
|
||||||
|
background-color: var(--au-warning-600);
|
||||||
|
}
|
||||||
|
|
||||||
|
.danger .switchInput:checked {
|
||||||
|
background-color: var(--au-danger-600);
|
||||||
|
}
|
||||||
|
|
||||||
|
.info .switchInput:checked {
|
||||||
|
background-color: var(--au-info-600);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Sizes */
|
||||||
|
.sm .switchInput {
|
||||||
|
width: 36px;
|
||||||
|
height: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sm .switchInput::after {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sm .switchInput:checked::after {
|
||||||
|
transform: translateX(16px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.md .switchInput {
|
||||||
|
width: 44px;
|
||||||
|
height: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.md .switchInput::after {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.md .switchInput:checked::after {
|
||||||
|
transform: translateX(20px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.lg .switchInput {
|
||||||
|
width: 52px;
|
||||||
|
height: 28px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lg .switchInput::after {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lg .switchInput:checked::after {
|
||||||
|
transform: translateX(24px);
|
||||||
|
}
|
||||||
|
|
||||||
65
packages/ui-components/src/components/Switch.tsx
Normal file
65
packages/ui-components/src/components/Switch.tsx
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { cn } from '../utils/helpers';
|
||||||
|
import type { ComponentBaseProps } from '../types';
|
||||||
|
import styles from './Switch.module.css';
|
||||||
|
|
||||||
|
export interface SwitchProps extends ComponentBaseProps {
|
||||||
|
label?: string;
|
||||||
|
checked?: boolean;
|
||||||
|
defaultChecked?: boolean;
|
||||||
|
onChange?: (event: React.ChangeEvent<HTMLInputElement>) => void;
|
||||||
|
disabled?: boolean;
|
||||||
|
name?: string;
|
||||||
|
variant?: 'primary' | 'success' | 'warning' | 'danger' | 'info';
|
||||||
|
size?: 'sm' | 'md' | 'lg';
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Switch = React.forwardRef<HTMLInputElement, SwitchProps>(
|
||||||
|
(props, ref) => {
|
||||||
|
const {
|
||||||
|
label,
|
||||||
|
checked,
|
||||||
|
defaultChecked,
|
||||||
|
onChange,
|
||||||
|
disabled = false,
|
||||||
|
name,
|
||||||
|
variant = 'primary',
|
||||||
|
size = 'md',
|
||||||
|
className,
|
||||||
|
style,
|
||||||
|
id,
|
||||||
|
...rest
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<label
|
||||||
|
className={cn(
|
||||||
|
styles.switch,
|
||||||
|
styles[variant],
|
||||||
|
styles[size],
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
style={style}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
ref={ref}
|
||||||
|
type="checkbox"
|
||||||
|
className={styles.switchInput}
|
||||||
|
checked={checked}
|
||||||
|
defaultChecked={defaultChecked}
|
||||||
|
onChange={onChange}
|
||||||
|
disabled={disabled}
|
||||||
|
name={name}
|
||||||
|
id={id}
|
||||||
|
role="switch"
|
||||||
|
aria-checked={checked}
|
||||||
|
{...rest}
|
||||||
|
/>
|
||||||
|
{label && <span className={styles.switchLabel}>{label}</span>}
|
||||||
|
</label>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
Switch.displayName = 'Switch';
|
||||||
|
|
||||||
135
packages/ui-components/src/components/Tabs.module.css
Normal file
135
packages/ui-components/src/components/Tabs.module.css
Normal file
@ -0,0 +1,135 @@
|
|||||||
|
.tabs {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--au-spacing-4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tabsList {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--au-spacing-2);
|
||||||
|
border-bottom: 2px solid var(--au-border-light);
|
||||||
|
overflow-x: auto;
|
||||||
|
scrollbar-width: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tabsList::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--au-spacing-2);
|
||||||
|
padding: var(--au-spacing-3) var(--au-spacing-4);
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
border-bottom: 2px solid transparent;
|
||||||
|
color: var(--au-text-secondary);
|
||||||
|
font-size: var(--au-text-sm);
|
||||||
|
font-weight: var(--au-font-medium);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all var(--au-transition-fast);
|
||||||
|
white-space: nowrap;
|
||||||
|
position: relative;
|
||||||
|
bottom: -2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab:hover:not(:disabled) {
|
||||||
|
color: var(--au-text-primary);
|
||||||
|
background-color: var(--au-surface-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab.active {
|
||||||
|
color: var(--au-primary);
|
||||||
|
border-bottom-color: var(--au-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab:focus-visible {
|
||||||
|
outline: 2px solid var(--au-primary);
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tabIcon {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tabPanel {
|
||||||
|
padding: var(--au-spacing-4) 0;
|
||||||
|
animation: au-fadeIn var(--au-transition-base);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Variants */
|
||||||
|
.pills .tabsList {
|
||||||
|
border-bottom: none;
|
||||||
|
gap: var(--au-spacing-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pills .tab {
|
||||||
|
border-bottom: none;
|
||||||
|
border-radius: var(--au-radius-md);
|
||||||
|
bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pills .tab.active {
|
||||||
|
background-color: var(--au-primary);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pills .tab.active:hover {
|
||||||
|
background-color: var(--au-primary-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.boxed .tabsList {
|
||||||
|
border-bottom: none;
|
||||||
|
gap: var(--au-spacing-1);
|
||||||
|
background-color: var(--au-bg-tertiary);
|
||||||
|
padding: var(--au-spacing-1);
|
||||||
|
border-radius: var(--au-radius-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
.boxed .tab {
|
||||||
|
border-bottom: none;
|
||||||
|
border-radius: var(--au-radius-base);
|
||||||
|
bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.boxed .tab.active {
|
||||||
|
background-color: var(--au-surface);
|
||||||
|
color: var(--au-text-primary);
|
||||||
|
box-shadow: var(--au-shadow-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Vertical */
|
||||||
|
.vertical {
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vertical .tabsList {
|
||||||
|
flex-direction: column;
|
||||||
|
border-bottom: none;
|
||||||
|
border-right: 2px solid var(--au-border-light);
|
||||||
|
gap: var(--au-spacing-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.vertical .tab {
|
||||||
|
width: 100%;
|
||||||
|
justify-content: flex-start;
|
||||||
|
border-bottom: none;
|
||||||
|
border-right: 2px solid transparent;
|
||||||
|
bottom: 0;
|
||||||
|
right: -2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vertical .tab.active {
|
||||||
|
border-right-color: var(--au-primary);
|
||||||
|
border-bottom-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
95
packages/ui-components/src/components/Tabs.tsx
Normal file
95
packages/ui-components/src/components/Tabs.tsx
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import { cn } from '../utils/helpers';
|
||||||
|
import type { ComponentBaseProps } from '../types';
|
||||||
|
import styles from './Tabs.module.css';
|
||||||
|
|
||||||
|
export interface Tab {
|
||||||
|
id: string;
|
||||||
|
label: string;
|
||||||
|
icon?: React.ReactNode;
|
||||||
|
disabled?: boolean;
|
||||||
|
content: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TabsProps extends ComponentBaseProps {
|
||||||
|
tabs: Tab[];
|
||||||
|
defaultActiveTab?: string;
|
||||||
|
activeTab?: string;
|
||||||
|
onTabChange?: (tabId: string) => void;
|
||||||
|
variant?: 'default' | 'pills' | 'boxed';
|
||||||
|
orientation?: 'horizontal' | 'vertical';
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Tabs: React.FC<TabsProps> = (props) => {
|
||||||
|
const {
|
||||||
|
tabs,
|
||||||
|
defaultActiveTab,
|
||||||
|
activeTab: controlledActiveTab,
|
||||||
|
onTabChange,
|
||||||
|
variant = 'default',
|
||||||
|
orientation = 'horizontal',
|
||||||
|
className,
|
||||||
|
style,
|
||||||
|
id,
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
const [internalActiveTab, setInternalActiveTab] = useState(
|
||||||
|
defaultActiveTab || tabs[0]?.id
|
||||||
|
);
|
||||||
|
|
||||||
|
const activeTab = controlledActiveTab !== undefined ? controlledActiveTab : internalActiveTab;
|
||||||
|
|
||||||
|
const handleTabClick = (tabId: string, disabled?: boolean) => {
|
||||||
|
if (disabled) return;
|
||||||
|
|
||||||
|
if (controlledActiveTab === undefined) {
|
||||||
|
setInternalActiveTab(tabId);
|
||||||
|
}
|
||||||
|
onTabChange?.(tabId);
|
||||||
|
};
|
||||||
|
|
||||||
|
const activeTabContent = tabs.find((tab) => tab.id === activeTab)?.content;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
styles.tabs,
|
||||||
|
styles[variant],
|
||||||
|
orientation === 'vertical' && styles.vertical,
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
style={style}
|
||||||
|
id={id}
|
||||||
|
>
|
||||||
|
<div className={styles.tabsList} role="tablist">
|
||||||
|
{tabs.map((tab) => (
|
||||||
|
<button
|
||||||
|
key={tab.id}
|
||||||
|
className={cn(styles.tab, activeTab === tab.id && styles.active)}
|
||||||
|
onClick={() => handleTabClick(tab.id, tab.disabled)}
|
||||||
|
disabled={tab.disabled}
|
||||||
|
role="tab"
|
||||||
|
aria-selected={activeTab === tab.id}
|
||||||
|
aria-controls={`${tab.id}-panel`}
|
||||||
|
id={`${tab.id}-tab`}
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
{tab.icon && <span className={styles.tabIcon}>{tab.icon}</span>}
|
||||||
|
{tab.label}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className={styles.tabPanel}
|
||||||
|
role="tabpanel"
|
||||||
|
aria-labelledby={`${activeTab}-tab`}
|
||||||
|
id={`${activeTab}-panel`}
|
||||||
|
>
|
||||||
|
{activeTabContent}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
Tabs.displayName = 'Tabs';
|
||||||
|
|
||||||
121
packages/ui-components/src/components/Textarea.module.css
Normal file
121
packages/ui-components/src/components/Textarea.module.css
Normal file
@ -0,0 +1,121 @@
|
|||||||
|
.textareaWrapper {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--au-spacing-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.label {
|
||||||
|
display: block;
|
||||||
|
font-size: var(--au-text-sm);
|
||||||
|
font-weight: var(--au-font-medium);
|
||||||
|
color: var(--au-text-primary);
|
||||||
|
margin-bottom: var(--au-spacing-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.required {
|
||||||
|
color: var(--au-danger-500);
|
||||||
|
margin-left: var(--au-spacing-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.textarea {
|
||||||
|
width: 100%;
|
||||||
|
font-family: var(--au-font-sans);
|
||||||
|
font-size: var(--au-text-base);
|
||||||
|
color: var(--au-text-primary);
|
||||||
|
background-color: var(--au-surface);
|
||||||
|
border: 1px solid var(--au-border-medium);
|
||||||
|
border-radius: var(--au-radius-md);
|
||||||
|
padding: var(--au-spacing-3) var(--au-spacing-4);
|
||||||
|
min-height: 100px;
|
||||||
|
resize: vertical;
|
||||||
|
outline: none;
|
||||||
|
transition: all var(--au-transition-fast);
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.textarea::placeholder {
|
||||||
|
color: var(--au-text-tertiary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.textarea:hover:not(:disabled) {
|
||||||
|
border-color: var(--au-border-dark);
|
||||||
|
}
|
||||||
|
|
||||||
|
.textarea:focus {
|
||||||
|
border-color: var(--au-primary);
|
||||||
|
box-shadow: 0 0 0 3px var(--au-primary-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
.textarea:disabled {
|
||||||
|
opacity: 0.6;
|
||||||
|
cursor: not-allowed;
|
||||||
|
background-color: var(--au-bg-tertiary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Error state */
|
||||||
|
.error .textarea {
|
||||||
|
border-color: var(--au-danger-500);
|
||||||
|
}
|
||||||
|
|
||||||
|
.error .textarea:focus {
|
||||||
|
border-color: var(--au-danger-600);
|
||||||
|
box-shadow: 0 0 0 3px var(--au-danger-50);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Success state */
|
||||||
|
.success .textarea {
|
||||||
|
border-color: var(--au-success-500);
|
||||||
|
}
|
||||||
|
|
||||||
|
.success .textarea:focus {
|
||||||
|
border-color: var(--au-success-600);
|
||||||
|
box-shadow: 0 0 0 3px var(--au-success-50);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Helper text */
|
||||||
|
.helperText {
|
||||||
|
font-size: var(--au-text-sm);
|
||||||
|
color: var(--au-text-secondary);
|
||||||
|
margin-top: var(--au-spacing-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.errorText {
|
||||||
|
font-size: var(--au-text-sm);
|
||||||
|
color: var(--au-danger-600);
|
||||||
|
margin-top: var(--au-spacing-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Character count */
|
||||||
|
.characterCount {
|
||||||
|
font-size: var(--au-text-xs);
|
||||||
|
color: var(--au-text-tertiary);
|
||||||
|
margin-top: var(--au-spacing-1);
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.characterCount.limit {
|
||||||
|
color: var(--au-danger-600);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Resize options */
|
||||||
|
.resizeNone .textarea {
|
||||||
|
resize: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resizeVertical .textarea {
|
||||||
|
resize: vertical;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resizeHorizontal .textarea {
|
||||||
|
resize: horizontal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resizeBoth .textarea {
|
||||||
|
resize: both;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Full width */
|
||||||
|
.fullWidth {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
133
packages/ui-components/src/components/Textarea.tsx
Normal file
133
packages/ui-components/src/components/Textarea.tsx
Normal file
@ -0,0 +1,133 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import { cn } from '../utils/helpers';
|
||||||
|
import type { ComponentBaseProps } from '../types';
|
||||||
|
import styles from './Textarea.module.css';
|
||||||
|
|
||||||
|
export interface TextareaProps extends ComponentBaseProps {
|
||||||
|
label?: string;
|
||||||
|
placeholder?: string;
|
||||||
|
value?: string;
|
||||||
|
defaultValue?: string;
|
||||||
|
onChange?: (event: React.ChangeEvent<HTMLTextAreaElement>) => void;
|
||||||
|
onBlur?: (event: React.FocusEvent<HTMLTextAreaElement>) => void;
|
||||||
|
onFocus?: (event: React.FocusEvent<HTMLTextAreaElement>) => void;
|
||||||
|
disabled?: boolean;
|
||||||
|
required?: boolean;
|
||||||
|
error?: boolean;
|
||||||
|
errorMessage?: string;
|
||||||
|
success?: boolean;
|
||||||
|
helperText?: string;
|
||||||
|
fullWidth?: boolean;
|
||||||
|
name?: string;
|
||||||
|
rows?: number;
|
||||||
|
maxLength?: number;
|
||||||
|
showCharacterCount?: boolean;
|
||||||
|
resize?: 'none' | 'vertical' | 'horizontal' | 'both';
|
||||||
|
autoFocus?: boolean;
|
||||||
|
readOnly?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
|
||||||
|
(props, ref) => {
|
||||||
|
const {
|
||||||
|
label,
|
||||||
|
placeholder,
|
||||||
|
value,
|
||||||
|
defaultValue,
|
||||||
|
onChange,
|
||||||
|
onBlur,
|
||||||
|
onFocus,
|
||||||
|
disabled = false,
|
||||||
|
required = false,
|
||||||
|
error = false,
|
||||||
|
errorMessage,
|
||||||
|
success = false,
|
||||||
|
helperText,
|
||||||
|
fullWidth = false,
|
||||||
|
className,
|
||||||
|
style,
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
rows = 4,
|
||||||
|
maxLength,
|
||||||
|
showCharacterCount = false,
|
||||||
|
resize = 'vertical',
|
||||||
|
autoFocus,
|
||||||
|
readOnly,
|
||||||
|
...rest
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
const [charCount, setCharCount] = useState(
|
||||||
|
value?.length || defaultValue?.length || 0
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||||
|
setCharCount(e.target.value.length);
|
||||||
|
onChange?.(e);
|
||||||
|
};
|
||||||
|
|
||||||
|
const isAtLimit = maxLength && charCount >= maxLength;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
styles.textareaWrapper,
|
||||||
|
error && styles.error,
|
||||||
|
success && styles.success,
|
||||||
|
fullWidth && styles.fullWidth,
|
||||||
|
styles[`resize${resize.charAt(0).toUpperCase() + resize.slice(1)}`],
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
style={style}
|
||||||
|
>
|
||||||
|
{label && (
|
||||||
|
<label htmlFor={id} className={styles.label}>
|
||||||
|
{label}
|
||||||
|
{required && <span className={styles.required}>*</span>}
|
||||||
|
</label>
|
||||||
|
)}
|
||||||
|
<textarea
|
||||||
|
ref={ref}
|
||||||
|
id={id}
|
||||||
|
name={name}
|
||||||
|
className={styles.textarea}
|
||||||
|
placeholder={placeholder}
|
||||||
|
value={value}
|
||||||
|
defaultValue={defaultValue}
|
||||||
|
onChange={handleChange}
|
||||||
|
onBlur={onBlur}
|
||||||
|
onFocus={onFocus}
|
||||||
|
disabled={disabled}
|
||||||
|
required={required}
|
||||||
|
rows={rows}
|
||||||
|
maxLength={maxLength}
|
||||||
|
autoFocus={autoFocus}
|
||||||
|
readOnly={readOnly}
|
||||||
|
aria-invalid={error}
|
||||||
|
aria-describedby={
|
||||||
|
errorMessage ? `${id}-error` : helperText ? `${id}-helper` : undefined
|
||||||
|
}
|
||||||
|
{...rest}
|
||||||
|
/>
|
||||||
|
{showCharacterCount && maxLength && (
|
||||||
|
<div className={cn(styles.characterCount, isAtLimit && styles.limit)}>
|
||||||
|
{charCount}/{maxLength}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{errorMessage && error && (
|
||||||
|
<span id={`${id}-error`} className={styles.errorText}>
|
||||||
|
{errorMessage}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{helperText && !error && (
|
||||||
|
<span id={`${id}-helper`} className={styles.helperText}>
|
||||||
|
{helperText}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
Textarea.displayName = 'Textarea';
|
||||||
|
|
||||||
@ -11,6 +11,21 @@ export type { CardProps, CardSectionProps } from './components/Card';
|
|||||||
export { Input } from './components/Input';
|
export { Input } from './components/Input';
|
||||||
export type { InputProps } from './components/Input';
|
export type { InputProps } from './components/Input';
|
||||||
|
|
||||||
|
export { Textarea } from './components/Textarea';
|
||||||
|
export type { TextareaProps } from './components/Textarea';
|
||||||
|
|
||||||
|
export { Select } from './components/Select';
|
||||||
|
export type { SelectProps, SelectOption } from './components/Select';
|
||||||
|
|
||||||
|
export { Checkbox } from './components/Checkbox';
|
||||||
|
export type { CheckboxProps } from './components/Checkbox';
|
||||||
|
|
||||||
|
export { Radio } from './components/Radio';
|
||||||
|
export type { RadioProps } from './components/Radio';
|
||||||
|
|
||||||
|
export { Switch } from './components/Switch';
|
||||||
|
export type { SwitchProps } from './components/Switch';
|
||||||
|
|
||||||
export { Dropdown, DropdownItem, DropdownDivider, DropdownHeader } from './components/Dropdown';
|
export { Dropdown, DropdownItem, DropdownDivider, DropdownHeader } from './components/Dropdown';
|
||||||
export type { DropdownProps, DropdownItemProps } from './components/Dropdown';
|
export type { DropdownProps, DropdownItemProps } from './components/Dropdown';
|
||||||
|
|
||||||
@ -32,6 +47,21 @@ export type { SpinnerProps } from './components/Spinner';
|
|||||||
export { Alert } from './components/Alert';
|
export { Alert } from './components/Alert';
|
||||||
export type { AlertProps } from './components/Alert';
|
export type { AlertProps } from './components/Alert';
|
||||||
|
|
||||||
|
export { Tabs } from './components/Tabs';
|
||||||
|
export type { TabsProps, Tab } from './components/Tabs';
|
||||||
|
|
||||||
|
export { Accordion } from './components/Accordion';
|
||||||
|
export type { AccordionProps, AccordionItem } from './components/Accordion';
|
||||||
|
|
||||||
|
export { Breadcrumb } from './components/Breadcrumb';
|
||||||
|
export type { BreadcrumbProps, BreadcrumbItem } from './components/Breadcrumb';
|
||||||
|
|
||||||
|
export { Progress } from './components/Progress';
|
||||||
|
export type { ProgressProps } from './components/Progress';
|
||||||
|
|
||||||
|
export { Pagination } from './components/Pagination';
|
||||||
|
export type { PaginationProps } from './components/Pagination';
|
||||||
|
|
||||||
// Types
|
// Types
|
||||||
export type { ButtonVariant, ButtonSize, Theme, ComponentBaseProps } from './types';
|
export type { ButtonVariant, ButtonSize, Theme, ComponentBaseProps } from './types';
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user