@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,358 @@
1
+ <script setup>
2
+ /**
3
+ * <Loader> — Indicador configurable de progreso / countdown / indeterminate.
4
+ *
5
+ * shape:
6
+ * - 'circle' (default) — círculo SVG con stroke-dasharray
7
+ * - 'square' — cuadrado SVG con stroke-dasharray sobre el perímetro
8
+ * - 'bar' — barra horizontal
9
+ * - 'dots' — 3 puntitos pulsantes (solo indeterminate)
10
+ *
11
+ * mode:
12
+ * - 'progress' — controlado por prop `value` (0-100)
13
+ * - 'countdown' — anima de A→B durante `duration` ms y emite @complete
14
+ * - 'indeterminate' — animación continua (rotación / pulso)
15
+ *
16
+ * direction (solo countdown/progress):
17
+ * - 'fill' — de 0 → value (default)
18
+ * - 'empty' — de 100 → 0 (típico para countdowns "tiempo restante")
19
+ *
20
+ * ```vue
21
+ * <!-- countdown circular -->
22
+ * <Loader shape="circle" mode="countdown" :duration="5000" direction="empty"
23
+ * :size="22" :stroke="2" color="emerald" />
24
+ *
25
+ * <!-- progress controlado -->
26
+ * <Loader shape="circle" :value="65" :size="64" :stroke="6" color="primary" show-value />
27
+ *
28
+ * <!-- spinner indeterminate -->
29
+ * <Loader shape="circle" mode="indeterminate" :size="32" :stroke="3" color="primary" />
30
+ *
31
+ * <!-- barra -->
32
+ * <Loader shape="bar" :value="40" :stroke="6" color="emerald" />
33
+ *
34
+ * <!-- cuadrado de countdown -->
35
+ * <Loader shape="square" mode="countdown" :duration="3000" :size="48" :stroke="4" />
36
+ *
37
+ * <!-- dots indeterminate -->
38
+ * <Loader shape="dots" color="primary" />
39
+ * ```
40
+ */
41
+
42
+ const props = defineProps({
43
+ shape: { type: String, default: 'circle', validator: v => ['circle','square','bar','dots'].includes(v) },
44
+ mode: { type: String, default: 'progress', validator: v => ['progress','countdown','indeterminate'].includes(v) },
45
+ /** 0-100. Solo aplica en mode='progress'. */
46
+ value: { type: Number, default: 0 },
47
+ /** ms. Solo aplica en mode='countdown' (y para 'indeterminate' bar/dots define velocidad). */
48
+ duration: { type: Number, default: 3000 },
49
+ /** 'fill' (0→value) o 'empty' (100→0). */
50
+ direction: { type: String, default: 'fill', validator: v => ['fill','empty'].includes(v) },
51
+ /** Tamaño en px o preset xs|sm|md|lg|xl. Para 'bar' sólo afecta la altura via `stroke`. */
52
+ size: { type: [Number, String], default: 32 },
53
+ /** Grosor del trazo (circle/square) o alto de la barra (bar). */
54
+ stroke: { type: Number, default: 3 },
55
+ /** Color del trazo. Acepta semánticos (primary/success/danger/warning/info/muted/foreground/emerald/red/amber/blue) o cualquier color CSS. */
56
+ color: { type: String, default: 'primary' },
57
+ /** Color del track (background). 'auto' usa --color-muted. */
58
+ track: { type: String, default: 'auto' },
59
+ /** Linecaps/corners redondeados. */
60
+ rounded: { type: Boolean, default: true },
61
+ /** Muestra el % en el centro (circle/square). */
62
+ showValue: { type: Boolean, default: false },
63
+ /** Texto custom en el centro (override de showValue). */
64
+ label: { type: String, default: '' },
65
+ /** Auto-arrancar countdown al montar. */
66
+ autoStart: { type: Boolean, default: true },
67
+ })
68
+
69
+ const emit = defineEmits(['complete', 'tick'])
70
+
71
+ // ── Color resolution ────────────────────────────────────────────────────────
72
+ const COLOR_MAP = {
73
+ primary: 'var(--color-primary)',
74
+ foreground: 'var(--color-foreground)',
75
+ muted: 'var(--color-muted-foreground)',
76
+ success: '#10b981',
77
+ danger: '#ef4444',
78
+ warning: '#f59e0b',
79
+ info: '#3b82f6',
80
+ emerald: '#10b981',
81
+ red: '#ef4444',
82
+ amber: '#f59e0b',
83
+ blue: '#3b82f6',
84
+ violet: '#8b5cf6',
85
+ pink: '#ec4899',
86
+ }
87
+
88
+ const resolvedColor = computed(() => COLOR_MAP[props.color] ?? props.color)
89
+ const resolvedTrack = computed(() => {
90
+ if (props.track !== 'auto') return COLOR_MAP[props.track] ?? props.track
91
+ return 'color-mix(in srgb, currentColor 15%, transparent)'
92
+ })
93
+
94
+ // ── Size resolution ─────────────────────────────────────────────────────────
95
+ const resolvedSize = computed(() => {
96
+ if (typeof props.size === 'number') return props.size
97
+ return { xs: 14, sm: 20, md: 32, lg: 48, xl: 64 }[props.size] ?? 32
98
+ })
99
+
100
+ // ── State (currentValue 0-100) ──────────────────────────────────────────────
101
+ const initialValue = () => {
102
+ if (props.mode === 'countdown') return props.direction === 'empty' ? 100 : 0
103
+ if (props.mode === 'progress') return props.value
104
+ return 0
105
+ }
106
+ const currentValue = ref(initialValue())
107
+
108
+ let raf = null
109
+ let startedAt = 0
110
+
111
+ const start = () => {
112
+ stop()
113
+ if (props.mode !== 'countdown') return
114
+ currentValue.value = props.direction === 'empty' ? 100 : 0
115
+ startedAt = performance.now()
116
+ const tick = (now) => {
117
+ const elapsed = now - startedAt
118
+ const t = Math.min(1, elapsed / props.duration)
119
+ currentValue.value = props.direction === 'empty' ? 100 * (1 - t) : 100 * t
120
+ emit('tick', currentValue.value)
121
+ if (t < 1) {
122
+ raf = requestAnimationFrame(tick)
123
+ } else {
124
+ raf = null
125
+ emit('complete')
126
+ }
127
+ }
128
+ raf = requestAnimationFrame(tick)
129
+ }
130
+
131
+ /** Loop infinito 0→100 (usado por square indeterminate — CSS keyframes con
132
+ * var() en stroke-dashoffset no interpola sin @property registrado). */
133
+ const startIndeterminateLoop = () => {
134
+ stop()
135
+ const loopDur = props.duration > 0 ? props.duration : 1200
136
+ startedAt = performance.now()
137
+ const tick = (now) => {
138
+ const elapsed = (now - startedAt) % loopDur
139
+ currentValue.value = (elapsed / loopDur) * 100
140
+ raf = requestAnimationFrame(tick)
141
+ }
142
+ raf = requestAnimationFrame(tick)
143
+ }
144
+
145
+ const stop = () => {
146
+ if (raf) { cancelAnimationFrame(raf); raf = null }
147
+ }
148
+
149
+ watch(() => props.value, v => {
150
+ if (props.mode === 'progress') currentValue.value = v
151
+ })
152
+
153
+ const needsIndeterminateLoop = computed(
154
+ () => props.mode === 'indeterminate' && props.shape === 'square'
155
+ )
156
+
157
+ watch(() => [props.mode, props.duration, props.direction, props.shape], () => {
158
+ if (props.mode === 'countdown' && props.autoStart) {
159
+ start()
160
+ } else if (props.mode === 'progress') {
161
+ stop()
162
+ currentValue.value = props.value
163
+ } else if (needsIndeterminateLoop.value) {
164
+ startIndeterminateLoop()
165
+ } else {
166
+ stop()
167
+ }
168
+ })
169
+
170
+ onMounted(() => {
171
+ if (props.mode === 'countdown' && props.autoStart) start()
172
+ else if (needsIndeterminateLoop.value) startIndeterminateLoop()
173
+ })
174
+ onBeforeUnmount(stop)
175
+
176
+ defineExpose({ start, stop, restart: start })
177
+
178
+ // ── Geometría SVG ───────────────────────────────────────────────────────────
179
+ // viewBox = 100x100 para todas las shapes — escala con width/height.
180
+ const radius = computed(() => 50 - props.stroke / 2)
181
+ const circumference = computed(() => 2 * Math.PI * radius.value)
182
+ const dashOffset = computed(() => circumference.value * (1 - currentValue.value / 100))
183
+
184
+ // Rect inscripto en viewBox con el stroke considerado
185
+ const squareGeom = computed(() => ({
186
+ x: props.stroke / 2,
187
+ y: props.stroke / 2,
188
+ size: 100 - props.stroke,
189
+ rx: props.rounded ? Math.max(4, props.stroke * 1.5) : 0,
190
+ }))
191
+ // Aproximación al perímetro (rect con esquinas redondeadas)
192
+ const squarePerim = computed(() => {
193
+ const s = squareGeom.value.size
194
+ const rx = squareGeom.value.rx
195
+ return 4 * (s - 2 * rx) + 2 * Math.PI * rx
196
+ })
197
+ const squareOffset = computed(() => squarePerim.value * (1 - currentValue.value / 100))
198
+
199
+ const isIndeterminate = computed(() => props.mode === 'indeterminate')
200
+
201
+ // Transición CSS para que el dashoffset interpole suave en mode='progress'
202
+ const transitionStyle = computed(() => {
203
+ if (props.mode === 'progress') return 'stroke-dashoffset 0.25s cubic-bezier(.2,.8,.2,1)'
204
+ return 'none'
205
+ })
206
+ </script>
207
+
208
+ <template>
209
+ <!-- ── CIRCLE ────────────────────────────────────────────────────────── -->
210
+ <div
211
+ v-if="shape === 'circle'"
212
+ class="relative inline-flex items-center justify-center shrink-0"
213
+ :style="{ width: resolvedSize + 'px', height: resolvedSize + 'px', color: resolvedColor }"
214
+ :aria-label="label || `loader ${Math.round(currentValue)}%`"
215
+ role="progressbar"
216
+ >
217
+ <svg
218
+ :width="resolvedSize"
219
+ :height="resolvedSize"
220
+ viewBox="0 0 100 100"
221
+ :class="isIndeterminate ? 'loader-spin' : ''"
222
+ :style="isIndeterminate ? { animationDuration: duration + 'ms' } : {}"
223
+ >
224
+ <circle cx="50" cy="50" :r="radius" fill="none" :stroke="resolvedTrack" :stroke-width="stroke" />
225
+ <circle
226
+ cx="50" cy="50" :r="radius" fill="none"
227
+ :stroke="resolvedColor"
228
+ :stroke-width="stroke"
229
+ :stroke-dasharray="isIndeterminate ? `${circumference * 0.25} ${circumference}` : circumference"
230
+ :stroke-dashoffset="isIndeterminate ? 0 : dashOffset"
231
+ :stroke-linecap="rounded ? 'round' : 'butt'"
232
+ transform="rotate(-90 50 50)"
233
+ :style="{ transition: transitionStyle }"
234
+ />
235
+ </svg>
236
+ <span v-if="showValue || label || $slots.default"
237
+ class="absolute text-[10px] font-semibold text-foreground tabular-nums leading-none">
238
+ <slot>{{ label || `${Math.round(currentValue)}%` }}</slot>
239
+ </span>
240
+ </div>
241
+
242
+ <!-- ── SQUARE (no rota — el stroke recorre el perímetro) ────────────── -->
243
+ <div
244
+ v-else-if="shape === 'square'"
245
+ class="relative inline-flex items-center justify-center shrink-0"
246
+ :style="{ width: resolvedSize + 'px', height: resolvedSize + 'px', color: resolvedColor }"
247
+ role="progressbar"
248
+ >
249
+ <svg :width="resolvedSize" :height="resolvedSize" viewBox="0 0 100 100">
250
+ <rect
251
+ :x="squareGeom.x" :y="squareGeom.y"
252
+ :width="squareGeom.size" :height="squareGeom.size"
253
+ :rx="squareGeom.rx"
254
+ fill="none" :stroke="resolvedTrack" :stroke-width="stroke"
255
+ />
256
+ <rect
257
+ :x="squareGeom.x" :y="squareGeom.y"
258
+ :width="squareGeom.size" :height="squareGeom.size"
259
+ :rx="squareGeom.rx"
260
+ fill="none"
261
+ :stroke="resolvedColor"
262
+ :stroke-width="stroke"
263
+ :stroke-dasharray="isIndeterminate ? `${squarePerim * 0.25} ${squarePerim * 0.75}` : squarePerim"
264
+ :stroke-dashoffset="squareOffset"
265
+ :stroke-linecap="rounded ? 'round' : 'butt'"
266
+ :style="{ transition: isIndeterminate ? 'none' : transitionStyle }"
267
+ />
268
+ </svg>
269
+ <span v-if="showValue || label || $slots.default"
270
+ class="absolute text-[10px] font-semibold text-foreground tabular-nums leading-none">
271
+ <slot>{{ label || `${Math.round(currentValue)}%` }}</slot>
272
+ </span>
273
+ </div>
274
+
275
+ <!-- ── BAR ───────────────────────────────────────────────────────────── -->
276
+ <div
277
+ v-else-if="shape === 'bar'"
278
+ class="w-full relative overflow-hidden"
279
+ :style="{
280
+ height: stroke + 'px',
281
+ backgroundColor: resolvedTrack,
282
+ borderRadius: rounded ? '999px' : '0',
283
+ color: resolvedColor,
284
+ }"
285
+ role="progressbar"
286
+ >
287
+ <div
288
+ v-if="!isIndeterminate"
289
+ class="h-full"
290
+ :style="{
291
+ width: currentValue + '%',
292
+ backgroundColor: resolvedColor,
293
+ borderRadius: rounded ? '999px' : '0',
294
+ transition: props.mode === 'progress' ? 'width 0.25s cubic-bezier(.2,.8,.2,1)' : 'width 100ms linear',
295
+ }"
296
+ />
297
+ <div
298
+ v-else
299
+ class="absolute inset-y-0 w-1/3 loader-bar-indeterminate"
300
+ :style="{
301
+ backgroundColor: resolvedColor,
302
+ borderRadius: rounded ? '999px' : '0',
303
+ animationDuration: duration + 'ms',
304
+ }"
305
+ />
306
+ </div>
307
+
308
+ <!-- ── DOTS (sólo indeterminate) ────────────────────────────────────── -->
309
+ <div
310
+ v-else-if="shape === 'dots'"
311
+ class="inline-flex items-center gap-1"
312
+ :style="{ color: resolvedColor }"
313
+ role="progressbar"
314
+ >
315
+ <span
316
+ v-for="i in 3" :key="i"
317
+ class="loader-dot rounded-full"
318
+ :style="{
319
+ width: Math.max(4, stroke * 1.5) + 'px',
320
+ height: Math.max(4, stroke * 1.5) + 'px',
321
+ backgroundColor: resolvedColor,
322
+ animationDelay: ((i - 1) * 0.15) + 's',
323
+ animationDuration: duration + 'ms',
324
+ }"
325
+ />
326
+ </div>
327
+ </template>
328
+
329
+ <style scoped>
330
+ .loader-spin {
331
+ animation: loader-spin 1.2s linear infinite;
332
+ transform-origin: center;
333
+ }
334
+ @keyframes loader-spin {
335
+ to { transform: rotate(360deg); }
336
+ }
337
+
338
+ /* Square indeterminate: el `stroke-dashoffset` se anima vía rAF en JS porque
339
+ CSS keyframes con `calc(var(--x) * -1)` no interpola en stroke-dashoffset
340
+ sin registrar la var con @property. Driveado por currentValue (loop 0→100). */
341
+
342
+ .loader-bar-indeterminate {
343
+ animation: loader-bar 1.4s ease-in-out infinite;
344
+ }
345
+ @keyframes loader-bar {
346
+ 0% { transform: translateX(-100%); }
347
+ 50% { transform: translateX(150%); }
348
+ 100% { transform: translateX(150%); }
349
+ }
350
+
351
+ .loader-dot {
352
+ animation: loader-dot 1.2s ease-in-out infinite both;
353
+ }
354
+ @keyframes loader-dot {
355
+ 0%, 80%, 100% { opacity: 0.3; transform: scale(0.8); }
356
+ 40% { opacity: 1; transform: scale(1); }
357
+ }
358
+ </style>
@@ -1,8 +1,13 @@
1
1
  <script setup>
2
+ /**
3
+ * <ModalBase> — Wrapper deprecated retenido por backward-compat.
4
+ * Para uso nuevo: usar directamente <Modal> o las variantes <ModalConfirm>, <ModalForm>.
5
+ */
2
6
  const props = defineProps({
3
7
  modelValue: { type: Boolean, default: false },
4
8
  title: { type: String, default: '' },
5
9
  size: { type: String, default: 'md' },
10
+ variant: { type: String, default: 'center' },
6
11
  loading: { type: Boolean, default: false },
7
12
  closable: { type: Boolean, default: true },
8
13
  })
@@ -15,6 +20,7 @@ const emit = defineEmits(['update:modelValue', 'close'])
15
20
  :model-value="modelValue"
16
21
  :title="title"
17
22
  :size="size"
23
+ :variant="variant"
18
24
  :closable="closable && !loading"
19
25
  :backdrop-dismiss="closable && !loading"
20
26
  :show-footer="!!$slots.footer"
@@ -0,0 +1,89 @@
1
+ <script setup>
2
+ import {
3
+ IconAlertTriangle, IconAlertCircle, IconInfoCircle, IconCircleCheck, IconHelp,
4
+ } from '@tabler/icons-vue'
5
+
6
+ /**
7
+ * <ModalConfirm> — Diálogo de confirmación con severities.
8
+ *
9
+ * Severities: 'danger' | 'warning' | 'info' | 'success' | 'question' (default)
10
+ * Emite 'confirm' (clic en CTA principal) y 'cancel' (clic cancelar / cerrar).
11
+ *
12
+ * Reemplaza <ModalDeleteConfirm> — mantenemos ese por backward-compat como
13
+ * un wrapper que pasa severity="danger".
14
+ */
15
+ const props = defineProps({
16
+ modelValue: { type: Boolean, default: false },
17
+ severity: { type: String, default: 'question', validator: v => ['danger','warning','info','success','question'].includes(v) },
18
+ title: { type: String, default: '' },
19
+ message: { type: String, default: '' },
20
+ confirmText: { type: String, default: 'Confirmar' },
21
+ cancelText: { type: String, default: 'Cancelar' },
22
+ loading: { type: Boolean, default: false },
23
+ /** Oculta el botón cancelar para alerts simples. */
24
+ hideCancel: { type: Boolean, default: false },
25
+ })
26
+
27
+ const emit = defineEmits(['update:modelValue', 'confirm', 'cancel'])
28
+
29
+ const meta = computed(() => ({
30
+ danger: { icon: IconAlertTriangle, btn: 'danger', bg: 'bg-red-500/10 text-red-600 dark:text-red-400', title: props.title || 'Confirmar acción' },
31
+ warning: { icon: IconAlertCircle, btn: 'warning', bg: 'bg-amber-500/10 text-amber-600 dark:text-amber-400', title: props.title || 'Atención' },
32
+ info: { icon: IconInfoCircle, btn: 'primary', bg: 'bg-blue-500/10 text-blue-600 dark:text-blue-400', title: props.title || 'Información' },
33
+ success: { icon: IconCircleCheck, btn: 'primary', bg: 'bg-emerald-500/10 text-emerald-600 dark:text-emerald-400', title: props.title || 'Confirmado' },
34
+ question: { icon: IconHelp, btn: 'primary', bg: 'bg-muted text-muted-foreground', title: props.title || '¿Continuar?' },
35
+ }[props.severity]))
36
+
37
+ const cancel = () => {
38
+ if (props.loading) return
39
+ emit('update:modelValue', false)
40
+ emit('cancel')
41
+ }
42
+
43
+ const confirm = () => {
44
+ if (props.loading) return
45
+ emit('confirm')
46
+ }
47
+ </script>
48
+
49
+ <template>
50
+ <Modal
51
+ :model-value="modelValue"
52
+ size="sm"
53
+ :show-header="false"
54
+ :closable="!loading"
55
+ :backdrop-dismiss="!loading"
56
+ padding="lg"
57
+ @update:model-value="emit('update:modelValue', $event)"
58
+ >
59
+ <div class="flex items-start gap-4">
60
+ <div :class="['size-10 rounded-control flex items-center justify-center shrink-0', meta.bg]">
61
+ <component :is="meta.icon" class="size-5" :stroke="1.75" />
62
+ </div>
63
+ <div class="flex-1 min-w-0">
64
+ <h3 class="text-sm font-semibold text-foreground">{{ meta.title }}</h3>
65
+ <p v-if="message || $slots.default" class="text-sm text-muted-foreground mt-1">
66
+ <slot>{{ message }}</slot>
67
+ </p>
68
+ </div>
69
+ </div>
70
+
71
+ <div class="flex justify-end gap-2 mt-5">
72
+ <AppButton
73
+ v-if="!hideCancel"
74
+ :text="cancelText"
75
+ severity="secondary"
76
+ size="sm"
77
+ :disabled="loading"
78
+ @click="cancel"
79
+ />
80
+ <AppButton
81
+ :text="confirmText"
82
+ :severity="meta.btn"
83
+ size="sm"
84
+ :loading="loading"
85
+ @click="confirm"
86
+ />
87
+ </div>
88
+ </Modal>
89
+ </template>
@@ -1,4 +1,8 @@
1
1
  <script setup>
2
+ /**
3
+ * <ModalDeleteConfirm> — Wrapper deprecated sobre <ModalConfirm> con severity="danger".
4
+ * Para uso nuevo: usar <ModalConfirm severity="danger"> directamente.
5
+ */
2
6
  const props = defineProps({
3
7
  modelValue: { type: Boolean, default: false },
4
8
  title: { type: String, default: 'Confirmar eliminación' },
@@ -9,40 +13,19 @@ const props = defineProps({
9
13
  })
10
14
 
11
15
  const emit = defineEmits(['update:modelValue', 'confirm', 'cancel'])
12
-
13
- const close = () => {
14
- if (props.loading) return
15
- emit('update:modelValue', false)
16
- emit('cancel')
17
- }
18
16
  </script>
19
17
 
20
18
  <template>
21
- <Modal
19
+ <ModalConfirm
22
20
  :model-value="modelValue"
21
+ severity="danger"
23
22
  :title="title"
24
- size="sm"
25
- :closable="!loading"
26
- :backdrop-dismiss="!loading"
27
- @update:model-value="$emit('update:modelValue', $event)"
28
- >
29
- <p class="text-sm text-muted-foreground">{{ message }}</p>
30
-
31
- <div class="flex justify-end gap-2 mt-5">
32
- <AppButton
33
- :text="cancelText"
34
- severity="secondary"
35
- size="sm"
36
- :disabled="loading"
37
- @click="close"
38
- />
39
- <AppButton
40
- :text="confirmText"
41
- severity="danger"
42
- size="sm"
43
- :loading="loading"
44
- @click="$emit('confirm')"
45
- />
46
- </div>
47
- </Modal>
23
+ :message="message"
24
+ :confirm-text="confirmText"
25
+ :cancel-text="cancelText"
26
+ :loading="loading"
27
+ @update:model-value="emit('update:modelValue', $event)"
28
+ @confirm="emit('confirm')"
29
+ @cancel="emit('cancel')"
30
+ />
48
31
  </template>
@@ -0,0 +1,76 @@
1
+ <script setup>
2
+ /**
3
+ * <ModalForm> — Modal especializado para formularios largos.
4
+ *
5
+ * - Body scroll con header + footer sticky.
6
+ * - Footer con submit (severity configurable) y cancelar.
7
+ * - Soporta variant: 'center' | 'drawer-right' | 'drawer-left' | 'fullscreen'.
8
+ * - Emite 'submit' al click del CTA principal y 'cancel' al cerrar/cancelar.
9
+ *
10
+ * El consumidor envuelve sus campos en el slot default — el componente NO
11
+ * monta un <form> nativo para no interferir con la lógica del consumidor.
12
+ * Si querés el comportamiento submit-on-enter, envolvé el contenido en <form>.
13
+ */
14
+ const props = defineProps({
15
+ modelValue: { type: Boolean, default: false },
16
+ title: { type: String, default: '' },
17
+ size: { type: String, default: 'lg' },
18
+ variant: { type: String, default: 'center' },
19
+ submitText: { type: String, default: 'Guardar' },
20
+ cancelText: { type: String, default: 'Cancelar' },
21
+ submitSeverity: { type: String, default: 'primary' },
22
+ loading: { type: Boolean, default: false },
23
+ /** Deshabilita el submit (validación). */
24
+ disabled: { type: Boolean, default: false },
25
+ closable: { type: Boolean, default: true },
26
+ })
27
+
28
+ const emit = defineEmits(['update:modelValue', 'submit', 'cancel'])
29
+
30
+ const cancel = () => {
31
+ if (props.loading) return
32
+ emit('update:modelValue', false)
33
+ emit('cancel')
34
+ }
35
+
36
+ const submit = () => {
37
+ if (props.loading || props.disabled) return
38
+ emit('submit')
39
+ }
40
+ </script>
41
+
42
+ <template>
43
+ <Modal
44
+ :model-value="modelValue"
45
+ :title="title"
46
+ :size="size"
47
+ :variant="variant"
48
+ :closable="closable && !loading"
49
+ :backdrop-dismiss="closable && !loading"
50
+ :show-footer="true"
51
+ padding="md"
52
+ @update:model-value="emit('update:modelValue', $event)"
53
+ @close="emit('cancel')"
54
+ >
55
+ <slot />
56
+ <template #footer>
57
+ <slot name="footer">
58
+ <AppButton
59
+ :text="cancelText"
60
+ severity="secondary"
61
+ size="sm"
62
+ :disabled="loading"
63
+ @click="cancel"
64
+ />
65
+ <AppButton
66
+ :text="submitText"
67
+ :severity="submitSeverity"
68
+ size="sm"
69
+ :loading="loading"
70
+ :disabled="disabled"
71
+ @click="submit"
72
+ />
73
+ </slot>
74
+ </template>
75
+ </Modal>
76
+ </template>
@@ -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>