@innertia-solutions/ui 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/components/App/EmptyState.vue +433 -0
- package/components/App/Icon.vue +99 -0
- package/components/App/LoadingState.vue +40 -0
- package/components/App/SwitchColorTheme.vue +55 -0
- package/components/Forms/Select.vue +89 -0
- package/components/Modal/DeleteConfirm.vue +163 -0
- package/components/Table/DownloadDropdown.vue +111 -0
- package/components/Table/FilterDropdown.vue +226 -0
- package/components/Toast/Alert.vue +113 -0
- package/components/Toast/Notification.vue +45 -0
- package/components/Toast/Process.vue +88 -0
- package/composables/useDate.js +240 -0
- package/composables/useDevice.js +21 -0
- package/composables/useDownload.js +67 -0
- package/composables/useForm.js +259 -0
- package/composables/useRutFormatter.js +20 -0
- package/composables/useTable.ts +124 -0
- package/composables/useTimeAgo.js +25 -0
- package/composables/useToast.js +69 -0
- package/nuxt.config.ts +8 -0
- package/package.json +22 -0
- package/stores/toast.js +131 -0
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
import { reactive, toRefs, onMounted } from 'vue';
|
|
2
|
+
|
|
3
|
+
// Constantes de colores para errores
|
|
4
|
+
const ERROR_COLORS = {
|
|
5
|
+
border: {
|
|
6
|
+
light: 'border-red-400',
|
|
7
|
+
dark: 'dark:border-red-500/50'
|
|
8
|
+
},
|
|
9
|
+
text: {
|
|
10
|
+
light: 'text-slate-900', // Keep text readable
|
|
11
|
+
dark: 'dark:text-white'
|
|
12
|
+
},
|
|
13
|
+
placeholder: {
|
|
14
|
+
light: 'placeholder-red-300',
|
|
15
|
+
dark: 'dark:placeholder-red-500/50'
|
|
16
|
+
},
|
|
17
|
+
ring: {
|
|
18
|
+
light: 'focus:ring-red-500/20', // Subtle glow instead of thick ring
|
|
19
|
+
dark: 'dark:focus:ring-red-500/20'
|
|
20
|
+
},
|
|
21
|
+
focusBorder: {
|
|
22
|
+
light: 'focus:border-red-500',
|
|
23
|
+
dark: 'dark:focus:border-red-500'
|
|
24
|
+
},
|
|
25
|
+
message: {
|
|
26
|
+
light: 'text-red-500',
|
|
27
|
+
dark: 'dark:text-red-400'
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const rules = {
|
|
32
|
+
required: (value) => {
|
|
33
|
+
// inválido si: null/undefined, string vacío o solo espacios, array vacío
|
|
34
|
+
if (value === null || value === undefined) return 'Este campo es obligatorio';
|
|
35
|
+
if (typeof value === 'string' && value.trim() === '') return 'Este campo es obligatorio';
|
|
36
|
+
if (Array.isArray(value) && value.length === 0) return 'Este campo es obligatorio';
|
|
37
|
+
return true;
|
|
38
|
+
},
|
|
39
|
+
email: (value) => /.+@.+\..+/.test(value) || 'El correo no es válido',
|
|
40
|
+
min: (value, arg) => value.length >= arg || `Debe tener al menos ${arg} caracteres`,
|
|
41
|
+
int: (value) => Number.isInteger(+value) || 'Debe ser un número entero',
|
|
42
|
+
rut: (value) => validateRut(value) || 'El RUT no es válido',
|
|
43
|
+
same: (value, arg, form) => value === form[arg] || 'Los campos no coinciden',
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const dictionary = {
|
|
47
|
+
unique: 'Ya está registrado',
|
|
48
|
+
required: 'Este campo es obligatorio',
|
|
49
|
+
invalid: 'Dato inválido',
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
export function useForm(containerId, formDefinition, options = {}) {
|
|
53
|
+
const zodSchema = options.zodSchema;
|
|
54
|
+
const form = reactive({});
|
|
55
|
+
const errors = reactive({});
|
|
56
|
+
|
|
57
|
+
for (const field in formDefinition) {
|
|
58
|
+
form[field] = formDefinition[field]?.value ?? '';
|
|
59
|
+
errors[field] = [];
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const reset = () => {
|
|
63
|
+
for (const field in formDefinition) {
|
|
64
|
+
form[field] = '';
|
|
65
|
+
errors[field] = [];
|
|
66
|
+
clearValidation(field);
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const resetErrors = () => {
|
|
71
|
+
for (const field in formDefinition) {
|
|
72
|
+
errors[field] = [];
|
|
73
|
+
clearValidation(field);
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const validateField = (field) => {
|
|
78
|
+
const def = formDefinition[field];
|
|
79
|
+
const value = form[field];
|
|
80
|
+
errors[field] = [];
|
|
81
|
+
|
|
82
|
+
if (!def.rules) return true;
|
|
83
|
+
|
|
84
|
+
def.rules.forEach(rule => {
|
|
85
|
+
let ruleName = typeof rule === 'string' ? rule : rule.name;
|
|
86
|
+
let arg = typeof rule === 'object' ? rule.arg : undefined;
|
|
87
|
+
const result = rules[ruleName](value, arg, form);
|
|
88
|
+
if (result !== true) {
|
|
89
|
+
const custom = def.messages?.[ruleName];
|
|
90
|
+
errors[field].push(custom || result);
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
updateValidation(field);
|
|
95
|
+
return errors[field].length === 0;
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
const validateForm = () => {
|
|
99
|
+
if (zodSchema) {
|
|
100
|
+
const result = zodSchema.safeParse(form);
|
|
101
|
+
resetErrors();
|
|
102
|
+
|
|
103
|
+
if (!result.success) {
|
|
104
|
+
for (const issue of result.error.errors) {
|
|
105
|
+
const field = issue.path[0];
|
|
106
|
+
if (errors[field]) {
|
|
107
|
+
errors[field].push(issue.message);
|
|
108
|
+
updateValidation(field);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return true;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
for (const field in formDefinition) validateField(field);
|
|
118
|
+
|
|
119
|
+
return Object.values(errors).every(e => e.length === 0);
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
const updateValidation = (field) => {
|
|
123
|
+
const el = document.querySelector(`#${containerId} [name="${field}"]`);
|
|
124
|
+
if (!el) return;
|
|
125
|
+
|
|
126
|
+
if (errors[field].length > 0) {
|
|
127
|
+
// Limpiar validación anterior solo para reemplazarla
|
|
128
|
+
clearValidation(field);
|
|
129
|
+
|
|
130
|
+
// Aplicar clases de error al elemento usando las constantes
|
|
131
|
+
el.classList.add(
|
|
132
|
+
ERROR_COLORS.border.light,
|
|
133
|
+
ERROR_COLORS.border.dark,
|
|
134
|
+
ERROR_COLORS.text.light,
|
|
135
|
+
ERROR_COLORS.text.dark,
|
|
136
|
+
ERROR_COLORS.placeholder.light,
|
|
137
|
+
ERROR_COLORS.placeholder.dark,
|
|
138
|
+
ERROR_COLORS.ring.light,
|
|
139
|
+
ERROR_COLORS.ring.dark,
|
|
140
|
+
ERROR_COLORS.focusBorder.light,
|
|
141
|
+
ERROR_COLORS.focusBorder.dark
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
const errorEl = document.createElement('p');
|
|
145
|
+
errorEl.className = `mt-1 text-sm ${ERROR_COLORS.message.light} ${ERROR_COLORS.message.dark} form-error-message form-error-${field}`;
|
|
146
|
+
errorEl.textContent = errors[field][0];
|
|
147
|
+
|
|
148
|
+
// Buscar el contenedor correcto para insertar el mensaje de error
|
|
149
|
+
let container = el.parentNode;
|
|
150
|
+
|
|
151
|
+
// Si el input está dentro de un div relativo (como en el caso del password),
|
|
152
|
+
// el mensaje debe ir después de ese div
|
|
153
|
+
if (container && container.classList.contains('relative')) {
|
|
154
|
+
container.parentNode?.appendChild(errorEl);
|
|
155
|
+
} else {
|
|
156
|
+
// Para inputs normales y selects, agregar después del contenedor
|
|
157
|
+
container?.appendChild(errorEl);
|
|
158
|
+
}
|
|
159
|
+
} else {
|
|
160
|
+
// Solo limpiar si no hay errores
|
|
161
|
+
clearValidation(field);
|
|
162
|
+
}
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
const clearValidation = (field) => {
|
|
166
|
+
const el = document.querySelector(`#${containerId} [name="${field}"]`);
|
|
167
|
+
if (!el) return;
|
|
168
|
+
|
|
169
|
+
// Remover clases de error usando las constantes
|
|
170
|
+
el.classList.remove(
|
|
171
|
+
ERROR_COLORS.border.light,
|
|
172
|
+
ERROR_COLORS.border.dark,
|
|
173
|
+
ERROR_COLORS.text.light,
|
|
174
|
+
ERROR_COLORS.text.dark,
|
|
175
|
+
ERROR_COLORS.placeholder.light,
|
|
176
|
+
ERROR_COLORS.placeholder.dark,
|
|
177
|
+
ERROR_COLORS.ring.light,
|
|
178
|
+
ERROR_COLORS.ring.dark,
|
|
179
|
+
ERROR_COLORS.focusBorder.light,
|
|
180
|
+
ERROR_COLORS.focusBorder.dark
|
|
181
|
+
);
|
|
182
|
+
|
|
183
|
+
// Buscar y eliminar solo los mensajes de error específicos de este campo
|
|
184
|
+
const formContainer = document.querySelector(`#${containerId}`);
|
|
185
|
+
if (formContainer) {
|
|
186
|
+
const fieldErrorMessages = formContainer.querySelectorAll(`.form-error-${field}`);
|
|
187
|
+
fieldErrorMessages.forEach(errorEl => errorEl.remove());
|
|
188
|
+
}
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
const addError = (field, message) => {
|
|
192
|
+
if (message.startsWith('validation.')) {
|
|
193
|
+
const key = message.split('.')[1];
|
|
194
|
+
message = dictionary[key] || key;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
errors[field].push(message);
|
|
198
|
+
updateValidation(field);
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
const attachEvents = () => {
|
|
202
|
+
// Solo adjuntar eventos si se especifican explícitamente
|
|
203
|
+
// Si options.events es undefined o no se proporciona, no adjuntar eventos automáticos
|
|
204
|
+
if (!options?.events || options.events.length === 0) {
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const events = options.events;
|
|
209
|
+
|
|
210
|
+
for (const field in formDefinition) {
|
|
211
|
+
const el = document.querySelector(`#${containerId} [name="${field}"]`);
|
|
212
|
+
if (el) {
|
|
213
|
+
events.forEach(evt => el.addEventListener(evt, () => validateField(field)));
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
onMounted(() => {
|
|
219
|
+
if (containerId) attachEvents();
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
const loadFromObject = (obj) => {
|
|
223
|
+
for (const field in formDefinition) {
|
|
224
|
+
if (obj[field] !== undefined) {
|
|
225
|
+
console.log(`Loading field ${field} with value:`, obj[field]);
|
|
226
|
+
form[field] = obj[field];
|
|
227
|
+
clearValidation(field);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return {
|
|
233
|
+
...toRefs(form),
|
|
234
|
+
values: form,
|
|
235
|
+
errors,
|
|
236
|
+
validate: (field) => field ? validateField(field) : validateForm(),
|
|
237
|
+
reset,
|
|
238
|
+
resetErrors,
|
|
239
|
+
addError,
|
|
240
|
+
loadFromObject,
|
|
241
|
+
config: formDefinition
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function validateRut(rut) {
|
|
246
|
+
if (!rut || typeof rut !== 'string') return false;
|
|
247
|
+
rut = rut.replace(/^0+|[^0-9kK]+/g, '').toUpperCase();
|
|
248
|
+
if (rut.length < 8) return false;
|
|
249
|
+
const body = rut.slice(0, -1);
|
|
250
|
+
const dv = rut.slice(-1);
|
|
251
|
+
let sum = 0, multiplier = 2;
|
|
252
|
+
for (let i = body.length - 1; i >= 0; i--) {
|
|
253
|
+
sum += parseInt(body[i]) * multiplier;
|
|
254
|
+
multiplier = multiplier < 7 ? multiplier + 1 : 2;
|
|
255
|
+
}
|
|
256
|
+
const expected = 11 - (sum % 11);
|
|
257
|
+
const expectedDV = expected === 11 ? '0' : expected === 10 ? 'K' : expected.toString();
|
|
258
|
+
return dv === expectedDV;
|
|
259
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
|
|
2
|
+
export const useRutFormatter = (inputElement) => {
|
|
3
|
+
const formatRut = (rut) => {
|
|
4
|
+
const cleanRut = rut.replace(/[^\dKk]/g, '').toUpperCase(); // Limpiar caracteres no numéricos
|
|
5
|
+
|
|
6
|
+
if (cleanRut.length <= 1) return cleanRut; // Si solo hay un dígito, no hacer formato
|
|
7
|
+
|
|
8
|
+
const rutBody = cleanRut.slice(0, -1); // Cuerpo del RUT
|
|
9
|
+
const dv = cleanRut.slice(-1); // Dígito verificador
|
|
10
|
+
|
|
11
|
+
const formattedRut = rutBody.replace(/\B(?=(\d{3})+(?!\d))/g, '.'); // Formatear cuerpo
|
|
12
|
+
|
|
13
|
+
return `${formattedRut}-${dv}`; // Retornar RUT formateado
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
inputElement.addEventListener('input', (e) => {
|
|
17
|
+
let formattedRut = formatRut(e.target.value);
|
|
18
|
+
e.target.value = formattedRut;
|
|
19
|
+
});
|
|
20
|
+
};
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
// composables/useTable.ts
|
|
2
|
+
|
|
3
|
+
export function useTable() {
|
|
4
|
+
// Función para invalidar caché de una tabla específica
|
|
5
|
+
const invalidateCache = (tableName: string) => {
|
|
6
|
+
if (!tableName) {
|
|
7
|
+
console.warn('[useTable] No table name provided');
|
|
8
|
+
return;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const fullCacheKey = `table_cache_${tableName}`;
|
|
12
|
+
|
|
13
|
+
try {
|
|
14
|
+
sessionStorage.removeItem(fullCacheKey);
|
|
15
|
+
console.log(`🗑️ Cache invalidated for table: ${tableName}`);
|
|
16
|
+
} catch (error) {
|
|
17
|
+
console.warn('[useTable] Error invalidating cache:', error);
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
// Función para invalidar múltiples cachés
|
|
22
|
+
const invalidateMultiple = (tableNames: string[]) => {
|
|
23
|
+
tableNames.forEach(name => invalidateCache(name));
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
// Función para limpiar todo el caché de tablas
|
|
27
|
+
const clearAllCache = () => {
|
|
28
|
+
try {
|
|
29
|
+
// Obtener todas las claves del sessionStorage
|
|
30
|
+
const keys = Object.keys(sessionStorage);
|
|
31
|
+
|
|
32
|
+
// Filtrar solo las claves de tablas
|
|
33
|
+
const tableCacheKeys = keys.filter(key => key.startsWith('table_cache_'));
|
|
34
|
+
|
|
35
|
+
// Eliminar todas
|
|
36
|
+
tableCacheKeys.forEach(key => {
|
|
37
|
+
sessionStorage.removeItem(key);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
console.log(`🗑️ Cleared ${tableCacheKeys.length} table caches`);
|
|
41
|
+
} catch (error) {
|
|
42
|
+
console.warn('[useTable] Error clearing all cache:', error);
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
// Función para manejar búsquedas persistentes con invalidación automática de caché
|
|
47
|
+
const useSearch = (tableName: string) => {
|
|
48
|
+
if (!tableName) {
|
|
49
|
+
throw new Error('[useTable] Table name is required for useSearch');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Estado global compartido para todas las búsquedas de tablas
|
|
53
|
+
const searchCache = useState<Record<string, string>>("table-search-cache", () => ({}));
|
|
54
|
+
|
|
55
|
+
// Referencia reactiva para la búsqueda de esta tabla específica
|
|
56
|
+
const search = ref(searchCache.value[tableName] || "");
|
|
57
|
+
|
|
58
|
+
// Watcher para sincronizar automáticamente con el estado global
|
|
59
|
+
// e invalidar caché cuando cambia la búsqueda
|
|
60
|
+
watch(search, (newSearch, oldSearch) => {
|
|
61
|
+
// Guardar en estado global
|
|
62
|
+
searchCache.value[tableName] = newSearch;
|
|
63
|
+
console.log(`[useTable] Saved search for "${tableName}":`, newSearch);
|
|
64
|
+
|
|
65
|
+
// Invalidar caché si la búsqueda cambió (excepto en la carga inicial)
|
|
66
|
+
if (oldSearch !== undefined && newSearch !== oldSearch) {
|
|
67
|
+
console.log(`[useTable] Search changed for "${tableName}", invalidating cache`);
|
|
68
|
+
invalidateCache(tableName);
|
|
69
|
+
}
|
|
70
|
+
}, { immediate: true });
|
|
71
|
+
|
|
72
|
+
// Función para limpiar la búsqueda específica
|
|
73
|
+
const clearSearch = () => {
|
|
74
|
+
search.value = "";
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
return {
|
|
78
|
+
search,
|
|
79
|
+
clearSearch
|
|
80
|
+
};
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
// Función para limpiar todas las búsquedas guardadas
|
|
84
|
+
const clearAllSearches = () => {
|
|
85
|
+
const searchCache = useState<Record<string, string>>("table-search-cache", () => ({}));
|
|
86
|
+
searchCache.value = {};
|
|
87
|
+
console.log('[useTable] Cleared all saved searches');
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
// Función para obtener el estado del caché de búsquedas
|
|
91
|
+
const getSearchCache = () => {
|
|
92
|
+
const searchCache = useState<Record<string, string>>("table-search-cache", () => ({}));
|
|
93
|
+
return searchCache.value;
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
// Manager for persistent filters
|
|
97
|
+
const useFilters = <T extends Record<string, any>>(tableName: string, initialFilters: T) => {
|
|
98
|
+
if (!tableName) {
|
|
99
|
+
throw new Error('[useTable] Table name is required for useFilters');
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Use Nuxt useState to persist filters across client-side navigation
|
|
103
|
+
const filters = useState<T>(`table_filters_${tableName}`, () => ({ ...initialFilters }));
|
|
104
|
+
|
|
105
|
+
const resetFilters = () => {
|
|
106
|
+
filters.value = { ...initialFilters };
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
return {
|
|
110
|
+
filters,
|
|
111
|
+
resetFilters
|
|
112
|
+
};
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
return {
|
|
116
|
+
invalidateCache,
|
|
117
|
+
invalidateMultiple,
|
|
118
|
+
clearAllCache,
|
|
119
|
+
useSearch,
|
|
120
|
+
useFilters,
|
|
121
|
+
clearAllSearches,
|
|
122
|
+
getSearchCache
|
|
123
|
+
};
|
|
124
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// composables/useTimeAgo.ts
|
|
2
|
+
import { ref, onMounted, onBeforeUnmount } from 'vue'
|
|
3
|
+
import dayjs from 'dayjs'
|
|
4
|
+
|
|
5
|
+
export const useTimeAgo = (utcDatetime) => {
|
|
6
|
+
const now = ref(new Date())
|
|
7
|
+
let interval
|
|
8
|
+
|
|
9
|
+
onMounted(() => {
|
|
10
|
+
interval = setInterval(() => {
|
|
11
|
+
now.value = new Date()
|
|
12
|
+
}, 60000)
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
onBeforeUnmount(() => {
|
|
16
|
+
clearInterval(interval)
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
const timeAgo = () => {
|
|
20
|
+
// Convertir desde UTC a la hora local del navegador
|
|
21
|
+
return dayjs.utc(utcDatetime).local().from(now.value)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return { timeAgo }
|
|
25
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// app/composables/useToast.js
|
|
2
|
+
import { useToastStore } from '@/stores/toast'
|
|
3
|
+
|
|
4
|
+
export function useToast() {
|
|
5
|
+
const toast = useToastStore()
|
|
6
|
+
|
|
7
|
+
return {
|
|
8
|
+
// Métodos originales
|
|
9
|
+
success: toast.success,
|
|
10
|
+
info: toast.info,
|
|
11
|
+
error: toast.error,
|
|
12
|
+
show: toast.show,
|
|
13
|
+
remove: toast.remove,
|
|
14
|
+
update: toast.update,
|
|
15
|
+
updateProgress: toast.updateProgress,
|
|
16
|
+
completeProcess: toast.completeProcess,
|
|
17
|
+
|
|
18
|
+
// Métodos rápidos para diferentes tipos de toast
|
|
19
|
+
alert: {
|
|
20
|
+
success: (message, config = {}) => toast.success({
|
|
21
|
+
type: 'alert',
|
|
22
|
+
message,
|
|
23
|
+
duration: 3000,
|
|
24
|
+
...config
|
|
25
|
+
}),
|
|
26
|
+
|
|
27
|
+
error: (message, config = {}) => toast.error({
|
|
28
|
+
type: 'alert',
|
|
29
|
+
message,
|
|
30
|
+
duration: 5000,
|
|
31
|
+
...config
|
|
32
|
+
}),
|
|
33
|
+
|
|
34
|
+
warning: (message, config = {}) => toast.show({
|
|
35
|
+
type: 'alert',
|
|
36
|
+
severity: 'warning',
|
|
37
|
+
icon: 'ti ti-alert-triangle',
|
|
38
|
+
title: 'Warning',
|
|
39
|
+
message,
|
|
40
|
+
duration: 4000,
|
|
41
|
+
...config
|
|
42
|
+
}),
|
|
43
|
+
|
|
44
|
+
info: (message, config = {}) => toast.info({
|
|
45
|
+
type: 'alert',
|
|
46
|
+
message,
|
|
47
|
+
duration: 3000,
|
|
48
|
+
...config
|
|
49
|
+
})
|
|
50
|
+
},
|
|
51
|
+
|
|
52
|
+
notification: (title, message, config = {}) => toast.show({
|
|
53
|
+
type: 'notification',
|
|
54
|
+
title,
|
|
55
|
+
message,
|
|
56
|
+
duration: 0, // Las notificaciones no se auto-dismiss por defecto
|
|
57
|
+
...config
|
|
58
|
+
}),
|
|
59
|
+
|
|
60
|
+
process: (title, config = {}) => toast.show({
|
|
61
|
+
type: 'process',
|
|
62
|
+
title,
|
|
63
|
+
progress: 0,
|
|
64
|
+
progressLabel: 'Iniciando...',
|
|
65
|
+
duration: 0, // Los procesos no se auto-dismiss
|
|
66
|
+
...config
|
|
67
|
+
})
|
|
68
|
+
}
|
|
69
|
+
}
|
package/nuxt.config.ts
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@innertia-solutions/ui",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Innertia Solutions — Nuxt UI layer: components and composables",
|
|
5
|
+
"keywords": ["nuxt", "vue", "components", "design-system"],
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"publishConfig": {
|
|
8
|
+
"access": "public"
|
|
9
|
+
},
|
|
10
|
+
"main": "./nuxt.config.ts",
|
|
11
|
+
"exports": {
|
|
12
|
+
".": "./nuxt.config.ts"
|
|
13
|
+
},
|
|
14
|
+
"peerDependencies": {
|
|
15
|
+
"nuxt": ">=3.0.0",
|
|
16
|
+
"vue": ">=3.0.0"
|
|
17
|
+
},
|
|
18
|
+
"devDependencies": {
|
|
19
|
+
"nuxt": "^3.16.0",
|
|
20
|
+
"vue": "^3.5.0"
|
|
21
|
+
}
|
|
22
|
+
}
|
package/stores/toast.js
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
// stores/toast.js
|
|
2
|
+
import { defineStore } from 'pinia'
|
|
3
|
+
import { v4 as uuid } from 'uuid'
|
|
4
|
+
|
|
5
|
+
const toastConfig = {
|
|
6
|
+
severity: 'info',
|
|
7
|
+
title: null,
|
|
8
|
+
message: null,
|
|
9
|
+
duration: 5000,
|
|
10
|
+
progress: true,
|
|
11
|
+
position: 'top-right',
|
|
12
|
+
icon: null,
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const useToastStore = defineStore('toast', {
|
|
16
|
+
state: () => ({
|
|
17
|
+
toasts: {
|
|
18
|
+
'top-left': [],
|
|
19
|
+
'top-center': [],
|
|
20
|
+
'top-right': [],
|
|
21
|
+
'bottom-left': [],
|
|
22
|
+
'bottom-center': [],
|
|
23
|
+
'bottom-right': [],
|
|
24
|
+
},
|
|
25
|
+
timeouts: new Map(), // Para trackear los timeouts activos
|
|
26
|
+
}),
|
|
27
|
+
actions: {
|
|
28
|
+
success(config) {
|
|
29
|
+
const normalizedConfig = typeof config === 'string' ? { message: config } : config
|
|
30
|
+
const toast = {
|
|
31
|
+
...toastConfig,
|
|
32
|
+
id: uuid(),
|
|
33
|
+
severity: 'success',
|
|
34
|
+
icon: 'ti ti-circle-check text-green-500',
|
|
35
|
+
position: 'top-center',
|
|
36
|
+
...normalizedConfig,
|
|
37
|
+
}
|
|
38
|
+
this.toasts[toast.position].push(toast)
|
|
39
|
+
this._scheduleRemoval(toast)
|
|
40
|
+
},
|
|
41
|
+
info(config) {
|
|
42
|
+
const normalizedConfig = typeof config === 'string' ? { message: config } : config
|
|
43
|
+
const toast = {
|
|
44
|
+
...toastConfig,
|
|
45
|
+
id: uuid(),
|
|
46
|
+
severity: 'info',
|
|
47
|
+
icon: 'ti ti-info-circle',
|
|
48
|
+
title: 'Info',
|
|
49
|
+
position: 'top-right',
|
|
50
|
+
...normalizedConfig,
|
|
51
|
+
}
|
|
52
|
+
this.toasts[toast.position].push(toast)
|
|
53
|
+
this._scheduleRemoval(toast)
|
|
54
|
+
},
|
|
55
|
+
error(config) {
|
|
56
|
+
const normalizedConfig = typeof config === 'string' ? { message: config } : config
|
|
57
|
+
const toast = {
|
|
58
|
+
...toastConfig,
|
|
59
|
+
id: uuid(),
|
|
60
|
+
severity: 'danger',
|
|
61
|
+
icon: 'ti ti-exclamation-circle',
|
|
62
|
+
title: 'Atención',
|
|
63
|
+
position: 'top-right',
|
|
64
|
+
...normalizedConfig,
|
|
65
|
+
}
|
|
66
|
+
this.toasts[toast.position].push(toast)
|
|
67
|
+
this._scheduleRemoval(toast)
|
|
68
|
+
},
|
|
69
|
+
show(config) {
|
|
70
|
+
const toast = {
|
|
71
|
+
id: uuid(),
|
|
72
|
+
severity: 'info',
|
|
73
|
+
position: 'top-right',
|
|
74
|
+
...toastConfig,
|
|
75
|
+
...config,
|
|
76
|
+
}
|
|
77
|
+
this.toasts[toast.position].push(toast)
|
|
78
|
+
this._scheduleRemoval(toast)
|
|
79
|
+
},
|
|
80
|
+
update(id, data) {
|
|
81
|
+
for (const key in this.toasts) {
|
|
82
|
+
const idx = this.toasts[key].findIndex(t => t.id === id)
|
|
83
|
+
if (idx !== -1) {
|
|
84
|
+
this.toasts[key][idx] = { ...this.toasts[key][idx], ...data }
|
|
85
|
+
break
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
},
|
|
89
|
+
remove(id) {
|
|
90
|
+
// Limpiar timeout si existe
|
|
91
|
+
if (this.timeouts.has(id)) {
|
|
92
|
+
clearTimeout(this.timeouts.get(id))
|
|
93
|
+
this.timeouts.delete(id)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
for (const key in this.toasts) {
|
|
97
|
+
this.toasts[key] = this.toasts[key].filter(t => t.id !== id)
|
|
98
|
+
}
|
|
99
|
+
},
|
|
100
|
+
_scheduleRemoval(toast) {
|
|
101
|
+
// Solo programar auto-remove si duration > 0
|
|
102
|
+
if (toast.duration && toast.duration > 0) {
|
|
103
|
+
const timeoutId = setTimeout(() => {
|
|
104
|
+
this.remove(toast.id)
|
|
105
|
+
}, toast.duration)
|
|
106
|
+
|
|
107
|
+
this.timeouts.set(toast.id, timeoutId)
|
|
108
|
+
}
|
|
109
|
+
},
|
|
110
|
+
|
|
111
|
+
// Método específico para actualizar progreso de un toast tipo process
|
|
112
|
+
updateProgress(id, progress, progressLabel = null) {
|
|
113
|
+
const updateData = { progress }
|
|
114
|
+
if (progressLabel) {
|
|
115
|
+
updateData.progressLabel = progressLabel
|
|
116
|
+
}
|
|
117
|
+
this.update(id, updateData)
|
|
118
|
+
},
|
|
119
|
+
|
|
120
|
+
// Método para completar un proceso
|
|
121
|
+
completeProcess(id, message = 'Completado') {
|
|
122
|
+
this.update(id, {
|
|
123
|
+
progress: 100,
|
|
124
|
+
progressLabel: message,
|
|
125
|
+
duration: 3000, // Auto-remove después de completar
|
|
126
|
+
})
|
|
127
|
+
// Programar removal para el proceso completado
|
|
128
|
+
this._scheduleRemoval({ id, duration: 3000 })
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
})
|