@innertia-solutions/nuxt-theme-spark 0.1.11

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 (34) hide show
  1. package/components/Admin/Base.vue +73 -0
  2. package/components/Admin/Header.vue +32 -0
  3. package/components/Admin/Page.vue +5 -0
  4. package/components/Admin/PageHeader.vue +18 -0
  5. package/components/App/Button.vue +59 -0
  6. package/components/App/Dropdown.vue +286 -0
  7. package/components/App/EmptyState.vue +433 -0
  8. package/components/App/LoadingState.vue +40 -0
  9. package/components/App/PageLoadingSpinner.vue +118 -0
  10. package/components/App/SwitchColorTheme.vue +55 -0
  11. package/components/App/Tag.vue +193 -0
  12. package/components/Forms/DatePicker.vue +255 -0
  13. package/components/Forms/Input.vue +72 -0
  14. package/components/Forms/Select.vue +100 -0
  15. package/components/Forms/SelectServer.vue +726 -0
  16. package/components/Layout/Admin.vue +32 -0
  17. package/components/Layout/Auth.vue +29 -0
  18. package/components/Modal/DeleteConfirm.vue +48 -0
  19. package/components/Modal.vue +103 -0
  20. package/components/Nav/Tabs.vue +39 -0
  21. package/components/Table/DownloadDropdown.vue +111 -0
  22. package/components/Table/FilterDropdown.vue +226 -0
  23. package/components/Toast/Alert.vue +113 -0
  24. package/components/Toast/Container.vue +34 -0
  25. package/components/Toast/Notification.vue +45 -0
  26. package/components/Toast/Process.vue +88 -0
  27. package/nuxt.config.ts +15 -0
  28. package/package.json +45 -0
  29. package/plugins/preline.client.ts +68 -0
  30. package/shared/composables/useForm.js +119 -0
  31. package/shared/composables/useTable.ts +84 -0
  32. package/shared/composables/useToast.js +69 -0
  33. package/shared/stores/toast.js +129 -0
  34. package/spark.css +207 -0
@@ -0,0 +1,113 @@
1
+ <template>
2
+ <div
3
+ class="max-w-xs relative bg-white dark:bg-slate-800 border border-gray-200 dark:border-slate-700 rounded-xl shadow-lg p-4 pr-10 flex items-start overflow-hidden"
4
+ :class="{
5
+ 'border-green-200': toast.severity === 'success',
6
+ 'border-red-200': toast.severity === 'danger',
7
+ 'border-yellow-200': toast.severity === 'warning',
8
+ 'border-blue-200': toast.severity === 'info',
9
+ }"
10
+ role="alert"
11
+ >
12
+ <div class="mr-3 mt-1">
13
+ <i v-if="toast.icon" :class="toast.icon + ' text-gray-400 text-xl'" />
14
+ </div>
15
+ <div class="flex-1 mt-1">
16
+ <h3 v-if="toast.title" class="font-semibold text-sm text-gray-800">
17
+ {{ toast.title }}
18
+ </h3>
19
+ <div class="text-sm dark:text-white text-gray-600" v-html="toast.message"></div>
20
+ </div>
21
+ <button
22
+ class="ml-3 text-gray-400 hover:text-gray-700 absolute top-2 right-2"
23
+ @click="$emit('close')"
24
+ >
25
+ <span class="sr-only">Cerrar</span>
26
+ <svg
27
+ class="size-4"
28
+ viewBox="0 0 24 24"
29
+ fill="none"
30
+ stroke="currentColor"
31
+ stroke-width="2"
32
+ stroke-linecap="round"
33
+ stroke-linejoin="round"
34
+ >
35
+ <path d="M18 6 6 18" />
36
+ <path d="m6 6 12 12" />
37
+ </svg>
38
+ </button>
39
+
40
+ <!-- Barra de progreso temporal -->
41
+ <div
42
+ v-if="toast.duration && toast.duration > 0"
43
+ class="absolute bottom-0 left-0 w-full h-1 bg-gray-200 dark:bg-gray-700"
44
+ >
45
+ <div
46
+ class="h-full bg-gradient-to-r"
47
+ :class="{
48
+ 'from-green-400 to-green-600': toast.severity === 'success',
49
+ 'from-red-400 to-red-600': toast.severity === 'danger',
50
+ 'from-yellow-400 to-yellow-600': toast.severity === 'warning',
51
+ 'from-blue-400 to-blue-600': toast.severity === 'info',
52
+ }"
53
+ :style="{ width: progressWidth + '%' }"
54
+ ></div>
55
+ </div>
56
+ </div>
57
+ </template>
58
+
59
+ <script setup>
60
+ import { ref, computed, watch, onMounted, onUnmounted } from 'vue'
61
+
62
+ const props = defineProps({ toast: Object });
63
+
64
+ const progressWidth = ref(100)
65
+ const startTime = ref(null)
66
+ let animationFrame = null
67
+
68
+ // Computed para detectar cambios en duration
69
+ const duration = computed(() => props.toast?.duration || 0)
70
+
71
+ // Función para actualizar el progreso
72
+ const updateProgress = () => {
73
+ if (!startTime.value || duration.value <= 0) {
74
+ progressWidth.value = 100
75
+ return
76
+ }
77
+
78
+ const elapsed = Date.now() - startTime.value
79
+ const remaining = Math.max(0, duration.value - elapsed)
80
+ progressWidth.value = (remaining / duration.value) * 100
81
+
82
+ if (remaining > 0) {
83
+ animationFrame = requestAnimationFrame(updateProgress)
84
+ }
85
+ }
86
+
87
+ // Watch para reiniciar cuando cambie el duration
88
+ watch(duration, (newDuration) => {
89
+ if (animationFrame) {
90
+ cancelAnimationFrame(animationFrame)
91
+ }
92
+
93
+ if (newDuration && newDuration > 0) {
94
+ startTime.value = Date.now()
95
+ updateProgress()
96
+ } else {
97
+ progressWidth.value = 100
98
+ }
99
+ }, { immediate: true })
100
+
101
+ onMounted(() => {
102
+ if (duration.value > 0) {
103
+ startTime.value = Date.now()
104
+ updateProgress()
105
+ }
106
+ })
107
+
108
+ onUnmounted(() => {
109
+ if (animationFrame) {
110
+ cancelAnimationFrame(animationFrame)
111
+ }
112
+ })
113
+ </script>
@@ -0,0 +1,34 @@
1
+ <script setup>
2
+ const toastStore = useToastStore()
3
+
4
+ const positions = [
5
+ 'top-left', 'top-center', 'top-right',
6
+ 'bottom-left', 'bottom-center', 'bottom-right',
7
+ ]
8
+
9
+ const positionClass = {
10
+ 'top-left': 'top-4 left-4',
11
+ 'top-center': 'top-4 left-1/2 -translate-x-1/2',
12
+ 'top-right': 'top-4 right-4',
13
+ 'bottom-left': 'bottom-4 left-4',
14
+ 'bottom-center': 'bottom-4 left-1/2 -translate-x-1/2',
15
+ 'bottom-right': 'bottom-4 right-4',
16
+ }
17
+ </script>
18
+
19
+ <template>
20
+ <template v-for="pos in positions" :key="pos">
21
+ <div
22
+ v-if="toastStore.toasts[pos]?.length"
23
+ class="fixed z-50 flex flex-col gap-2"
24
+ :class="positionClass[pos]"
25
+ >
26
+ <ToastAlert
27
+ v-for="toast in toastStore.toasts[pos]"
28
+ :key="toast.id"
29
+ :toast="toast"
30
+ @close="toastStore.remove(toast.id)"
31
+ />
32
+ </div>
33
+ </template>
34
+ </template>
@@ -0,0 +1,45 @@
1
+ <template>
2
+ <div
3
+ class="max-w-xs bg-white border border-gray-200 rounded-xl shadow-lg p-4 flex"
4
+ role="alert"
5
+ >
6
+ <div class="shrink-0">
7
+ <svg
8
+ class="size-5 text-gray-600 mt-1"
9
+ xmlns="http://www.w3.org/2000/svg"
10
+ width="24"
11
+ height="24"
12
+ viewBox="0 0 24 24"
13
+ fill="none"
14
+ stroke="currentColor"
15
+ stroke-width="2"
16
+ stroke-linecap="round"
17
+ stroke-linejoin="round"
18
+ >
19
+ <path d="M6 8a6 6 0 0 1 12 0c0 7 3 9 3 9H3s3-2 3-9" />
20
+ <path d="M10.3 21a1.94 1.94 0 0 0 3.4 0" />
21
+ </svg>
22
+ </div>
23
+ <div class="ms-4">
24
+ <h3 v-if="toast.title" class="text-gray-800 font-semibold">
25
+ {{ toast.title }}
26
+ </h3>
27
+ <div class="mt-1 text-sm text-gray-600" v-html="toast.message"></div>
28
+ <div v-if="toast.actions" class="mt-4">
29
+ <div class="flex gap-x-3">
30
+ <button
31
+ v-for="(action, i) in toast.actions"
32
+ :key="i"
33
+ @click="action.onClick"
34
+ class="text-blue-600 decoration-2 hover:underline font-medium text-sm focus:outline-hidden focus:underline"
35
+ >
36
+ {{ action.label }}
37
+ </button>
38
+ </div>
39
+ </div>
40
+ </div>
41
+ </div>
42
+ </template>
43
+ <script setup>
44
+ defineProps({ toast: Object });
45
+ </script>
@@ -0,0 +1,88 @@
1
+ <template>
2
+ <div
3
+ class="max-w-xs relative bg-white border border-gray-200 rounded-xl shadow-lg"
4
+ role="alert"
5
+ >
6
+ <div class="flex gap-x-3 p-4">
7
+ <div class="shrink-0">
8
+ <span
9
+ class="m-1 inline-flex justify-center items-center size-8 rounded-full bg-gray-100 text-gray-800"
10
+ >
11
+ <svg
12
+ class="shrink-0 size-4"
13
+ xmlns="http://www.w3.org/2000/svg"
14
+ width="24"
15
+ height="24"
16
+ viewBox="0 0 24 24"
17
+ fill="none"
18
+ stroke="currentColor"
19
+ stroke-width="2"
20
+ stroke-linecap="round"
21
+ stroke-linejoin="round"
22
+ >
23
+ <path
24
+ d="M4 14.899A7 7 0 1 1 15.71 8h1.79a4.5 4.5 0 0 1 2.5 8.242"
25
+ />
26
+ <path d="M12 12v9" />
27
+ <path d="m16 16-4-4-4 4" />
28
+ </svg>
29
+ </span>
30
+ <button
31
+ class="absolute top-3 end-3 inline-flex shrink-0 justify-center items-center size-5 rounded-lg text-gray-800 opacity-50 hover:opacity-100 focus:outline-hidden focus:opacity-100"
32
+ @click="$emit('close')"
33
+ aria-label="Close"
34
+ >
35
+ <span class="sr-only">Close</span>
36
+ <svg
37
+ class="shrink-0 size-4"
38
+ xmlns="http://www.w3.org/2000/svg"
39
+ width="24"
40
+ height="24"
41
+ viewBox="0 0 24 24"
42
+ fill="none"
43
+ stroke="currentColor"
44
+ stroke-width="2"
45
+ stroke-linecap="round"
46
+ stroke-linejoin="round"
47
+ >
48
+ <path d="M18 6 6 18" />
49
+ <path d="m6 6 12 12" />
50
+ </svg>
51
+ </button>
52
+ </div>
53
+ <div class="grow me-5">
54
+ <h3 v-if="toast.title" class="text-gray-800 font-medium text-sm">
55
+ {{ toast.title }}
56
+ </h3>
57
+ <div
58
+ v-if="toast.progress !== undefined"
59
+ class="mt-2 flex flex-col gap-x-3"
60
+ >
61
+ <span class="block mb-1.5 text-xs text-gray-500">{{
62
+ toast.progressLabel
63
+ }}</span>
64
+ <div
65
+ class="flex w-full h-1 bg-gray-200 rounded-full overflow-hidden"
66
+ role="progressbar"
67
+ :aria-valuenow="toast.progress"
68
+ aria-valuemin="0"
69
+ aria-valuemax="100"
70
+ >
71
+ <div
72
+ class="flex flex-col justify-center overflow-hidden bg-blue-600 text-xs text-white text-center whitespace-nowrap"
73
+ :style="{ width: toast.progress + '%' }"
74
+ ></div>
75
+ </div>
76
+ </div>
77
+ <div
78
+ v-else
79
+ class="mt-2 text-sm text-gray-600"
80
+ v-html="toast.message"
81
+ ></div>
82
+ </div>
83
+ </div>
84
+ </div>
85
+ </template>
86
+ <script setup>
87
+ defineProps({ toast: Object });
88
+ </script>
package/nuxt.config.ts ADDED
@@ -0,0 +1,15 @@
1
+ import tailwindcss from '@tailwindcss/vite'
2
+
3
+ export default defineNuxtConfig({
4
+ extends: ['@innertia-solutions/nuxt-core'],
5
+ css: ['@innertia-solutions/nuxt-theme-spark/spark.css'],
6
+ components: [
7
+ { path: './components', pathPrefix: true, prefix: '' },
8
+ ],
9
+ imports: {
10
+ dirs: ['shared/composables', 'shared/stores'],
11
+ },
12
+ vite: {
13
+ plugins: [tailwindcss()],
14
+ },
15
+ })
package/package.json ADDED
@@ -0,0 +1,45 @@
1
+ {
2
+ "name": "@innertia-solutions/nuxt-theme-spark",
3
+ "version": "0.1.11",
4
+ "description": "Innertia Solutions — Spark theme: backoffice, landing and mobile components and layouts",
5
+ "keywords": [
6
+ "nuxt",
7
+ "vue",
8
+ "theme",
9
+ "spark",
10
+ "backoffice",
11
+ "landing"
12
+ ],
13
+ "license": "MIT",
14
+ "repository": {
15
+ "type": "git",
16
+ "url": "https://github.com/innertia-solutions/innertia-ui-kit"
17
+ },
18
+ "publishConfig": {
19
+ "access": "public"
20
+ },
21
+ "main": "./nuxt.config.ts",
22
+ "exports": {
23
+ ".": "./nuxt.config.ts",
24
+ "./spark.css": "./spark.css"
25
+ },
26
+ "peerDependencies": {
27
+ "nuxt": ">=4.0.0",
28
+ "vue": ">=3.5.0"
29
+ },
30
+ "dependencies": {
31
+ "@innertia-solutions/nuxt-core": "^0.1.4",
32
+ "@tabler/icons-vue": "^3.44.0",
33
+ "preline": "^3.2.3",
34
+ "@tailwindcss/aspect-ratio": "^0.4.2",
35
+ "@tailwindcss/forms": "^0.5.10",
36
+ "@tailwindcss/vite": "^4.0.0",
37
+ "tailwindcss": "^4.0.0",
38
+ "vanilla-calendar-pro": "^3.1.0",
39
+ "uuid": "^13.0.0"
40
+ },
41
+ "devDependencies": {
42
+ "nuxt": "^4.4.2",
43
+ "vue": "^3.5.0"
44
+ }
45
+ }
@@ -0,0 +1,68 @@
1
+ declare global {
2
+ interface Window {
3
+ HSStaticMethods?: { autoInit?: () => void }
4
+ HSSelect?: new (el: HTMLElement) => { destroy?: () => void }
5
+ HSThemeAppearance?: { init?: () => void }
6
+ }
7
+ }
8
+
9
+ export default defineNuxtPlugin(async () => {
10
+ if (!process.client) return
11
+
12
+ try {
13
+ await import('preline')
14
+
15
+ const initPreline = () => {
16
+ try { window.HSStaticMethods?.autoInit?.() } catch (_) {}
17
+ try { window.HSThemeAppearance?.init?.() } catch (_) {}
18
+ }
19
+
20
+ const performMultipleInits = () => {
21
+ initPreline()
22
+ setTimeout(initPreline, 50)
23
+ setTimeout(initPreline, 200)
24
+ setTimeout(initPreline, 500)
25
+ }
26
+
27
+ if (document.readyState === 'loading') {
28
+ document.addEventListener('DOMContentLoaded', performMultipleInits)
29
+ } else {
30
+ nextTick(performMultipleInits)
31
+ }
32
+
33
+ const nuxtApp = useNuxtApp()
34
+ nuxtApp.hooks.hookOnce('app:mounted', () => performMultipleInits())
35
+ nuxtApp.hooks.hook('page:finish', () => requestAnimationFrame(performMultipleInits))
36
+
37
+ const observer = new MutationObserver((mutations) => {
38
+ const hasPreline = mutations.some(({ addedNodes }) =>
39
+ Array.from(addedNodes).some((node) => {
40
+ if (node.nodeType !== Node.ELEMENT_NODE) return false
41
+ const el = node as Element
42
+ return (
43
+ el.querySelector?.('[data-hs-overlay],[data-hs-dropdown],[data-hs-select]') ||
44
+ el.hasAttribute?.('data-hs-overlay') ||
45
+ el.hasAttribute?.('data-hs-dropdown') ||
46
+ el.hasAttribute?.('data-hs-select') ||
47
+ (typeof el.className === 'string' && el.className.includes('hs-'))
48
+ )
49
+ })
50
+ )
51
+ if (hasPreline) {
52
+ setTimeout(initPreline, 10)
53
+ setTimeout(initPreline, 100)
54
+ }
55
+ })
56
+
57
+ if (document.readyState === 'loading') {
58
+ document.addEventListener('DOMContentLoaded', () =>
59
+ observer.observe(document.body, { childList: true, subtree: true })
60
+ )
61
+ } else {
62
+ observer.observe(document.body, { childList: true, subtree: true })
63
+ }
64
+
65
+ } catch (e) {
66
+ console.warn('[nuxt-core] Error al cargar Preline:', e)
67
+ }
68
+ })
@@ -0,0 +1,119 @@
1
+ const rules = {
2
+ required: (value) => {
3
+ if (value === null || value === undefined) return 'Este campo es obligatorio'
4
+ if (typeof value === 'string' && value.trim() === '') return 'Este campo es obligatorio'
5
+ if (Array.isArray(value) && value.length === 0) return 'Este campo es obligatorio'
6
+ return true
7
+ },
8
+ email: (value) => /.+@.+\..+/.test(value) || 'El correo no es válido',
9
+ min: (value, arg) => value.length >= arg || `Debe tener al menos ${arg} caracteres`,
10
+ int: (value) => Number.isInteger(+value) || 'Debe ser un número entero',
11
+ rut: (value) => validateRut(value) || 'El RUT no es válido',
12
+ same: (value, arg, form) => value === form[arg] || 'Los campos no coinciden',
13
+ }
14
+
15
+ const dictionary = {
16
+ unique: 'Ya está registrado',
17
+ required: 'Este campo es obligatorio',
18
+ invalid: 'Dato inválido',
19
+ }
20
+
21
+ function validateRut(rut) {
22
+ if (!rut || typeof rut !== 'string') return false
23
+ rut = rut.replace(/^0+|[^0-9kK]+/g, '').toUpperCase()
24
+ if (rut.length < 8) return false
25
+ const body = rut.slice(0, -1)
26
+ const dv = rut.slice(-1)
27
+ let sum = 0, multiplier = 2
28
+ for (let i = body.length - 1; i >= 0; i--) {
29
+ sum += parseInt(body[i]) * multiplier
30
+ multiplier = multiplier < 7 ? multiplier + 1 : 2
31
+ }
32
+ const expected = 11 - (sum % 11)
33
+ const expectedDV = expected === 11 ? '0' : expected === 10 ? 'K' : expected.toString()
34
+ return dv === expectedDV
35
+ }
36
+
37
+ export function useForm(formDefinition, options = {}) {
38
+ const zodSchema = options.zodSchema
39
+ const form = reactive({})
40
+ const errors = reactive({})
41
+
42
+ for (const field in formDefinition) {
43
+ form[field] = formDefinition[field]?.value !== undefined ? formDefinition[field].value : ''
44
+ errors[field] = []
45
+ }
46
+
47
+ const reset = () => {
48
+ for (const field in formDefinition) {
49
+ form[field] = formDefinition[field]?.value !== undefined ? formDefinition[field].value : ''
50
+ errors[field] = []
51
+ }
52
+ }
53
+
54
+ const resetErrors = () => {
55
+ for (const field in formDefinition) {
56
+ errors[field] = []
57
+ }
58
+ }
59
+
60
+ const validateField = (field) => {
61
+ const def = formDefinition[field]
62
+ const value = form[field]
63
+ errors[field] = []
64
+ if (!def?.rules) return true
65
+ def.rules.forEach(rule => {
66
+ const ruleName = typeof rule === 'string' ? rule : rule.name
67
+ const arg = typeof rule === 'object' ? rule.arg : undefined
68
+ const result = rules[ruleName](value, arg, form)
69
+ if (result !== true) {
70
+ const custom = def.messages?.[ruleName]
71
+ errors[field].push(custom || result)
72
+ }
73
+ })
74
+ return errors[field].length === 0
75
+ }
76
+
77
+ const validateForm = () => {
78
+ if (zodSchema) {
79
+ const result = zodSchema.safeParse(form)
80
+ resetErrors()
81
+ if (!result.success) {
82
+ for (const issue of result.error.errors) {
83
+ const field = issue.path[0]
84
+ if (errors[field] !== undefined) errors[field].push(issue.message)
85
+ }
86
+ return false
87
+ }
88
+ return true
89
+ }
90
+ for (const field in formDefinition) validateField(field)
91
+ return Object.values(errors).every(e => e.length === 0)
92
+ }
93
+
94
+ const addError = (field, message) => {
95
+ if (message.startsWith('validation.')) {
96
+ const key = message.split('.')[1]
97
+ message = dictionary[key] || key
98
+ }
99
+ if (errors[field] !== undefined) errors[field].push(message)
100
+ }
101
+
102
+ const loadFromObject = (obj) => {
103
+ for (const field in formDefinition) {
104
+ if (obj[field] !== undefined) form[field] = obj[field]
105
+ }
106
+ }
107
+
108
+ return {
109
+ ...toRefs(form),
110
+ values: form,
111
+ errors,
112
+ validate: (field) => field ? validateField(field) : validateForm(),
113
+ reset,
114
+ resetErrors,
115
+ addError,
116
+ loadFromObject,
117
+ config: formDefinition,
118
+ }
119
+ }
@@ -0,0 +1,84 @@
1
+ // composables/useTable.ts
2
+
3
+ export function useTable() {
4
+ const invalidateCache = (tableName: string) => {
5
+ if (!tableName) {
6
+ console.warn('[useTable] No table name provided');
7
+ return;
8
+ }
9
+
10
+ const fullCacheKey = `table_cache_${tableName}`;
11
+
12
+ try {
13
+ sessionStorage.removeItem(fullCacheKey);
14
+ } catch (error) {
15
+ console.warn('[useTable] Error invalidating cache:', error);
16
+ }
17
+ };
18
+
19
+ const invalidateMultiple = (tableNames: string[]) => {
20
+ tableNames.forEach(name => invalidateCache(name));
21
+ };
22
+
23
+ const clearAllCache = () => {
24
+ try {
25
+ const keys = Object.keys(sessionStorage);
26
+ const tableCacheKeys = keys.filter(key => key.startsWith('table_cache_'));
27
+ tableCacheKeys.forEach(key => sessionStorage.removeItem(key));
28
+ } catch (error) {
29
+ console.warn('[useTable] Error clearing all cache:', error);
30
+ }
31
+ };
32
+
33
+ const useSearch = (tableName: string) => {
34
+ if (!tableName) {
35
+ throw new Error('[useTable] Table name is required for useSearch');
36
+ }
37
+
38
+ const searchCache = useState<Record<string, string>>("table-search-cache", () => ({}));
39
+ const search = ref(searchCache.value[tableName] || "");
40
+
41
+ watch(search, (newSearch, oldSearch) => {
42
+ searchCache.value[tableName] = newSearch;
43
+
44
+ if (oldSearch !== undefined && newSearch !== oldSearch) {
45
+ invalidateCache(tableName);
46
+ }
47
+ }, { immediate: true });
48
+
49
+ const clearSearch = () => { search.value = ""; };
50
+
51
+ return { search, clearSearch };
52
+ };
53
+
54
+ const clearAllSearches = () => {
55
+ const searchCache = useState<Record<string, string>>("table-search-cache", () => ({}));
56
+ searchCache.value = {};
57
+ };
58
+
59
+ const getSearchCache = () => {
60
+ const searchCache = useState<Record<string, string>>("table-search-cache", () => ({}));
61
+ return searchCache.value;
62
+ };
63
+
64
+ const useFilters = <T extends Record<string, any>>(tableName: string, initialFilters: T) => {
65
+ if (!tableName) {
66
+ throw new Error('[useTable] Table name is required for useFilters');
67
+ }
68
+
69
+ const filters = useState<T>(`table_filters_${tableName}`, () => ({ ...initialFilters }));
70
+ const resetFilters = () => { filters.value = { ...initialFilters }; };
71
+
72
+ return { filters, resetFilters };
73
+ };
74
+
75
+ return {
76
+ invalidateCache,
77
+ invalidateMultiple,
78
+ clearAllCache,
79
+ useSearch,
80
+ useFilters,
81
+ clearAllSearches,
82
+ getSearchCache
83
+ };
84
+ }
@@ -0,0 +1,69 @@
1
+ // app/composables/useToast.js
2
+ import { useToastStore } from '../stores/toast'
3
+
4
+ export function useToast() {
5
+ const toast = useToastStore()
6
+
7
+ return {
8
+ // Métodos originales
9
+ success: toast.success,
10
+ info: toast.info,
11
+ error: toast.error,
12
+ show: toast.show,
13
+ remove: toast.remove,
14
+ update: toast.update,
15
+ updateProgress: toast.updateProgress,
16
+ completeProcess: toast.completeProcess,
17
+
18
+ // Métodos rápidos para diferentes tipos de toast
19
+ alert: {
20
+ success: (message, config = {}) => toast.success({
21
+ type: 'alert',
22
+ message,
23
+ duration: 3000,
24
+ ...config
25
+ }),
26
+
27
+ error: (message, config = {}) => toast.error({
28
+ type: 'alert',
29
+ message,
30
+ duration: 5000,
31
+ ...config
32
+ }),
33
+
34
+ warning: (message, config = {}) => toast.show({
35
+ type: 'alert',
36
+ severity: 'warning',
37
+ icon: 'ti ti-alert-triangle',
38
+ title: 'Warning',
39
+ message,
40
+ duration: 4000,
41
+ ...config
42
+ }),
43
+
44
+ info: (message, config = {}) => toast.info({
45
+ type: 'alert',
46
+ message,
47
+ duration: 3000,
48
+ ...config
49
+ })
50
+ },
51
+
52
+ notification: (title, message, config = {}) => toast.show({
53
+ type: 'notification',
54
+ title,
55
+ message,
56
+ duration: 0, // Las notificaciones no se auto-dismiss por defecto
57
+ ...config
58
+ }),
59
+
60
+ process: (title, config = {}) => toast.show({
61
+ type: 'process',
62
+ title,
63
+ progress: 0,
64
+ progressLabel: 'Iniciando...',
65
+ duration: 0, // Los procesos no se auto-dismiss
66
+ ...config
67
+ })
68
+ }
69
+ }