@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.
Files changed (51) hide show
  1. package/components/Attention.vue +269 -0
  2. package/components/Chart/index.vue +193 -0
  3. package/components/Container.vue +116 -0
  4. package/components/CopyButton.vue +98 -0
  5. package/components/Files/Card.vue +215 -0
  6. package/components/Files/Preview.vue +675 -0
  7. package/components/Files/Upload.vue +408 -0
  8. package/components/Files/UploadModal.vue +83 -0
  9. package/components/Forms/InputPin.vue +162 -0
  10. package/components/InfoToolbar.vue +54 -0
  11. package/components/Kanban/Standard.vue +588 -0
  12. package/components/Loader.vue +358 -0
  13. package/components/Modal/Base.vue +6 -0
  14. package/components/Modal/Confirm.vue +89 -0
  15. package/components/Modal/DeleteConfirm.vue +14 -31
  16. package/components/Modal/Form.vue +76 -0
  17. package/components/Modal/Host.vue +58 -0
  18. package/components/Modal/Prompt.vue +74 -0
  19. package/components/Modal.vue +119 -34
  20. package/components/Table/Standard.vue +198 -25
  21. package/components/Table/index.vue +169 -369
  22. package/components/Table/views/ViewCards.vue +74 -0
  23. package/components/Table/views/ViewDatabase.vue +236 -0
  24. package/components/Table/views/ViewTable.vue +299 -0
  25. package/components/Toast/Alert.vue +16 -111
  26. package/components/Toast/Container.vue +4 -29
  27. package/components/Toast/Notification.vue +8 -43
  28. package/components/Toast/Process.vue +8 -86
  29. package/components/Toast/Stack.vue +59 -0
  30. package/components/Toast/Standard.vue +115 -0
  31. package/components/Tree/Standard.vue +49 -4
  32. package/components/Workflow/Board.vue +356 -0
  33. package/components/Workflow/Builder.vue +454 -0
  34. package/components/Workflow/Form.vue +526 -0
  35. package/components/Workflow/Viewer.vue +225 -0
  36. package/components/Workflow/edges/TransitionEdge.vue +84 -0
  37. package/components/Workflow/nodes/StepNode.vue +88 -0
  38. package/composables/useClipboard.js +79 -0
  39. package/composables/useConfetti.js +194 -0
  40. package/composables/useFile.js +189 -0
  41. package/composables/useModal.js +71 -0
  42. package/composables/useToast.js +38 -65
  43. package/composables/useWorkflow/form.js +162 -0
  44. package/composables/useWorkflow/index.js +18 -0
  45. package/composables/useWorkflow/view.js +220 -0
  46. package/composables/useWorkflowLayout.js +162 -0
  47. package/css/tokens.css +10 -10
  48. package/nuxt.config.ts +1 -1
  49. package/package.json +13 -8
  50. package/stores/modal.js +48 -0
  51. 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>