@groupteknology/vuno 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/LICENSE +21 -0
- package/README.md +215 -0
- package/dist/module.d.mts +10 -0
- package/dist/module.json +9 -0
- package/dist/module.mjs +28 -0
- package/dist/runtime/app/assets/css/main.css +1 -0
- package/dist/runtime/app/components/Confirm/Confirm.d.vue.ts +21 -0
- package/dist/runtime/app/components/Confirm/Confirm.vue +65 -0
- package/dist/runtime/app/components/Confirm/Confirm.vue.d.ts +21 -0
- package/dist/runtime/app/components/Field/FieldArray.d.vue.ts +5 -0
- package/dist/runtime/app/components/Field/FieldArray.vue +128 -0
- package/dist/runtime/app/components/Field/FieldArray.vue.d.ts +5 -0
- package/dist/runtime/app/components/Field/FieldCheckbox.d.vue.ts +5 -0
- package/dist/runtime/app/components/Field/FieldCheckbox.vue +30 -0
- package/dist/runtime/app/components/Field/FieldCheckbox.vue.d.ts +5 -0
- package/dist/runtime/app/components/Field/FieldColor.d.vue.ts +5 -0
- package/dist/runtime/app/components/Field/FieldColor.vue +57 -0
- package/dist/runtime/app/components/Field/FieldColor.vue.d.ts +5 -0
- package/dist/runtime/app/components/Field/FieldEmail.d.vue.ts +5 -0
- package/dist/runtime/app/components/Field/FieldEmail.vue +37 -0
- package/dist/runtime/app/components/Field/FieldEmail.vue.d.ts +5 -0
- package/dist/runtime/app/components/Field/FieldFile.d.vue.ts +5 -0
- package/dist/runtime/app/components/Field/FieldFile.vue +93 -0
- package/dist/runtime/app/components/Field/FieldFile.vue.d.ts +5 -0
- package/dist/runtime/app/components/Field/FieldNumber.d.vue.ts +5 -0
- package/dist/runtime/app/components/Field/FieldNumber.vue +38 -0
- package/dist/runtime/app/components/Field/FieldNumber.vue.d.ts +5 -0
- package/dist/runtime/app/components/Field/FieldPassword.d.vue.ts +5 -0
- package/dist/runtime/app/components/Field/FieldPassword.vue +35 -0
- package/dist/runtime/app/components/Field/FieldPassword.vue.d.ts +5 -0
- package/dist/runtime/app/components/Field/FieldRadio.d.vue.ts +5 -0
- package/dist/runtime/app/components/Field/FieldRadio.vue +33 -0
- package/dist/runtime/app/components/Field/FieldRadio.vue.d.ts +5 -0
- package/dist/runtime/app/components/Field/FieldSelect.d.vue.ts +5 -0
- package/dist/runtime/app/components/Field/FieldSelect.vue +38 -0
- package/dist/runtime/app/components/Field/FieldSelect.vue.d.ts +5 -0
- package/dist/runtime/app/components/Field/FieldSlug.d.vue.ts +5 -0
- package/dist/runtime/app/components/Field/FieldSlug.vue +92 -0
- package/dist/runtime/app/components/Field/FieldSlug.vue.d.ts +5 -0
- package/dist/runtime/app/components/Field/FieldText.d.vue.ts +5 -0
- package/dist/runtime/app/components/Field/FieldText.vue +38 -0
- package/dist/runtime/app/components/Field/FieldText.vue.d.ts +5 -0
- package/dist/runtime/app/components/Field/FieldTextarea.d.vue.ts +5 -0
- package/dist/runtime/app/components/Field/FieldTextarea.vue +36 -0
- package/dist/runtime/app/components/Field/FieldTextarea.vue.d.ts +5 -0
- package/dist/runtime/app/components/Form/Form.d.vue.ts +48 -0
- package/dist/runtime/app/components/Form/Form.vue +137 -0
- package/dist/runtime/app/components/Form/Form.vue.d.ts +48 -0
- package/dist/runtime/app/components/Form/FormField.d.vue.ts +7 -0
- package/dist/runtime/app/components/Form/FormField.vue +61 -0
- package/dist/runtime/app/components/Form/FormField.vue.d.ts +7 -0
- package/dist/runtime/app/components/Form/FormModal.d.vue.ts +54 -0
- package/dist/runtime/app/components/Form/FormModal.vue +82 -0
- package/dist/runtime/app/components/Form/FormModal.vue.d.ts +54 -0
- package/dist/runtime/app/components/Form/FormSection.d.vue.ts +12 -0
- package/dist/runtime/app/components/Form/FormSection.vue +43 -0
- package/dist/runtime/app/components/Form/FormSection.vue.d.ts +12 -0
- package/dist/runtime/app/components/Page/Page.d.vue.ts +24 -0
- package/dist/runtime/app/components/Page/Page.vue +63 -0
- package/dist/runtime/app/components/Page/Page.vue.d.ts +24 -0
- package/dist/runtime/app/components/State/EmptyState.d.vue.ts +20 -0
- package/dist/runtime/app/components/State/EmptyState.vue +36 -0
- package/dist/runtime/app/components/State/EmptyState.vue.d.ts +20 -0
- package/dist/runtime/app/components/State/FailureState.d.vue.ts +10 -0
- package/dist/runtime/app/components/State/FailureState.vue +34 -0
- package/dist/runtime/app/components/State/FailureState.vue.d.ts +10 -0
- package/dist/runtime/app/components/Table/Table.d.vue.ts +70 -0
- package/dist/runtime/app/components/Table/Table.vue +432 -0
- package/dist/runtime/app/components/Table/Table.vue.d.ts +70 -0
- package/dist/runtime/app/components/Table/TableCell.d.vue.ts +8 -0
- package/dist/runtime/app/components/Table/TableCell.vue +84 -0
- package/dist/runtime/app/components/Table/TableCell.vue.d.ts +8 -0
- package/dist/runtime/app/composables/useVunoCrud.d.ts +23 -0
- package/dist/runtime/app/composables/useVunoCrud.js +117 -0
- package/dist/runtime/app/composables/useVunoField.d.ts +10 -0
- package/dist/runtime/app/composables/useVunoField.js +34 -0
- package/dist/runtime/app/composables/useVunoForm.d.ts +2 -0
- package/dist/runtime/app/composables/useVunoForm.js +9 -0
- package/dist/runtime/app/composables/useVunoTableQuery.d.ts +51 -0
- package/dist/runtime/app/composables/useVunoTableQuery.js +110 -0
- package/dist/runtime/app/composables/useVunoTheme.d.ts +12 -0
- package/dist/runtime/app/composables/useVunoTheme.js +35 -0
- package/dist/runtime/app/theme/index.d.ts +14 -0
- package/dist/runtime/app/theme/index.js +210 -0
- package/dist/runtime/app/types/app-config.d.ts +8 -0
- package/dist/runtime/app/types/app-config.js +1 -0
- package/dist/runtime/app/types/confirm.d.ts +11 -0
- package/dist/runtime/app/types/confirm.js +0 -0
- package/dist/runtime/app/types/crud.d.ts +64 -0
- package/dist/runtime/app/types/crud.js +0 -0
- package/dist/runtime/app/types/field.d.ts +121 -0
- package/dist/runtime/app/types/field.js +0 -0
- package/dist/runtime/app/types/form.d.ts +39 -0
- package/dist/runtime/app/types/form.js +1 -0
- package/dist/runtime/app/types/index.d.ts +9 -0
- package/dist/runtime/app/types/index.js +1 -0
- package/dist/runtime/app/types/page.d.ts +6 -0
- package/dist/runtime/app/types/page.js +0 -0
- package/dist/runtime/app/types/responsive.d.ts +8 -0
- package/dist/runtime/app/types/responsive.js +0 -0
- package/dist/runtime/app/types/table.d.ts +130 -0
- package/dist/runtime/app/types/table.js +0 -0
- package/dist/runtime/app/types/theme.d.ts +77 -0
- package/dist/runtime/app/types/theme.js +0 -0
- package/dist/runtime/app/utils/format.d.ts +9 -0
- package/dist/runtime/app/utils/format.js +30 -0
- package/dist/runtime/app/utils/grid.d.ts +9 -0
- package/dist/runtime/app/utils/grid.js +91 -0
- package/dist/runtime/app/utils/path.d.ts +16 -0
- package/dist/runtime/app/utils/path.js +74 -0
- package/dist/types.d.mts +5 -0
- package/package.json +83 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Diego Otayza
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
# @groupteknology/vuno
|
|
2
|
+
|
|
3
|
+
Formularios, tablas y diálogos declarativos para Nuxt 4, construidos sobre [Nuxt UI](https://ui.nuxt.com).
|
|
4
|
+
|
|
5
|
+
El módulo **no aporta estética**. Todo lo que pinta sale del tema del proyecto que lo instala: usa las utilidades semánticas de Nuxt UI (`bg-default`, `text-muted`, `border-default`), su escala de radios y sus colores. Si cambias `primary` o `--ui-radius`, los componentes de vuno cambian contigo.
|
|
6
|
+
|
|
7
|
+
## Instalación
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pnpm add @groupteknology/vuno
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
// nuxt.config.ts
|
|
15
|
+
export default defineNuxtConfig({
|
|
16
|
+
modules: ['@nuxt/ui', '@groupteknology/vuno'],
|
|
17
|
+
})
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Formularios
|
|
21
|
+
|
|
22
|
+
Un formulario se declara como datos y se renderiza solo. El estado vive en un único objeto y cada campo lo lee y lo escribe por ruta, incluidas las anidadas.
|
|
23
|
+
|
|
24
|
+
```vue
|
|
25
|
+
<script setup lang="ts">
|
|
26
|
+
import type { VunoFormDefinition } from '@groupteknology/vuno'
|
|
27
|
+
import * as z from 'zod'
|
|
28
|
+
|
|
29
|
+
const schema = z.object({
|
|
30
|
+
sku: z.string().min(1, 'El SKU es requerido'),
|
|
31
|
+
name: z.string().min(1, 'El nombre es requerido'),
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
const state = ref({ sku: '', name: '', slug: '' })
|
|
35
|
+
|
|
36
|
+
const definition = {
|
|
37
|
+
layout: 'tabs', // 'sections' (por defecto) | 'tabs'
|
|
38
|
+
sections: [
|
|
39
|
+
{
|
|
40
|
+
id: 'general',
|
|
41
|
+
title: 'Datos generales',
|
|
42
|
+
icon: 'lucide:info',
|
|
43
|
+
fields: [
|
|
44
|
+
{ name: 'sku', type: 'text', label: 'SKU', columns: { xs: 12, lg: 6 }, required: true },
|
|
45
|
+
{ name: 'name', type: 'text', label: 'Nombre', columns: { xs: 12, lg: 6 }, required: true },
|
|
46
|
+
{ name: 'slug', type: 'slug', label: 'Slug', source: 'name' },
|
|
47
|
+
],
|
|
48
|
+
},
|
|
49
|
+
],
|
|
50
|
+
} satisfies VunoFormDefinition
|
|
51
|
+
</script>
|
|
52
|
+
|
|
53
|
+
<template>
|
|
54
|
+
<VForm v-model:state="state" :definition="definition" :schema="schema" @submit="guardar">
|
|
55
|
+
<template #footer="{ dirty, reset }">
|
|
56
|
+
<UButton :disabled="!dirty" label="Descartar" variant="ghost" @click="reset" />
|
|
57
|
+
<UButton label="Guardar" type="submit" />
|
|
58
|
+
</template>
|
|
59
|
+
</VForm>
|
|
60
|
+
</template>
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### Tipos de campo
|
|
64
|
+
|
|
65
|
+
`text` · `email` · `password` · `textarea` · `number` · `select` · `radio` · `checkbox` · `color` · `slug` · `file` · `array`
|
|
66
|
+
|
|
67
|
+
Cada uno declara sus propios props mediante una unión discriminada por `type`, así que el editor te avisa si a un `slug` le falta el `source` o si le pasas un `placeholder` a un `file`.
|
|
68
|
+
|
|
69
|
+
### Campos repetibles
|
|
70
|
+
|
|
71
|
+
`type: 'array'` repite un grupo de campos y genera rutas anidadas (`variants.0.sku`) que coinciden con las que produce la validación, de modo que cada campo muestra su propio error. Admite anidamiento.
|
|
72
|
+
|
|
73
|
+
```ts
|
|
74
|
+
{
|
|
75
|
+
name: 'variants',
|
|
76
|
+
type: 'array',
|
|
77
|
+
label: 'Variantes',
|
|
78
|
+
sortable: true,
|
|
79
|
+
fields: [
|
|
80
|
+
{ name: 'sku', type: 'text', label: 'SKU', columns: { xs: 12, md: 6 } },
|
|
81
|
+
{ name: 'stock', type: 'number', label: 'Stock', columns: { xs: 12, md: 6 } },
|
|
82
|
+
],
|
|
83
|
+
}
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Con `layout: 'tabs'`, cada pestaña muestra un contador de los campos con error que contiene, incluidos los anidados.
|
|
87
|
+
|
|
88
|
+
### En un modal
|
|
89
|
+
|
|
90
|
+
`VFormModal` añade el diálogo, los botones y el ancho adecuado. Los botones van en el pie del modal —fijo mientras el cuerpo hace scroll— asociados al formulario por `id`.
|
|
91
|
+
|
|
92
|
+
```vue
|
|
93
|
+
<VFormModal v-model:open="open" v-model:state="state" :definition="definition" :schema="schema" title="Nuevo producto" close-on-submit @submit="guardar" />
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## Una pantalla de mantenimiento entera
|
|
97
|
+
|
|
98
|
+
`useVunoCrud` resuelve el ciclo completo: listar, dar de alta o editar en un modal, borrar con confirmación, invalidar la caché y avisar. Está construido sobre [TanStack Query](https://tanstack.com/query).
|
|
99
|
+
|
|
100
|
+
```vue
|
|
101
|
+
<script setup lang="ts">
|
|
102
|
+
const query = useVunoTableQuery({ sort: 'sku', url: true })
|
|
103
|
+
|
|
104
|
+
const { items, total, failure, isPending, isSaving, isDeleting,
|
|
105
|
+
formOpen, editing, state, submit, openCreate, openEdit,
|
|
106
|
+
confirmOpen, deleting, askDelete, confirmDelete } = useVunoCrud<Product, ProductForm>({
|
|
107
|
+
key: 'products',
|
|
108
|
+
query,
|
|
109
|
+
emptyForm: () => ({ name: '', sku: '' }),
|
|
110
|
+
list: (q) => $fetch('/api/products', { query: q }),
|
|
111
|
+
create: (form) => $fetch('/api/products', { body: form, method: 'POST' }),
|
|
112
|
+
update: (id, form) => $fetch(`/api/products/${id}`, { body: form, method: 'PATCH' }),
|
|
113
|
+
remove: (row) => $fetch(`/api/products/${row.id}`, { method: 'DELETE' }),
|
|
114
|
+
})
|
|
115
|
+
</script>
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
La consulta forma parte de la clave de caché, así que cada página, filtro y orden se guarda por separado. Usa `placeholderData: keepPreviousData`, que mantiene en pantalla los datos anteriores mientras llega la nueva página: sin eso, cada cambio de filtro deja `data` en `undefined` un instante y la tabla parpadea enseñando su estado vacío. Los textos de los avisos salen de `vuno.messages` y se pueden afinar por pantalla con la opción `messages`.
|
|
119
|
+
|
|
120
|
+
`@tanstack/vue-query` es una dependencia opcional: solo hace falta si usas este composable.
|
|
121
|
+
|
|
122
|
+
## Páginas
|
|
123
|
+
|
|
124
|
+
`VPage` pone la estructura que repite cualquier pantalla de mantenimiento:
|
|
125
|
+
|
|
126
|
+
```vue
|
|
127
|
+
<VPage title="Productos" description="Catálogo completo." :breadcrumb="[{ label: 'Inicio', to: '/' }, { label: 'Productos' }]">
|
|
128
|
+
<template #actions>
|
|
129
|
+
<UButton icon="lucide:plus" label="Nuevo producto" @click="openCreate" />
|
|
130
|
+
</template>
|
|
131
|
+
|
|
132
|
+
<VTable ... />
|
|
133
|
+
</VPage>
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
## Tablas
|
|
137
|
+
|
|
138
|
+
`VTable` funciona en modo servidor: emite el estado de consulta y tú devuelves la página.
|
|
139
|
+
|
|
140
|
+
```vue
|
|
141
|
+
<script setup lang="ts">
|
|
142
|
+
const query = useVunoTableQuery({ sort: 'sku', url: true, history: 'push', filterKeys: ['status'] })
|
|
143
|
+
|
|
144
|
+
const { data, status } = await useFetch('/api/products', {
|
|
145
|
+
query: computed(() => ({ page: query.value.page, sort: query.value.sort?.column, ...query.value.filters })),
|
|
146
|
+
})
|
|
147
|
+
</script>
|
|
148
|
+
|
|
149
|
+
<template>
|
|
150
|
+
<VTable v-model:query="query" :definition="definition" :data="data?.data ?? []" :total="data?.total ?? 0" :loading="status === 'pending'" />
|
|
151
|
+
</template>
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
Con `url: true` los filtros, la página, la búsqueda y el orden viajan en la query string: el enlace es compartible y recargar conserva la vista.
|
|
155
|
+
|
|
156
|
+
### Una tabla vacía no es una que falló
|
|
157
|
+
|
|
158
|
+
Son dos estados distintos y se pintan distinto. Sin separarlos, una consulta que falla se normaliza a lista vacía (`data ?? []`) y la tabla dice «No hay resultados»: afirma que no hay nada cuando lo cierto es que no se pudo saber.
|
|
159
|
+
|
|
160
|
+
```ts
|
|
161
|
+
const failure = computed(() => isError.value
|
|
162
|
+
? { title: 'No pudimos cargar los productos', hint: 'Puede ser algo temporal.', retry: () => void refetch() }
|
|
163
|
+
: undefined)
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
El reintento va **dentro** del objeto para que no se pueda pintar un error sin ofrecer salida. Mientras hay fallo, la tabla no muestra filas ni paginación. El estado vacío se declara en la definición (`empty: { title, hint, icon }`) y tiene un slot `empty-actions` donde encaja el «crear el primero».
|
|
167
|
+
|
|
168
|
+
Los dos estados existen además como componentes sueltos, para las pantallas que no son una tabla:
|
|
169
|
+
|
|
170
|
+
```vue
|
|
171
|
+
<VEmptyState title="Todavía no hay pedidos" hint="Aparecerán aquí en cuanto entre el primero." icon="lucide:inbox">
|
|
172
|
+
<template #actions><UButton label="Crear pedido" /></template>
|
|
173
|
+
</VEmptyState>
|
|
174
|
+
|
|
175
|
+
<VFailureState :failure="failure" />
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
Tipos de columna: `text` · `number` · `currency` · `date` · `boolean` · `badge` · `image` · `custom` · `actions`. Trae búsqueda con antirrebote, paginación, selección múltiple, panel lateral de filtros y menú de acciones por fila.
|
|
179
|
+
|
|
180
|
+
## Personalización
|
|
181
|
+
|
|
182
|
+
Tres niveles, de más global a más concreto:
|
|
183
|
+
|
|
184
|
+
```ts
|
|
185
|
+
// app.config.ts — para toda la aplicación
|
|
186
|
+
export default defineAppConfig({
|
|
187
|
+
ui: { colors: { primary: 'indigo' } },
|
|
188
|
+
vuno: {
|
|
189
|
+
formSection: { slots: { root: 'border-dashed' } },
|
|
190
|
+
icons: { slugEdit: 'lucide:wand-sparkles' },
|
|
191
|
+
messages: { selectPlaceholder: 'Elige una opción' },
|
|
192
|
+
},
|
|
193
|
+
})
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
```vue
|
|
197
|
+
<!-- prop `ui` — para una instancia -->
|
|
198
|
+
<VForm :ui="{ sections: 'gap-8' }" ... />
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
Los textos e iconos están todos en `vuno.messages` y `vuno.icons`: el paquete no impone idioma ni colección de iconos.
|
|
202
|
+
|
|
203
|
+
## Estado
|
|
204
|
+
|
|
205
|
+
En desarrollo.
|
|
206
|
+
|
|
207
|
+
### Nota sobre pruebas automatizadas
|
|
208
|
+
|
|
209
|
+
Al comprobar los diálogos con un navegador controlado por automatización, el nodo del modal puede seguir en el DOM después de cerrarlo. No es un fallo: Chrome congela las animaciones CSS en pestañas ocultas y Reka UI espera el evento `animationend` para desmontar el diálogo. El componente ya está cerrado —lo indica su `data-state="closed"`—, solo falta la limpieza.
|
|
210
|
+
|
|
211
|
+
Comprueba la visibilidad real (`opacity`, `data-state`) en lugar de la existencia del nodo, o haz la prueba con la pestaña en primer plano.
|
|
212
|
+
|
|
213
|
+
## Licencia
|
|
214
|
+
|
|
215
|
+
[MIT](./LICENSE) © Diego Otayza
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { NuxtModule } from '@nuxt/schema';
|
|
2
|
+
export * from '../dist/runtime/app/types/index.js';
|
|
3
|
+
|
|
4
|
+
interface ModuleOptions {
|
|
5
|
+
prefix?: string;
|
|
6
|
+
}
|
|
7
|
+
declare const module$1: NuxtModule<ModuleOptions>;
|
|
8
|
+
|
|
9
|
+
export { module$1 as default };
|
|
10
|
+
export type { ModuleOptions };
|
package/dist/module.json
ADDED
package/dist/module.mjs
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { defineNuxtModule, createResolver, addComponentsDir, addImportsDir } from '@nuxt/kit';
|
|
2
|
+
|
|
3
|
+
const module$1 = defineNuxtModule({
|
|
4
|
+
defaults: {
|
|
5
|
+
prefix: "V"
|
|
6
|
+
},
|
|
7
|
+
meta: {
|
|
8
|
+
name: "vuno",
|
|
9
|
+
configKey: "vuno"
|
|
10
|
+
},
|
|
11
|
+
moduleDependencies: {
|
|
12
|
+
"@nuxt/ui": {}
|
|
13
|
+
},
|
|
14
|
+
setup(options, nuxt) {
|
|
15
|
+
const resolver = createResolver(import.meta.url);
|
|
16
|
+
nuxt.options.alias["#vuno"] = resolver.resolve("./runtime/app");
|
|
17
|
+
addComponentsDir({
|
|
18
|
+
global: true,
|
|
19
|
+
path: resolver.resolve("./runtime/app/components"),
|
|
20
|
+
pathPrefix: false,
|
|
21
|
+
prefix: options.prefix
|
|
22
|
+
});
|
|
23
|
+
addImportsDir(resolver.resolve("./runtime/app/composables"));
|
|
24
|
+
nuxt.options.css.push(resolver.resolve("./runtime/app/assets/css/main.css"));
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
export { module$1 as default };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
@import "tailwindcss";@import "@nuxt/ui";@source "../../**/*";
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { VunoConfirmOptions } from '#vuno/types/confirm';
|
|
2
|
+
import type { VunoFieldSlots } from '#vuno/types/field';
|
|
3
|
+
type __VLS_Props = VunoConfirmOptions & {
|
|
4
|
+
loading?: boolean;
|
|
5
|
+
ui?: VunoFieldSlots;
|
|
6
|
+
};
|
|
7
|
+
type __VLS_ModelProps = {
|
|
8
|
+
'open'?: boolean;
|
|
9
|
+
};
|
|
10
|
+
type __VLS_PublicProps = __VLS_Props & __VLS_ModelProps;
|
|
11
|
+
declare const __VLS_export: import("vue").DefineComponent<__VLS_PublicProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
12
|
+
"update:open": (value: boolean) => any;
|
|
13
|
+
confirm: () => any;
|
|
14
|
+
cancel: () => any;
|
|
15
|
+
}, string, import("vue").PublicProps, Readonly<__VLS_PublicProps> & Readonly<{
|
|
16
|
+
"onUpdate:open"?: ((value: boolean) => any) | undefined;
|
|
17
|
+
onConfirm?: (() => any) | undefined;
|
|
18
|
+
onCancel?: (() => any) | undefined;
|
|
19
|
+
}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
20
|
+
declare const _default: typeof __VLS_export;
|
|
21
|
+
export default _default;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { computed } from "vue";
|
|
3
|
+
import { useVunoIcons, useVunoMessages, useVunoTheme } from "#vuno/composables/useVunoTheme";
|
|
4
|
+
const props = defineProps({
|
|
5
|
+
cancelLabel: { type: String, required: false },
|
|
6
|
+
color: { type: String, required: false },
|
|
7
|
+
confirmLabel: { type: String, required: false },
|
|
8
|
+
description: { type: String, required: false },
|
|
9
|
+
icon: { type: String, required: false },
|
|
10
|
+
title: { type: String, required: false },
|
|
11
|
+
loading: { type: Boolean, required: false },
|
|
12
|
+
ui: { type: Object, required: false }
|
|
13
|
+
});
|
|
14
|
+
const emit = defineEmits(["cancel", "confirm"]);
|
|
15
|
+
const open = defineModel("open", { type: Boolean, ...{ default: false } });
|
|
16
|
+
const icons = useVunoIcons();
|
|
17
|
+
const messages = useVunoMessages();
|
|
18
|
+
const ui = useVunoTheme("confirm", () => props.ui);
|
|
19
|
+
const color = computed(() => props.color ?? "error");
|
|
20
|
+
const modalUi = computed(() => ({ content: ui.value.content, footer: ui.value.footer }));
|
|
21
|
+
function onCancel() {
|
|
22
|
+
open.value = false;
|
|
23
|
+
emit("cancel");
|
|
24
|
+
}
|
|
25
|
+
function onConfirm() {
|
|
26
|
+
emit("confirm");
|
|
27
|
+
if (!props.loading) open.value = false;
|
|
28
|
+
}
|
|
29
|
+
</script>
|
|
30
|
+
|
|
31
|
+
<template>
|
|
32
|
+
<UModal
|
|
33
|
+
v-model:open="open"
|
|
34
|
+
:dismissible="!props.loading"
|
|
35
|
+
scrollable
|
|
36
|
+
:title="props.title ?? messages.confirmTitle"
|
|
37
|
+
:ui="modalUi"
|
|
38
|
+
>
|
|
39
|
+
<template #body>
|
|
40
|
+
<div :class="ui.body">
|
|
41
|
+
<UIcon
|
|
42
|
+
:class="[ui.icon, `text-${color}`]"
|
|
43
|
+
:name="props.icon ?? icons.confirm"
|
|
44
|
+
/>
|
|
45
|
+
<p>{{ props.description ?? messages.confirmDescription }}</p>
|
|
46
|
+
</div>
|
|
47
|
+
</template>
|
|
48
|
+
|
|
49
|
+
<template #footer>
|
|
50
|
+
<UButton
|
|
51
|
+
color="neutral"
|
|
52
|
+
:disabled="props.loading"
|
|
53
|
+
:label="props.cancelLabel ?? messages.cancel"
|
|
54
|
+
variant="ghost"
|
|
55
|
+
@click="onCancel"
|
|
56
|
+
/>
|
|
57
|
+
<UButton
|
|
58
|
+
:color="color"
|
|
59
|
+
:label="props.confirmLabel ?? messages.confirm"
|
|
60
|
+
:loading="props.loading"
|
|
61
|
+
@click="onConfirm"
|
|
62
|
+
/>
|
|
63
|
+
</template>
|
|
64
|
+
</UModal>
|
|
65
|
+
</template>
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { VunoConfirmOptions } from '#vuno/types/confirm';
|
|
2
|
+
import type { VunoFieldSlots } from '#vuno/types/field';
|
|
3
|
+
type __VLS_Props = VunoConfirmOptions & {
|
|
4
|
+
loading?: boolean;
|
|
5
|
+
ui?: VunoFieldSlots;
|
|
6
|
+
};
|
|
7
|
+
type __VLS_ModelProps = {
|
|
8
|
+
'open'?: boolean;
|
|
9
|
+
};
|
|
10
|
+
type __VLS_PublicProps = __VLS_Props & __VLS_ModelProps;
|
|
11
|
+
declare const __VLS_export: import("vue").DefineComponent<__VLS_PublicProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
12
|
+
"update:open": (value: boolean) => any;
|
|
13
|
+
confirm: () => any;
|
|
14
|
+
cancel: () => any;
|
|
15
|
+
}, string, import("vue").PublicProps, Readonly<__VLS_PublicProps> & Readonly<{
|
|
16
|
+
"onUpdate:open"?: ((value: boolean) => any) | undefined;
|
|
17
|
+
onConfirm?: (() => any) | undefined;
|
|
18
|
+
onCancel?: (() => any) | undefined;
|
|
19
|
+
}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
20
|
+
declare const _default: typeof __VLS_export;
|
|
21
|
+
export default _default;
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { VunoFieldArray } from '#vuno/types/field';
|
|
2
|
+
type __VLS_Props = Omit<VunoFieldArray, 'class' | 'columns'>;
|
|
3
|
+
declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
4
|
+
declare const _default: typeof __VLS_export;
|
|
5
|
+
export default _default;
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { computed } from "vue";
|
|
3
|
+
import { useVunoField } from "#vuno/composables/useVunoField";
|
|
4
|
+
import { useVunoIcons, useVunoMessages, useVunoTheme } from "#vuno/composables/useVunoTheme";
|
|
5
|
+
import { getColumnSpanClass } from "#vuno/utils/grid";
|
|
6
|
+
import FormField from "../Form/FormField.vue";
|
|
7
|
+
const props = defineProps({
|
|
8
|
+
name: { type: String, required: true },
|
|
9
|
+
description: { type: String, required: false },
|
|
10
|
+
disabled: { type: Boolean, required: false },
|
|
11
|
+
help: { type: String, required: false },
|
|
12
|
+
hint: { type: String, required: false },
|
|
13
|
+
label: { type: String, required: false },
|
|
14
|
+
required: { type: Boolean, required: false },
|
|
15
|
+
size: { type: String, required: false },
|
|
16
|
+
ui: { type: Object, required: false },
|
|
17
|
+
addLabel: { type: String, required: false },
|
|
18
|
+
fields: { type: Array, required: true },
|
|
19
|
+
max: { type: Number, required: false },
|
|
20
|
+
min: { type: Number, required: false },
|
|
21
|
+
sortable: { type: Boolean, required: false }
|
|
22
|
+
});
|
|
23
|
+
const { value } = useVunoField(() => props.name);
|
|
24
|
+
const icons = useVunoIcons();
|
|
25
|
+
const messages = useVunoMessages();
|
|
26
|
+
const ui = useVunoTheme("fieldArray", () => props.ui);
|
|
27
|
+
const items = computed(() => value.value ?? []);
|
|
28
|
+
const canAdd = computed(() => props.max === void 0 || items.value.length < props.max);
|
|
29
|
+
const canRemove = computed(() => items.value.length > (props.min ?? 0));
|
|
30
|
+
function emptyItem() {
|
|
31
|
+
const item = {};
|
|
32
|
+
for (const field of props.fields) {
|
|
33
|
+
item[field.name] = field.type === "checkbox" ? false : field.type === "number" ? void 0 : field.type === "array" || field.type === "file" ? [] : "";
|
|
34
|
+
}
|
|
35
|
+
return item;
|
|
36
|
+
}
|
|
37
|
+
function fieldAt(field, index) {
|
|
38
|
+
return { ...field, name: `${props.name}.${index}.${field.name}` };
|
|
39
|
+
}
|
|
40
|
+
function add() {
|
|
41
|
+
if (!canAdd.value) return;
|
|
42
|
+
value.value = [...items.value, emptyItem()];
|
|
43
|
+
}
|
|
44
|
+
function remove(index) {
|
|
45
|
+
if (!canRemove.value) return;
|
|
46
|
+
value.value = items.value.filter((_, position) => position !== index);
|
|
47
|
+
}
|
|
48
|
+
function move(index, offset) {
|
|
49
|
+
const next = [...items.value];
|
|
50
|
+
const target = index + offset;
|
|
51
|
+
if (target < 0 || target >= next.length) return;
|
|
52
|
+
const [moved] = next.splice(index, 1);
|
|
53
|
+
if (!moved) return;
|
|
54
|
+
next.splice(target, 0, moved);
|
|
55
|
+
value.value = next;
|
|
56
|
+
}
|
|
57
|
+
</script>
|
|
58
|
+
|
|
59
|
+
<template>
|
|
60
|
+
<div :class="ui.root">
|
|
61
|
+
<div
|
|
62
|
+
v-for="(item, index) in items"
|
|
63
|
+
:key="index"
|
|
64
|
+
:class="ui.item"
|
|
65
|
+
>
|
|
66
|
+
<div :class="ui.itemHeader">
|
|
67
|
+
<span :class="ui.itemLabel">{{ index + 1 }}</span>
|
|
68
|
+
|
|
69
|
+
<div :class="ui.itemActions">
|
|
70
|
+
<UButton
|
|
71
|
+
v-if="props.sortable && index > 0"
|
|
72
|
+
:aria-label="messages.fileMoveUp"
|
|
73
|
+
color="neutral"
|
|
74
|
+
:icon="icons.fileMoveUp"
|
|
75
|
+
size="xs"
|
|
76
|
+
variant="ghost"
|
|
77
|
+
@click="move(index, -1)"
|
|
78
|
+
/>
|
|
79
|
+
<UButton
|
|
80
|
+
v-if="props.sortable && index < items.length - 1"
|
|
81
|
+
:aria-label="messages.fileMoveDown"
|
|
82
|
+
color="neutral"
|
|
83
|
+
:icon="icons.fileMoveDown"
|
|
84
|
+
size="xs"
|
|
85
|
+
variant="ghost"
|
|
86
|
+
@click="move(index, 1)"
|
|
87
|
+
/>
|
|
88
|
+
<UButton
|
|
89
|
+
:aria-label="messages.removeItem"
|
|
90
|
+
color="error"
|
|
91
|
+
:disabled="!canRemove"
|
|
92
|
+
:icon="icons.removeItem"
|
|
93
|
+
size="xs"
|
|
94
|
+
variant="ghost"
|
|
95
|
+
@click="remove(index)"
|
|
96
|
+
/>
|
|
97
|
+
</div>
|
|
98
|
+
</div>
|
|
99
|
+
|
|
100
|
+
<div :class="ui.itemFields">
|
|
101
|
+
<div
|
|
102
|
+
v-for="field in props.fields"
|
|
103
|
+
:key="`${index}-${field.name}`"
|
|
104
|
+
:class="getColumnSpanClass(field.columns)"
|
|
105
|
+
>
|
|
106
|
+
<FormField :field="fieldAt(field, index)" />
|
|
107
|
+
</div>
|
|
108
|
+
</div>
|
|
109
|
+
</div>
|
|
110
|
+
|
|
111
|
+
<div
|
|
112
|
+
v-if="items.length === 0"
|
|
113
|
+
:class="ui.empty"
|
|
114
|
+
>
|
|
115
|
+
{{ messages.emptyArray }}
|
|
116
|
+
</div>
|
|
117
|
+
|
|
118
|
+
<UButton
|
|
119
|
+
class="self-start"
|
|
120
|
+
color="neutral"
|
|
121
|
+
:disabled="!canAdd"
|
|
122
|
+
:icon="icons.addItem"
|
|
123
|
+
:label="props.addLabel ?? messages.addItem"
|
|
124
|
+
variant="outline"
|
|
125
|
+
@click="add"
|
|
126
|
+
/>
|
|
127
|
+
</div>
|
|
128
|
+
</template>
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { VunoFieldArray } from '#vuno/types/field';
|
|
2
|
+
type __VLS_Props = Omit<VunoFieldArray, 'class' | 'columns'>;
|
|
3
|
+
declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
4
|
+
declare const _default: typeof __VLS_export;
|
|
5
|
+
export default _default;
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { VunoFieldCheckbox } from '#vuno/types/field';
|
|
2
|
+
type __VLS_Props = Omit<VunoFieldCheckbox, 'class' | 'columns'>;
|
|
3
|
+
declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
4
|
+
declare const _default: typeof __VLS_export;
|
|
5
|
+
export default _default;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { useVunoField } from "#vuno/composables/useVunoField";
|
|
3
|
+
import { useVunoTheme } from "#vuno/composables/useVunoTheme";
|
|
4
|
+
const props = defineProps({
|
|
5
|
+
name: { type: String, required: true },
|
|
6
|
+
description: { type: String, required: false },
|
|
7
|
+
disabled: { type: Boolean, required: false },
|
|
8
|
+
help: { type: String, required: false },
|
|
9
|
+
hint: { type: String, required: false },
|
|
10
|
+
label: { type: String, required: false },
|
|
11
|
+
required: { type: Boolean, required: false },
|
|
12
|
+
size: { type: String, required: false },
|
|
13
|
+
ui: { type: Object, required: false },
|
|
14
|
+
indeterminate: { type: Boolean, required: false }
|
|
15
|
+
});
|
|
16
|
+
const { value } = useVunoField(() => props.name);
|
|
17
|
+
const ui = useVunoTheme("fieldCheckbox", () => props.ui);
|
|
18
|
+
</script>
|
|
19
|
+
|
|
20
|
+
<template>
|
|
21
|
+
<UCheckbox
|
|
22
|
+
v-model="value"
|
|
23
|
+
:disabled="props.disabled"
|
|
24
|
+
:indeterminate="props.indeterminate"
|
|
25
|
+
:name="props.name"
|
|
26
|
+
:required="props.required"
|
|
27
|
+
:size="props.size"
|
|
28
|
+
:ui="ui"
|
|
29
|
+
/>
|
|
30
|
+
</template>
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { VunoFieldCheckbox } from '#vuno/types/field';
|
|
2
|
+
type __VLS_Props = Omit<VunoFieldCheckbox, 'class' | 'columns'>;
|
|
3
|
+
declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
4
|
+
declare const _default: typeof __VLS_export;
|
|
5
|
+
export default _default;
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { VunoFieldColor } from '#vuno/types/field';
|
|
2
|
+
type __VLS_Props = Omit<VunoFieldColor, 'class' | 'columns'>;
|
|
3
|
+
declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
4
|
+
declare const _default: typeof __VLS_export;
|
|
5
|
+
export default _default;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { useVunoField } from "#vuno/composables/useVunoField";
|
|
3
|
+
import { useVunoTheme } from "#vuno/composables/useVunoTheme";
|
|
4
|
+
const props = defineProps({
|
|
5
|
+
name: { type: String, required: true },
|
|
6
|
+
description: { type: String, required: false },
|
|
7
|
+
disabled: { type: Boolean, required: false },
|
|
8
|
+
help: { type: String, required: false },
|
|
9
|
+
hint: { type: String, required: false },
|
|
10
|
+
label: { type: String, required: false },
|
|
11
|
+
required: { type: Boolean, required: false },
|
|
12
|
+
size: { type: String, required: false },
|
|
13
|
+
ui: { type: Object, required: false },
|
|
14
|
+
placeholder: { type: String, required: false }
|
|
15
|
+
});
|
|
16
|
+
const { value } = useVunoField(() => props.name);
|
|
17
|
+
const ui = useVunoTheme("fieldColor", () => props.ui);
|
|
18
|
+
</script>
|
|
19
|
+
|
|
20
|
+
<template>
|
|
21
|
+
<div :class="ui.root">
|
|
22
|
+
<!-- El selector nativo y el texto comparten valor: se puede pegar un
|
|
23
|
+
código de la guía de marca sin pelearse con la rueda de color. -->
|
|
24
|
+
<UPopover>
|
|
25
|
+
<UButton
|
|
26
|
+
:aria-label="props.label"
|
|
27
|
+
color="neutral"
|
|
28
|
+
:disabled="props.disabled"
|
|
29
|
+
:size="props.size"
|
|
30
|
+
:ui="{ leadingIcon: 'rounded-full' }"
|
|
31
|
+
variant="outline"
|
|
32
|
+
>
|
|
33
|
+
<span
|
|
34
|
+
:class="ui.swatch"
|
|
35
|
+
:style="{ backgroundColor: value || 'transparent' }"
|
|
36
|
+
/>
|
|
37
|
+
</UButton>
|
|
38
|
+
|
|
39
|
+
<template #content>
|
|
40
|
+
<UColorPicker
|
|
41
|
+
v-model="value"
|
|
42
|
+
class="p-2"
|
|
43
|
+
/>
|
|
44
|
+
</template>
|
|
45
|
+
</UPopover>
|
|
46
|
+
|
|
47
|
+
<UInput
|
|
48
|
+
v-model="value"
|
|
49
|
+
class="font-mono"
|
|
50
|
+
:disabled="props.disabled"
|
|
51
|
+
:name="props.name"
|
|
52
|
+
:placeholder="props.placeholder ?? '#000000'"
|
|
53
|
+
:size="props.size"
|
|
54
|
+
:ui="{ root: 'w-full' }"
|
|
55
|
+
/>
|
|
56
|
+
</div>
|
|
57
|
+
</template>
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { VunoFieldColor } from '#vuno/types/field';
|
|
2
|
+
type __VLS_Props = Omit<VunoFieldColor, 'class' | 'columns'>;
|
|
3
|
+
declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
4
|
+
declare const _default: typeof __VLS_export;
|
|
5
|
+
export default _default;
|