@innertia-solutions/innertia-nuxt 0.1.11 → 0.1.13
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/CHANGELOG.md +38 -0
- package/app.config.ts +10 -10
- package/components/Attention.vue +269 -0
- package/components/Auth/LoginForm.vue +1 -1
- package/components/Chart/index.vue +193 -0
- package/components/Container.vue +116 -0
- package/components/CopyButton.vue +98 -0
- package/components/Files/Card.vue +215 -0
- package/components/Files/Preview.vue +675 -0
- package/components/Files/Upload.vue +408 -0
- package/components/Files/UploadModal.vue +83 -0
- package/components/Forms/InputPin.vue +162 -0
- package/components/InfoToolbar.vue +54 -0
- package/components/Kanban/Standard.vue +588 -0
- package/components/Loader.vue +358 -0
- package/components/MobileBlocker.vue +5 -5
- package/components/MobileLoginPicker.vue +13 -13
- package/components/Modal/Base.vue +6 -0
- package/components/Modal/Confirm.vue +89 -0
- package/components/Modal/DeleteConfirm.vue +14 -31
- package/components/Modal/Form.vue +76 -0
- package/components/Modal/Host.vue +58 -0
- package/components/Modal/Prompt.vue +74 -0
- package/components/Modal.vue +119 -34
- package/components/Organization/Picker.vue +3 -3
- package/components/Table/Standard.vue +198 -25
- package/components/Table/index.vue +169 -369
- package/components/Table/views/ViewCards.vue +74 -0
- package/components/Table/views/ViewDatabase.vue +236 -0
- package/components/Table/views/ViewTable.vue +299 -0
- package/components/Toast/Alert.vue +16 -111
- package/components/Toast/Container.vue +4 -29
- package/components/Toast/Notification.vue +8 -43
- package/components/Toast/Process.vue +8 -86
- package/components/Toast/Stack.vue +59 -0
- package/components/Toast/Standard.vue +115 -0
- package/components/Tree/Standard.vue +49 -4
- package/components/Workflow/Board.vue +356 -0
- package/components/Workflow/Builder.vue +454 -0
- package/components/Workflow/Form.vue +526 -0
- package/components/Workflow/Viewer.vue +225 -0
- package/components/Workflow/edges/TransitionEdge.vue +84 -0
- package/components/Workflow/nodes/StepNode.vue +88 -0
- package/composables/useAuth.js +1 -1
- package/composables/useClipboard.js +79 -0
- package/composables/useConfetti.js +194 -0
- package/composables/useContext.js +53 -5
- package/composables/useFile.js +189 -0
- package/composables/useMobileGuard.ts +21 -21
- package/composables/useModal.js +71 -0
- package/composables/useOrganization.ts +3 -3
- package/composables/useToast.js +38 -65
- package/composables/useWorkflow/form.js +162 -0
- package/composables/useWorkflow/index.js +18 -0
- package/composables/useWorkflow/view.js +220 -0
- package/composables/useWorkflowLayout.js +162 -0
- package/css/tokens.css +10 -10
- package/docs/superpowers/plans/2026-05-27-nuxt-apps-to-contexts-rename.md +630 -0
- package/docs/superpowers/specs/2026-05-27-nuxt-apps-to-contexts-rename-design.md +113 -0
- package/middleware/{03.apps.global.ts → 03.contexts.global.ts} +25 -25
- package/nuxt.config.ts +1 -1
- package/package.json +13 -8
- package/stores/modal.js +48 -0
- package/stores/toast.js +156 -117
- package/composables/useApp.ts +0 -46
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
/**
|
|
3
|
+
* <ModalHost /> — Mount-once en el layout root para habilitar useModal().
|
|
4
|
+
*
|
|
5
|
+
* Renderiza la cola de modales programáticos del store. Cada entry sabe
|
|
6
|
+
* dispatchar al componente adecuado según su `kind`.
|
|
7
|
+
*/
|
|
8
|
+
import { useModalStore } from '../../stores/modal'
|
|
9
|
+
|
|
10
|
+
const store = useModalStore()
|
|
11
|
+
|
|
12
|
+
const closeWith = (id, value) => store.resolve(id, value)
|
|
13
|
+
</script>
|
|
14
|
+
|
|
15
|
+
<template>
|
|
16
|
+
<template v-for="entry in store.queue" :key="entry.id">
|
|
17
|
+
<!-- Confirm -->
|
|
18
|
+
<ModalConfirm
|
|
19
|
+
v-if="entry.kind === 'confirm'"
|
|
20
|
+
:model-value="true"
|
|
21
|
+
v-bind="entry.props"
|
|
22
|
+
@confirm="closeWith(entry.id, true)"
|
|
23
|
+
@cancel="closeWith(entry.id, false)"
|
|
24
|
+
@update:model-value="v => !v && closeWith(entry.id, false)"
|
|
25
|
+
/>
|
|
26
|
+
|
|
27
|
+
<!-- Alert (confirm sin cancel) -->
|
|
28
|
+
<ModalConfirm
|
|
29
|
+
v-else-if="entry.kind === 'alert'"
|
|
30
|
+
:model-value="true"
|
|
31
|
+
hide-cancel
|
|
32
|
+
v-bind="entry.props"
|
|
33
|
+
@confirm="closeWith(entry.id, undefined)"
|
|
34
|
+
@cancel="closeWith(entry.id, undefined)"
|
|
35
|
+
@update:model-value="v => !v && closeWith(entry.id, undefined)"
|
|
36
|
+
/>
|
|
37
|
+
|
|
38
|
+
<!-- Prompt -->
|
|
39
|
+
<ModalPrompt
|
|
40
|
+
v-else-if="entry.kind === 'prompt'"
|
|
41
|
+
:model-value="true"
|
|
42
|
+
v-bind="entry.props"
|
|
43
|
+
@submit="value => closeWith(entry.id, value)"
|
|
44
|
+
@cancel="closeWith(entry.id, null)"
|
|
45
|
+
@update:model-value="v => !v && closeWith(entry.id, null)"
|
|
46
|
+
/>
|
|
47
|
+
|
|
48
|
+
<!-- Custom component — recibe modelValue + props + emite close(value) -->
|
|
49
|
+
<component
|
|
50
|
+
v-else-if="entry.kind === 'component' && entry.component"
|
|
51
|
+
:is="entry.component"
|
|
52
|
+
:model-value="true"
|
|
53
|
+
v-bind="entry.props"
|
|
54
|
+
@close="value => closeWith(entry.id, value)"
|
|
55
|
+
@update:model-value="v => !v && closeWith(entry.id, undefined)"
|
|
56
|
+
/>
|
|
57
|
+
</template>
|
|
58
|
+
</template>
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
/**
|
|
3
|
+
* <ModalPrompt> — Diálogo para capturar un string del usuario.
|
|
4
|
+
*
|
|
5
|
+
* Usado principalmente por useModal().prompt(). Emite 'submit' con el valor
|
|
6
|
+
* y 'cancel' al cerrar.
|
|
7
|
+
*/
|
|
8
|
+
const props = defineProps({
|
|
9
|
+
modelValue: { type: Boolean, default: false },
|
|
10
|
+
title: { type: String, default: 'Ingresar valor' },
|
|
11
|
+
message: { type: String, default: '' },
|
|
12
|
+
placeholder: { type: String, default: '' },
|
|
13
|
+
defaultValue: { type: String, default: '' },
|
|
14
|
+
confirmText: { type: String, default: 'Aceptar' },
|
|
15
|
+
cancelText: { type: String, default: 'Cancelar' },
|
|
16
|
+
required: { type: Boolean, default: false },
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
const emit = defineEmits(['update:modelValue', 'submit', 'cancel'])
|
|
20
|
+
|
|
21
|
+
const value = ref(props.defaultValue)
|
|
22
|
+
const inputRef = ref(null)
|
|
23
|
+
|
|
24
|
+
watch(() => props.modelValue, async (v) => {
|
|
25
|
+
if (v) {
|
|
26
|
+
value.value = props.defaultValue
|
|
27
|
+
await nextTick()
|
|
28
|
+
inputRef.value?.focus()
|
|
29
|
+
}
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
const cancel = () => {
|
|
33
|
+
emit('update:modelValue', false)
|
|
34
|
+
emit('cancel')
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const submit = () => {
|
|
38
|
+
if (props.required && !value.value.trim()) return
|
|
39
|
+
emit('submit', value.value)
|
|
40
|
+
}
|
|
41
|
+
</script>
|
|
42
|
+
|
|
43
|
+
<template>
|
|
44
|
+
<Modal
|
|
45
|
+
:model-value="modelValue"
|
|
46
|
+
:title="title"
|
|
47
|
+
size="sm"
|
|
48
|
+
:show-footer="true"
|
|
49
|
+
padding="md"
|
|
50
|
+
@update:model-value="emit('update:modelValue', $event)"
|
|
51
|
+
@close="cancel"
|
|
52
|
+
>
|
|
53
|
+
<p v-if="message" class="text-sm text-muted-foreground mb-3">{{ message }}</p>
|
|
54
|
+
<input
|
|
55
|
+
ref="inputRef"
|
|
56
|
+
v-model="value"
|
|
57
|
+
type="text"
|
|
58
|
+
:placeholder="placeholder"
|
|
59
|
+
class="innertia-field w-full"
|
|
60
|
+
@keydown.enter="submit"
|
|
61
|
+
/>
|
|
62
|
+
|
|
63
|
+
<template #footer>
|
|
64
|
+
<AppButton :text="cancelText" severity="secondary" size="sm" @click="cancel" />
|
|
65
|
+
<AppButton
|
|
66
|
+
:text="confirmText"
|
|
67
|
+
severity="primary"
|
|
68
|
+
size="sm"
|
|
69
|
+
:disabled="required && !value.trim()"
|
|
70
|
+
@click="submit"
|
|
71
|
+
/>
|
|
72
|
+
</template>
|
|
73
|
+
</Modal>
|
|
74
|
+
</template>
|
package/components/Modal.vue
CHANGED
|
@@ -1,23 +1,69 @@
|
|
|
1
1
|
<script setup>
|
|
2
|
+
import { IconX } from '@tabler/icons-vue'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* <Modal> — Core modal. Sigue tokens semánticos (rounded-modal, bg-card,
|
|
6
|
+
* border-card-line). Variantes: center (default), drawer-right, drawer-left,
|
|
7
|
+
* fullscreen. Tamaños xs..3xl.
|
|
8
|
+
*
|
|
9
|
+
* No tiene lógica de negocio. Para casos típicos preferir:
|
|
10
|
+
* - <ModalConfirm> → confirmaciones con severities
|
|
11
|
+
* - <ModalForm> → modal con form, scroll y footer sticky
|
|
12
|
+
* - useModal() → API promise-based (alert/confirm/prompt/open)
|
|
13
|
+
*/
|
|
2
14
|
const props = defineProps({
|
|
3
|
-
modelValue:
|
|
4
|
-
title:
|
|
5
|
-
size:
|
|
6
|
-
|
|
15
|
+
modelValue: { type: Boolean, default: false },
|
|
16
|
+
title: { type: String, default: '' },
|
|
17
|
+
size: { type: String, default: 'md', validator: v => ['xs','sm','md','lg','xl','2xl','3xl','fullscreen'].includes(v) },
|
|
18
|
+
variant: { type: String, default: 'center', validator: v => ['center','drawer-right','drawer-left','fullscreen'].includes(v) },
|
|
19
|
+
closable: { type: Boolean, default: true },
|
|
7
20
|
backdropDismiss: { type: Boolean, default: true },
|
|
8
|
-
showHeader:
|
|
9
|
-
showFooter:
|
|
21
|
+
showHeader: { type: Boolean, default: true },
|
|
22
|
+
showFooter: { type: Boolean, default: false },
|
|
23
|
+
/** Hace el body scrollable con el header/footer sticky. */
|
|
24
|
+
scrollBody: { type: Boolean, default: true },
|
|
25
|
+
/** Padding del body. 'none' para que el slot controle su propio padding. */
|
|
26
|
+
padding: { type: String, default: 'md', validator: v => ['none','sm','md','lg'].includes(v) },
|
|
10
27
|
})
|
|
11
28
|
|
|
12
29
|
const emit = defineEmits(['update:modelValue', 'close'])
|
|
13
30
|
|
|
14
31
|
const modalId = `modal-${Math.random().toString(36).slice(2, 9)}`
|
|
15
32
|
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
33
|
+
const isDrawer = computed(() => props.variant.startsWith('drawer'))
|
|
34
|
+
const isFullscreen = computed(() => props.variant === 'fullscreen' || props.size === 'fullscreen')
|
|
35
|
+
|
|
36
|
+
const sizeClass = computed(() => {
|
|
37
|
+
if (isFullscreen.value) return 'max-w-full w-full h-full'
|
|
38
|
+
return ({
|
|
39
|
+
xs: 'max-w-xs', sm: 'max-w-sm', md: 'max-w-md',
|
|
40
|
+
lg: 'max-w-lg', xl: 'max-w-xl', '2xl': 'max-w-2xl', '3xl': 'max-w-3xl',
|
|
41
|
+
}[props.size] ?? 'max-w-md')
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
const paddingClass = computed(() => ({
|
|
45
|
+
none: 'p-0', sm: 'p-3', md: 'p-5', lg: 'p-6',
|
|
46
|
+
}[props.padding] ?? 'p-5'))
|
|
47
|
+
|
|
48
|
+
const containerClass = computed(() => {
|
|
49
|
+
if (isFullscreen.value) return 'inset-0'
|
|
50
|
+
if (props.variant === 'drawer-right') return 'inset-y-0 right-0'
|
|
51
|
+
if (props.variant === 'drawer-left') return 'inset-y-0 left-0'
|
|
52
|
+
return 'inset-0 items-center justify-center p-4'
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
const panelClass = computed(() => {
|
|
56
|
+
if (isFullscreen.value) return 'w-full h-full rounded-none'
|
|
57
|
+
if (isDrawer.value) return `h-full ${sizeClass.value} rounded-none`
|
|
58
|
+
return `w-full ${sizeClass.value} rounded-modal`
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
const transitionName = computed(() => {
|
|
62
|
+
if (isFullscreen.value) return 'modal-fade'
|
|
63
|
+
if (props.variant === 'drawer-right') return 'modal-slide-right'
|
|
64
|
+
if (props.variant === 'drawer-left') return 'modal-slide-left'
|
|
65
|
+
return 'modal-pop'
|
|
66
|
+
})
|
|
21
67
|
|
|
22
68
|
const close = () => {
|
|
23
69
|
emit('update:modelValue', false)
|
|
@@ -31,54 +77,67 @@ const onBackdrop = (e) => {
|
|
|
31
77
|
const onEsc = (e) => { if (e.key === 'Escape' && props.modelValue && props.closable) close() }
|
|
32
78
|
|
|
33
79
|
watch(() => props.modelValue, v => {
|
|
34
|
-
|
|
80
|
+
if (typeof document !== 'undefined') {
|
|
81
|
+
document.body.style.overflow = v ? 'hidden' : ''
|
|
82
|
+
}
|
|
35
83
|
})
|
|
36
84
|
|
|
37
85
|
onMounted(() => document.addEventListener('keydown', onEsc))
|
|
38
86
|
onUnmounted(() => {
|
|
39
87
|
document.removeEventListener('keydown', onEsc)
|
|
40
|
-
document.body.style.overflow = ''
|
|
88
|
+
if (typeof document !== 'undefined') document.body.style.overflow = ''
|
|
41
89
|
})
|
|
42
90
|
</script>
|
|
43
91
|
|
|
44
92
|
<template>
|
|
45
93
|
<Teleport v-if="modelValue" to="body">
|
|
46
|
-
<Transition name="
|
|
94
|
+
<Transition :name="transitionName" appear>
|
|
47
95
|
<div
|
|
48
|
-
class="fixed
|
|
96
|
+
class="fixed z-[9999] bg-black/40 dark:bg-black/60 backdrop-blur-sm flex"
|
|
97
|
+
:class="containerClass"
|
|
49
98
|
role="dialog"
|
|
50
99
|
tabindex="-1"
|
|
51
100
|
:aria-labelledby="`${modalId}-label`"
|
|
52
101
|
@click="onBackdrop"
|
|
53
102
|
>
|
|
54
103
|
<div
|
|
55
|
-
:class="['bg-card border border-card-line
|
|
104
|
+
:class="['bg-card border border-card-line shadow-xl flex flex-col modal-panel max-h-full', panelClass]"
|
|
56
105
|
@click.stop
|
|
57
106
|
>
|
|
58
107
|
<!-- Header -->
|
|
59
|
-
<div
|
|
60
|
-
|
|
108
|
+
<div
|
|
109
|
+
v-if="showHeader"
|
|
110
|
+
class="shrink-0 flex items-center justify-between px-5 py-4 border-b border-card-line"
|
|
111
|
+
>
|
|
112
|
+
<h3 :id="`${modalId}-label`" class="text-sm font-semibold text-foreground truncate">
|
|
61
113
|
<slot name="header">{{ title }}</slot>
|
|
62
114
|
</h3>
|
|
63
115
|
<button
|
|
64
116
|
v-if="closable"
|
|
65
117
|
type="button"
|
|
66
|
-
class="size-7 flex items-center justify-center rounded-
|
|
118
|
+
class="size-7 flex items-center justify-center rounded-control text-muted-foreground hover:text-foreground hover:bg-muted-hover transition-colors shrink-0"
|
|
67
119
|
@click="close"
|
|
120
|
+
aria-label="Cerrar"
|
|
68
121
|
>
|
|
69
|
-
<
|
|
70
|
-
<path d="M18 6 6 18M6 6l12 12" stroke-linecap="round" stroke-linejoin="round" />
|
|
71
|
-
</svg>
|
|
122
|
+
<IconX class="size-4" />
|
|
72
123
|
</button>
|
|
73
124
|
</div>
|
|
74
125
|
|
|
75
126
|
<!-- Body -->
|
|
76
|
-
<div
|
|
127
|
+
<div
|
|
128
|
+
:class="[
|
|
129
|
+
paddingClass,
|
|
130
|
+
scrollBody ? 'flex-1 min-h-0 overflow-y-auto' : '',
|
|
131
|
+
]"
|
|
132
|
+
>
|
|
77
133
|
<slot />
|
|
78
134
|
</div>
|
|
79
135
|
|
|
80
136
|
<!-- Footer -->
|
|
81
|
-
<div
|
|
137
|
+
<div
|
|
138
|
+
v-if="showFooter"
|
|
139
|
+
class="shrink-0 flex items-center justify-end gap-2 px-5 py-4 border-t border-card-line bg-card"
|
|
140
|
+
>
|
|
82
141
|
<slot name="footer">
|
|
83
142
|
<AppButton v-if="closable" text="Cerrar" severity="secondary" size="sm" @click="close" />
|
|
84
143
|
</slot>
|
|
@@ -90,14 +149,40 @@ onUnmounted(() => {
|
|
|
90
149
|
</template>
|
|
91
150
|
|
|
92
151
|
<style scoped>
|
|
93
|
-
|
|
94
|
-
.modal-
|
|
95
|
-
.modal-
|
|
96
|
-
.modal-
|
|
97
|
-
|
|
98
|
-
.modal-enter-from .modal-
|
|
99
|
-
.modal-leave-to .modal-
|
|
100
|
-
.modal-enter-to .modal-
|
|
101
|
-
.modal-leave-from .modal-
|
|
102
|
-
.modal-
|
|
152
|
+
/* Center pop */
|
|
153
|
+
.modal-pop-enter-active,
|
|
154
|
+
.modal-pop-leave-active { transition: opacity 0.15s ease; }
|
|
155
|
+
.modal-pop-enter-from,
|
|
156
|
+
.modal-pop-leave-to { opacity: 0; }
|
|
157
|
+
.modal-pop-enter-from .modal-panel,
|
|
158
|
+
.modal-pop-leave-to .modal-panel { transform: scale(0.97) translateY(-8px); opacity: 0; }
|
|
159
|
+
.modal-pop-enter-to .modal-panel,
|
|
160
|
+
.modal-pop-leave-from .modal-panel { transform: scale(1) translateY(0); opacity: 1; }
|
|
161
|
+
.modal-panel { transition: transform 0.18s cubic-bezier(.2,.8,.2,1), opacity 0.18s ease; }
|
|
162
|
+
|
|
163
|
+
/* Fade fullscreen */
|
|
164
|
+
.modal-fade-enter-active,
|
|
165
|
+
.modal-fade-leave-active { transition: opacity 0.2s ease; }
|
|
166
|
+
.modal-fade-enter-from,
|
|
167
|
+
.modal-fade-leave-to { opacity: 0; }
|
|
168
|
+
|
|
169
|
+
/* Drawer right */
|
|
170
|
+
.modal-slide-right-enter-active,
|
|
171
|
+
.modal-slide-right-leave-active { transition: opacity 0.2s ease; }
|
|
172
|
+
.modal-slide-right-enter-from,
|
|
173
|
+
.modal-slide-right-leave-to { opacity: 0; }
|
|
174
|
+
.modal-slide-right-enter-from .modal-panel,
|
|
175
|
+
.modal-slide-right-leave-to .modal-panel { transform: translateX(100%); }
|
|
176
|
+
.modal-slide-right-enter-to .modal-panel,
|
|
177
|
+
.modal-slide-right-leave-from .modal-panel { transform: translateX(0); }
|
|
178
|
+
|
|
179
|
+
/* Drawer left */
|
|
180
|
+
.modal-slide-left-enter-active,
|
|
181
|
+
.modal-slide-left-leave-active { transition: opacity 0.2s ease; }
|
|
182
|
+
.modal-slide-left-enter-from,
|
|
183
|
+
.modal-slide-left-leave-to { opacity: 0; }
|
|
184
|
+
.modal-slide-left-enter-from .modal-panel,
|
|
185
|
+
.modal-slide-left-leave-to .modal-panel { transform: translateX(-100%); }
|
|
186
|
+
.modal-slide-left-enter-to .modal-panel,
|
|
187
|
+
.modal-slide-left-leave-from .modal-panel { transform: translateX(0); }
|
|
103
188
|
</style>
|
|
@@ -10,12 +10,12 @@
|
|
|
10
10
|
*/
|
|
11
11
|
import { IconBuildingSkyscraper, IconArrowRight } from '@tabler/icons-vue'
|
|
12
12
|
|
|
13
|
-
const { current:
|
|
13
|
+
const { current: currentContext } = useContext()
|
|
14
14
|
const { available, switchTo } = useOrganization()
|
|
15
15
|
|
|
16
16
|
const title = computed(() =>
|
|
17
|
-
|
|
18
|
-
? `Elige una organización para continuar en ${
|
|
17
|
+
currentContext.value
|
|
18
|
+
? `Elige una organización para continuar en ${currentContext.value.label}`
|
|
19
19
|
: 'Elige una organización'
|
|
20
20
|
)
|
|
21
21
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<script setup>
|
|
2
|
-
import { IconSearch, IconLayoutColumns, IconGripVertical, IconX, IconPlus, IconChevronLeft, IconCheck, IconChevronDown } from '@tabler/icons-vue'
|
|
2
|
+
import { IconSearch, IconLayoutColumns, IconGripVertical, IconX, IconPlus, IconChevronLeft, IconCheck, IconChevronDown, IconTable, IconLayoutGrid, IconDatabase, IconLayoutSidebarRight } from '@tabler/icons-vue'
|
|
3
3
|
import Table from './index.vue'
|
|
4
4
|
|
|
5
5
|
const props = defineProps({
|
|
@@ -16,6 +16,12 @@ const props = defineProps({
|
|
|
16
16
|
showSearch: { type: Boolean, default: true },
|
|
17
17
|
showFilters: { type: Boolean, default: true },
|
|
18
18
|
showExport: { type: Boolean, default: true },
|
|
19
|
+
showColumns: { type: Boolean, default: true },
|
|
20
|
+
/** Mostrar selector "Filas por página" en el footer. Default false. */
|
|
21
|
+
showPerPage: { type: Boolean, default: false },
|
|
22
|
+
|
|
23
|
+
// Tamaño visible (wrap + overflow auto). 'sm' | 'md' (default) | 'lg' | 'fit'.
|
|
24
|
+
size: { type: String, default: 'md' },
|
|
19
25
|
filters: { type: Array, default: () => [] },
|
|
20
26
|
splitRatio: { type: Number, default: 60 },
|
|
21
27
|
autoClosePreview: { type: Boolean, default: true },
|
|
@@ -23,13 +29,103 @@ const props = defineProps({
|
|
|
23
29
|
previewDeletable: { type: Boolean, default: false },
|
|
24
30
|
defaultPinnedColumns: { type: Object, default: null }, // { left?: string[], right?: string[] }
|
|
25
31
|
persistPreferences: { type: Boolean, default: true }, // persist column prefs in backend
|
|
32
|
+
|
|
33
|
+
// bordered=false → quita border + rounded-card del wrapper exterior
|
|
34
|
+
bordered: { type: Boolean, default: true },
|
|
35
|
+
|
|
36
|
+
// 'bottom' (default) | 'top' | 'none'
|
|
37
|
+
// 'bottom' → footer + pagination debajo
|
|
38
|
+
// 'top' → mismo footer + pagination, arriba (vía CSS order)
|
|
39
|
+
// 'none' → sin footer/pagination; reload + instant compactos junto a columns
|
|
40
|
+
infoPosition: { type: String, default: 'bottom' },
|
|
41
|
+
|
|
42
|
+
// View switcher
|
|
43
|
+
showViewSwitcher: { type: Boolean, default: false },
|
|
44
|
+
defaultViewType: { type: String, default: 'table' },
|
|
45
|
+
/** Page size when in cards view (default 12 for clean grid layouts). */
|
|
46
|
+
cardsPageSize: { type: Number, default: 12 },
|
|
47
|
+
/** Mutation function for database inline editing: async (row, colKey, value) => void */
|
|
48
|
+
updateMutation: { type: Function, default: null },
|
|
26
49
|
})
|
|
27
50
|
|
|
28
51
|
const resolvedEndpoint = computed(() => props.table?.endpoint ?? props.endpoint)
|
|
29
52
|
const resolvedName = computed(() => props.table?.name ?? props.name)
|
|
30
53
|
|
|
54
|
+
// ─── View switcher state ──────────────────────────────────────────────────────
|
|
55
|
+
const STORAGE_KEY_VIEW = computed(() => `table-view-${resolvedName.value || 'default'}`)
|
|
56
|
+
const viewType = ref(props.defaultViewType)
|
|
57
|
+
|
|
58
|
+
const viewModes = [
|
|
59
|
+
{ id: 'table', icon: IconTable, label: 'Tabla' },
|
|
60
|
+
{ id: 'cards', icon: IconLayoutGrid, label: 'Tarjetas' },
|
|
61
|
+
{ id: 'database', icon: IconDatabase, label: 'Base de datos' },
|
|
62
|
+
{ id: 'divider', icon: IconLayoutSidebarRight, label: 'Vista dividida' },
|
|
63
|
+
]
|
|
64
|
+
|
|
65
|
+
onMounted(() => {
|
|
66
|
+
if (resolvedName.value && props.showViewSwitcher) {
|
|
67
|
+
const stored = localStorage.getItem(STORAGE_KEY_VIEW.value)
|
|
68
|
+
if (stored && ['table', 'cards', 'database', 'divider'].includes(stored)) {
|
|
69
|
+
viewType.value = stored
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
watch(viewType, (newView, oldView) => {
|
|
75
|
+
if (resolvedName.value && props.showViewSwitcher) {
|
|
76
|
+
localStorage.setItem(STORAGE_KEY_VIEW.value, newView)
|
|
77
|
+
}
|
|
78
|
+
// Adjust page size when entering/leaving cards view
|
|
79
|
+
if (newView === 'cards') {
|
|
80
|
+
nextTick(() => tableRef.value?.setPageSize(props.cardsPageSize))
|
|
81
|
+
} else if (oldView === 'cards') {
|
|
82
|
+
nextTick(() => tableRef.value?.setPageSize(10))
|
|
83
|
+
}
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
// ─── Resizable divider ────────────────────────────────────────────────────────
|
|
87
|
+
const localSplitRatio = ref(props.splitRatio)
|
|
88
|
+
let _dividerAnimFrame = null
|
|
89
|
+
|
|
90
|
+
const startDividerDrag = (e) => {
|
|
91
|
+
e.preventDefault()
|
|
92
|
+
const startX = e.clientX
|
|
93
|
+
const startRatio = localSplitRatio.value
|
|
94
|
+
const containerWidth = containerRef.value?.getBoundingClientRect().width ?? window.innerWidth
|
|
95
|
+
|
|
96
|
+
const onMove = (ev) => {
|
|
97
|
+
if (_dividerAnimFrame) cancelAnimationFrame(_dividerAnimFrame)
|
|
98
|
+
_dividerAnimFrame = requestAnimationFrame(() => {
|
|
99
|
+
const dx = ev.clientX - startX
|
|
100
|
+
localSplitRatio.value = Math.min(80, Math.max(20, startRatio + (dx / containerWidth) * 100))
|
|
101
|
+
})
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const onUp = () => {
|
|
105
|
+
document.removeEventListener('mousemove', onMove)
|
|
106
|
+
document.removeEventListener('mouseup', onUp)
|
|
107
|
+
document.body.style.cursor = ''
|
|
108
|
+
document.body.style.userSelect = ''
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
document.body.style.cursor = 'col-resize'
|
|
112
|
+
document.body.style.userSelect = 'none'
|
|
113
|
+
document.addEventListener('mousemove', onMove)
|
|
114
|
+
document.addEventListener('mouseup', onUp)
|
|
115
|
+
}
|
|
116
|
+
|
|
31
117
|
// ─── Table preferences (column pinning, visibility, order) ───────────────────
|
|
32
118
|
const tablePrefName = computed(() => resolvedName.value || 'default')
|
|
119
|
+
|
|
120
|
+
const sizeStyle = computed(() => {
|
|
121
|
+
switch (props.size) {
|
|
122
|
+
case 'sm': return { minHeight: '18rem', maxHeight: '24rem', overflow: 'auto' }
|
|
123
|
+
case 'lg': return { minHeight: '45rem', maxHeight: '75vh', overflow: 'auto' }
|
|
124
|
+
case 'fit': return {}
|
|
125
|
+
case 'md':
|
|
126
|
+
default: return {}
|
|
127
|
+
}
|
|
128
|
+
})
|
|
33
129
|
const { preferences: tablePrefs, load: loadPrefs, save: savePrefs } = useTablePreferences(tablePrefName.value)
|
|
34
130
|
|
|
35
131
|
// Resolved initial pinned columns: merge defaultPinnedColumns with saved preferences
|
|
@@ -562,7 +658,36 @@ defineExpose({ getSelectedRows, reload, clearCache, exportTable, tableRef, close
|
|
|
562
658
|
|
|
563
659
|
<!-- Secondary actions: pushed to the right, icon-only style -->
|
|
564
660
|
<div class="ml-auto flex items-center gap-1">
|
|
661
|
+
<!-- Reload + Instant compactos cuando infoPosition === 'none' -->
|
|
662
|
+
<InfoToolbar
|
|
663
|
+
v-if="infoPosition === 'none'"
|
|
664
|
+
:show-reload="showReloadButton"
|
|
665
|
+
:is-fetching="tableRef?.isFetching ?? false"
|
|
666
|
+
:show-instant="tableRef?.isDataFromCache ?? false"
|
|
667
|
+
@reload="() => tableRef?.reload?.()"
|
|
668
|
+
/>
|
|
669
|
+
|
|
670
|
+
<!-- View switcher -->
|
|
671
|
+
<div v-if="showViewSwitcher" class="flex items-center gap-0.5 p-0.5 bg-surface rounded-control border border-card-line">
|
|
672
|
+
<button
|
|
673
|
+
v-for="mode in viewModes"
|
|
674
|
+
:key="mode.id"
|
|
675
|
+
type="button"
|
|
676
|
+
@click="viewType = mode.id"
|
|
677
|
+
:title="mode.label"
|
|
678
|
+
:class="[
|
|
679
|
+
'p-1 inline-flex items-center justify-center rounded transition-colors',
|
|
680
|
+
viewType === mode.id
|
|
681
|
+
? 'bg-card shadow-sm text-foreground'
|
|
682
|
+
: 'text-muted-foreground hover:text-foreground'
|
|
683
|
+
]"
|
|
684
|
+
>
|
|
685
|
+
<component :is="mode.icon" :size="14" :stroke="1.75" />
|
|
686
|
+
</button>
|
|
687
|
+
</div>
|
|
688
|
+
|
|
565
689
|
<button
|
|
690
|
+
v-if="showColumns"
|
|
566
691
|
ref="columnButtonRef"
|
|
567
692
|
type="button"
|
|
568
693
|
@click="showColumnPanel = !showColumnPanel"
|
|
@@ -608,35 +733,83 @@ defineExpose({ getSelectedRows, reload, clearCache, exportTable, tableRef, close
|
|
|
608
733
|
</div>
|
|
609
734
|
</div>
|
|
610
735
|
|
|
611
|
-
<!-- Tabla -->
|
|
612
|
-
<div
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
:
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
:
|
|
622
|
-
:
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
736
|
+
<!-- Tabla — normal or split layout -->
|
|
737
|
+
<div
|
|
738
|
+
:class="[
|
|
739
|
+
bordered ? 'overflow-hidden border border-card-line rounded-card' : '',
|
|
740
|
+
viewType === 'divider' ? 'flex' : '',
|
|
741
|
+
]"
|
|
742
|
+
:style="viewType === 'divider' ? { ...sizeStyle, minHeight: sizeStyle.minHeight ?? '28rem' } : sizeStyle"
|
|
743
|
+
>
|
|
744
|
+
<!-- Table panel -->
|
|
745
|
+
<div
|
|
746
|
+
:class="viewType === 'divider' ? 'overflow-hidden min-w-0' : ''"
|
|
747
|
+
:style="viewType === 'divider' ? { width: localSplitRatio + '%', minWidth: '280px' } : {}"
|
|
748
|
+
>
|
|
749
|
+
<Table
|
|
750
|
+
ref="tableRef"
|
|
751
|
+
:endpoint="resolvedEndpoint"
|
|
752
|
+
:columns="columns"
|
|
753
|
+
:name="resolvedName"
|
|
754
|
+
:params="mergedParams"
|
|
755
|
+
:search="search"
|
|
756
|
+
:checkable="checkable"
|
|
757
|
+
:cached="cached"
|
|
758
|
+
:show-reload-button="showReloadButton"
|
|
759
|
+
:info-position="infoPosition"
|
|
760
|
+
:show-per-page="showPerPage"
|
|
761
|
+
:click-row-to-open="clickRowToOpen"
|
|
762
|
+
:preview-row-id="previewRow?.id ?? null"
|
|
763
|
+
:preview-mode="!!previewEnabled"
|
|
764
|
+
:pinned-columns="resolvedPinnedColumns"
|
|
765
|
+
:view-mode="viewType === 'divider' ? 'table' : viewType"
|
|
766
|
+
:update-mutation="updateMutation"
|
|
767
|
+
:bordered="bordered"
|
|
768
|
+
@row-click="handleRowClick"
|
|
769
|
+
@loaded="handleLoaded"
|
|
770
|
+
@page-change="closePreview"
|
|
771
|
+
@per-page-change="closePreview"
|
|
772
|
+
>
|
|
773
|
+
<template v-for="(_, name) in forwardedSlots" #[name]="slotProps">
|
|
774
|
+
<slot :name="name" v-bind="slotProps ?? {}" />
|
|
775
|
+
</template>
|
|
776
|
+
</Table>
|
|
777
|
+
</div>
|
|
778
|
+
|
|
779
|
+
<!-- Resizable drag handle (divider mode only) -->
|
|
780
|
+
<div
|
|
781
|
+
v-if="viewType === 'divider'"
|
|
782
|
+
class="w-1 shrink-0 bg-card-line hover:bg-primary/40 active:bg-primary/60 cursor-col-resize transition-colors relative group select-none"
|
|
783
|
+
@mousedown="startDividerDrag"
|
|
631
784
|
>
|
|
632
|
-
|
|
633
|
-
|
|
785
|
+
<!-- Visual pill indicator -->
|
|
786
|
+
<div class="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-1 h-8 rounded-full bg-card-line group-hover:bg-primary/50 transition-colors" />
|
|
787
|
+
</div>
|
|
788
|
+
|
|
789
|
+
<!-- Preview panel (divider mode only) -->
|
|
790
|
+
<div v-if="viewType === 'divider'" class="flex-1 min-w-0 overflow-auto">
|
|
791
|
+
<template v-if="previewRow">
|
|
792
|
+
<div class="flex items-center justify-between px-4 py-3 border-b border-card-line">
|
|
793
|
+
<slot name="preview-header" :row="previewRow" :close="closePreview" />
|
|
794
|
+
<button
|
|
795
|
+
@click="closePreview"
|
|
796
|
+
class="p-1 rounded-control text-muted-foreground hover:text-foreground hover:bg-muted-hover ml-auto"
|
|
797
|
+
>
|
|
798
|
+
<IconX :size="16" />
|
|
799
|
+
</button>
|
|
800
|
+
</div>
|
|
801
|
+
<slot name="preview" :row="previewRow" :close="closePreview" />
|
|
634
802
|
</template>
|
|
635
|
-
|
|
803
|
+
<div v-else class="flex flex-col items-center justify-center h-full gap-3 text-muted-foreground py-16">
|
|
804
|
+
<IconLayoutSidebarRight :size="32" :stroke="1.25" class="opacity-30" />
|
|
805
|
+
<p class="text-sm">Seleccioná un elemento para ver el detalle</p>
|
|
806
|
+
</div>
|
|
807
|
+
</div>
|
|
636
808
|
</div>
|
|
637
809
|
|
|
638
|
-
<!-- Preview compartido (panel overlay + chip flotante + history tab) -->
|
|
810
|
+
<!-- Preview compartido (panel overlay + chip flotante + history tab) — only when NOT in divider mode -->
|
|
639
811
|
<DataPreview
|
|
812
|
+
v-if="viewType !== 'divider'"
|
|
640
813
|
ref="dataPreviewRef"
|
|
641
814
|
v-model:row="previewRow"
|
|
642
815
|
:enabled="previewEnabled"
|