@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.
Files changed (65) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/app.config.ts +10 -10
  3. package/components/Attention.vue +269 -0
  4. package/components/Auth/LoginForm.vue +1 -1
  5. package/components/Chart/index.vue +193 -0
  6. package/components/Container.vue +116 -0
  7. package/components/CopyButton.vue +98 -0
  8. package/components/Files/Card.vue +215 -0
  9. package/components/Files/Preview.vue +675 -0
  10. package/components/Files/Upload.vue +408 -0
  11. package/components/Files/UploadModal.vue +83 -0
  12. package/components/Forms/InputPin.vue +162 -0
  13. package/components/InfoToolbar.vue +54 -0
  14. package/components/Kanban/Standard.vue +588 -0
  15. package/components/Loader.vue +358 -0
  16. package/components/MobileBlocker.vue +5 -5
  17. package/components/MobileLoginPicker.vue +13 -13
  18. package/components/Modal/Base.vue +6 -0
  19. package/components/Modal/Confirm.vue +89 -0
  20. package/components/Modal/DeleteConfirm.vue +14 -31
  21. package/components/Modal/Form.vue +76 -0
  22. package/components/Modal/Host.vue +58 -0
  23. package/components/Modal/Prompt.vue +74 -0
  24. package/components/Modal.vue +119 -34
  25. package/components/Organization/Picker.vue +3 -3
  26. package/components/Table/Standard.vue +198 -25
  27. package/components/Table/index.vue +169 -369
  28. package/components/Table/views/ViewCards.vue +74 -0
  29. package/components/Table/views/ViewDatabase.vue +236 -0
  30. package/components/Table/views/ViewTable.vue +299 -0
  31. package/components/Toast/Alert.vue +16 -111
  32. package/components/Toast/Container.vue +4 -29
  33. package/components/Toast/Notification.vue +8 -43
  34. package/components/Toast/Process.vue +8 -86
  35. package/components/Toast/Stack.vue +59 -0
  36. package/components/Toast/Standard.vue +115 -0
  37. package/components/Tree/Standard.vue +49 -4
  38. package/components/Workflow/Board.vue +356 -0
  39. package/components/Workflow/Builder.vue +454 -0
  40. package/components/Workflow/Form.vue +526 -0
  41. package/components/Workflow/Viewer.vue +225 -0
  42. package/components/Workflow/edges/TransitionEdge.vue +84 -0
  43. package/components/Workflow/nodes/StepNode.vue +88 -0
  44. package/composables/useAuth.js +1 -1
  45. package/composables/useClipboard.js +79 -0
  46. package/composables/useConfetti.js +194 -0
  47. package/composables/useContext.js +53 -5
  48. package/composables/useFile.js +189 -0
  49. package/composables/useMobileGuard.ts +21 -21
  50. package/composables/useModal.js +71 -0
  51. package/composables/useOrganization.ts +3 -3
  52. package/composables/useToast.js +38 -65
  53. package/composables/useWorkflow/form.js +162 -0
  54. package/composables/useWorkflow/index.js +18 -0
  55. package/composables/useWorkflow/view.js +220 -0
  56. package/composables/useWorkflowLayout.js +162 -0
  57. package/css/tokens.css +10 -10
  58. package/docs/superpowers/plans/2026-05-27-nuxt-apps-to-contexts-rename.md +630 -0
  59. package/docs/superpowers/specs/2026-05-27-nuxt-apps-to-contexts-rename-design.md +113 -0
  60. package/middleware/{03.apps.global.ts → 03.contexts.global.ts} +25 -25
  61. package/nuxt.config.ts +1 -1
  62. package/package.json +13 -8
  63. package/stores/modal.js +48 -0
  64. package/stores/toast.js +156 -117
  65. package/composables/useApp.ts +0 -46
@@ -0,0 +1,454 @@
1
+ <script setup>
2
+ import { ref, computed, watch, markRaw, nextTick } from 'vue'
3
+ import { VueFlow, Panel } from '@vue-flow/core'
4
+ import { Background } from '@vue-flow/background'
5
+ import { Controls } from '@vue-flow/controls'
6
+ import StepNode from './nodes/StepNode.vue'
7
+ import TransitionEdge from './edges/TransitionEdge.vue'
8
+ import { definitionToFlow, flowToDefinition, autoLayoutWorkflow } from '~/composables/useWorkflowLayout.js'
9
+
10
+ // ── Props & emits ─────────────────────────────────────────────────────────────
11
+ const props = defineProps({
12
+ modelValue: { type: Object, default: () => ({ steps: [], transitions: [] }) },
13
+ readonly: { type: Boolean, default: false },
14
+ })
15
+
16
+ const emit = defineEmits(['update:modelValue', 'node-click', 'edge-click', 'connect'])
17
+
18
+ // ── Vue Flow types ────────────────────────────────────────────────────────────
19
+ const nodeTypes = markRaw({ step: StepNode })
20
+ const edgeTypes = markRaw({ transition: TransitionEdge })
21
+
22
+ // ── Local nodes/edges refs (controlled) ──────────────────────────────────────
23
+ const flowNodes = ref([])
24
+ const flowEdges = ref([])
25
+
26
+ let _skipEmit = false
27
+
28
+ watch(
29
+ () => props.modelValue,
30
+ (def) => {
31
+ _skipEmit = true
32
+ const { nodes: n, edges: e } = definitionToFlow(def)
33
+ flowNodes.value = n
34
+ flowEdges.value = e
35
+ nextTick(() => { _skipEmit = false })
36
+ },
37
+ { immediate: true, deep: true },
38
+ )
39
+
40
+ const emitDefinition = () => {
41
+ if (props.readonly || _skipEmit) return
42
+ emit('update:modelValue', flowToDefinition(flowNodes.value, flowEdges.value))
43
+ }
44
+
45
+ // ── View mode ─────────────────────────────────────────────────────────────────
46
+ const viewMode = ref('flow')
47
+
48
+ // ── Re-layout ─────────────────────────────────────────────────────────────────
49
+ const relayout = () => {
50
+ const steps = flowNodes.value.map(n => n.data)
51
+ const transitions = flowEdges.value.map(e => ({ from: e.source, to: e.target }))
52
+ const positions = autoLayoutWorkflow(steps, transitions)
53
+ flowNodes.value = flowNodes.value.map(n => ({
54
+ ...n,
55
+ position: positions[n.id] ?? n.position,
56
+ }))
57
+ // Forzar re-render del canvas con nueva key
58
+ flowKey.value++
59
+ }
60
+
61
+ const flowKey = ref(0)
62
+
63
+ // ── Vue Flow events ───────────────────────────────────────────────────────────
64
+ const onNodesChange = (changes) => {
65
+ // Vue Flow muta los nodes internamente; sólo emitimos tras posición/cambio
66
+ nextTick(emitDefinition)
67
+ }
68
+
69
+ const onEdgesChange = () => {
70
+ nextTick(emitDefinition)
71
+ }
72
+
73
+ const onConnect = (params) => {
74
+ if (props.readonly) return
75
+ flowEdges.value = [
76
+ ...flowEdges.value,
77
+ {
78
+ id: `${params.source}__${params.target}`,
79
+ source: params.source,
80
+ target: params.target,
81
+ type: 'transition',
82
+ label: '',
83
+ data: { from: params.source, to: params.target, label: '', restrictions: [] },
84
+ markerEnd: { type: 'arrowclosed' },
85
+ },
86
+ ]
87
+ emit('connect', params)
88
+ nextTick(emitDefinition)
89
+ }
90
+
91
+ // ── Config panel ──────────────────────────────────────────────────────────────
92
+ const selectedItem = ref(null)
93
+ const showPanel = ref(false)
94
+
95
+ const onNodeClick = ({ node }) => {
96
+ selectedItem.value = { type: 'step', id: node.id, data: { ...node.data } }
97
+ showPanel.value = true
98
+ emit('node-click', node)
99
+ }
100
+
101
+ const onEdgeClick = ({ edge }) => {
102
+ selectedItem.value = { type: 'transition', id: edge.id, data: { ...edge.data, label: edge.label ?? '' } }
103
+ showPanel.value = true
104
+ emit('edge-click', edge)
105
+ }
106
+
107
+ const closePanel = () => { showPanel.value = false; selectedItem.value = null }
108
+
109
+ const savePanel = () => {
110
+ if (!selectedItem.value || props.readonly) return
111
+ if (selectedItem.value.type === 'step') {
112
+ flowNodes.value = flowNodes.value.map(n =>
113
+ n.id === selectedItem.value.id
114
+ ? { ...n, id: selectedItem.value.data.key, data: { ...selectedItem.value.data } }
115
+ : n
116
+ )
117
+ // Update edges if key changed
118
+ if (selectedItem.value.id !== selectedItem.value.data.key) {
119
+ const oldKey = selectedItem.value.id
120
+ const newKey = selectedItem.value.data.key
121
+ flowEdges.value = flowEdges.value.map(e => ({
122
+ ...e,
123
+ source: e.source === oldKey ? newKey : e.source,
124
+ target: e.target === oldKey ? newKey : e.target,
125
+ id: e.id.replace(oldKey, newKey),
126
+ }))
127
+ }
128
+ } else {
129
+ flowEdges.value = flowEdges.value.map(e =>
130
+ e.id === selectedItem.value.id
131
+ ? { ...e, data: { ...selectedItem.value.data }, label: selectedItem.value.data.label ?? '' }
132
+ : e
133
+ )
134
+ }
135
+ emitDefinition()
136
+ closePanel()
137
+ }
138
+
139
+ const deleteSelected = () => {
140
+ if (!selectedItem.value || props.readonly) return
141
+ if (selectedItem.value.type === 'step') {
142
+ flowNodes.value = flowNodes.value.filter(n => n.id !== selectedItem.value.id)
143
+ flowEdges.value = flowEdges.value.filter(e =>
144
+ e.source !== selectedItem.value.id && e.target !== selectedItem.value.id
145
+ )
146
+ } else {
147
+ flowEdges.value = flowEdges.value.filter(e => e.id !== selectedItem.value.id)
148
+ }
149
+ emitDefinition()
150
+ closePanel()
151
+ }
152
+
153
+ // ── Add step ──────────────────────────────────────────────────────────────────
154
+ const STEP_TYPES = ['start', 'in_progress', 'pause_internal', 'pause_external', 'finished', 'cancelled']
155
+ const STEP_LABELS = {
156
+ start: 'Inicio',
157
+ in_progress: 'En progreso',
158
+ pause_internal: 'Pausa interna',
159
+ pause_external: 'Pausa externa',
160
+ finished: 'Finalizado',
161
+ cancelled: 'Cancelado',
162
+ }
163
+
164
+ const showAddStep = ref(false)
165
+ const newStep = ref({ key: '', label: '', type: 'in_progress', description: '' })
166
+
167
+ const addStep = () => {
168
+ if (!newStep.value.key || !newStep.value.label) return
169
+ const col = flowNodes.value.length
170
+ flowNodes.value = [
171
+ ...flowNodes.value,
172
+ {
173
+ id: newStep.value.key,
174
+ type: 'step',
175
+ position: { x: col * 310, y: 0 },
176
+ data: { ...newStep.value },
177
+ },
178
+ ]
179
+ emitDefinition()
180
+ newStep.value = { key: '', label: '', type: 'in_progress', description: '' }
181
+ showAddStep.value = false
182
+ }
183
+
184
+ // ── Restrictions ──────────────────────────────────────────────────────────────
185
+ const RESTRICTION_TYPES = [
186
+ { value: 'role', label: 'Rol' },
187
+ { value: 'required_fields', label: 'Campos requeridos' },
188
+ { value: 'approval', label: 'Aprobación' },
189
+ { value: 'checklist', label: 'Checklist' },
190
+ { value: 'document', label: 'Documento' },
191
+ { value: 'min_items', label: 'Mínimo ítems' },
192
+ { value: 'date', label: 'Fecha' },
193
+ { value: 'custom', label: 'Personalizado' },
194
+ ]
195
+
196
+ const addRestriction = () => {
197
+ if (!selectedItem.value?.data) return
198
+ selectedItem.value.data.restrictions = [
199
+ ...(selectedItem.value.data.restrictions ?? []),
200
+ { type: 'role', roles: [], message: '' },
201
+ ]
202
+ }
203
+
204
+ const removeRestriction = (idx) => {
205
+ selectedItem.value.data.restrictions.splice(idx, 1)
206
+ }
207
+
208
+ // ── List view ─────────────────────────────────────────────────────────────────
209
+ const stepsWithTransitions = computed(() =>
210
+ (props.modelValue.steps ?? []).map(step => ({
211
+ ...step,
212
+ outgoing: (props.modelValue.transitions ?? []).filter(t => t.from === step.key),
213
+ }))
214
+ )
215
+ </script>
216
+
217
+ <template>
218
+ <div class="flex flex-col h-full min-h-0">
219
+
220
+ <!-- Toolbar -->
221
+ <div class="flex items-center justify-between gap-3 px-3 py-2 border-b border-card-line bg-surface shrink-0">
222
+ <div class="inline-flex items-center rounded-control border border-card-line bg-card overflow-hidden">
223
+ <button
224
+ v-for="mode in [{ id: 'flow', label: 'Flujo' }, { id: 'list', label: 'Lista' }]"
225
+ :key="mode.id"
226
+ type="button"
227
+ @click="viewMode = mode.id"
228
+ :class="[
229
+ 'px-3 py-1.5 text-xs font-medium transition-colors',
230
+ viewMode === mode.id
231
+ ? 'bg-primary text-primary-foreground'
232
+ : 'text-muted-foreground hover:bg-muted-hover',
233
+ ]"
234
+ >
235
+ {{ mode.label }}
236
+ </button>
237
+ </div>
238
+
239
+ <div v-if="viewMode === 'flow'" class="flex items-center gap-2">
240
+ <button
241
+ type="button"
242
+ @click="relayout"
243
+ class="inline-flex items-center gap-1.5 px-2.5 py-1.5 text-xs font-medium text-muted-foreground hover:text-foreground hover:bg-muted rounded-control transition-colors"
244
+ >
245
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="w-3.5 h-3.5">
246
+ <path fill-rule="evenodd" d="M15.312 11.424a5.5 5.5 0 0 1-9.201 2.466l-.312-.311h2.433a.75.75 0 0 0 0-1.5H3.989a.75.75 0 0 0-.75.75v4.242a.75.75 0 0 0 1.5 0v-2.43l.31.31a7 7 0 0 0 11.712-3.138.75.75 0 0 0-1.449-.39Zm1.23-3.723a.75.75 0 0 0 .219-.53V2.929a.75.75 0 0 0-1.5 0V5.36l-.31-.31A7 7 0 0 0 3.239 8.188a.75.75 0 1 0 1.448.389A5.5 5.5 0 0 1 13.89 6.11l.311.31h-2.432a.75.75 0 0 0 0 1.5h4.243a.75.75 0 0 0 .53-.219Z" clip-rule="evenodd" />
247
+ </svg>
248
+ Re-layout
249
+ </button>
250
+ <button
251
+ v-if="!readonly"
252
+ type="button"
253
+ @click="showAddStep = true"
254
+ class="inline-flex items-center gap-1.5 px-2.5 py-1.5 text-xs font-medium bg-primary text-primary-foreground hover:bg-primary-hover rounded-control transition-colors"
255
+ >
256
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="w-3.5 h-3.5">
257
+ <path d="M10.75 4.75a.75.75 0 0 0-1.5 0v4.5h-4.5a.75.75 0 0 0 0 1.5h4.5v4.5a.75.75 0 0 0 1.5 0v-4.5h4.5a.75.75 0 0 0 0-1.5h-4.5v-4.5Z" />
258
+ </svg>
259
+ Agregar step
260
+ </button>
261
+ </div>
262
+ </div>
263
+
264
+ <!-- Content -->
265
+ <div class="flex flex-1 min-h-0 relative overflow-hidden">
266
+
267
+ <!-- ── FLOW VIEW ────────────────────────────────────────────────────── -->
268
+ <div v-if="viewMode === 'flow'" class="flex-1 min-w-0 h-full">
269
+ <VueFlow
270
+ :key="flowKey"
271
+ v-model:nodes="flowNodes"
272
+ v-model:edges="flowEdges"
273
+ :node-types="nodeTypes"
274
+ :edge-types="edgeTypes"
275
+ :nodes-draggable="!readonly"
276
+ :nodes-connectable="!readonly"
277
+ :elements-selectable="true"
278
+ fit-view-on-init
279
+ class="h-full bg-background"
280
+ @nodes-change="onNodesChange"
281
+ @edges-change="onEdgesChange"
282
+ @connect="onConnect"
283
+ @node-click="onNodeClick"
284
+ @edge-click="onEdgeClick"
285
+ >
286
+ <Background pattern-color="var(--color-card-line)" :gap="20" />
287
+ <Controls :show-interactive="!readonly" />
288
+ </VueFlow>
289
+ </div>
290
+
291
+ <!-- ── LIST VIEW ─────────────────────────────────────────────────────── -->
292
+ <div v-if="viewMode === 'list'" class="flex-1 overflow-y-auto p-4 space-y-3 h-full">
293
+ <div
294
+ v-for="step in stepsWithTransitions"
295
+ :key="step.key"
296
+ class="bg-card rounded-card border border-card-line overflow-hidden"
297
+ >
298
+ <div class="flex items-center gap-3 px-4 py-3 border-b border-card-line/50">
299
+ <span :class="[
300
+ 'w-2 h-2 rounded-full shrink-0',
301
+ { 'bg-emerald-500': step.type === 'start', 'bg-blue-500': step.type === 'in_progress',
302
+ 'bg-amber-500': step.type === 'pause_internal', 'bg-orange-500': step.type === 'pause_external',
303
+ 'bg-violet-500': step.type === 'finished', 'bg-red-500': step.type === 'cancelled' },
304
+ ]" />
305
+ <div class="flex-1 min-w-0">
306
+ <p class="text-sm font-medium text-foreground truncate">{{ step.label }}</p>
307
+ <p class="text-xs text-muted-foreground font-mono">{{ step.key }} · {{ STEP_LABELS[step.type] }}</p>
308
+ </div>
309
+ <span class="text-xs text-muted-foreground">
310
+ {{ step.outgoing.length }} transición{{ step.outgoing.length !== 1 ? 'es' : '' }}
311
+ </span>
312
+ </div>
313
+ <div v-if="step.outgoing.length" class="divide-y divide-card-line/50">
314
+ <div v-for="t in step.outgoing" :key="t.to" class="flex items-center gap-3 px-4 py-2.5 text-sm">
315
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="w-4 h-4 text-muted-foreground-2 shrink-0">
316
+ <path fill-rule="evenodd" d="M2 10a.75.75 0 0 1 .75-.75h12.59l-2.1-1.95a.75.75 0 1 1 1.02-1.1l3.5 3.25a.75.75 0 0 1 0 1.1l-3.5 3.25a.75.75 0 1 1-1.02-1.1l2.1-1.95H2.75A.75.75 0 0 1 2 10Z" clip-rule="evenodd" />
317
+ </svg>
318
+ <span class="flex-1 text-foreground">{{ t.label || t.to }}</span>
319
+ <span v-if="t.restrictions?.length" class="text-xs px-1.5 py-0.5 rounded-badge bg-muted text-muted-foreground">
320
+ {{ t.restrictions.length }} restricción{{ t.restrictions.length !== 1 ? 'es' : '' }}
321
+ </span>
322
+ </div>
323
+ </div>
324
+ <div v-else class="px-4 py-2.5 text-xs text-muted-foreground italic">Sin transiciones salientes</div>
325
+ </div>
326
+ <div v-if="!stepsWithTransitions.length" class="flex items-center justify-center py-16 text-muted-foreground text-sm">
327
+ No hay steps definidos
328
+ </div>
329
+ </div>
330
+
331
+ <!-- ── CONFIG PANEL ──────────────────────────────────────────────────── -->
332
+ <Transition
333
+ enter-active-class="transition-all duration-200 ease-out"
334
+ enter-from-class="translate-x-full opacity-0"
335
+ enter-to-class="translate-x-0 opacity-100"
336
+ leave-active-class="transition-all duration-150 ease-in"
337
+ leave-from-class="translate-x-0 opacity-100"
338
+ leave-to-class="translate-x-full opacity-0"
339
+ >
340
+ <div
341
+ v-if="showPanel && selectedItem"
342
+ class="absolute right-0 top-0 bottom-0 w-80 bg-card border-l border-card-line shadow-xl z-10 flex flex-col overflow-hidden"
343
+ >
344
+ <div class="flex items-center justify-between px-4 py-3 border-b border-card-line shrink-0">
345
+ <p class="text-sm font-medium text-foreground">
346
+ {{ selectedItem.type === 'step' ? 'Configurar step' : 'Configurar transición' }}
347
+ </p>
348
+ <button type="button" @click="closePanel" class="p-1 text-muted-foreground hover:text-foreground transition-colors">
349
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="w-4 h-4">
350
+ <path d="M6.28 5.22a.75.75 0 0 0-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 1 0 1.06 1.06L10 11.06l3.72 3.72a.75.75 0 1 0 1.06-1.06L11.06 10l3.72-3.72a.75.75 0 0 0-1.06-1.06L10 8.94 6.28 5.22Z" />
351
+ </svg>
352
+ </button>
353
+ </div>
354
+
355
+ <div class="flex-1 overflow-y-auto p-4 space-y-4">
356
+ <template v-if="selectedItem.type === 'step'">
357
+ <div class="space-y-1">
358
+ <label class="text-xs font-medium text-foreground">Clave (key)</label>
359
+ <input v-model="selectedItem.data.key" :disabled="readonly" class="w-full h-8 px-2.5 text-xs rounded-control border border-card-line bg-card text-foreground focus:outline-none focus:border-primary disabled:opacity-50" />
360
+ </div>
361
+ <div class="space-y-1">
362
+ <label class="text-xs font-medium text-foreground">Label</label>
363
+ <input v-model="selectedItem.data.label" :disabled="readonly" class="w-full h-8 px-2.5 text-xs rounded-control border border-card-line bg-card text-foreground focus:outline-none focus:border-primary disabled:opacity-50" />
364
+ </div>
365
+ <div class="space-y-1">
366
+ <label class="text-xs font-medium text-foreground">Tipo</label>
367
+ <select v-model="selectedItem.data.type" :disabled="readonly" class="w-full h-8 px-2.5 text-xs rounded-control border border-card-line bg-card text-foreground focus:outline-none focus:border-primary disabled:opacity-50">
368
+ <option v-for="t in STEP_TYPES" :key="t" :value="t">{{ STEP_LABELS[t] }} ({{ t }})</option>
369
+ </select>
370
+ </div>
371
+ <div class="space-y-1">
372
+ <label class="text-xs font-medium text-foreground">Descripción</label>
373
+ <textarea v-model="selectedItem.data.description" :disabled="readonly" rows="2" class="w-full px-2.5 py-1.5 text-xs rounded-control border border-card-line bg-card text-foreground focus:outline-none focus:border-primary disabled:opacity-50 resize-none" />
374
+ </div>
375
+ </template>
376
+
377
+ <template v-else-if="selectedItem.type === 'transition'">
378
+ <div class="space-y-1">
379
+ <label class="text-xs font-medium text-foreground">Label</label>
380
+ <input v-model="selectedItem.data.label" :disabled="readonly" class="w-full h-8 px-2.5 text-xs rounded-control border border-card-line bg-card text-foreground focus:outline-none focus:border-primary disabled:opacity-50" />
381
+ </div>
382
+ <div class="space-y-1">
383
+ <label class="text-xs font-medium text-foreground">Descripción</label>
384
+ <textarea v-model="selectedItem.data.description" :disabled="readonly" rows="2" class="w-full px-2.5 py-1.5 text-xs rounded-control border border-card-line bg-card text-foreground focus:outline-none focus:border-primary disabled:opacity-50 resize-none" />
385
+ </div>
386
+
387
+ <div class="space-y-2">
388
+ <div class="flex items-center justify-between">
389
+ <label class="text-xs font-medium text-foreground">Restricciones</label>
390
+ <button v-if="!readonly" type="button" @click="addRestriction" class="text-xs text-primary hover:underline">+ Agregar</button>
391
+ </div>
392
+ <div v-for="(r, idx) in selectedItem.data.restrictions ?? []" :key="idx" class="rounded-control border border-card-line bg-surface p-2.5 space-y-2">
393
+ <div class="flex items-center gap-2">
394
+ <select v-model="r.type" :disabled="readonly" class="flex-1 h-7 px-2 text-[11px] rounded border border-card-line bg-card text-foreground focus:outline-none disabled:opacity-50">
395
+ <option v-for="rt in RESTRICTION_TYPES" :key="rt.value" :value="rt.value">{{ rt.label }}</option>
396
+ </select>
397
+ <button v-if="!readonly" type="button" @click="removeRestriction(idx)" class="text-red-400 hover:text-red-600 transition-colors">
398
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor" class="w-3.5 h-3.5"><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>
399
+ </button>
400
+ </div>
401
+ <input v-model="r.message" :disabled="readonly" placeholder="Mensaje de error…" class="w-full h-7 px-2 text-[11px] rounded border border-card-line bg-card text-foreground focus:outline-none disabled:opacity-50" />
402
+ <input v-if="r.type === 'role'" v-model="r.roles" :disabled="readonly" placeholder="admin, manager" class="w-full h-7 px-2 text-[11px] rounded border border-card-line bg-card text-foreground focus:outline-none disabled:opacity-50" />
403
+ <input v-if="r.type === 'required_fields'" v-model="r.fields" :disabled="readonly" placeholder="campo1, campo2" class="w-full h-7 px-2 text-[11px] rounded border border-card-line bg-card text-foreground focus:outline-none disabled:opacity-50" />
404
+ <input v-if="r.type === 'checklist'" v-model="r.checklist" :disabled="readonly" placeholder="nombre del checklist" class="w-full h-7 px-2 text-[11px] rounded border border-card-line bg-card text-foreground focus:outline-none disabled:opacity-50" />
405
+ <input v-if="r.type === 'document'" v-model="r.document" :disabled="readonly" placeholder="nombre del documento" class="w-full h-7 px-2 text-[11px] rounded border border-card-line bg-card text-foreground focus:outline-none disabled:opacity-50" />
406
+ <input v-if="r.type === 'approval'" v-model="r.role" :disabled="readonly" placeholder="rol que debe aprobar" class="w-full h-7 px-2 text-[11px] rounded border border-card-line bg-card text-foreground focus:outline-none disabled:opacity-50" />
407
+ </div>
408
+ <p v-if="!selectedItem.data.restrictions?.length" class="text-xs text-muted-foreground italic">Sin restricciones</p>
409
+ </div>
410
+ </template>
411
+ </div>
412
+
413
+ <div class="flex items-center gap-2 px-4 py-3 border-t border-card-line shrink-0">
414
+ <button v-if="!readonly" type="button" @click="deleteSelected" class="px-3 py-1.5 text-xs font-medium text-red-500 hover:bg-red-50 dark:hover:bg-red-950/30 rounded-control transition-colors">Eliminar</button>
415
+ <div class="flex-1" />
416
+ <button type="button" @click="closePanel" class="px-3 py-1.5 text-xs font-medium text-muted-foreground hover:bg-muted rounded-control transition-colors">Cancelar</button>
417
+ <button v-if="!readonly" type="button" @click="savePanel" class="px-3 py-1.5 text-xs font-medium bg-primary text-primary-foreground hover:bg-primary-hover rounded-control transition-colors">Guardar</button>
418
+ </div>
419
+ </div>
420
+ </Transition>
421
+ </div>
422
+
423
+ <!-- Modal: Agregar step -->
424
+ <Teleport to="body">
425
+ <Transition enter-active-class="transition-opacity duration-150" enter-from-class="opacity-0" enter-to-class="opacity-100" leave-active-class="transition-opacity duration-100" leave-from-class="opacity-100" leave-to-class="opacity-0">
426
+ <div v-if="showAddStep" class="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/40" @click.self="showAddStep = false">
427
+ <div class="bg-card rounded-modal border border-card-line shadow-xl w-full max-w-sm p-5 space-y-4">
428
+ <p class="text-sm font-semibold text-foreground">Nuevo step</p>
429
+ <div class="space-y-3">
430
+ <div class="space-y-1">
431
+ <label class="text-xs font-medium text-foreground">Clave (key)</label>
432
+ <input v-model="newStep.key" placeholder="ej. revision" class="w-full h-8 px-2.5 text-xs rounded-control border border-card-line bg-card text-foreground focus:outline-none focus:border-primary" />
433
+ </div>
434
+ <div class="space-y-1">
435
+ <label class="text-xs font-medium text-foreground">Label</label>
436
+ <input v-model="newStep.label" placeholder="ej. Revisión" class="w-full h-8 px-2.5 text-xs rounded-control border border-card-line bg-card text-foreground focus:outline-none focus:border-primary" />
437
+ </div>
438
+ <div class="space-y-1">
439
+ <label class="text-xs font-medium text-foreground">Tipo</label>
440
+ <select v-model="newStep.type" class="w-full h-8 px-2.5 text-xs rounded-control border border-card-line bg-card text-foreground focus:outline-none focus:border-primary">
441
+ <option v-for="t in STEP_TYPES" :key="t" :value="t">{{ STEP_LABELS[t] }} ({{ t }})</option>
442
+ </select>
443
+ </div>
444
+ </div>
445
+ <div class="flex justify-end gap-2">
446
+ <button type="button" @click="showAddStep = false" class="px-3 py-1.5 text-xs text-muted-foreground hover:bg-muted rounded-control transition-colors">Cancelar</button>
447
+ <button type="button" @click="addStep" :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">Crear</button>
448
+ </div>
449
+ </div>
450
+ </div>
451
+ </Transition>
452
+ </Teleport>
453
+ </div>
454
+ </template>