@innertia-solutions/innertia-nuxt 0.1.12 → 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/components/Attention.vue +269 -0
- 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/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/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/useClipboard.js +79 -0
- package/composables/useConfetti.js +194 -0
- package/composables/useFile.js +189 -0
- package/composables/useModal.js +71 -0
- 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/nuxt.config.ts +1 -1
- package/package.json +13 -8
- package/stores/modal.js +48 -0
- package/stores/toast.js +156 -117
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { ref, computed, markRaw } from 'vue'
|
|
3
|
+
import {
|
|
4
|
+
IconLayoutKanban, IconTable, IconGitBranch, IconChartBar,
|
|
5
|
+
} from '@tabler/icons-vue'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* WorkflowBoard — vista unificada de un workflow con switcher de modos.
|
|
9
|
+
*
|
|
10
|
+
* Recibe una definición y los items (o los resultados de useWorkflowView)
|
|
11
|
+
* y renderiza la vista activa: kanban / table / flow / funnel.
|
|
12
|
+
*
|
|
13
|
+
* Diseñado para ser delgado: delega el estado al composable useWorkflowView
|
|
14
|
+
* que el consumer instancia y pasa como props.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
const props = defineProps({
|
|
18
|
+
/** { steps, transitions } — WorkflowDefinition.config */
|
|
19
|
+
definition: { type: Object, required: true },
|
|
20
|
+
/** Array de items con un campo stateKey */
|
|
21
|
+
items: { type: Array, default: () => [] },
|
|
22
|
+
/** Campo del item que indica el step actual */
|
|
23
|
+
stateKey: { type: String, default: 'current_step' },
|
|
24
|
+
/** Columnas derivadas (de useWorkflowView.columns) */
|
|
25
|
+
columns: { type: Array, default: null },
|
|
26
|
+
/** Función de movimiento validada (de useWorkflowView.onMove) */
|
|
27
|
+
onMove: { type: Function, default: null },
|
|
28
|
+
/** Datos para el funnel (de useWorkflowView.funnelData) */
|
|
29
|
+
funnelData: { type: Array, default: null },
|
|
30
|
+
/** Modo de vista activo */
|
|
31
|
+
viewMode: { type: String, default: 'kanban' },
|
|
32
|
+
/** Loading state */
|
|
33
|
+
loading: { type: Boolean, default: false },
|
|
34
|
+
/** Vistas disponibles */
|
|
35
|
+
views: { type: Array, default: () => ['kanban', 'table', 'flow', 'funnel'] },
|
|
36
|
+
/** Opciones extra para pasar a KanbanStandard */
|
|
37
|
+
kanbanProps: { type: Object, default: () => ({}) },
|
|
38
|
+
/** Opciones extra para pasar a TableStandard */
|
|
39
|
+
tableProps: { type: Object, default: () => ({}) },
|
|
40
|
+
/** Nombre para persistencia (igual que en KanbanStandard / TableStandard) */
|
|
41
|
+
name: { type: String, default: '' },
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
const emit = defineEmits(['update:viewMode', 'move', 'row-click'])
|
|
45
|
+
|
|
46
|
+
// ── Vistas disponibles ────────────────────────────────────────────────────────
|
|
47
|
+
|
|
48
|
+
const VIEW_META = [
|
|
49
|
+
{ id: 'kanban', label: 'Kanban', icon: markRaw(IconLayoutKanban) },
|
|
50
|
+
{ id: 'table', label: 'Tabla', icon: markRaw(IconTable) },
|
|
51
|
+
{ id: 'flow', label: 'Flujo', icon: markRaw(IconGitBranch) },
|
|
52
|
+
{ id: 'funnel', label: 'Embudo', icon: markRaw(IconChartBar) },
|
|
53
|
+
]
|
|
54
|
+
|
|
55
|
+
const activeViews = computed(() =>
|
|
56
|
+
VIEW_META.filter(v => props.views.includes(v.id))
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
const setView = (id) => emit('update:viewMode', id)
|
|
60
|
+
|
|
61
|
+
// ── Columnas derivadas (fallback si no se pasan) ───────────────────────────────
|
|
62
|
+
|
|
63
|
+
const STEP_TYPE_COLOR = {
|
|
64
|
+
start: 'slate',
|
|
65
|
+
in_progress: 'blue',
|
|
66
|
+
pause_internal: 'amber',
|
|
67
|
+
pause_external: 'orange',
|
|
68
|
+
finished: 'green',
|
|
69
|
+
cancelled: 'red',
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const resolvedColumns = computed(() =>
|
|
73
|
+
props.columns ?? (props.definition?.steps ?? []).map(step => ({
|
|
74
|
+
key: step.key,
|
|
75
|
+
label: step.label,
|
|
76
|
+
color: STEP_TYPE_COLOR[step.type] ?? 'slate',
|
|
77
|
+
type: step.type,
|
|
78
|
+
}))
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
// ── DnD ──────────────────────────────────────────────────────────────────────
|
|
82
|
+
|
|
83
|
+
const draggedId = ref(null)
|
|
84
|
+
const draggedFrom = ref(null)
|
|
85
|
+
const dragOverCol = ref(null)
|
|
86
|
+
|
|
87
|
+
const onDragStart = (item, colKey) => {
|
|
88
|
+
draggedId.value = item.id
|
|
89
|
+
draggedFrom.value = colKey
|
|
90
|
+
}
|
|
91
|
+
const onDragOver = (e, colKey) => { e.preventDefault(); dragOverCol.value = colKey }
|
|
92
|
+
const onDragLeave = (e) => {
|
|
93
|
+
// Ignorar si el mouse sigue dentro del mismo column (hijo → padre)
|
|
94
|
+
if (!e.currentTarget.contains(e.relatedTarget)) dragOverCol.value = null
|
|
95
|
+
}
|
|
96
|
+
const onDrop = async (targetCol) => {
|
|
97
|
+
dragOverCol.value = null
|
|
98
|
+
const id = draggedId.value
|
|
99
|
+
const from = draggedFrom.value
|
|
100
|
+
draggedId.value = null
|
|
101
|
+
draggedFrom.value = null
|
|
102
|
+
if (!id || from === targetCol) return
|
|
103
|
+
await moveMutation(id, targetCol)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// ── Color map para drag-over highlight ────────────────────────────────────────
|
|
107
|
+
|
|
108
|
+
const COLOR_OVER = {
|
|
109
|
+
slate: 'ring-2 ring-slate-400/60',
|
|
110
|
+
blue: 'ring-2 ring-blue-400/60',
|
|
111
|
+
amber: 'ring-2 ring-amber-400/60',
|
|
112
|
+
orange: 'ring-2 ring-orange-400/60',
|
|
113
|
+
green: 'ring-2 ring-green-400/60',
|
|
114
|
+
red: 'ring-2 ring-red-400/60',
|
|
115
|
+
violet: 'ring-2 ring-violet-400/60',
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const COLOR_DOT = {
|
|
119
|
+
slate: 'bg-slate-400',
|
|
120
|
+
blue: 'bg-blue-500',
|
|
121
|
+
amber: 'bg-amber-500',
|
|
122
|
+
orange: 'bg-orange-500',
|
|
123
|
+
green: 'bg-green-500',
|
|
124
|
+
red: 'bg-red-500',
|
|
125
|
+
violet: 'bg-violet-500',
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// ── Move mutation ─────────────────────────────────────────────────────────────
|
|
129
|
+
|
|
130
|
+
const moveMutation = async (id, toStep) => {
|
|
131
|
+
const item = props.items.find(i => i.id === id)
|
|
132
|
+
const fromStep = item?.[props.stateKey]
|
|
133
|
+
if (props.onMove) {
|
|
134
|
+
const ok = await props.onMove(id, toStep, fromStep)
|
|
135
|
+
if (ok !== false) emit('move', { id, from: fromStep, to: toStep })
|
|
136
|
+
return ok
|
|
137
|
+
}
|
|
138
|
+
emit('move', { id, from: fromStep, to: toStep })
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// ── Funnel series para Chart ──────────────────────────────────────────────────
|
|
142
|
+
|
|
143
|
+
const funnelSeries = computed(() => {
|
|
144
|
+
const data = props.funnelData ?? resolvedColumns.value.map(col => ({
|
|
145
|
+
key: col.key,
|
|
146
|
+
label: col.label,
|
|
147
|
+
count: props.items.filter(i => i[props.stateKey] === col.key).length,
|
|
148
|
+
}))
|
|
149
|
+
return [{
|
|
150
|
+
name: 'Items',
|
|
151
|
+
data: data.map(d => d.count),
|
|
152
|
+
}]
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
const funnelCategories = computed(() => {
|
|
156
|
+
const data = props.funnelData ?? resolvedColumns.value
|
|
157
|
+
return data.map(d => d.label)
|
|
158
|
+
})
|
|
159
|
+
</script>
|
|
160
|
+
|
|
161
|
+
<template>
|
|
162
|
+
<div class="flex flex-col h-full min-h-0">
|
|
163
|
+
|
|
164
|
+
<!-- ── Toolbar con switcher ──────────────────────────────────────────────── -->
|
|
165
|
+
<div class="flex items-center gap-3 px-3 py-2 border-b border-card-line bg-surface shrink-0">
|
|
166
|
+
|
|
167
|
+
<!-- View switcher -->
|
|
168
|
+
<div class="inline-flex items-center rounded-control border border-card-line bg-card overflow-hidden">
|
|
169
|
+
<button
|
|
170
|
+
v-for="view in activeViews"
|
|
171
|
+
:key="view.id"
|
|
172
|
+
type="button"
|
|
173
|
+
@click="setView(view.id)"
|
|
174
|
+
:title="view.label"
|
|
175
|
+
:class="[
|
|
176
|
+
'inline-flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium transition-colors',
|
|
177
|
+
viewMode === view.id
|
|
178
|
+
? 'bg-primary text-primary-foreground'
|
|
179
|
+
: 'text-muted-foreground hover:bg-muted-hover',
|
|
180
|
+
]"
|
|
181
|
+
>
|
|
182
|
+
<component :is="view.icon" :size="14" :stroke="1.75" />
|
|
183
|
+
<span class="hidden sm:inline">{{ view.label }}</span>
|
|
184
|
+
</button>
|
|
185
|
+
</div>
|
|
186
|
+
|
|
187
|
+
<!-- Slot para toolbar extra del consumer -->
|
|
188
|
+
<slot name="toolbar" :view-mode="viewMode" :columns="resolvedColumns" />
|
|
189
|
+
|
|
190
|
+
<div class="flex-1" />
|
|
191
|
+
|
|
192
|
+
<!-- Loading indicator -->
|
|
193
|
+
<div v-if="loading" class="flex items-center gap-1.5 text-xs text-muted-foreground">
|
|
194
|
+
<svg class="w-3.5 h-3.5 animate-spin" viewBox="0 0 24 24" fill="none">
|
|
195
|
+
<circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="3" stroke-dasharray="32" stroke-dashoffset="12" />
|
|
196
|
+
</svg>
|
|
197
|
+
Cargando…
|
|
198
|
+
</div>
|
|
199
|
+
</div>
|
|
200
|
+
|
|
201
|
+
<!-- ── Vistas ────────────────────────────────────────────────────────────── -->
|
|
202
|
+
<div class="flex-1 min-h-0 overflow-hidden">
|
|
203
|
+
|
|
204
|
+
<!-- KANBAN -->
|
|
205
|
+
<template v-if="viewMode === 'kanban'">
|
|
206
|
+
<slot
|
|
207
|
+
name="kanban"
|
|
208
|
+
:columns="resolvedColumns"
|
|
209
|
+
:items="items"
|
|
210
|
+
:move-mutation="moveMutation"
|
|
211
|
+
:state-key="stateKey"
|
|
212
|
+
:on-drag-start="onDragStart"
|
|
213
|
+
:on-drag-over="onDragOver"
|
|
214
|
+
:on-drag-leave="onDragLeave"
|
|
215
|
+
:on-drop="onDrop"
|
|
216
|
+
>
|
|
217
|
+
<!-- Default: kanban con DnD HTML5 -->
|
|
218
|
+
<div class="h-full overflow-x-auto">
|
|
219
|
+
<div class="flex gap-3 p-3 h-full min-w-max items-start">
|
|
220
|
+
<div
|
|
221
|
+
v-for="col in resolvedColumns"
|
|
222
|
+
:key="col.key"
|
|
223
|
+
class="flex flex-col w-72 shrink-0 rounded-card bg-muted/50 overflow-hidden transition-shadow"
|
|
224
|
+
:class="dragOverCol === col.key ? (COLOR_OVER[col.color] ?? COLOR_OVER.slate) : ''"
|
|
225
|
+
@dragover="onDragOver($event, col.key)"
|
|
226
|
+
@dragleave="onDragLeave($event)"
|
|
227
|
+
@drop="onDrop(col.key)"
|
|
228
|
+
>
|
|
229
|
+
<!-- Column header -->
|
|
230
|
+
<div class="flex items-center justify-between px-3 py-2.5">
|
|
231
|
+
<div class="flex items-center gap-2">
|
|
232
|
+
<span class="w-2 h-2 rounded-full shrink-0" :class="COLOR_DOT[col.color] ?? 'bg-slate-400'" />
|
|
233
|
+
<span class="text-[11px] font-semibold text-foreground uppercase tracking-wider">{{ col.label }}</span>
|
|
234
|
+
</div>
|
|
235
|
+
<span class="text-[11px] font-semibold text-muted-foreground bg-card/70 rounded-badge px-2 py-0.5 tabular-nums">
|
|
236
|
+
{{ items.filter(i => i[stateKey] === col.key).length }}
|
|
237
|
+
</span>
|
|
238
|
+
</div>
|
|
239
|
+
|
|
240
|
+
<!-- Cards drop zone -->
|
|
241
|
+
<div class="flex-1 overflow-y-auto max-h-[calc(100%-2.75rem)] flex flex-col gap-2 p-2 min-h-16">
|
|
242
|
+
<div
|
|
243
|
+
v-for="item in items.filter(i => i[stateKey] === col.key)"
|
|
244
|
+
:key="item.id"
|
|
245
|
+
draggable="true"
|
|
246
|
+
@dragstart="onDragStart(item, col.key)"
|
|
247
|
+
@click="emit('row-click', item)"
|
|
248
|
+
class="bg-card rounded-card border border-card-line p-3 cursor-grab active:cursor-grabbing hover:border-primary/40 hover:shadow-sm transition-all select-none"
|
|
249
|
+
:class="draggedId === item.id ? 'opacity-40' : ''"
|
|
250
|
+
>
|
|
251
|
+
<slot name="card" :item="item" :column="col">
|
|
252
|
+
<p class="text-sm font-medium text-foreground truncate">{{ item.title ?? item.name ?? item.id }}</p>
|
|
253
|
+
<p class="text-xs text-muted-foreground font-mono mt-1">{{ item.id?.slice(0, 8) }}</p>
|
|
254
|
+
</slot>
|
|
255
|
+
</div>
|
|
256
|
+
|
|
257
|
+
<div
|
|
258
|
+
v-if="!items.filter(i => i[stateKey] === col.key).length"
|
|
259
|
+
class="flex-1 flex items-center justify-center py-4 text-xs text-muted-foreground-2 italic"
|
|
260
|
+
>
|
|
261
|
+
Vacío
|
|
262
|
+
</div>
|
|
263
|
+
</div>
|
|
264
|
+
</div>
|
|
265
|
+
</div>
|
|
266
|
+
</div>
|
|
267
|
+
</slot>
|
|
268
|
+
</template>
|
|
269
|
+
|
|
270
|
+
<!-- TABLE -->
|
|
271
|
+
<template v-if="viewMode === 'table'">
|
|
272
|
+
<slot name="table" :columns="resolvedColumns" :items="items" :state-key="stateKey">
|
|
273
|
+
<!-- Tabla simple de fallback -->
|
|
274
|
+
<div class="h-full overflow-auto p-4">
|
|
275
|
+
<table class="w-full text-sm">
|
|
276
|
+
<thead>
|
|
277
|
+
<tr class="border-b border-card-line">
|
|
278
|
+
<th class="text-left py-2 px-3 text-xs font-semibold text-muted-foreground uppercase tracking-wider">ID</th>
|
|
279
|
+
<th class="text-left py-2 px-3 text-xs font-semibold text-muted-foreground uppercase tracking-wider">Nombre</th>
|
|
280
|
+
<th class="text-left py-2 px-3 text-xs font-semibold text-muted-foreground uppercase tracking-wider">Step actual</th>
|
|
281
|
+
</tr>
|
|
282
|
+
</thead>
|
|
283
|
+
<tbody class="divide-y divide-card-line/50">
|
|
284
|
+
<tr
|
|
285
|
+
v-for="item in items"
|
|
286
|
+
:key="item.id"
|
|
287
|
+
class="hover:bg-layer-hover cursor-pointer transition-colors"
|
|
288
|
+
@click="emit('row-click', item)"
|
|
289
|
+
>
|
|
290
|
+
<td class="py-2 px-3 font-mono text-xs text-muted-foreground">{{ item.id?.slice(0, 8) }}</td>
|
|
291
|
+
<td class="py-2 px-3 text-foreground">{{ item.title ?? item.name ?? '—' }}</td>
|
|
292
|
+
<td class="py-2 px-3">
|
|
293
|
+
<span class="inline-flex items-center gap-1.5 text-xs px-2 py-0.5 rounded-badge bg-muted text-foreground">
|
|
294
|
+
{{ resolvedColumns.find(c => c.key === item[stateKey])?.label ?? item[stateKey] ?? '—' }}
|
|
295
|
+
</span>
|
|
296
|
+
</td>
|
|
297
|
+
</tr>
|
|
298
|
+
</tbody>
|
|
299
|
+
</table>
|
|
300
|
+
</div>
|
|
301
|
+
</slot>
|
|
302
|
+
</template>
|
|
303
|
+
|
|
304
|
+
<!-- FLOW -->
|
|
305
|
+
<template v-if="viewMode === 'flow'">
|
|
306
|
+
<slot name="flow" :definition="definition" :items="items" :state-key="stateKey">
|
|
307
|
+
<WorkflowViewer
|
|
308
|
+
:definition="definition"
|
|
309
|
+
class="h-full"
|
|
310
|
+
/>
|
|
311
|
+
</slot>
|
|
312
|
+
</template>
|
|
313
|
+
|
|
314
|
+
<!-- FUNNEL -->
|
|
315
|
+
<template v-if="viewMode === 'funnel'">
|
|
316
|
+
<slot name="funnel" :funnel-data="funnelData" :columns="resolvedColumns">
|
|
317
|
+
<div class="h-full overflow-auto p-6">
|
|
318
|
+
<div class="max-w-2xl mx-auto space-y-3">
|
|
319
|
+
<p class="text-sm font-medium text-foreground mb-4">Distribución por step</p>
|
|
320
|
+
<div
|
|
321
|
+
v-for="(col, idx) in resolvedColumns"
|
|
322
|
+
:key="col.key"
|
|
323
|
+
class="flex items-center gap-3"
|
|
324
|
+
>
|
|
325
|
+
<span class="text-xs text-muted-foreground w-32 truncate text-right shrink-0">{{ col.label }}</span>
|
|
326
|
+
<div class="flex-1 h-8 bg-surface rounded-control border border-card-line overflow-hidden">
|
|
327
|
+
<div
|
|
328
|
+
:style="{
|
|
329
|
+
width: items.length > 0
|
|
330
|
+
? Math.max(4, (items.filter(i => i[stateKey] === col.key).length / items.length) * 100) + '%'
|
|
331
|
+
: '0%',
|
|
332
|
+
}"
|
|
333
|
+
:class="[
|
|
334
|
+
'h-full transition-all duration-500 rounded-control',
|
|
335
|
+
col.color === 'blue' ? 'bg-blue-400/70 dark:bg-blue-600/60' :
|
|
336
|
+
col.color === 'green' ? 'bg-green-400/70 dark:bg-green-600/60' :
|
|
337
|
+
col.color === 'amber' ? 'bg-amber-400/70 dark:bg-amber-600/60' :
|
|
338
|
+
col.color === 'orange' ? 'bg-orange-400/70 dark:bg-orange-600/60':
|
|
339
|
+
col.color === 'red' ? 'bg-red-400/70 dark:bg-red-600/60' :
|
|
340
|
+
col.color === 'violet' ? 'bg-violet-400/70 dark:bg-violet-600/60':
|
|
341
|
+
'bg-slate-400/70 dark:bg-slate-600/60'
|
|
342
|
+
]"
|
|
343
|
+
/>
|
|
344
|
+
</div>
|
|
345
|
+
<span class="text-xs font-semibold text-foreground tabular-nums w-8 shrink-0">
|
|
346
|
+
{{ items.filter(i => i[stateKey] === col.key).length }}
|
|
347
|
+
</span>
|
|
348
|
+
</div>
|
|
349
|
+
</div>
|
|
350
|
+
</div>
|
|
351
|
+
</slot>
|
|
352
|
+
</template>
|
|
353
|
+
|
|
354
|
+
</div>
|
|
355
|
+
</div>
|
|
356
|
+
</template>
|