@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,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,12 +1,12 @@
1
1
  <script setup lang="ts">
2
2
  import { IconDeviceDesktop, IconArrowRight, IconCopy, IconCheck } from '@tabler/icons-vue'
3
- import type { AppDefinition } from '~/configs/apps'
3
+ import type { ContextDefinition } from '~/configs/apps'
4
4
 
5
5
  const props = defineProps<{
6
- /** App al que se intentó acceder (para el copy contextual). */
7
- blockedApp?: AppDefinition | null
8
- /** App mobile-friendly al que el usuario puede continuar (opcional). */
9
- fallbackApp?: AppDefinition | null
6
+ /** Context al que se intentó acceder (para el copy contextual). */
7
+ blockedApp?: ContextDefinition | null
8
+ /** Context mobile-friendly al que el usuario puede continuar (opcional). */
9
+ fallbackApp?: ContextDefinition | null
10
10
  }>()
11
11
 
12
12
  const route = useRoute()
@@ -1,18 +1,18 @@
1
1
  <script setup lang="ts">
2
2
  import * as TablerIcons from '@tabler/icons-vue'
3
3
  import { IconArrowRight } from '@tabler/icons-vue'
4
- import type { AppDefinition } from '~/configs/apps'
4
+ import type { ContextDefinition } from '~/configs/apps'
5
5
 
6
6
  const props = defineProps<{
7
- /** Apps a mostrar como opciones de login. */
8
- options: AppDefinition[]
7
+ /** Contexts a mostrar como opciones de login. */
8
+ options: ContextDefinition[]
9
9
  /** Título personalizable. */
10
10
  title?: string
11
11
  }>()
12
12
 
13
13
  const emit = defineEmits<{
14
- /** Emitido cuando el usuario elige un app — el padre se encarga de navegar y recordar la elección. */
15
- pick: [app: AppDefinition]
14
+ /** Emitido cuando el usuario elige un context — el padre se encarga de navegar y recordar la elección. */
15
+ pick: [context: ContextDefinition]
16
16
  }>()
17
17
 
18
18
  const heading = computed(() => props.title ?? '¿Cómo quieres ingresar?')
@@ -46,25 +46,25 @@ function resolveIcon(name?: string) {
46
46
  <!-- Opciones -->
47
47
  <div class="space-y-3">
48
48
  <button
49
- v-for="app in options"
50
- :key="app.path"
49
+ v-for="context in options"
50
+ :key="context.path"
51
51
  type="button"
52
52
  class="group w-full flex items-center gap-4 rounded-2xl border border-slate-200 dark:border-slate-800 bg-white dark:bg-slate-900 px-4 py-4 text-left hover:border-violet-300 dark:hover:border-violet-700 hover:bg-violet-50/50 dark:hover:bg-violet-900/10 transition-colors"
53
- @click="emit('pick', app)"
53
+ @click="emit('pick', context)"
54
54
  >
55
55
  <div class="shrink-0 size-11 rounded-xl bg-violet-100 dark:bg-violet-900/30 flex items-center justify-center group-hover:bg-violet-200 dark:group-hover:bg-violet-900/50 transition-colors">
56
56
  <component
57
- v-if="resolveIcon(app.icon)"
58
- :is="resolveIcon(app.icon)"
57
+ v-if="resolveIcon(context.icon)"
58
+ :is="resolveIcon(context.icon)"
59
59
  class="size-5 text-violet-600 dark:text-violet-400"
60
60
  />
61
61
  </div>
62
62
  <div class="flex-1 min-w-0">
63
63
  <div class="text-base font-semibold text-slate-900 dark:text-white">
64
- {{ app.label }}
64
+ {{ context.label }}
65
65
  </div>
66
- <div v-if="app.description" class="text-xs text-slate-500 dark:text-slate-400 mt-0.5 line-clamp-2">
67
- {{ app.description }}
66
+ <div v-if="context.description" class="text-xs text-slate-500 dark:text-slate-400 mt-0.5 line-clamp-2">
67
+ {{ context.description }}
68
68
  </div>
69
69
  </div>
70
70
  <IconArrowRight class="shrink-0 size-5 text-slate-400 group-hover:text-violet-600 dark:group-hover:text-violet-400 transition-colors" />
@@ -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>