@innertia-solutions/nuxt-theme-spark 0.1.86 → 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.
@@ -1,73 +1,96 @@
1
1
  <script setup lang="ts">
2
- const isOpen = ref(false)
3
- const open = () => { isOpen.value = true }
4
- const close = () => { isOpen.value = false }
2
+ import * as icons from '@tabler/icons-vue'
5
3
 
6
- provide('vantage:sidebar', { isOpen, open, close })
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
- <div class="bg-slate-50 dark:bg-slate-900 min-h-screen">
11
-
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>
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
- <!-- 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">
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
- <!-- 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>
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
- <!-- 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" />
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 footer -->
62
- <div class="shrink-0 border-t border-slate-200 dark:border-slate-700 p-5">
63
- <slot name="user-footer" />
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
- </aside>
68
-
69
- <!-- Main content -->
70
- <slot />
91
+ </div>
92
+ </aside>
71
93
 
72
- </div>
94
+ <!-- Main content -->
95
+ <slot />
73
96
  </template>
@@ -1,7 +1,73 @@
1
- <script setup></script>
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="lg:ps-65">
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>
@@ -57,7 +57,7 @@ onBeforeUnmount(() => {
57
57
  <style scoped>
58
58
  .page-loading-spinner {
59
59
  position: fixed;
60
- top: 0.625rem;
60
+ bottom: 0.625rem;
61
61
  right: 0.5rem;
62
62
  z-index: 2147483647;
63
63
  display: flex;
@@ -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
- <AdminBase>
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>
@@ -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
- activeClass: 'bg-white dark:bg-slate-800 text-blue-600 dark:text-blue-400 shadow-sm',
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-700">
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
- ? activeClass
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
- 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 },
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="endpoint"
104
+ :endpoint="resolvedEndpoint"
101
105
  :columns="columns"
102
- :name="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-800 dark:border-slate-700 dark:text-slate-300 dark:hover:bg-slate-700 dark:focus:bg-slate-700"
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-700 dark:bg-slate-800 dark:border dark:border-slate-700 border-t border-slate-200 dark:border-slate-700"
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-800 dark:border-slate-600"
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-800 dark:border-slate-600"
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-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";
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-800 border border-slate-200 dark:border-slate-700 rounded-xl shadow-xl max-h-60 overflow-auto',
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
- endpoint: { type: String, required: true },
7
- columns: { type: Array, required: true },
8
- name: { type: String, required: true },
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="endpoint"
48
+ :endpoint="resolvedEndpoint"
45
49
  :columns="columns"
46
- :name="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
- endpoint: { type: String, required: true }, // POST endpoint returning paginated list
10
- name: { type: String, required: true }, // used as queryKey base
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(props.endpoint, { perPage: props.perPage, ...props.params })
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
- endpoint: { type: String, required: true },
8
- name: { type: String, required: true },
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(() => [props.name, 'infinite', debouncedSearch.value, props.params])
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(props.endpoint, {
38
+ api.post(resolvedEndpoint.value, {
34
39
  search: debouncedSearch.value,
35
40
  page: pageParam,
36
41
  perPage: props.perPage,