@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.
Files changed (112) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +215 -0
  3. package/dist/module.d.mts +10 -0
  4. package/dist/module.json +9 -0
  5. package/dist/module.mjs +28 -0
  6. package/dist/runtime/app/assets/css/main.css +1 -0
  7. package/dist/runtime/app/components/Confirm/Confirm.d.vue.ts +21 -0
  8. package/dist/runtime/app/components/Confirm/Confirm.vue +65 -0
  9. package/dist/runtime/app/components/Confirm/Confirm.vue.d.ts +21 -0
  10. package/dist/runtime/app/components/Field/FieldArray.d.vue.ts +5 -0
  11. package/dist/runtime/app/components/Field/FieldArray.vue +128 -0
  12. package/dist/runtime/app/components/Field/FieldArray.vue.d.ts +5 -0
  13. package/dist/runtime/app/components/Field/FieldCheckbox.d.vue.ts +5 -0
  14. package/dist/runtime/app/components/Field/FieldCheckbox.vue +30 -0
  15. package/dist/runtime/app/components/Field/FieldCheckbox.vue.d.ts +5 -0
  16. package/dist/runtime/app/components/Field/FieldColor.d.vue.ts +5 -0
  17. package/dist/runtime/app/components/Field/FieldColor.vue +57 -0
  18. package/dist/runtime/app/components/Field/FieldColor.vue.d.ts +5 -0
  19. package/dist/runtime/app/components/Field/FieldEmail.d.vue.ts +5 -0
  20. package/dist/runtime/app/components/Field/FieldEmail.vue +37 -0
  21. package/dist/runtime/app/components/Field/FieldEmail.vue.d.ts +5 -0
  22. package/dist/runtime/app/components/Field/FieldFile.d.vue.ts +5 -0
  23. package/dist/runtime/app/components/Field/FieldFile.vue +93 -0
  24. package/dist/runtime/app/components/Field/FieldFile.vue.d.ts +5 -0
  25. package/dist/runtime/app/components/Field/FieldNumber.d.vue.ts +5 -0
  26. package/dist/runtime/app/components/Field/FieldNumber.vue +38 -0
  27. package/dist/runtime/app/components/Field/FieldNumber.vue.d.ts +5 -0
  28. package/dist/runtime/app/components/Field/FieldPassword.d.vue.ts +5 -0
  29. package/dist/runtime/app/components/Field/FieldPassword.vue +35 -0
  30. package/dist/runtime/app/components/Field/FieldPassword.vue.d.ts +5 -0
  31. package/dist/runtime/app/components/Field/FieldRadio.d.vue.ts +5 -0
  32. package/dist/runtime/app/components/Field/FieldRadio.vue +33 -0
  33. package/dist/runtime/app/components/Field/FieldRadio.vue.d.ts +5 -0
  34. package/dist/runtime/app/components/Field/FieldSelect.d.vue.ts +5 -0
  35. package/dist/runtime/app/components/Field/FieldSelect.vue +38 -0
  36. package/dist/runtime/app/components/Field/FieldSelect.vue.d.ts +5 -0
  37. package/dist/runtime/app/components/Field/FieldSlug.d.vue.ts +5 -0
  38. package/dist/runtime/app/components/Field/FieldSlug.vue +92 -0
  39. package/dist/runtime/app/components/Field/FieldSlug.vue.d.ts +5 -0
  40. package/dist/runtime/app/components/Field/FieldText.d.vue.ts +5 -0
  41. package/dist/runtime/app/components/Field/FieldText.vue +38 -0
  42. package/dist/runtime/app/components/Field/FieldText.vue.d.ts +5 -0
  43. package/dist/runtime/app/components/Field/FieldTextarea.d.vue.ts +5 -0
  44. package/dist/runtime/app/components/Field/FieldTextarea.vue +36 -0
  45. package/dist/runtime/app/components/Field/FieldTextarea.vue.d.ts +5 -0
  46. package/dist/runtime/app/components/Form/Form.d.vue.ts +48 -0
  47. package/dist/runtime/app/components/Form/Form.vue +137 -0
  48. package/dist/runtime/app/components/Form/Form.vue.d.ts +48 -0
  49. package/dist/runtime/app/components/Form/FormField.d.vue.ts +7 -0
  50. package/dist/runtime/app/components/Form/FormField.vue +61 -0
  51. package/dist/runtime/app/components/Form/FormField.vue.d.ts +7 -0
  52. package/dist/runtime/app/components/Form/FormModal.d.vue.ts +54 -0
  53. package/dist/runtime/app/components/Form/FormModal.vue +82 -0
  54. package/dist/runtime/app/components/Form/FormModal.vue.d.ts +54 -0
  55. package/dist/runtime/app/components/Form/FormSection.d.vue.ts +12 -0
  56. package/dist/runtime/app/components/Form/FormSection.vue +43 -0
  57. package/dist/runtime/app/components/Form/FormSection.vue.d.ts +12 -0
  58. package/dist/runtime/app/components/Page/Page.d.vue.ts +24 -0
  59. package/dist/runtime/app/components/Page/Page.vue +63 -0
  60. package/dist/runtime/app/components/Page/Page.vue.d.ts +24 -0
  61. package/dist/runtime/app/components/State/EmptyState.d.vue.ts +20 -0
  62. package/dist/runtime/app/components/State/EmptyState.vue +36 -0
  63. package/dist/runtime/app/components/State/EmptyState.vue.d.ts +20 -0
  64. package/dist/runtime/app/components/State/FailureState.d.vue.ts +10 -0
  65. package/dist/runtime/app/components/State/FailureState.vue +34 -0
  66. package/dist/runtime/app/components/State/FailureState.vue.d.ts +10 -0
  67. package/dist/runtime/app/components/Table/Table.d.vue.ts +70 -0
  68. package/dist/runtime/app/components/Table/Table.vue +432 -0
  69. package/dist/runtime/app/components/Table/Table.vue.d.ts +70 -0
  70. package/dist/runtime/app/components/Table/TableCell.d.vue.ts +8 -0
  71. package/dist/runtime/app/components/Table/TableCell.vue +84 -0
  72. package/dist/runtime/app/components/Table/TableCell.vue.d.ts +8 -0
  73. package/dist/runtime/app/composables/useVunoCrud.d.ts +23 -0
  74. package/dist/runtime/app/composables/useVunoCrud.js +117 -0
  75. package/dist/runtime/app/composables/useVunoField.d.ts +10 -0
  76. package/dist/runtime/app/composables/useVunoField.js +34 -0
  77. package/dist/runtime/app/composables/useVunoForm.d.ts +2 -0
  78. package/dist/runtime/app/composables/useVunoForm.js +9 -0
  79. package/dist/runtime/app/composables/useVunoTableQuery.d.ts +51 -0
  80. package/dist/runtime/app/composables/useVunoTableQuery.js +110 -0
  81. package/dist/runtime/app/composables/useVunoTheme.d.ts +12 -0
  82. package/dist/runtime/app/composables/useVunoTheme.js +35 -0
  83. package/dist/runtime/app/theme/index.d.ts +14 -0
  84. package/dist/runtime/app/theme/index.js +210 -0
  85. package/dist/runtime/app/types/app-config.d.ts +8 -0
  86. package/dist/runtime/app/types/app-config.js +1 -0
  87. package/dist/runtime/app/types/confirm.d.ts +11 -0
  88. package/dist/runtime/app/types/confirm.js +0 -0
  89. package/dist/runtime/app/types/crud.d.ts +64 -0
  90. package/dist/runtime/app/types/crud.js +0 -0
  91. package/dist/runtime/app/types/field.d.ts +121 -0
  92. package/dist/runtime/app/types/field.js +0 -0
  93. package/dist/runtime/app/types/form.d.ts +39 -0
  94. package/dist/runtime/app/types/form.js +1 -0
  95. package/dist/runtime/app/types/index.d.ts +9 -0
  96. package/dist/runtime/app/types/index.js +1 -0
  97. package/dist/runtime/app/types/page.d.ts +6 -0
  98. package/dist/runtime/app/types/page.js +0 -0
  99. package/dist/runtime/app/types/responsive.d.ts +8 -0
  100. package/dist/runtime/app/types/responsive.js +0 -0
  101. package/dist/runtime/app/types/table.d.ts +130 -0
  102. package/dist/runtime/app/types/table.js +0 -0
  103. package/dist/runtime/app/types/theme.d.ts +77 -0
  104. package/dist/runtime/app/types/theme.js +0 -0
  105. package/dist/runtime/app/utils/format.d.ts +9 -0
  106. package/dist/runtime/app/utils/format.js +30 -0
  107. package/dist/runtime/app/utils/grid.d.ts +9 -0
  108. package/dist/runtime/app/utils/grid.js +91 -0
  109. package/dist/runtime/app/utils/path.d.ts +16 -0
  110. package/dist/runtime/app/utils/path.js +74 -0
  111. package/dist/types.d.mts +5 -0
  112. 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 };
@@ -0,0 +1,9 @@
1
+ {
2
+ "name": "vuno",
3
+ "configKey": "vuno",
4
+ "version": "0.1.0",
5
+ "builder": {
6
+ "@nuxt/module-builder": "1.0.3",
7
+ "unbuild": "unknown"
8
+ }
9
+ }
@@ -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;