@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.
- package/components/Admin/Base.vue +73 -0
- package/components/Admin/Header.vue +32 -0
- package/components/Admin/Page.vue +5 -0
- package/components/Admin/PageHeader.vue +18 -0
- package/components/App/Button.vue +59 -0
- package/components/App/Dropdown.vue +286 -0
- package/components/App/EmptyState.vue +433 -0
- package/components/App/LoadingState.vue +40 -0
- package/components/App/PageLoadingSpinner.vue +118 -0
- package/components/App/SwitchColorTheme.vue +55 -0
- package/components/App/Tag.vue +193 -0
- package/components/Forms/DatePicker.vue +255 -0
- package/components/Forms/Input.vue +72 -0
- package/components/Forms/Select.vue +100 -0
- package/components/Forms/SelectServer.vue +726 -0
- package/components/Layout/Admin.vue +32 -0
- package/components/Layout/Auth.vue +29 -0
- package/components/Modal/DeleteConfirm.vue +48 -0
- package/components/Modal.vue +103 -0
- package/components/Nav/Tabs.vue +39 -0
- package/components/Table/DownloadDropdown.vue +111 -0
- package/components/Table/FilterDropdown.vue +226 -0
- package/components/Toast/Alert.vue +113 -0
- package/components/Toast/Container.vue +34 -0
- package/components/Toast/Notification.vue +45 -0
- package/components/Toast/Process.vue +88 -0
- package/nuxt.config.ts +15 -0
- package/package.json +45 -0
- package/plugins/preline.client.ts +68 -0
- package/shared/composables/useForm.js +119 -0
- package/shared/composables/useTable.ts +84 -0
- package/shared/composables/useToast.js +69 -0
- package/shared/stores/toast.js +129 -0
- 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
|
+
}
|