@innertia-solutions/nuxt-theme-spark 0.1.87 → 0.1.89
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 +58 -81
- package/components/Admin/Header.vue +32 -0
- package/components/Admin/Page.vue +2 -68
- package/components/App/PageLoadingSpinner.vue +1 -1
- package/components/Layout/Admin.vue +2 -51
- package/components/Nav/Tabs.vue +3 -18
- package/components/Table/Database.vue +5 -9
- package/components/Table/DownloadDropdown.vue +4 -4
- package/components/Table/FilterDropdown.vue +3 -3
- package/components/Table/Grid.vue +5 -9
- package/components/Table/Kanban.vue +3 -20
- package/components/Table/List.vue +4 -9
- package/components/Table/Standard.vue +26 -26
- package/components/Table.vue +21 -41
- package/components/TableExportable.vue +127 -147
- package/package.json +1 -1
- package/spark.css +20 -25
- 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
|
@@ -1,96 +1,73 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
|
|
2
|
+
const isOpen = ref(false)
|
|
3
|
+
const open = () => { isOpen.value = true }
|
|
4
|
+
const close = () => { isOpen.value = false }
|
|
3
5
|
|
|
4
|
-
|
|
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
|
-
}
|
|
6
|
+
provide('vantage:sidebar', { isOpen, open, close })
|
|
16
7
|
</script>
|
|
17
8
|
|
|
18
9
|
<template>
|
|
19
|
-
<
|
|
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>
|
|
10
|
+
<div class="bg-slate-50 dark:bg-slate-900 min-h-screen">
|
|
52
11
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
12
|
+
<!-- Backdrop mobile -->
|
|
13
|
+
<Transition enter-from-class="opacity-0" enter-active-class="transition-opacity duration-300"
|
|
14
|
+
leave-to-class="opacity-0" leave-active-class="transition-opacity duration-300">
|
|
15
|
+
<div v-if="isOpen" class="lg:hidden fixed inset-0 z-50 bg-black/40 backdrop-blur-sm" @click="close" />
|
|
16
|
+
</Transition>
|
|
57
17
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
18
|
+
<!-- Sidebar -->
|
|
19
|
+
<aside
|
|
20
|
+
tabindex="-1"
|
|
21
|
+
aria-label="Sidebar"
|
|
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">
|
|
62
31
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
32
|
+
<!-- Logo + close mobile -->
|
|
33
|
+
<header class="h-11.5 ps-2 pe-2 lg:ps-5 flex items-center gap-x-1 shrink-0">
|
|
34
|
+
<slot name="logo" />
|
|
35
|
+
<div class="lg:hidden ms-auto">
|
|
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>
|
|
78
47
|
</div>
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
48
|
+
</header>
|
|
49
|
+
|
|
50
|
+
<!-- Menu scrollable -->
|
|
51
|
+
<div class="flex-1 min-h-0 mt-1.5 overflow-y-auto
|
|
52
|
+
[&::-webkit-scrollbar]:w-2
|
|
53
|
+
[&::-webkit-scrollbar-thumb]:rounded-full
|
|
54
|
+
[&::-webkit-scrollbar-track]:bg-slate-100
|
|
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" />
|
|
84
59
|
</div>
|
|
85
60
|
|
|
86
|
-
<!--
|
|
87
|
-
<div class="
|
|
88
|
-
<
|
|
61
|
+
<!-- User footer -->
|
|
62
|
+
<div class="shrink-0 border-t border-slate-200 dark:border-slate-700 p-5">
|
|
63
|
+
<slot name="user-footer" />
|
|
89
64
|
</div>
|
|
65
|
+
|
|
90
66
|
</div>
|
|
91
|
-
</
|
|
92
|
-
|
|
67
|
+
</aside>
|
|
68
|
+
|
|
69
|
+
<!-- Main content -->
|
|
70
|
+
<slot />
|
|
93
71
|
|
|
94
|
-
|
|
95
|
-
<slot />
|
|
72
|
+
</div>
|
|
96
73
|
</template>
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
defineProps<{
|
|
3
|
+
title?: string
|
|
4
|
+
}>()
|
|
5
|
+
|
|
6
|
+
const sidebar = inject('vantage:sidebar', null) as any
|
|
7
|
+
</script>
|
|
8
|
+
|
|
9
|
+
<template>
|
|
10
|
+
<header class="sticky top-0 z-40 bg-white dark:bg-slate-800 border-b border-slate-200 dark:border-slate-700 px-4 lg:px-6 h-14 flex items-center gap-x-3">
|
|
11
|
+
<!-- Hamburger mobile -->
|
|
12
|
+
<button type="button"
|
|
13
|
+
class="lg:hidden size-8 flex items-center justify-center text-slate-500 hover:text-slate-700 dark:text-slate-400 dark:hover:text-slate-200"
|
|
14
|
+
@click="sidebar?.open()">
|
|
15
|
+
<svg class="size-5" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
|
16
|
+
<line x1="3" y1="6" x2="21" y2="6" /><line x1="3" y1="12" x2="21" y2="12" /><line x1="3" y1="18" x2="21" y2="18" />
|
|
17
|
+
</svg>
|
|
18
|
+
</button>
|
|
19
|
+
|
|
20
|
+
<!-- Left slot -->
|
|
21
|
+
<div class="flex-1 flex items-center gap-x-3">
|
|
22
|
+
<slot name="left">
|
|
23
|
+
<span v-if="title" class="text-sm font-medium text-slate-700 dark:text-slate-300">{{ title }}</span>
|
|
24
|
+
</slot>
|
|
25
|
+
</div>
|
|
26
|
+
|
|
27
|
+
<!-- Right slot -->
|
|
28
|
+
<div class="flex items-center gap-x-2">
|
|
29
|
+
<slot name="right" />
|
|
30
|
+
</div>
|
|
31
|
+
</header>
|
|
32
|
+
</template>
|
|
@@ -1,73 +1,7 @@
|
|
|
1
|
-
<script setup
|
|
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>
|
|
1
|
+
<script setup></script>
|
|
25
2
|
|
|
26
3
|
<template>
|
|
27
|
-
<div class="
|
|
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 -->
|
|
4
|
+
<div class="lg:ps-65">
|
|
70
5
|
<slot />
|
|
71
|
-
|
|
72
6
|
</div>
|
|
73
7
|
</template>
|
|
@@ -1,8 +1,4 @@
|
|
|
1
1
|
<script setup>
|
|
2
|
-
const props = defineProps({
|
|
3
|
-
floating: { type: Boolean, default: false },
|
|
4
|
-
})
|
|
5
|
-
|
|
6
2
|
const showAnimation = ref(false)
|
|
7
3
|
onMounted(() => {
|
|
8
4
|
const seen = sessionStorage.getItem('auth-entered')
|
|
@@ -15,57 +11,12 @@ onMounted(() => {
|
|
|
15
11
|
|
|
16
12
|
<template>
|
|
17
13
|
<div :class="{ 'animate-entrance': showAnimation }">
|
|
18
|
-
|
|
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">
|
|
14
|
+
<AdminBase>
|
|
64
15
|
<template #logo><slot name="logo" /></template>
|
|
65
16
|
<template #menu><slot name="menu" /></template>
|
|
66
17
|
<template #user-footer><slot name="user-footer" /></template>
|
|
67
18
|
|
|
68
|
-
<div
|
|
19
|
+
<div class="lg:ps-65">
|
|
69
20
|
<slot />
|
|
70
21
|
</div>
|
|
71
22
|
</AdminBase>
|
package/components/Nav/Tabs.vue
CHANGED
|
@@ -10,26 +10,11 @@ interface Tab {
|
|
|
10
10
|
|
|
11
11
|
const props = withDefaults(defineProps<{
|
|
12
12
|
tabs: Tab[]
|
|
13
|
-
color?: string
|
|
14
13
|
activeClass?: string
|
|
15
14
|
}>(), {
|
|
16
|
-
|
|
15
|
+
activeClass: 'bg-white dark:bg-slate-800 text-blue-600 dark:text-blue-400 shadow-sm',
|
|
17
16
|
})
|
|
18
17
|
|
|
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
|
-
|
|
33
18
|
const route = useRoute()
|
|
34
19
|
|
|
35
20
|
const isActive = (tab: Tab) =>
|
|
@@ -37,14 +22,14 @@ const isActive = (tab: Tab) =>
|
|
|
37
22
|
</script>
|
|
38
23
|
|
|
39
24
|
<template>
|
|
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-
|
|
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-700">
|
|
41
26
|
<NuxtLink
|
|
42
27
|
v-for="tab in tabs"
|
|
43
28
|
:key="tab.to"
|
|
44
29
|
:to="tab.to"
|
|
45
30
|
class="flex items-center gap-x-2 px-4 py-2 text-xs font-bold rounded-lg transition-all"
|
|
46
31
|
:class="isActive(tab)
|
|
47
|
-
?
|
|
32
|
+
? activeClass
|
|
48
33
|
: 'text-slate-500 dark:text-slate-400 hover:text-slate-700 dark:hover:text-slate-200'"
|
|
49
34
|
>
|
|
50
35
|
<component :is="tab.icon" v-if="tab.icon" class="size-4" />
|
|
@@ -3,10 +3,9 @@ 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
|
-
|
|
9
|
-
name: { type: String, default: undefined },
|
|
6
|
+
endpoint: { type: String, required: true },
|
|
7
|
+
columns: { type: Array, required: true }, // [{ key, label, editable?, type?: 'text'|'number'|'select', options?: [] }]
|
|
8
|
+
name: { type: String, required: true },
|
|
10
9
|
params: { type: Object, default: () => ({}) },
|
|
11
10
|
updateMutation: { type: Function, default: null }, // (id, field, value) => Promise
|
|
12
11
|
cached: { type: Boolean, default: false },
|
|
@@ -16,9 +15,6 @@ const props = defineProps({
|
|
|
16
15
|
|
|
17
16
|
const emit = defineEmits(['row-click', 'cell-save'])
|
|
18
17
|
|
|
19
|
-
const resolvedEndpoint = computed(() => props.table?.endpoint ?? props.endpoint)
|
|
20
|
-
const resolvedName = computed(() => props.table?.name ?? props.name)
|
|
21
|
-
|
|
22
18
|
const tableRef = ref(null)
|
|
23
19
|
|
|
24
20
|
// ─── Inline editing ───────────────────────────────────────────────────────────
|
|
@@ -101,9 +97,9 @@ defineExpose({ reload })
|
|
|
101
97
|
<div class="overflow-x-auto border border-slate-200 dark:border-slate-700 rounded-xl">
|
|
102
98
|
<Table
|
|
103
99
|
ref="tableRef"
|
|
104
|
-
:endpoint="
|
|
100
|
+
:endpoint="endpoint"
|
|
105
101
|
:columns="columns"
|
|
106
|
-
:name="
|
|
102
|
+
:name="name"
|
|
107
103
|
:params="mergedParams"
|
|
108
104
|
:search="search"
|
|
109
105
|
: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-800 dark:border-slate-700 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-700 dark:bg-slate-800 dark:border dark:border-slate-700 border-t border-slate-200 dark:border-slate-700"
|
|
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-800 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-800 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-800 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-700";
|
|
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-800 border border-slate-200 dark:border-slate-700 rounded-xl shadow-xl max-h-60 overflow-auto',
|
|
173
173
|
menuClass,
|
|
174
174
|
]"
|
|
175
175
|
>
|
|
@@ -3,10 +3,9 @@ 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
|
-
|
|
9
|
-
name: { type: String, default: undefined },
|
|
6
|
+
endpoint: { type: String, required: true },
|
|
7
|
+
columns: { type: Array, required: true },
|
|
8
|
+
name: { type: String, required: true },
|
|
10
9
|
params: { type: Object, default: () => ({}) },
|
|
11
10
|
cached: { type: Boolean, default: true },
|
|
12
11
|
searchPlaceholder: { type: String, default: 'Buscar...' },
|
|
@@ -17,9 +16,6 @@ const props = defineProps({
|
|
|
17
16
|
|
|
18
17
|
const emit = defineEmits(['row-click', 'loaded'])
|
|
19
18
|
|
|
20
|
-
const resolvedEndpoint = computed(() => props.table?.endpoint ?? props.endpoint)
|
|
21
|
-
const resolvedName = computed(() => props.table?.name ?? props.name)
|
|
22
|
-
|
|
23
19
|
const search = ref('')
|
|
24
20
|
const tableRef = ref(null)
|
|
25
21
|
|
|
@@ -45,9 +41,9 @@ defineExpose({ reload, clearCache })
|
|
|
45
41
|
|
|
46
42
|
<DataTable
|
|
47
43
|
ref="tableRef"
|
|
48
|
-
:endpoint="
|
|
44
|
+
:endpoint="endpoint"
|
|
49
45
|
:columns="columns"
|
|
50
|
-
:name="
|
|
46
|
+
:name="name"
|
|
51
47
|
:params="params"
|
|
52
48
|
:search="search"
|
|
53
49
|
:cached="cached"
|
|
@@ -6,9 +6,8 @@ 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
|
-
|
|
11
|
-
name: { type: String, default: undefined },
|
|
9
|
+
endpoint: { type: String, required: true }, // POST endpoint returning paginated list
|
|
10
|
+
name: { type: String, required: true }, // used as queryKey base
|
|
12
11
|
params: { type: Object, default: () => ({}) },
|
|
13
12
|
stateKey: { type: String, default: 'status' }, // field in row that holds the state key
|
|
14
13
|
states: { // column definitions
|
|
@@ -26,22 +25,6 @@ const emit = defineEmits(['move', 'card-click'])
|
|
|
26
25
|
const api = useApi()
|
|
27
26
|
const queryClient = useQueryClient()
|
|
28
27
|
|
|
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
|
-
|
|
45
28
|
// ─── Fetch all rows ───────────────────────────────────────────────────────────
|
|
46
29
|
const loading = ref(false)
|
|
47
30
|
const rows = ref([])
|
|
@@ -49,7 +32,7 @@ const rows = ref([])
|
|
|
49
32
|
const fetchAll = async () => {
|
|
50
33
|
loading.value = true
|
|
51
34
|
try {
|
|
52
|
-
const res = await api.post(
|
|
35
|
+
const res = await api.post(props.endpoint, { perPage: props.perPage, ...props.params })
|
|
53
36
|
rows.value = Array.isArray(res?.data) ? res.data : (Array.isArray(res) ? res : [])
|
|
54
37
|
} catch (e) {
|
|
55
38
|
console.error('[Kanban] fetch error', e)
|
|
@@ -4,9 +4,8 @@ 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
|
-
|
|
9
|
-
name: { type: String, default: undefined },
|
|
7
|
+
endpoint: { type: String, required: true },
|
|
8
|
+
name: { type: String, required: true },
|
|
10
9
|
params: { type: Object, default: () => ({}) },
|
|
11
10
|
searchPlaceholder: { type: String, default: 'Buscar...' },
|
|
12
11
|
showSearch: { type: Boolean, default: true },
|
|
@@ -16,10 +15,6 @@ const props = defineProps({
|
|
|
16
15
|
const emit = defineEmits(['row-click'])
|
|
17
16
|
|
|
18
17
|
const api = useApi()
|
|
19
|
-
|
|
20
|
-
const resolvedEndpoint = computed(() => props.table?.endpoint ?? props.endpoint)
|
|
21
|
-
const resolvedName = computed(() => props.table?.name ?? props.name)
|
|
22
|
-
|
|
23
18
|
const search = ref('')
|
|
24
19
|
const sentinel = ref(null)
|
|
25
20
|
|
|
@@ -30,12 +25,12 @@ watch(search, (v) => {
|
|
|
30
25
|
searchTimeout = setTimeout(() => { debouncedSearch.value = v }, 400)
|
|
31
26
|
})
|
|
32
27
|
|
|
33
|
-
const queryKey = computed(() => [
|
|
28
|
+
const queryKey = computed(() => [props.name, 'infinite', debouncedSearch.value, props.params])
|
|
34
29
|
|
|
35
30
|
const { data, fetchNextPage, hasNextPage, isFetchingNextPage, isLoading, refetch } = useInfiniteQuery({
|
|
36
31
|
queryKey,
|
|
37
32
|
queryFn: ({ pageParam = 1 }) =>
|
|
38
|
-
api.post(
|
|
33
|
+
api.post(props.endpoint, {
|
|
39
34
|
search: debouncedSearch.value,
|
|
40
35
|
page: pageParam,
|
|
41
36
|
perPage: props.perPage,
|