@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,36 @@
1
+ <script setup>
2
+ import { useVunoIcons, useVunoMessages, useVunoTheme } from "#vuno/composables/useVunoTheme";
3
+ const props = defineProps({
4
+ hint: { type: String, required: false },
5
+ icon: { type: String, required: false },
6
+ title: { type: String, required: false },
7
+ ui: { type: Object, required: false }
8
+ });
9
+ defineSlots();
10
+ const icons = useVunoIcons();
11
+ const messages = useVunoMessages();
12
+ const ui = useVunoTheme("state", () => props.ui);
13
+ </script>
14
+
15
+ <template>
16
+ <div :class="ui.root">
17
+ <UIcon
18
+ class="text-dimmed"
19
+ :class="ui.icon"
20
+ :name="props.icon ?? icons.empty"
21
+ />
22
+
23
+ <p :class="ui.title">{{ props.title ?? messages.emptyTable }}</p>
24
+
25
+ <p
26
+ v-if="props.hint"
27
+ :class="ui.hint"
28
+ >
29
+ {{ props.hint }}
30
+ </p>
31
+
32
+ <div :class="ui.actions">
33
+ <slot name="actions" />
34
+ </div>
35
+ </div>
36
+ </template>
@@ -0,0 +1,20 @@
1
+ import type { VunoFieldSlots } from '#vuno/types/field';
2
+ type __VLS_Props = {
3
+ hint?: string;
4
+ icon?: string;
5
+ title?: string;
6
+ ui?: VunoFieldSlots;
7
+ };
8
+ type __VLS_Slots = {
9
+ /** La invitación a crear el primer registro va aquí. */
10
+ actions?: () => unknown;
11
+ };
12
+ declare const __VLS_base: 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>;
13
+ declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
14
+ declare const _default: typeof __VLS_export;
15
+ export default _default;
16
+ type __VLS_WithSlots<T, S> = T & {
17
+ new (): {
18
+ $slots: S;
19
+ };
20
+ };
@@ -0,0 +1,10 @@
1
+ import type { VunoFieldSlots } from '#vuno/types/field';
2
+ import type { VunoTableFailure } from '#vuno/types/table';
3
+ type __VLS_Props = {
4
+ /** El fallo con su reintento dentro: no hay forma de pintarlo sin salida. */
5
+ failure: VunoTableFailure;
6
+ ui?: VunoFieldSlots;
7
+ };
8
+ 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>;
9
+ declare const _default: typeof __VLS_export;
10
+ export default _default;
@@ -0,0 +1,34 @@
1
+ <script setup>
2
+ import { useVunoIcons, useVunoMessages, useVunoTheme } from "#vuno/composables/useVunoTheme";
3
+ const props = defineProps({
4
+ failure: { type: Object, required: true },
5
+ ui: { type: Object, required: false }
6
+ });
7
+ const icons = useVunoIcons();
8
+ const messages = useVunoMessages();
9
+ const ui = useVunoTheme("state", () => props.ui);
10
+ </script>
11
+
12
+ <template>
13
+ <div :class="ui.root">
14
+ <UIcon
15
+ class="text-error"
16
+ :class="ui.icon"
17
+ :name="icons.failure"
18
+ />
19
+
20
+ <p :class="ui.title">{{ props.failure.title ?? messages.failureTitle }}</p>
21
+
22
+ <p :class="ui.hint">{{ props.failure.hint ?? messages.failureHint }}</p>
23
+
24
+ <div :class="ui.actions">
25
+ <UButton
26
+ color="neutral"
27
+ :icon="icons.retry"
28
+ :label="messages.retry"
29
+ variant="outline"
30
+ @click="props.failure.retry()"
31
+ />
32
+ </div>
33
+ </div>
34
+ </template>
@@ -0,0 +1,10 @@
1
+ import type { VunoFieldSlots } from '#vuno/types/field';
2
+ import type { VunoTableFailure } from '#vuno/types/table';
3
+ type __VLS_Props = {
4
+ /** El fallo con su reintento dentro: no hay forma de pintarlo sin salida. */
5
+ failure: VunoTableFailure;
6
+ ui?: VunoFieldSlots;
7
+ };
8
+ 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>;
9
+ declare const _default: typeof __VLS_export;
10
+ export default _default;
@@ -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,432 @@
1
+ <script setup>
2
+ import { computed, onBeforeUnmount, ref, useTemplateRef, watch } from "vue";
3
+ import EmptyState from "#vuno/components/State/EmptyState.vue";
4
+ import FailureState from "#vuno/components/State/FailureState.vue";
5
+ import Form from "#vuno/components/Form/Form.vue";
6
+ import { useVunoIcons, useVunoMessages, useVunoTheme } from "#vuno/composables/useVunoTheme";
7
+ import { getPath } from "#vuno/utils/path";
8
+ import TableCell from "./TableCell.vue";
9
+ const props = defineProps({
10
+ data: { type: Array, required: true },
11
+ definition: { type: Object, required: true },
12
+ failure: { type: Object, required: false },
13
+ loading: { type: Boolean, required: false },
14
+ total: { type: Number, required: false },
15
+ ui: { type: Object, required: false }
16
+ });
17
+ const total = computed(() => props.total ?? 0);
18
+ const hasFailure = computed(() => Boolean(props.failure));
19
+ const emit = defineEmits(["rowClick"]);
20
+ const query = defineModel("query", { type: Object, ...{ required: true } });
21
+ const selection = defineModel("selection", { type: Array, ...{ default: () => [] } });
22
+ defineSlots();
23
+ const icons = useVunoIcons();
24
+ const messages = useVunoMessages();
25
+ const ui = useVunoTheme("table", () => props.ui);
26
+ const rowKey = computed(() => props.definition.rowKey ?? "id");
27
+ const pageSizes = computed(() => props.definition.pageSizes ?? [10, 25, 50, 100]);
28
+ const getRowId = (row) => String(getPath(row, rowKey.value) ?? "");
29
+ const search = ref(query.value.search);
30
+ let searchTimer;
31
+ watch(search, (value) => {
32
+ clearTimeout(searchTimer);
33
+ searchTimer = setTimeout(() => {
34
+ if (value === query.value.search) return;
35
+ query.value = { ...query.value, page: 1, search: value };
36
+ }, 300);
37
+ });
38
+ watch(
39
+ () => query.value.search,
40
+ (value) => {
41
+ if (value !== search.value) search.value = value;
42
+ }
43
+ );
44
+ onBeforeUnmount(() => clearTimeout(searchTimer));
45
+ const searchInput = useTemplateRef("searchInput");
46
+ function clearSearch() {
47
+ clearTimeout(searchTimer);
48
+ search.value = "";
49
+ query.value = { ...query.value, page: 1, search: "" };
50
+ searchInput.value?.inputRef?.focus();
51
+ }
52
+ function sortDirectionFor(key) {
53
+ return query.value.sort?.column === key ? query.value.sort.direction : null;
54
+ }
55
+ function toggleSort(key) {
56
+ const current = sortDirectionFor(key);
57
+ const sort = current === null ? { column: key, direction: "asc" } : current === "asc" ? { column: key, direction: "desc" } : null;
58
+ query.value = { ...query.value, page: 1, sort };
59
+ }
60
+ function sortIcon(key) {
61
+ const direction = sortDirectionFor(key);
62
+ if (direction === "asc") return icons.value.sortAsc;
63
+ if (direction === "desc") return icons.value.sortDesc;
64
+ return icons.value.sort;
65
+ }
66
+ const filtersOpen = ref(false);
67
+ const filterState = ref({ ...query.value.filters });
68
+ watch(
69
+ () => query.value.filters,
70
+ (value) => {
71
+ filterState.value = { ...value };
72
+ },
73
+ { deep: true }
74
+ );
75
+ const hasValue = (value) => value !== void 0 && value !== null && value !== "";
76
+ const filtersDefinition = computed(() => ({
77
+ sections: [{ id: "filters", fields: props.definition.filters ?? [] }]
78
+ }));
79
+ const activeFilters = computed(
80
+ () => (props.definition.filters ?? []).filter((field) => hasValue(query.value.filters[field.name])).map((field) => {
81
+ const value = query.value.filters[field.name];
82
+ const option = "items" in field ? field.items?.find((item) => String(item.value) === String(value)) : void 0;
83
+ return {
84
+ name: field.name,
85
+ label: field.label ?? field.name,
86
+ value: option?.label ?? String(value)
87
+ };
88
+ })
89
+ );
90
+ function applyFilters() {
91
+ query.value = { ...query.value, filters: { ...filterState.value }, page: 1 };
92
+ filtersOpen.value = false;
93
+ }
94
+ function clearFilters() {
95
+ filterState.value = {};
96
+ query.value = { ...query.value, filters: {}, page: 1 };
97
+ filtersOpen.value = false;
98
+ }
99
+ function removeFilter(name) {
100
+ const filters = { ...query.value.filters };
101
+ delete filters[name];
102
+ query.value = { ...query.value, filters, page: 1 };
103
+ }
104
+ const rowSelection = computed({
105
+ get: () => Object.fromEntries(selection.value.map((key) => [String(key), true])),
106
+ set: (value) => {
107
+ selection.value = Object.entries(value).filter(([, selected]) => selected).map(([key]) => key);
108
+ }
109
+ });
110
+ const clearSelection = () => {
111
+ selection.value = [];
112
+ };
113
+ const columns = computed(() => {
114
+ const list = [];
115
+ if (props.definition.selectable) {
116
+ list.push({ id: "select", enableSorting: false });
117
+ }
118
+ for (const column of props.definition.columns) {
119
+ list.push({
120
+ id: column.key,
121
+ accessorKey: column.key,
122
+ enableSorting: false,
123
+ // La columna de acciones no lleva rótulo salvo que se pida uno.
124
+ header: column.type === "actions" ? column.label ?? "" : column.label ?? column.key,
125
+ meta: {
126
+ class: {
127
+ td: [column.align === "end" ? "text-end" : column.align === "center" ? "text-center" : "", column.class].filter(Boolean).join(" "),
128
+ th: [column.align === "end" ? "text-end" : column.align === "center" ? "text-center" : "", column.width].filter(Boolean).join(" ")
129
+ }
130
+ }
131
+ });
132
+ }
133
+ return list;
134
+ });
135
+ const rangeStart = computed(() => total.value === 0 ? 0 : (query.value.page - 1) * query.value.pageSize + 1);
136
+ const rangeEnd = computed(() => Math.min(query.value.page * query.value.pageSize, total.value));
137
+ function onPageChange(page) {
138
+ query.value = { ...query.value, page };
139
+ }
140
+ function onPageSizeChange(size) {
141
+ query.value = { ...query.value, page: 1, pageSize: size };
142
+ }
143
+ defineExpose({
144
+ clearFilters,
145
+ clearSearch,
146
+ clearSelection,
147
+ goToPage: onPageChange
148
+ });
149
+ function onRowSelect(_event, row) {
150
+ emit("rowClick", row.original);
151
+ }
152
+ </script>
153
+
154
+ <template>
155
+ <div :class="ui.root">
156
+ <div :class="ui.toolbar">
157
+ <UInput
158
+ ref="searchInput"
159
+ v-model="search"
160
+ :icon="icons.search"
161
+ :placeholder="props.definition.searchPlaceholder ?? messages.searchPlaceholder"
162
+ :ui="{ root: ui.search }"
163
+ >
164
+ <template
165
+ v-if="search.length > 0"
166
+ #trailing
167
+ >
168
+ <UButton
169
+ :aria-label="messages.clearSearch"
170
+ color="neutral"
171
+ :icon="icons.clearSearch"
172
+ size="xs"
173
+ variant="link"
174
+ @click="clearSearch"
175
+ />
176
+ </template>
177
+ </UInput>
178
+
179
+ <div :class="ui.toolbarActions">
180
+ <UButton
181
+ v-if="(props.definition.filters?.length ?? 0) > 0"
182
+ color="neutral"
183
+ :icon="icons.filter"
184
+ :label="messages.filters"
185
+ variant="outline"
186
+ @click="filtersOpen = true"
187
+ >
188
+ <!-- `UButton` no admite `badge`: el contador va en el slot. -->
189
+ <template
190
+ v-if="activeFilters.length > 0"
191
+ #trailing
192
+ >
193
+ <UBadge
194
+ color="primary"
195
+ :label="String(activeFilters.length)"
196
+ size="sm"
197
+ variant="solid"
198
+ />
199
+ </template>
200
+ </UButton>
201
+
202
+ <slot
203
+ name="toolbar"
204
+ :query="query"
205
+ />
206
+ </div>
207
+ </div>
208
+
209
+ <div
210
+ v-if="props.definition.selectable && selection.length > 0"
211
+ :class="ui.selection"
212
+ >
213
+ <slot
214
+ :clear="clearSelection"
215
+ :count="selection.length"
216
+ :keys="selection"
217
+ name="selection"
218
+ >
219
+ <span>{{ selection.length }} {{ selection.length === 1 ? messages.rowSelected : messages.rowsSelected }}</span>
220
+ <UButton
221
+ color="neutral"
222
+ :label="messages.clearSelection"
223
+ size="xs"
224
+ variant="ghost"
225
+ @click="clearSelection"
226
+ />
227
+ </slot>
228
+ </div>
229
+
230
+ <div
231
+ v-if="activeFilters.length > 0"
232
+ :class="ui.chips"
233
+ >
234
+ <UBadge
235
+ v-for="filter in activeFilters"
236
+ :key="filter.name"
237
+ color="neutral"
238
+ variant="subtle"
239
+ >
240
+ <span>{{ filter.label }}: {{ filter.value }}</span>
241
+ <UButton
242
+ :aria-label="`${messages.clearFilters}: ${filter.label}`"
243
+ color="neutral"
244
+ :icon="icons.fileRemove"
245
+ size="xs"
246
+ variant="link"
247
+ @click="removeFilter(filter.name)"
248
+ />
249
+ </UBadge>
250
+
251
+ <UButton
252
+ color="neutral"
253
+ :label="messages.clearFilters"
254
+ size="xs"
255
+ variant="ghost"
256
+ @click="clearFilters"
257
+ />
258
+ </div>
259
+
260
+ <USlideover
261
+ v-model:open="filtersOpen"
262
+ :title="messages.filtersTitle"
263
+ >
264
+ <template #body>
265
+ <Form
266
+ v-model:state="filterState"
267
+ :definition="filtersDefinition"
268
+ :ui="{ sections: ui.filtersBody }"
269
+ />
270
+ </template>
271
+
272
+ <template #footer>
273
+ <div :class="ui.filtersFooter">
274
+ <UButton
275
+ color="neutral"
276
+ :label="messages.clearFilters"
277
+ variant="ghost"
278
+ @click="clearFilters"
279
+ />
280
+ <UButton
281
+ :label="messages.applyFilters"
282
+ @click="applyFilters"
283
+ />
284
+ </div>
285
+ </template>
286
+ </USlideover>
287
+
288
+ <UTable
289
+ v-model:rowSelection="rowSelection"
290
+ :columns="columns"
291
+ :data="hasFailure ? [] : props.data"
292
+ :getRowId="getRowId"
293
+ :loading="props.loading ?? false"
294
+ :paginationOptions="{ manualPagination: true }"
295
+ :sortingOptions="{ manualSorting: true }"
296
+ :ui="{ base: ui.table, tbody: ui.tbody, td: ui.td, th: ui.th }"
297
+ @select="onRowSelect"
298
+ >
299
+ <template #select-header>
300
+ <UCheckbox
301
+ :modelValue="props.data.length > 0 && props.data.every((row2) => selection.includes(getRowId(row2)))"
302
+ @update:model-value="
303
+ (checked) => {
304
+ const keys = props.data.map((row2) => getRowId(row2));
305
+ selection = checked ? [.../* @__PURE__ */ new Set([...selection, ...keys])] : selection.filter((key) => !keys.includes(String(key)));
306
+ }
307
+ "
308
+ />
309
+ </template>
310
+
311
+ <template #select-cell="{ row }">
312
+ <UCheckbox
313
+ :modelValue="row.getIsSelected()"
314
+ @update:model-value="row.toggleSelected(!row.getIsSelected())"
315
+ />
316
+ </template>
317
+
318
+ <template
319
+ v-for="column in props.definition.columns"
320
+ :key="`${column.key}-header`"
321
+ #[`${column.key}-header`]
322
+ >
323
+ <UButton
324
+ v-if="column.sortable"
325
+ class="-mx-2"
326
+ color="neutral"
327
+ :icon="sortIcon(column.key)"
328
+ :label="column.label ?? column.key"
329
+ size="xs"
330
+ trailing
331
+ variant="ghost"
332
+ @click="toggleSort(column.key)"
333
+ />
334
+ <span v-else>{{ column.type === "actions" ? column.label ?? "" : column.label ?? column.key }}</span>
335
+ </template>
336
+
337
+ <template
338
+ v-for="column in props.definition.columns"
339
+ :key="`${column.key}-cell`"
340
+ #[`${column.key}-cell`]="{ row }"
341
+ >
342
+ <!-- `stop`: sin esto, abrir el menú dispara también el clic de
343
+ fila y se acaban abriendo dos diálogos a la vez. -->
344
+ <UDropdownMenu
345
+ v-if="column.type === 'actions'"
346
+ :items="[
347
+ column.actions.filter((action) => !action.hidden?.(row.original)).map((action) => ({
348
+ color: action.color,
349
+ disabled: action.disabled?.(row.original) ?? false,
350
+ icon: action.icon,
351
+ label: action.label,
352
+ onSelect: () => action.onSelect(row.original)
353
+ }))
354
+ ]"
355
+ @click.stop
356
+ >
357
+ <UButton
358
+ :aria-label="messages.rowActions"
359
+ color="neutral"
360
+ :icon="icons.actions"
361
+ size="xs"
362
+ variant="ghost"
363
+ />
364
+ </UDropdownMenu>
365
+
366
+ <slot
367
+ v-else
368
+ :name="`cell-${column.key}`"
369
+ :row="row.original"
370
+ :value="getPath(row.original, column.key)"
371
+ >
372
+ <TableCell
373
+ :column="column"
374
+ :row="row.original"
375
+ />
376
+ </slot>
377
+ </template>
378
+
379
+ <template #empty>
380
+ <!-- Un fallo y una lista vacía no son lo mismo: el primero
381
+ ofrece reintentar, el segundo invita a crear el primero. -->
382
+ <FailureState
383
+ v-if="props.failure"
384
+ :failure="props.failure"
385
+ />
386
+
387
+ <!-- Cargando y sin filas todavía no es «no hay nada»: decirlo
388
+ antes de tiempo hace parpadear la tabla en cada cambio. -->
389
+ <div v-else-if="props.loading" />
390
+
391
+ <slot
392
+ v-else
393
+ name="empty"
394
+ >
395
+ <EmptyState
396
+ :hint="props.definition.empty?.hint"
397
+ :icon="props.definition.empty?.icon"
398
+ :title="props.definition.empty?.title"
399
+ >
400
+ <template #actions>
401
+ <slot name="empty-actions" />
402
+ </template>
403
+ </EmptyState>
404
+ </slot>
405
+ </template>
406
+ </UTable>
407
+
408
+ <div
409
+ v-if="total > 0 && !hasFailure"
410
+ :class="ui.footer"
411
+ >
412
+ <div :class="ui.meta">
413
+ <span>{{ messages.showing }} {{ rangeStart }}–{{ rangeEnd }} {{ messages.of }} {{ total }}</span>
414
+
415
+ <USelect
416
+ :items="pageSizes"
417
+ :modelValue="query.pageSize"
418
+ size="xs"
419
+ :ui="{ base: 'w-20' }"
420
+ @update:model-value="(size) => onPageSizeChange(Number(size))"
421
+ />
422
+ </div>
423
+
424
+ <UPagination
425
+ :itemsPerPage="query.pageSize"
426
+ :page="query.page"
427
+ :total="total"
428
+ @update:page="onPageChange"
429
+ />
430
+ </div>
431
+ </div>
432
+ </template>