@innertia-solutions/nuxt-theme-spark 0.1.85 → 0.1.87
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 +81 -58
- package/components/Admin/Page.vue +68 -2
- package/components/App/DevEnvironmentBar.vue +43 -0
- package/components/App/PageLoadingSpinner.vue +1 -1
- package/components/Layout/Admin.vue +51 -2
- package/components/Nav/Tabs.vue +18 -3
- package/components/Table/Database.vue +9 -5
- package/components/Table/DownloadDropdown.vue +4 -4
- package/components/Table/FilterDropdown.vue +3 -3
- package/components/Table/Grid.vue +9 -5
- package/components/Table/Kanban.vue +20 -3
- package/components/Table/List.vue +9 -4
- package/components/Table/Standard.vue +21 -15
- package/components/Table.vue +41 -21
- package/components/TableExportable.vue +147 -127
- package/package.json +1 -1
- package/public/favicon.png +0 -0
- package/public/icon.png +0 -0
- package/public/isologo-dark.png +0 -0
- package/public/isologo-light.png +0 -0
- package/spark.css +25 -20
- package/components/Admin/Header.vue +0 -32
|
@@ -1,73 +1,96 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
|
|
3
|
-
const open = () => { isOpen.value = true }
|
|
4
|
-
const close = () => { isOpen.value = false }
|
|
2
|
+
import * as icons from '@tabler/icons-vue'
|
|
5
3
|
|
|
6
|
-
|
|
4
|
+
const props = defineProps({
|
|
5
|
+
floating: { type: Boolean, default: false },
|
|
6
|
+
user: { type: Object as () => { name?: string; email?: string } | null, default: null },
|
|
7
|
+
})
|
|
8
|
+
|
|
9
|
+
const emit = defineEmits(['logout'])
|
|
10
|
+
|
|
11
|
+
const getInitials = (name?: string) => {
|
|
12
|
+
if (!name) return 'U'
|
|
13
|
+
const parts = name.split(' ')
|
|
14
|
+
return parts.length > 1 ? parts[0][0] + parts[1][0] : parts[0][0]
|
|
15
|
+
}
|
|
7
16
|
</script>
|
|
8
17
|
|
|
9
18
|
<template>
|
|
10
|
-
<
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
19
|
+
<aside
|
|
20
|
+
id="hs-pro-sidebar"
|
|
21
|
+
:class="[
|
|
22
|
+
'hs-overlay [--auto-close:lg]',
|
|
23
|
+
'hs-overlay-open:translate-x-0 -translate-x-full transition-transform duration-300 transform',
|
|
24
|
+
'w-65 hidden fixed z-60',
|
|
25
|
+
'lg:block lg:translate-x-0 lg:inset-e-auto',
|
|
26
|
+
floating
|
|
27
|
+
? 'inset-y-3 start-3 rounded-2xl shadow-sm border border-sidebar-line bg-sidebar'
|
|
28
|
+
: 'inset-y-0 inset-s-0 h-full border-e border-sidebar-line bg-sidebar lg:bottom-0',
|
|
29
|
+
]"
|
|
30
|
+
tabindex="-1"
|
|
31
|
+
aria-label="Sidebar"
|
|
32
|
+
>
|
|
33
|
+
<div class="relative flex flex-col h-full max-h-full pt-3">
|
|
34
|
+
<!-- Logo + close button (mobile) -->
|
|
35
|
+
<header class="h-11.5 ps-2 pe-2 lg:ps-5 flex items-center gap-x-1 shrink-0">
|
|
36
|
+
<slot name="logo" />
|
|
37
|
+
<div class="lg:hidden ms-auto">
|
|
38
|
+
<button
|
|
39
|
+
type="button"
|
|
40
|
+
class="w-6 h-7 inline-flex justify-center items-center gap-x-2 text-sm font-medium rounded-md bg-layer border border-layer-line text-layer-foreground shadow-2xs hover:bg-layer-hover disabled:opacity-50 disabled:pointer-events-none focus:outline-hidden focus:bg-layer-focus"
|
|
41
|
+
data-hs-overlay="#hs-pro-sidebar"
|
|
42
|
+
>
|
|
43
|
+
<svg class="shrink-0 size-4" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
44
|
+
<polyline points="7 8 3 12 7 16" />
|
|
45
|
+
<line x1="21" x2="11" y1="12" y2="12" />
|
|
46
|
+
<line x1="21" x2="11" y1="6" y2="6" />
|
|
47
|
+
<line x1="21" x2="11" y1="18" y2="18" />
|
|
48
|
+
</svg>
|
|
49
|
+
</button>
|
|
50
|
+
</div>
|
|
51
|
+
</header>
|
|
17
52
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
:class="[
|
|
23
|
-
'fixed inset-y-0 start-0 z-60 w-65 h-full',
|
|
24
|
-
'bg-white dark:bg-slate-800 border-e border-slate-200 dark:border-slate-700',
|
|
25
|
-
'transition-transform duration-300',
|
|
26
|
-
'lg:translate-x-0',
|
|
27
|
-
isOpen ? 'translate-x-0' : 'max-lg:-translate-x-full',
|
|
28
|
-
]"
|
|
29
|
-
>
|
|
30
|
-
<div class="flex flex-col h-full pt-3 lg:pt-6">
|
|
53
|
+
<!-- Search slot -->
|
|
54
|
+
<div v-if="$slots.search" class="px-4 py-2 shrink-0">
|
|
55
|
+
<slot name="search" />
|
|
56
|
+
</div>
|
|
31
57
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
<button type="button"
|
|
37
|
-
class="w-6 h-7 inline-flex justify-center items-center rounded-md border border-slate-200 bg-white text-slate-500 hover:bg-slate-50 dark:bg-slate-800 dark:border-slate-700 dark:text-slate-300 dark:hover:bg-slate-700"
|
|
38
|
-
@click="close">
|
|
39
|
-
<svg class="shrink-0 size-4" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"
|
|
40
|
-
fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
41
|
-
<polyline points="7 8 3 12 7 16" />
|
|
42
|
-
<line x1="21" x2="11" y1="12" y2="12" />
|
|
43
|
-
<line x1="21" x2="11" y1="6" y2="6" />
|
|
44
|
-
<line x1="21" x2="11" y1="18" y2="18" />
|
|
45
|
-
</svg>
|
|
46
|
-
</button>
|
|
47
|
-
</div>
|
|
48
|
-
</header>
|
|
58
|
+
<!-- Nav content - scrollable -->
|
|
59
|
+
<div class="flex-1 min-h-0 mt-1.5 overflow-y-auto [&::-webkit-scrollbar]:w-2 [&::-webkit-scrollbar-thumb]:rounded-full [&::-webkit-scrollbar-track]:bg-scrollbar-track [&::-webkit-scrollbar-thumb]:bg-scrollbar-thumb">
|
|
60
|
+
<slot name="menu" />
|
|
61
|
+
</div>
|
|
49
62
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
[&::-webkit-scrollbar-thumb]:bg-slate-300
|
|
56
|
-
dark:[&::-webkit-scrollbar-track]:bg-slate-700
|
|
57
|
-
dark:[&::-webkit-scrollbar-thumb]:bg-slate-500">
|
|
58
|
-
<slot name="menu" />
|
|
63
|
+
<!-- Sidebar footer -->
|
|
64
|
+
<div class="shrink-0">
|
|
65
|
+
<!-- Controls slot (dark mode, alerts, etc.) -->
|
|
66
|
+
<div v-if="$slots['user-controls']" class="flex items-center gap-x-2 px-3 pt-2.5">
|
|
67
|
+
<slot name="user-controls" />
|
|
59
68
|
</div>
|
|
60
69
|
|
|
61
|
-
<!-- User
|
|
62
|
-
<div class="
|
|
63
|
-
<
|
|
70
|
+
<!-- User info -->
|
|
71
|
+
<div class="flex items-center gap-x-2.5 px-3 py-5">
|
|
72
|
+
<div class="size-8 rounded-full bg-blue-600 text-white text-xs font-semibold flex items-center justify-center uppercase shrink-0">
|
|
73
|
+
{{ getInitials(user?.name) }}
|
|
74
|
+
</div>
|
|
75
|
+
<div class="min-w-0 flex-1">
|
|
76
|
+
<p class="text-sm font-medium text-slate-800 dark:text-slate-200 truncate leading-tight">{{ user?.name ?? 'Usuario' }}</p>
|
|
77
|
+
<p class="text-xs text-slate-400 dark:text-slate-500 truncate leading-tight">{{ user?.email ?? '—' }}</p>
|
|
78
|
+
</div>
|
|
79
|
+
<button type="button" @click="emit('logout')" title="Cerrar sesión"
|
|
80
|
+
class="shrink-0 inline-flex items-center justify-center size-7 rounded-lg text-slate-400 dark:text-slate-500 hover:bg-red-50 hover:text-red-500 dark:hover:bg-red-500/10 dark:hover:text-red-400 transition-colors"
|
|
81
|
+
>
|
|
82
|
+
<icons.IconLogout class="size-4" />
|
|
83
|
+
</button>
|
|
64
84
|
</div>
|
|
65
85
|
|
|
86
|
+
<!-- Env bar -->
|
|
87
|
+
<div class="px-3 pb-3">
|
|
88
|
+
<App.DevEnvironmentBar sidebar />
|
|
89
|
+
</div>
|
|
66
90
|
</div>
|
|
67
|
-
</
|
|
68
|
-
|
|
69
|
-
<!-- Main content -->
|
|
70
|
-
<slot />
|
|
91
|
+
</div>
|
|
92
|
+
</aside>
|
|
71
93
|
|
|
72
|
-
|
|
94
|
+
<!-- Main content -->
|
|
95
|
+
<slot />
|
|
73
96
|
</template>
|
|
@@ -1,7 +1,73 @@
|
|
|
1
|
-
<script setup
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import * as icons from '@tabler/icons-vue'
|
|
3
|
+
|
|
4
|
+
const props = defineProps({
|
|
5
|
+
title: { type: String, default: '' },
|
|
6
|
+
description: { type: String, default: '' },
|
|
7
|
+
icon: { type: String, default: '' },
|
|
8
|
+
color: { type: String, default: 'gray' },
|
|
9
|
+
sticky: { type: Boolean, default: true },
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
const colorMap: Record<string, { icon: string; bg: string; border: string }> = {
|
|
13
|
+
gray: { icon: 'text-slate-500 dark:text-slate-400', bg: 'bg-slate-100 dark:bg-slate-800', border: 'border-slate-200 dark:border-slate-700' },
|
|
14
|
+
blue: { icon: 'text-blue-600 dark:text-blue-400', bg: 'bg-blue-50 dark:bg-blue-500/10', border: 'border-blue-200 dark:border-blue-500/30' },
|
|
15
|
+
green: { icon: 'text-green-600 dark:text-green-400', bg: 'bg-green-50 dark:bg-green-500/10', border: 'border-green-200 dark:border-green-500/30' },
|
|
16
|
+
red: { icon: 'text-red-600 dark:text-red-400', bg: 'bg-red-50 dark:bg-red-500/10', border: 'border-red-200 dark:border-red-500/30' },
|
|
17
|
+
amber: { icon: 'text-amber-600 dark:text-amber-400', bg: 'bg-amber-50 dark:bg-amber-500/10', border: 'border-amber-200 dark:border-amber-500/30' },
|
|
18
|
+
purple: { icon: 'text-purple-600 dark:text-purple-400', bg: 'bg-purple-50 dark:bg-purple-500/10', border: 'border-purple-200 dark:border-purple-500/30' },
|
|
19
|
+
teal: { icon: 'text-teal-600 dark:text-teal-400', bg: 'bg-teal-50 dark:bg-teal-500/10', border: 'border-teal-200 dark:border-teal-500/30' },
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const accent = computed(() => colorMap[props.color] ?? colorMap.gray)
|
|
23
|
+
const iconComponent = computed(() => props.icon ? (icons as any)[props.icon] : null)
|
|
24
|
+
</script>
|
|
2
25
|
|
|
3
26
|
<template>
|
|
4
|
-
<div class="
|
|
27
|
+
<div class="space-y-4">
|
|
28
|
+
|
|
29
|
+
<!-- Page header card -->
|
|
30
|
+
<div :class="sticky ? 'sticky top-3 z-20' : ''">
|
|
31
|
+
<div class="flex items-center justify-between bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-800 rounded-2xl shadow-sm px-4 py-3">
|
|
32
|
+
<div class="flex items-center gap-x-4">
|
|
33
|
+
|
|
34
|
+
<!-- Icon -->
|
|
35
|
+
<div v-if="iconComponent"
|
|
36
|
+
class="shrink-0 size-10 rounded-xl flex items-center justify-center border"
|
|
37
|
+
:class="[accent.bg, accent.border]"
|
|
38
|
+
>
|
|
39
|
+
<component :is="iconComponent" class="size-5" :class="accent.icon" />
|
|
40
|
+
</div>
|
|
41
|
+
|
|
42
|
+
<!-- Title + description -->
|
|
43
|
+
<div>
|
|
44
|
+
<div class="flex items-baseline gap-x-2">
|
|
45
|
+
<h1 class="text-lg font-semibold text-slate-800 dark:text-slate-100">{{ title }}</h1>
|
|
46
|
+
<template v-if="description">
|
|
47
|
+
<span class="size-1 rounded-full bg-slate-300 dark:bg-slate-600 shrink-0 self-center"></span>
|
|
48
|
+
<p class="text-sm text-slate-400 dark:text-slate-500">{{ description }}</p>
|
|
49
|
+
</template>
|
|
50
|
+
</div>
|
|
51
|
+
<div v-if="$slots.breadcrumb" class="flex items-center gap-x-1 mt-0.5">
|
|
52
|
+
<slot name="breadcrumb" />
|
|
53
|
+
</div>
|
|
54
|
+
</div>
|
|
55
|
+
</div>
|
|
56
|
+
|
|
57
|
+
<!-- Actions -->
|
|
58
|
+
<div v-if="$slots.actions" class="flex items-center gap-x-2">
|
|
59
|
+
<slot name="actions" />
|
|
60
|
+
</div>
|
|
61
|
+
</div>
|
|
62
|
+
</div>
|
|
63
|
+
|
|
64
|
+
<!-- Tabs -->
|
|
65
|
+
<div v-if="$slots.tabs">
|
|
66
|
+
<slot name="tabs" :color="color" />
|
|
67
|
+
</div>
|
|
68
|
+
|
|
69
|
+
<!-- Content -->
|
|
5
70
|
<slot />
|
|
71
|
+
|
|
6
72
|
</div>
|
|
7
73
|
</template>
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
const props = defineProps({ sidebar: { type: Boolean, default: false } })
|
|
3
|
+
|
|
4
|
+
const config = useRuntimeConfig()
|
|
5
|
+
const appEnv = config.public.appEnv
|
|
6
|
+
|
|
7
|
+
const isVisible = computed(() => appEnv && appEnv !== 'production')
|
|
8
|
+
|
|
9
|
+
useHead({
|
|
10
|
+
htmlAttrs: { class: (!props.sidebar && isVisible.value) ? 'has-env-bar' : '' },
|
|
11
|
+
style: [{ children: (!props.sidebar && isVisible.value) ? ':root { --env-bar-height: 1.5rem; }' : ':root { --env-bar-height: 0px; }' }],
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
const envColor = computed(() => {
|
|
15
|
+
const env = appEnv.toLowerCase()
|
|
16
|
+
if (env === 'staging') return props.sidebar ? 'bg-amber-50 text-amber-600 border border-amber-200 dark:bg-amber-500/10 dark:text-amber-400 dark:border-amber-500/30' : 'bg-amber-500 text-white'
|
|
17
|
+
if (env === 'local' || env === 'dev') return props.sidebar ? 'bg-blue-50 text-blue-600 border border-blue-200 dark:bg-blue-500/10 dark:text-blue-400 dark:border-blue-500/30' : 'bg-blue-600 text-white'
|
|
18
|
+
return props.sidebar ? 'bg-red-50 text-red-600 border border-red-200 dark:bg-red-500/10 dark:text-red-400 dark:border-red-500/30' : 'bg-red-600 text-white'
|
|
19
|
+
})
|
|
20
|
+
</script>
|
|
21
|
+
|
|
22
|
+
<template>
|
|
23
|
+
<!-- Sidebar inline variant -->
|
|
24
|
+
<div v-if="isVisible && sidebar"
|
|
25
|
+
:class="['w-full rounded-lg px-3 py-1.5 flex items-center justify-center gap-x-1.5 select-none', envColor]"
|
|
26
|
+
>
|
|
27
|
+
<span class="text-[9px] font-black tracking-widest uppercase">ENTORNO: {{ appEnv }}</span>
|
|
28
|
+
</div>
|
|
29
|
+
|
|
30
|
+
<!-- Full-width fixed bar -->
|
|
31
|
+
<div v-else-if="isVisible && !sidebar"
|
|
32
|
+
class="fixed bottom-0 left-0 right-0 z-[100] h-6 overflow-hidden select-none pointer-events-none"
|
|
33
|
+
>
|
|
34
|
+
<div
|
|
35
|
+
class="flex items-center justify-center w-full h-full text-[10px] font-bold tracking-widest uppercase opacity-90 shadow-lg border-t border-white/10"
|
|
36
|
+
:class="envColor"
|
|
37
|
+
>
|
|
38
|
+
<span class="mr-2">━━━━━</span>
|
|
39
|
+
ENTORNO: {{ appEnv }}
|
|
40
|
+
<span class="ml-2">━━━━━</span>
|
|
41
|
+
</div>
|
|
42
|
+
</div>
|
|
43
|
+
</template>
|
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
<script setup>
|
|
2
|
+
const props = defineProps({
|
|
3
|
+
floating: { type: Boolean, default: false },
|
|
4
|
+
})
|
|
5
|
+
|
|
2
6
|
const showAnimation = ref(false)
|
|
3
7
|
onMounted(() => {
|
|
4
8
|
const seen = sessionStorage.getItem('auth-entered')
|
|
@@ -11,12 +15,57 @@ onMounted(() => {
|
|
|
11
15
|
|
|
12
16
|
<template>
|
|
13
17
|
<div :class="{ 'animate-entrance': showAnimation }">
|
|
14
|
-
|
|
18
|
+
<!-- Header - matches Preline exactly -->
|
|
19
|
+
<header class="lg:ms-65 fixed top-0 inset-x-0 flex flex-wrap md:justify-start md:flex-nowrap z-50 bg-navbar border-b border-navbar-line">
|
|
20
|
+
<div class="flex justify-between xl:grid xl:grid-cols-3 basis-full items-center w-full py-2.5 px-2 sm:px-5">
|
|
21
|
+
|
|
22
|
+
<!-- Left col: mobile toggle + desktop search -->
|
|
23
|
+
<div class="xl:col-span-1 flex items-center md:gap-x-3">
|
|
24
|
+
<!-- Mobile sidebar toggle -->
|
|
25
|
+
<div class="lg:hidden">
|
|
26
|
+
<button
|
|
27
|
+
type="button"
|
|
28
|
+
class="w-7 h-9.5 inline-flex justify-center items-center gap-x-2 text-sm font-medium rounded-lg bg-layer border border-layer-line text-layer-foreground shadow-2xs hover:bg-layer-hover disabled:opacity-50 disabled:pointer-events-none focus:outline-hidden focus:bg-layer-focus"
|
|
29
|
+
aria-haspopup="dialog"
|
|
30
|
+
aria-expanded="false"
|
|
31
|
+
aria-controls="hs-pro-sidebar"
|
|
32
|
+
aria-label="Toggle navigation"
|
|
33
|
+
data-hs-overlay="#hs-pro-sidebar"
|
|
34
|
+
>
|
|
35
|
+
<svg class="shrink-0 size-4" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
36
|
+
<path d="M17 8L21 12L17 16M3 12H13M3 6H13M3 18H13" />
|
|
37
|
+
</svg>
|
|
38
|
+
</button>
|
|
39
|
+
</div>
|
|
40
|
+
|
|
41
|
+
<!-- Desktop search -->
|
|
42
|
+
<div class="hidden lg:block min-w-80 xl:w-full">
|
|
43
|
+
<slot name="header-left" />
|
|
44
|
+
</div>
|
|
45
|
+
</div>
|
|
46
|
+
|
|
47
|
+
<!-- Right col: actions -->
|
|
48
|
+
<div class="xl:col-span-2 flex justify-end items-center gap-x-2">
|
|
49
|
+
<div class="flex items-center">
|
|
50
|
+
<!-- Mobile search icon -->
|
|
51
|
+
<div class="lg:hidden">
|
|
52
|
+
<slot name="header-search-mobile" />
|
|
53
|
+
</div>
|
|
54
|
+
|
|
55
|
+
<slot name="header-right" />
|
|
56
|
+
</div>
|
|
57
|
+
</div>
|
|
58
|
+
|
|
59
|
+
</div>
|
|
60
|
+
</header>
|
|
61
|
+
|
|
62
|
+
<!-- Sidebar + main content -->
|
|
63
|
+
<AdminBase :floating="floating">
|
|
15
64
|
<template #logo><slot name="logo" /></template>
|
|
16
65
|
<template #menu><slot name="menu" /></template>
|
|
17
66
|
<template #user-footer><slot name="user-footer" /></template>
|
|
18
67
|
|
|
19
|
-
<div class="lg:ps-65">
|
|
68
|
+
<div :class="floating ? 'lg:ps-70 pt-14' : 'lg:ps-65 pt-14'">
|
|
20
69
|
<slot />
|
|
21
70
|
</div>
|
|
22
71
|
</AdminBase>
|
package/components/Nav/Tabs.vue
CHANGED
|
@@ -10,11 +10,26 @@ interface Tab {
|
|
|
10
10
|
|
|
11
11
|
const props = withDefaults(defineProps<{
|
|
12
12
|
tabs: Tab[]
|
|
13
|
+
color?: string
|
|
13
14
|
activeClass?: string
|
|
14
15
|
}>(), {
|
|
15
|
-
|
|
16
|
+
color: 'blue',
|
|
16
17
|
})
|
|
17
18
|
|
|
19
|
+
const colorMap: Record<string, string> = {
|
|
20
|
+
gray: 'bg-white dark:bg-slate-800 text-slate-700 dark:text-slate-200 shadow-sm',
|
|
21
|
+
blue: 'bg-white dark:bg-slate-800 text-blue-600 dark:text-blue-400 shadow-sm',
|
|
22
|
+
green: 'bg-white dark:bg-slate-800 text-green-600 dark:text-green-400 shadow-sm',
|
|
23
|
+
red: 'bg-white dark:bg-slate-800 text-red-600 dark:text-red-400 shadow-sm',
|
|
24
|
+
amber: 'bg-white dark:bg-slate-800 text-amber-600 dark:text-amber-400 shadow-sm',
|
|
25
|
+
purple: 'bg-white dark:bg-slate-800 text-purple-600 dark:text-purple-400 shadow-sm',
|
|
26
|
+
teal: 'bg-white dark:bg-slate-800 text-teal-600 dark:text-teal-400 shadow-sm',
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const resolvedActiveClass = computed(() =>
|
|
30
|
+
props.activeClass ?? colorMap[props.color] ?? colorMap.blue
|
|
31
|
+
)
|
|
32
|
+
|
|
18
33
|
const route = useRoute()
|
|
19
34
|
|
|
20
35
|
const isActive = (tab: Tab) =>
|
|
@@ -22,14 +37,14 @@ const isActive = (tab: Tab) =>
|
|
|
22
37
|
</script>
|
|
23
38
|
|
|
24
39
|
<template>
|
|
25
|
-
<div class="flex items-center gap-x-1 p-1 bg-slate-100 dark:bg-slate-900/50 rounded-xl w-fit border border-slate-200 dark:border-slate-
|
|
40
|
+
<div class="flex items-center gap-x-1 p-1 bg-slate-100 dark:bg-slate-900/50 rounded-xl w-fit border border-slate-200 dark:border-slate-800">
|
|
26
41
|
<NuxtLink
|
|
27
42
|
v-for="tab in tabs"
|
|
28
43
|
:key="tab.to"
|
|
29
44
|
:to="tab.to"
|
|
30
45
|
class="flex items-center gap-x-2 px-4 py-2 text-xs font-bold rounded-lg transition-all"
|
|
31
46
|
:class="isActive(tab)
|
|
32
|
-
?
|
|
47
|
+
? resolvedActiveClass
|
|
33
48
|
: 'text-slate-500 dark:text-slate-400 hover:text-slate-700 dark:hover:text-slate-200'"
|
|
34
49
|
>
|
|
35
50
|
<component :is="tab.icon" v-if="tab.icon" class="size-4" />
|
|
@@ -3,9 +3,10 @@ import { IconSearch, IconLoader2, IconCheck, IconX } from '@tabler/icons-vue'
|
|
|
3
3
|
|
|
4
4
|
// Dense database view with inline cell editing — click cell → input → blur → mutation
|
|
5
5
|
const props = defineProps({
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
table: { type: Object, default: null },
|
|
7
|
+
endpoint: { type: String, default: undefined },
|
|
8
|
+
columns: { type: Array, required: true }, // [{ key, label, editable?, type?: 'text'|'number'|'select', options?: [] }]
|
|
9
|
+
name: { type: String, default: undefined },
|
|
9
10
|
params: { type: Object, default: () => ({}) },
|
|
10
11
|
updateMutation: { type: Function, default: null }, // (id, field, value) => Promise
|
|
11
12
|
cached: { type: Boolean, default: false },
|
|
@@ -15,6 +16,9 @@ const props = defineProps({
|
|
|
15
16
|
|
|
16
17
|
const emit = defineEmits(['row-click', 'cell-save'])
|
|
17
18
|
|
|
19
|
+
const resolvedEndpoint = computed(() => props.table?.endpoint ?? props.endpoint)
|
|
20
|
+
const resolvedName = computed(() => props.table?.name ?? props.name)
|
|
21
|
+
|
|
18
22
|
const tableRef = ref(null)
|
|
19
23
|
|
|
20
24
|
// ─── Inline editing ───────────────────────────────────────────────────────────
|
|
@@ -97,9 +101,9 @@ defineExpose({ reload })
|
|
|
97
101
|
<div class="overflow-x-auto border border-slate-200 dark:border-slate-700 rounded-xl">
|
|
98
102
|
<Table
|
|
99
103
|
ref="tableRef"
|
|
100
|
-
:endpoint="
|
|
104
|
+
:endpoint="resolvedEndpoint"
|
|
101
105
|
:columns="columns"
|
|
102
|
-
:name="
|
|
106
|
+
:name="resolvedName"
|
|
103
107
|
:params="mergedParams"
|
|
104
108
|
:search="search"
|
|
105
109
|
:cached="cached"
|
|
@@ -31,7 +31,7 @@ const exportTable = (format) => {
|
|
|
31
31
|
<button
|
|
32
32
|
id="hs-as-table-table-export-dropdown"
|
|
33
33
|
type="button"
|
|
34
|
-
class="py-1.5 sm:py-2 px-2.5 inline-flex items-center gap-x-1.5 text-sm sm:text-xs font-medium rounded-lg border border-slate-200 bg-white text-slate-800 shadow-2xs hover:bg-slate-50 disabled:opacity-50 disabled:pointer-events-none focus:outline-hidden focus:bg-slate-50 dark:bg-slate-
|
|
34
|
+
class="py-1.5 sm:py-2 px-2.5 inline-flex items-center gap-x-1.5 text-sm sm:text-xs font-medium rounded-lg border border-slate-200 bg-white text-slate-800 shadow-2xs hover:bg-slate-50 disabled:opacity-50 disabled:pointer-events-none focus:outline-hidden focus:bg-slate-50 dark:bg-slate-900 dark:border-slate-800 dark:text-slate-300 dark:hover:bg-slate-700 dark:focus:bg-slate-700"
|
|
35
35
|
aria-haspopup="menu"
|
|
36
36
|
aria-expanded="false"
|
|
37
37
|
aria-label="Dropdown"
|
|
@@ -40,7 +40,7 @@ const exportTable = (format) => {
|
|
|
40
40
|
Exportar
|
|
41
41
|
</button>
|
|
42
42
|
<div
|
|
43
|
-
class="hs-dropdown-menu transition-[opacity,margin] duration hs-dropdown-open:opacity-100 opacity-0 hidden divide-y divide-slate-200 min-w-48 z-10 bg-white shadow-md rounded-lg p-2 mt-2 dark:divide-slate-
|
|
43
|
+
class="hs-dropdown-menu transition-[opacity,margin] duration hs-dropdown-open:opacity-100 opacity-0 hidden divide-y divide-slate-200 min-w-48 z-10 bg-white shadow-md rounded-lg p-2 mt-2 dark:divide-slate-800 dark:bg-slate-900 dark:border dark:border-slate-800 border-t border-slate-200 dark:border-slate-800"
|
|
44
44
|
role="menu"
|
|
45
45
|
aria-orientation="vertical"
|
|
46
46
|
aria-labelledby="hs-as-table-table-export-dropdown"
|
|
@@ -90,7 +90,7 @@ const exportTable = (format) => {
|
|
|
90
90
|
<input
|
|
91
91
|
type="checkbox"
|
|
92
92
|
v-model="exportAllPages"
|
|
93
|
-
class="shrink-0 border-slate-300 rounded-sm text-blue-600 focus:ring-blue-500 dark:bg-slate-
|
|
93
|
+
class="shrink-0 border-slate-300 rounded-sm text-blue-600 focus:ring-blue-500 dark:bg-slate-900 dark:border-slate-600"
|
|
94
94
|
/>
|
|
95
95
|
Todas las páginas
|
|
96
96
|
</label>
|
|
@@ -101,7 +101,7 @@ const exportTable = (format) => {
|
|
|
101
101
|
<input
|
|
102
102
|
type="checkbox"
|
|
103
103
|
v-model="exportFilteredRows"
|
|
104
|
-
class="shrink-0 border-slate-300 rounded-sm text-blue-600 focus:ring-blue-500 dark:bg-slate-
|
|
104
|
+
class="shrink-0 border-slate-300 rounded-sm text-blue-600 focus:ring-blue-500 dark:bg-slate-900 dark:border-slate-600"
|
|
105
105
|
/>
|
|
106
106
|
Solo filas filtradas
|
|
107
107
|
</label>
|
|
@@ -55,8 +55,8 @@ const displayText = computed(() => {
|
|
|
55
55
|
|
|
56
56
|
const selectClasses = computed(() => {
|
|
57
57
|
const base =
|
|
58
|
-
"relative w-full rounded-lg border bg-white dark:bg-slate-
|
|
59
|
-
const validation = "border-gray-200 dark:border-slate-
|
|
58
|
+
"relative w-full rounded-lg border bg-white dark:bg-slate-900 transition-colors cursor-pointer text-slate-900 dark:text-white py-2 px-3 text-sm focus:outline-none focus:ring-0 focus:border-gray-400";
|
|
59
|
+
const validation = "border-gray-200 dark:border-slate-800";
|
|
60
60
|
const disabled = props.disabled ? "opacity-50 cursor-not-allowed" : "";
|
|
61
61
|
return `${base} ${validation} ${disabled}`;
|
|
62
62
|
});
|
|
@@ -169,7 +169,7 @@ onUnmounted(() => document.removeEventListener("mousedown", handleClickOutside))
|
|
|
169
169
|
<div
|
|
170
170
|
v-show="isOpen"
|
|
171
171
|
:class="[
|
|
172
|
-
'absolute z-50 w-full mt-1 bg-white dark:bg-slate-
|
|
172
|
+
'absolute z-50 w-full mt-1 bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-800 rounded-xl shadow-xl max-h-60 overflow-auto',
|
|
173
173
|
menuClass,
|
|
174
174
|
]"
|
|
175
175
|
>
|
|
@@ -3,9 +3,10 @@ import { IconSearch } from '@tabler/icons-vue'
|
|
|
3
3
|
|
|
4
4
|
// Grid / card layout view — wraps DataTable with viewMode="grid"
|
|
5
5
|
const props = defineProps({
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
table: { type: Object, default: null },
|
|
7
|
+
endpoint: { type: String, default: undefined },
|
|
8
|
+
columns: { type: Array, required: true },
|
|
9
|
+
name: { type: String, default: undefined },
|
|
9
10
|
params: { type: Object, default: () => ({}) },
|
|
10
11
|
cached: { type: Boolean, default: true },
|
|
11
12
|
searchPlaceholder: { type: String, default: 'Buscar...' },
|
|
@@ -16,6 +17,9 @@ const props = defineProps({
|
|
|
16
17
|
|
|
17
18
|
const emit = defineEmits(['row-click', 'loaded'])
|
|
18
19
|
|
|
20
|
+
const resolvedEndpoint = computed(() => props.table?.endpoint ?? props.endpoint)
|
|
21
|
+
const resolvedName = computed(() => props.table?.name ?? props.name)
|
|
22
|
+
|
|
19
23
|
const search = ref('')
|
|
20
24
|
const tableRef = ref(null)
|
|
21
25
|
|
|
@@ -41,9 +45,9 @@ defineExpose({ reload, clearCache })
|
|
|
41
45
|
|
|
42
46
|
<DataTable
|
|
43
47
|
ref="tableRef"
|
|
44
|
-
:endpoint="
|
|
48
|
+
:endpoint="resolvedEndpoint"
|
|
45
49
|
:columns="columns"
|
|
46
|
-
:name="
|
|
50
|
+
:name="resolvedName"
|
|
47
51
|
:params="params"
|
|
48
52
|
:search="search"
|
|
49
53
|
:cached="cached"
|
|
@@ -6,8 +6,9 @@ import { useQueryClient } from '@tanstack/vue-query'
|
|
|
6
6
|
// Usage: <TableKanban endpoint="..." :states="[{key:'todo',label:'Pendiente',color:'slate'}]" state-key="status" ... />
|
|
7
7
|
|
|
8
8
|
const props = defineProps({
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
table: { type: Object, default: null },
|
|
10
|
+
endpoint: { type: String, default: undefined },
|
|
11
|
+
name: { type: String, default: undefined },
|
|
11
12
|
params: { type: Object, default: () => ({}) },
|
|
12
13
|
stateKey: { type: String, default: 'status' }, // field in row that holds the state key
|
|
13
14
|
states: { // column definitions
|
|
@@ -25,6 +26,22 @@ const emit = defineEmits(['move', 'card-click'])
|
|
|
25
26
|
const api = useApi()
|
|
26
27
|
const queryClient = useQueryClient()
|
|
27
28
|
|
|
29
|
+
const resolvedEndpoint = computed(() => props.table?.endpoint ?? props.endpoint)
|
|
30
|
+
const resolvedName = computed(() => props.table?.name ?? props.name)
|
|
31
|
+
|
|
32
|
+
// ─── TanStack invalidation signal ─────────────────────────────────────────────
|
|
33
|
+
const { data: _invalidateSignal } = useQuery({
|
|
34
|
+
queryKey: computed(() => ['table', resolvedName.value]),
|
|
35
|
+
queryFn: () => Date.now(),
|
|
36
|
+
initialData: 0,
|
|
37
|
+
staleTime: Infinity,
|
|
38
|
+
refetchOnWindowFocus: false,
|
|
39
|
+
refetchOnMount: false,
|
|
40
|
+
})
|
|
41
|
+
let _signalReady = false
|
|
42
|
+
onMounted(() => { _signalReady = true })
|
|
43
|
+
watch(_invalidateSignal, () => { if (_signalReady) fetchAll() })
|
|
44
|
+
|
|
28
45
|
// ─── Fetch all rows ───────────────────────────────────────────────────────────
|
|
29
46
|
const loading = ref(false)
|
|
30
47
|
const rows = ref([])
|
|
@@ -32,7 +49,7 @@ const rows = ref([])
|
|
|
32
49
|
const fetchAll = async () => {
|
|
33
50
|
loading.value = true
|
|
34
51
|
try {
|
|
35
|
-
const res = await api.post(
|
|
52
|
+
const res = await api.post(resolvedEndpoint.value, { perPage: props.perPage, ...props.params })
|
|
36
53
|
rows.value = Array.isArray(res?.data) ? res.data : (Array.isArray(res) ? res : [])
|
|
37
54
|
} catch (e) {
|
|
38
55
|
console.error('[Kanban] fetch error', e)
|
|
@@ -4,8 +4,9 @@ import { useInfiniteQuery } from '@tanstack/vue-query'
|
|
|
4
4
|
|
|
5
5
|
// Infinite scroll list — fetches next page when sentinel enters viewport
|
|
6
6
|
const props = defineProps({
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
table: { type: Object, default: null },
|
|
8
|
+
endpoint: { type: String, default: undefined },
|
|
9
|
+
name: { type: String, default: undefined },
|
|
9
10
|
params: { type: Object, default: () => ({}) },
|
|
10
11
|
searchPlaceholder: { type: String, default: 'Buscar...' },
|
|
11
12
|
showSearch: { type: Boolean, default: true },
|
|
@@ -15,6 +16,10 @@ const props = defineProps({
|
|
|
15
16
|
const emit = defineEmits(['row-click'])
|
|
16
17
|
|
|
17
18
|
const api = useApi()
|
|
19
|
+
|
|
20
|
+
const resolvedEndpoint = computed(() => props.table?.endpoint ?? props.endpoint)
|
|
21
|
+
const resolvedName = computed(() => props.table?.name ?? props.name)
|
|
22
|
+
|
|
18
23
|
const search = ref('')
|
|
19
24
|
const sentinel = ref(null)
|
|
20
25
|
|
|
@@ -25,12 +30,12 @@ watch(search, (v) => {
|
|
|
25
30
|
searchTimeout = setTimeout(() => { debouncedSearch.value = v }, 400)
|
|
26
31
|
})
|
|
27
32
|
|
|
28
|
-
const queryKey = computed(() => [
|
|
33
|
+
const queryKey = computed(() => ['table', resolvedName.value, 'infinite', debouncedSearch.value, props.params])
|
|
29
34
|
|
|
30
35
|
const { data, fetchNextPage, hasNextPage, isFetchingNextPage, isLoading, refetch } = useInfiniteQuery({
|
|
31
36
|
queryKey,
|
|
32
37
|
queryFn: ({ pageParam = 1 }) =>
|
|
33
|
-
api.post(
|
|
38
|
+
api.post(resolvedEndpoint.value, {
|
|
34
39
|
search: debouncedSearch.value,
|
|
35
40
|
page: pageParam,
|
|
36
41
|
perPage: props.perPage,
|