@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,526 @@
1
+ <script setup>
2
+ import { ref, computed } from 'vue'
3
+ import {
4
+ IconPlus, IconTrash, IconChevronDown, IconChevronUp,
5
+ IconGripVertical, IconArrowRight, IconAlertTriangle,
6
+ } from '@tabler/icons-vue'
7
+
8
+ /**
9
+ * WorkflowForm — editor estructurado de WorkflowDefinition.
10
+ * Alternativa al canvas Builder.vue para flujos lineales o usuarios no técnicos.
11
+ *
12
+ * Usa useWorkflowForm internamente o acepta modelValue externo.
13
+ */
14
+
15
+ const props = defineProps({
16
+ /** Definición inicial { steps:[], transitions:[] } */
17
+ modelValue: { type: Object, default: null },
18
+ /** Solo lectura */
19
+ readonly: { type: Boolean, default: false },
20
+ })
21
+
22
+ const emit = defineEmits(['update:modelValue'])
23
+
24
+ // ── Composable de edición ─────────────────────────────────────────────────────
25
+ const {
26
+ definition, isDirty, isValid,
27
+ addStep, updateStep, deleteStep, reorderSteps,
28
+ addTransition, updateTransition, deleteTransition,
29
+ addRestriction, removeRestriction,
30
+ transitionsFrom,
31
+ } = useWorkflowForm(props.modelValue)
32
+
33
+ // Sync hacia arriba
34
+ watch(definition, (val) => emit('update:modelValue', val), { deep: true })
35
+
36
+ // Sync desde arriba (si el parent cambia modelValue externamente)
37
+ watch(() => props.modelValue, (val) => {
38
+ if (val && JSON.stringify(val) !== JSON.stringify(definition.value)) {
39
+ definition.value = JSON.parse(JSON.stringify(val))
40
+ }
41
+ }, { deep: true })
42
+
43
+ // ── Constantes de tipos ───────────────────────────────────────────────────────
44
+
45
+ const STEP_TYPES = [
46
+ { value: 'start', label: 'Inicio', color: 'emerald', description: 'Punto de entrada — solo uno permitido' },
47
+ { value: 'in_progress', label: 'En progreso', color: 'blue', description: 'Paso activo del proceso' },
48
+ { value: 'pause_internal', label: 'Pausa interna', color: 'amber', description: 'Pausado por el sistema' },
49
+ { value: 'pause_external', label: 'Pausa externa', color: 'orange', description: 'Esperando acción externa' },
50
+ { value: 'finished', label: 'Finalizado', color: 'violet', description: 'Estado terminal exitoso' },
51
+ { value: 'cancelled', label: 'Cancelado', color: 'red', description: 'Estado terminal cancelado' },
52
+ ]
53
+
54
+ const RESTRICTION_TYPES = [
55
+ { value: 'role', label: 'Rol', fields: ['roles'] },
56
+ { value: 'required_fields', label: 'Campos requeridos', fields: ['fields'] },
57
+ { value: 'approval', label: 'Aprobación', fields: ['role'] },
58
+ { value: 'checklist', label: 'Checklist completo', fields: ['checklist'] },
59
+ { value: 'document', label: 'Documento requerido', fields: ['document'] },
60
+ { value: 'min_items', label: 'Mínimo de ítems', fields: ['relation', 'min'] },
61
+ { value: 'date', label: 'Fecha mínima', fields: ['after'] },
62
+ { value: 'custom', label: 'Personalizado', fields: ['entity', 'field', 'value'] },
63
+ ]
64
+
65
+ const stepTypeColor = (type) => STEP_TYPES.find(t => t.value === type)?.color ?? 'slate'
66
+ const stepTypeLabel = (type) => STEP_TYPES.find(t => t.value === type)?.label ?? type
67
+
68
+ // ── Steps: nuevo step ─────────────────────────────────────────────────────────
69
+
70
+ const showNewStep = ref(false)
71
+ const newStep = ref({ key: '', label: '', type: 'in_progress', description: '' })
72
+ const newStepError = ref('')
73
+
74
+ const submitNewStep = () => {
75
+ newStepError.value = ''
76
+ try {
77
+ addStep({ ...newStep.value })
78
+ newStep.value = { key: '', label: '', type: 'in_progress', description: '' }
79
+ showNewStep.value = false
80
+ } catch (e) {
81
+ newStepError.value = e.message
82
+ }
83
+ }
84
+
85
+ // ── Steps: edición inline ─────────────────────────────────────────────────────
86
+
87
+ const editingStep = ref(null) // key del step en edición
88
+ const stepDraft = ref({})
89
+
90
+ const startEditStep = (step) => {
91
+ editingStep.value = step.key
92
+ stepDraft.value = { ...step }
93
+ }
94
+
95
+ const saveStep = () => {
96
+ try {
97
+ updateStep(editingStep.value, stepDraft.value)
98
+ editingStep.value = null
99
+ } catch (e) {}
100
+ }
101
+
102
+ const cancelEditStep = () => { editingStep.value = null }
103
+
104
+ // ── Steps: reorder (drag handle simple con botones up/down) ──────────────────
105
+
106
+ const moveStepUp = (idx) => { if (idx > 0) reorderSteps(idx, idx - 1) }
107
+ const moveStepDown = (idx) => { if (idx < definition.value.steps.length - 1) reorderSteps(idx, idx + 1) }
108
+
109
+ // ── Transiciones: panel expandible por step ──────────────────────────────────
110
+
111
+ const expandedStepKey = ref(null)
112
+ const toggleStep = (key) => {
113
+ expandedStepKey.value = expandedStepKey.value === key ? null : key
114
+ }
115
+
116
+ // ── Transiciones: nuevo ───────────────────────────────────────────────────────
117
+
118
+ const showingAddTransition = ref(null) // step.key que está mostrando el form
119
+ const newTransition = ref({ to: '', label: '', description: '' })
120
+ const newTransitionError = ref('')
121
+
122
+ const openAddTransition = (fromKey) => {
123
+ showingAddTransition.value = fromKey
124
+ newTransition.value = { to: '', label: '', description: '' }
125
+ newTransitionError.value = ''
126
+ }
127
+
128
+ const submitTransition = (fromKey) => {
129
+ newTransitionError.value = ''
130
+ try {
131
+ addTransition({
132
+ from: fromKey,
133
+ to: newTransition.value.to,
134
+ label: newTransition.value.label,
135
+ description: newTransition.value.description,
136
+ restrictions: [],
137
+ })
138
+ showingAddTransition.value = null
139
+ } catch (e) {
140
+ newTransitionError.value = e.message
141
+ }
142
+ }
143
+
144
+ // ── Restricciones ─────────────────────────────────────────────────────────────
145
+
146
+ const expandedTransition = ref(null) // `${from}__${to}`
147
+ const toggleTransition = (from, to) => {
148
+ const key = `${from}__${to}`
149
+ expandedTransition.value = expandedTransition.value === key ? null : key
150
+ }
151
+
152
+ const isTransitionExpanded = (from, to) => expandedTransition.value === `${from}__${to}`
153
+
154
+ const addRestrictionTo = (from, to) => {
155
+ addRestriction(from, to, { type: 'role', roles: [], message: '' })
156
+ }
157
+
158
+ // ── Validaciones visuales ────────────────────────────────────────────────────
159
+
160
+ const warnings = computed(() => {
161
+ const w = []
162
+ const steps = definition.value.steps ?? []
163
+ const transitions = definition.value.transitions ?? []
164
+
165
+ if (!steps.some(s => s.type === 'start'))
166
+ w.push('Falta un step de tipo Inicio')
167
+ if (!steps.some(s => ['finished', 'cancelled'].includes(s.type)))
168
+ w.push('Falta al menos un step terminal (Finalizado o Cancelado)')
169
+
170
+ const startSteps = steps.filter(s => s.type === 'start')
171
+ if (startSteps.length > 1)
172
+ w.push('Solo puede haber un step de tipo Inicio')
173
+
174
+ // Steps sin transiciones salientes (excepto terminales)
175
+ for (const step of steps) {
176
+ if (['finished', 'cancelled'].includes(step.type)) continue
177
+ if (!transitions.some(t => t.from === step.key))
178
+ w.push(`Step "${step.label}" no tiene transiciones salientes`)
179
+ }
180
+
181
+ return w
182
+ })
183
+
184
+ // ── Computed para el selector de destino ─────────────────────────────────────
185
+
186
+ const availableTargets = (fromKey) =>
187
+ definition.value.steps.filter(s => {
188
+ if (s.key === fromKey) return false
189
+ // No ofrecer si ya existe esa transición
190
+ return !definition.value.transitions.some(t => t.from === fromKey && t.to === s.key)
191
+ })
192
+ </script>
193
+
194
+ <template>
195
+ <div class="flex flex-col gap-4">
196
+
197
+ <!-- ── Warnings de validación ─────────────────────────────────────────── -->
198
+ <div v-if="warnings.length" class="rounded-card border border-amber-300 dark:border-amber-700 bg-amber-50 dark:bg-amber-900/20 p-3 space-y-1">
199
+ <div v-for="w in warnings" :key="w" class="flex items-start gap-2 text-xs text-amber-700 dark:text-amber-400">
200
+ <IconAlertTriangle :size="13" class="shrink-0 mt-0.5" />
201
+ {{ w }}
202
+ </div>
203
+ </div>
204
+
205
+ <!-- ── STEPS ──────────────────────────────────────────────────────────── -->
206
+ <div class="space-y-2">
207
+ <div class="flex items-center justify-between">
208
+ <h3 class="text-sm font-semibold text-foreground">Steps</h3>
209
+ <button
210
+ v-if="!readonly"
211
+ type="button"
212
+ @click="showNewStep = !showNewStep"
213
+ class="inline-flex items-center gap-1 text-xs font-medium text-primary hover:underline"
214
+ >
215
+ <IconPlus :size="13" /> Agregar step
216
+ </button>
217
+ </div>
218
+
219
+ <!-- Form nuevo step -->
220
+ <Transition
221
+ enter-active-class="transition-all duration-150 ease-out"
222
+ enter-from-class="opacity-0 -translate-y-1"
223
+ enter-to-class="opacity-100 translate-y-0"
224
+ leave-active-class="transition-all duration-100"
225
+ leave-from-class="opacity-100"
226
+ leave-to-class="opacity-0"
227
+ >
228
+ <div v-if="showNewStep" class="rounded-card border border-primary/30 bg-primary/5 p-3 space-y-3">
229
+ <p class="text-xs font-medium text-foreground">Nuevo step</p>
230
+ <div class="grid grid-cols-2 gap-2">
231
+ <div class="space-y-1">
232
+ <label class="text-[11px] text-muted-foreground">Clave</label>
233
+ <input
234
+ v-model="newStep.key"
235
+ placeholder="ej. revision"
236
+ @keydown.enter="submitNewStep"
237
+ class="w-full h-7 px-2 text-xs rounded-control border border-card-line bg-card text-foreground focus:outline-none focus:border-primary"
238
+ />
239
+ </div>
240
+ <div class="space-y-1">
241
+ <label class="text-[11px] text-muted-foreground">Label</label>
242
+ <input
243
+ v-model="newStep.label"
244
+ placeholder="ej. Revisión"
245
+ @keydown.enter="submitNewStep"
246
+ class="w-full h-7 px-2 text-xs rounded-control border border-card-line bg-card text-foreground focus:outline-none focus:border-primary"
247
+ />
248
+ </div>
249
+ </div>
250
+ <div class="space-y-1">
251
+ <label class="text-[11px] text-muted-foreground">Tipo</label>
252
+ <div class="grid grid-cols-2 sm:grid-cols-3 gap-1.5">
253
+ <button
254
+ v-for="t in STEP_TYPES"
255
+ :key="t.value"
256
+ type="button"
257
+ @click="newStep.type = t.value"
258
+ :class="[
259
+ 'flex items-center gap-1.5 px-2 py-1.5 rounded-control border text-xs transition-colors text-left',
260
+ newStep.type === t.value
261
+ ? 'border-primary bg-primary/10 text-primary font-medium'
262
+ : 'border-card-line text-muted-foreground hover:border-primary/50',
263
+ ]"
264
+ >
265
+ <span :class="[
266
+ 'w-2 h-2 rounded-full shrink-0',
267
+ t.color === 'emerald' ? 'bg-emerald-500' : t.color === 'blue' ? 'bg-blue-500' :
268
+ t.color === 'amber' ? 'bg-amber-500' : t.color === 'orange' ? 'bg-orange-500' :
269
+ t.color === 'violet' ? 'bg-violet-500' : 'bg-red-500'
270
+ ]" />
271
+ {{ t.label }}
272
+ </button>
273
+ </div>
274
+ </div>
275
+ <div class="space-y-1">
276
+ <label class="text-[11px] text-muted-foreground">Descripción (opcional)</label>
277
+ <input
278
+ v-model="newStep.description"
279
+ placeholder="Describe qué sucede en este step"
280
+ class="w-full h-7 px-2 text-xs rounded-control border border-card-line bg-card text-foreground focus:outline-none focus:border-primary"
281
+ />
282
+ </div>
283
+ <p v-if="newStepError" class="text-xs text-red-500">{{ newStepError }}</p>
284
+ <div class="flex justify-end gap-2">
285
+ <button type="button" @click="showNewStep = false" class="px-3 py-1.5 text-xs text-muted-foreground hover:bg-muted rounded-control transition-colors">Cancelar</button>
286
+ <button type="button" @click="submitNewStep" :disabled="!newStep.key || !newStep.label" class="px-3 py-1.5 text-xs bg-primary text-primary-foreground hover:bg-primary-hover rounded-control transition-colors disabled:opacity-50">Agregar</button>
287
+ </div>
288
+ </div>
289
+ </Transition>
290
+
291
+ <!-- Lista de steps -->
292
+ <div class="rounded-card border border-card-line overflow-hidden divide-y divide-card-line">
293
+
294
+ <div v-if="!definition.steps.length" class="px-4 py-6 text-center text-sm text-muted-foreground italic">
295
+ No hay steps. Agrega el primero.
296
+ </div>
297
+
298
+ <div
299
+ v-for="(step, idx) in definition.steps"
300
+ :key="step.key"
301
+ class="bg-card"
302
+ >
303
+ <!-- Step row -->
304
+ <div class="flex items-center gap-2 px-3 py-2.5">
305
+
306
+ <!-- Reorder buttons -->
307
+ <div v-if="!readonly" class="flex flex-col gap-0.5 shrink-0">
308
+ <button type="button" @click="moveStepUp(idx)" :disabled="idx === 0" class="text-muted-foreground hover:text-foreground disabled:opacity-20 transition-colors">
309
+ <IconChevronUp :size="12" />
310
+ </button>
311
+ <button type="button" @click="moveStepDown(idx)" :disabled="idx === definition.steps.length - 1" class="text-muted-foreground hover:text-foreground disabled:opacity-20 transition-colors">
312
+ <IconChevronDown :size="12" />
313
+ </button>
314
+ </div>
315
+
316
+ <!-- Type dot -->
317
+ <span :class="[
318
+ 'w-2.5 h-2.5 rounded-full shrink-0',
319
+ stepTypeColor(step.type) === 'emerald' ? 'bg-emerald-500' :
320
+ stepTypeColor(step.type) === 'blue' ? 'bg-blue-500' :
321
+ stepTypeColor(step.type) === 'amber' ? 'bg-amber-500' :
322
+ stepTypeColor(step.type) === 'orange' ? 'bg-orange-500' :
323
+ stepTypeColor(step.type) === 'violet' ? 'bg-violet-500' :
324
+ stepTypeColor(step.type) === 'red' ? 'bg-red-500' : 'bg-slate-400'
325
+ ]" />
326
+
327
+ <!-- Info -->
328
+ <div class="flex-1 min-w-0">
329
+ <template v-if="editingStep === step.key">
330
+ <div class="flex items-center gap-2">
331
+ <input v-model="stepDraft.label" @keydown.enter="saveStep" @keydown.escape="cancelEditStep" class="flex-1 h-7 px-2 text-xs rounded-control border border-primary bg-card text-foreground focus:outline-none" />
332
+ <input v-model="stepDraft.key" @keydown.enter="saveStep" @keydown.escape="cancelEditStep" class="w-28 h-7 px-2 text-xs font-mono rounded-control border border-card-line bg-card text-foreground focus:outline-none focus:border-primary" />
333
+ <select v-model="stepDraft.type" class="h-7 px-2 text-xs rounded-control border border-card-line bg-card text-foreground focus:outline-none focus:border-primary">
334
+ <option v-for="t in STEP_TYPES" :key="t.value" :value="t.value">{{ t.label }}</option>
335
+ </select>
336
+ <button type="button" @click="saveStep" class="text-xs font-medium text-primary hover:underline shrink-0">OK</button>
337
+ <button type="button" @click="cancelEditStep" class="text-xs text-muted-foreground hover:underline shrink-0">×</button>
338
+ </div>
339
+ </template>
340
+ <template v-else>
341
+ <div class="flex items-center gap-2">
342
+ <span class="text-sm font-medium text-foreground truncate">{{ step.label }}</span>
343
+ <span class="text-xs font-mono text-muted-foreground hidden sm:inline">{{ step.key }}</span>
344
+ <span class="text-[10px] px-1.5 py-0.5 rounded-badge bg-muted text-muted-foreground shrink-0">{{ stepTypeLabel(step.type) }}</span>
345
+ </div>
346
+ <p v-if="step.description" class="text-xs text-muted-foreground truncate mt-0.5">{{ step.description }}</p>
347
+ </template>
348
+ </div>
349
+
350
+ <!-- Transiciones badge -->
351
+ <button
352
+ type="button"
353
+ @click="toggleStep(step.key)"
354
+ class="shrink-0 flex items-center gap-1 text-xs text-muted-foreground hover:text-foreground transition-colors px-2 py-1 rounded-control hover:bg-muted"
355
+ >
356
+ <IconArrowRight :size="12" />
357
+ <span class="tabular-nums">{{ transitionsFrom(step.key).length }}</span>
358
+ <IconChevronDown :size="12" :class="['transition-transform', expandedStepKey === step.key ? 'rotate-180' : '']" />
359
+ </button>
360
+
361
+ <!-- Edit / Delete -->
362
+ <template v-if="!readonly">
363
+ <button v-if="editingStep !== step.key" type="button" @click="startEditStep(step)" class="shrink-0 text-xs text-muted-foreground hover:text-foreground transition-colors p-1">
364
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor" class="w-3.5 h-3.5">
365
+ <path d="M13.488 2.513a1.75 1.75 0 0 0-2.475 0L3.76 9.768a1.75 1.75 0 0 0-.455.88l-.5 2.5a.75.75 0 0 0 .875.876l2.5-.5a1.75 1.75 0 0 0 .88-.455l7.255-7.254a1.75 1.75 0 0 0 0-2.474Z" />
366
+ </svg>
367
+ </button>
368
+ <button type="button" @click="deleteStep(step.key)" class="shrink-0 text-red-400 hover:text-red-600 transition-colors p-1">
369
+ <IconTrash :size="14" />
370
+ </button>
371
+ </template>
372
+ </div>
373
+
374
+ <!-- Panel de transiciones del step (expandible) -->
375
+ <Transition
376
+ enter-active-class="transition-all duration-150 ease-out"
377
+ enter-from-class="opacity-0"
378
+ enter-to-class="opacity-100"
379
+ leave-active-class="transition-all duration-100"
380
+ leave-from-class="opacity-100"
381
+ leave-to-class="opacity-0"
382
+ >
383
+ <div v-if="expandedStepKey === step.key" class="border-t border-card-line bg-surface px-3 py-3 space-y-2">
384
+
385
+ <!-- Lista de transiciones -->
386
+ <div
387
+ v-for="t in transitionsFrom(step.key)"
388
+ :key="t.to"
389
+ class="rounded-control border border-card-line bg-card overflow-hidden"
390
+ >
391
+ <!-- Transition row -->
392
+ <div class="flex items-center gap-2 px-3 py-2">
393
+ <IconArrowRight :size="13" class="text-muted-foreground-2 shrink-0" />
394
+ <div class="flex-1 min-w-0">
395
+ <span class="text-xs font-medium text-foreground">
396
+ {{ definition.steps.find(s => s.key === t.to)?.label ?? t.to }}
397
+ </span>
398
+ <span v-if="t.label" class="text-xs text-muted-foreground ml-1.5 italic">— {{ t.label }}</span>
399
+ </div>
400
+ <button
401
+ type="button"
402
+ @click="toggleTransition(step.key, t.to)"
403
+ class="shrink-0 text-xs text-muted-foreground hover:text-foreground flex items-center gap-1 px-1.5 py-0.5 rounded hover:bg-muted transition-colors"
404
+ >
405
+ <span v-if="t.restrictions?.length" class="font-medium">{{ t.restrictions.length }} restricción{{ t.restrictions.length !== 1 ? 'es' : '' }}</span>
406
+ <span v-else class="italic">sin restricciones</span>
407
+ <IconChevronDown :size="11" :class="['transition-transform', isTransitionExpanded(step.key, t.to) ? 'rotate-180' : '']" />
408
+ </button>
409
+ <button v-if="!readonly" type="button" @click="deleteTransition(step.key, t.to)" class="shrink-0 text-red-400 hover:text-red-600 transition-colors p-1">
410
+ <IconTrash :size="12" />
411
+ </button>
412
+ </div>
413
+
414
+ <!-- Restrictions panel -->
415
+ <div v-if="isTransitionExpanded(step.key, t.to)" class="border-t border-card-line bg-surface px-3 py-2.5 space-y-2">
416
+
417
+ <div
418
+ v-for="(r, ridx) in (t.restrictions ?? [])"
419
+ :key="ridx"
420
+ class="rounded border border-card-line bg-card p-2 space-y-1.5"
421
+ >
422
+ <div class="flex items-center gap-2">
423
+ <select
424
+ v-model="r.type"
425
+ :disabled="readonly"
426
+ @change="updateTransition(step.key, t.to, { restrictions: t.restrictions })"
427
+ class="flex-1 h-6 px-1.5 text-[11px] rounded border border-card-line bg-card text-foreground focus:outline-none focus:border-primary disabled:opacity-50"
428
+ >
429
+ <option v-for="rt in RESTRICTION_TYPES" :key="rt.value" :value="rt.value">{{ rt.label }}</option>
430
+ </select>
431
+ <button v-if="!readonly" type="button" @click="removeRestriction(step.key, t.to, ridx)" class="text-red-400 hover:text-red-600 transition-colors shrink-0">
432
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor" class="w-3 h-3"><path d="M5.28 4.22a.75.75 0 0 0-1.06 1.06L6.94 8l-2.72 2.72a.75.75 0 1 0 1.06 1.06L8 9.06l2.72 2.72a.75.75 0 1 0 1.06-1.06L9.06 8l2.72-2.72a.75.75 0 0 0-1.06-1.06L8 6.94 5.28 4.22Z" /></svg>
433
+ </button>
434
+ </div>
435
+ <input
436
+ v-model="r.message"
437
+ :disabled="readonly"
438
+ placeholder="Mensaje de error al usuario…"
439
+ @blur="updateTransition(step.key, t.to, { restrictions: t.restrictions })"
440
+ class="w-full h-6 px-1.5 text-[11px] rounded border border-card-line bg-card text-foreground focus:outline-none focus:border-primary disabled:opacity-50"
441
+ />
442
+ <!-- Campos específicos por tipo -->
443
+ <input v-if="r.type === 'role'" v-model="r.roles" :disabled="readonly" placeholder="admin, manager" @blur="updateTransition(step.key, t.to, { restrictions: t.restrictions })" class="w-full h-6 px-1.5 text-[11px] rounded border border-card-line bg-card text-foreground focus:outline-none focus:border-primary disabled:opacity-50" />
444
+ <input v-if="r.type === 'required_fields'" v-model="r.fields" :disabled="readonly" placeholder="campo1, campo2" @blur="updateTransition(step.key, t.to, { restrictions: t.restrictions })" class="w-full h-6 px-1.5 text-[11px] rounded border border-card-line bg-card text-foreground focus:outline-none focus:border-primary disabled:opacity-50" />
445
+ <input v-if="r.type === 'checklist'" v-model="r.checklist" :disabled="readonly" placeholder="nombre del checklist" @blur="updateTransition(step.key, t.to, { restrictions: t.restrictions })" class="w-full h-6 px-1.5 text-[11px] rounded border border-card-line bg-card text-foreground focus:outline-none focus:border-primary disabled:opacity-50" />
446
+ <input v-if="r.type === 'document'" v-model="r.document" :disabled="readonly" placeholder="nombre del documento" @blur="updateTransition(step.key, t.to, { restrictions: t.restrictions })" class="w-full h-6 px-1.5 text-[11px] rounded border border-card-line bg-card text-foreground focus:outline-none focus:border-primary disabled:opacity-50" />
447
+ <input v-if="r.type === 'approval'" v-model="r.role" :disabled="readonly" placeholder="rol que debe aprobar" @blur="updateTransition(step.key, t.to, { restrictions: t.restrictions })" class="w-full h-6 px-1.5 text-[11px] rounded border border-card-line bg-card text-foreground focus:outline-none focus:border-primary disabled:opacity-50" />
448
+ <div v-if="r.type === 'min_items'" class="flex gap-1.5">
449
+ <input v-model="r.relation" :disabled="readonly" placeholder="relación (ej. attachments)" @blur="updateTransition(step.key, t.to, { restrictions: t.restrictions })" class="flex-1 h-6 px-1.5 text-[11px] rounded border border-card-line bg-card text-foreground focus:outline-none focus:border-primary disabled:opacity-50" />
450
+ <input v-model.number="r.min" :disabled="readonly" type="number" placeholder="min" @blur="updateTransition(step.key, t.to, { restrictions: t.restrictions })" class="w-16 h-6 px-1.5 text-[11px] rounded border border-card-line bg-card text-foreground focus:outline-none focus:border-primary disabled:opacity-50" />
451
+ </div>
452
+ <input v-if="r.type === 'date'" v-model="r.after" :disabled="readonly" type="date" @blur="updateTransition(step.key, t.to, { restrictions: t.restrictions })" class="w-full h-6 px-1.5 text-[11px] rounded border border-card-line bg-card text-foreground focus:outline-none focus:border-primary disabled:opacity-50" />
453
+ </div>
454
+
455
+ <p v-if="!t.restrictions?.length" class="text-[11px] text-muted-foreground italic">Sin restricciones — cualquier usuario puede transicionar</p>
456
+
457
+ <button
458
+ v-if="!readonly"
459
+ type="button"
460
+ @click="addRestrictionTo(step.key, t.to)"
461
+ class="text-xs text-primary hover:underline"
462
+ >
463
+ + Agregar restricción
464
+ </button>
465
+ </div>
466
+ </div>
467
+
468
+ <!-- Form nueva transición -->
469
+ <div v-if="showingAddTransition === step.key" class="rounded-control border border-primary/30 bg-primary/5 p-2.5 space-y-2">
470
+ <div class="flex items-center gap-2">
471
+ <select
472
+ v-model="newTransition.to"
473
+ class="flex-1 h-7 px-2 text-xs rounded-control border border-card-line bg-card text-foreground focus:outline-none focus:border-primary"
474
+ >
475
+ <option value="" disabled>Destino…</option>
476
+ <option v-for="s in availableTargets(step.key)" :key="s.key" :value="s.key">{{ s.label }}</option>
477
+ </select>
478
+ <input
479
+ v-model="newTransition.label"
480
+ placeholder="Label (opcional)"
481
+ class="flex-1 h-7 px-2 text-xs rounded-control border border-card-line bg-card text-foreground focus:outline-none focus:border-primary"
482
+ />
483
+ </div>
484
+ <p v-if="newTransitionError" class="text-xs text-red-500">{{ newTransitionError }}</p>
485
+ <div class="flex justify-end gap-2">
486
+ <button type="button" @click="showingAddTransition = null" class="px-2.5 py-1 text-xs text-muted-foreground hover:bg-muted rounded-control transition-colors">Cancelar</button>
487
+ <button type="button" @click="submitTransition(step.key)" :disabled="!newTransition.to" class="px-2.5 py-1 text-xs bg-primary text-primary-foreground hover:bg-primary-hover rounded-control transition-colors disabled:opacity-50">Agregar</button>
488
+ </div>
489
+ </div>
490
+
491
+ <!-- Botón agregar transición -->
492
+ <button
493
+ v-if="!readonly && showingAddTransition !== step.key && !['finished','cancelled'].includes(step.type)"
494
+ type="button"
495
+ @click="openAddTransition(step.key)"
496
+ class="inline-flex items-center gap-1 text-xs text-primary hover:underline"
497
+ >
498
+ <IconPlus :size="12" /> Agregar transición
499
+ </button>
500
+
501
+ <p v-if="['finished','cancelled'].includes(step.type)" class="text-xs text-muted-foreground italic">
502
+ Los steps terminales no tienen transiciones salientes.
503
+ </p>
504
+ </div>
505
+ </Transition>
506
+ </div>
507
+ </div>
508
+ </div>
509
+
510
+ <!-- ── Footer con estado ──────────────────────────────────────────────── -->
511
+ <div class="flex items-center gap-3 text-xs text-muted-foreground pt-1 border-t border-card-line">
512
+ <span>{{ definition.steps.length }} steps · {{ definition.transitions.length }} transiciones</span>
513
+ <span
514
+ v-if="isValid"
515
+ class="inline-flex items-center gap-1 text-emerald-600 dark:text-emerald-400"
516
+ >
517
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor" class="w-3.5 h-3.5">
518
+ <path fill-rule="evenodd" d="M12.416 3.376a.75.75 0 0 1 .208 1.04l-5 7.5a.75.75 0 0 1-1.154.114l-3-3a.75.75 0 0 1 1.06-1.06l2.353 2.353 4.493-6.74a.75.75 0 0 1 1.04-.207Z" clip-rule="evenodd" />
519
+ </svg>
520
+ Definición válida
521
+ </span>
522
+ <span v-if="isDirty && !readonly" class="text-amber-500">· cambios sin guardar</span>
523
+ </div>
524
+
525
+ </div>
526
+ </template>