@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
@@ -0,0 +1,70 @@
1
+ import type { VunoFieldSlots } from '#vuno/types/field';
2
+ import type { VunoTableDefinition, VunoTableFailure, VunoTableQuery, VunoTableRow } from '#vuno/types/table';
3
+ declare const __VLS_export: <TRow extends VunoTableRow>(__VLS_props: NonNullable<Awaited<typeof __VLS_setup>>["props"], __VLS_ctx?: __VLS_PrettifyLocal<Pick<NonNullable<Awaited<typeof __VLS_setup>>, "attrs" | "emit" | "slots">>, __VLS_exposed?: NonNullable<Awaited<typeof __VLS_setup>>["expose"], __VLS_setup?: Promise<{
4
+ props: import("vue").PublicProps & __VLS_PrettifyLocal<({
5
+ data: TRow[];
6
+ definition: VunoTableDefinition<TRow>;
7
+ /**
8
+ * Fallo de la consulta. Sin esto, un error se pintaría como «no hay
9
+ * resultados» y la tabla afirmaría que no hay nada cuando en realidad
10
+ * no se pudo saber.
11
+ */
12
+ failure?: VunoTableFailure;
13
+ loading?: boolean;
14
+ /** Total de filas en servidor, no las de la página actual. */
15
+ total?: number;
16
+ ui?: VunoFieldSlots;
17
+ } & {
18
+ /**
19
+ * Estado de consulta. El consumidor lo observa y devuelve `data` y `total`.
20
+ *
21
+ * Es obligatorio a propósito: la tabla no trae datos por sí misma, así que
22
+ * un estado interno avanzaría de página sin que nadie pidiera la siguiente
23
+ * —el pie diría «11-20» mientras se siguen viendo las filas 1-10—. Dónde
24
+ * vive ese estado lo decide `useVunoTableQuery`: un `ref` normal o la
25
+ * query string.
26
+ */
27
+ query: VunoTableQuery;
28
+ /** Claves de las filas seleccionadas, no las filas: con paginación en servidor las de otras páginas no están cargadas. */
29
+ selection?: (number | string)[];
30
+ }) & {
31
+ onRowClick?: ((row: TRow) => any) | undefined;
32
+ "onUpdate:query"?: ((value: VunoTableQuery) => any) | undefined;
33
+ "onUpdate:selection"?: ((value: (string | number)[]) => any) | undefined;
34
+ }> & (typeof globalThis extends {
35
+ __VLS_PROPS_FALLBACK: infer P;
36
+ } ? P : {});
37
+ expose: (exposed: import("vue").ShallowUnwrapRef<{
38
+ clearFilters: () => void;
39
+ clearSearch: () => void;
40
+ clearSelection: () => void;
41
+ goToPage: (page: number) => void;
42
+ }>) => void;
43
+ attrs: any;
44
+ slots: {
45
+ [key: `cell-${string}`]: (props: {
46
+ row: TRow;
47
+ value: unknown;
48
+ }) => unknown;
49
+ empty?: () => unknown;
50
+ 'empty-actions'?: () => unknown;
51
+ selection?: (props: {
52
+ count: number;
53
+ keys: (number | string)[];
54
+ clear: () => void;
55
+ }) => unknown;
56
+ toolbar?: (props: {
57
+ query: VunoTableQuery;
58
+ }) => unknown;
59
+ };
60
+ emit: ((evt: "rowClick", row: TRow) => void) & (((event: "update:query", value: VunoTableQuery) => void) & ((event: "update:selection", value: (string | number)[]) => void));
61
+ }>) => import("vue").VNode & {
62
+ __ctx?: NonNullable<Awaited<typeof __VLS_setup>>;
63
+ };
64
+ declare const _default: typeof __VLS_export;
65
+ export default _default;
66
+ type __VLS_PrettifyLocal<T> = (T extends any ? {
67
+ [K in keyof T]: T[K];
68
+ } : {
69
+ [K in keyof T as K]: T[K];
70
+ }) & {};
@@ -0,0 +1,8 @@
1
+ import type { VunoTableColumn, VunoTableRow } from '#vuno/types/table';
2
+ type __VLS_Props = {
3
+ column: VunoTableColumn;
4
+ row: VunoTableRow;
5
+ };
6
+ 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>;
7
+ declare const _default: typeof __VLS_export;
8
+ export default _default;
@@ -0,0 +1,84 @@
1
+ <script setup>
2
+ import { computed } from "vue";
3
+ import { useLocale } from "@nuxt/ui/composables";
4
+ import { formatCurrency, formatDate, formatNumber } from "#vuno/utils/format";
5
+ import { getPath } from "#vuno/utils/path";
6
+ const props = defineProps({
7
+ column: { type: null, required: true },
8
+ row: { type: Object, required: true }
9
+ });
10
+ const locale = useLocale();
11
+ const value = computed(() => getPath(props.row, props.column.key));
12
+ const text = computed(() => {
13
+ const raw = value.value;
14
+ if (raw === null || raw === void 0) return "";
15
+ return String(raw);
16
+ });
17
+ const badge = computed(() => {
18
+ if (props.column.type !== "badge") return null;
19
+ const entry = props.column.map?.[text.value];
20
+ return {
21
+ color: entry?.color ?? props.column.color ?? "neutral",
22
+ label: entry?.label ?? text.value
23
+ };
24
+ });
25
+ const alt = computed(() => {
26
+ if (props.column.type !== "image") return "";
27
+ if (props.column.altKey) return String(getPath(props.row, props.column.altKey) ?? "");
28
+ return props.column.alt ?? "";
29
+ });
30
+ </script>
31
+
32
+ <template>
33
+ <span
34
+ v-if="props.column.type === 'number'"
35
+ class="tabular-nums"
36
+ >
37
+ {{ formatNumber(value, locale.code.value, props.column.decimals) }}
38
+ </span>
39
+
40
+ <span
41
+ v-else-if="props.column.type === 'currency'"
42
+ class="tabular-nums"
43
+ >
44
+ {{ formatCurrency(value, locale.code.value, props.column.currency, props.column.decimals) }}
45
+ </span>
46
+
47
+ <span
48
+ v-else-if="props.column.type === 'date'"
49
+ class="tabular-nums"
50
+ >
51
+ {{ formatDate(value, locale.code.value, { dateStyle: props.column.dateStyle ?? "medium", ...props.column.timeStyle ? { timeStyle: props.column.timeStyle } : {} }) }}
52
+ </span>
53
+
54
+ <UBadge
55
+ v-else-if="props.column.type === 'badge' && badge"
56
+ :color="badge.color"
57
+ :label="badge.label"
58
+ variant="subtle"
59
+ />
60
+
61
+ <UIcon
62
+ v-else-if="props.column.type === 'boolean' && !props.column.trueLabel && !props.column.falseLabel"
63
+ :class="value ? 'text-success' : 'text-muted'"
64
+ :name="value ? 'lucide:check' : 'lucide:minus'"
65
+ />
66
+
67
+ <span v-else-if="props.column.type === 'boolean'">
68
+ {{ value ? props.column.trueLabel ?? "" : props.column.falseLabel ?? "" }}
69
+ </span>
70
+
71
+ <UAvatar
72
+ v-else-if="props.column.type === 'image'"
73
+ :alt="alt"
74
+ :size="props.column.size ?? 'md'"
75
+ :src="text"
76
+ />
77
+
78
+ <span
79
+ v-else
80
+ :class="props.column.type === 'text' && props.column.truncate ? 'block max-w-[32ch] truncate' : void 0"
81
+ >
82
+ {{ text }}
83
+ </span>
84
+ </template>
@@ -0,0 +1,8 @@
1
+ import type { VunoTableColumn, VunoTableRow } from '#vuno/types/table';
2
+ type __VLS_Props = {
3
+ column: VunoTableColumn;
4
+ row: VunoTableRow;
5
+ };
6
+ 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>;
7
+ declare const _default: typeof __VLS_export;
8
+ export default _default;
@@ -0,0 +1,23 @@
1
+ import type { VunoCrud, VunoCrudOptions } from '#vuno/types/crud';
2
+ import type { VunoTableRow } from '#vuno/types/table';
3
+ /**
4
+ * El ciclo completo de una pantalla de mantenimiento: listar, dar de alta o
5
+ * editar en un modal, borrar con confirmación, refrescar la caché y avisar.
6
+ *
7
+ * Existe porque ese ciclo es idéntico en todas las entidades y, escrito a mano,
8
+ * son unas cien líneas por pantalla en las que es fácil olvidar justo lo que no
9
+ * se ve: invalidar la caché, avisar del fallo o distinguir un listado vacío de
10
+ * uno que no se pudo cargar.
11
+ *
12
+ * ```ts
13
+ * const crud = useVunoCrud({
14
+ * key: 'brands',
15
+ * list: (query) => $fetch('/api/brands', { query }),
16
+ * create: (form) => $fetch('/api/brands', { body: form, method: 'POST' }),
17
+ * update: (id, form) => $fetch(`/api/brands/${id}`, { body: form, method: 'PATCH' }),
18
+ * remove: (row) => $fetch(`/api/brands/${row.id}`, { method: 'DELETE' }),
19
+ * emptyForm: () => ({ name: '', slug: '' }),
20
+ * })
21
+ * ```
22
+ */
23
+ export declare function useVunoCrud<TRow extends VunoTableRow, TForm extends Record<string, unknown>>(options: VunoCrudOptions<TRow, TForm>): VunoCrud<TRow, TForm>;
@@ -0,0 +1,117 @@
1
+ import { keepPreviousData, useMutation, useQuery, useQueryClient } from "@tanstack/vue-query";
2
+ import { useToast } from "@nuxt/ui/composables";
3
+ import { computed, ref, shallowRef } from "vue";
4
+ import { getPath } from "#vuno/utils/path";
5
+ import { useVunoMessages } from "./useVunoTheme.js";
6
+ import { useVunoTableQuery } from "./useVunoTableQuery.js";
7
+ export function useVunoCrud(options) {
8
+ const toast = useToast();
9
+ const queryClient = useQueryClient();
10
+ const messages = useVunoMessages();
11
+ const query = options.query ?? useVunoTableQuery();
12
+ const baseKey = computed(() => Array.isArray(options.key) ? options.key : [options.key]);
13
+ const rowKey = options.rowKey ?? "id";
14
+ const text = computed(() => ({
15
+ created: options.messages?.created ?? messages.value.created,
16
+ createFailed: options.messages?.createFailed ?? messages.value.saveFailed,
17
+ deleted: options.messages?.deleted ?? messages.value.deleted,
18
+ deleteFailed: options.messages?.deleteFailed ?? messages.value.deleteFailed,
19
+ updated: options.messages?.updated ?? messages.value.updated,
20
+ updateFailed: options.messages?.updateFailed ?? messages.value.saveFailed
21
+ }));
22
+ const { data, isError, isPending, refetch } = useQuery({
23
+ // Sin esto, cambiar de filtro o de página deja `data` en undefined
24
+ // mientras llega la respuesta y la tabla parpadea enseñando su estado
25
+ // vacío. Con los datos anteriores en pantalla, el cambio no salta.
26
+ placeholderData: keepPreviousData,
27
+ queryKey: computed(() => [...baseKey.value, query.value]),
28
+ // La consulta forma parte de la clave: otra página u otro filtro es otra
29
+ // consulta, y así cada una se cachea por separado.
30
+ queryFn: () => options.list(query.value)
31
+ });
32
+ const items = computed(() => data.value?.data ?? []);
33
+ const total = computed(() => data.value?.total ?? 0);
34
+ const failure = computed(() => isError.value ? { retry: () => void refetch() } : void 0);
35
+ function invalidate() {
36
+ void queryClient.invalidateQueries({ queryKey: baseKey.value });
37
+ }
38
+ const formOpen = ref(false);
39
+ const editing = shallowRef(null);
40
+ const state = ref(options.emptyForm());
41
+ const save = useMutation({
42
+ onError: (error) => {
43
+ toast.add({ color: "error", description: describe(error), title: editing.value ? text.value.updateFailed : text.value.createFailed });
44
+ },
45
+ onSuccess: (result) => {
46
+ invalidate();
47
+ formOpen.value = false;
48
+ toast.add({ color: "success", title: result === "updated" ? text.value.updated : text.value.created });
49
+ },
50
+ mutationFn: async (form) => {
51
+ const row = editing.value;
52
+ if (row && options.update) {
53
+ await options.update(getPath(row, rowKey), form);
54
+ return "updated";
55
+ }
56
+ if (options.create) await options.create(form);
57
+ return "created";
58
+ }
59
+ });
60
+ function openCreate() {
61
+ editing.value = null;
62
+ state.value = options.emptyForm();
63
+ formOpen.value = true;
64
+ }
65
+ function openEdit(row) {
66
+ editing.value = row;
67
+ state.value = options.toForm ? options.toForm(row) : { ...row };
68
+ formOpen.value = true;
69
+ }
70
+ const confirmOpen = ref(false);
71
+ const deleting = shallowRef(null);
72
+ const remove = useMutation({
73
+ onError: (error) => {
74
+ toast.add({ color: "error", description: describe(error), title: text.value.deleteFailed });
75
+ },
76
+ onSuccess: () => {
77
+ invalidate();
78
+ confirmOpen.value = false;
79
+ deleting.value = null;
80
+ toast.add({ color: "success", title: text.value.deleted });
81
+ },
82
+ mutationFn: async (row) => {
83
+ if (options.remove) await options.remove(row);
84
+ }
85
+ });
86
+ function askDelete(row) {
87
+ deleting.value = row;
88
+ confirmOpen.value = true;
89
+ }
90
+ function confirmDelete() {
91
+ if (deleting.value) remove.mutate(deleting.value);
92
+ }
93
+ return {
94
+ askDelete,
95
+ confirmDelete,
96
+ confirmOpen,
97
+ deleting,
98
+ editing,
99
+ failure,
100
+ formOpen,
101
+ invalidate,
102
+ isDeleting: remove.isPending,
103
+ isPending,
104
+ isSaving: save.isPending,
105
+ items,
106
+ openCreate,
107
+ openEdit,
108
+ query,
109
+ refetch,
110
+ state,
111
+ total,
112
+ submit: () => save.mutate(state.value)
113
+ };
114
+ }
115
+ function describe(error) {
116
+ return error instanceof Error ? error.message : void 0;
117
+ }
@@ -0,0 +1,10 @@
1
+ import { type MaybeRefOrGetter } from 'vue';
2
+ export declare function useVunoField<TValue = unknown>(name: MaybeRefOrGetter<string>): {
3
+ name: import("vue").ComputedRef<string>;
4
+ dirty: import("vue").ComputedRef<boolean>;
5
+ reset: () => void;
6
+ setTouched: (touched?: boolean) => void;
7
+ setValue: (newValue: TValue) => void;
8
+ touched: import("vue").ComputedRef<boolean>;
9
+ value: import("vue").WritableComputedRef<TValue | undefined, TValue | undefined>;
10
+ };
@@ -0,0 +1,34 @@
1
+ import { computed, toValue } from "vue";
2
+ import { useVunoForm } from "./useVunoForm.js";
3
+ export function useVunoField(name) {
4
+ const form = useVunoForm();
5
+ const resolvedName = computed(() => toValue(name));
6
+ const value = computed({
7
+ get() {
8
+ return form.getValue(resolvedName.value);
9
+ },
10
+ set(newValue) {
11
+ form.setValue(resolvedName.value, newValue);
12
+ }
13
+ });
14
+ const dirty = computed(() => form.isDirty(resolvedName.value));
15
+ const touched = computed(() => form.isTouched(resolvedName.value));
16
+ function reset() {
17
+ form.resetField(resolvedName.value);
18
+ }
19
+ function setTouched(touched2 = true) {
20
+ form.setTouched(resolvedName.value, touched2);
21
+ }
22
+ function setValue(newValue) {
23
+ value.value = newValue;
24
+ }
25
+ return {
26
+ name: resolvedName,
27
+ dirty,
28
+ reset,
29
+ setTouched,
30
+ setValue,
31
+ touched,
32
+ value
33
+ };
34
+ }
@@ -0,0 +1,2 @@
1
+ import type { VunoFormContext, VunoFormState } from '#vuno/types/form';
2
+ export declare function useVunoForm<TState extends VunoFormState = VunoFormState>(): VunoFormContext<TState>;
@@ -0,0 +1,9 @@
1
+ import { inject } from "vue";
2
+ import { vunoFormInjectionKey } from "#vuno/types/form";
3
+ export function useVunoForm() {
4
+ const form = inject(vunoFormInjectionKey, null);
5
+ if (!form) {
6
+ throw new Error("[Vuno] useVunoForm() debe utilizarse dentro de <VForm>.");
7
+ }
8
+ return form;
9
+ }
@@ -0,0 +1,51 @@
1
+ import { type Ref } from 'vue';
2
+ import type { VunoTableFilters, VunoTableQuery, VunoTableSort } from '#vuno/types/table';
3
+ export type VunoTableQueryOptions = {
4
+ /**
5
+ * Nombres de los filtros que viajan en la URL. Hace falta declararlos
6
+ * porque al arrancar sin valores no habría forma de saber cuáles leer.
7
+ */
8
+ filterKeys?: string[];
9
+ filters?: VunoTableFilters;
10
+ /**
11
+ * Cómo se escribe en el historial del navegador.
12
+ *
13
+ * - `replace` (por defecto): no deja rastro. Recargar y compartir el
14
+ * enlace funcionan, pero el botón atrás sale de la página en vez de
15
+ * deshacer el último filtro.
16
+ * - `push`: cada cambio es una entrada, así que atrás deshace filtro a
17
+ * filtro. A cambio, teclear en el buscador llena el historial —el
18
+ * antirrebote de la tabla lo suaviza, pero no lo evita.
19
+ */
20
+ history?: 'push' | 'replace';
21
+ page?: number;
22
+ pageSize?: number;
23
+ /** Prefijo de los parámetros, para tener varias tablas en la misma página. */
24
+ prefix?: string;
25
+ search?: string;
26
+ /**
27
+ * Orden inicial de la tabla. Admite la forma corta `'sku'` (ascendente).
28
+ *
29
+ * Declararlo cambia el ciclo de la cabecera: en vez de asc → desc → sin
30
+ * orden, el tercer clic devuelve a este orden.
31
+ *
32
+ * Va aquí y no en la definición porque el primer `fetch` —el del servidor
33
+ * incluido— tiene que salir ya ordenado; aplicarlo al montar la tabla
34
+ * provocaría una segunda petición y un parpadeo.
35
+ */
36
+ sort?: null | string | VunoTableSort;
37
+ /**
38
+ * Guarda el estado en la query string: el enlace es compartible y recargar
39
+ * conserva los filtros. Si además quieres que el botón atrás los deshaga,
40
+ * usa `history: 'push'`.
41
+ */
42
+ url?: boolean;
43
+ };
44
+ /**
45
+ * Estado de consulta para `<VTable v-model:query>`.
46
+ *
47
+ * Sin `url` es un `ref` normal. Con `url: true` se sincroniza en los dos
48
+ * sentidos con la query string, de modo que navegar atrás o pegar el enlace
49
+ * en otra pestaña reproduce la misma vista.
50
+ */
51
+ export declare function useVunoTableQuery(options?: VunoTableQueryOptions): Ref<VunoTableQuery>;
@@ -0,0 +1,110 @@
1
+ import { computed, ref, watch } from "vue";
2
+ import { useRoute, useRouter } from "#imports";
3
+ export function useVunoTableQuery(options = {}) {
4
+ const defaults = {
5
+ filters: options.filters ?? {},
6
+ page: options.page ?? 1,
7
+ pageSize: options.pageSize ?? 10,
8
+ search: options.search ?? "",
9
+ sort: typeof options.sort === "string" ? { column: options.sort, direction: "asc" } : options.sort ?? null
10
+ };
11
+ const normalize = (value) => ({ ...value, sort: value.sort ?? defaults.sort });
12
+ if (!options.url) {
13
+ const local = ref({ ...defaults });
14
+ return computed({
15
+ get: () => local.value,
16
+ set: (value) => {
17
+ local.value = normalize(value);
18
+ }
19
+ });
20
+ }
21
+ const route = useRoute();
22
+ const router = useRouter();
23
+ const prefix = options.prefix ?? "";
24
+ const keys = {
25
+ direction: `${prefix}direction`,
26
+ page: `${prefix}page`,
27
+ pageSize: `${prefix}pageSize`,
28
+ search: `${prefix}search`,
29
+ sort: `${prefix}sort`
30
+ };
31
+ const filterKeys = options.filterKeys ?? Object.keys(defaults.filters);
32
+ const readFilters = () => {
33
+ const filters = { ...defaults.filters };
34
+ for (const key of filterKeys) {
35
+ const raw = route.query[`${prefix}${key}`];
36
+ if (raw === void 0) {
37
+ delete filters[key];
38
+ continue;
39
+ }
40
+ const value = readString(raw, "");
41
+ filters[key] = value === "true" ? true : value === "false" ? false : value;
42
+ }
43
+ return filters;
44
+ };
45
+ const fromRoute = () => {
46
+ const column = readString(route.query[keys.sort], "");
47
+ return {
48
+ filters: readFilters(),
49
+ page: readNumber(route.query[keys.page], defaults.page),
50
+ pageSize: readNumber(route.query[keys.pageSize], defaults.pageSize),
51
+ search: readString(route.query[keys.search], defaults.search),
52
+ // Sin parámetro se cae al orden por defecto.
53
+ sort: column ? { column, direction: readString(route.query[keys.direction], "asc") === "desc" ? "desc" : "asc" } : defaults.sort
54
+ };
55
+ };
56
+ const state = ref(fromRoute());
57
+ const toRouteQuery = (value) => {
58
+ const next = { ...route.query };
59
+ next[keys.page] = value.page === defaults.page ? void 0 : String(value.page);
60
+ next[keys.pageSize] = value.pageSize === defaults.pageSize ? void 0 : String(value.pageSize);
61
+ next[keys.search] = value.search === "" ? void 0 : value.search;
62
+ const esElOrdenPorDefecto = value.sort?.column === defaults.sort?.column && value.sort?.direction === defaults.sort?.direction;
63
+ if (esElOrdenPorDefecto) {
64
+ next[keys.sort] = void 0;
65
+ next[keys.direction] = void 0;
66
+ } else if (value.sort) {
67
+ next[keys.sort] = value.sort.column;
68
+ next[keys.direction] = value.sort.direction;
69
+ } else {
70
+ next[keys.sort] = void 0;
71
+ next[keys.direction] = void 0;
72
+ }
73
+ for (const key of filterKeys) {
74
+ const filter = value.filters[key];
75
+ next[`${prefix}${key}`] = filter === void 0 || filter === null || filter === "" ? void 0 : String(filter);
76
+ }
77
+ for (const key of Object.keys(next)) {
78
+ if (next[key] === void 0) delete next[key];
79
+ }
80
+ return next;
81
+ };
82
+ const serialized = computed(() => JSON.stringify(toRouteQuery(state.value)));
83
+ watch(serialized, () => {
84
+ const to = { query: toRouteQuery(state.value) };
85
+ void (options.history === "push" ? router.push(to) : router.replace(to));
86
+ });
87
+ watch(
88
+ () => route.query,
89
+ () => {
90
+ const next = fromRoute();
91
+ if (JSON.stringify(next) !== JSON.stringify(state.value)) {
92
+ state.value = next;
93
+ }
94
+ }
95
+ );
96
+ return computed({
97
+ get: () => state.value,
98
+ set: (value) => {
99
+ state.value = normalize(value);
100
+ }
101
+ });
102
+ }
103
+ function readNumber(value, fallback) {
104
+ const parsed = Number(Array.isArray(value) ? value[0] : value);
105
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
106
+ }
107
+ function readString(value, fallback) {
108
+ const raw = Array.isArray(value) ? value[0] : value;
109
+ return typeof raw === "string" ? raw : fallback;
110
+ }
@@ -0,0 +1,12 @@
1
+ import { type ComputedRef, type MaybeRefOrGetter } from 'vue';
2
+ import type { VunoFieldSlots } from '#vuno/types/field';
3
+ import type { VunoIcons, VunoMessages, VunoThemeKey } from '#vuno/types/theme';
4
+ /**
5
+ * Resuelve las clases de un componente del módulo.
6
+ *
7
+ * @param key Clave del tema (`fieldText`, `form`, ...).
8
+ * @param ui Override de la instancia, normalmente `props.ui`.
9
+ */
10
+ export declare function useVunoTheme(key: VunoThemeKey, ui?: MaybeRefOrGetter<undefined | VunoFieldSlots>): ComputedRef<VunoFieldSlots>;
11
+ export declare function useVunoIcons(): ComputedRef<VunoIcons>;
12
+ export declare function useVunoMessages(): ComputedRef<VunoMessages>;
@@ -0,0 +1,35 @@
1
+ import { computed, toValue } from "vue";
2
+ import { useAppConfig } from "#imports";
3
+ import { defaultIcons, defaultMessages, defaultTheme } from "#vuno/theme";
4
+ export function useVunoTheme(key, ui) {
5
+ const vuno = useVunoAppConfig();
6
+ return computed(() => mergeSlots(defaultTheme[key]?.slots, vuno.value[key]?.slots, toValue(ui)));
7
+ }
8
+ export function useVunoIcons() {
9
+ const vuno = useVunoAppConfig();
10
+ return computed(() => ({ ...defaultIcons, ...vuno.value.icons }));
11
+ }
12
+ export function useVunoMessages() {
13
+ const vuno = useVunoAppConfig();
14
+ return computed(() => ({ ...defaultMessages, ...vuno.value.messages }));
15
+ }
16
+ function toClassString(value) {
17
+ if (!value) return "";
18
+ return Array.isArray(value) ? value.filter(Boolean).join(" ") : value;
19
+ }
20
+ function mergeSlots(...sources) {
21
+ const merged = {};
22
+ for (const source of sources) {
23
+ if (!source) continue;
24
+ for (const [slot, value] of Object.entries(source)) {
25
+ const className = toClassString(value);
26
+ if (!className) continue;
27
+ merged[slot] = merged[slot] ? `${merged[slot]} ${className}` : className;
28
+ }
29
+ }
30
+ return merged;
31
+ }
32
+ function useVunoAppConfig() {
33
+ const appConfig = useAppConfig();
34
+ return computed(() => appConfig.vuno ?? {});
35
+ }
@@ -0,0 +1,14 @@
1
+ import type { VunoIcons, VunoMessages, VunoThemeConfig } from '#vuno/types/theme';
2
+ /**
3
+ * Tema por defecto. Solo aporta geometría (ancho, grid, espaciado) y
4
+ * utilidades semánticas de Nuxt UI; ningún color, radio ni tipografía
5
+ * literal, de modo que el proyecto que consume el módulo sigue mandando
6
+ * a través de `ui.colors`, `--ui-radius` y compañía.
7
+ *
8
+ * Las claves `field*` describen los slots del componente de Nuxt UI que
9
+ * cada campo envuelve. El `w-full` es deliberado y no opcional por diseño:
10
+ * el ancho de un campo lo decide el grid de `utils/grid`, nunca el campo.
11
+ */
12
+ export declare const defaultTheme: Required<VunoThemeConfig>;
13
+ export declare const defaultIcons: VunoIcons;
14
+ export declare const defaultMessages: VunoMessages;